如何使用 OpenCV 扫描图像、查找表和时间测量

目标

我们将寻求以下问题的答案:

  • 如何浏览图像的每个像素?
  • OpenCV 矩阵值是如何存储的?
  • 如何衡量我们算法的性能?
  • 什么是查找表,为什么要使用它们?

我们的测试用例

让我们考虑一种简单的颜色减少方法。通过使用无符号字符 C 和 C++ 类型进行矩阵项存储,像素通道最多可以有 256 个不同的值。对于三通道图像,这可能会导致形成太多的颜色(准确地说是 1600 万)。使用如此多的色调可能会对我们的算法性能造成沉重打击。但是,有时只需使用更少的人即可获得相同的最终结果。

在这种情况下,我们通常会减少色彩空间。这意味着我们将颜色空间当前值除以新的输入值,最终得到更少的颜色。例如,0 到 9 之间的每个值都采用新值 0,10 到 19 之间的每个值都采用值 10,依此类推。

当您将 uchar(无符号 char - 又名介于 0 和 255 之间的值)值与 int 值相除时,结果也将是 char。这些值只能是 char 值。因此,任何分数都将向下舍入。利用这一事实,uchar 域中的上层操作可以表示为:

一个简单的色彩空间缩减算法将包括仅通过图像矩阵的每个像素并应用此公式。值得注意的是,我们进行了除法和乘法运算。对于系统来说,这些操作的成本非常高。如果可能的话,值得通过使用更便宜的操作来避免它们,例如一些减法、加法或在最好的情况下进行简单的赋值。此外,请注意,我们只有有限数量的上层操作的输入值。在 uchar 系统的情况下,确切地说是 256。

因此,对于较大的图像,明智的做法是事先计算所有可能的值,并且在分配过程中,只需使用查找表进行分配即可。查找表是简单的数组(具有一个或多个维度),对于给定的输入值变体,它保存最终输出值。它的优势在于我们不需要进行计算,我们只需要读取结果。

我们的测试用例程序(以及下面的代码示例)将执行以下操作:读取作为命令行参数传递的图像(它可以是彩色或灰度),并使用给定的命令行参数整数值应用缩减。在 OpenCV 中,目前有三种主要方法可以逐像素地浏览图像。为了让事情变得更有趣,我们将使用这些方法中的每一种对图像进行扫描,并打印出花费了多长时间。

您可以在此处下载完整的源代码,也可以在 OpenCV 的 samples 目录中查找核心部分的 cpp 教程代码。它的基本用法是:

how_to_scan_images imageName.jpg intValueToReduce [G]

最后一个参数是可选的。如果给定图像将以灰度格式加载,否则使用 BGR 色彩空间。第一件事是计算查找表。

int 除以 = 0;将输入字符串转换为数字 - C++ 样式
字符串流 s;
s << argv[2];
s >> divideWith;
if (!s || !divideWith)
{
cout << “输入的用于除法的无效数字。 ” << endl;
返回 -1;
}
乌查尔表[256];
forint i = 0; i < 256; ++i)
table[i] = (uchar)(除法 * (i/divideWith));

在这里,我们首先使用 C++ stringstream 类将第三个命令行参数从文本转换为整数格式。然后我们使用简单的外观和上面的公式来计算查找表。这里没有特定于 OpenCV 的东西。

另一个问题是我们如何测量时间?OpenCV提供了两个简单的函数来实现这个 cv::getTickCount() 和 cv::getTickFrequency() 。第一个返回某个事件(例如自您启动系统以来)的系统 CPU 的滴答数。第二个返回 CPU 在一秒钟内发出滴答声的次数。因此,测量两次操作之间经过的时间就像以下几点一样简单:

double t = (double)getTickCount();
做点什么。。。
t = ((double)getTickCount() - t)/getTickFrequency();
cout << “以秒为单位的时间:” << t << endl;

图像矩阵如何存储在内存中?

正如您已经在我的 Mat - 基本图像容器教程中读到的那样,矩阵的大小取决于所使用的颜色系统。更准确地说,这取决于使用的通道数量。对于灰度图像,我们有如下内容:

