瞅瞅 Opencv:扫描图像

扫描图像查询表

  • 一、概述
  • 二、图像矩阵如何存储在内存中?
  • 三、高效的方式
  • 四、迭代器(安全)方法
  • 五、带引用返回的动态地址计算
  • 六、核心功能
  • 七、性能差异

一、概述

让我们考虑一种简单的色彩还原方法。通过使用unsigned char C和c++类型进行矩阵项存储,一个像素通道可以有多达256个不同的值。对于一个三通道图像,这可能会形成太多的颜色(确切地说是1600万)。处理如此多的色度可能会严重影响我们的算法性能。然而,有时使用更少的元素就足以获得相同的最终结果。

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

当我们将uchar (unsigned char -也就是0到255之间的值)值与int值相除时,结果也将是char。这些值只能是字符值。因此,任何分数都将四舍五入。利用这一事实,uchar域的上操作可以表示为:

Inew = (Iold10)∗10

一个简单的色彩空间缩减算法只需要遍历图像矩阵的每个像素并应用这个公式即可。值得注意的是,我们做了一个除法和一个乘法运算。这些操作对于系统来说是非常昂贵的。如果可能的话,可以通过使用一些更便宜的操作来避免它们,比如一些减法、加法,或者在最好的情况下一个简单的赋值。此外,请注意,我们只有有限数量的输入值用于上面的操作。在uchar系统中,确切地说是256。

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

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

 int divideWith = 0; // convert our input string to number - C++ style
 stringstream s;
 s << argv[2];
 s >> divideWith;
 if (!s || !divideWith)
 {
 cout << "Invalid number entered for dividing. " << endl;
 return -1;
 }
 uchar table[256];
 for (int i = 0; i < 256; ++i)
 table[i] = (uchar)(divideWith * (i/divideWith));

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

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

double t = (double)getTickCount();
// do something ...
t = ((double)getTickCount() - t)/getTickFrequency();
cout << "Times passed in seconds: " << t << endl;

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

矩阵的大小取决于所使用的颜色系统。更准确地说,它取决于所使用的通道的数量。在灰度图像的情况下,我们有这样的东西:
tutorial_how_matrix_stored_1.png

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

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

三、高效的方式

说到性能,你无法击败经典的C风格操作符访问。因此,我们可以推荐的最有效的分配方法是:

Mat& ScanImageAndReduceC(Mat& I, const uchar* const table)
{
	 // accept only char type matrices
	 CV_Assert(I.depth() == CV_8U);
	 int channels = I.channels();
	 int nRows = I.rows;
	 
	 int nCols = I.cols * channels;	//单通道就是灰度图、三通道一般就是GBR色系
	 if (I.isContinuous())
	 {
		 nCols *= nRows;
		 nRows = 1;
	 }
	 int i,j;
	 uchar* p;
	 for( i = 0; i < nRows; ++i)
	 {
		 p = I.ptr<uchar>(i);
		 for ( j = 0; j < nCols; ++j)
			 {
			 	p[j] = table[p[j]];
			 }
	 }
	 return I;
}

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

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

uchar* p = I.data;
for( unsigned int i = 0; i < ncol*nrows; ++i)
 *p++ = table[*p];

你会得到相同的结果。然而,这段代码在后面阅读起来要困难得多。如果你有一些更高级的技术,它会变得更难。此外,在实践中,我观察到我们将获得相同的性能结果(因为大多数现代编译器可能会自动为我们进行这个小优化技巧)。

四、迭代器(安全)方法

在有效的情况下,确保我们通过适当数量的uchar字段,并跳过行之间可能发生的空白是我们的责任。迭代器方法被认为是一种更安全的方法,因为它从用户那里接管了这些任务。你所需要做的就是询问图像矩阵的开始和结束,然后只增加开始迭代器,直到你到达结束。要获取迭代器所指向的值,可以使用操作符(在其前面加上)。

Mat& ScanImageAndReduceIterator(Mat& I, const uchar* const table)
{
 // accept only char type matrices
 CV_Assert(I.depth() == CV_8U);
 const int channels = I.channels();
 switch(channels)
 {
 case 1:
 {
 MatIterator_<uchar> it, end;
 for( it = I.begin<uchar>(), end = I.end<uchar>(); it != end; ++it)
 *it = table[*it];
 break;
 }
 case 3:
 {
 MatIterator_<Vec3b> it, end;
 for( it = I.begin<Vec3b>(), end = I.end<Vec3b>(); it != end; ++it)
 {
 (*it)[0] = table[(*it)[0]];
 (*it)[1] = table[(*it)[1]];
 (*it)[2] = table[(*it)[2]];
 }
 }
 }
 return I;
}

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

五、带引用返回的动态地址计算

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

