Delphi图像处理 -- Photoshop图像亮度/对比度调整

本人已经写过几篇关于亮度调整的文章,但是关于图像的对比度调整的过程和文章却一直没有写,其原因是一直没找到一个好的算法。可能有人会说,图像的亮度,对比度调整是最简单的图形操作,其算法网上可说是一搜一大把,确实如此,可就是这最简单的操作,网上的文章却五花八门,我拣几个试了一下,好像都不太理想,关键是算法太简单,实际操作效果不好,于是想,Photoshop的对比度还是较好的,而且也通用,但偏偏网上没有介绍它的算法,用了大半天时间研究了一下,再花了1个来小时写了个Delphi过程,试了一下,居然和Photoshop的对比度调整完全一样的效果!于是认真写了个测试程序,把亮度和对比度放在一起进行调整(亮度和对比度处理过程为各自独立的,其中亮度过程基本是本BLOG文章《GDI+ 在Delphi程序的应用 -- 调整图像亮度》的代码),可是效果却和Photoshop大不一样了,是什么原因呢,Photoshop的亮度调整算法是最简单的那种,与我的亮度过程做出来的是一样的(效果比较图参见《GDI+ 在Delphi程序的应用 -- 线性调整图像亮度》),而前面说了,对比度过程算法也是和Photoshop一样的,可放在一起调整就不行了,无论是先调整亮度,还是先调整对比度都这样。后来仔细分析了一下,Photoshop是用一个函数处理亮度/对比度,而且亮度调整是按对比度的正负分别对待的,下面是实现代码:

过程定义: // 图像亮度调整,Value亮度值 procedure ImageBrightness(var Data: TImageData; Value: Integer); // 图像亮度/对比度调整。参数: // Dest输出图,Source原图,Data自身操作图像 // Bright亮度,Contrast对比度,Threshold对比度阀值(可用灰度统计数据的平均灰度) // Callback回调函数,返回True终止操作,CallbackData回调函数参数地址 procedure ImageBrightContrast(var Data: TImageData; Bright, Contrast: Integer; Threshold: LongWord); overload; {$IF RTLVersion >= 17.00}inline;{$IFEND} procedure ImageBrightContrast(var Dest: TImageData; const Source: TImageData; Bright, Contrast: Integer; Threshold: LongWord); overload; {$IF RTLVersion >= 17.00}inline;{$IFEND} // 无效参数或者被回调函数终止操作返回False。 function ImageBrightContrast(var Dest: TImageData; const Source: TImageData; Bright, Contrast: Integer; Threshold: LongWord; Callback: TImageAbort; CallbackData: Pointer): Boolean; overload; 实现代码: procedure ImageBrightness(var Data: TImageData; Value: Integer); asm push esi push edi push ebx call IsValid32 jc @@Exit mov esi, edx test edx, edx jz @@Exit // if (Value == 0) return jns @@1 neg edx // if (Value < 0) Value = -Value @@1: and edx, 0ffh movd mm1, edx // mm1 = 00 00 00 00 00 00 00 Value punpcklbw mm1, mm1 punpcklwd mm1, mm1 punpckldq mm1, mm1 psrld mm1, 8 // mm1 = 00 Value(7 byte) call SetDataRegs32 test ecx, 1 jz @@2 add ebx, 4 @@2: mov eax, ecx shr ecx, 1 // ecx = Data.Width / 2 test esi, esi jl @@yLoopS // if (value < 0) BrightnessSub @@yLoopA: // for (y = Data.Height; y > 0; y--) push ecx // { @@xLoopA: // for (x = edx - 1; x >= 0; x --) dec ecx // { js @@1_1 movq mm0, [edi] // mm0 = A1 R1 G1 B1 A0 R0 G0 B0 paddusb mm0, mm1 // mm0 += mm1 movq [edi], mm0 // (int64* )edi = mm0 add edi, 8 // edi += 8 jmp @@xLoopA // } @@1_1: test eax, 1 // if (Data.Width % 2 == 1) jz @@1_2 // { movd mm0, [edi] // mm0 = 00 00 00 00 A0 R0 G0 B0 paddusb mm0, mm1 // mm0 += mm1 movd [edi], mm0 // (int* )edi = mm0 @@1_2: // } pop ecx add edi, ebx // edi += Offset dec edx jnz @@yLoopA // } jmp @@Exit @@yLoopS: // for (y = Data.Height; y > 0; y--) push ecx // { @@xLoopS: // for (x = edx - 1; x >= 0; x --) dec ecx // { js @@2_1 movq mm0, [edi] // mm0 = A1 R1 G1 B1 A0 R0 G0 B0 psubusb mm0, mm1 // mm0 -= mm1 movq [edi], mm0 // (int64* )edi = mm0 add edi, 8 // edi += 8 jmp @@xLoopS // } @@2_1: test eax, 1 // if (Data.Width % 2 == 1) jz @@2_2 // { movd mm0, [edi] // mm0 = 00 00 00 00 A0 R0 G0 B0 psubusb mm0, mm1 // mm0 -= mm1 movd [edi], mm0 // (int* )edi = mm0 @@2_2: // } pop ecx add edi, ebx // edi += Offset dec edx jnz @@yLoopS // } @@Exit: emms pop ebx pop edi pop esi end; function ClacBrightContrast(value: Integer): Integer; asm test eax, eax jg @@1 cmp eax, -255 jge @@Exit mov eax, -255 jmp @@Exit @@1: push ecx push edx mov ecx, 256 sub ecx, eax jg @@2 mov ecx, 1 @@2: mov eax, 65536 xor edx, edx div ecx sub eax, 256 pop edx pop ecx @@Exit: end; procedure BrightContrast(bright, contrast, threshold, cv: Integer); pascal; var height, count: Integer; dstOffset, srcOffset: Integer; asm mov height, edx mov dstOffset, ebx mov srcOffset, eax mov ebx, contrast // ebx = contrast mov edx, threshold @@yLoop: push ecx @@xLoop: push ecx mov count, 3 @@rgbLoop: movzx eax, [esi] // eax = rgb test ebx, ebx jz @@21 // if (contrast > 0) js @@10 // { add eax, bright // rgb += bright jns @@2 xor eax, eax @@2: cmp ebx, 255 jl @@15 // if (contrast >= 255) cmp eax, edx // { jl @@3 // rgb = rgb >= threshold? 255 : 0 mov eax, 255 // goto @@next jmp @@next // } @@3: // } xor eax, eax jmp @@next @@10: cmp ebx, -255 // else if (contrast <= -255) jg @@15 // { mov eax, edx // rgb = threshold; goto @@20 jmp @@20 // } @@15: mov ecx, eax // rgb = rgb + (rgb - threshold) * cv / 256 sub eax, edx imul eax, cv sar eax, 8 add eax, ecx jns @@20 xor eax, eax @@20: test ebx, ebx jg @@22 @@21: add eax, bright // if (contrast <= 0) rgb += bright jns @@22 xor eax, eax @@22: cmp eax, 255 jbe @@next mov eax, 255 @@next: stosb inc esi dec count jnz @@rgbLoop pop ecx movsb dec ecx jnz @@xLoop add edi, dstOffset add esi, srcOffset pop ecx dec height jnz @@yLoop end; function ImageBrightContrast(var Dest: TImageData; const Source: TImageData; Bright, Contrast: Integer; Threshold: LongWord; Callback: TImageAbort; CallbackData: Pointer): Boolean; var cv: Integer; begin Result := False; if ImageEmpty(Dest) or ImageEmpty(Source) then Exit; if Threshold > 255 then Threshold := 255; cv := ClacBrightContrast(Contrast); if Assigned(Callback) then Result := ExecuteAbort(Dest, Source, @BrightContrast, [Bright, Contrast, Threshold, cv], Callback, CallbackData) else Result := ExecuteProc(Dest, Source, @BrightContrast, [Bright, Contrast, Threshold, cv]); end; procedure ImageBrightContrast(var Dest: TImageData; const Source: TImageData; Bright, Contrast: Integer; Threshold: LongWord); begin ImageBrightContrast(Dest, Source, Bright, Contrast, Threshold, nil, nil); end; procedure ImageBrightContrast(var Data: TImageData; Bright, Contrast: Integer; Threshold: LongWord); begin ImageBrightContrast(Data, Data, Bright, Contrast, Threshold, nil, nil); end;