对于多通道图像,列包含的子列数与通道数一样多。例如,在BGR颜色系统的情况下:

请注意,通道的顺序是相反的:BGR 而不是 RGB。因为在许多情况下,内存足够大,可以连续存储行,所以行可能会一个接一个地跟随,从而形成一个长行。因为所有内容都在一个地方,所以一个接一个地进行,这可能有助于加快扫描过程。我们可以使用 cv::Mat::isContinuous() 函数来询问矩阵是否是这种情况。继续下一部分以查找示例。

高效的方式

在性能方面,您无法击败经典的 C 风格 operator[](指针)访问。因此,我们可以推荐的最有效的分配方法是:

Mat& ScanImageAndReduceC(Mat& I, const uchar* const table)
{
仅接受 char 类型矩阵
CV_Assert(I.depth() == CV_8U);
int 通道 = I.channels();
int nRows = I.rows;
int nCols = I.cols * 通道;
如果 (I.isContinuous())
{
nCols *= n行;
n行 = 1;
}
整数 i,j;
uchar* p;
for( i = 0; i < nRows; ++i)
{
p = I.ptr<uchar>(i);
for ( j = 0; j < nCols; ++j)
{
p[j] = 表[p[j]];
}
}
返回我;
}

在这里,我们基本上只是获取一个指向每行开头的指针,并遍历它直到它结束。在矩阵以连续方式存储的特殊情况下,我们只需要请求指针一次,然后一直到最后。我们需要注意彩色图像:我们有三个通道,因此我们需要在每行中通过三倍以上的项目。

还有另一种方法。Mat 对象的数据成员返回指向第一行第一列的指针。如果此指针为 null,则该对象中没有有效的输入。检查这是检查图像加载是否成功的最简单方法。如果存储是连续的,我们可以使用它来遍历整个数据指针。如果是灰度图像,这将如下所示:

uchar* p = I.数据;
forunsigned int i = 0; i < ncol*nrows; ++i)
*p++ = 表[*p];

你会得到相同的结果。但是,此代码稍后更难阅读。如果你在那里有一些更先进的技术,那就更难了。此外,在实践中,我观察到你会得到相同的性能结果(因为大多数现代编译器可能会自动为你制作这个小的优化技巧)。

迭代器(安全)方法

在有效方法的情况下,请确保您通过适量的 uchar 字段并跳过行之间可能出现的间隙是您的责任。迭代器方法被认为是一种更安全的方法,因为它从用户手中接管了这些任务。您需要做的就是询问图像矩阵的开始和结束,然后增加开始迭代器,直到到达末尾。若要获取迭代器指向的值,请使用 * 运算符(在它前面添加它)。

Mat& ScanImageAndReduceIterator(Mat& I, const uchar* const table)
{
仅接受 char 类型矩阵
CV_Assert(I.depth() == CV_8U);
const int 通道 = I.channels();
switch(通道)
{
案例一:
{
MatIterator_<uchar> 它,结束;
for( it = I.begin<uchar>(), end = I.end<uchar>(); it != end; ++it)
*it = table[*it];
;
}
案例3:
{
MatIterator_<Vec3b> 它,结束;
for( it = I.begin<Vec3b>(), end = I.end<Vec3b>(); it != end; ++it)
{
(*它)[0] = 表[(*it)[0]];
(*它)[1] = 表[(*it)[1]];
(*它)[2] = 表[(*it)[2]];
}
}
}
返回我;
}

对于彩色图像,我们每列有三个 uchar 项目。这可以被认为是 uchar 项目的短向量,它已在 OpenCV 中以 Vec3b 名称受洗。要访问第 n 个子列,我们使用简单的 operator[] 访问。重要的是要记住,OpenCV 迭代器会遍历列并自动跳到下一行。因此,对于彩色图像,如果您使用简单的 uchar 迭代器,您将只能访问蓝色通道值。

动态地址计算和引用返回

