delphi验证码识别学习之图像的灰度化、二值化及反色

图像的灰度化、二值化和反色是些较简单的图像像素处理过程,我在《GDI+ 在Delphi程序的应用 -- ColorMatrix与图像灰度化》和《GDI+ 在Delphi程序的应用 -- 图像二值化》二篇文章中讲了如何利用GDI+的ColorMatrix实现图像灰度化和二值化,但是那种处理只适合GDI+的图像类,本文的方法同时适用于GDI+图像和Delphi的TGraphic图像。

过程定义:

  // 图像求反
  procedure ImageInvert(var Data: TImageData);
  // 灰度化图像
  procedure ImageGray(var Data: TImageData);
  // 灰度统计。GrayData为灰度统计结构;缺省统计彩色图灰度,
  procedure ImageGrayStat(const Data: TImageData; var GrayData: TGrayStatData;
    IsGrayImage: Boolean = False);
  // 二值化图像。Threshold阀值,需要32位灰度图数据
  procedure ImageTwoValues(var Data: TImageData; Threshold: LongWord = 127);
  // 动态阀值(子图灰度平均值)二值化图像,SubSize子图大小,需要32位灰度图数据
  procedure ImageDynamTwoValues(var Data: TImageData; SubSize: Integer);

实现代码:

procedure ImageInvert(var Data: TImageData);
asm
    push    edi
    push    ebx
    call    IsValid32
    jc      @@Exit
    call    SetDataRegs32
    mov     eax, 0ffffffh
@@yLoop:
    push    ecx
@@xLoop:
    xor     [edi], eax
    add     edi, 4
    loop    @@xLoop
    pop     ecx
    add     edi, ebx
    dec     edx
    jnz     @@yLoop
@@Exit:
    pop     ebx
    pop     edi
end;

procedure ImageGray(var Data: TImageData);
asm
    push    ebp
    push    esi
    push    edi
    push    ebx
    call    IsValid32
    jc      @@Exit
    call    SetDataRegs32
@@yLoop:
    push    ecx
@@xLoop:
    movzx   eax, [edi].TARGBQuad.Blue
    movzx   esi, [edi].TARGBQuad.Green
    movzx   ebp, [edi].TARGBQuad.Red
    imul    eax, 117        // blue   0.114 * 1024
    imul    esi, 601        // green  0.587 * 1024
    imul    ebp, 306        // red    0.299 * 1024
    add     eax, esi
    add     eax, ebp
    add     eax, 512        // Rounding
    shr     eax, 10         // eax = Round((R * 306 + G * 601 + B * 117) / 1024)
    mov     [edi].TARGBQuad.Red, al
    mov     [edi].TARGBQuad.Green, al
    mov     [edi].TARGBQuad.Blue, al
    add     edi, 4
    loop    @@xLoop
    pop     ecx
    add     edi, ebx
    dec     edx
    jnz     @@yLoop
@@Exit:
    pop     ebx
    pop     edi
    pop     esi
    pop     ebp
end;

procedure GrayStat(const Data: TImageData; var GrayData: TGrayStatData; IsGrayImage: Boolean);
asm
    push    ebp
    push    esi
    push    edi
    push    ebx
    mov     edi, edx        // edi = GrayData
    mov     edx, ecx
    mov     esi, [eax].TImageData.Scan0
    mov     ecx, [eax].TImageData.Width
    mov     ebx, [eax].TImageData.Height
    mov     ebp, [eax].TImageData.Stride
    mov     eax, ecx
    shl     eax, 2
    sub     ebp, eax            // ebp = offset
    mov     eax, ecx
    imul    eax, ebx
    push    eax                 // Total pixel count
    push    ecx                 // init Grays
    push    edi
    mov     ecx, 256
    xor     eax, eax
    rep     stosd
    pop     edi
    pop     ecx
    cmp     edx, TRUE
    je      @@yGrayLoop
    // 建立彩色图的灰度数组
@@yLoop:
    push    ecx
@@xLoop:
    movzx   eax, [esi].TARGBQuad.Blue
    movzx   edx, [esi].TARGBQuad.Green
    imul    eax, 117            // blue   0.114 * 1024
    imul    edx, 601            // green  0.587 * 1024
    add     edx, eax
    movzx   eax, [esi].TARGBQuad.Red
    imul    eax, 306            // red    0.299 * 1024
    add     eax, edx
    add     eax, 512            // Rounding
    shr     eax, 10             // eax = Round((R * 306 + G * 601 + B * 117) / 1024)
    inc     dword ptr [edi].TGrayStatData.Grays[eax*4]// grayData.Grays[eax] ++
    add     esi, 4              // esi += 4
    loop    @@xLoop
    pop     ecx
    add     esi, ebp
    dec     ebx
    jnz     @@yLoop
    jmp     @@SumStart
    // 建立灰度图的灰度数组
@@yGrayLoop:
    push    ecx
@@xGrayLoop:
    movzx   eax, [esi].TARGBQuad.Blue// eax = Gray = *esi
    inc     dword ptr [edi].TGrayStatData.Grays[eax*4]// grayData.Grays[eax] ++
    add     esi, 4              // esi +=4
    loop    @@xGrayLoop
    pop     ecx
    add     esi, ebp
    dec     ebx
    jnz     @@yGrayLoop
    // 计算总的灰度值、最大灰度值及最小灰度值
@@SumStart:
    push    edi
    xor     eax, eax            // edx:eax = 0 (GrayData.Total)
    xor     edx, edx
    mov     esi, edi            // esi = ebx = &GrayData.Grays[0]
    mov     ebx, edi
    xor     ecx, ecx            // for (index = 0; index < 256; index ++)
