【问题标题】:How to efficiently rotate bitmaps in code如何在代码中有效地旋转位图
【发布时间】:2010-10-25 06:48:55
【问题描述】:

有没有比简单地使用倒置坐标执行嵌套循环更快的方法将位图旋转 90 或 270 度?

位图为 8bpp,通常为 2048x2400x8bpp

目前我通过简单地复制参数反转来做到这一点,大致(伪代码:

for x = 0 to 2048-1
  for y = 0 to 2048-1
    dest[x][y]=src[y][x];

(实际上我是用指针来做的,速度更快,但幅度大致相同)

GDI 在处理大图像时非常慢,纹理(GF7 卡)的 GPU 加载/存储时间与当前 CPU 时间相同。

任何提示,指针?就地算法甚至会更好,但速度比就地更重要。

目标是 Delphi,但它更像是一个算法问题。 SSE(2) 向量化没问题,对我来说在汇编程序中编码已经足够大了


跟进尼尔斯的回答

  • 图像 2048x2700 -> 2700x2048
  • 编译器 Turbo Explorer 2006 已开启优化。
  • Windows:电源方案设置为“始终开启”。 (重要!!!!
  • 机器:Core2 6600 (2.4 GHz)

使用旧程序的时间:32 毫秒(第 1 步)

步长为 8 的时间:12ms

步长为 16 的时间:10ms

步长 32+ 的时间:9 毫秒

同时我还在 Athlon 64 X2 (5200+ iirc) 上进行了测试,那里的加速比原来的四倍多一点(80 到 19 毫秒)。

加速是值得的,谢谢。也许在夏天的几个月里,我会用 SSE(2) 版本来折磨自己。但是我已经考虑过如何解决这个问题,我想我会用完 SSE2 寄存器来直接实现:

for n:=0 to 7 do
  begin
    load r0, <source+n*rowsize> 
    shift byte from r0 into r1
    shift byte from r0 into r2
    ..
    shift byte from r0 into r8
  end; 
store r1, <target>   
store r2, <target+1*<rowsize>
..
store r8, <target+7*<rowsize>   

所以 8x8 需要 9 个寄存器,但 32 位 SSE 只有 8 个。无论如何,这是夏季月份的事情:-)

请注意,指针是我出于本能而做的事情,但实际上它可能有一些东西,如果您的尺寸没有硬编码,编译器就无法将 mul 转换为移位。虽然现在 muls an sich 很便宜,但它们也会产生更多的注册压力。

代码(通过从“naieve”rotate1 实现中减去结果来验证):

const stepsize = 32;
procedure rotatealign(Source: tbw8image; Target:tbw8image);

var stepsx,stepsy,restx,resty : Integer;
   RowPitchSource, RowPitchTarget : Integer;
   pSource, pTarget,ps1,ps2 : pchar;
   x,y,i,j: integer;
   rpstep : integer;
begin
  RowPitchSource := source.RowPitch;          // bytes to jump to next line. Can be negative (includes alignment)
  RowPitchTarget := target.RowPitch;        rpstep:=RowPitchTarget*stepsize;
  stepsx:=source.ImageWidth div stepsize;
  stepsy:=source.ImageHeight div stepsize;
  // check if mod 16=0 here for both dimensions, if so -> SSE2.
  for y := 0 to stepsy - 1 do
    begin
      psource:=source.GetImagePointer(0,y*stepsize);    // gets pointer to pixel x,y
      ptarget:=Target.GetImagePointer(target.imagewidth-(y+1)*stepsize,0);
      for x := 0 to stepsx - 1 do
        begin
          for i := 0 to stepsize - 1 do
            begin
              ps1:=@psource[rowpitchsource*i];   // ( 0,i)
              ps2:=@ptarget[stepsize-1-i];       //  (maxx-i,0);
              for j := 0 to stepsize - 1 do
               begin
                 ps2[0]:=ps1[j];
                 inc(ps2,RowPitchTarget);
               end;
            end;
          inc(psource,stepsize);
          inc(ptarget,rpstep);
        end;
    end;
  // 3 more areas to do, with dimensions
  // - stepsy*stepsize * restx        // right most column of restx width
  // - stepsx*stepsize * resty        // bottom row with resty height
  // - restx*resty                    // bottom-right rectangle.
  restx:=source.ImageWidth mod stepsize;   // typically zero because width is 
                                          // typically 1024 or 2048
  resty:=source.Imageheight mod stepsize;
  if restx>0 then
    begin
      // one loop less, since we know this fits in one line of  "blocks"
      psource:=source.GetImagePointer(source.ImageWidth-restx,0);    // gets pointer to pixel x,y
      ptarget:=Target.GetImagePointer(Target.imagewidth-stepsize,Target.imageheight-restx);
      for y := 0 to stepsy - 1 do
        begin
          for i := 0 to stepsize - 1 do
            begin
              ps1:=@psource[rowpitchsource*i];   // ( 0,i)
              ps2:=@ptarget[stepsize-1-i];       //  (maxx-i,0);
              for j := 0 to restx - 1 do
               begin
                 ps2[0]:=ps1[j];
                 inc(ps2,RowPitchTarget);
               end;
            end;
         inc(psource,stepsize*RowPitchSource);
         dec(ptarget,stepsize);
       end;
    end;
  if resty>0 then
    begin
      // one loop less, since we know this fits in one line of  "blocks"
      psource:=source.GetImagePointer(0,source.ImageHeight-resty);    // gets pointer to pixel x,y
      ptarget:=Target.GetImagePointer(0,0);
      for x := 0 to stepsx - 1 do
        begin
          for i := 0 to resty- 1 do
            begin
              ps1:=@psource[rowpitchsource*i];   // ( 0,i)
              ps2:=@ptarget[resty-1-i];       //  (maxx-i,0);
              for j := 0 to stepsize - 1 do
               begin
                 ps2[0]:=ps1[j];
                 inc(ps2,RowPitchTarget);
               end;
            end;
         inc(psource,stepsize);
         inc(ptarget,rpstep);
       end;
    end;
 if (resty>0) and (restx>0) then
    begin
      // another loop less, since only one block
      psource:=source.GetImagePointer(source.ImageWidth-restx,source.ImageHeight-resty);    // gets pointer to pixel x,y
      ptarget:=Target.GetImagePointer(0,target.ImageHeight-restx);
      for i := 0 to resty- 1 do
        begin
          ps1:=@psource[rowpitchsource*i];   // ( 0,i)
          ps2:=@ptarget[resty-1-i];       //  (maxx-i,0);
          for j := 0 to restx - 1 do
            begin
              ps2[0]:=ps1[j];
              inc(ps2,RowPitchTarget);
            end;
       end;
    end;
end;

更新 2 泛型

我试图在 Delphi XE 中将此代码更新为泛型版本。我因为 QC 99703 失败了,论坛的人已经确认它也存在于 XE2 中。请投票给它:-)

