阅读提示:
《Delphi图像处理》系列以效率为侧重点,一般代码为PASCAL,核心代码采用BASM。
《C++图像处理》系列以代码清晰,可读性为主,全部使用C++代码。
尽可能保持二者内容一致,可相互对照。
本文代码必须包括文章《Delphi图像处理 -- 数据类型及公用过程》中的ImageData.pas单元。
这是《Delphi图像处理 -- 中值滤波》一文的改进版。亦可参见《C++图像处理 -- 中值滤波》。
中值滤波是图像处理中常用的一种噪声滤波方法。传统的图像中值滤波代码采用排序方法实现,处理速度主要取决于排序算法,但无论什么排序算法,总离不开大量的元素比较、交换或移动,而这些恰好是当前计算机处理的“弱项”(有经验的程序员都知道,计算机数据处理中,比较、转移、交换和频繁的数据移动比直接的算术运算和逻辑运算耗时多了),再加上没有一种好的排序算法能同时适应不同滤波半径的数据排序速度,所以在传统中值滤波实现代码中多使用选择排序、冒泡排序或者直接排序等简单排序算法,高级点的如快速排序法用在中值滤波代码中往往会使处理速度更慢。对于半径为1的中值滤波倒是有一种较好的排序算法,我在《Delphi图像处理 -- 中值滤波》一文中实现过,处理速度还是较快的。
既然排序过程是图像中值滤波处理的瓶颈,能不能抛开它,用其它手段实现呢?这就是本文要探讨的问题。有朋友可能会有疑问,不排序怎么获取中间值呢,是否采用网上有些文章介绍的近似值来代替?不,本文介绍的方法决不是近似中间值,而是的的确确的“精确”中间值。
图像中值滤波中的中间值。在统计学中叫做中位数,是平均数指标的一种。平均数指标按统计的复杂程度可分为简单平均数和加权平均数,所谓简单平均数就是对统计总体的每个个体进行累计计算后求得;而加权平均数则是先对统计总体的所有个体进行分组,然后以各个组的大小作为权数后进行累计计算所得。中位数既然是平均数指标的一种,当然也存在两种计算方法。加权中位数和加权算术平均数的区别在于后者是对各个分组用权数相乘进行累积后除以总体个数,而前者只需要对各组权数进行累积后除以2,累积权数大于或等于这个数的组的值就是中位数。传统中值滤波实现代码中采用的排序手段实质就是简单中位平均数统计方法,而本文要介绍的是采用分组加权的方法来获取中位平均数,即中值滤波的中间值。
采用分组加权统计方法的前提条件是对无限统计总体进行有限的分组,例如,人口年龄统计,就是首先确定各个年龄段,这个年龄段可以是一岁、五岁、十岁等。而图像像素的R、G、B值在0 -- 255之间,正好符合有限分组的前提条件。说到这里,很多人可能已经明白我要表达的意思了:
1、按R、G、B分别定义一个256大小的灰度统计数据;
2、将图像像素总体的每个个体像素的R、G、B值进行归类;
3、确定累积中间值权数的大小;
4、从数组左端或者右端开始权数累计,累积到大于或等于中间值权数的那个灰度组就是中间值!
从前面几个步骤不难看出,前3个步骤就是图像处理中灰度统计的分类方法,第4个累积步骤与灰度统计累积计算有2个不同点,一是灰度统计累积的是权数*灰度,而这里只累积灰度;二是灰度统计累积需要全部完成,而中值累积只要达到中间值权数那个组就可以终止。在这4个步骤中,前3个步骤是相当快的(实际上只是第二个步骤),因为其中既无乘除运算,也没有比较转移,更没有元素交换或移动等动作。而制约数据处理速度的瓶颈就在第4步,因为对每个像素都必须分别按R、G、B通道对一共768个元素大小的数据进行累积,哪怕是每个通道除了2个加法运算和唯一的一次比较判断外,没有其它运算,也不需要每次都必须累积到位,但仍然是比较耗时的,不过本文在代码实现过程中,尽可能地作了一些弥补。最后结果同排序方法比起来,可算是相当快捷了,而且滤波半径越大,差距越明显,几十倍的差距绝不是天方夜谭!就算是同我前面所说的半径为1的改进排序算法比较来,大多数情况下,也略有胜出。
procedure MedianValue(var Dest: TImageData; const Source: TImageData;
MedianGray, Size, Stride: Integer);
var
buffer: array[0..767] of Integer;
redAddr, greenAddr, blueAddr, delta: Integer;
redOff, greenOff, blueOff: Integer;
width, height, dstOffset, srcOffset: Integer;
median, rowOffset, sizeOffset, mOffset: Integer;
asm
push esi
push edi
push ebx
push ecx
call _SetCopyRegs
mov width, ecx
mov height, edx
mov dstOffset, ebx
add eax, 4
mov srcOffset, eax
pop ecx
mov eax, Size
mov edx, Stride
mov ebx, edx
imul edx, eax
shl eax, 2
mov sizeOffset, eax // sizeOffset = Size * 4
sub ebx, eax
mov rowOffset, ebx // rowOffset = Stride - Size * 4
sub edx, 4
mov mOffset, edx // mOffset = Stride * Size - 4
mov eax, Size
mul Size
inc eax
shr eax, 1
mov median, eax // median = (size * size + 1) / 2
mov blueAddr, 0
mov greenAddr, 256*4
mov redAddr, 512*4
cmp ecx, 128
jb @@1
mov blueOff, 256*4
mov greenOff, 512*4
mov redOff, 768*4
mov delta, -4
jmp @@2
@@1:
mov blueOff, -4
mov greenOff, 256*4-4
mov redOff, 512*4-4
mov delta, 4
@@2:
lea eax, buffer
add blueAddr, eax
add greenAddr, eax
add redAddr, eax
add blueOff, eax
add greenOff, eax
add redOff, eax
@@yLoop:
push width
// RGB灰度统计缓冲区清零
push edi
mov edi, blueAddr
mov ecx, 768
xor eax, eax
rep stosd
pop edi
// 对每行第一个像素临近区域(Size*Size)的RGB进行灰度统计
push esi
mov ecx, Size
@@statY:
push ecx
mov ecx, Size
@@statX:
movzx eax, [esi].TARGBQuad.Blue
movzx edx, [esi].TARGBQuad.Green
movzx ebx, [esi].TARGBQuad.Red
inc dword ptr buffer[eax*4]
inc dword ptr buffer[edx*4+256*4]
inc dword ptr buffer[ebx*4+512*4]
add esi, 4
loop @@statX
pop ecx
add esi, rowOffset
loop @@statY
pop esi
jmp @@setValue
@@xLoop:
// 剔除上个坐标点最左边一列像素的RGB的统计值,
// 追加当前像素最右边一列像素RGB的统计值
mov ecx, Size
mov ebx, sizeOffset
add ebx, esi
@@subLoop:
movzx eax, [esi].TARGBQuad.Blue
movzx edx, [esi].TARGBQuad.Green
dec dword ptr buffer[eax*4]
dec dword ptr buffer[edx*4+256*4]
movzx eax, [esi].TARGBQuad.Red
movzx edx, [ebx].TARGBQuad.Blue
dec dword ptr buffer[eax*4+512*4]
inc dword ptr buffer[edx*4]
movzx eax, [ebx].TARGBQuad.Green
movzx edx, [ebx].TARGBQuad.Red
inc dword ptr buffer[eax*4+256*4]
inc dword ptr buffer[edx*4+512*4]
add esi, Stride
add ebx, Stride
loop @@subLoop
sub esi, mOffset
@@setValue:
mov edx, median
mov ecx, delta
mov ebx, blueOff
xor eax, eax
@@blueLoop:
add ebx, ecx
add eax, [ebx]
cmp eax, edx
jb @@blueLoop
sub ebx, blueAddr
shr ebx, 2
mov [edi].TARGBQuad.Blue, bl
mov ebx, greenOff
xor eax, eax
@@greenLoop:
add ebx, ecx
add eax, [ebx]
cmp eax, edx
jb @@greenLoop
sub ebx, greenAddr
shr ebx, 2
mov [edi].TARGBQuad.Green, bl
mov ebx, redOff
xor eax, eax
@@redLoop:
add ebx, ecx
add eax, [ebx]
cmp eax, edx
jb @@redLoop
sub ebx, redAddr
shr ebx, 2
mov [edi].TARGBQuad.Red, bl
add edi, 4
dec width
jnz @@xLoop
@@xEnd:
add esi, srcOffset
add edi, dstOffset
pop width
dec height
jnz @@yLoop
pop ebx
pop edi
pop esi
end;
function GetMedianGray(const Data: TImageData): Integer;
var
buffer: array[0..255] of Integer;
asm
push esi
push edi
push ebx
mov esi, eax
lea edi, buffer
mov ecx, 256
xor eax, eax
rep stosd
mov ecx, [esi].TImageData.Width
mov edx, [esi].TImageData.Height
mov ebx, [esi].TImageData.Stride
mov edi, [esi].TImageData.Scan0
shr ecx, 3
shr edx, 3
sal ebx, 3
mov eax, ecx
shl eax, 3+2
sub ebx, eax
lea esi, buffer
push ebp
mov ebp, edx
mov eax, ecx
mul ebp
lea eax, [eax+eax*2]
shr eax, 1 // MedianValue = data.width * data.height * 3 / 2
push eax
@@yLoop:
push ecx
@@xLoop:
movzx eax, [edi].TARGBQuad.Blue
movzx edx, [edi].TARGBQuad.Green
inc dword ptr [esi+eax*4]
inc dword ptr [esi+edx*4]
movzx eax, [edi].TARGBQuad.Red
inc dword ptr [esi+eax*4]
add edi, 32
dec ecx
jg @@xLoop
pop ecx
add edi, ebx
dec ebp
jg @@yLoop
pop edx
pop ebp
mov edi, esi
add edi, 256*4
mov ecx, -4
xor eax, eax
@@stat:
add edi, ecx
add eax, [edi]
cmp eax, edx
jb @@stat
mov eax, edi
sub eax, esi
shr eax, 2
pop ebx
pop edi
pop esi
end;
procedure ImageMedianValue(var Data: TImageData; Radius: Integer);
var
exp: TImageData;
begin
if Radius <= 0 then Radius := 1;
exp := _GetExpandData(Data, Radius);
MedianValue(Data, exp, GetMedianGray(Data), (Radius shl 1) + 1, exp.Stride);
FreeImageData(exp);
end;
实现代码中,在前面所说的4个步骤基础上作了3点完善:
1、并非对图像的所有像素都按4个步骤进行。除了每行行首像素作了完整的分类统计外,其它像素只是从统计数组中剔除前一像素临近值的最左边一列和追加当前像素临近值的最右边一列,这无疑加快了分类统计速度,而且滤波半径越大,效果越明显。
2、在对图像进行滤波处理前,对整个图像像素总体的1/8样本求了一次中间值权数,其作用是确定像素分类后的累积方向,如果这个总体中间值权数小于128,从灰度统计数据左边(低端)开始累积,否则则从灰度统计数据右边(高端)开始累积。通过这个总体中间值权数就可以大致确定该图像处理速度的快慢,如果这个值接近两端,处理速度相对就快些,反之如靠近128附近,处理速度则相应会慢些,同样大小的图片,因这个原因,可能造成成倍的差距,但是最坏的情况下,对灰度统计数组累积时的平均值也不会超过一半。
3、制作滤波备份源图时按滤波半径对图像边缘作了扩充,这样有利于对图像边缘进行滤波处理。有些人在写中值滤波代码时往往对图像边缘略过不作处理,这在小半径滤波范围内还无所谓,但是对于较大半径的滤波处理后,由于未进行滤波处理的边缘较宽。会使得图像很难看。
正式由于第2点中所说的差距原因,本文也不好给出具体的测试数据,但有一点是肯定的:同等条件下(语言、程序员水平及编译器优化程度等),比传统排序法实现的中值滤波要快不少,滤波半径越大,差距越大!
最后还是贴个中值滤波测试代码和效果图:前面是源图,后面是50半径中值滤波图:
procedure TForm1.Button3Click(Sender: TObject);
var
bmp: TGpBitmap;
g: TGpGraphics;
data: TImageData;
begin
bmp := TGpBitmap.Create('..\media\source.jpg');
g := TGpGraphics.Create(Canvas.Handle);
g.DrawImage(bmp, 0, 0);
data := LockGpBitmap(bmp);
ImageMedianValue(data, 50);
UnlockGpBitmap(bmp, data);
g.DrawImage(bmp, data.Width, 0);
g.Free;
bmp.Free;
end;
《Delphi图像处理》系列使用GDI+单元下载地址和说明见文章《GDI+ for VCL基础 -- GDI+ 与 VCL》。
因水平有限,错误在所难免,欢迎指正和指导。邮箱地址:maozefa@hotmail.com
这里可访问《Delphi图像处理 -- 文章索引》。