OpenCV 并行计算函数 parallel_for_ 的使用

摘录:

在使用 OpenCV 的过程中,对图片的处理计算量还是很大的,所以在实施运行的程序中如何高效的计算会节省很多时间。现有的方法有很多,如 OpenMp, TBB, OpenCL 当然还有 Nvidia 的 CUDA。

CUDA 是个好东西,但是并不太适合毫秒级别的程序运行,单一张图片在 cpu 和 gpu 之间的传输时间就已经达到 300ms(使用 opencv 的cuda 库函数);在TX2上直接对cuda进行编程,数据的传输也是在 50ms(不包含初始化)以上,根本不能拿来做实时的运算。所以如何在cpu上更加高效的计算变得尤为重要。偶然间发现了 OpenCV 的并行计算函数 parallel_for_,它整合了上述的多个组件。

对于一些基本的循环运算,如果我们直接使用循环,即便是使用指针,运算效率也不高,如果我们使用并行计算,会大大提升运算效率,OpenCV 里面的很多运算都是使用了并行加速的。在 OpenCV 3.2 中,并行框架按照以下顺序提供:

可以看到,OpenCV 库中可以使用多个并行框架:
一些并行库是第三方库,必须在 CMake(例如TBB)中进行显式构建和启用,其他可以自动与平台(例如 APPLE GCD)一起使用,但是您应该可以使用这些库来访问并行框架直接或通过启用CMake中的选项并重建库;
第二个(弱)前提条件与要实现的任务更相关,因为并不是所有的计算都是合适的/可以被平行地运行。为了保持简单,可以分解成 多个基本操作而没有内存依赖性(无可能的竞争条件)的任务很容易并行化。计算机视觉处理通常易于并行化,因为大多数时间一个像素的处理不依赖于其他像素的状态。

本文主要描述 Parallel_for_与 ParallelLoopBody 的使用方法,需要使用

Parallel_for_ 的介绍为 parallel data processor,有两种使用方式:

void cv::parallel_for_ (const Range &range, const ParallelLoopBody &body, double nstripes=-1.)
static void cv::parallel_for_ (const Range &range, std::function< void(const Range &)> functor, double nstripes=-1.)

1. 先从构造函数的运算符重载说起

我们在调用函数的时候,实际上是使用了括号 ( ) 运算符,构造函数也是普通的函数,所以也用到了括号运算符,那如果想要重载这个括号 ( ) 运算符怎么做呢?先来看一个简单的小例子:

class Animal
{
public:
    Animal(float weight_,int age_)
    {
        weight = weight_;
        age = age_;
    }
    void operator()(float height) const
    {
        height = 100.0;
        std::cout << "the age is : " << age << std::endl;
        std::cout << "the weight is : " << weight << std::endl;
        std::cout << "the height is : " << height << std::endl;
    }

private:
    float weight;
    int age;
};

现在调用:

int main(int argc, char* argv[])
{
    myanimal::Animal animal(50.0,25);
    animal(100.0);

    getchar();
    return 0;
}

总结:

(1)重载的括号运算符就像一个对象的方法一杨,依旧是通过对象去调用,调用的方式为 “对象名(参数列表)” 这样的形式;

(2)重载括号运算符的一般操作为 “返回类型 operator(参数列表)” ,后面的const可以不要,参数列表可以使任意的,

没有参数,则调用方式为:obj()
一个参数,则调用方式为:obj(参数1)
多个参数,则调用方式为:obj(参数1,参数2,…)

2. Parallel_for_ 结合 ParallelLoopBody 使用的一般步骤

使用步骤一般遵循三步走的原则

(1)第一步:自定义一个类或者是一个结构体,使这个结构体或者是类 继承自 ParallelLoopBody 类,如下:

class MyParallelClass : public ParallelLoopBody
{}
struct MyParallelStruct : public ParallelLoopBody
{}

(2)第二步:在自定义的类或者是结构体中, 重写括号运算符( ),注意:虽然前面讲括号运算符重载可以接受任意数量的参数,但是这里只能接受一个 Range 类型的参数(这是与一般的重载不一样的地方),因为后面的parallel_for_需要使用,如下:

void operator()(const Range& range)
{

}

(3)第三步:使用 parallel_for_ 进行并行处理

再看一下parallel_for_的函数原型

