阅读提示:
《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》。
因水平有限,错误在所难免,欢迎指正和指导。邮箱地址:maozefa@hotmail.com
这里可访问《Delphi图像处理 -- 文章索引》。