如何使用OpenCV parallel_for_来并行化代码
目标
本教程的目的是向您展示如何使用OpenCV parallel_for_框架轻松并行化代码。为了说明这个概念,我们将编写一个程序来绘制一个利用几乎所有可用CPU负载的Mandelbrot集合。完整的教程代码在这里。如果您想要有关多线程的更多信息,则必须参考参考书或课程,因为本教程的目的是保持简单。
前提
第一个先决条件是使用OpenCV构建并行框架。在OpenCV 3.2中,以下并行框架按照以下顺序提供:
- 英特尔线程构建块(第三方库,应显式启用)
- C =并行C / C ++编程语言扩展(第三方库,应明确启用)
- OpenMP(集成到编译器,应该被显式启用)
- APPLE GCD(系统范围广,自动使用(仅限APPLE))
- Windows RT并发(系统范围,自动使用(仅Windows RT))
- Windows并发(运行时的一部分,自动使用(仅限Windows) - MSVC ++> = 10))
- Pthreads(如果有的话)
您可以看到,OpenCV库中可以使用多个并行框架。一些并行库是第三方库,必须在CMake(例如TBB,C =)中进行显式构建和启用,其他可以自动与平台(例如APPLE GCD)一起使用,但是您应该可以使用这些库来访问并行框架直接或通过启用CMake中的选项并重建库。
第二个(弱)前提条件与要实现的任务更相关,因为并不是所有的计算都是合适的/可以被平行地运行。为了保持简单,可以分解成多个基本操作而没有内存依赖性(无可能的竞争条件)的任务很容易并行化。计算机视觉处理通常易于并行化,因为大多数时间一个像素的处理不依赖于其他像素的状态。
简单的例子:绘制一个Mandelbrot集
我们将使用绘制Mandelbrot集的示例来显示如何从常规的顺序代码中轻松调整代码来平滑计算。
理论
Mandelbrot定义被数学家Adrien Douady命名为数学家Benoit Mandelbrot。它在数学领域以外是着名的,因为图像表示是一类分形的一个例子,一个表现出每个尺度显示的重复图案的数学集(甚至更多的是,Mandelbrot集是整体形状可以是自相似的反复看不同规模)。对于更深入的介绍,您可以查看相应的维基百科文章。在这里,我们将介绍公式来绘制Mandelbrot集(从维基百科的文章)。
Mandelbrot集是在二次映射迭代中的0的轨道的复平面中的的值的集合c
依然有限。也就是说,复数是Mandelbrot集的一部分,如果以z_0 = 0开始并重复应用迭代,则z_n的绝对值保持有界,然而大n得到。这也可以表示为
Pseudocode
用于生成Mandelbrot集合的表示的简单算法称为“逃逸时间算法”。对于渲染图像中的每个像素,如果复数在最大迭代次数下是有界的,则使用递归关系进行测试。不属于Mandelbrot集的像素将迅速逃脱,而我们假设像素在固定的最大迭代次数后位于集合中。迭代次数很高会产生更为详细的图像,但计算时间会相应增加。我们使用“转义”所需的迭代次数来描绘图像中的像素值。
For each pixel (Px, Py) on the screen, do:
{
x0 = scaled x coordinate of pixel (scaled to lie in the Mandelbrot X scale (-2, 1))
y0 = scaled y coordinate of pixel (scaled to lie in the Mandelbrot Y scale (-1, 1))
x = 0.0
y = 0.0
iteration = 0
max_iteration = 1000
while (x*x + y*y < 2*2 AND iteration < max_iteration) {
xtemp = x*x - y*y + x0
y = 2*x*y + y0
x = xtemp
iteration = iteration + 1
}
color = palette[iteration]
plot(Px, Py, color)
}
关于伪代码和理论之间的关系,我们有:
在这个数字上,我们记得一个复数的实部是在x轴和y轴上的虚部。如果我们放大特定位置,您可以看到整个形状可以反复显示。
履行
逃脱时间算法实现
int mandelbrot(const complex<float> &z0, const int max)
{
complex<float> z = z0;
for (int t = 0; t < max; t++)
{
if (z.real()*z.real() + z.imag()*z.imag() > 4.0f) return t;
z = z*z + z0;
}
return max;
}
在这里,我们使用std::complex模板类来表示一个复数。此函数执行测试以检查像素是否处于置位,并返回“转义”迭代。
顺序Mandelbrot实现
void sequentialMandelbrot(Mat &img, const float x1, const float y1, const float scaleX, const float scaleY)
{
for (int i = 0; i < img.rows; i++)
{
for (int j = 0; j < img.cols; j++)
{
float x0 = j / scaleX + x1;
float y0 = i / scaleY + y1;
complex<float> z0(x0, y0);
uchar value = (uchar) mandelbrotFormula(z0);
img.ptr<uchar>(i)[j] = value;
}
}
}
在这个实现中,我们依次迭代渲染图像中的像素,以执行测试以检查像素是否可能属于Mandelbrot集。
另一个要做的就是将像素坐标转换为Mandelbrot集空间:
Mat mandelbrotImg(4800, 5400, CV_8U);
float x1 = -2.1f, x2 = 0.6f;
float y1 = -1.2f, y2 = 1.2f;
float scaleX = mandelbrotImg.cols / (x2 - x1);
float scaleY = mandelbrotImg.rows / (y2 - y1);
最后,要将灰度值分配给像素,我们使用以下规则:
- 如果达到最大迭代次数(像素假定为Mandelbrot集合),则像素为黑色,
- 否则我们根据转义的迭代分配灰度值,并按比例缩放以适应灰度范围。
int mandelbrotFormula(const complex<float> &z0, const int maxIter=500) {
int value = mandelbrot(z0, maxIter);
if(maxIter - value == 0)
{
return 0;
}
return cvRound(sqrt(value / (float) maxIter) * 255);
}
使用线性尺度变换不足以感知灰度变化。为了克服这个问题,我们将通过使用平方根尺度转换(从他的博客文章中借鉴于Jeremy D. Frens )来提高感知:
绿色曲线对应于简单的线性尺度变换,蓝色一到平方根尺度变换,您可以观察到在这些位置观察斜率时最低值将如何提升。
并行Mandelbrot实现
当看到顺序实现时,我们可以注意到每个像素是独立计算的。为了优化计算,我们可以通过利用现代处理器的多核架构并行执行多个像素计算。为了轻松实现,我们将使用OpenCV cv :: parallel_for_框架。
class ParallelMandelbrot : public ParallelLoopBody
{
public:
ParallelMandelbrot (Mat &img, const float x1, const float y1, const float scaleX, const float scaleY)
: m_img(img), m_x1(x1), m_y1(y1), m_scaleX(scaleX), m_scaleY(scaleY)
{
}
virtual void operator ()(const Range& range) const
{
for (int r = range.start; r < range.end; r++)
{
int i = r / m_img.cols;
int j = r % m_img.cols;
float x0 = j / m_scaleX + m_x1;
float y0 = i / m_scaleY + m_y1;
complex<float> z0(x0, y0);
uchar value = (uchar) mandelbrotFormula(z0);
m_img.ptr<uchar>(i)[j] = value;
}
}
ParallelMandelbrot& operator=(const ParallelMandelbrot &) {
return *this;
};
private:
Mat &m_img;
float m_x1;
float m_y1;
float m_scaleX;
float m_scaleY;
};
首先是声明一个继承自cv :: ParallelLoopBody并覆盖的自定义类virtual void operator ()(const cv::Range& range) const。
该范围operator ()表示将由单独线程处理的像素子集。这种分割是自动完成的,以平均分配计算负荷。我们必须将像素索引坐标转换为2D [row, col]坐标。另请注意,我们必须保留对mat图像的参考,以便能够原地修改图像。
并行执行调用:
ParallelMandelbrot parallelMandelbrot(mandelbrotImg,x1,y1,scaleX,scaleY);
parallel_for_(Range(0,mandelbrotImg.rows * mandelbrotImg.cols),parallelMandelbrot);
这里,范围表示要执行的操作的总数,因此图像中的像素总数。要设置线程数,可以使用:cv :: setNumThreads。您还可以使用cv :: parallel_for_中的nstripes参数指定拆分次数。例如,如果您的处理器有4个线程,则设置cv::setNumThreads(2)或设置nstripes=2应与默认值相同,它将使用所有可用的处理器线程,但将仅在两个线程上分割工作负载。
- 注意
- C ++ 11标准允许通过摆脱
ParallelMandelbrot
类并用lambda表达式来替换它来简化并行实现:
parallel_for_(Range(0, mandelbrotImg.rows*mandelbrotImg.cols), [&](const Range& range){
for (int r = range.start; r < range.end; r++)
{
int i = r / mandelbrotImg.cols;
int j = r % mandelbrotImg.cols;
float x0 = j / scaleX + x1;
float y0 = i / scaleY + y1;
complex<float> z0(x0, y0);
uchar value = (uchar) mandelbrotFormula(z0);
mandelbrotImg.ptr<uchar>(i)[j] = value;
}
});
结果
您可以在这里找到完整的教程代码。并行实现的性能取决于您拥有的CPU类型。例如,在4个内核/ 8个线程CPU上,您可以预期加速大约为6.9X。有很多因素可以解释为什么我们不能达到将近8倍的加速。主要原因主要是由于:
- 创建和管理线程的开销,
- 后台进程并行运行,
- 4个硬核之间的差异,每个核心有2个逻辑线程,8个硬件核心。
由教程代码生成的图像(您可以修改代码以使用更多迭代,并根据转义的迭代分配像素颜色,并使用调色板获得更多美学图像):
Mandelbrot设置为xMin = -2.1,xMax = 0.6,yMin = -1.2,yMax = 1.2,maxIterations = 500