这与像素访问无关。它更多的是关于您对每个像素执行的计算量、可能对计算进行矢量化、可能对计算进行并行化(正如您在第二次尝试中所做的那样)等等(但幸运的是,我们可以在这里忽略这些细节)。
让我们首先关注我们不使用显式并行化的场景(即暂时不使用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.1 的alpha 因子以在同一个调用中处理除以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 > 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 倍。