如何使用 OpenCV parallel_for_并行化代码

   返回:OpenCV系列文章目录(持续更新中......)

上一篇:OpenCV 如何使用 XML 和 YAML 文件的文件输入和输出

下一篇:OpenCV系列文章目录(持续更新中......)

目标

本教程的目的是演示如何使用 OpenCV 框架轻松并行化代码。为了说明parallel_for_这个概念,我们将编写一个程序来对图像执行卷积运算。完整的教程代码在这里。

前提

并行框架

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

  • 英特尔线程构建模块(第三方库,应显式启用)
  • OpenMP(集成到编译器中,应显式启用)
  • APPLE GCD(系统范围,自动使用(仅限 APPLE))
  • Windows RT 并发(系统范围,自动使用(仅限 Windows RT))
  • Windows 并发(运行时的一部分,自动使用(仅限 Windows - MSVC++ >= 10))
  • 螺纹

如您所见,OpenCV 库中可以使用多个并行框架。一些并行库是第三方库,在构建之前必须在 CMake 中显式启用,而其他并行库则在平台上自动可用(例如 APPLE GCD)。

假如条件

当多个线程尝试同时写入读取和写入特定内存位置时,会出现争用情况。基于此,我们可以将算法大致分为两类:-

  1. 只有单个线程将数据写入特定内存位置的算法。
    • 例如,在卷积中,即使多个线程可以在特定时间从一个像素读取,也只有一个线程入特定像素。
  2. 多个线程可以写入单个内存位置的算法。
    • 寻找轮廓、特征等。这种算法可能要求每个线程同时向全局变量添加数据。例如,在检测特征时,每个线程会将其图像各自部分的特征添加到公共向量中,从而创建争用条件。

卷积

我们将使用执行卷积的示例来演示parallel_for_ 并行化计算的用法。这是一个不会导致争用条件的算法示例。

理论

卷积是一种在图像处理中广泛使用的简单数学运算。在这里,我们在图像上滑动一个较小的矩阵,称为内核,内核中像素值和相应值的乘积之和为我们提供了输出中特定像素的值(称为内核的锚点)。根据内核中的值,我们得到不同的结果。在下面的示例中,我们使用一个 3x3 内核(锚定在其中心)并在 5x5 矩阵上卷积以生成一个 3x3 矩阵。可以通过用合适的值填充输入来更改输出的大小。

有关不同内核及其作用的更多信息,请查看此处

在本教程中,我们将实现最简单的函数形式,该函数采用灰度图像(1 通道)和奇数长度的方形核并生成输出图像。该操作将不会就地执行。

注意

我们可以暂时存储一些相关的像素,以确保我们在卷积期间使用原始值,然后就地进行。但是,本教程的目的是介绍parallel_for_函数,就地实现可能过于复杂。

伪代码

InputImage src, OutputImage dst, kernel(size n)
makeborder(src, n/2)
for each pixel (i, j) strictly inside borders, do:
{value := 0for k := -n/2 to n/2, do:for l := -n/2 to n/2, do:value += kernel[n/2 + k][n/2 + l]*src[i + k][j + l]dst[i][j] := value
}

对于 n 大小的内核,我们将添加大小为 n/2 的边框来处理边缘情况。然后,我们运行两个循环来沿内核移动并将乘积相加

实现

顺序实现

void conv_seq(Mat src, Mat &dst, Mat kernel)
{int rows = src.rows, cols = src.cols;dst = Mat(rows, cols, src.type());// Taking care of edge values// Make border = kernel.rows / 2;int sz = kernel.rows / 2;copyMakeBorder(src, src, sz, sz, sz, sz, BORDER_REPLICATE);for (int i = 0; i < rows; i++){uchar *dptr = dst.ptr(i);for (int j = 0; j < cols; j++){double value = 0;for (int k = -sz; k <= sz; k++){// slightly faster results when we create a ptr due to more efficient memory access.uchar *sptr = src.ptr(i + sz + k);for (int l = -sz; l <= sz; l++){value += kernel.ptr<double>(k + sz)[l + sz] * sptr[j + sz + l];}}dptr[j] = saturate_cast<uchar>(value);}}
}

