解释 ARM Neon 图像采样

Posted

技术标签:

【中文标题】解释 ARM Neon 图像采样【英文标题】:Explaining ARM Neon Image Sampling 【发布时间】:2013-03-13 17:04:08 【问题描述】:

我正在尝试编写 OpenCV 的 cv::resize() 的更好版本,但我遇到了一个交叉代码:https://github.com/rmaz/NEON-Image-Downscaling/blob/master/ImageResize/BDPViewController.m 该代码用于将图像下采样 2,但我无法获得算法。我想首先将该算法转换为 C,然后尝试对其进行修改以用于学习目的。是否也很容易将其转换为任意大小的下采样?

函数是:

static void inline resizeRow(uint32_t *dst, uint32_t *src, uint32_t pixelsPerRow)

    const uint32_t * rowB = src + pixelsPerRow;

    // force the number of pixels per row to a multiple of 8
    pixelsPerRow = 8 * (pixelsPerRow / 8);

    __asm__ volatile("Lresizeloop: \n" // start loop
                     "vld1.32 d0-d3, [%1]! \n" // load 8 pixels from the top row
                     "vld1.32 d4-d7, [%2]! \n" // load 8 pixels from the bottom row
                     "vhadd.u8 q0, q0, q2 \n" // average the pixels vertically
                     "vhadd.u8 q1, q1, q3 \n"
                     "vtrn.32 q0, q2 \n" // transpose to put the horizontally adjacent pixels in different registers
                     "vtrn.32 q1, q3 \n"
                     "vhadd.u8 q0, q0, q2 \n" // average the pixels horizontally
                     "vhadd.u8 q1, q1, q3 \n"
                     "vtrn.32 d0, d1 \n" // fill the registers with pixels
                     "vtrn.32 d2, d3 \n"
                     "vswp d1, d2 \n"
                     "vst1.64 d0-d1, [%0]! \n" // store the result
                     "subs %3, %3, #8 \n" // subtract 8 from the pixel count
                     "bne Lresizeloop \n" // repeat until the row is complete
: "=r"(dst), "=r"(src), "=r"(rowB), "=r"(pixelsPerRow)
: "0"(dst), "1"(src), "2"(rowB), "3"(pixelsPerRow)
: "q0", "q1", "q2", "q3", "cc"
);


To call it:

 // downscale the image in place
    for (size_t rowIndex = 0; rowIndex < height; rowIndex+=2)
    
        void *sourceRow = (uint8_t *)buffer + rowIndex * bytesPerRow;
        void *destRow = (uint8_t *)buffer + (rowIndex / 2) * bytesPerRow;
        resizeRow(destRow, sourceRow, width);
    

【问题讨论】:

你发现了一个非常糟糕的例子:1)它会随着每个半加而截断,因此结果不太准确。 2)除了令人困惑之外,所有这些转置都在浪费宝贵的循环。使用 VPADD 和 VPADAL 而不是半相加,该函数将更快(转置消失)和更准确。 (只截断一次) 【参考方案1】:

算法非常简单。它从当前行读取 8 个像素,从下一行读取 8 个像素。然后它使用 vhadd(减半相加)指令垂直平均 8 个像素。然后它转置像素的位置,以便水平相邻的像素对现在位于单独的寄存器中(垂直排列)。然后它执行另一组减半加法以将它们平均在一起。然后将结果再次转换以将它们放在原始位置并写入目的地。可以重写该算法以处理不同的缩放积分大小,但正如所写的那样,它只能通过平均进行 2x2 到 1 的缩减。这是等效的 C 代码:

static void inline resizeRow(uint32_t *dst, uint32_t *src, uint32_t pixelsPerRow)

    uint8_t * pSrc8 = (uint8_t *)src;
    uint8_t * pDest8 = (uint8_t *)dst;
    int stride = pixelsPerRow * sizeof(uint32_t);
    int x;
    int r, g, b, a;

    for (x=0; x<pixelsPerRow; x++)
    
       r = pSrc8[0] + pSrc8[4] + pSrc8[stride+0] + pSrc8[stride+4];
       g = pSrc8[1] + pSrc8[5] + pSrc8[stride+1] + pSrc8[stride+5];
       b = pSrc8[2] + pSrc8[6] + pSrc8[stride+2] + pSrc8[stride+6];
       a = pSrc8[3] + pSrc8[7] + pSrc8[stride+3] + pSrc8[stride+7];
       pDest8[0] = (uint8_t)((r + 2)/4); // average with rounding
       pDest8[1] = (uint8_t)((g + 2)/4);
       pDest8[2] = (uint8_t)((b + 2)/4);
       pDest8[3] = (uint8_t)((a + 2)/4);
       pSrc8 += 8; // skip forward 2 source pixels
       pDest8 += 4; // skip forward 1 destination pixel
    

【讨论】:

惊人的答案。像素是如何垂直平均的?两个像素,然后是两个像素?以及支持不同规模的缩放需要什么?我在纸上试了一下还是没有得到转置操作和水平相邻的像素:/ vhadd 指令等价于 c = (a+b+1)/2。代码的第一部分对像素进行垂直平均,因为包含顶行和底行的 NEON 寄存器是一起平均的。由于没有跨 NEON 向量元素水平操作的 vhadd,因此需要转置这些值,以便将水平相邻的元素放置在单独的寄存器中。转置后,它们再次被平均(“水平”平均像素)。在此处查看 VTRN 的描述:infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0489c/… 写的没错。我写了 4 个字节,然后将目标指针提前 4 个字节。 嗯?二维数组?不,它是指向一维像素行的简单指针。不知道你的困惑来自哪里。

以上是关于解释 ARM Neon 图像采样的主要内容,如果未能解决你的问题,请参考以下文章

SAD 16*4 的 Arm-neon 优化版本未提供预期增益

使用 Neon 将图像下采样 3 的算法

使用 arm neon 进行 Rgb 到灰度转换

使用NEON优化ARM的卷积运算

ARM NEON 图像转换优化

图像复制在 PHP 中重新采样,有人可以解释一下吗?