CV_EXPORTS void parallel_for_(const Range& range,
                              const ParallelLoopBody& body,
                              double nstripes=-1.);

使用方式如下:

parallel_for_(Range(start, end), MyParallelClass(构造函数列表));

常规的使用方式应该像如下这样才对。这样写本身没什么问题,但是 这样的执行方式,在括号重载运算符里面的内容是按照顺序执行的,并没有并发处理

MyParallelClass obj = MyParallelClass(构造函数列表));
obj(Range(start, end));

而上面所述的使用方式:

parallel_for_(Range(start, end), MyParallelClass(构造函数列表));

没有显式的调用重载的括号运算符,但实际上是隐式调用的,并且以并发方式处理重载运算里面的内容。

3. Parallel_for_结合ParallelLoopBody的加速效果实验

任务描述:我要定义两个Mat矩阵的逐元素乘积,如下所示

(1)自定义一个类继承自 ParallelLoopBody,并且重载括号运算

class ParallelAdd : public ParallelLoopBody
{
public:
    ParallelAdd(Mat& _src1,Mat& _src2,Mat _result)
    {
        src1 = _src1;
        src2 = _src2;
        result = _result;
        CV_Assert((src1.rows == src2.rows) && (src1.cols == src2.cols));
        rows = src1.rows;
        cols = src1.cols;
    }

    void operator()(const Range& range) const
    {
        int step = (int)(result.step / result.elemSize1());

        for (int col = range.start; col < range.end; ++col)
        {
            float * pData = (float*)result.col(col).data;
            float * p1 = (float*)src1.col(col).data;
            float * p2 = (float*)src2.col(col).data;
            for (int row = 0; row < result.rows; ++row)
                pData[row*step] = p1[row*step] * p2[row*step];
        }
    }

private:
    Mat src1;
    Mat src2;
    Mat result;
    int rows;
    int cols;
};

可见重载的运算符里面是一个耗时操作,现在定义两种方式来实现两个 Mat 的逐元素乘积,一种是普通的逐元素处理,另一种是使用parallel进行并发处理,分别通过两个函数完成,如下:


void testParallelClassWithFor(Mat _src1,Mat _src2,Mat result)
{
    result = Mat(_src1.rows, _src1.cols, _src1.type());
    int step = (int)(result.step / result.elemSize1());
    int totalCols = _src1.cols;
    ParallelAdd add = ParallelAdd(_src1, _src2, result);
    add(Range(0, totalCols));
}

void testParallelClassTestWithParallel_for_(Mat _src1,Mat _src2,Mat result)
{
    result = Mat(_src1.rows, _src1.cols, _src1.type());
    int step = (int)(result.step / result.elemSize1());
    int totalCols = _src1.cols;
    parallel_for_(Range(0, totalCols), ParallelAdd(_src1,_src2,result));
}

现在开始测试耗时对比:

int main(int argc, char* argv[])
{
    Mat testInput1 = Mat::ones(6400, 5400, CV_32F);
    Mat testInput2 = Mat::ones(6400, 5400, CV_32F);

    Mat result1, result2, result3;
    clock_t start, stop;

    start = clock();
    testParallelClassWithFor(testInput1, testInput2, result1);
    stop = clock();
    cout << "Running time using \'general for \':" << (double)(stop - start) / CLOCKS_PER_SEC * 1000 << "ms" << endl;

    start = clock();
    testParallelClassWithParallel_for_(testInput1, testInput2, result2);
    stop = clock();
    cout << "Running time using \'parallel for \':" << (double)(stop - start) / CLOCKS_PER_SEC * 1000 << "ms" << endl;

    start = clock();
    result3 = testInput1.mul(testInput2);
    stop = clock();
    cout << "Running time using \'mul function \':" << (double)(stop - start) / CLOCKS_PER_SEC * 1000 << "ms" << endl;

    return 0;
}

总结:我们可以看见,使用 parallel_for_ 并发的方式的确 比直接调用快一些,快了将近 200ms, 但是依旧没有使用 OpenCV 自带的标准函数 mul 函数速度快,因为,OpenCV 实现的函数库不仅仅经过了并行处理,还是用了更强大的底层优化,所以, 只要是 OpenCV 自己带的方法,一般都是优先使用,除非自己写的比 OpenCV 的还牛逼一些。

