GDI+ 在Delphi程序的应用 -- 线性调整图像亮度

      我曾写过2篇关于GDI+图像亮度调整的文章:《 GDI+ 在Delphi程序的应用 -- 调整图像亮度》和《 GDI+ 在Delphi程序的应用 -- ColorMatrix与图像亮度》,前者采用GDI+的Bitmap扫描线逐点增加或减少图像像素RGB的值,后者则通过设置GDI+的ColorMatrix进行调整,但是这两种方法都属于非线性的亮度调整,优点是代码简单、速度快,缺点是在调整亮度的同时,也损失了图像的色彩的纯度。

    利用HSL颜色空间,通过只对其L(亮度)部分调整,可达到图像亮度的线性调整。但是,RGB和HSL颜色空间的转换很繁琐,一般还需要浮点数的运算,不仅增加了代码的复杂度,更重要的是要逐点将RGB转换为HSL,然后确定新的L值,再将HSL转换为RGB,运行速度可想而知是很慢的。要想提高图像亮度线性调整的速度,应该从三方面考虑,一是变浮点运算为整数运算,二是只提取HSL的L部分进行调整,三是采用汇编代码,在Delphi中,当然是BASM。下面是按照这三方面考虑写的图像亮度线性调整代码:

 

    说明:为了统一《GDI+ 在Delphi程序的应用》系列文章所用数据类型和图像处理格式,本文代码已作了修订,代码中所用Gdiplus单元下载地址及BUG更正见文章《GDI+ for VCL基础 -- GDI+ 与 VCL》。(2008.8.18记)
 
    数据类型:
  1. type
  2.   // 与GDI+ TBitmapData结构兼容的图像数据结构
  3.   TImageData = packed record
  4.     Width: LongWord;         // 图像宽度
  5.     Height: LongWord;        // 图像高度
  6.     Stride: LongWord;        // 图像扫描线字节长度
  7.     PixelFormat: LongWord;   // 未使用
  8.     Scan0: Pointer;          // 图像数据地址
  9.     Reserved: LongWord;      // 保留
  10.   end;
  11.   PImageData = ^TImageData;
  12. // 获取TBitmap图像的TImageData数据结构,便于处理TBitmap图像
  13. function GetImageData(Bmp: TBitmap): TImageData;
  14. begin
  15.   Bmp.PixelFormat := pf32bit;
  16.   Result.Width := Bmp.Width;
  17.   Result.Height := Bmp.Height;
  18.   Result.Scan0 := Bmp.ScanLine[Bmp.Height - 1];
  19.   Result.Stride := Result.Width shl 2;
  20. //  Result.Stride := (((32 * Bmp.Width) + 31) and $ffffffe0) shr 3;
  21. end;

   

