GDI+ 在Delphi程序的应用 -- 图像饱和度调整

自闭症网瘾萝莉.ら 提交于 2020-03-08 16:40:53

  

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);
    double delta = rgbMax - rgbMin;
    // 如果delta=0,S=0,所以不能调整饱和度
    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;
  1. // 图像饱和度调整。Value:(-255 - +255,没作范围检查)
  2. procedure Saturation(Data: TImageData; Value: Integer);
  3. asm
  4.     push    ebp
  5.     push    esi
  6.     push    edi
  7.     push    ebx
  8.     mov     edi, [eax + 16]// edi = Data.Scan0
  9.     mov     ecx, [eax + 4// ecx = Data.Width * Data.Height
  10.     imul    ecx, [eax]
  11.     mov     ebp, edx
  12.     cld
  13.   @PixelLoop:              // for (i = ecx - 1; i >= 0; i --)
  14.     dec     ecx            // {
  15.     js      @end
  16.     movzx   eax, [edi + 2]
  17.     movzx   ebx, [edi + 1]
  18.     movzx   esi, [edi]
  19.     cmp     esi, ebx
  20.     jge     @@1
  21.     xchg    esi, ebx
  22.   @@1:
  23.     cmp     esi, eax
  24.     jge     @@2
  25.     xchg    esi, eax
  26.   @@2:
  27.     cmp     ebx, eax
  28.     jle     @@3
  29.     mov     ebx, eax
  30.   @@3:
  31.     mov     eax, esi       //   delta = varMax - varMin
  32.     sub     eax, ebx       //   if (delta == 0)
  33.     jnz     @@4            //   {
  34.     add     edi, 4         //     edi += 4
  35.     jmp     @PixelLoop     //     continue
  36.   @@4:                     //   }
  37.     add     esi, ebx
  38.     mov     ebx, esi       //   ebx = varMax + varMin
  39.     shr     esi, 1         //   esi = L = (varMax + varMin) / 2
  40.     cmp     esi, 128
  41.     jl      @@5
  42.     neg     ebx            //   if (L >= 128) ebx = 510 - ebx
  43.     add     ebx, 510
  44.   @@5:
  45.     imul    eax, 255       //   eax = S = delta * 255 / ebx
  46.     cdq
  47.     div     ebx
  48.     mov     ebx, ebp       //   ebx = value
  49.     test    ebx, ebx       //   if (ebx > 0)
  50.     js      @@10           //   {
  51.     add     bl, al
  52.     jnc     @@6            //     if (ebx + S >= 255)
  53.     mov     ebx, eax       //       ebx = S
  54.     jmp     @@7
  55.   @@6:
  56.     mov     ebx, 255
  57.     sub     ebx, ebp       //     else ebx = 255 - value
  58.   @@7:
  59.     mov     eax, 65025     //     ebx = 65025 / ebx - 255
  60.     cdq
  61.     div     ebx
  62.     sub     eax, 255
  63.     mov     ebx, eax       //   }
  64.   @@10:
  65.     push    ebp
  66.     mov     ebp, 255
  67.     push    ecx
  68.     mov     ecx, 3
  69.   @RGBLoop:                //   for (j = 3; j > 0; j --)
  70.     movzx   eax, [edi]     //   {
  71.     push    eax
  72.     sub     eax, esi       //     rgb = rgb + (rgb - L) * ebx / 255
  73.     imul    eax, ebx
  74.     cdq
  75.     idiv    ebp
  76.     pop     edx
  77.     add     eax, edx
  78.     jns     @@11
  79.     xor     eax, eax       //     if (rgb < 0) rgb = 0
  80.     jmp     @@12
  81.   @@11:
  82.     cmp     eax, 255
  83.     jle     @@12
  84.     mov     eax, 255       //     else if (rgb > 255) rgb = 255
  85.   @@12:
  86.     stosb                  //     *edi ++ = rgb
  87.     loop    @RGBLoop       //   }
  88.     pop     ecx
  89.     pop     ebp
  90.     inc     edi            // edi ++
  91.     jmp     @PixelLoop     // }
  92.   @end:
  93.     pop     ebx
  94.     pop     edi
  95.     pop     esi
  96.     pop     ebp
  97. end;
  98. procedure GdipSaturation(Bmp: TGpBitmap; Value: Integer);
  99. var
  100.   Data: TBitmapData;
  101. begin
  102.   if Value = 0 then Exit;
  103.   Data := Bmp.LockBits(GpRect(00, Bmp.Width, Bmp.Height), [imRead, imWrite], pf32bppARGB);
  104.   try
  105.     Saturation(TImageData(Data), Value);
  106.   finally
  107.     Bmp.UnlockBits(Data);
  108.   end;
  109. end;
  110. procedure BitmapSaturation(Bmp: TBitmap; Value: Integer);
  111. begin
  112.   if Value <> 0 then
  113.     Saturation(GetImageData(Bmp), Value);
  114. 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的差多少),所以,要完全破解,还得试验,如果有谁知道,请务必告知,本人在此先谢了。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!