阅读提示:
《Delphi图像处理》系列以效率为侧重点,一般代码为PASCAL,核心代码采用BASM。
《C++图像处理》系列以代码清晰,可读性为主,全部使用C++代码。
尽可能保持二者内容一致,可相互对照。
本文代码必须包括文章《Delphi图像处理 -- 数据类型及公用过程》中的ImageData.pas单元。
在Photoshop中,图像色阶调整应用很广泛,本文介绍的图像色阶调整过程与Photoshop处理效果基本一致。
Photoshop的色阶调整分输入色阶调整和输出色阶调整,其中输入色阶调整有3个调整点,即通常所说的黑场、白场及灰场调整。
输入色阶调整的基本算法并不复杂,首先计算出白场与黑场的离差Diff,然后计算出像素各份量值与黑场的离差rgbDiff,如果rgbDiff<=0,像素各份量值等于0,否则,计算以rgbDiff与Diff的比值为底的灰场倒数的幂。用公式表示:
Diff = Highlight -Shadow
rgbDiff = RGB - Shadow
clRGB = Power(rgbDiff / Diff, 1 / Midtones)
其中Shadow为输入色阶低端数据(黑场),Highlight为输入色阶高端数据(白场), Midtones为输入色阶中间数据(灰场),Diff为二者的离差(必须大于1),RGB为调整前的像素分量值,clRGB为调整输入色阶后的像素分量值。
输出色阶调整更简单,首先计算输出色阶白场与黑场的离差与255的比值系数,然后用输入色阶调整后的像素分量值乘上这个系数,再加上输出黑场值即可。用公式表示:
outClRGB = clRGB * (outHighlight - outShadow) / 255 + outShadow
其中,outShadow为输出黑场,outHighlight为输出白场,outClRGB为全部色阶调整后的像素分量值。
前面已经提到输入色阶黑白场的离差必须大于1,而输入色阶并没有这个限制,输出黑白场的离差可以为负数,当输出黑场与白场完全颠倒时,输出色阶调整后的图片为原图片的负片。
色阶调整涉及四个通道,即R、G、B各分量通道及整体颜色通道,调整如果每个通道单独调整,将是比较麻烦和耗时的,本文采用色阶表替换法,可一次性完成所有四个通道的色阶调整。
下面直接给出一个完整的图像色阶调整例子源代码,其中包含了色阶调整和灰度计算函数:
unit main; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, ExtCtrls, ComCtrls, ImageData, Gdiplus; type // 色阶项结构 PColorLevelItem = ^TColorLevelItem; TColorLevelItem = packed record Shadow: LongWord; Midtones: Single; Highlight: LongWord; OutShadow: LongWord; OutHighlight: LongWord; end; // 色阶通道结构 PColorLevelData = ^TColorLevelData; TColorLevelData = record Blue: TColorLevelItem; Green: TColorLevelItem; Red: TColorLevelItem; RGB: TColorLevelItem; end; // 256色灰度统计数组,每个元素表示该下标对应的颜色个数 PGrayArray = ^TGrayArray; TGrayArray = array[0..255] of LongWord; // 灰度信息结构 PImageGrayInfo = ^TImageGrayInfo; TImageGrayInfo = packed record Grays: TGrayArray; // 灰度数组 Total: int64; // 总的灰度值 Count: LongWord; // 总的像素点数 MaxValue: LongWord; // 像素点最多的灰度值 MinValue: LongWord; // 像素点最少的灰度值 Average: LongWord; // 平均灰度值(Total / Count) end; TMainForm = class(TForm) LBar: TTrackBar; HBar: TTrackBar; Button1: TButton; Label3: TLabel; GrayMap: TPaintBox; ComboBox1: TComboBox; Label1: TLabel; LLabel: TLabel; HLabel: TLabel; MBar: TTrackBar; MLabel: TLabel; PaintBox1: TPaintBox; OLBar: TTrackBar; OHBar: TTrackBar; OLLabel: TLabel; OHLabel: TLabel; Label5: TLabel; procedure FormCreate(Sender: TObject); procedure FormDestroy(Sender: TObject); procedure GrayMapPaint(Sender: TObject); procedure LBarChange(Sender: TObject); procedure HBarChange(Sender: TObject); procedure Button1Click(Sender: TObject); procedure ComboBox1Change(Sender: TObject); procedure MBarChange(Sender: TObject); procedure PaintBox1Paint(Sender: TObject); procedure OLBarChange(Sender: TObject); procedure OHBarChange(Sender: TObject); private { Private declarations } FBitmap: TGpBitmap; FSource: TImageData; FDest: TImageData; FLevelData: TColorLevelData; FLevelItems: array[0..3] of PColorLevelItem; FGrayInfos: array[0..3] of TImageGrayInfo; FLock: Boolean; FIsRun: Boolean; FAbort: Boolean; public { Public declarations } procedure AdjustmentImage; procedure GetGrayInfos; end; var MainForm: TMainForm; implementation uses Math; {$R *.dfm} const GRAY_MMX: TMMType = (3735, 19235, 9798, 0); // [0.114,0.587,0.229] * 32768 // 获取图像灰度信息 function GetGrayInfo(const Data: TImageData; var GrayInfo: TImageGrayInfo; isGrayImage: Boolean = False): Integer; asm push ebp push esi push edi push ebx push ecx push eax mov edi, edx mov esi, edx xor eax, eax mov ecx, 256 rep stosd // init Grays pop eax call _SetDataRegs mov eax, ecx imul eax, edx xchg eax, [esp] // pixel count test al, al jnz @@yGrayLoop // 建立灰度数组 pxor mm7, mm7 movq mm2, GRAY_MMX @@yLoop: push ecx @@xLoop: movd mm0, [edi] punpcklbw mm0, mm7 pmaddwd mm0, mm2 movq mm1, mm0 psrlq mm1, 32 paddd mm0, mm1 movd eax, mm0 add eax, 16384 shr eax, 15 inc [esi].TImageGrayInfo.Grays[eax*4].Integer add edi, 4 // grayData.Grays[gray] ++ loop @@xLoop pop ecx add edi, ebx dec edx jnz @@yLoop emms jmp @@SumStart // 建立灰度图的灰度数组 @@yGrayLoop: push ecx @@xGrayLoop: movzx eax, [edi].TARGBQuad.Blue // gray = *edi inc [esi].TImageGrayInfo.Grays[eax*4].Integer// grayData.Grays[gray] ++ add edi, 4 loop @@xGrayLoop pop ecx add edi, ebx dec edx jnz @@yGrayLoop // 计算总的灰度值、最大灰度值及最小灰度值 @@SumStart: push esi mov edi, esi // esi = ebx = &GrayData.Grays[0] mov ebx, esi xor eax, eax // edx:eax = 0 (GrayData.Total) xor edx, edx xor ecx, ecx // for (index = 0; index < 256; index ++) @@SumLoop: // { mov ebp, [edi] cmp [esi], ebp cmovb esi, edi // if (*esi < *edi) esi = edi cmp [ebx], ebp cmova ebx, edi // if (*ebx > *edi) ebx = edi imul ebp, ecx // ebp = *edi * index add eax, ebp // edx:eax += ebp adc edx, 0 add edi, 4 // edi += 4 inc ecx cmp ecx, 255 jle @@SumLoop // } pop edi sub ebx, edi shr ebx, 2 // min = (ebx - &GrayData.Grays[0]) / 4 mov [edi].TImageGrayInfo.MinValue, ebx sub esi, edi shr esi, 2 // max = (esi - &GrayData.Grays[0]) / 4 mov [edi].TImageGrayInfo.MaxValue, esi pop ebx // count = data.Width * data.Height mov [edi].TImageGrayInfo.Count, ebx mov dword ptr[edi].TImageGrayInfo.Total, eax // total = edx:eax mov dword ptr[edi].TImageGrayInfo.Total+4, edx mov ecx, ebx shr ecx, 1 add eax, ecx adc edx, 0 div ebx // average = (total + count / 2) / count mov [edi].TImageGrayInfo.Average, eax @@Exit: // return GrayData.Average pop ebx pop edi pop esi pop ebp end; // 用色阶表替换Source像素值到Dest procedure _DoColorLevel(var Dest: TImageData; const Source: TImageData; const Table: PGrayTable); var height, dstOffset, srcOffset: Integer; asm push ecx call _SetCopyRegs mov height, edx mov dstOffset, ebx mov srcOffset, eax pop ebx @@yLoop: push ecx @@xLoop: movzx eax, [esi].TARGBQuad.Blue movzx edx, [esi].TARGBQuad.Green mov al, [ebx+eax] mov dl, [ebx+edx+256] mov [edi].TARGBQuad.Blue, al mov [edi].TARGBQuad.Green, dl movzx eax, [esi].TARGBQuad.Red mov al, [ebx+eax+512] mov ah, [esi].TARGBQuad.Alpha mov [edi].TARGBQuad.Red.Word, ax add esi, 4 add edi, 4 loop @@xLoop pop ecx add esi, srcOffset add edi, dstOffset dec height jnz @@yLoop end; // 拷贝Source到Dest procedure _DoCopyImageData(var Dest: TImageData; const Source: TImageData); asm call _SetCopyRegs @@yLoop: push ecx rep movsd pop ecx add esi, eax add edi, ebx dec edx jnz @@yLoop end; // 如果色阶项Item参数非初始值,计算色阶表Table并返回真 function GetColorLevelTable(Item: TColorLevelItem; var Table: TGrayTable): Boolean; var i, v: Integer; outDiff, diff: Integer; outCoef, coef: double; exponent: double; isMidtones: Boolean; begin outDiff := Integer(Item.OutHighlight - Item.OutShadow); diff := Integer(Item.Highlight - Item.Shadow); isMidtones := (Item.Midtones <> 1.0) and not ((Item.Midtones > 9.99) or (Item.Midtones < 0.1)); Result := ((Item.Highlight <= 255) and (diff < 255) and (diff >= 2)) or ((Item.OutHighlight <= 255) and (Item.OutShadow <= 255) and (outDiff < 255)) or isMidtones; if not Result then Exit; Coef := 255 / diff; outCoef := outDiff / 255; exponent := 1 / Item.Midtones; for i := 0 to 255 do begin // 计算输入色阶黑白场 if Table[i] <= Item.Shadow then v := 0 else begin v := Round((Table[i] - Item.Shadow) * coef); if v > 255 then v := 255; end; // 计算输入色阶灰场 v := Round(Power(v / 255, exponent) * 255); // 计算输出色阶 Table[i] := Round(v * outCoef + Item.OutShadow); end; end; // 如果色阶通道数据clData参数非初始值,计算所有通道色阶表并返回真 function _CheckColorLevelData(const clData: TColorLevelData; var Tables: array of TGrayTable): Boolean; type PColorLevels = ^TColorLevels; TColorLevels = array[0..2] of TColorLevelItem; var i, j: Integer; begin Result := False; for i := 0 to 2 do // 初始化R、G、B通道色阶表 begin for j := 0 to 255 do Tables[i, j] := j; end; for i := 0 to 2 do // 计算R、G、B通道色阶表 begin if GetColorLevelTable(PColorLevels(@clData)^[i], Tables[i]) then Result := True; end; for i := 0 to 2 do // 计算整个RGB图像色阶表 begin if not GetColorLevelTable(clData.RGB, Tables[i]) then Break; Result := True; end; end; // 初始化色阶通道数据 procedure InitColorLevelData(var clData: TColorLevelData); procedure InitTColorLevelItem(var Item: TColorLevelItem); begin Item.Shadow := 0; Item.Midtones := 1.0; Item.Highlight := 255; Item.OutShadow := 0; Item.OutHighlight := 255; end; begin InitTColorLevelItem(clData.Blue); InitTColorLevelItem(clData.Green); InitTColorLevelItem(clData.Red); InitTColorLevelItem(clData.RGB); end; // 按clData拷贝Source的色阶调整数据到Dest procedure ImageColorLevel(var Dest: TImageData; const Source: TImageData; const clData: TColorLevelData); var Tables: array[0..2] of TGrayTable; begin if _CheckColorLevelData(clData, Tables) then _DoColorLevel(Dest, Source, @Tables) else _DoCopyImageData(Dest, Source); end; procedure TMainForm.AdjustmentImage; begin if not FIsRun then begin FIsRun := True; FAbort := False; ImageColorLevel(FDest, FSource, FLevelData); PaintBox1Paint(nil); FIsRun := False; end else FAbort := True; end; procedure TMainForm.Button1Click(Sender: TObject); begin Close; end; procedure TMainForm.ComboBox1Change(Sender: TObject); var x: Integer; begin FLock := True; x := ComboBox1.ItemIndex; LBar.Position := FLevelItems[x].Shadow; MBar.Position := 46 - Round(Ln(FLevelItems[x].Midtones * 10) * 10); HBar.Position := FLevelItems[x].Highlight; FLock := False; GrayMap.Invalidate; end; procedure TMainForm.FormCreate(Sender: TObject); begin FBitmap := TGpBitmap.Create('..\..\media\source1.jpg'); FSource := LockGpBitmap(FBitmap); FDest := NewImageData(FSource.Width, FSource.Height); InitColorLevelData(FLevelData); FLevelItems[0] := @FLevelData.RGB; FLevelItems[1] := @FLevelData.Red; FLevelItems[2] := @FLevelData.Green; FLevelItems[3] := @FLevelData.Blue; GetGrayInfos; ComboBox1.ItemIndex := 0; AdjustmentImage; end; procedure TMainForm.FormDestroy(Sender: TObject); begin FreeImageData(FDest); UnlockGpBitmap(FBitmap, FSource); FBitmap.Free; end; procedure TMainForm.GetGrayInfos; var i, j: Integer; maxIdx, minIdx: Integer; begin for i := 1 to 3 do begin GetGrayInfo(FSource, FGrayInfos[i], True); for j := 0 to 255 do Inc(FGrayInfos[0].Grays[j], FGrayInfos[i].Grays[j]); Inc(Integer(FSource.Scan0), 1); end; Dec(Integer(FSource.Scan0), 3); maxIdx := 0; minIdx := 0; for i := 0 to 255 do begin FGrayInfos[0].Grays[i] := Round(FGrayInfos[0].Grays[i] / 3); Inc(FGrayInfos[0].Total, FGrayInfos[0].Grays[i] * i); if (FGrayInfos[0].Grays[minIdx] > FGrayInfos[0].Grays[i]) then minIdx := i; if (FGrayInfos[0].Grays[maxIdx] < FGrayInfos[0].Grays[i]) then maxIdx := i; end; FGrayInfos[0].MaxValue := maxIdx; FGrayInfos[0].MinValue := minIdx; FGrayInfos[0].Count := FGrayInfos[1].Count; FGrayInfos[0].Average := FGrayInfos[0].Total div FGrayInfos[0].Count; end; procedure TMainForm.GrayMapPaint(Sender: TObject); const PenColor: array[0..3] of TColor = ($000000, $0000FF, $008000, $FF0000); var I, v, x: Integer; begin x := ComboBox1.ItemIndex; GrayMap.Canvas.Brush.Color := clSkyBlue; GrayMap.Canvas.FillRect(GrayMap.ClientRect); GrayMap.Canvas.Pen.Color := PenColor[x]; for I := 0 to 255 do begin v := Round(FGrayInfos[x].Grays[i] / FGrayInfos[x].Grays[FGrayInfos[x].MaxValue] * GrayMap.Height); GrayMap.Canvas.MoveTo(I, GrayMap.Height); GrayMap.Canvas.LineTo(I, GrayMap.Height - v); end; end; procedure TMainForm.HBarChange(Sender: TObject); begin HLabel.Caption := IntToStr(HBar.Position); if FLock then Exit; if HBar.Position - LBar.Position < 2 then begin FLock := True; HBar.Position := LBar.Position + 2; FLock := False; end; FLevelItems[ComboBox1.ItemIndex].Highlight := HBar.Position; AdjustmentImage; end; procedure TMainForm.LBarChange(Sender: TObject); begin LLabel.Caption := IntToStr(LBar.Position); if FLock then Exit; if HBar.Position - LBar.Position < 2 then begin FLock := True; LBar.Position := HBar.Position - 2; FLock := False; end; FLevelItems[ComboBox1.ItemIndex].Shadow := LBar.Position; AdjustmentImage; end; procedure TMainForm.MBarChange(Sender: TObject); var v: Single; begin v := Power(Exp(1), (MBar.Max - MBar.Position) / 10.0) / 10; MLabel.Caption := Format('%.1f', [v]); if FLock then Exit; FLevelItems[ComboBox1.ItemIndex].Midtones := StrToFloat(MLabel.Caption); AdjustmentImage; end; procedure TMainForm.OHBarChange(Sender: TObject); begin OHLabel.Caption := IntToStr(OHBar.Position); if FLock then Exit; FLevelItems[ComboBox1.ItemIndex].OutHighlight := OHBar.Position; AdjustmentImage; end; procedure TMainForm.OLBarChange(Sender: TObject); begin OLLabel.Caption := IntToStr(OLBar.Position); if FLock then Exit; FLevelItems[ComboBox1.ItemIndex].OutShadow := OLBar.Position; AdjustmentImage; end; procedure TMainForm.PaintBox1Paint(Sender: TObject); var dstBmp, srcBmp: TGpBitmap; g: TGpGraphics; begin g := TGpGraphics.Create(PaintBox1.Canvas.Handle); dstBmp := TGpBitmap.Create(FDest.Width, FDest.Height, FDest.Stride, pf32bppArgb, FDest.Scan0); srcBmp := TGpBitmap.Create(FSource.Width, FSource.Height, FSource.Stride, pf32bppArgb, FSource.Scan0); try g.DrawImage(dstBmp, 0, 0); g.DrawImage(srcBmp, 0, FDest.Height); finally srcBmp.Free; dstBmp.Free; g.Free; end; end; end.
下面是2张运行效果图,第一张效果图绿色通道色阶调整,第二张效果图是RGB输出色阶调整到完全颠倒时的负片图:
PhotoShop中的色阶调整只用了2个滑条分别进行输入、输出色阶调整,我没有这种滑槽组件,只好用了5个滑条组件,不太美观。当然在实用时完全可写一个与PhotoShop类似的元件,并不复杂。
说明:本文章里的灰度计算和色阶调整源代码可以在http://download.csdn.net/detail/maozefa/8323289下载,但其中的输出色阶调整和输入色阶调整一样,是不允许黑白场颠倒的,而且黑白场离差最小允许值是4而不是2,可以按本文代码修正过来。
《Delphi图像处理》系列使用GDI+单元下载地址和说明见文章《GDI+ for VCL基础 -- GDI+ 与 VCL》。
因水平有限,错误在所难免,欢迎指正和指导。邮箱地址:[email protected]
这里可访问《Delphi图像处理 -- 文章索引》。