过程代码:

 

  1. procedure GetHSL_L;
  2. asm
  3.     movzx   eax, [esi]
  4.     movzx   ebx, [esi + 1]
  5.     movzx   ecx, [esi + 2]
  6.     cmp     ebx, ecx      // L = (Max(R, Max(G, B)) + Min(R, Min(G, B))) >> 1;
  7.     jge     @@1
  8.     xchg    ebx, ecx
  9.   @@1:
  10.     cmp     ebx, eax
  11.     jge     @@2
  12.     xchg    ebx, eax
  13.   @@2:
  14.     cmp     ecx, eax
  15.     jle     @@3
  16.     xchg    ecx, eax
  17.   @@3:
  18.     add     ebx, ecx
  19.     shr     ebx, 1        // ebx = L
  20. end;
  21. procedure SetHSL_L;
  22. asm
  23.     sub     edi, 128      // edi = newL
  24.     xor     ebp, ebp      // if (L <= 128){
  25.     cmp     ebx, 128      //   ebp = 0; ebx = L
  26.     jle     @@0           // }
  27.     mov     ebp, 256      // else
  28.     sub     ebp, ebx      // {
  29.     xchg    ebp, ebx      //   ebx = 256 - L
  30.     sub     ebp, 128      //   ebp = (L - 128) * 256
  31.     shl     ebp, 8        // }
  32.   @@0:
  33.     mov     ecx, 3        // for (ecx = 3; ecx > 0; ecx --)
  34.   @RGBLoop:               // {
  35.     movzx   eax, [esi]    //   if (ebx == 0)
  36.     test    ebx, ebx      //     rgbToHS = *esi
  37.     jz      @@1
  38.     shl     eax, 7        //   else
  39.     sub     eax, ebp      //     rgbToHS = (*esi * 128 - ebp) / ebx
  40.     cdq
  41.     div     ebx
  42.   @@1:
  43.     test    edi, edi
  44.     js      @@2
  45.     mov     edx, 256      //   if (edi >= 0)
  46.     sub     edx, eax      //     rgb = rgbToHS + (256 - rgbToHS) * edi / 128
  47.     imul    edx, edi
  48.     add     edx, 127      //  由于2次舍弃对高亮度部分造成误差,+127作补偿
  49.     shr     edx, 7
  50.     jmp     @@3
  51.   @@2:
  52.     mov     edx, eax      //   else
  53.     imul    eax, edi      //     rgb = rgbToHS + rgbToHS * edi / 128
  54.     neg     eax
  55.     shr     eax, 7
  56.     neg     eax
  57.   @@3:
  58.     add     eax, edx      //   rgb = Max(0, Min(255, rgb))
  59.     jns     @@4
  60.     xor     eax, eax
  61.     jmp     @@5
  62.   @@4:
  63.     cmp     eax, 255
  64.     jle     @@5
  65.     mov     eax, 255
  66.   @@5:
  67.     mov     [esi], al     //   *esi ++ = rgb
  68.     inc     esi
  69.     loop    @RGBLoop      // }
  70.     inc     esi           // esi ++
  71. end;
  72. // 利用HSL线性调整图像亮度,Value亮度值
  73. procedure HSLBrightness(Data: TImageData; Value: Integer);
  74. asm
  75.     push    ebp
  76.     push    esi
  77.     push    edi
  78.     push    ebx
  79.     mov     esi, [eax + 16]
  80.     mov     ecx, [eax + 4]
  81.     imul    ecx, [eax]
  82.     mov     ebp, edx
  83.   @PixelLoop:
  84.     push    ecx
  85.     call    GetHSL_L
  86.     mov     edi, ebx
  87.     add     edi, ebp
  88.     push    ebp
  89.     call    SetHSL_L
  90.     pop     ebp
  91.     pop     ecx
  92.     loop    @PixelLoop
  93.     pop     ebx
  94.     pop     edi
  95.     pop     esi
  96.     pop     ebp
  97. end;
  98. // 利用HSL线性调整GDI+图像亮度,Value亮度值
  99. procedure GdipHSLBrightness(Bmp: TGpBitmap; Value: Integer);
  100. var
  101.   Data: TBitmapData;
  102. begin
  103.   if Value = 0 then Exit;
  104.   Data := Bmp.LockBits(GpRect(00, Bmp.Width, Bmp.Height), [imRead, imWrite], pf32bppARGB);
  105.   try
  106.     HSLBrightness(TImageData(Data), Value);
  107.   finally
  108.     Bmp.UnlockBits(Data);
  109.   end;
  110. end;
  111. // 利用HSL线性调整TBitmap图像亮度,Value亮度值
  112. procedure BitmapHSLBrightness(Bmp: TBitmap; Value: Integer);
  113. begin
  114.   if Value <> 0 then
  115.     HSLBrightness(GetImageData(Bmp), Value);
  116. end;

         其中的GetHSL_L和SetHSL_L过程是2个内部过程(不能直接调用的),为了方便调试和测试用的,可以将它们的代码直接移入到HSLBrightness过程中,分别替换call    GetHSL_L和call    SetHSL_L,速度还可得到小小的提升。

        GetHSL_L是用来提取像素HSL颜色的L部分,比较简单,用Pascal语法就一句话:

        L := (Max(R, Max(G, B)) + Min(R, Min(G, B))) div 2;

L没有采用通常的百分比表示,而是取值0 - 255,这样就不必要采用浮点数运算了。

        SetHSL_L相对要复杂多了,也是代码的核心部分,主要完成2个功能,一是用以前的L值(GetHSL_L中得到的)分别RGB求出其HSL的HS部分,其公式用Pascal表示为:

if L > 128 then
begin
  rHS := (R * 128 - (L - 128) * 256) div (256 - L);
  gHS := (G * 128 - (L - 128) * 256) div (256 - L);
  bHS := (B * 128 - (L - 128) * 256) div (256 - L);