不建议使用最后一种方法进行扫描。它是为了获取或修改图像中的随机元素而制作的。它的基本用途是指定要访问的项的行号和列号。在我们早期的扫描方法中,您已经注意到,我们通过哪种类型查看图像很重要。这里没有什么不同,因为您需要手动指定在自动查找时使用的类型。在以下源代码的灰度图像中,您可以观察到这一点(+ cv::Mat::at() 函数的使用):

Mat& ScanImageAndReduceRandomAccess(Mat& I, const uchar* const table)
{
仅接受 char 类型矩阵
CV_Assert(I.depth() == CV_8U);
const int 通道 = I.channels();
switch(通道)
{
案例一:
{
forint i = 0; i < I.rows; ++i)
forint j = 0; j < I.cols; ++j )
I.at<uchar>(i,j) = table[I.at<uchar>(i,j)];
;
}
案例3:
{
Mat_<Vec3b> _I = 我;
forint i = 0; i < I.rows; ++i)
forint j = 0; j < I.cols; ++j )
{
_I(i,j)[0] = table[_I(i,j)[0]];
_I(i,j)[1] = table[_I(i,j)[1]];
_I(i,j)[2] = table[_I(i,j)[2]];
}
我 = _I;
;
}
}
返回我;
}

该函数采用您的输入类型和坐标,并计算查询项的地址。然后返回对该引用的引用。当您获取该值时,这可能是一个常量,而在设置该值时,这可能是一个非常量。作为调试模式下的安全步骤*,将检查输入坐标是否有效且确实存在。如果不是这种情况,您将在标准错误输出流上收到一条很好的输出消息。与发布模式下的有效方法相比,使用它的唯一区别是,对于图像的每个元素,您将获得一个新的行指针,用于我们使用 C 运算符 [] 来获取列元素。

如果需要使用此方法对图像进行多次查找,则为每个访问输入类型和 at 关键字可能会很麻烦且耗时。为了解决这个问题,OpenCV 有一个 cv::Mat_ 数据类型。它与 Mat 相同,但需要额外的需求,即在定义时,您需要通过查看数据矩阵来指定数据类型,但是作为回报,您可以使用 operator() 来快速访问项目。为了让事情变得更好,这很容易从通常的 cv::Mat 数据类型转换。在上面函数的彩色图像的情况下,您可以看到它的示例用法。不过,需要注意的是,可以使用 cv::Mat::at 函数完成相同的操作(具有相同的运行时速度)。这只是为懒惰的程序员技巧编写的少一些内容。

核心功能

这是在图像中实现查找表修改的一种奖励方法。在图像处理中,通常希望将所有给定的图像值修改为其他值。OpenCV提供了修改图像值的功能,无需编写图像的扫描逻辑。我们使用核心模块的 cv::LUT() 函数。首先,我们构建一个 Mat 类型的查找表:

垫子 lookUpTable(1, 256, CV_8U);
uchar* p = lookUpTable.ptr();
forint i = 0; i < 256; ++i)
p[i] = 表[i];

最后调用函数(I 是我们的输入图像,J 是输出图像):

LUT(I, lookUpTable, J);

性能差异

为获得最佳效果,请编译程序并自行运行。为了更清楚地说明差异,我使用了一张相当大的 (2560 X 1600) 图像。此处介绍的性能适用于彩色图像。为了获得更准确的值,我将从函数调用中获得的值平均了一百次。

方法时间
高效方式79.4717 毫秒
迭 代83.7201 毫秒
即时 RA93.7878 毫秒
LUT 函数32.5759 毫秒

我们可以得出几点结论。如果可能的话,使用 OpenCV 已经制作的函数(而不是重新发明这些函数)。最快的方法是 LUT 函数。这是因为 OpenCV 库是通过英特尔线程构建模块实现多线程的。但是,如果您需要编写简单的图像扫描,则首选指针方法。迭代器是一个更安全的选择,但速度要慢得多。在调试模式下,使用动态引用访问方法进行完整映像扫描的成本最高。在发布模式下,它可能会击败迭代器方法,但肯定会为此牺牲迭代器的安全特性。

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

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

