【问题标题】:Resize 8-bit image by 2 with ARM NEON使用 ARM NEON 将 8 位图像大小调整为 2
【发布时间】:2013-07-22 20:43:19
【问题描述】:

我有一个 8 位 640x480 图像,我想将其缩小为 320x240 图像:

void reducebytwo(uint8_t *dst, uint8_t *src)
//src is 640x480, dst is 320x240

使用 ARM SIMD NEON 的最佳方法是什么?任何地方的示例代码?

作为一个起点,我只是想做相当于:

for (int h = 0; h < 240; h++)
    for (int w = 0; w < 320; w++)
        dst[h * 320 + w] = (src[640 * h * 2 + w * 2] + src[640 * h * 2 + w * 2 + 1] + src[640 * h * 2 + 640 + w * 2] + src[640 * h * 2 + 640 + w * 2 + 1]) / 4; 

【问题讨论】:

  • 最佳需要被定义。最快、最高质量、最小尺寸等?对于最高质量,图像缩小有不同的权衡。在某些情况下保持低频内容很重要,在其他情况下保持高频内容很重要。什么是 8 位?灰度、颜色映射或其他?
  • 这是一个灰度输入。最好 = 最快。

标签: image image-processing arm simd neon


【解决方案1】:

这是您的代码与 NEON 内在函数的一对一翻译:

#include <arm_neon.h>
#include <stdint.h>

static void resize_line (uint8_t * __restrict src1, uint8_t * __restrict src2, uint8_t * __restrict dest)
{
  int i;
  for (i=0; i<640; i+=16)
  {
    // load upper line and add neighbor pixels:
    uint16x8_t a = vpaddlq_u8 (vld1q_u8 (src1));

    // load lower line and add neighbor pixels:
    uint16x8_t b = vpaddlq_u8 (vld1q_u8 (src2));

    // sum of upper and lower line: 
    uint16x8_t c = vaddq_u16 (a,b);

    // divide by 4, convert to char and store:
    vst1_u8 (dest, vshrn_n_u16 (c, 2));

    // move pointers to next chunk of data
    src1+=16;
    src2+=16;
    dest+=8;
   }
}   

void resize_image (uint8_t * src, uint8_t * dest)
{
  int h;    
  for (h = 0; h < 240 - 1; h++)
  {
    resize_line (src+640*(h*2+0), 
                 src+640*(h*2+1), 
                 dest+320*h);
  }
}

它处理 32 个源像素,每次迭代生成 8 个输出像素。

我快速查看了汇编程序的输出,看起来还不错。如果您在汇编程序中编写 resize_line 函数,展开循环并消除管道停顿,您可以获得更好的性能。这将使您的性能提升估计为三倍。

不过,它应该比不更改汇编程序的实现要快得多。

注意:我没有测试过代码...

【讨论】:

  • 太棒了!您是否认为在汇编程序中使用整个 resize_image 函数会更快,还是您认为根据您的建议,我已经节省了 90% 的时间?
  • 会更快.. 毫无疑问。
【解决方案2】:

如果您不太关心精度,那么与更精确的算法相比,这个内部循环应该会给您两倍的计算吞吐量:

for (i=0; i<640; i+= 32)
{
    uint8x16x2_t a, b;
    uint8x16_t c, d;

    /* load upper row, splitting even and odd pixels into a.val[0]
     * and a.val[1] respectively. */
    a = vld2q_u8(src1);

    /* as above, but for lower row */
    b = vld2q_u8(src2);

    /* compute average of even and odd pixel pairs for upper row */
    c = vrhaddq_u8(a.val[0], a.val[1]);
    /* compute average of even and odd pixel pairs for lower row */
    d = vrhaddq_u8(b.val[0], b.val[1]);

    /* compute average of upper and lower rows, and store result */
    vst1q_u8(dest, vrhaddq_u8(c, d));

    src1+=32;
    src2+=32;
    dest+=16;
}

它通过使用vhadd 操作来工作,其结果与输入的大小相同。这样您就不必将最终总和移回 8 位,并且整个算术都是 8 位的,这意味着您可以在每条指令中执行两倍的操作。

但是它不太准确,因为中间和是量化的,而 GCC 4.7 在生成代码方面做得很糟糕。 GCC 4.8 做得很好。

不过,整个操作很有可能会受到 I/O 限制。应该展开循环以最大限度地分离负载和算术,并且应该使用__builtin_prefetch()(或PLD)在需要之前将传入数据提升到缓存中。

【讨论】:

    【解决方案3】:

    这是 @Nils Pipenbrinck 建议的 reduce_line 上的 asm 版本

    static void reduce2_neon_line(uint8_t* __restrict src1, uint8_t* __restrict src2, uint8_t* __restrict dest, int width) {
        for(int i=0; i<width; i+=16) {
            asm (
                 "pld [%[line1], #0xc00]     \n"
                 "pld [%[line2], #0xc00]     \n"
                 "vldm %[line1]!, {d0,d1}  \n"
                 "vldm %[line2]!, {d2,d3}  \n"
                 "vpaddl.u8 q0, q0         \n"
                 "vpaddl.u8 q1, q1         \n"
                 "vadd.u16  q0, q1         \n"
                 "vshrn.u16 d0, q0, #2     \n"
    
                 "vst1.8 {d0}, [%[dst]]! \n"
    
                 :
                 : [line1] "r"(src1), [line2] "r"(src2), [dst] "r"(dest)
                 : "q0", "q1", "memory"
                 );
        }
    }
    

    比 C 版快 4 倍左右(在 iPhone 5 上测试)。

    【讨论】:

      猜你喜欢
      • 2014-10-03
      • 1970-01-01
      • 2013-06-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-06-06
      • 1970-01-01
      相关资源
      最近更新 更多