end else
begin
  rHS := R * 128 div L;
  gHS := G * 128 div L;
  bHS := B * 128 div L;
end;

        二是用新的L值(老的L值加需要调整的亮度值(0 - 255))和上面求出的HS值计算出新的RGB值,计算方法为:
newL := L + Value - 128;
if newL > 0 then
begin
  newR := rHS + (256 - rHS) * newL div 128;
  newG := gHS + (256 - gHS) * newL div 128;
  newB := bHS + (256 - bHS) * newL div 128;
else begin
  newR := rHS + rHS * newL div 128;
  newG := gHS + gHS * newL div 128;
  newB := bHS + bHS * newL div 128;
end;

        如此,一个像素点的线性亮度调整就基本完成了,这也是比非线性亮度调整多出来的代码,运行速度肯定比非线性亮度调整慢,但完全可以满足要求:在我的机器上测试(P464 2.8G, DDR2 667 1GB),一幅1000W像素的数码照片用非线性调整为750毫秒,而线性调整过程为1300毫秒,其中,TGpBitmap的Lock过程就占了600多毫秒,实际非线性调整约150毫秒,线性亮度调整大约为750毫秒左右,是非线性调整耗时的5倍!当然,对于小的照片这些完全可以忽略。至于HSLBrightness过程在这里只是个循环处理框架罢了。

        至于上述代码计算是否正确,我做过随机单个像素提取和还原的测试,也就是随机给出R、G、B,提取其L值,然后不改变L值,提取HS值后还原为新的R、G、B值,其准确度差不多为99%以上,我也用《Delphi数字图像处理及高级应用》一书中的HSL与RGB转换代码测试过,本过程比书上的过程还原准确度高多了!其原因是我只提取了HSL的L,虽然也求了HS部分,但并没有将其拆开,这就减少了运算精度损失;另外,该书的L值以100为单位,而本过程用255为单位,虽然是整数运算,但只要在合适的地方给与适度补偿,其运算精度比书上的浮点数运算还高。我把测试GetHSL_L和SetHSL_L的代码附在下面,有兴趣的朋友可以测试一下:

procedure LTest(R, G, B: Integer; var L, RI, GI, BI: Integer);
var
  v: array[
0 .. 3 ] of Integer;
asm
  push    esi
  push    edi
  push    ebx

  lea     esi, v
  mov     eax, R
  mov     [esi], al
  mov     eax, G
  mov     [esi 
+   1 ], al
  mov     eax, B
  mov     [esi 
+   2 ], al
  call    GetHSL_L
  mov     eax, L
  mov     [eax], ebx
  mov     edi, ebx
  push    ebp
  call    SetHSL_L
  pop     ebp
  sub     esi, 4

  movzx   eax, [esi]
  mov     ebx, RI
  mov     [ebx], eax

  movzx   eax, [esi 
+   1 ]
  mov     ebx, GI
  mov     [ebx], eax

  movzx   eax, [esi 
+   2 ]
  mov     ebx, BI
  mov     [ebx], eax

  pop     ebx
  pop     edi
  pop     esi
end;

procedure TForm1.Button1Click(Sender: TObject);
const
  s = #13 + #10;
var
  R, G, B, RT, GT, BT, L: Integer;
begin
  Randomize;
  R := Random(255);
  G := Random(255);
  B := Random(255);
  LTest(R, G, B, L, RT, GT, BT);
  ShowMessage(Format(' R: %d,  G: %d,  B: %d%sRI: %d, GI: %d, BI: %d, L:%d',
                     [R, G, B, s, RT, GT, BT, L]));
end;

    好了,下面可以给出图像亮度线性调整过程的测试代码了:

procedure TForm1.Button2Click(Sender: TObject);
var
  bmp1, bmp2: TGpBitmap;
  g: TGpGraphics;
  r: TGpRect;
  value: Integer;
begin
  value :
=   20 ;
  bmp1 :
=  TGpBitmap.Create( ' d:/001-1.jpg ' );
  r :
=  GpRect( 0 0 , bmp1.Width, bmp1.Height);
  bmp2 :
=  bmp1.Clone(r, pf24bppRGB);
  g :
=  TGpGraphics.Create(Handle, False);
  
try
    GdipBrightness( bmp1  value);
    GdipHSLBrightness(
bmp2, value);
    g.TranslateTransform(
195 0 );
    g.DrawImage(bmp1, r);
    g.TranslateTransform(
195 0 );
    g.DrawImage(bmp2, r);
  
