我曾写过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。下面是按照这三方面考虑写的图像亮度线性调整代码:
数据类型:
- type
-
- TImageData=packedrecord
- Width:LongWord;
- Height:LongWord;
- Stride:LongWord;
- PixelFormat:LongWord;
- Scan0:Pointer;
- Reserved:LongWord;
- end;
- PImageData=^TImageData;
-
-
- functionGetImageData(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.Widthshl2;
-
- end;
过程代码:
- procedureGetHSL_L;
- asm
- movzxeax,[esi]
- movzxebx,[esi+1]
- movzxecx,[esi+2]
- cmpebx,ecx
- jge@@1
- xchgebx,ecx
- @@1:
- cmpebx,eax
- jge@@2
- xchgebx,eax
- @@2:
- cmpecx,eax
- jle@@3
- xchgecx,eax
- @@3:
- addebx,ecx
- shrebx,1
- end;
- procedureSetHSL_L;
- asm
- subedi,128
- xorebp,ebp
- cmpebx,128
- jle@@0
- movebp,256
- subebp,ebx
- xchgebp,ebx
- subebp,128
- shlebp,8
- @@0:
- movecx,3
- @RGBLoop:
- movzxeax,[esi]
- testebx,ebx
- jz@@1
- shleax,7
- subeax,ebp
- cdq
- divebx
- @@1:
- testedi,edi
- js@@2
- movedx,256
- subedx,eax
- imuledx,edi
- addedx,127
- shredx,7
- jmp@@3
- @@2:
- movedx,eax
- imuleax,edi
- negeax
- shreax,7
- negeax
- @@3:
- addeax,edx
- jns@@4
- xoreax,eax
- jmp@@5
- @@4:
- cmpeax,255
- jle@@5
- moveax,255
- @@5:
- mov[esi],al
- incesi
- loop@RGBLoop
- incesi
- end;
-
- procedureHSLBrightness(Data:TImageData;Value:Integer);
- asm
- pushebp
- pushesi
- pushedi
- pushebx
- movesi,[eax+16]
- movecx,[eax+4]
- imulecx,[eax]
- movebp,edx
- @PixelLoop:
- pushecx
- callGetHSL_L
- movedi,ebx
- addedi,ebp
- pushebp
- callSetHSL_L
- popebp
- popecx
- loop@PixelLoop
- popebx
- popedi
- popesi
- popebp
- end;
-
- procedureGdipHSLBrightness(Bmp:TGpBitmap;Value:Integer);
- var
- Data:TBitmapData;
- begin
- ifValue=0thenExit;
- Data:=Bmp.LockBits(GpRect(0,0,Bmp.Width,Bmp.Height),[imRead,imWrite],pf32bppARGB);
- try
- HSLBrightness(TImageData(Data),Value);
- finally
- Bmp.UnlockBits(Data);
- end;
- end;
-
- procedureBitmapHSLBrightness(Bmp:TBitmap;Value:Integer);
- begin
- ifValue<>0then
- HSLBrightness(GetImageData(Bmp),Value);
- 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的代码附在下面,有兴趣的朋友可以测试一下:
procedureLTest(R,G,B:Integer;varL,RI,GI,BI:Integer);
var
v:array[
0
..
3
]ofInteger;
asm
pushesi
pushedi
pushebx
leaesi,v
moveax,R
mov[esi],al
moveax,G
mov[esi
+
1
],al
moveax,B
mov[esi
+
2
],al
callGetHSL_L
moveax,L
mov[eax],ebx
movedi,ebx
pushebp
callSetHSL_L
popebp
subesi,4
movzxeax,[esi]
movebx,RI
mov[ebx],eax
movzxeax,[esi
+
1
]
movebx,GI
mov[ebx],eax
movzxeax,[esi
+
2
]
movebx,BI
mov[ebx],eax
popebx
popedi
popesi
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;
好了,下面可以给出图像亮度线性调整过程的测试代码了:
procedureTForm1.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;
procedureTForm1.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篇文章(好像做广告了,呵呵)。运行结果如下:
先给出测试图片原图:
再给出运行结果图和Photoshop处理的图片合成图(便于比较,免得贴太多的图):
通过上面的比较图,不难发现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)