相关文章

在行情一般的情况下,就说说23级应届生如何找java工作

Java应届生找工作&#xff0c;不能单靠背面试题&#xff0c;更不能在简历中堆砌和找工作关系不大的校园实践经历&#xff0c;而是更要在面试中能证明自己的java相关商业项目经验。其实不少应届生Java求职者不是说没真实Java项目经验&#xff0c;而是不知道怎么挖掘&#xff0c;…

windows下redis使用教程

创建临时服务 redis-server.exe redis.windows.conf启动客户端 验证 # 使用set和get命令&#xff0c;对Redis数据库进行数据存储和获取&#xff0c;如下图所示 config get *创建永久服务 关闭临时服务的cmd窗口&#xff0c;输入以下命令 redis-server.exe --service-insta…

如何在文件夹中打开 powershell

在一个文件夹中&#xff0c;我们只要 按住 shift 然后在空白处右键鼠标就可以看到这个命令了

求职中遇到的性格测试

怎样才能不被刷? 最主要的就是自己的性格特征跟当前应聘的岗位是否相符合&#xff0c;这个符合程度越高&#xff0c;通过率自然也就越高。正规的做法都有一个岗位模型&#xff0c;也叫岗位胜任力模型。 以大五人格测试为例&#xff0c;完整版包含30个性格维度&#xff0c;从…

Python爬虫IP池

目录 一、介绍 1.1 为什么需要IP池&#xff1f; 1.2 IP池与代理池的区别 二、构建一个简单的IP池 三、注意事项 一、介绍 在网络爬虫的世界中&#xff0c;IP池是一个关键的概念。它允许爬虫程序在请求网页时使用多个IP地址&#xff0c;从而降低被封禁的风险&#xff0c;提高…

【C++干货铺】C++11新特性——lambda表达式 | 包装器

个人主页点击直达&#xff1a;小白不是程序媛 C系列专栏&#xff1a;C干货铺 代码仓库&#xff1a;Gitee 目录 C98中的排序 lambda表达式 lambda表达式语法 表达式中的各部分说明 lambda表达式的使用 基本的使用 [var]值传递捕捉变量var ​编辑 [&var]引用传递捕…

【C语言编程之旅 6】刷题篇-for循环

第1题 解析 思路&#xff1a; 两个循环进行控制 外层循环控制打印多少行 内部循环控制每行打印多少个表达式以及表达式内容&#xff0c; 比较简单&#xff0c;具体参考代码 #include <stdio.h> int main() {int i 0;//控制行数for(i1; i<9; i){//打印每一行内容&am…

JUC并发编程知识点总结

JMM Java内存模型规定所有的变量都存储在主内存中&#xff0c;包括实例变量&#xff0c;静态变量&#xff0c;但是不包括局部变量和方法参数。每个线程都有自己的工作内存&#xff0c;线程的工作内存保存了该线程用到的变量和主内存的副本拷贝&#xff0c;线程对变量的操作都在…

手把手教你如何搭建性能测试环境

前言 在进行性能则试前&#xff0c;需要完成性能测试的搭建工作&#xff0c;一般包括硬件环境、软件环境及网络环境&#xff0c;可以要求配置和开发工程师协助完成&#xff0c;但是作为一个优秀性能测试工程师&#xff0c;这也是你的必备技能之一。 性能测试环境与功能测试环…

常用排序算法总结(直接插入排序、选择排序、冒泡排序、堆排序、快速排序、希尔排序、归并排序)

目录 一. 直接插入排序 二:选择排序 三:冒泡排序 四.堆排序 五:希尔排序 六:快速排序(递归与非递归) 七.归并排序(递归与非递归) 一. 直接插入排序 &#x1f31f;排序思路 直接插入排序的基本原理是将一条记录插入到已排好的有序表中&#xff0c;从而得到一个新的、记录…

【白皮书下载】GPU计算在汽车中的应用