任务描述:现在定义一个并行运算的结构体,实现Mat逐元素的三次方运算

(1)自定义一个结构体继承自 ParallelLoopBody,并且重载括号运算,如下:

struct ParallelPow:ParallelLoopBody
{
    Mat* src;
    ParallelPow(Mat& _src)
    {
        src = &_src;
    }
    void operator()(const Range& range) const
    {
        Mat& result = *src;
        int step = (int)(result.step / result.elemSize1());
        for (int col = range.start; col < range.end; ++col)
        {
            float* pData = (float*)result.col(col).data;
            for (int row = 0; row < result.rows; ++row)
                pData[row*step] = std::pow(pData[row*step], 3);
        }
    }
};

下面定义两个函数,一个是直接通过 for 循环逐元素进行立方运算,一个是通过 parallel_for_ 并发运算的,通过两个函数实现,如下所示:

void testParallelStructWithFor(Mat _src)
{
    int totalCols = _src.cols;
    ParallelPow obj = ParallelPow(_src);
    obj(Range(0, totalCols));
}

void testParallelStructWithParallel_for(Mat _src)
{
    int totalCols = _src.cols;
    parallel_for_(Range(0, totalCols), ParallelPow(_src));
}

现在开始测试耗时对比:

int main(int argc, char* argv[])
{
    Mat testInput1 = Mat::ones(6400, 5400, CV_32F);
    Mat testInput2 = Mat::ones(6400, 5400, CV_32F);

    Mat result1, result2, result3;
    clock_t start, stop;

    start = clock();
    testParallelStructWithFor(testInput1);
    stop = clock();
    cout << "Running time using \'general for \':" << (double)(stop - start) / CLOCKS_PER_SEC * 1000 << "ms" << endl;

    start = clock();
    testParallelStructWithParallel_for(testInput1);
    stop = clock();
    cout << "Running time using \'parallel for \':" << (double)(stop - start) / CLOCKS_PER_SEC * 1000 << "ms" << endl;

    start = clock();
    testInput1.mul(testInput1).mul(testInput1);
    stop = clock();
    cout << "Running time using \'mul function \':" << (double)(stop - start) / CLOCKS_PER_SEC * 1000 << "ms" << endl;

    return 0;
}

可以发现,并行运算效率有着显著提升,但是相较于OpenCV的标准实现,依旧偏慢。因此在能够使用OpenCV标准函数实现的时候,尽量不要再自己定义运算,标准的OpenCV函数式经过优化了的,运算效率很高。

4. Parallel_for_不结合与结合ParallelLoopBody比较

事实上在c++11中,不一定要用一个类或结构体去继承并行计算循环体类(ParallelLoopBody),可以使用函数来替代。两种方式的比较在下面可见:

class Parallel_My : public ParallelLoopBody
{
public:
    Parallel_My (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++)
        {

        }
    }

    Parallel_My& operator=(const Parallel_My &) {
        return *this;
    };

private:
    Mat &m_img;
    float m_x1;
    float m_y1;
    float m_scaleX;
    float m_scaleY;
};

int main()
{

    Mat Img(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);

    #ifdef CV_CXX11
    parallel_for_(Range(0, Img.rows*tImg.cols), [&](const Range& range)
    {
        for (int r = range.start; r < range.end; r++)
        {

        }
    });

    #else
    Parallel_My parallel_my0(Img, x1, y1, scaleX, scaleY);
    parallel_for_(Range(0, Img.rows*Img.cols), parallel_my0);
    #endif
}

5. 单层光流(补充内容)

上面讲的应该比较清楚了,可以看一个关于光流的示例:

class OpticalFlowTracker {
public:
    OpticalFlowTracker(
        const Mat &img1_,
        const Mat &img2_,
        const vector<KeyPoint> &kp1_,
        vector<KeyPoint> &kp2_,
        vector<bool> &success_,
        bool inverse_ = true, bool has_initial_ = false) :
        img1(img1_), img2(img2_), kp1(kp1_), kp2(kp2_), success(success_), inverse(inverse_),
        has_initial(has_initial_) {}

    void calculateOpticalFlow(const Range &range);

private:
    const Mat &img1;
    const Mat &img2;
    const vector<KeyPoint> &kp1;
    vector<KeyPoint> &kp2;
    vector<bool> &success;
    bool inverse = true;
    bool has_initial = false;
};

