【问题标题】:Fastest way to access each pixel of an image?访问图像每个像素的最快方法是什么?
【发布时间】:2019-10-28 19:15:07
【问题描述】:

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

#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;
    }
}

option 1option 2 (0.0034 > 0.001) 慢得多,这是我根据this page 所期望的。

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

【问题讨论】:

  • option1 什么都不做,而 option2 修改图像...
  • 为什么你会期望调用pow 的函数比不这样做的函数更快?此外,在声称代码速度时,您应该发布用于构建应用程序的编译器选项。如果您正在运行“调试”或未优化的代码,那么这一切都毫无意义。
  • 你在比较苹果和没有苹果。如果您在启用优化的情况下进行编译(您应该这样做,否则比较运行时毫无意义),那么选项 1 应该花费零时间。
  • 对不起,各位。我更正了代码。第二个选项实际上更快。
  • 一般来说,如果你想让你的 OpenCV 代码更快,你应该使用库提供的函数,因为它们是 SIMD 优化的,而不是编写你自己的循环。

标签: c++ opencv foreach


【解决方案1】:

这与像素访问无关。它更多的是关于您对每个像素执行的计算量、可能对计算进行矢量化、可能对计算进行并行化(正如您在第二次尝试中所做的那样)等等(但幸运的是,我们可以在这里忽略这些细节)。

让我们首先关注我们不使用显式并行化的场景(即暂时不使用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 };
    }
}

并以下列方式驱动它:

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

我们将在随机生成的大小为 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.");
    }
}

改进1

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

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

std::pow(double(pixel.x) / 10, 2.5) > 100

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

接下来,我们需要将第一个通道转换为double类型。为此,我们可以使用 cv::Mat::convertTo。这个函数提供了另一个优点——它允许我们指定一个比例因子。我们可以提供0.1alpha 因子以在同一个调用中处理除以10。

下一步,我们使用cv::pow 以有效的方式对整个数组执行求幂。我们将结果与阈值 100 进行比较。OpenCV 提供的比较运算符将为true 返回 255,为false 返回 0。鉴于此,我们只需合并结果数组的 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);
}

此实现在 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);
}

此实现在 516 毫秒内完成。


改进 3

好的,但请稍等。对于每个像素,我们必须除以 10(或乘以 0.1),然后计算第 2.5 个指数(这会很昂贵)……但是对于可能具有数百万像素的图像,只有 256 个可能的输入值。如果我们预先计算 lookup table 并使用它而不是按像素计算会怎样?

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);
}

此实现在 68 毫秒内完成。


改进 4

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

让我们应用适当的倒数来消除左侧的幂​​运算。

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

最后让我们乘以 10 来消除左边的分数。

因为我们只处理整数,所以我们可以使用

x &gt; 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);
    }
}

此实现在 166 毫秒内完成。

注意:与上一步相比,这可能看起来很糟糕,但与类似的基线相比,这几乎是 20 倍的改进。


改进 5

这看起来真的像第一个通道上的阈值操作,它被复制到其余 2 个通道上。

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);
}

此实现在 65 毫秒内完成。


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

基线算法的并行实现:

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

此实现在 350 毫秒内完成。


简化算法的并行实现:

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

此实现在 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);
}

时间是:

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.

那么,在启用 HT 的 6 核 CPU 上,性能提升了 285 倍。

【讨论】:

  • 非矢量化和矢量化之间的不良基准测试结果
  • @Dan Mašek 你能说你在哪里定义了impl_fn吗?
  • @HadiGhahremanNezhad 哦,忘了放在那里,只是typedef void(*impl_fn)(cv::Mat);
  • @HadiGhahremanNezhad 不在我的示例代码中。如果您发布一个新问题,其中包含重现此减速的示例代码,可能会更好(并在此处粘贴指向它的链接,以便我知道在哪里查看)。
  • @DanMašek 对不起,你是对的。问题出在其他功能上。
【解决方案2】:

OpenCV 提供了一个高级并行图形库,它利用了特殊的 CPU 和 GPU 指令集,并利用了 OpenCL 统一并行平台。 OpenCV 算法经过优化,足以成为最快的库之一。
另一方面,所有高级库都会损失一点性能以达到指定的统一性、简单性、性能等水平。您几乎总能通过使用本机和低级编程指令和 API,但它通常需要更多的并行编程知识以及更多的开发时间。最终的源代码也会比较复杂。

【讨论】:

  • 你能解释一下它们的不同之处吗?
  • 请注意,OP 更新了问题。选项2确实更快,现在的问题是“如何更高效?”
猜你喜欢
  • 1970-01-01
  • 2011-09-13
  • 2023-03-25
  • 1970-01-01
  • 2010-09-30
  • 2012-11-18
  • 1970-01-01
  • 2011-06-05
  • 2013-05-12
相关资源
最近更新 更多