這幾天研究了一下Photoshop的色相/飽和度命令,也就是所謂的HSB顏色模式,沒完全搞明白,網上搜尋也沒一點結果,看了一些介紹HSB演算法的文章,其實講的就是HSV或者HSL的演算法。
關於PS色相/飽和度中的色相,就不用研究了,原理和HSV或者HSL的H都是一樣的。
而飽和度在-100,0,+100這三點上的效果與HSL完全一樣,其它範圍就有區別了,特別是在0 -- +100範圍,調整時比HSL的H調整要平坦,所以有效調整幅度較大,有些圖片調整到+50%以上還不覺很大失真(這裡的“失真”是針對顏色中難看的斑點來說的,並不是說整個圖片不覺失真),而HSL的H的正向調整10%以上就很難看了;與HSV則沒一點是相同的,可見PS的色相/飽和度演算法應該是在HSL基礎上改進的。
最令人困惑的是PS的明度調整,好像是“獨立”於色相飽和度的。我們知道,要在程式中利用HSV或HSL模式調整V或者L,往往要先將RGB轉換為HSV或HSL,或者至少要在其中將V或者L部分分離出來,修改後再轉換為RGB模式(可參見我的文章《GDI+ 在Delphi程式的應用 -- 線性調整映像亮度 》分離HSL的L部分調整亮度),而PS的明度調整則不一樣,完全不用轉換RGB到所謂的HSB進行調整,直接寫個函數就可以了,請看下面的Delphi過程及測試代碼(分別用GDI+的TGpBitmap和Delphi的TBitmap測試),用於模仿PS明度調整(嚴格的說不叫模仿,而是實實在在的PS明度調整過程):
說明:為了統一《GDI+ 在Delphi程式的應用》系列文章所用資料類型和影像處理格式,本文代碼已作了修訂,代碼中所用Gdiplus單元及BUG更正見文章《GDI+ for VCL基礎 -- GDI+ 與 VCL》。(2008.8.18記)
資料類型:
- type
- // 與GDI+ TBitmapData結構相容的映像資料結構
- TImageData = packed record
- Width: LongWord; // 映像寬度
- Height: LongWord; // 映像高度
- Stride: LongWord; // 映像掃描線位元組長度
- PixelFormat: LongWord; // 未使用
- Scan0: Pointer; // 映像資料地址
- Reserved: LongWord; // 保留
- end;
- PImageData = ^TImageData;
- // 擷取TBitmap映像的TImageData資料結構,便於處理TBitmap映像
- function GetImageData(Bmp: TBitmap): TImageData;
- begin
- Bmp.PixelFormat := pf32bit;
- Result.Width := Bmp.Width;
- Result.Height := Bmp.Height;
- Result.Scan0 := Bmp.ScanLine[Bmp.Height - 1];
- Result.Stride := Result.Width shl 2;
- // Result.Stride := (((32 * Bmp.Width) + 31) and $ffffffe0) shr 3;
- end;
過程代碼:
- // 調整圖象明度
- procedure PSBrightness(Data: TImageData; Value: Integer);
- asm
- push ebp
- push esi
- push edi
- push ebx
- mov edi, [eax + 16] // edi = Data.Scan0
- mov ebp, [eax + 4] // edp = Data.Height * Data.Width
- imul ebp, [eax]
- mov esi, edx // esi = Value
- mov ebx, 255 // ebx = 255
- cld
- @PixelLoop: // for (i = ebp; i > 0; i --)
- mov ecx, 3 // {
- @vLoop: // for (j = 3; j > 0; j --)
- movzx eax, [edi] // {
- push eax
- test esi, esi
- js @@1
- neg eax // if (Value > 0)
- add eax, ebx // rgb = rgb + (255 - rgb) * Value / 255
- @@1:
- imul eax, esi // else
- cdq // rgb = rgb + rgb * Value / 255
- idiv ebx
- pop edx
- add eax, edx
- jns @@2 // rgb = max(0, min(255, rgb))
- xor eax, eax
- jmp @@3
- @@2:
- cmp eax, ebx
- jle @@3
- mov eax, ebx
- @@3:
- stosb // *edi ++ = rgb
- loop @vLoop // }
- inc edi // edi ++
- dec ebp
- jnz @PixelLoop // }
- pop ebx
- pop edi
- pop esi
- pop ebp
- end;
- // 調整GDI+圖象明度
- procedure GdipPSBrightness(Bmp: TGpBitmap; Value: Integer);
- var
- Data: TBitmapData;
- begin
- if Value = 0 then Exit;
- Data := Bmp.LockBits(GpRect(0, 0, Bmp.Width, Bmp.Height), [imRead, imWrite], pf32bppARGB);
- try
- PSBrightness(TImageData(Data), Value);
- finally
- Bmp.UnlockBits(Data);
- end;
- end;
- // 調整TBitmap圖象明度
- procedure BitmapPSBrightness(Bmp: TBitmap; Value: Integer);
- begin
- if Value <> 0 then
- HSLBrightness(GetImageData(Bmp), Value);
- end;
可以看出,上面的PSBrightness過程沒有依賴任何顏色模式轉換,而是採用了下面這個虛擬碼公式:
if (value >= 0)
RGB = RGB + (255 - RGB) * value / 255;
else
RGB = RGB + RGB * value / 255;
其中RGB分別表示顏色的R、G、B,value為明度值。那麼這個公式的含義是什麼的,其實就是HSL轉換為RGB的L部分的公式變形,我在《GDI+ 在Delphi程式的應用 -- 線性調整映像亮度 》中採用的公式和它形式是一樣的,只是計算基數不同 :
L = L - 128;
if (L >= 0)
RGB = RGB + (255 - RGB) * L / 128;
else
RGB = RGB + RGB * L / 128;
前者使用的是value的全範圍255或者100%(公式後的/255改為/100),而後者採用的是L的1/2,也就是128或者50%(就這點區別,效果可就大相徑庭了),而且要利用L調整亮度,必須從HSL空間中擷取L再加上調整值,而PS明度調整則不需要從HSB的得到原來的B,這就意味著B在HSB中始終為“0”!雖然PS的明度調整是和色相/飽和度調整放在一起的,但完全不依賴於色相/飽和度,這就是我感覺其好像是“獨立”的原因,也是研究PS飽和度演算法不果的重要原因(一個B=0的HSB模式,光靠HS部分怎樣正確轉換為R、G、B?要知道HSV的V和HSL中的L在正確轉換為RGB模式中至關重要!)。
假如我的感覺是正確的,那麼PS“獨立”的明度調整又是什麼原理呢?我們知道,一般的非線性RGB亮度調整隻是在原有R、G、B值基礎上增加和減少一定量來實現的,而PS的明度調整原理還得從前面那個公式上去找。我們將正向明度調整公式:RGB = RGB + (255 - RGB) * value / 255轉換為RGB = (RGB * (255 - value) + 255 * value) / 255,如果value用1表示最大值255,則為RGB = RGB * (1 - value) + 255 * value,可以看出什麼呢?凡是知道映像合成的人都知道這個公式,其實PS的明度調整是採用Alpha合成方式,這裡的value就是Alpha,公式前面部分RGB * (1 - value)的是映像部分,後面的255 * value部分則是一個白色遮照層,明度越大,遮照層的Alpha越大,映像就越談,反之亦然。而明度的負調整則是以一個黑色遮照層來完成的。負100%就全黑了。只有遮照層Alpha=0,也就是明度值為0時,才是完完全全的圖片顯示。要驗證上面的說法很簡單,一是運行我的測試代碼,而是在PS中,用一個全白或全黑圖層覆蓋在一張圖片上,調整這個層的不透明度,可以看出和明度調整效果完全一樣!
其實,我只是對PS的飽和度調整感興趣,原因前面已經說了,比HSV和HSL的飽和度調整效果要好,範圍要大,飽和度演算法沒研究出來,到搞了個明度調整過程。希望知道PS飽和度演算法的朋友不吝賜教,本人不甚感激!
測試代碼:
- procedure TForm1.Button1Click(Sender: TObject);
- var
- Image: TGpBitmap;
- g: TGpGraphics;
- begin
- Image := TGpBitmap.Create('D:/VclLib/GdiplusDemo/Media/20041001.jpg');
- g := TGpGraphics.Create(Handle, False);
- g.DrawImage(Image, 10, 10);
- GdipPSBrightness(Image, 30);
- g.DrawImage(Image, 10, 220);
- Image.Free;
- g.Free;
- end;
- procedure TForm1.Button2Click(Sender: TObject);
- var
- Image: TBitmap;
- begin
- Image := TBitmap.Create;
- Image.LoadFromFile('D:/VclLib/GdiplusDemo/Media/20041001.bmp');
- Canvas.Draw(10, 10, Image);
- BitmapPSBrightness(Image, -30);
- Canvas.Draw(10, 220, Image);
- Image.Free;
- end;
如有錯誤請來信指正:maozefa@hotmail.com