Delphi 汇编学习(一)--- 图像灰值化

17 篇文章 4 订阅
15 篇文章 2 订阅

使用 Delphi 多年了,虽然看过 Delphi 中的汇编代码,但基本没有写过汇编代码。因为没有什么项目需求。
今天因为要优化老程序的一段代码,到网上搜索了一下。发现很多都是汇编写的。效率也的确很高。
这让我有了学习汇编的冲动。

汇编代码一般都是在对效率要求比较高的场合才会使用到,比如多媒体、3D、游戏、视频领域。
虽然我不是做相关的行业的。但学习学习总没有坏处。

学习的最好办法是看/写例子。越简单越好。
所以以 RGB2Gray 下手。图片 RGB 转 灰度图。
网络上这样的代码很多,都是用 C/C++ 写的。
我打算用 Delphi 来写。边阅读 C/C++ 代码,边学习。
尽量保持简单。代码少,容易理解。

图片统一解码为 32位,位图格式;JPEG 图片用 GDI+ 解码;因为它效率最好。
测试图片为 4096*4096*32;下面的时间是在我的机器上的运行时间。
Delphi 的 Release 模式是有优化的,Debug 是没有的;下面的时间,都是在我的机器上,DEBUG 模式下的运行用时。
解码图片单元见 db.Image.Load.pas 单元;
GetBitsPointer 等函数(包含知识点:类的私有变量、保护变量的获取)和一些常量的定义都放在 db.Image.Common.pas 单元里面。
灰度变换函数都放在 db.Image.Gray.pas 单元里面;
汇编代码都封装在独立的函数里面。这样很容易扩展至X64平台。

基本原理
  Gray = (R + G + B) / 3 = (R + G + B) * 85  div  255 = (R + G + B) * 85 >>  8
  Gray = R*0.299 + G*0.587 + B*0.114
  Gray = R,G,B 中的最大值

定点优化:
  Gray = (R*77   + G*151  + B*28)   >> 8
  Gray = (R*$4D + G*$97 + B*$1C) >> 8

查表优化:
  R、G、B,都在 0---255 之间,可以将 R(0---255)*77、G(0---255)*151、B(0---255)*28 预先计算好,存放在常量表中,优化掉乘法。
  灰度值也在 0---255之间,一不做二不休,将求灰度的RGB(Gray, Gray, Gray), 也做成常量表。

RGB2Gray 要实现的方法有很多种。我一一写出来。因为还在学习中,有的暂时还不会写。还望高手指点。
type
  TGrayType = (gtAPI,  gtScanLine,  gtDelphi,  gtFourPoint, gtParallel,
  gtGDIPLUS,  gtTable,  gtASM,  gtMMX,  gtSSE,  gtAVX,  gtAVX512,  gtGPU,  gtOther);

看字面就很容易理解。API方法、Scanline方法,等等;
函数定义(默认使用 SSE 优化函数,因为现代电脑,估计没有哪个电脑不支持 SSE 指令的了):

procedure Gray(bmp: TBitmap; const gt: TGrayType = gtSSE);
begin
  case gt of
    gtAPI:
      Gray_API(bmp);
    gtScanLine:
      Gray_ScanLine(bmp);
    gtDelphi:
      Gray_Delphi(bmp);
    gtFourPoint:
      Gray_FourPoint(bmp);
    gtParallel:
      Gray_Parallel(bmp);
    gtGDIPLUS:
      Gray_GDIPLUS(bmp);
    gtTable:
      Gray_Table(bmp);
    gtASM:
      Gray_ASM(bmp);
    gtMMX:
      Gray_MMX(bmp);
    gtSSE:
      Gray_SSE(bmp);
    gtAVX:
      Gray_AVX(bmp);
    gtAVX512:
      Gray_AVX512(bmp);
    gtGPU:
      Gray_GPU(bmp);
    gtOther:
      Gray_Other(bmp);
  end;
end;

下面一一实现这些函数。

01、Gray_API(gtAPI):用 WINDOWS API 的方法,获取图像内存区。进行灰度变换。再写回内存区;用时 219 毫秒。

