访问图像每个像素的最快方法?

Had*_*had 0 c++ foreach opencv

我试图找到最快的方法来访问图像中的像素。我尝试了两种选择:

#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;

// Define a pixel 
typedef Point3_<uint8_t> Pixel;

void complicatedThreshold(Pixel& pixel);

int main()
{
    cv::Mat frame = imread("img.jpg");

    clock_t t1, t2;
    t1 = clock();

    for (int i = 0; i < 10; i++)
    {
        //===================
        // Option 1: Using pointer arithmetic 
        //===================
        const Pixel* endPixel = pixel + frame.cols * frame.rows;
        for (; pixel != endPixel; pixel++)
        {
            complicatedThreshold(*pixel);
        }

        //===================
        // Option 2: Call forEach
        //===================
        frame.forEach<Pixel>
            (
                [](Pixel& pixel, const int* position) -> void
                {
                    complicatedThreshold(pixel);
                }
        );
    }

    t2 = clock();
    float t_diff((float)t2 - (float)t1);
    float seconds = t_diff / CLOCKS_PER_SEC;
    float mins = seconds / 60.0;
    float hrs = mins / 60.0;

    cout << "Execution Time (mins): " << mins << "\n";

    cvWaitKey(1);
}

void complicatedThreshold(Pixel& pixel)
{
    if (pow(double(pixel.x) / 10, 2.5) > 100)
    {
        pixel.x = 255;
        pixel.y = 255;
        pixel.z = 255;
    }
    else
    {
        pixel.x = 0;
        pixel.y = 0;
        pixel.z = 0;
    }
}
Run Code Online (Sandbox Code Playgroud)

选项1选项2(0.0034> 0.001)慢得多,这是我根据此页面所期望的。

有没有更有效的方法来访问图像的像素?

Dan*_*šek 6

这实际上与像素访问无关。它更多地是关于每个像素的计算量,可能对计算进行矢量化,可能对计算进行并行化(就像您在第二次尝试中所做的那样)以及更多(但我们可以在这里忽略这些详细信息)。

首先,让我们关注不使用显式并行化(即暂时不forEach使用)的情况。

让我们从原始的阈值函数开始,使其变得更简洁,然后将其标记为内联(这在一定程度上有所帮助):

inline void complicatedThreshold(Pixel& pixel)
{
    if (std::pow(double(pixel.x) / 10, 2.5) > 100) {
        pixel = { 255, 255, 255 };
    } else {
        pixel = { 0, 0, 0 };
    }
}
Run Code Online (Sandbox Code Playgroud)

并以以下方式驱动它:

void impl_1(cv::Mat frame)
{
    auto pixel = frame.ptr<Pixel>();
    auto const endPixel = pixel + frame.total();
    for (; pixel != endPixel; ++pixel) {
        complicatedThreshold(*pixel);
    }
}
Run Code Online (Sandbox Code Playgroud)

我们将在随机生成的尺寸为8192x8192的3通道图像上测试此版本(以及改进版本)。

基线在3139毫秒内完成。


使用impl_1作为基准,我们将检查使用以下模板函数的正确性所有的改进:

template <typename T>
void require_same_result(cv::Mat frame, T const& fn1, T const& fn2)
{
    cv::Mat working_frame_1(frame.clone());
    fn1(working_frame_1);

    cv::Mat working_frame_2(frame.clone());
    fn2(working_frame_2);


    if (cv::sum(working_frame_1 != working_frame_2) != cv::Scalar(0, 0, 0, 0)) {
        throw std::runtime_error("Mismatch.");
    }
}
Run Code Online (Sandbox Code Playgroud)

改进1

我们可以尝试利用OpenCV提供的优化功能。

让我们回想一下,对于每个像素,我们在以下条件下执行阈值运算:

std::pow(double(pixel.x) / 10, 2.5) > 100
Run Code Online (Sandbox Code Playgroud)

首先,我们只需要第一个通道即可进行计算。让我们使用提取它cv::extractChannel

接下来,我们需要将第一个通道转换为doubletype。为此,我们可以使用 cv::Mat::convertTo。此功能提供了另一个优点-它使我们可以指定比例因子。我们可以在同一通话中提供乘以除以10的alpha因子0.1

下一步,我们将使用cv::pow高效的方式对整个数组执行幂运算。我们将结果与阈值100进行比较。OpenCV提供的比较运算符将返回255 true和0 false。鉴于此,我们只需要合并结果数组的3个相同副本就可以了。

void impl_2(cv::Mat frame)
{
    cv::Mat1b first_channel;
    cv::extractChannel(frame, first_channel, 0);

    cv::Mat1d tmp;
    first_channel.convertTo(tmp, CV_64FC1, 0.1);
    cv::pow(tmp, 2.5, tmp);

    first_channel = tmp > 100;

    cv::merge(std::vector<cv::Mat>{ first_channel, first_channel, first_channel }, frame);
}
Run Code Online (Sandbox Code Playgroud)

该实现在842毫秒内完成。


改进2

该计算实际上并不需要双精度...让我们仅使用浮点数来执行它。

void impl_3(cv::Mat frame)
{
    cv::Mat1b first_channel;
    cv::extractChannel(frame, first_channel, 0);

    cv::Mat1f tmp;
    first_channel.convertTo(tmp, CV_32FC1, 0.1);
    cv::pow(tmp, 2.5, tmp);

    first_channel = tmp > 100;

    cv::merge(std::vector<cv::Mat>{ first_channel, first_channel, first_channel }, frame);
}
Run Code Online (Sandbox Code Playgroud)

