阅读提示:
《Delphi图像处理》系列以效率为侧重点,一般代码为PASCAL,核心代码采用BASM。
《C++图像处理》系列以代码清晰,可读性为主,全部使用C++代码。
尽可能保持二者内容一致,可相互对照。
本文代码必须包括文章《Delphi图像处理 -- 数据类型及公用过程》中的ImageData.pas单元。
常用图像的灰度化方法有最大值法、平均值法和YUV法。最大值法是取像素R、G、B分量中的最大值为灰度值;平均值法是以像素R、G、B分量的平均值作为灰度值;而YUV法则是根据YUV颜色空间原理,以亮度值Y来作为像素的灰度值,其计算公式为:
Y = 0.299 * R + 0.587 * G + 0.114 * B
本文的图像灰度化过程以这个公式来计算像素灰度值。
图像灰度化和灰度直方图统计过程代码如下:
type
// 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;
// 通道灰度信息结构
PChannelsInfo = ^TChannelsInfo;
TChannelsInfo = packed record
RGB: TImageGrayInfo;
Red: TImageGrayInfo;
Green: TImageGrayInfo;
Blue: TImageGrayInfo;
end;
const
GRAY_MMX: TMMType = (3735, 19235, 9798, 0); // [0.114,0.587,0.229] * 32768
// 图像数据Source灰度拷贝到Dest
procedure ImageGray(var Dest: TImageData; const Source: TImageData); overload;
asm
push esi
push edi
push ebx
call _SetCopyRegs
pxor xmm7, xmm7
pcmpeqd xmm6, xmm6
psllq xmm6, 48
movq xmm2, GRAY_MMX
movlhps xmm2, xmm2
test ecx, 1
jz @@yLoop
add ebx, 4
@@yLoop:
push ecx
shr ecx, 1
jz @@1
@@xLoop:
movq xmm0, [esi]
punpcklbw xmm0, xmm7
movaps xmm3, xmm0
pand xmm3, xmm6
pmaddwd xmm0, xmm2
movaps xmm1, xmm0
psrlq xmm1, 32
paddd xmm0, xmm1
psrlq xmm0, 15
pshuflw xmm0, xmm0, 11000000b
pshufhw xmm0, xmm0, 11000000b
por xmm0, xmm3
packuswb xmm0, xmm7
movq [edi], xmm0
add esi, 8
add edi, 8
loop @@xLoop
@@1:
pop ecx
test ecx, 1
jz @@next
movd xmm0, [esi]
punpcklbw xmm0, xmm7
movaps xmm3, xmm0
pand xmm3, xmm6
pmaddwd xmm0, xmm2
movaps xmm1, xmm0
psrlq xmm1, 32
paddd xmm0, xmm1
psrld xmm0, 15
pshuflw xmm0, xmm0, 11000000b
por xmm0, xmm3
packuswb xmm0, xmm7
movd [edi], xmm0
@@next:
add esi, eax
add edi, ebx
dec edx
jnz @@yLoop
pop ebx
pop edi
pop esi
end;
// 图像数据灰度化
procedure ImageGray(var Data: TImageData); overload;
begin
ImageGray(Data, Data);
end;
// 获取图像灰度信息
function ImageGetGrayInfo(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
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;
// 获取图像通道灰度信息
function ImageGetChannelsInfo(const Data: TImageData; var ChannelsInfo: TChannelsInfo): Integer;
asm
push esi
push edi
push ebx
mov ebx, eax
mov edi, edx
mov esi, edi
xor eax, eax
mov ecx, 256+6
rep stosd
mov ecx, 3
add [ebx].TImageData.Scan0, ecx
@@statLoop:
push ecx
push esi
dec [ebx].TImageData.Scan0
mov ecx, True
mov edx, edi
mov eax, ebx
call ImageGetGrayInfo
mov ecx, 256
@@SumLoop:
mov eax, [edi]
add [esi], eax
add esi, 4
add edi, 4
loop @@SumLoop
add edi, 6*4
pop esi
pop ecx
loop @@statLoop
mov eax, [ebx].TImageData.Width
mul [ebx].TImageData.Height
mov [esi].TImageGrayInfo.Count, eax
push esi
push ebp
xor eax, eax
xor edx, edx
mov edi, esi
mov ebp, esi
mov ecx, 3
push ecx
xor ecx, ecx
@@calcLoop:
fild dword ptr[ebp] // grayData[0].Grays[j] /= 3
fidiv dword ptr[esp]
fistp dword ptr[ebp]
fwait
mov ebx, [ebp]
cmp [esi], ebx
cmovb esi, ebp
cmp [edi], ebx
cmova edi, ebp
imul ebx, ecx
add eax, ebx // total += grayData[0].Grays[j]
adc edx, 0
add ebp, 4
inc ecx
cmp ecx, 256
jb @@calcLoop
pop ecx
pop ebp
pop ebx
sub esi, ebx
sub edi, ebx
shr esi, 2
shr edi, 2
mov [ebx].TImageGrayInfo.MaxValue, esi
mov [ebx].TImageGrayInfo.MinValue, edi
mov dword ptr[ebx].TImageGrayInfo.Total, eax
mov dword ptr[ebx].TImageGrayInfo.Total+4, edx
div [ebx].TImageGrayInfo.Count
mov [ebx].TImageGrayInfo.Average, eax
@@Exit:
pop ebx
pop edi
pop esi
end;
本文灰度化过程和灰度直方图统计过程在计算RGB灰度时使用了定点数处理,将前面公式中的常数乘扩大了32768倍;同时在图像灰度化过程使用了SSE指令,每次处理2个像素,大大加快了图像灰度化执行效率。
灰度直方图统计过程有2个,ImageGetGrayInfo过程是对整个图像进行灰度数据统计,并计算出最大、最小灰度值以及平均灰度值;而ImageGetChannelsInfo过程则是分别调用ImageGetGrayInfo过程统计出R、G、B各通道的灰度信息,然后在R、G、B通道灰度直方图统计基础上再进行全图的RGB灰度直方图统计,这个RGB灰度直方图统计结果与ImageGetGrayInfo结果是不一样的,后者是直接分类统计图像的灰度值数量,而前者的灰度信息则是在R、G、B通道灰度分类统计的基础上平均得来的。
下面是一个简单的界面例子程序代码和运行界面图:
unit main;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, ExtCtrls;
type
TForm1 = class(TForm)
PaintBox1: TPaintBox;
PaintBox2: TPaintBox;
PaintBox3: TPaintBox;
PaintBox4: TPaintBox;
PaintBox5: TPaintBox;
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure PaintBox1Paint(Sender: TObject);
procedure PaintBox2Paint(Sender: TObject);
private
{ Private declarations }
Bmp: TBitmap;
GrayBmp: TBitmap;
ChannelsInfo: TChannelsInfo;
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
uses JPEG, ImageData;
{$R *.dfm}
procedure TForm1.FormCreate(Sender: TObject);
var
jpeg: TJPEGImage;
data: TImageData;
begin
// 装入图像到Bmp
Bmp := TBitmap.Create;
jpeg := TJPEGImage.Create;
try
jpeg.LoadFromFile('..\..\media\source1.jpg');
Bmp.Assign(jpeg);
finally
jpeg.Free;
end;
// 获取Bmp的通道灰度信息
data := GetBitmapData(Bmp);
ImageGetChannelsInfo(data, ChannelsInfo);
// 建立新的位图GrayBmp并灰度化
GrayBmp := TBitmap.Create;
GrayBmp.Assign(Bmp);
data := GetBitmapData(GrayBmp);
ImageGray(data);
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
GrayBmp.Free;
Bmp.Free;
end;
procedure TForm1.PaintBox1Paint(Sender: TObject);
begin
PaintBox1.Canvas.Draw(0, 0, GrayBmp); // 画灰度图
PaintBox1.Canvas.Draw(0, GrayBmp.Height, Bmp);// 画源图
end;
procedure TForm1.PaintBox2Paint(Sender: TObject);
const
PenColor: array[0..3] of TColor = ($000000, $0000FF, $008000, $FF0000);
var
I, v: Integer;
PInfo: PImageGrayInfo;
begin
PInfo := @ChannelsInfo.RGB; // 灰度信息指针指向RGB通道
with TPaintBox(Sender) do
begin
Inc(PInfo, Tag); // 按预设的TPaintBox控件属性Tag指向相应通道
Canvas.Brush.Color := clSkyBlue;
Canvas.FillRect(ClientRect); // 填充蓝灰色背景
Canvas.Pen.Color := PenColor[Tag];// 按Tag获取通道直方图画笔颜色
for I := 0 to 255 do // 按256级灰度和TPaintBox高度比例画直方图
begin
v := Round(PInfo^.Grays[I] / PInfo^.Grays[PInfo^.MaxValue] * Height);
Canvas.MoveTo(I, Height);
Canvas.LineTo(I, Height - v);
end;
end;
end;
end.
运行界面图:
界面左下是原图,左上是灰度图,而右边是通道灰度直方图,从上到下分别为RGB通道、红色通道、绿色通道和蓝色通道直方图。
《Delphi图像处理》系列使用GDI+单元下载地址和说明见文章《GDI+ for VCL基础 -- GDI+ 与 VCL》。
因水平有限,错误在所难免,欢迎指正和指导。邮箱地址:maozefa@hotmail.com
这里可访问《Delphi图像处理 -- 文章索引》。