Canny算法中的非最大抑制:使用SSE进行优化

时间:2015-08-13 14:06:02

标签: c++ computer-vision sse edge-detection canny-operator

我有“荣幸”来改善其他人的以下代码的运行时间。 (这是来自canny算法的非最大限制 - )。我的第一个想法是使用SSE内在代码,我在这方面很新,所以我的问题是。

有没有机会这样做? 如果是这样,有人可以给我一些提示吗?

void vNonMaximumSupression(
          float* fpDst, 
          float const*const fpMagnitude, 
          unsigned char  const*const ucpGradient,                                                                           ///< [in] 0 -> 0°, 1 -> 45°, 2 -> 90°, 3 -> 135°
int iXCount, 
int iXOffset, 
int iYCount, 
int ignoreX, 
int ignoreY)
{
    memset(fpDst, 0, sizeof(fpDst[0]) * iXCount * iXOffset);

    for (int y = ignoreY; y < iYCount - ignoreY; ++y)
    {
        for (int x = ignoreX; x < iXCount - ignoreX; ++x)
        {
            int idx = iXOffset * y + x;
            unsigned char dir = ucpGradient[idx];
            float fMag = fpMagnitude[idx];

            if (dir == 0 && fpMagnitude[idx - 1]           < fMag && fMag > fpMagnitude[idx + 1] ||
                dir == 1 && fpMagnitude[idx - iXCount + 1] < fMag && fMag > fpMagnitude[idx + iXCount - 1] ||
                dir == 2 && fpMagnitude[idx - iXCount]     < fMag && fMag > fpMagnitude[idx + iXCount] ||
                dir == 3 && fpMagnitude[idx - iXCount - 1] < fMag && fMag > fpMagnitude[idx + iXCount + 1]
                )
                    fpDst[idx] = fMag;
            else
                fpDst[idx] = 0;
        }
    }
}

1 个答案:

答案 0 :(得分:5)

讨论

正如@harold所指出的,这里矢量化的主要问题是算法对每个像素使用不同的偏移(由方向矩阵指定)。我可以想到几种可能的矢量化方法:

  1. @nikie:立即评估所有分支,即将每个像素与其所有邻居进行比较。这些比较的结果根据方向值进行混合。
  2. @PeterCordes:将大量像素加载到SSE寄存器中,然后使用_mm_shuffle_epi8仅选择给定方向上的邻居。然后进行两次矢量化比较。
  3. (me):使用标量指令沿方向加载正确的两个相邻像素。然后将四个像素的这些值组合成SSE寄存器。最后,在SSE中进行两次比较。
  4. 第二种方法很难有效地实现,因为对于一个4像素的包,有18个相邻的像素可供选择。我认为这需要太多的洗牌。

    第一种方法看起来不错,但每像素执行的操作会增加四倍。我认为矢量指令的加速会被过多的计算所淹没。

    我建议使用第三种方法。您可以在下面看到有关提高性能的提示。

    混合方法

    首先,我们希望尽快制作标量代码。您提供的代码包含太多分支。其中大多数都是不可预测的,例如按方向切换。

    为了删除分支,我建议创建一个数组delta = {1, stride - 1, stride, stride + 1},它从方向给出索引偏移量。通过使用此数组,您可以找到要与之比较的相邻像素的索引(没有分支)。然后你做两个比较。最后,您可以编写像res = (isMax ? curr : 0);这样的三元运算符,希望编译器可以为它生成无分支代码。

    不幸的是,编译器(至少是MSVC2013)不够智能,无法避免isMax分支。这就是我们可以通过标量SSE内在函数重写内循环的原因。查找the guide以供参考。你需要大多数内在函数以_ss结尾,因为代码是完全标量的。

    最后,除了加载相邻像素外,我们可以对所有内容进行矢量化。为了加载相邻的像素,我们可以使用带有标量参数的_mm_setr_ps内在函数,要求编译器为我们生成一些好的代码=)

    __m128 forw = _mm_setr_ps(src[idx+0 + offset0], src[idx+1 + offset1], src[idx+2 + offset2], src[idx+3 + offset3]);
    __m128 back = _mm_setr_ps(src[idx+0 - offset0], src[idx+1 - offset1], src[idx+2 - offset2], src[idx+3 - offset3]);
    

    我自己刚刚实现了它。在Ivy Bridge 3.4Ghz上测试单线程。使用1024×1024分辨率的随机图像作为源。结果(以毫秒为单位)为:

    original: 13.078     //your code
    branchless: 8.556    //'branchless' code
    scalarsse: 2.151     //after rewriting to sse intrinsics
    hybrid: 1.159        //partially vectorized code
    

    他们确认每个步骤的性能改进。最终代码需要一个多毫秒的时间来处理一百万像素的图像。总加速约为 11.3次。实际上,你可以在GPU上获得更好的性能=)

    我希望所提供的信息足以让您重现这些步骤。如果您正在寻找可怕的破坏者,请查看here以了解我所有这些阶段的实现。