{ 219 ms }
procedure Gray_API(bmp: TBitmap);
var
  I, Count: Cardinal;
  pColor  : PRGBQuad;
  byeGray : Byte;
begin
  Count := bmp.Width * bmp.Height;
  GetMem(pColor, Count * 4);
  try
    GetBitmapBits(bmp.Handle, Count * 4, pColor);
    for I := 0 to Count - 1 do
    begin
      byeGray := Round(0.299 * pColor^.rgbRed + 0.587 * pColor^.rgbGreen + 0.114 * pColor^.rgbBlue);
      pColor^ := TRGBQuad(RGB(byeGray, byeGray, byeGray));
      Inc(pColor);
    end;
    Dec(pColor, Count);
    SetBitmapBits(bmp.Handle, Count * 4, pColor);
  finally
    FreeMem(pColor);
  end;
end;

02、Gray_ScanLine(gtScanLine):用 ScanLine 的方法,获取图像内存区。进行灰度变换;想比较于 Gray_API ,优化掉了内存读写和使用了定点优化。速度提高不少。用时 122 毫秒;

{ 122 ms }
procedure Gray_ScanLine(bmp: TBitmap);
var
  I, J   : Integer;
  pColor : PRGBQuad;
  byeGray: Byte;
begin
  for I := 0 to bmp.Height - 1 do
  begin
    pColor := bmp.ScanLine[I];
    for J  := 0 to bmp.Width - 1 do
    begin
      byeGray := (77 * pColor^.rgbRed + 151 * pColor^.rgbGreen + 28 * pColor^.rgbBlue) shr 8;
      pColor^ := TRGBQuad(RGB(byeGray, byeGray, byeGray));
      Inc(pColor);
    end;
  end;
end;

03、Gray_Delphi(gtDelphi):用 Delphi 对象的方法,直接获取图像到内存区。相对于 Gray_Scanline 优化了循环。将两层循环优化成了一层。用时 86 毫秒;

{ 86 ms }
procedure Gray_Delphi(bmp: TBitmap);
var
  I, Count: Integer;
  pColor  : PRGBQuad;
  byeGray : Byte;
begin
  Count  := bmp.Width * bmp.Height;
  pColor := GetBitsPointer(bmp);
  for I  := 0 to Count - 1 do
  begin
    byeGray := (77 * pColor^.rgbRed + 151 * pColor^.rgbGreen + 28 * pColor^.rgbBlue) shr 8;
    pColor^ := TRGBQuad(RGB(byeGray, byeGray, byeGray));
    Inc(pColor);
  end;
end;

04、Gray_FourPoint(gtFourPoint):同时4点进行运算。效率好像没有多少提高。用时 90 毫秒;

{ 90 ms }
procedure Gray_FourPoint(bmp: TBitmap);
var
  I, J, Count: Integer;
  pColor     : PRGBQuad;
  byeGray    : Byte;
begin
  Count  := bmp.Width * bmp.Height;
  pColor := GetBitsPointer(bmp);
  for I  := 0 to Count div 4 - 1 do
  begin
    for J := 0 to 3 do
    begin
      byeGray := (77 * pColor^.rgbRed + 151 * pColor^.rgbGreen + 28 * pColor^.rgbBlue) shr 8;
      pColor^ := TRGBQuad(RGB(byeGray, byeGray, byeGray));
      Inc(pColor);
    end;
  end;
end;

05、Gray_Parallel(gtParallel):并行优化。注意:需要脱离 IDE 执行 和 ScanLine 不能用于 TParallel.For 中。用时 45 毫秒;

{ 45 ms }
procedure Gray_Parallel(bmp: TBitmap);
var
  StartScanLine: Integer;
  bmpWidthBytes: Integer;