我们首先制作一个与 src 大小相同的输出矩阵 (dst),并在 src 图像中添加边框(以处理边缘情况)。

int rows = src.rows, cols = src.cols;
dst = Mat(rows, cols, src.type());
// Taking care of edge values
// Make border = kernel.rows / 2;
int sz = kernel.rows / 2;
copyMakeBorder(src, src, sz, sz, sz, sz, BORDER_REPLICATE);

然后,我们按顺序迭代 src 图像中的像素,并计算内核和相邻像素值的值。然后,我们将值填充到 dst 图像中的相应像素。

 for (int i = 0; i < rows; i++){uchar *dptr = dst.ptr(i);for (int j = 0; j < cols; j++){double value = 0;for (int k = -sz; k <= sz; k++){// slightly faster results when we create a ptr due to more efficient memory access.uchar *sptr = src.ptr(i + sz + k);for (int l = -sz; l <= sz; l++){value += kernel.ptr<double>(k + sz)[l + sz] * sptr[j + sz + l];}}dptr[j] = saturate_cast<uchar>(value);}}

并行实施

在查看顺序实现时,我们可以注意到每个像素都依赖于多个相邻像素,但一次只编辑一个像素。因此,为了优化计算,我们可以将图像拆分为条带,并通过利用现代处理器的多核架构对每个条带并行执行卷积。OpenCV cv::p arallel_for_ 框架自动决定如何有效地拆分计算,并为我们完成大部分工作。

注意

尽管特定条带中的像素值可能取决于条带外的像素值,但这些只是只读操作,因此不会导致未定义的行为。

我们首先声明一个继承自 cv::ParallelLoopBody 的自定义类,并覆盖 .virtual void operator ()(const cv::Range& range) const

We first declare a custom class that inherits from cv::ParallelLoopBody and override the virtual void operator ()(const cv::Range& range) const.

class parallelConvolution : public ParallelLoopBody
{
private:Mat m_src, &m_dst;Mat m_kernel;int sz;public:parallelConvolution(Mat src, Mat &dst, Mat kernel): m_src(src), m_dst(dst), m_kernel(kernel){sz = kernel.rows / 2;}virtual void operator()(const Range &range) const CV_OVERRIDE{for (int r = range.start; r < range.end; r++){int i = r / m_src.cols, j = r % m_src.cols;double value = 0;for (int k = -sz; k <= sz; k++){uchar *sptr = m_src.ptr(i + sz + k);for (int l = -sz; l <= sz; l++){value += m_kernel.ptr<double>(k + sz)[l + sz] * sptr[j + sz + l];}}m_dst.ptr(i)[j] = saturate_cast<uchar>(value);}}
};

operator ()中的范围表示将由单个线程处理的值的子集。根据要求,可能有不同的方法来分割范围,这反过来又会改变计算。

例如,我们可以

  1. 拆分图像的整个遍历,并按以下方式获取 [row, col] 坐标(如上代码所示):

 virtual void operator()(const Range &range) const CV_OVERRIDE{for (int r = range.start; r < range.end; r++){int i = r / m_src.cols, j = r % m_src.cols;double value = 0;for (int k = -sz; k <= sz; k++){uchar *sptr = m_src.ptr(i + sz + k);for (int l = -sz; l <= sz; l++){value += m_kernel.ptr<double>(k + sz)[l + sz] * sptr[j + sz + l];}}m_dst.ptr(i)[j] = saturate_cast<uchar>(value);}}

 然后,我们将按以下方式调用 parallel_for_ 函数:

 parallelConvolution obj(src, dst, kernel);parallel_for_(Range(0, rows * cols), obj);

拆分行并计算每行:

 virtual void operator()(const Range &range) const CV_OVERRIDE{for (int i = range.start; i < range.end; i++){uchar *dptr = dst.ptr(i);for (int j = 0; j < cols; j++){double value = 0;for (int k = -sz; k <= sz; k++){uchar *sptr = src.ptr(i + sz + k);for (int l = -sz; l <= sz; l++){value += kernel.ptr<double>(k + sz)[l + sz] * sptr[j + sz + l];}}dptr[j] = saturate_cast<uchar>(value);}}}

在本例中,我们调用具有不同范围的 parallel_for_ 函数:

 parallelConvolutionRowSplit obj(src, dst, kernel);parallel_for_(Range(0, rows), obj);

 

注意

在我们的例子中,两种实现的性能相似。在某些情况下,可能会提供更好的内存访问模式或其他性能优势。

若要设置线程数,可以使用: cv::setNumThreads。您还可以使用 cv::p arallel_for_ 中的 nstripes 参数指定拆分次数。例如,如果您的处理器有 4 个线程,则设置cv::setNumThreads(2)或设置nstripes=2应与默认情况下相同,它将使用所有可用的处理器线程,但仅在两个线程上拆分工作负载。

注意

C++ 11 标准允许通过删除类parallelConvolution并将其替换为 lambda 表达式来简化并行实现:

 parallel_for_(Range(0, rows * cols), [&](const Range &range){for (int r = range.start; r < range.end; r++){int i = r / cols, j = r % cols;double value = 0;for (int k = -sz; k <= sz; k++){uchar *sptr = src.ptr(i + sz + k);for (int l = -sz; l <= sz; l++){value += kernel.ptr<double>(k + sz)[l + sz] * sptr[j + sz + l];}}dst.ptr(i)[j] = saturate_cast<uchar>(value);}});

  • 512x512 输入和 5x5 内核
  This program shows how to use the OpenCV parallel_for_ function andcompares the performance of the sequential and parallel implementations for aconvolution operationUsage:./a.out [image_path -- default lena.jpg]Sequential Implementation: 0.0953564sParallel Implementation: 0.0246762sParallel Implementation(Row Split): 0.0248722s
  • 512x512 输入,带有 3x3 内核 并行实现的性能取决于您拥有的 CPU 类型。例如,在 4 核 - 8 线程 CPU 上,运行时可能比顺序实现快 6 到 7 倍。有很多因素可以解释为什么我们没有实现 8 倍的加速

  This program shows how to use the OpenCV parallel_for_ function andcompares the performance of the sequential and parallel implementations for aconvolution operationUsage:./a.out [image_path -- default lena.jpg]Sequential Implementation: 0.0301325sParallel Implementation: 0.0117053sParallel Implementation(Row Split): 0.0117894s

  • 创建和管理线程的开销,
  • 并行运行的后台进程,
  • 4 个硬件内核(每个内核有 2 个逻辑线程)和 8 个硬件内核之间的区别。

在本教程中,我们使用了水平渐变滤镜(如上面的动画所示),它生成了一个突出显示垂直边缘的图像。

结果图像


参考文献:

1、How to use the OpenCV parallel_for_ to parallelize your code

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/577153.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

P5210A泰克P5210A高压差分探头

181/2461/8938产品概述&#xff1a; P5210A 是 Tektronix 的差分探头。差分探头是具有两个输入的有源探头。负输入和正输入连同驱动 50 欧姆电缆的地线以传输数字示波器通道的输出 泰克 P5210A 特性&#xff1a; 带宽&#xff1a;50 兆赫 差分电压&#xff1a;1000X&#xf…

强烈安利!一分钟带你看懂Redis整合SpringBoot及如何实现读写缓存

文章目录 SpringBoot整合Redis1. 环境搭建2. 测试连接3. 存储对象3.1 Json存储3.2 JDK序列化3.3 Redis序列化 4. 简便工具类5. 读写缓存 SpringBoot整合Redis 1. 环境搭建 编辑配置文件redis.conf&#xff0c;注释bind 127.0.0.1 -::1行&#xff0c;表示允许远程连接 编辑配置文…

kubernetes-加密 Secrets

1 前言 默认 Secrets 对象的值是 base64 编码的内容&#xff0c;这个可以反编码得到原文的&#xff0c;不能起到加密重要密文的作用。 解决方法是使用开源的 Sealed Secrets 2 认识 Sealed Secrets github 地址&#xff1a; https://github.com/bitnami-labs/sealed-secre…

访学博后须知|携带手机等电子产品入境美国注意事项

美国对携带手机等电子产品入境有着严格的规定&#xff0c;因此知识人网小编提醒拟出国做访问学者、博士后或联合培养的博士生了解以下注意事项&#xff0c;尽量减少不必要的麻烦。 随着互联网的普及&#xff0c;手机等电子产品在人民生活中占有不可或缺的地位。因为研究和工作需…

换芯片流程

1寻找芯片包 2.在这里改芯片 3.改芯片容量 4.改芯片启动文件 启动文件有且只能有1个 这里是把MD删掉改HD 5.改仿真

860.柠檬水找零

// 定义一个名为Solution的类 class Solution {// 定义一个公开的方法lemonadeChange&#xff0c;接收一个整数数组作为参数&#xff0c;并返回一个布尔值public boolean lemonadeChange(int[] bills) {// 初始化持有5元和10元硬币的数量分别为0int five 0;int ten 0;// 遍历…

Vitis AI——FPGA学习笔记<?>

参考资料&#xff1a; Xilinx/Vitis-AI-Tutorials (github.com) Xilinx/Vitis-AI: Vitis AI is Xilinx’s development stack for AI inference on Xilinx hardware platforms 【03】ALINX Zynq UltraScale MPSoC XILINX FPGA视频教程Vitis AI开发 一. 简介 1.简介 边缘计…

C++从入门到精通——函数重载

函数重载 前言一、函数重载概念二、函数重载的分类参数类型不同的函数重载参数个数不同的函数重载参数类型顺序不同的函数重载 三、函数重载的具体代码展示main.cpp 四、为什么为什么C支持函数重载&#xff0c;而C语言不支持函数重载呢 前言 函数重载是指在同一个作用域内&…

SpringMVC注解及使用规则

文章目录 前言一、SpringMVC注解是什么&#xff1f;二、使用步骤1.注解使用2创建JSP3 SpringMVC视图1. 逻辑视图&#xff08;Logical View&#xff09;2. 物理视图&#xff08;Physical View&#xff09;区别和关系 4 SpringMVC注解总结 总结 前言 提示&#xff1a;这里可以添…

Python批量提取图像灰度共生矩阵(GLCM)、支持批量处理、任意图像格式

目录 一、介绍 二、实现 1、特征计算 2、批量处理 3、结果 一、介绍 灰度共生矩阵(Grey Level Co-occurrence Matrix)也叫做空间灰度级依赖矩阵(SGLDM),它是一种基于统计的纹理特征提取的方法。 一般包括四个方向&#xff1a; (a,b)(1,0)&#xff0c;像素对是水平的&am…

千川素材投放效果如何追踪:精准识别爆款、潜力、首发、优质素材

在数字营销和广告领域&#xff0c;素材投放的效果直接关乎广告的成功与否。为了在竞争激烈的市场中脱颖而出&#xff0c;广告主和广告从业者需要密切关注素材投放效果&#xff0c;并及时识别出不同类型的素材&#xff0c;如爆款、潜力、首发和优质素材。本文将详细探讨如何进行…

淘宝订单中的涉及红包检测、优惠券检测方案|工具|API

首先&#xff0c;检测订单红包的核心价值是什么&#xff1f; “红包的本质就是薅平台羊毛&#xff1a;不用怀疑&#xff0c;平台对于这种损害平台利益的行为肯定是最高等级的稽查”。那么&#xff0c;在日常运营中&#xff0c;需要尽可能过滤这类订单。 其次&#xff0c;如何使…