我正在尝试找到访问图像中像素的最快方法。我尝试了两种选择:
#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;
}
}
选项1 比选项2 (0.0034> 0.001)慢得多,这是我根据this page所期望的。
是否有更有效的方法来访问图像的像素?
答案 0 :(得分: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 };
}
}
并以以下方式驱动它:
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.");
}
}
我们可以尝试利用OpenCV提供的优化功能。
让我们回想一下,对于每个像素,我们在以下条件下执行阈值运算:
std::pow(double(pixel.x) / 10, 2.5) > 100
首先,我们只需要第一个通道即可进行计算。让我们使用cv::extractChannel
提取它。
接下来,我们需要将第一个通道转换为double
类型。为此,我们可以使用
cv::Mat::convertTo
。此功能提供了另一个优点-它使我们可以指定比例因子。我们可以提供alpha
的{{1}}因子,以便在同一次调用中除以10。
下一步,我们使用cv::pow
对整个数组进行有效的幂运算。我们将结果与阈值100进行比较。OpenCV提供的比较运算符将为0.1
返回255,为true
返回0。鉴于此,我们只需要合并结果数组的3个相同副本就可以了。
false
此实现在842毫秒内完成。
此计算实际上并不需要双精度...让我们仅使用浮点数来执行它。
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);
}
此实现在516毫秒内完成。
好的,但是等等。对于每个像素,我们必须除以10(或乘以0.1),然后计算第2.5个指数(这会很昂贵)...但是对于具有数百万个像素的图像,只有256个可能的输入值。如果我们预先计算了lookup table并使用它而不是按像素计算怎么办?
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);
}
此实现在68毫秒内完成。
但是,我们实际上并不需要查找表。我们可以做一些数学运算来简化“复杂的”阈值函数:
让我们应用适当的倒数以消除左侧的求幂。
让我们隐含右手边(这是一个常数)。
最后让我们乘以10,以消除左侧的分数。
由于我们只处理整数,因此可以使用
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);
}
好的,让我们尝试第一个变体。
x > 63
此实现在166毫秒内完成。
注意:与上一步相比,这看起来很糟糕,但与类似基准相比,几乎提高了20倍。
这实际上看起来像是第一个通道上的阈值操作,已复制到其余2个通道上。
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);
}
}
此实现在65毫秒内完成。
该尝试并行化了。让我们从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);
}
开始。
并行执行基线算法:
forEach
此实现在350毫秒内完成。
简化算法的并行实现:
void impl_7(cv::Mat frame)
{
frame.forEach<Pixel>(
[](Pixel& pixel, const int* position)
{
complicatedThreshold(pixel);
}
);
}
此实现将在20毫秒内完成。
那非常好,与原始的朴素算法相比,我们的性能提高了157倍左右。甚至击败最佳非并行尝试近3次。我们可以做得更好吗?
另一种简单的选择是尝试void impl_8(cv::Mat frame)
{
frame.forEach<Pixel>(
[](Pixel& pixel, const int* position)
{
complicatedThreshold_2(pixel);
}
);
}
。
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);
}
因此,您可以在启用HT的6核CPU上提高285倍。
答案 1 :(得分:0)
OpenCV提供了高级并行图形库,该库利用了特殊的CPU和GPU指令集,还利用了OpenCL统一并行平台。 OpenCV算法经过充分优化,可以跻身最快的库之列。
另一方面,所有高级库都失去了一点性能,无法达到指定的统一性,简单性,性能等级别。您几乎总是能够使用本机为特定且有限的问题开发更快的代码和低级编程指令和API,但通常需要更多有关并行编程的知识以及更多的开发时间。最终的源代码也将更加复杂。