Mat& ScanImageAndReduceRandomAccess(Mat& I, const uchar* const table)
{
 // accept only char type matrices
 CV_Assert(I.depth() == CV_8U);
 const int channels = I.channels();
 switch(channels)
 {
 case 1:
 {
 for( int i = 0; i < I.rows; ++i)
 for( int j = 0; j < I.cols; ++j )
 I.at<uchar>(i,j) = table[I.at<uchar>(i,j)];
 break;
 }
 case 3:
 {
 Mat_<Vec3b> _I = I;
 for( int i = 0; i < I.rows; ++i)
 for( int 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 = _I;
 break;
 }
 }
 return I;
}

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

如果我们需要使用此方法对图像进行多次查找,那么为每次访问输入类型和at关键字可能会很麻烦且耗时。为了解决这个问题,OpenCV有一个cv::Mat_数据类型。它与Mat一样,只是需要在定义时通过查看数据矩阵来指定数据类型,但是反过来,我们可以使用operator()来快速访问项。为了使事情变得更好,这很容易从通常的cv::Mat数据类型转换为cv::Mat。我们可以在上面函数的彩色图像中看到一个示例用法。然而,需要注意的是,cv::Mat::at函数也可以执行相同的操作(运行时速度相同)。这只是一个为懒惰程序员编写的小技巧。

六、核心功能

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

 Mat lookUpTable(1, 256, CV_8U);
 uchar* p = lookUpTable.ptr();
 for( int i = 0; i < 256; ++i)
 p[i] = table[i];

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

 LUT(I, lookUpTable, J);

七、性能差异

为了获得最好的结果,编译程序并自己运行它。为了使差异更清楚,我使用了一个相当大的(2560 X 1600)图像。这里展示的表演是针对彩色图像的。为了得到一个更精确的值,我已经将函数调用得到的值平均了100次。

方法时间
有效方式79.4717毫秒
迭代器83.7201毫秒
动态RA93.7878毫秒
LUT函数32.5759毫秒

我们可以得出一些结论。如果可能的话,使用OpenCV已有的函数(而不是重新发明这些函数)。最快的方法是LUT函数。这是因为OpenCV库是通过英特尔线程构建块启用多线程的。但是,如果我们需要编写一个简单的图像扫描首选指针方法。迭代器是一种更安全的选择,但是相当慢。在调试模式下,使用动态参考访问方法进行全像扫描是成本最高的。在发布模式下,它可能优于迭代器方法,也可能不优于迭代器方法,但它肯定为此牺牲了迭代器的安全特性。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

音视频技术开发周刊 | 318

每周一期&#xff0c;纵览音视频技术领域的干货。 新闻投稿&#xff1a;contributelivevideostack.com。 日程揭晓&#xff01;速览深圳站大会专题议程详解 LiveVideoStackCon 2023 音视频技术大会深圳站&#xff0c;保持着往届强大的讲师阵容以及高水准的演讲质量。两天的参会…

git commit规范提交

Git每次提交代码时&#xff0c;都要写Commit Message&#xff08;提交说明&#xff09;&#xff0c;通常情况下&#xff0c;Commit Message应该清晰明了&#xff0c;说明本次提交的目的和具体操作等。然而笔者工作多年来发现&#xff0c;有些公司对Commit Message没有明确的要求…

wpf Grid布局详解 `Auto` 和 `*` 是两种常见的设置方式 行或列占多个单元格,有点像excel里的合并单元格。使其余的列平均分配剩余的空间

比如只有行的界面 <Window x:Class"GenerateTokenApp.MainWindow"xmlns"http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x"http://schemas.microsoft.com/winfx/2006/xaml"xmlns:d"http://schemas.microsoft.com/exp…

SpringCloudAlibaba——Sentinel

Sentinel也就是我们之前的Hystrix&#xff0c;而且比Hystrix功能更加的强大。Sentinel是分布式系统的流量防卫兵&#xff0c;以流量为切入点&#xff0c;从流量控制、流量路由、熔断降级等多个维度保护服务的稳定性。 Sentinel采用的是懒加载&#xff0c;这个接口被访问一次&a…

企业级低代码开发,科技赋能让企业具备“驾驭软件的能力”

科技作为第一生产力&#xff0c;其强大的影响力在各个领域中都有所体现。数字技术&#xff0c;作为科技领域中的一股重要力量&#xff0c;正在对传统的商业模式进行深度的变革&#xff0c;为各行业注入新的生命力。随着数字技术的不断发展和应用&#xff0c;企业数字化转型的趋…

SpringBoot自动装配 Spring相关 常用设计模式 双亲委派 MongoDB Redis 适配器模式与策略模式

SpringBoot自动装配 阿里云登录 - 欢迎登录阿里云&#xff0c;安全稳定的云计算服务平台 Spring相关 阿里云登录 - 欢迎登录阿里云&#xff0c;安全稳定的云计算服务平台 常用设计模式 双亲委派 Java虚拟机定义了三个主要的类加载器: 1、启动类加载器 2、扩展类加载器 …

《网络协议》02. 物理层 · 数据链路层 · 网络层

title: 《网络协议》02. 物理层 数据链路层 网络层 date: 2022-08-31 22:26:48 updated: 2023-11-08 06:58:52 categories: 学习记录&#xff1a;网络协议 excerpt: 物理层&#xff08;数据通信模型&#xff0c;信道&#xff09;、数据链路层&#xff08;封装成帧&#xff0c…

CSDN中: Markdown编辑器使用说明

Markdown编辑器使用说明 欢迎使用Markdown编辑器新的改变功能快捷键合理的创建标题&#xff0c;有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants 创建一个自定义列表如何创建一…

Android UI 开发·界面布局开发·案例分析

目录 ​编辑 1. 线性布局&#xff08;LinearLayout&#xff09; 2. 相对布局&#xff08;RelativeLayout&#xff09; 3. 表格布局&#xff08;TableLayout&#xff09; 4. 帧布局&#xff08;FrameLayout&#xff09; 5. 网格布局&#xff08;GridLayout&#xff0…

05【保姆级】-GO语言的标识符

之前我学过C、Java、Python语言时总结的经验&#xff1a; 先建立整体框架&#xff0c;然后再去抠细节。先Know how&#xff0c;然后know why。先做出来&#xff0c;然后再去一点点研究&#xff0c;才会事半功倍。适当的囫囵吞枣。因为死抠某个知识点很浪费时间的。对于GO语言&a…

Android 10.0 系统默认打开OEM解锁开关功能实现

1.前言 在10.0的系统定制中,在9.0系统以后为了设备的安装,系统开始启用oem机制,所以在adb push文件就需要先oem解锁,然后才可以 进行相关操作,所以就需要默认打开oem解锁的开关,来方便oem解锁功能的实现 如图: 2.系统默认打开OEM解锁开关功能实现的核心类 packages\ap…

初步了解 RabbitMQ

目录 ​编辑一、MQ 概述 1、MQ 的简介 2、MQ 的用途 &#xff08;1&#xff09;限流削峰 &#xff08;2&#xff09;异步解耦 (3)数据收集 二、RabbitMQ 概述 1、RabbitMQ 简介 2、四大核心概念 3、RabbitMQ 的核心部分 ​编辑 4、名词解释&#xff1a; 三、Hello …

ESP32 C3 smartconfig一键配网报错

AP配网 在调试我的esp32c3的智能配网过程中&#xff0c;发现ap配网使用云智能App是可以正常配置的。 切记用户如果在menu菜单里使能AP配网&#xff0c;默认SSID名字为adh_PK值_MAC后6位。用户可以修改这个apssid的键值&#xff0c;但是要使用云智能app则这个名字的开头必须为ad…

香港金融科技周VERTU CSO Sophie谈Web3.0的下一个风口 手机虚拟货币移动支付

10月31日&#xff0c;香港金融科技周正式拉开帷幕。这项香港金融科技界地年度盛事今年已经踏入了第八届&#xff0c;本届活动吸引超过数百位金融科技专业人士、创业者和行业领袖现场参与&#xff0c;线上参与观众超过10万人次。 在金融科技周的圆桌会议上&#xff0c;VERTU首席…

Java-继承

1 继承 1.1 为什么需要继承 Java中使用类对现实世界中实体来进行描述&#xff0c;类经过实例化之后的产物对象&#xff0c;则可以用来表示现实中的实体&#xff0c;但是现实世界错综复杂&#xff0c;事物之间可能会存在一些关联&#xff0c;那在设计程序是就需要考虑。 以下…

Vulnhub靶场之Funbox

正如该靶场的描述所说&#xff0c;它对初学者来说非常简单。 项目地址&#xff1a;Funbox: Scriptkiddie ~ VulnHub 所需工具&#xff1a; KaliLinux即可。 0x00 信息收集 打开虚拟机后使用nmap扫描一下网段存活&#xff0c;这里我给的虚拟机的范围是100-253,其中kali的IP是10…

Git 安全警告修复手册:解决 `fatal: detected dubious ownership in repository at ` 问题 ️

&#x1f337;&#x1f341; 博主猫头虎 带您 Go to New World.✨&#x1f341; &#x1f984; 博客首页——猫头虎的博客&#x1f390; &#x1f433;《面试题大全专栏》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33a; &a…

postman接口测试

postman使用 开发中经常用postman来测试接口&#xff0c;一个简单的注册接口用postman测试&#xff1a; 接口正常工作只是最基本的要求&#xff0c;经常要评估接口性能&#xff0c;进行压力测试。 postman进行简单压力测试 下面是压测数据源&#xff0c;支持json和csv两个格式…

论文阅读——变化检测

Viewpoint Integration and Registration with Vision Language Foundation Model for Image Change Understanding 只有fused adapter image encoder, viewpoint registration flow, semantic emphasizing module, 和 fully connected layer 训练&#xff0c;其他参数冻结。 F…

Linux内核有什么之内存管理子系统有什么第三回 —— 小内存分配(1)

接前一篇文章&#xff1a;Linux内核有什么之内存管理子系统有什么第二回 —— 单刀直入 本文内容参考&#xff1a; 内存分配不再神秘&#xff1a;深入剖析malloc函数实现原理与机制 系统调用与内存管理&#xff08;sbrk、brk、mmap、munmap&#xff09; 特此致谢&#xff01;…