begin
  StartScanLine := Integer(bmp.ScanLine[0]);
  bmpWidthBytes := Integer(bmp.ScanLine[1]) - Integer(bmp.ScanLine[0]);

  TParallel.For(0, bmp.Height - 1,
    procedure(Y: Integer)
    var
      X: Integer;
      pColor: PRGBQuad;
    begin
      pColor := PRGBQuad(StartScanLine + Y * bmpWidthBytes);
      for X := 0 to bmp.Width - 1 do
      begin
        pColor^ := GetPixelGray(pColor^.rgbRed, pColor^.rgbGreen, pColor^.rgbBlue);
        Inc(pColor);
      end;
    end);
end;

06、Gray_GDIPLUS(gtGDIPLUS):GDI+。用时 1030 毫秒;

{ 1036 ms }
procedure Gray_GDIPLUS(bmp: TBitmap);
var
  img: TGPImage;
  iab: TGPImageAttributes;
  gpg: TGPGraphics;
begin
  img := TGPBitmap.Create(bmp.Handle, bmp.Palette);
  gpg := TGPGraphics.Create(bmp.Canvas.Handle);
  iab := TGPImageAttributes.Create;
  try
    iab.SetColorMatrix(c_GrayColorMatrix, ColorMatrixFlagsDefault, ColorAdjustTypeBitmap);
    gpg.DrawImage(img, TGPRect(bmp.Canvas.ClipRect), 0, 0, bmp.Width, bmp.Height, UnitPixel, iab);
  finally
    iab.Free;
    gpg.Free;
    img.Free;
  end;
end;

07、Gray_Table(gtTable):查表优化。没有了乘法运算。用时 80 毫秒;(效果一般。看来内存定位速度比CPU寄存器的运算速度慢的多)

{ 80 ms }
procedure Gray_Table(bmp: TBitmap);
var
  I, Count: Integer;
  pColor  : PRGBQuad;
  byeGray : Byte;
begin
  Count  := bmp.Width * bmp.Height;
  pColor := GetBitsPointer(bmp);
  for I  := 0 to Count - 1 do
  begin
    byeGray := (c_GrayR77[pColor^.rgbRed] + c_GrayG151[pColor^.rgbGreen] + c_GrayB28[pColor^.rgbBlue]) shr 8;
    pColor^ := TRGBQuad(c_GrayValue[byeGray]);
    Inc(pColor);
  end;
end;

08、Gray_ASM(gtASM):将上面的 Gray_Table 改写为汇编代码。效果较好。用时 25 毫秒。(看来 Delphi 的自身编译器优化效果不如人为优化)

procedure Gray_ASM_Proc(pColor: PRGBQuad; const Count: Integer); register;
asm
  MOV    ECX, EDX                       // ECX = Count 循环计数 EDX (Count) 赋给 ECX;通常将 ECX 作为计数器来使用。只是约定俗成,不是标准
@LOOP:                                  // 循环;EAX 中存在着 pColor 的首地址
  MOVZX  EBX, [EAX].TRGBQuad.RGBRed     // EBX = pColor^.rgbRed
  MOVZX  EDX, [EAX].TRGBQuad.rgbGreen   // EDX = pColor^.rgbGreen
  MOVZX  ESI, [EAX].TRGBQuad.rgbBlue    // ESI = pColor^.rgbBlue
  MOV    EBX, [EBX*4 + c_GrayR77]       // EBX = c_GrayR77[pColor^.rgbRed]
  MOV    EDX, [EDX*4 + c_GrayG151]      // EDX = c_GrayG151[pColor^.rgbGreen]
  MOV    ESI, [ESI*4 + c_GrayB28]       // ESI = c_GrayB28[pColor^.rgbBlue]
  ADD    EBX, EDX                       // EBX = c_GrayR77[pColor^.rgbRed] + c_GrayG151[pColor^.rgbGreen]
  ADD    EBX, ESI                       // EBX = c_GrayR77[pColor^.rgbRed] + c_GrayG151[pColor^.rgbGreen] + c_GrayB28[pColor^.rgbBlue]
  SHR    EBX, 8                         // EBX = (c_GrayR77[pColor^.rgbRed] + c_GrayG151[pColor^.rgbGreen] + c_GrayB28[pColor^.rgbBlue]) shr 8;
  MOV    EBX, [EBX*4 + c_GrayValue]     // EBX = TRGBQuad(c_GrayValue[byeGray])
  MOV    [EAX],  EBX                    // [EAX] = TRGBQuad(c_GrayValue[byeGray])
  ADD    EAX, 4                         // EAX   = 指向下一个像素
  DEC    ECX                            // Count 减一
  JNZ    @LOOP                          // 循环