驾驶舱域控制器 (CDC) 是汽车 GPU 的传统应用领域。在这里&#xff0c;它可以驱动仪表板上的图形&#xff0c;与车辆保持高度响应和直观的用户界面&#xff0c;甚至为乘客提供游戏体验。随着车辆屏幕数量的增加和分辨率的提高&#xff0c;对汽车 GPU 在 CDC 中进行图形处理的需…

Spark On Hive配置测试及分布式SQL ThriftServer配置

文章目录 Spark On Hive的原理及配置配置步骤在代码中集成Spark On Hive Spark分布式SQL执行原理及配置配置步骤在代码中集成Spark JDBC ThriftServer 总结 Spark On Hive的原理及配置 Spark本身是一个执行引擎&#xff0c;而没有管理metadate的能力&#xff0c;当我们在执行S…

Jenkins环境配置篇-更换插件源

作为持续集成的利器 Jenkins 已经得到了广泛地应用&#xff0c;仅仅作为一个工具&#xff0c;Jenkins 已然有了 自己的生态圈&#xff0c;支持其的 plugin 更是超过 1300。在实际中如何使用以及如何更好地使用 jenkins&#xff0c;一直是大家在实践并讨论的。本系列文章将会从如…

Beego之Beego快速入门

1、beego快速入门 1.1 新建项目 新建一个项目&#xff1a; [rootzsx src]# bee new quickstart 2023/02/19 15:55:50.370 [D] init global config instance failed. If you do not use this, just ignore it. open conf/app.conf: no such file or directory 2023/02/19 1…

接口自动化框架搭建-写在前面

从今天开始&#xff0c;我将带领大家一起学习接口自动化框架的搭建&#xff0c;在学习之前&#xff0c;我们先了解搭建一个接口自动化框架需要具备哪些知识&#xff0c;应该做哪些准备工作 测试开发工程师的入门条件 近几年比较流行测试开发岗位&#xff0c;很多小伙伴都不知…

消息队列介绍

什么是 MQ MQ(message queue)&#xff0c;本质是个队列&#xff0c;FIFO 先入先出&#xff0c;只不过队列中存放的内容是 message 而已&#xff0c;还是一种跨进程的通信机制&#xff0c;用于上下游传递消息。在互联网架构中&#xff0c;MQ 是一种非常常 见的上下游“逻辑解耦…

SpringBoot 项目中后端实现跨域的5种方式!!!

文章目录 SpringBoot 项目中后端实现跨域的5种方式&#xff01;&#xff01;&#xff01;一、为什么会出现跨域问题二、什么是跨域三、非同源限制四、Java后端 实现 CORS 跨域请求的方式1、返回新的 CorsFilter(全局跨域)2、重写 WebMvcConfigurer(全局跨域)3、使用注解 (局部跨…

实战纪实 | 某配送平台zabbix 未授权访问 + 弱口令

本文由掌控安全学院 - 17828147368 投稿 找到一个某src的子站&#xff0c;通过信息收集插件wappalyzer&#xff0c;发现ZABBIX-监控系统&#xff1a; 使用谷歌搜索历史漏洞&#xff1a;zabbix漏洞 通过目录扫描扫描到后台&#xff0c;谷歌搜索一下有没有默认弱口令 成功进去了…

LeetCode:206. 反转链表

力扣链接 算法思想&#xff1a;由于单链表是单向的&#xff0c;想要对当前元素进行操作&#xff0c;需找到前一个元素。本题利用双指针&#xff0c;初始pre指针指向NULL&#xff0c;cur指针指向head.再对局部翻转之前&#xff0c;先把下一个结点存到temp指针中。当进行完如下代…

构建中国人自己的私人GPT

创作不易&#xff0c;请大家多鼓励支持。 在现实生活中&#xff0c;很多人的资料是不愿意公布在互联网上的&#xff0c;但是我们又要使用人工智能的能力帮我们处理文件、做决策、执行命令那怎么办呢&#xff1f;于是我们构建自己或公司的私人GPT变得非常重要。 先看效果 他的…