Delphi图像处理 -- 中值滤波(灰度分组统计法)

阅读提示:

    《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图像处理 -- 文章索引》。

 

  • 6
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 8
    评论
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值