void OpticalFlowSingleLevel(
    const Mat &img1,
    const Mat &img2,
    const vector<KeyPoint> &kp1,
    vector<KeyPoint> &kp2,
    vector<bool> &success,
    bool inverse, bool has_initial) {
    kp2.resize(kp1.size());
    success.resize(kp1.size());
    OpticalFlowTracker tracker(img1, img2, kp1, kp2, success, inverse, has_initial);
    parallel_for_(Range(0, kp1.size()),
                  std::bind(&OpticalFlowTracker::calculateOpticalFlow, &tracker, placeholders::_1));
}

void OpticalFlowTracker::calculateOpticalFlow(const Range &range) {

    int half_patch_size = 4;
    int iterations = 10;
    for (size_t i = range.start; i < range.end; i++) {
        auto kp = kp1[i];
        double dx = 0, dy = 0;
        if (has_initial) {
            dx = kp2[i].pt.x - kp.pt.x;
            dy = kp2[i].pt.y - kp.pt.y;
        }

        double cost = 0, lastCost = 0;
        bool succ = true;

        Eigen::Matrix2d H = Eigen::Matrix2d::Zero();
        Eigen::Vector2d b = Eigen::Vector2d::Zero();
        Eigen::Vector2d J;
        for (int iter = 0; iter < iterations; iter++) {
            if (inverse == false) {
                H = Eigen::Matrix2d::Zero();
                b = Eigen::Vector2d::Zero();
            } else {

                b = Eigen::Vector2d::Zero();
            }

            cost = 0;

            for (int x = -half_patch_size; x < half_patch_size; x++)
                for (int y = -half_patch_size; y < half_patch_size; y++) {
                    double error = GetPixelValue(img1, kp.pt.x + x, kp.pt.y + y) -
                                   GetPixelValue(img2, kp.pt.x + x + dx, kp.pt.y + y + dy);;
                    if (inverse == false) {
                        J = -1.0 * Eigen::Vector2d(
                            0.5 * (GetPixelValue(img2, kp.pt.x + dx + x + 1, kp.pt.y + dy + y) -
                                   GetPixelValue(img2, kp.pt.x + dx + x - 1, kp.pt.y + dy + y)),
                            0.5 * (GetPixelValue(img2, kp.pt.x + dx + x, kp.pt.y + dy + y + 1) -
                                   GetPixelValue(img2, kp.pt.x + dx + x, kp.pt.y + dy + y - 1))
                        );
                    } else if (iter == 0) {

                        J = -1.0 * Eigen::Vector2d(
                            0.5 * (GetPixelValue(img1, kp.pt.x + x + 1, kp.pt.y + y) -
                                   GetPixelValue(img1, kp.pt.x + x - 1, kp.pt.y + y)),
                            0.5 * (GetPixelValue(img1, kp.pt.x + x, kp.pt.y + y + 1) -
                                   GetPixelValue(img1, kp.pt.x + x, kp.pt.y + y - 1))
                        );
                    }

                    b += -error * J;
                    cost += error * error;
                    if (inverse == false || iter == 0) {

                        H += J * J.transpose();
                    }
                }

            Eigen::Vector2d update = H.ldlt().solve(b);

            if (std::isnan(update[0])) {

                cout << "update is nan" << endl;
                succ = false;
                break;
            }

            if (iter > 0 && cost > lastCost) {
                break;
            }

            dx += update[0];
            dy += update[1];
            lastCost = cost;
            succ = true;

            if (update.norm() < 1e-2) {

                break;
            }
        }

        success[i] = succ;

        kp2[i].pt = kp.pt + Point2f(dx, dy);
    }
}

代码在 calculateOpticalFlow 函数中实现了单层光流函数,其中调用了 cv::parallel_for_ 并行调用 OpticalFlowTracker::calculateOpticalFlow 该函数计算指定范围内特征点的光流。 这个并行 for 循环内部是 Intel tbb 库实现的,我们只需按照其接口,将函数本体定义出来,然后将函数作为 std::function 对象传递给它

如果对 std::bind 不太熟悉,可以参考之前的文章:

Original: https://blog.csdn.net/qq_28087491/article/details/118992396
Author: 泠山
Title: OpenCV 并行计算函数 parallel_for_ 的使用

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/645263/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球