更新 3 泛型 现在在 XE10 中工作

更新 4

2017 年,我为 8x8 cubes of 8bpp images only 和相关的 SO question 做了一些关于 shuffle 瓶颈的工作,Peter Cordes 慷慨地帮助了我。这段代码仍然有一个错失的机会,并且仍然需要另一个循环层级来将多个 8x8 块迭代聚合成更大的伪迭代,比如 64x64。现在又是整行,太浪费了。

【问题讨论】:

    标签: delphi image-processing image-manipulation rotation


    【解决方案1】:

    是的,有更快的方法来做到这一点。

    您的简单循环大部分时间都在缓存未命中。之所以会发生这种情况,是因为您在一个紧密的循环中在非常不同的地方接触了大量数据。更糟糕的是:你的记忆位置正好是两个的幂。这是缓存性能最差的大小。

    如果您改进内存访问的局部性,您可以改进此轮换算法。

    执行此操作的一种简单方法是使用与整个位图相同的代码自行旋转每个 8x8 像素块,然后包装另一个循环,将图像旋转拆分为每个 8x8 像素的块。

    例如像这样的东西(没有检查,很抱歉C代码。我的Delphi技能不是最新的):

     // this is the outer-loop that breaks your image rotation
     // into chunks of 8x8 pixels each:
     for (int block_x = 0; block_x < 2048; block_x+=8)
     {
        for (int block_y = 0; blocky_y < 2048; block_y+=8)
        { 
           // this is the inner-loop that processes a block
           // of 8x8 pixels.
           for (int x= 0; x<8; x++)
             for (int y=0; y<8; y++)
                dest[x+block_x][y+block_y] = src[y+block_y][x+block_x]
        }
     } 
    

    还有其他方法。您可以在 Hilbert-Order 或 Morton-Order 中处理数据。这在理论上会更快一些,但代码会复杂得多。

    顺便说一句 - 既然您提到 SSE 是您的选择。请注意,您可以在 SSE 寄存器中旋转 8x8 字节块。让它工作有点棘手,但看看 SSE 矩阵转置代码应该会让你开始,因为它是同一件事。


    编辑:

    刚刚检查过:

    代码块大小为 8x8 像素,运行时间约为在我的机器上快 5 倍。块大小为 16x16,它的运行速度提高了 10 倍。

    似乎尝试不同的块大小是个好主意。

    这是我用过的(非常简单的)测试程序:

    #include <stdio.h>
    #include <windows.h>
    
    char temp1[2048*2048];
    char temp2[2048*2048];
    
    void rotate1 (void)
    {
      int x,y;
      for (y=0; y<2048; y++)
      for (x=0; x<2048; x++)
        temp2[2048*y+x] = temp1[2048*x+y];
    }
    
    void rotate2 (void)
    {
      int x,y;
      int bx, by;
    
      for (by=0; by<2048; by+=8)
      for (bx=0; bx<2048; bx+=8)
      for (y=0; y<8; y++)
      for (x=0; x<8; x++)
        temp2[2048*(y+by)+x+bx] = temp1[2048*(x+bx)+y+by];
    }
    
    void rotate3 (void)
    {
      int x,y;
      int bx, by;
    
      for (by=0; by<2048; by+=16)
      for (bx=0; bx<2048; bx+=16)
      for (y=0; y<16; y++)
      for (x=0; x<16; x++)
        temp2[2048*(y+by)+x+bx] = temp1[2048*(x+bx)+y+by];
    }
    
    
    int main (int argc, char **args)
    {
      int i, t1;
    
      t1 = GetTickCount();
      for (i=0; i<20; i++) rotate1();
      printf ("%d\n", GetTickCount()-t1);
    
      t1 = GetTickCount();
      for (i=0; i<20; i++) rotate2();
      printf ("%d\n", GetTickCount()-t1);
    
      t1 = GetTickCount();
      for (i=0; i<20; i++) rotate3();
      printf ("%d\n", GetTickCount()-t1);
    
    }
    

    【讨论】:

    • 好的,谢谢。这听起来很有希望。现在加速 10 倍就足够了,我不会为了这件事而费心去搞乱 SSE。至少不是现在。我对小块做这件事有一种粗略的感觉,这既证实了这一点,也为我提供了一个实现。
    • 该技术称为循环平铺顺便说一句。哦 - 不要忘记您可以并行化缩放。只需启动两个线程,让每个线程处理一半的图像。
    • 顺便说一句,Marco,一旦你实现了它,让我们知道你有多少加速。我只是好奇它在实际应用程序中的表现。
    • 另一个更新。今年夏天我终于做了汇编程序。对于小图像尺寸,它比循环平铺速度快 4 倍,另请参阅 stackoverflow.com/questions/47478010/…
    【解决方案2】:

    如果您可以使用 C++,那么您可能需要查看 Eigen

    它是一个 C++ 模板库,使用 SSE(2 及更高版本)和 AltiVec 指令集,优雅地回退到非矢量化代码

    快。 (参见基准)。
    表达式模板允许在适当的时候智能地删除临时变量并启用惰性求值 - Eigen 会自动处理此问题并在大多数情况下也处理别名。
    对 SSE(2 和更高版本)和 AltiVec 指令集执行显式矢量化,并优雅地回退到非矢量化代码。表达式模板允许对整个表达式全局执行这些优化。
    使用固定大小的对象,可以避免动态内存分配,并且在有意义时展开循环。
    对于大型矩阵,需要特别注意缓存友好性。

    【讨论】:

    • 此类库的问题在于,您必须将格式转换为库使用的格式,通常已经在可能的增益范围内。
    【解决方案3】:

    可能可以通过在缓存对齐的块中而不是按行复制来改进它,因为此时任一 src dest 的步幅都将是未命中的(取决于 delphi 是否为 row major或专栏专业)。

    【讨论】:

    • (不是=的两边之一总是不对齐的问题吗?我要么线性走src,要么走dst,但从不同时走两者)
    • 不,你可以像 Nils 演示的那样复制一个块并在缓存中转置它(假设他所在的机器有 256 字节缓存行)
    • 好的,抱歉,我弄错了,是的,尼尔斯的答案就是我要找的
    【解决方案4】:

    如果图像不是正方形,则无法就地执行。即使您在方形图像中工作,变换也不利于就地工作。

    如果您想尝试更快地执行操作,可以尝试利用行步长来使其工作,但我认为您最好的方法是一次读取 4 个字节,从源,然后将其写入 dest 中的四个连续行。这应该会减少您的一些开销,但我预计不会有超过 5% 的改进。

    【讨论】:

    • 正确,但我有几个不同的用例,可以就地,例如通过轻轻放大图像使其呈方形。不过,它是较小的用例之一。正如您在下面看到的,加速非常显着(300-400%)。但是原地会消耗速度,因为你不能简单地交换块,而是必须旋转 4 x 90 度,所以我决定不推动它,现在认为副本是理所当然的。
    猜你喜欢
    • 2017-01-05
    • 2015-07-11
    • 2014-09-20
    • 2021-04-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多