end;

{ 25 ms }
procedure Gray_ASM(bmp: TBitmap);
begin
  Gray_ASM_Proc(GetBitsPointer(bmp), bmp.Width * bmp.Height);
end;

09、Gray_MMX(gtMMX):用 MMX 代码来做乘法运算。每一次只对一个像素进行了灰度处理。效果不是很好。看来要想效果好,必须得多点同时进行运算。用时 27 毫秒。

procedure Gray_MMX_Proc_P1(pColor: PByte; const Count: Integer); register;
asm
  EMMS
  MOV        ECX,  EDX                           // ECX = Count 循环计数 EDX (Count) 赋给 ECX;
  PXOR       MM7,  MM7                           // MM7 = $0000000000000000
  MOVQ       MM6,  c_GrayMaskARGB                // MM6 = $0000004D0097001C
@LOOP:                                           // 循环;EAX 中存在着 pColor 的首地址
  MOVD       MM0,   [EAX]                        // MM0 = $0000000000RRGGBB
  PUNPCKLBW  MM0,   MM7                          // MM0 = $000000RR00GG00BB
  PMADDWD    MM0,   MM6                          // MM0 = A * 0 + R * 77 | G * 151 + B * 28
  MOVD       EDX,   MM0                          // EDX = G * 151 + B * 28
  PSRLQ      MM0,   32                           // MM0 = R * 77
  MOVD       EBX,   MM0                          // EBX = R * 77
  ADD        EBX,   EDX                          // EBX = R * 77 + G * 151 + B * 28
  SHR        EBX,   8                            // EBX = (R * 77 + G * 151 + B * 28) >> 8
  MOV        EBX,   [EBX*4 + c_GrayValue]        // EBX = TRGBQuad(c_GrayValue[byeGray])
  MOV        [EAX], EBX                          // [EAX] = TRGBQuad(c_GrayValue[byeGray])
  ADD        EAX,   4                            // EAX   = 指向下一个像素
  SUB        ECX,   4                            // Count 减 4
  JNZ        @LOOP                               // 循环
  EMMS
end;

{ 27 ms }
procedure Gray_MMX(bmp: TBitmap);
begin
  Gray_MMX_Proc_P1(GetBitsPointer(bmp), bmp.Width * bmp.Height * 4);
end;

10、Gray_SSE(gtSSE):用 SSE 代码来做运算。四点同时进行。效果不错。为了熟悉 SSE 指令,先用 (R+G+B) / 3 来取灰度值。用时 17 毫秒。

{
  SSE 优化
  XMM0-----XMM7 8 个 128bit 寄存器
  4个像素可以同时运算

  GRAY = (R+G+B) / 3
  GRAY = (R+G+B) * 85   /   255
  GRAY = (R+G+B) * $55 SHR  8
}

procedure Gray_SSE_Proc_01(pColor: PRGBQuad; const Count: Integer); register;
asm
  MOV     ECX,  EDX
  MOVSS   XMM1, [c_GraySSEMask]           // XMM1 = 000000000000000000000000000000FF
  SHUFPS  XMM1, XMM1, 0                   // XMM1 = 000000FF000000FF000000FF000000FF
  MOVSS   XMM2, [c_GraySSEDiv3]           // XMM2 = 00000000000000000000000000000055
  SHUFPS  XMM2, XMM2, 0                   // XMM2 = 00000055000000550000005500000055
  MOVAPS  XMM3, XMM1                      // XMM3 = 000000FF000000FF000000FF000000FF
  PSLLD   XMM3, 24                        // XMM3 = FF000000FF000000FF000000FF000000