下面对亮度/对比度的原理简单介绍一下。

一、Photoshop对比度算法。可以用下面的公式来表示:

(1)、nRGB = RGB + (RGB - Threshold) * Contrast / 255

公式中,nRGB表示图像像素新的R、G、B分量,RGB表示图像像素R、G、B分量,Threshold为给定的阀值,Contrast为处理过的对比度增量。

Photoshop对于对比度增量,是按给定值的正负分别处理的:

当增量等于-255时,是图像对比度的下端极限,此时,图像RGB各分量都等于阀值,图像呈全灰色,灰度图上只有1条线,即阀值灰度;

当增量大于-255且小于0时,直接用上面的公式计算图像像素各分量;

当增量等于 255时,是图像对比度的上端极限,实际等于设置图像阀值,图像由最多八种颜色组成,灰度图上最多8条线,即红、黄、绿、青、蓝、紫及黑与白;

当增量大于0且小于255时,则先按下面公式(2)处理增量,然后再按上面公式(1)计算对比度:

(2)、nContrast = 255 * 255 / (255 - Contrast) - 255

公式中的nContrast为处理后的对比度增量,Contrast为给定的对比度增量。

二、图像亮度调整。本文采用的是最常用的非线性亮度调整(Phoposhop CS3以下版本也是这种亮度调整方式,CS3及以上版本也保留了该亮度调整方式的选项),本文亮度调整采用MMX,对亮度增量分正负情况分别进行了处理,每次处理2个像素,速度相当快,比常规BASM代码的亮度处理过程还要快几倍(参见《GDI+ 在Delphi程序的应用 -- 调整图像亮度》)。

三、图像亮度/对比度综合调整算法。这个很简单,当亮度、对比度同时调整时,如果对比度大于0,现调整亮度,再调整对比度;当对比度小于0时,则相反,先调整对比度,再调整亮度。

下面给出完整的调整亮度/对比度的Delphi代码,包括灰度统计、绘图等:

unit Main; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, ExtCtrls, ComCtrls, ImageUtils, GpBitmapUtils; type TPanel = class(ExtCtrls.TPanel) public procedure Paint; override; end; TMainForm = class(TForm) Label1: TLabel; Label2: TLabel; BBar: TTrackBar; CBar: TTrackBar; Button1: TButton; BEdit: TEdit; CEdit: TEdit; Panel1: TPanel; Label3: TLabel; GrayMap: TPaintBox; Average: TLabel; RadioButton1: TRadioButton; RadioButton2: TRadioButton; procedure FormCreate(Sender: TObject); procedure FormDestroy(Sender: TObject); procedure GrayMapPaint(Sender: TObject); procedure BBarChange(Sender: TObject); procedure BEditChange(Sender: TObject); procedure CBarChange(Sender: TObject); procedure Button1Click(Sender: TObject); procedure BEditKeyPress(Sender: TObject; var Key: Char); procedure RadioButton1Click(Sender: TObject); procedure RadioButton2Click(Sender: TObject); private { Private declarations } FData: TImageData; FtmpData: TImageData; FLock: Boolean; FGrayData: TGrayStatData; FGrayAverage: Integer; FIsRun: Boolean; FAbort: Boolean; public { Public declarations } procedure GrayDiagram; procedure AdjustmentImage; end; var MainForm: TMainForm; implementation uses Math, Gdiplus; {$R *.dfm} type TBrightContrast = function(var Dest: TImageData; const Source: TImageData; Bright, Contrast: Integer; Threshold: LongWord; Callback: TImageAbort; CallbackData: Pointer): Boolean; var BrightContrast: TBrightContrast = ImageBrightContrast; function MyAbort(data: Pointer): BOOL; stdcall; begin Result := TMainForm(data).FAbort; end; procedure TMainForm.AdjustmentImage; begin if not FIsRun then begin FIsRun := True; FAbort := False; if BrightContrast(FTmpData, FData, BBar.Position, Round(CBar.Position * 255.0 / 100.0) , FGrayAverage, MyAbort, Self) then GrayDiagram; Panel1.Paint; FIsRun := False; end else FAbort := True; end; procedure TMainForm.BBarChange(Sender: TObject); begin if not FLock then BEdit.Text := IntToStr(BBar.Position); end; procedure TMainForm.CBarChange(Sender: TObject); begin if not FLock then CEdit.Text := IntToStr(CBar.Position); end; procedure TMainForm.BEditChange(Sender: TObject); var v: Integer; begin with Sender as TEdit do begin FLock := True; if Text = '' then v := 0 else v := StrToInt(Text); if Tag = 1 then CBar.Position := v else BBar.Position := v; AdjustmentImage; FLock := False; end; end; procedure TMainForm.BEditKeyPress(Sender: TObject; var Key: Char); begin if (Key >= #32) and not (Key in ['0'..'9']) then Key := #0; end; procedure TMainForm.Button1Click(Sender: TObject); begin Close; end; procedure TMainForm.FormCreate(Sender: TObject); begin DoubleBuffered := True; FData := GetImageData(TGpBitmap.Create('..\..\Media\56-3.jpg'), True); FtmpData := NewImageData(FData.Width, FData.Height, 0); ImageGrayStat(FData, FGrayData); FGrayAverage := FGrayData.Average; AdjustmentImage; end; procedure TMainForm.FormDestroy(Sender: TObject); begin FreeImageData(FtmpData); FreeImageData(FData); end; procedure TMainForm.GrayMapPaint(Sender: TObject); var I: Integer; begin GrayMap.Canvas.Brush.Color := clSkyBlue; GrayMap.Canvas.FillRect(GrayMap.ClientRect); GrayMap.Canvas.Pen.Color := clRed; GrayMap.Canvas.Brush.Style := bsClear; GrayMap.Canvas.Rectangle(GrayMap.ClientRect); for I := 0 to 255 do begin GrayMap.Canvas.Pen.Color := RGB(I, I, I); GrayMap.Canvas.MoveTo(I + 1, GrayMap.Height); GrayMap.Canvas.LineTo(I + 1, GrayMap.Height - Round(35.0 * (Log10(FGrayData.Grays[I] + 1)))); end; end; procedure TMainForm.GrayDiagram; begin ImageGrayStat(FtmpData, FGrayData); Average.Caption := '平均灰度:' + IntToStr(FGrayData.Average); GrayMap.Invalidate; end; procedure TMainForm.RadioButton1Click(Sender: TObject); begin BrightContrast := ImageBrightContrast; AdjustmentImage; end; procedure TMainForm.RadioButton2Click(Sender: TObject); begin BrightContrast := ImageLineBrightContrast; AdjustmentImage; end; { TPanel } procedure TPanel.Paint; begin with MainForm do begin DrawImage(Panel1.Canvas, 0, 0, FtmpData); DrawImage(Panel1.Canvas, 0, FtmpData.Height, FData); end; end; end.

例子代码中的图像显示过程DrawImage见《Delphi图像处理 -- 图像显示》,灰度统计过程ImageGrayStat见《Delphi图像处理 -- 图像的灰度化、二值化及反色》。

例子中有个线性亮度选项,有关线性亮度/对比度调整另文介绍。

下面是运行界面,其效果和Photoshop基本一致(如果前面实现代码不优化速度,由256替代255,与Photoshop应该是完全相同的):

Delphi图像处理 -- Photoshop图像亮度/对比度调整_第1张图片

文章中使用GDI+版本下载地址和说明见《GDI+ for VCL基础 -- GDI+ 与 VCL》。

文章中所用数据类型及一些过程见《Delphi图像处理 -- 数据类型及内部过程》和《Delphi图像处理 -- 图像像素结构与图像数据转换》。

尽管我十分努力,但水平有限,错误在所难免,欢迎指正和指导。邮箱地址:

[email protected]

本文代码于2010.5.20重新修订过。增加了拷贝形式的调整过程和响应回调函数的调整过程(修改过的例子代码使用了这种处理过程)。代码中的ExecuteAbort过程和ExecuteProc过程见《Delphi图像处理 -- 图像像素结构与图像数据转换》。

你可能感兴趣的:(Delphi图像处理 -- Photoshop图像亮度/对比度调整)