GDI+ 在Delphi程序的应用 -- 图像饱和度调整
图像的饱和度调整有很多方法,最简单的就是判断每个象素的R、G、B值是否大于或小于128,大于加上调整值,小于则减去调整值;也可将象素RGB转换为HSV或者HSL,然后调整其S部分,从而达到线性调整图象饱和度的目的。这几种方法我都测试过,效果均不太好,简单的就不说了,利用HSV和HSL调整饱和度,其调节范围很窄,饱和度没达到,难看的色斑却出现了。而Photoshop的饱和度调整调节范围大多了,效果也好多了,请看下面25%饱和度调整时几种方法的效果对比图:
可以看出,都是25%的饱和度调整,Photoshop的调节幅度显得小一些(平坦些),效果也好多了,而HSV和HSL均出现了色斑,某些颜色也严重失真,尤其是HSV方式。
据网上和书上的介绍,Photoshop的是利用所谓HSB颜色模式实现色相/饱和度调节的,可是就是没有看到其算法,我只得自己进行琢磨,首先发现Photoshop色相/饱和度命令中的明度调节好象是“独立”的,也就是它不需要转换为所谓的HSB模式,直接靠白色和黑色遮照层进行调节,具体原理和代码可看我写的《GDI+ 在Delphi程序的应用 -- 仿Photoshop的明度调整》一文。后来,却又发现Photoshop的饱和度调节好象是“半独立的”,什么意思呢?就是说Photoshop的色相/饱和度的调整还是转换为HSL颜色模式进行的,只是饱和度的增减调节却是“独立”于SHL模式的另外一套算法,如果不是需要HSL的S和L部分进行饱和度的上下限控制,它也和明度调整一样,可以独立进行!下面是我写的C++算法(只是随手写的算法,不是真正的运行代码):
inline void SwapRGB(int &a, int &b)
{
a += b;
b = a - b;
a -= b;
}
// 利用HSL模式求得颜色的S和L
double rgbMax = R / 255;
double rgbMin = G / 255;
double rgbC = B / 255;
if (rgbMax < rgbC)
SwapRGB(rgbMax, rgbC);
if (rgbMax < rgbMin)
SwapRGB(rgbMax, rgbMin);
if (rgbMin > rgbC)
SwapRGB(rgbMin, rgbC);
if (delta == 0) return;
double value = rgbMax + rgbMin;
double S, L = value / 2;
if (L < 0.5)
S = delta / value;
else
S = delta / (2 - value);
// 具体的饱和度调整,sValue为饱和度增减量
// 如果增减量>0,饱和度呈级数增强,否则线性衰减
if (sValue > 0)
{
// 如果增减量+S > 1,用S代替增减量,以控制饱和度的上限
// 否则取增减量的补数
sValue = sValue + S >= 1? S : 1 - sValue;
// 求倒数 - 1,实现级数增强
sValue = 1 / sValue - 1;
}
// L在此作饱和度下限控制
R = R + (R - L * 255) * sValue;
G = G + (G - L * 255) * sValue;
B = B + (B - L * 255) * sValue;
从上面的算法代码中可以看到,Photoshop的饱和度调整没有像HSV和HSL的饱和度调整那样,将S加上增减量重新计算,并将HSL转换回RGB,而只是取得了颜色的S、L作为上下限控制,对原有的RGB进行了“补丁”式的调节。
下面是根据以上算法写的Delphi的BASM代码和GDI+调用的饱和度调整过程:
- 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;
- // 图像饱和度调整。Value:(-255 - +255,没作范围检查)
- procedure Saturation(Data: TImageData; Value: Integer);
- asm
- push ebp
- push esi
- push edi
- push ebx
- mov edi, [eax + 16]// edi = Data.Scan0
- mov ecx, [eax + 4] // ecx = Data.Width * Data.Height
- imul ecx, [eax]
- mov ebp, edx
- cld
- @PixelLoop: // for (i = ecx - 1; i >= 0; i --)
- dec ecx // {
- js @end
- movzx eax, [edi + 2]
- movzx ebx, [edi + 1]
- movzx esi, [edi]
- cmp esi, ebx
- jge @@1
- xchg esi, ebx
- @@1:
- cmp esi, eax
- jge @@2
- xchg esi, eax
- @@2:
- cmp ebx, eax
- jle @@3
- mov ebx, eax
- @@3:
- mov eax, esi // delta = varMax - varMin
- sub eax, ebx // if (delta == 0)
- jnz @@4 // {
- add edi, 4 // edi += 4
- jmp @PixelLoop // continue
- @@4: // }
- add esi, ebx
- mov ebx, esi // ebx = varMax + varMin
- shr esi, 1 // esi = L = (varMax + varMin) / 2
- cmp esi, 128
- jl @@5
- neg ebx // if (L >= 128) ebx = 510 - ebx
- add ebx, 510
- @@5:
- imul eax, 255 // eax = S = delta * 255 / ebx
- cdq
- div ebx
- mov ebx, ebp // ebx = value
- test ebx, ebx // if (ebx > 0)
- js @@10 // {
- add bl, al
- jnc @@6 // if (ebx + S >= 255)
- mov ebx, eax // ebx = S
- jmp @@7
- @@6:
- mov ebx, 255
- sub ebx, ebp // else ebx = 255 - value
- @@7:
- mov eax, 65025 // ebx = 65025 / ebx - 255
- cdq
- div ebx
- sub eax, 255
- mov ebx, eax // }
- @@10:
- push ebp
- mov ebp, 255
- push ecx
- mov ecx, 3
- @RGBLoop: // for (j = 3; j > 0; j --)
- movzx eax, [edi] // {
- push eax
- sub eax, esi // rgb = rgb + (rgb - L) * ebx / 255
- imul eax, ebx
- cdq
- idiv ebp
- pop edx
- add eax, edx
- jns @@11
- xor eax, eax // if (rgb < 0) rgb = 0
- jmp @@12
- @@11:
- cmp eax, 255
- jle @@12
- mov eax, 255 // else if (rgb > 255) rgb = 255
- @@12:
- stosb // *edi ++ = rgb
- loop @RGBLoop // }
- pop ecx
- pop ebp
- inc edi // edi ++
- jmp @PixelLoop // }
- @end:
- pop ebx
- pop edi
- pop esi
- pop ebp
- end;
- procedure GdipSaturation(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
- Saturation(TImageData(Data), Value);
- finally
- Bmp.UnlockBits(Data);
- end;
- end;
- procedure BitmapSaturation(Bmp: TBitmap; Value: Integer);
- begin
- if Value <> 0 then
- Saturation(GetImageData(Bmp), Value);
- end;
具体的测试代码就不写了,有兴趣者可参考我的《GDI+ 在Delphi程序的应用 -- 线性调整图像亮度》、《GDI+ 在Delphi程序的应用 -- 图像卷积操作及高斯模糊》和《GDI+ 在Delphi程序的应用 -- 图像亮度/对比度调整》等文章,写出GDI+的TGpBitmap和Delphi的TBitmap的测试代码,其运行结果与Photoshop完全一样。
对于色相的调整,HSV、HSL和HSB都是相同的,不同的只是饱和度和亮度(明度)的调整,前天我已经写了《GDI+ 在Delphi程序的应用 -- 仿Photoshop的明度调整》,加上这篇饱和度算法文章,是否意味Photoshop的HSB算法完全破解了呢?不然,Photoshop的饱和度和明度调整独立使用时,确实是我说的那样,与Photoshop效果完全一样,但是放在一起进行调节就有区别了,这里有个谁先谁后的时机问题,和我前天写的《GDI+ 在Delphi程序的应用 -- 图像亮度/对比度调整》中对比度和亮度关系一样,各自独立使用没问题,放在一起调整就麻烦,但是对比度和亮度的关系比较简单,几次测试就清楚了,而饱和度和明度的关系我试验过多次,均与Photoshop有区别(只是有区别而以,其效果不比Photoshop的差多少),所以,要完全破解,还得试验,如果有谁知道,请务必告知,本人在此先谢了。
来源:https://www.cnblogs.com/esreal/archive/2013/01/02/2841835.html