采用ARM NEON的快速高斯模糊图像滤波器

时间:2013-07-05 09:55:50

标签: opencv image-processing gaussian neon imagefilter

我正在尝试制作移动快速版本的高斯模糊图像滤镜。

我读过其他问题,例如:Fast Gaussian blur on unsigned char image- ARM Neon Intrinsics- iOS Dev

为了我的目的,我只需要一个固定大小(7x7)的固定sigma(2)高斯滤波器。

因此,在优化ARM NEON之前,我正在C ++中实现1D高斯内核,并直接在移动环境(Android与NDK)中将性能与OpenCV GaussianBlur()方法进行比较。这样,它将导致更简单的代码进行优化。

然而结果是我的实现比OpenCV4Android版本慢10倍。我已经读过OpenCV4 Tegra已经优化了GaussianBlur实现,但我不认为标准的OpenCV4Android有那种优化,那为什么我的代码这么慢?

这是我的实现(注意:在边界附近应用滤镜时,reflect101用于像素反射):

Mat myGaussianBlur(Mat src){
    Mat dst(src.rows, src.cols, CV_8UC1);
    Mat temp(src.rows, src.cols, CV_8UC1);
    float sum, x1, y1;

    // coefficients of 1D gaussian kernel with sigma = 2
    double coeffs[] = {0.06475879783, 0.1209853623, 0.1760326634, 0.1994711402, 0.1760326634, 0.1209853623, 0.06475879783};
    //Normalize coeffs
    float coeffs_sum = 0.9230247873f;
    for (int i = 0; i < 7; i++){
        coeffs[i] /= coeffs_sum;
    }

    // filter vertically
    for(int y = 0; y < src.rows; y++){
        for(int x = 0; x < src.cols; x++){
            sum = 0.0;
            for(int i = -3; i <= 3; i++){
                y1 = reflect101(src.rows, y - i);
                sum += coeffs[i + 3]*src.at<uchar>(y1, x);
            }
            temp.at<uchar>(y,x) = sum;
        }
    }

    // filter horizontally
    for(int y = 0; y < src.rows; y++){
        for(int x = 0; x < src.cols; x++){
            sum = 0.0;
            for(int i = -3; i <= 3; i++){
                x1 = reflect101(src.rows, x - i);
                sum += coeffs[i + 3]*temp.at<uchar>(y, x1);
            }
            dst.at<uchar>(y,x) = sum;
        }
    }

    return dst;
}

4 个答案:

答案 0 :(得分:5)

这里的问题很大一部分就是算法过于精确,正如@PaulR指出的那样。通常最好保持系数表不比数据精确。在这种情况下,由于您似乎正在处理uchar数据,因此您将大致使用8位系数表。

保持这些权重很小在你的NEON实现中尤为重要,因为你的算术越窄,你可以一次处理更多的通道。

除此之外,第一个突出的主要减速是在主循环中具有图像边缘反射代码。这将使大部分工作效率降低,因为在这种情况下通常不需要做任何特殊的工作。

如果你在边缘附近使用特殊版本的循环,它可能会更好,然后当你安全的时候,你使用一个不会调用{{1}的简化内循环功能。

第二个(与原型代码更相关)是在应用加权函数之前可以将窗口的翅膀加在一起,因为表格在两边都包含相同的系数。

reflect101()

这为每个像素节省了六次乘法,并且在控制溢出条件方面向其他一些优化迈出了一步。

然后还有一些与内存系统有关的其他问题。

原则上,双程方法很好,因为它可以避免执行大量重新计算。不幸的是,它可以将有用的数据从L1缓存中推出,这可能会使一切变得非常慢。这也意味着当你将结果写入内存时,你需要量化中间和,这会降低精度。

当您将此代码转换为NEON时,您需要关注的一件事就是尝试将您的工作集保留在寄存器文件中,但不要在它们被充分利用之前丢弃计算。

当人们使用两遍时,通常会转换中间数据 - 也就是说,一列输入变成一行输出。

这是因为CPU真的不喜欢在输入图像的多行中获取少量数据。如果你收集一堆水平像素并过滤那些水平像素,它的效率会更高(因为缓存的工作方式)。如果临时缓冲区被转置,那么第二遍将一堆水平点(在原始方向上垂直)收集起来,并再次转换其输出,使其以正确的方式出现。 / p>

如果您进行了优化以保持工作集的本地化,那么您可能不需要这种转置技巧,但它值得了解,以便您可以为自己设置一个健康的基线性能。不幸的是,像这样的本地化会迫使你回到非最佳内存提取,但是对于更广泛的数据类型,可以减轻惩罚。

答案 1 :(得分:2)

如果这是专门用于8位图像,那么你真的不需要浮点系数,尤其不是双精度。此外,您不希望为x1,y1使用浮点数。你应该只使用整数作为坐标,你可以使用系数的固定点(即整数)来保持所有滤波器算术在整数域中,例如。

Mat myGaussianBlur(Mat src){
    Mat dst(src.rows, src.cols, CV_8UC1);
    Mat temp(src.rows, src.cols, CV_16UC1); // <<<
    int sum, x1, y1;  // <<<

    // coefficients of 1D gaussian kernel with sigma = 2
    double coeffs[] = {0.06475879783, 0.1209853623, 0.1760326634, 0.1994711402, 0.1760326634, 0.1209853623, 0.06475879783};
    int coeffs_i[7] = { 0 }; // <<<
    //Normalize coeffs
    float coeffs_sum = 0.9230247873f;
    for (int i = 0; i < 7; i++){
        coeffs_i[i] = (int)(coeffs[i] / coeffs_sum * 256); // <<<
    }

    // filter vertically
    for(int y = 0; y < src.rows; y++){
        for(int x = 0; x < src.cols; x++){
            sum = 0; // <<<
            for(int i = -3; i <= 3; i++){
                y1 = reflect101(src.rows, y - i);
                sum += coeffs_i[i + 3]*src.at<uchar>(y1, x); // <<<
            }
            temp.at<uchar>(y,x) = sum;
        }
    }

    // filter horizontally
    for(int y = 0; y < src.rows; y++){
        for(int x = 0; x < src.cols; x++){
            sum = 0; // <<<
            for(int i = -3; i <= 3; i++){
                x1 = reflect101(src.rows, x - i);
                sum += coeffs_i[i + 3]*temp.at<uchar>(y, x1); // <<<
            }
            dst.at<uchar>(y,x) = sum / (256 * 256); // <<<
        }
    }

    return dst;
}

答案 2 :(得分:2)

这是执行@Paul R和@ sh1的所有建议后的代码,总结如下:

1)仅使用整数运算(具有精确度)