@LOOP:
  MOVUPS  XMM0, [EAX]      // XMM0 = |A3R3G3B3|A2R2G2B2|A1R1G1B1|A0R0G0B0|
  MOVAPS  XMM4, XMM0       // XMM4 = |A3R3G3B3|A2R2G2B2|A1R1G1B1|A0R0G0B0|
  MOVAPS  XMM5, XMM0       // XMM5 = |A3R3G3B3|A2R2G2B2|A1R1G1B1|A0R0G0B0|
  MOVAPS  XMM6, XMM0       // XMM6 = |A3R3G3B3|A2R2G2B2|A1R1G1B1|A0R0G0B0|

  // 获取 4 个像素的 B3, B2, B1, B0
  ANDPS   XMM4, XMM1       // XMM4 = |000000B3|000000B2|000000B1|000000B0|

  // 获取 4 个像素的 G3, G2, G1, G0
  PSRLD   XMM5, 8          // XMM5 = |00A3R3G3|00A2R2G2|00A1R1G1|00A0R0G0|
  ANDPS   XMM5, XMM1       // XMM5 = |000000G3|000000G2|000000G1|000000G0|

  // 获取 4 个像素的 R3, R2, R1, R0
  PSRLD   XMM6, 16         // XMM6 = |0000A3R3|0000A2R2|0000A1R1|0000A0R0|
  ANDPS   XMM6, XMM1       // XMM6 = |000000R3|000000R2|000000R1|000000R0|

  // 运算
  PADDD   XMM4, XMM5       // XMM4  = G+B
  PADDD   XMM4, XMM6       // XMM4  = G+B+R
  PMULLW  XMM4, XMM2       // XMM4  = (G+B+R) * $55
  PSRLD   XMM4, 8          // XMM4  = |000000Y3|000000Y2|000000Y1|000000Y0|
  
  // 返回结果
  MOVAPS  XMM5, XMM4       // XMM5  = |000000Y3|000000Y2|000000Y1|000000Y0|
  PSLLD   XMM5, 8          // XMM5  = |0000Y300|0000Y200|0000Y100|0000Y000|
  ORPS    XMM4, XMM5       // XMM4  = |0000Y3Y3|0000Y2Y2|0000Y1Y1|0000Y0Y0|
  PSLLD   XMM5, 8          // XMM5  = |00Y30000|00Y20000|00Y10000|00Y00000|
  ORPS    XMM4, XMM5       // XMM4  = |00Y3Y3Y3|00Y2Y2Y2|00Y1Y1Y1|00Y0Y0Y0|
  ANDPS   XMM0, XMM3       // XMM0  = |FF000000|FF000000|FF000000|FF000000|
  ORPS    XMM0, XMM4       // XMM0  = |FFY3Y3Y3|FFY2Y2Y2|FFY1Y1Y1|FFY0Y0Y0|
  MOVUPS  [EAX], XMM0      // [EAX] = XMM0

  ADD     EAX, 16          // pColor 地址加 16,EAX 指向下4个像素的地址
  SUB     ECX, 16          // Count 减 16
  JNZ     @LOOP            // 循环
end;

{ 17 ms }
procedure Gray_SSE(bmp: TBitmap);
begin
  Gray_SSE_Proc_01(GetBitsPointer(bmp), bmp.Width * bmp.Height * 4);
end;

11、Gray_AVX(gtAVX):用 AVX 代码来实现。
12、Gray_AVX512(gtAVX512):用 AVX512 代码来实现。
13、Gray_GPU(gtGPU):用 GPU 代码来实现。
14、Gray_Other(gtOther):用其它方法来实现。


从几百毫秒到十几毫秒,优化力度可谓不小。花点力气学习也是值得的。
能优化到几毫秒就完美了。见:Delphi 汇编学习(三)--- 图像灰值化的极致优化
详细代码地址:https://github.com/dbyoung720/ImageGray
水平有限,不足的地方还望高手指点。
qq交流群:101611228

 

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值