阅读(2309) (10)

如何使用OpenCV parallel_for_来并行化代码

2017-08-31 10:49:48 更新

目标

本教程的目的是向您展示如何使用OpenCV parallel_for_框架轻松并行化代码。为了说明这个概念,我们将编写一个程序来绘制一个利用几乎所有可用CPU负载的Mandelbrot集合。完整的教程代码在这里。如果您想要有关多线程的更多信息,则必须参考参考书或课程,因为本教程的目的是保持简单。

前提

第一个先决条件是使用OpenCV构建并行框架。在OpenCV 3.2中,以下并行框架按照以下顺序提供:

  1. 英特尔线程构建块(第三方库,应显式启用)
  2. C =并行C / C ++编程语言扩展(第三方库,应明确启用)
  3. OpenMP(集成到编译器,应该被显式启用)
  4. APPLE GCD(系统范围广,自动使用(仅限APPLE))
  5. Windows RT并发(系统范围,自动使用(仅Windows RT))
  6. Windows并发(运行时的一部分,自动使用(仅限Windows) - MSVC ++> = 10))
  7. Pthreads(如果有的话)

您可以看到,OpenCV库中可以使用多个并行框架。一些并行库是第三方库,必须在CMake(例如TBB,C =)中进行显式构建和启用,其他可以自动与平台(例如APPLE GCD)一起使用,但是您应该可以使用这些库来访问并行框架直接或通过启用CMake中的选项并重建库。

第二个(弱)前提条件与要实现的任务更相关,因为并不是所有的计算都是合适的/可以被平行地运行。为了保持简单,可以分解成多个基本操作而没有内存依赖性(无可能的竞争条件)的任务很容易并行化。计算机视觉处理通常易于并行化,因为大多数时间一个像素的处理不依赖于其他像素的状态。

简单的例子:绘制一个Mandelbrot集

我们将使用绘制Mandelbrot集的示例来显示如何从常规的顺序代码中轻松调整代码来平滑计算。

理论

Mandelbrot定义被数学家Adrien Douady命名为数学家Benoit Mandelbrot。它在数学领域以外是着名的,因为图像表示是一类分形的一个例子,一个表现出每个尺度显示的重复图案的数学集(甚至更多的是,Mandelbrot集是整体形状可以是自相似的反复看不同规模)。对于更深入的介绍,您可以查看相应的维基百科文章。在这里,我们将介绍公式来绘制Mandelbrot集(从维基百科的文章)。

Mandelbrot集是在二次映射迭代中的0的轨道的复平面中的的值的集合c

如何使用OpenCV parallel_for_来并行化代码

依然有限。也就是说,复数是Mandelbrot集的一部分,如果以z_0 = 0开始并重复应用迭代,则z_n的绝对值保持有界,然而大n得到。这也可以表示为QQ图片20170831105125

如何使用OpenCV parallel_for_来并行化代码

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

关于伪代码和理论之间的关系,我们有:

如何使用OpenCV parallel_for_来并行化代码

如何使用OpenCV parallel_for_来并行化代码

在这个数字上,我们记得一个复数的实部是在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 )来提高感知:

如何使用OpenCV parallel_for_来并行化代码

如何使用OpenCV parallel_for_来并行化代码

绿色曲线对应于简单的线性尺度变换,蓝色一到平方根尺度变换,您可以观察到在这些位置观察斜率时最低值将如何提升。

并行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个硬件核心。

由教程代码生成的图像(您可以修改代码以使用更多迭代,并根据转义的迭代分配像素颜色,并使用调色板获得更多美学图像):

如何使用OpenCV parallel_for_来并行化代码

Mandelbrot设置为xMin = -2.1,xMax = 0.6,yMin = -1.2,yMax = 1.2,maxIterations = 500