此实现在516毫秒内完成。


改进3

好的,等一下。对于每个像素,我们必须除以10(或乘以0.1),然后计算第2.5个指数(这将是昂贵的)...但是对于具有数百万个像素的图像,只有256个可能的输入值。如果我们预先计算了一个查找表而不是按像素计算该怎么办?

cv::Mat make_lut()
{
    cv::Mat1b result(256, 1);
    for (uint32_t i(0); i < 256; ++i) {
        if (pow(double(i) / 10, 2.5) > 100) {
            result.at<uchar>(i, 0) = 255;
        } else {
            result.at<uchar>(i, 0) = 0;
        }
    }
    return result;
}

void impl_4(cv::Mat frame)
{
    cv::Mat lut(make_lut());

    cv::Mat first_channel;
    cv::extractChannel(frame, first_channel, 0);

    cv::LUT(first_channel, lut, first_channel);

    cv::merge(std::vector<cv::Mat>{ first_channel, first_channel, first_channel }, frame);
}
Run Code Online (Sandbox Code Playgroud)

此实现在68毫秒内完成。


改进4

但是,我们实际上并不需要查找表。我们可以做一些数学运算来简化“复杂的”阈值函数:

<code> \ left(\ frac {x} {10} \ right)^ {2.5}> 100 </ code>

让我们应用适当的倒数以消除左侧的求幂。

<code> \ frac {x} {10}> \ sqrt [2.5] {100} </ code>

并且让我们隐含右手边(这是一个常数)。

<code> \ frac {x} {10}> 6.30957 </ code>

最后,让我们乘以10以消除左侧的分数。

<code> x> 63.0957 </ code>

由于我们只处理整数,因此可以使用

x > 63


好的,让我们尝试第一个变体。

inline void complicatedThreshold_2(Pixel& pixel)
{
    if (pixel.x > 63) {
        pixel = { 255, 255, 255 };
    } else {
        pixel = { 0, 0, 0 };
    }
}

void impl_5(cv::Mat frame)
{
    auto pixel = frame.ptr<Pixel>();
    auto const endPixel = pixel + frame.total();
    for (; pixel != endPixel; pixel++) {
        complicatedThreshold_2(*pixel);
    }
}
Run Code Online (Sandbox Code Playgroud)

该实现在166毫秒内完成。

注意:与上一步相比,这看起来很糟糕,但与类似基准相比,几乎提高了20倍。


改进5

这实际上看起来像第一个通道上的阈值操作,该操作已复制到其余两个通道上。

void impl_6(cv::Mat frame)
{
    cv::Mat first_channel;
    cv::extractChannel(frame, first_channel, 0);

    cv::threshold(first_channel, first_channel, 63, 255, cv::THRESH_BINARY);

    cv::merge(std::vector<cv::Mat>{ first_channel, first_channel, first_channel }, frame);
}
Run Code Online (Sandbox Code Playgroud)

此实现在65毫秒内完成。


是时候尝试并行化了。让我们从开始forEach

基线算法的并行实现:

void impl_7(cv::Mat frame)
{
    frame.forEach<Pixel>(
        [](Pixel& pixel, const int* position)
        {
            complicatedThreshold(pixel);
        }
    );
}
Run Code Online (Sandbox Code Playgroud)

此实现在350毫秒内完成。


简化算法的并行实现:

void impl_8(cv::Mat frame)
{
    frame.forEach<Pixel>(
        [](Pixel& pixel, const int* position)
        {
            complicatedThreshold_2(pixel);
        }
    );
}
Run Code Online (Sandbox Code Playgroud)

此实现将在20毫秒内完成。

很好,与原始的朴素算法相比,我们的性能提高了157倍左右。甚至击败最佳非并行尝试近3次。我们可以做得更好吗?


进一步改进

一种更简单的选择是尝试parallel_for_

typedef void(*impl_fn)(cv::Mat);

void impl_parallel(cv::Mat frame, impl_fn const& fn)
{
    cv::parallel_for_(cv::Range(0, frame.rows), [&](const cv::Range& range) {
        for (int r = range.start; r < range.end; r++) {
            fn(frame.row(r));
        }
    });
}


void impl_9(cv::Mat frame)
{
    impl_parallel(frame, impl_1);
}

void impl_10(cv::Mat frame)
{
    impl_parallel(frame, impl_2);
}

void impl_11(cv::Mat frame)
{
    impl_parallel(frame, impl_3);
}

void impl_12(cv::Mat frame)
{
    impl_parallel(frame, impl_4);
}

void impl_13(cv::Mat frame)
{
    impl_parallel(frame, impl_5);
}

void impl_14(cv::Mat frame)
{
    impl_parallel(frame, impl_6);
}
Run Code Online (Sandbox Code Playgroud)

时间是:

Test 9 minimum: 355 ms.
Test 10 minimum: 108 ms.
Test 11 minimum: 62 ms.
Test 12 minimum: 25 ms.
Test 13 minimum: 19 ms.
Test 14 minimum: 11 ms.
Run Code Online (Sandbox Code Playgroud)

因此,您可以在启用HT的6核CPU上将285倍改进。