@@SumLoop:                      // {
    mov     ebp, [edi]
    cmp     [esi], ebp
    jae     @@1
    mov     esi, edi            //   if (*esi < *edi) esi = edi
@@1:
    cmp     [ebx], ebp
    jbe     @@2
    mov     ebx, edi            //   if (*ebx > *edi) ebx = edi
@@2:
    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              // ebx = (ebx - &GrayData.Grays[0]) / 4
    mov     [edi].TGrayStatData.MinGray, ebx// GrayData.MinGray = ebx
    sub     esi, edi
    shr     esi, 2              // esi = (esi - &GrayData.Grays[0]) / 4
    mov     [edi].TGrayStatData.MaxGray, esi// GrayData.MaxGray = esi
    pop     ebx
    mov     [edi].TGrayStatData.Count, ebx // GrayData.Count = data.Width * data.Height
    mov     dword ptr [edi].TGrayStatData.Total, eax // GrayData.Total = edx:eax
    mov     dword ptr [edi].TGrayStatData.Total+4, edx
    mov     ecx, ebx
    shr     ecx, 1
    add     eax, ecx
    adc     edx, 0
    idiv    ebx
    // GrayData.Average = (GrayData.Total + GrayData.Count / 2) / GrayData.Count)
    mov     [edi].TGrayStatData.Average, eax
@@Exit:
    pop     ebx
    pop     edi
    pop     esi
    pop     ebp
end;

procedure ImageGrayStat(const Data: TImageData;
  var GrayData: TGrayStatData; IsGrayImage: Boolean);
begin
  if not ImageEmpty(Data) then
    GrayStat(Data, GrayData, IsGrayImage);
end;

procedure TwoValues(var Data: TImageData; Threshold: LongWord);
asm
    push    ebp
    push    esi
    push    edi
    push    ebx
    push    edx
    call    SetDataRegs32
    pop     eax
    mov     esi, 000ffffffh
    mov     ebp, esi
    not     ebp
@@yLoop:
    push    ecx
@@xLoop:
    cmp     [edi], al
    jb      @@1
    or      [edi], esi
    jmp     @@2
@@1:
    and     [edi], ebp
@@2:
    add     edi, 4
    loop    @@xLoop
    pop     ecx
    add     edi, ebx
    dec     edx
    jnz     @@yLoop
@@Exit:
    pop     ebx
    pop     edi
    pop     esi
    pop     ebp
end;

procedure ImageTwoValues(var Data: TImageData; Threshold: LongWord);
begin
  if not ImageEmpty(Data) then
    TwoValues(Data, Threshold);
end;

procedure ImageDynamTwoValues(var Data: TImageData; SubSize: Integer);
var
  Sub: TImageData;
  GrayData: TGrayStatData;
  x, y: Integer;
begin
  if ImageEmpty(Data) then Exit;
  if SubSize <= 0 then
  begin
    GrayStat(Data, GrayData, True);
    TwoValues(Data, GrayData.Average);
    Exit;
  end;
  y := 0;
  while y < Data.Height do
  begin
    x := 0;
    while x < Data.Width do
    begin
      Sub := GetSubData(Data, x, y, SubSize, SubSize);
      GrayStat(Sub, GrayData, True);
      TwoValues(Sub, GrayData.Average);
      Inc(x, SubSize);
    end;
    Inc(y, SubSize);
  end;
end;

灰度化过程还是依照大多数图像灰度处理惯例,计算YUV颜色空间的Y分量作为灰度图,公式为:

Y = 0.299 * R + 0.587 * G + 0.114 * B

本文灰度过程使用了定点数处理,将上面公式中的常数乘上1024,加快了处理过程,伪代码为:

Y = (306 * R + 601 * G + 117 * B + 512) >> 10

代码中的+512是做四舍五入,右移10位等于除以1024。

图像灰度统计过程和图像灰度化过程采用了相同的原理和计算过程,只不过没有改变图像,而是以计算结果作为256色灰度阶数组的下标,增加该灰度阶的个数而已。所有图像灰度统计指标都存放在TGrayStatData类型的结构中。见《Delphi图像处理 -- 数据类型及内部过程》。

图像二值化过程是在图像灰度处理基础上进行的,由于R、G、B三个分量相等,所以只要把任何其中一个与阀值比较即可:大于阀值为255,否则为0。因灰度图像素格式是32位的,所以过程中直接以0x00FFFFFF或RGB三个分量为255,以0xFF000000与RGB三个分量为0,图像二值化的黑白效果取决于阀值的大小。

因有些图像的灰度分布不太均匀,为了加强图像的二值特征,本文尝试写了一个图像动态分组二值化过程ImageDynamTwoValues,即将图像分组为一定大小的子图,对各子图分别进行灰度统计后,以该子图的灰度平均值为阀值进行子图的二值化,不过在测试过程中,发现如果子图尺寸确定的不合适,各子图之间有很明显的区别,这对图像的二值分析显然是不利的。

至于图像的反色处理更简单,直接用0xFFFFFF和RGB异或就成。

图像动态分组二值化例子:

var
  jpg: TJPEGImage;
  Data: TImageData;
  GrayData: TGrayStatData;
begin
  jpg := TJPEGImage.Create;
  jpg.LoadFromFile('D:\VclLib\GdiplusDemo\Media\20041001.jpg');
  Canvas.Draw(0, 0, jpg);
  Data := GetImageData(jpg);
  ImageGray(Data);
//  ImageTwoValues(Data);
  ImageDynamTwoValues(Data, 128);
  ImageDataAssignTo(Data, jpg);
  Canvas.Draw(0, 0, jpg);
  FreeImageData(Data);
  jpg.Free;
end;