2)在应用乘法之前,将距离掩模中心相同距离的像素值相加,以减少乘法次数。

3)仅应用水平过滤器以利用矩阵行的存储

4)在图像内部的边缘周围分开循环,不要对反射函数进行不必要的调用。我完全删除了反射的功能,包括它们沿着边缘的循环内部。

5)另外,作为个人观察,为了在不调用(慢)函数“round”或“cvRound”的情况下改进舍入,我添加了临时和最终像素结果0.5f(=整数精度为32768) )与OpenCV相比,减少错误/差异。

现在性能比OpenCV慢了大约15到6倍。

然而,得到的矩阵与使用OpenCV的高斯模糊获得的矩阵不完全相同。这不是因为算术长度(足够)以及移除错误。注意,这是两个版本产生的矩阵之间的最小差异,在像素强度的0和2之间(绝对值)。系数与OpenCV使用的相同,使用相同大小和西格玛的getGaussianKernel获得。

Mat myGaussianBlur(Mat src){

Mat dst(src.rows, src.cols, CV_8UC1);
Mat temp(src.rows, src.cols, CV_8UC1);
int sum;
int x1;

double coeffs[] = {0.070159, 0.131075, 0.190713, 0.216106, 0.190713, 0.131075, 0.070159};
int coeffs_i[7] = { 0 };
for (int i = 0; i < 7; i++){
        coeffs_i[i] = (int)(coeffs[i] * 65536); //65536
}

// filter horizontally - inside the image
for(int y = 0; y < src.rows; y++){
    uchar *ptr = src.ptr<uchar>(y);
    for(int x = 3; x < (src.cols - 3); x++){
        sum = ptr[x] * coeffs_i[3];
        for(int i = -3; i < 0; i++){
            int tmp = ptr[x+i] + ptr[x-i];
            sum += coeffs_i[i + 3]*tmp;
        }
        temp.at<uchar>(y,x) = (sum + 32768) / 65536;
    }
}
// filter horizontally - edges - needs reflect
for(int y = 0; y < src.rows; y++){
    uchar *ptr = src.ptr<uchar>(y);
    for(int x = 0; x <= 2; x++){
        sum = 0;
        for(int i = -3; i <= 3; i++){
            x1 = x + i;
            if(x1 < 0){
                x1 = -x1;
            }
            sum += coeffs_i[i + 3]*ptr[x1];
        }
        temp.at<uchar>(y,x) = (sum + 32768) / 65536;
    }
}
for(int y = 0; y < src.rows; y++){
    uchar *ptr = src.ptr<uchar>(y);
    for(int x = (src.cols - 3); x < src.cols; x++){
        sum = 0;
        for(int i = -3; i <= 3; i++){
            x1 = x + i;
            if(x1 >= src.cols){
                x1 = 2*src.cols - x1 - 2;
            }
            sum += coeffs_i[i + 3]*ptr[x1];
        }
        temp.at<uchar>(y,x) = (sum + 32768) / 65536;
    }
}

// transpose to apply again horizontal filter - better cache data locality
transpose(temp, temp);

// filter horizontally - inside the image
for(int y = 0; y < src.rows; y++){
    uchar *ptr = temp.ptr<uchar>(y);
    for(int x = 3; x < (src.cols - 3); x++){
        sum = ptr[x] * coeffs_i[3];
        for(int i = -3; i < 0; i++){
            int tmp = ptr[x+i] + ptr[x-i];
            sum += coeffs_i[i + 3]*tmp;
        }
        dst.at<uchar>(y,x) = (sum + 32768) / 65536;
    }
}
// filter horizontally - edges - needs reflect
for(int y = 0; y < src.rows; y++){
    uchar *ptr = temp.ptr<uchar>(y);
    for(int x = 0; x <= 2; x++){
        sum = 0;
        for(int i = -3; i <= 3; i++){
            x1 = x + i;
            if(x1 < 0){
                x1 = -x1;
            }
            sum += coeffs_i[i + 3]*ptr[x1];
        }
        dst.at<uchar>(y,x) = (sum + 32768) / 65536;
    }
}
for(int y = 0; y < src.rows; y++){
    uchar *ptr = temp.ptr<uchar>(y);
    for(int x = (src.cols - 3); x < src.cols; x++){
        sum = 0;
        for(int i = -3; i <= 3; i++){
            x1 = x + i;
            if(x1 >= src.cols){
                x1 = 2*src.cols - x1 - 2;
            }
            sum += coeffs_i[i + 3]*ptr[x1];
        }
        dst.at<uchar>(y,x) = (sum + 32768) / 65536;
    }
}

transpose(dst, dst);

return dst;
}

答案 3 :(得分:0)

在Google设备上,根据Google文档,使用float / double比使用int / uchar慢两倍。

您可能会在此Android文档中找到一些解决方案来加快C ++代码的速度。 https://developer.android.com/training/articles/perf-tips