本文是OpenCV核心模块(core module)的最后一节内容。下一部分会更新OpenCV的图像处理模块,基本的图像处理算法都在这个模块中。
本文的目标是展示如何用parallel_for_框架快速实现代码的并行运行。下面例程中,并行方法使用几乎100%的CPU资源,绘制一个Mandelbrot集合图像。与单线程相比,速度提升约7倍。如果想了解更多的多线程编程信息,需要阅读相关的参考书或课程,本例只是简单的例子。
- 原文网址How to use the OpenCV parallel_for_ to parallelize your code
- 本地目录D:opencvsourcesdoctutorialscorehow_to_use_OpenCV_parallel_for_
- 代码目录D:opencvsourcessamplescpptutorial_codecorehow_to_use_OpenCV_parallel_for_
- GitHub 有相应文档和OpenCV源代码
- 版本OpenCV4.1.2(版本兼容性见英文原文,部分文档适用于OpenCV2.0和3.0)
- 环境Windows、C++、VS2019 Community
预先条件
首先,需要OpenCV编译时添加一个并行框架。在OpenCV3.2中,提供了并行框架:
- Intel TBB(第三方库,显式引用)
- C= 并行C/C++编程语言扩展(第三方库,显式引用)
- OpenMP(集成到编译器,显式引用)
- APPLE GCD(自动使用,苹果系统专用)
- Windows RT并发(自动使用,Windows RT专用)
- Windows 并发(运行时的一部分,自动使用,Windows专用-MSVC++10以上)
- Pthreads
上面的并行框架都可用于OpenCV库。一些第三方库需要在OpenCV库生成时显式引用,cmake时选中。其他的根据平台不同可以自动使用,但是要有直接使用的权限或者编译生成时选择使用。
另外,要执行的任务适合于并行计算。或者能够分解为并行计算的子任务。计算机视觉通常很容易并行化,因为一般一个像素在处理时,与其他像素处理与否不冲突。
简单例子:绘制一个Mandelbrot集合
通过使用绘制Mandelbrot集的示例来说明,如何轻松调整常规的连续执行代码,实现并行化计算。
理论
Mandelbrot集合的命名是数学家Adrien Douady向数学家Benoit Mandelbrot致敬。是图像分形表示的一个例子,表现为图像的自相似性,即整个图像的形状在不同图像的尺度重复出现。为更深入的了解可以看维基百科。这里,只是介绍如何绘制Mandelbrot集合的公式。
Mandelbrot集合是复数c在复平面,从初值0开始进行二次迭代,保持有界的值的集合:
复数
伪代码
生成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轴。如果放大指定位置,整个形状重复可见。
实现
逃逸时间算法实现
//[mandelbrot逃逸时间算法]
这里用到了std::complex模板类来表示复数。这个函数来测试像素是否在集合中,并返回逃逸的迭代次数。
连续Mandelbrot实现
//[连续mandelbrot实现]
在实现过程中,遍历图像的每个像素,分别迭代、尺度变换,得到最终的像素值。
另外,需要将像素坐标转换到Mandelbrot集合坐标空间:
//[获取mandelbrot变换所需的初值x1,y1,scaleX,scaleY]
最后,给像素赋值,用下面的规则:
- 如果一个像素达到最大迭代次数,为黑色(认为像素在集合中)
- 否则用逃逸迭代次数赋值,然后将迭代次数的数值转换到灰度尺度范围内
//[mandelbrot灰度尺度变换]
使用线性尺度变换不能精确反映出灰度变化。为克服该缺点,用平方根尺度变换来增强显示:
绿色是线性尺度变换,蓝色对应平方根尺度变换。可以看到左侧最低值的增强。
并行Mandelbrot实现
从上面的连续Mandelbrot实现可以看到,每个像素都是独立计算的。为了优化计算,利用多核架构的处理器,执行多个像素并行计算。用OpenCV的cv::parallel_for_框架很容易实现。
//[并行mandelbrot实现]
首先要做的是声明一个自定义类,继承于cv::ParalelLoopBoody,并且重载虚函数virtual void operator()(const cv::Range& range) const。
operator中的range重新表示被独立线程处理的像素子集。分解任务是自动完成的,并根据CPU核心的计算能力平均分配。将像素索引坐标转换为[row,col]坐标。另外,对mat图像保持引用(reference),以实现in-place修改。
并行执行用下面方式调用:
//[并行mandelbrot调用方式二:需要前面定义的类]
这里,range表示将要执行的运算次数,即图像的像素个数。为了设置线程数目,可以用cv::setNumThreads(2)或者设置parallel_for_的第三个参数nstrips=2。默认会使用所有可用处理器线程,但是任务会分为两个线程。
注意:C++11标准允许通过lambda表达式代替ParalleMandelbrot类,简化上述并行实现:
//[并行mandelbrot调用方式一:cxx11,不需要前面定义的类]
代码
//---parallel_for_实现并行计算-----------//
结果
并行运算的性能取决于CPU的类型。例如4核8线程CPU,可以提升约6.9倍速度。有很多因素可以解释为什么没有达到8倍的加速。主要原因是:
- 创建与管理线程的时间
- 后台有其它进程在并行运行
- 4核每核两逻辑线程与8核的不同
本例运行结果生成的图像如下图。(可以修改代码使用更多的迭代次数,根据逃逸迭代次数的不同用调色板为像素配色)