finally
    g.Free;
    bmp2.Free;
    bmp1.Free;
  end;
end;
procedure TForm1.Button3Click(Sender: TObject);
var
  bmp1, bmp2: TBitmap;

  value: Integer;
begin
  bmp1 :
=  TBitmap.Create;
  bmp2 :
=  TBitmap.Create;
  value :
=   100 ;
  
try
    bmp1.LoadFromFile(
' d:/001-1.bmp ' );
    bmp2.Assign(bmp1);
    Canvas.Draw(
0 0 , bmp1);
    BitmapBrightness(bmp1, value);
    Canvas.Draw(
195 0 , bmp1);
    BitmapHSLBrightness(bmp2, value);
    Canvas.Draw(
390 0 , bmp2);
  
finally
    bmp2.Free;
    bmp1.Free;
  end;
end;

        测试代码有2个,一是使用GDI+的TGpBitmap,一是使用Delphi的TBitmap,测试结果是相同的。有人可能会问,GDI+的图像亮度调整过程能否调整TBitmap?自己测试一下不就明白了,其原理可以参见我的文章《GDI+ 在Delphi程序的应用 -- 图像卷积操作及高斯模糊》;其中,每个测试都使用了非线性调整和线性调整过程,其中的非线性调整过程可参见文章开头提到的2篇文章(好像做广告了,呵呵)。运行结果如下:

        先给出测试图片原图:GDI+ 在Delphi程序的应用 -- 线性调整图像亮度_第1张图片

       再给出运行结果图和Photoshop处理的图片合成图(便于比较,免得贴太多的图):

GDI+ 在Delphi程序的应用 -- 线性调整图像亮度_第2张图片

        通过上面的比较图,不难发现Photoshop的亮度处理也是非线性的,因为其处理结果与我的非线性RGB亮度调整过程的处理结果完全一样!我记得好像有网友在CSDN论坛上非要寻求Photoshop的亮度调整原理,看来大可不必了,它使用的也是一种最简单的调整方法,只不过Photoshop作了-100 - +100的范围控制而已。

        而线性亮度调整和非线性亮度调整结果比较,很显然,线性调整后的颜色深度和层次性要好多了,在亮度值20的时候,效果比非线性亮度调整好多了,至于亮度值100和-100的调整,表面看,似乎线性调整不如非线性调整好看:非线性调整比较平淡、均匀,所以看上去比较“顺眼”;而线性调整由于调整后的颜色的纯度没什么损失,随着图像亮度大幅度的线性增减,其色彩层次空间也相应变化很大,使部分像素超出0 - 255范围,所以显得“难看”。但是仔细观察一下,亮度值为100的图像,虽然面部因亮度太强而损失了明暗,头发上也因此出现噪声,但背景的颜色层次却凸现出来了(如左下角的蓝色背景),即使是难看的-100亮度调整后的图片,也显示出很深的色彩层次空间感,而非线性亮度调整后的图片,除了平淡,还是平淡。

        老生常谈:1、本人用GDI+过程与网上有区别;2、如有错误请指正,建议也请来信:[email protected]

        后记:今天修改了SetHSL_L过程中当L(ebx)=0时,作除数的错误,这只有RGB全为0时会出现。另外发现HSL线性亮度调整的一个特性:当RGB全部=0或者全部=255,也就是像素为黑或白的时候,亮度值增量在-128 -- + 128(如果以100%表示的HSL空间,是-50% -- + 50%)范围内是不会做任何改变的。而一般的图片是不会采用这么大的亮度增量的,那将会使图片严重失真!当然,一幅图片中全黑和全白的像素也不会太多。除非是二值图。(可能有人会说这就是线性调整的主要特征,有啥奇怪的。不过我文化低,不大懂理论,发现这一点就记载这里,作为备忘)。

        后记:在GDI+下,32位PNG图像经转换为24位图像格式处理还原后,原有的透明色在转换过程中损失,故将本文对24位图像处理代码改为了32位图像处理代码。另外,测试代码中的一般图像亮度调整过程代码在文章《GDI+ 在Delphi程序的应用 -- 调整图像亮度》中。(2007.12.12)

你可能感兴趣的:(Delphi,GDI+(VCL)应用)