【OpenCV C++20 学习笔记】基本图像容器——Mat

news2024/9/27 8:56:22

【OpenCV C++20 学习笔记】基本图像容器——Mat

  • 概述
  • `Mat`
    • 内部结构
    • 引用计数机制
    • 颜色数据格式
  • 显式创建`Mat`对象
    • 使用`cv::Mat::Mat`构造函数
      • 矩阵的数据项
    • 使用数组进行初始化的构造函数
    • `cv::Mat::create`函数
    • MATLAB风格的初始化
    • 小型矩阵
    • 通过复制创建`Mat`对象
  • `Mat`对象的输出
  • 其他普通数据项的输出

概述

电子设备中储存的图像本质是图像在每个像素点上的数值,这些数据形成一个矩阵,除此之外还包括一些描述这个数值矩阵的信息。OpenCV作为一个计算机视觉库,同样也是要处理这样的信息。所以,要学习OpenCV,首要的事情是了解在OpenCV中是如何储存图像的数值矩阵以及描述信息的。
(本文较长,详细介绍了Mat对象的原理、创建方式和输出方式,读者可根据目录跳转至相关章节)

Mat

2001年OpenCV诞生的时候,是在C语言的接口上创建的,并将图像储存在一个称作IplImage的C语言数据结构中。这种方式最大的一个缺点就是需要用户自己管理内存。如果是小型的项目尚可,如果数据量变大,管理内存就会使人很头疼。
OpenCV2.0 引入了C++接口,实现了内存的自动管理。Mat成为了OpenCV储存图片信息的数据结构。
Mat不需要手动分配或释放内存,大部分的OpenCV方法都会自动为输出的Mat对象分配内存。如果你已经为一个Mat对象分配了它需要的内存,那么你在传输它的时候,这个内存会被重复利用。也就是说,在执行任务的时候,不会使用多余的内存。

内部结构

Mat实质上是一个包含了两个部分的类:

  • 矩阵头(matrix header):它包含了矩阵的大小、存储方式、存储地址等信息
  • 指向矩阵的指针:Mat对象只是储存了矩阵的指针,并没有储存矩阵本身,而矩阵中包含了像素值(像素值矩阵的维度由存储方式决定)
    Mat对象的大小是固定的,但是矩阵本身的大小是跟随图像变化的。
    在函数间传递图像是OpenCV中非常常见的操作,而且某些图像处理算法很复杂。为了提高程序的运行速度,OpenCV使用了“引用计数机制”。每个Mat对象都有自己独立的矩阵头,但是同一个矩阵可能会被多个Mat对象共享,即多个Mat对象的指针可能会指向内存中的同一个矩阵。而复制操作只会复制Mat对象的矩阵头以及指向矩阵的指针,并不会直接复制矩阵的数值!

下面的代码详细展示了Mat对象在实际应用中的内存分配问题:

Mat A, C;	//创建Mat对象的时候只是创建了矩阵头的部分
A = imread(argv[1], IMREAD_COLOR);	//读取图片,分配内存存储图片的数值矩阵,并将A的指针指向这个矩阵的内存地址

Mat B(A);	//调用复制构造函数创建B,但仅仅是将A中的指向图片矩阵的指针复制到B中,并没有复制图片的数值矩阵

C = A;	//赋值操作也只是将A中的指针复制到C中

上面的代码最终使A、B、C3个Mat对象中的指针都指向同一个图片的数值矩阵,虽然进行了复制和赋值操作,但内存中始终只有一个数值矩阵,如下图:
Mat对象示意图
因为3个Mat对象的指针都是指向同一个数据矩阵,所以在任何一个Mat对象中对数据矩阵进行修改都会影响到其他Mat对象。实际上,不同的Mat对象只是为处理同一个数据矩阵提供了不同的使用方法。但是这些Mat对象的矩阵头部分是不同的,你甚至可以创建一个只指向数据矩阵的其中一部分的Mat对象。例如,要想在图像中创建一个感兴趣区域(region of interest,ROI),你可以新建一个Mat对象:

Mat D(A, Rect(10, 10, 100, 100);	//使用矩形区域
Mat E = A(Range::all(), Range(1, 3));	//使用行和列

引用计数机制

如果像上面的例子一样,同一个数据矩阵属于不同的Mat对象,那到底谁来负责释放它的内存呢?答案是:最后一个使用它的Mat对象。这就是通过上面所说的“引用计数机制”来实现的。当有指向数据矩阵A的Mat对象被复制的时候,矩阵A的引用计数就会增加;当有指向矩阵A的Mat对象被销毁的时候,矩阵A的引用计数就会减少。当计数为0的时候,矩阵A就会被释放。
OpenCV还提供了深度复制数据矩阵的方法,当你不想只是复制指针,而是想复制矩阵的值的时候,可以使用cv::Mat::clone()cv::Mat::copyTo()方法。

Mat F = A.clone();	//将A指向的数据矩阵复制给F
Mat G;
A.copyTo(G);	//将A指向的数据矩阵复制到G

这样,修改F和G的时候就不会影响A指向的数据矩阵了。

总结一下:

  • OpenCV中函数导出的图像数据是自动分配内存的(除非特别指定不自动分配)
  • 使用OpenCV的C++接口的时候不用考虑内存管理的问题
  • 赋值运算符和复制构造函数只是复制Mat对象的头部信息和指针
  • 可以用cv::Mat::clone()cv::Mat::copyTo()方法实现底层的图片数据矩阵的复制

颜色数据格式

对于如何储存像素的值,通常从两个方面考虑:颜色空间和数据类型。
颜色空间是指利用基本的颜色组合成特定的颜色的方式。有多种方式可以选择:

  • RGB:这是最常用的,因为它与人眼编码颜色的方式相似;由红、绿、蓝3中基本颜色的值,加上透明度alpha,来确定最终颜色;注意,OpenCV中的标准颜色显示系统为BGR,红色和蓝色的值调换了位置
  • HSV和HLS:将颜色分解为色调、饱和度和亮度;这种方式能更方便地处理图片的亮度
  • YCrCb:这是JPEG格式的图片常用的颜色编码方式
  • CIT Lab*:这种编码方式能够方便测量两种颜色之间的差距
  • 灰度:只有黑色和白色两种基本颜色

显式创建Mat对象

使用cv::Mat::Mat构造函数

Mat M(2,2, CV_8U3, Scalar(0,0,255));
cout << "M = " << endl << " " << M << endl << endl;

这里使用了Mat类的其中一个构造函数。该构造函数一共包括4个参数:

  1. 行数:定义矩阵行数
  2. 列数:定义矩阵列数
  3. 数据类型:定义每个数据项的类型,下文详述
  4. Scalar常量:用来定义每个数据项的值的向量数组

矩阵的数据项

矩阵数据项的数据类型的定义遵循以下语法规则:
CV_[每个数据项的比特数][有符号或无符号][类型前缀]C[通道数量]

  • 比特数:确定每个数据项,即像素点,的数值的长度,如8比特;比特数越高,每个像素点的值域就越大,比如,32比特的浮点类型比8比特的char类型能够储存更多的颜色值
  • 有符号或无符号:确定每个数据项的值是否是有符号的(可省略,默认为无)
  • 类型前缀:如果是char类型,则为C,如果是float类型,则为F……
  • 通道数量:确定每个数据项中包含的颜色通道数量;比如,RGB颜色空间可以有4个通道,分别是红色值、绿色值、蓝色值和透明度值;通道数量可以加上括号,如CV_8UC(3) (可省略,默认为1)
    上面代码中的CV_8U3就代表每个数据项的是具有3个通道的8比特无符号的值,输出结果如下:
    Mat构造函数创建Mat对象
    可以看到矩阵中每个项有3个数值,代表3个颜色通道;共有2*2个项;每个项中的3个颜色通道的值都与Scalar中定义的相同。

使用数组进行初始化的构造函数

除了2维的矩阵,也可以创建3维矩阵的Mat对象

int sz[3]{ 2,2,2 };
Mat L(3, sz, CV_8UC1, Scalar::all(0));

这个构造函数也使用4个参数:

  1. 维度:确定矩阵的维度
  2. 大小:一个数组,用来确定每个维度的大小
  3. 数据项的数据类型:同上一个构造函数
  4. Scalar常量:同上一个构造函数
    所以,这里创建了一个3维的矩阵,每个维度都只有2个数据项,即222;每个数据项使用的都是只有1个颜色通道的8比特无符号数值;每个数据项的值都为0。

cv::Mat::create函数

这个函数看起来像是在创建一个Mat对象,但其实它只能修改已有的Mat对象。
比如,对上面创建的M对象进行修改:

M.create(4, 4, CV_8UC2);
cout << "M = "<< endl << " " << M << endl << endl;

cv::Mat::create函数使用了3个参数:

  1. 行数:修改后的行数
  2. 列数:修改后的列数
  3. 数据项类型:修改后的数据项类型
    输出结果为:
    修改Mat对象
    可以看到原本22的3颜色通道的矩阵变成了44的2颜色通道矩阵。cv::Mat::create函数为M对象重新分配了内存,使其能储存修改之后的更大的矩阵。

MATLAB风格的初始化

cv::Mat::zeros, cv::Mat::ones, cv::Mat::eye等与MATLAB语言类似的函数也可以用来初始化OpenCV中的Mat对象
zeros函数用来创建全为0值的矩阵;ones函数用来创建全为1值的矩阵;eye函数用来创建对角线为1,其他值为0的矩阵

 Mat E {Mat::eye(4, 4, CV_64F)};
 cout << "E = " << endl << " " << E << endl << endl;
 Mat O {Mat::ones(2, 2, CV_32F)};
 cout << "O = " << endl << " " << O << endl << endl;
 Mat Z {Mat::zeros(3,3, CV_8UC1)};
 cout << "Z = " << endl << " " << Z << endl << endl;

这些函数都使用相同的参数列表:

  1. 行数
  2. 列数
  3. 数据项类型
    输出结果如下:
    MATLAB风格的Mat构造函数

小型矩阵

如果要构造小型矩阵,可以直接以逗号为间隔,用<<运算符将每个值一行一行依次输入;
在C++11之后,也可以使用{}风格的初始化列表

//<<运算符
 Mat C = (Mat_<double>(3,3) << 0, -1, 0, -1, 5, -1, 0, -1, 0);
 cout << "C = " << endl << " " << C << endl << endl;
 //初始化列表
  C = (Mat_<double>({0, -1, 0, -1, 5, -1, 0, -1, 0})).reshape(3);	//reshape函数将矩阵中的数据项变成3通道类型
 cout << "C = " << endl << " " << C << endl << endl;

输出结果如下:
小型矩阵

通过复制创建Mat对象

要复制Mat对象,需要第二节讲的使用cv::Mat::clonecv::Mat::copyTo 函数

Mat对象的输出

上面的例子中的输出使用的都是默认格式,但还有几种其他的输出格式
首先使用随机数创建一个3通道的3*2矩阵

 Mat R {Mat(3, 2, CV_8UC3)};
 randu(R, Scalar::all(0), Scalar::all(255));

cv::randu()为随机数生成函数,使用3个参数:

  1. Mat对象:用来储存随机值的Mat对象
  2. 最低值:Scalar常量类型,确定随机数的最小值
  3. 最高值:Scalar常量类型,确定随机数的最大值
    接下来使用format函数定义输出格式,该函数使用两个参数:
  4. Mat对象:需要输出的Mat对象
  5. 格式定义:在Formatter中定义的枚举类型
    详见以下代码:
cout << "R (default) = " << endl << " " << R << endl << endl;
cout << "R (Python) = " << endl << format(R, Formatter::FMT_PYTHON) << endl << endl;
cout << "R (csv) = " << endl << format(R, Formatter::FMT_CSV) << endl << endl;
cout << "R (numpy) = " << endl << format(R, Formatter::FMT_NUMPY) << endl << endl;
cout << "R (C) = " << endl << format(R, Formatter::FMT_C) << endl << endl;

输出结果如下:
Mat对象的输出格式

其他普通数据项的输出

OpenCV中的大部分数据结构都支持<<运算符
以下代码展示了如何运用<<运算符输出点、向量类型的对象

Point2f P(5, 1);
cout << "Point (2D)= " << P << endl << endl;

Point3f P3f(2, 6, 7);
cout << "Point (3D) = " << P3f << endl << endl;

vector<float> v;
v.push_back((float)CV_PI); v.push_back(2); v.push_back(3.01f);
cout << "Vector of floats via Mat = " << Mat(v) << endl << endl;

vector<Point2f> vPoints(20);
for (size_t i = 0; i < vPoints.size(); ++i)
	vPoints[i] = Point2f((float)(i * 5), (float)(i % 7));
cout << "A vector of 2D Points = " << vPoints << endl << endl;

输出结果如下:

输出数据对象

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

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

相关文章

软考:软件设计师 — 5.计算机网络

五. 计算机网络 1. OSI 七层模型 层次名称主要功能主要设备及协议7应用层实现具体的应用功能 POP3、FTP、HTTP、Telent、SMTP DHCP、TFTP、SNMP、DNS 6表示层数据的格式与表达、加密、压缩5会话层建立、管理和终止会话4传输层端到端的连接TCP、UDP3网络层分组传输和路由选择 三…

Spring事件机制

文章目录 一、Spring事件二、实现Spring事件1、自定义事件2、事件监听器2.1 实现ApplicationListener接口2.2 EventListener2.3 TransactionalEventListener 3、事件发布4、异步使用 三、EventBus1、事件模式2、EventBus三要素3、同步事件3.1 定义事件类3.2 定义事件监听3.3 测…

vscode回退不显示了,不方便操作

一、后退前进按钮 顶部显示&#xff0c;方便调试 <—— ——> 文件-> 首选项 -> 设置->commandcenter->勾选 Window: Title Bar Style->custom 将native —>custom

STM32是使用的内部时钟还是外部时钟

STM32是使用的内部时钟还是外部时钟&#xff0c;经常会有人问这个问题。 1、先了解时钟树&#xff0c;见下图&#xff1a; 2、在MDK中&#xff0c;使用的是HSEPLL作为SYSCLK&#xff0c;因此需要对时钟配置寄存器&#xff08;RCC_CFGR&#xff09;进行配置&#xff0c;寄存器内…

Linux:传输层(2) -- TCP协议(2)

目录 1. 流量控制 2. 滑动窗口 3. 拥塞控制 4. 延迟应答 5. 捎带应答 6. 面向字节流 7. 粘包问题 8. TCP异常情况 1. 流量控制 接收端处理数据的速度是有限的. 如果发送端发的太快 , 导致接收端的缓冲区被打满 , 这个时候如果发送端继续发送 , 就会造成丢包, 继而引…

享元模式(结构型)

目录 一、前言 二、享元模式 三、总结 一、前言 享元模式&#xff08;Flyweight Pattern&#xff09;是一种结构型设计模式&#xff0c;用于减少大量细粒度对象的内存占用。它通过共享尽可能多的相同数据来节约内存空间。 享元模式由以下角色组成&#xff1a; Flyweight&…

【OpenCV C++20 学习笔记】扫描图片数据

扫描图片数据 应用情景图像数据扫描的难点颜色空间缩减&#xff08;color space reduction&#xff09;查询表 扫描算法计算查询表统计运算时长连续内存3种扫描方法C风格的扫描方法迭代器方法坐标方法LUT方法 算法效率对比结论 应用情景 图像数据扫描的难点 在上一篇文章《基…

项目一缓存商品

文章目录 概要整体架构流程技术细节小结 概要 因为商品是经常被浏览的,所以数据库的访问量就问大大增加,造成负载过大影响性能,所以我们需要把商品缓存到redis当中,因为redis是存在内存中的,所以效率会比MySQL的快. 整体架构流程 技术细节 我们在缓存时需要保持数据的一致性所…

AHK是让任何软件都支持 Shift + 鼠标滚轮 实现界面水平滚动

目录 基本介绍 详细特点 图解安装 下载失败&#xff1f;缓慢&#xff1f; 创建并运行脚本代码&#x1f603; 新建空 xxx.ahk文件 vscode/记事本等编辑工具打开 复制并粘贴简易脚本 运行 其他问题 问题一&#xff1a;弹出无法执行此脚本 关闭脚本 基本介绍 AutoHot…

zookeeper开启SASL权限认证

目录 一、SASL介绍 二、使用 SASL 进行身份验证 2.1 服务器到服务器的身份验证 2.2 客户端到服务器身份验证 三、验证功能 一、SASL介绍 默认情况下&#xff0c;ZooKeeper 不使用任何形式的身份验证并允许匿名连接。但是&#xff0c;它支持 Java 身份验证与授权服务(JAAS)…

用户需要什么-软件的工程可用性(第一部分)01

Larry L. Constantine 著&#xff0c;Huang Yin 译 “究竟用户的需要是什么&#xff1f;”如果 Fred 作为一个程序员而不是一个心理学家时他可能会提出这 样一个问题。用户们通常需要更多&#xff0c;而开发人员似乎看上去并不能很好的领会并更好的满足他们。对于我们而言&…

WPF使用TouchSocket实现Tcp client

文章目录 前言1、页面展示2、主页面UI代码2、TCP client的UI代码3、Tcp client后台代码实现4、UI与后台代码的关联 前言 在该篇的Demo中&#xff0c;您可以找到以下内容&#xff1a; 1、TouchSocket的使用&#xff1b; 2、CommunityToolkit.Mvvm的使用&#xff1b; 3、AvalonD…

IF=8.5 MIMIC-IV高阶玩法!中国用新指标SHR+机器学习拿一区top,思路太牛了

‍ MIMIC-IV 发文难&#xff1f;那是你还没遇到对的思路&#xff01;如今机器学习数据库挖掘的文章层出不穷&#xff0c;今天介绍的这篇文章是在MIMIC-IV数据库的基础上&#xff0c;用了一个新指标—应激性高血糖比&#xff08;SHR&#xff09;&#xff0c;结合机器学习构建预测…

【iOS】——Block循环引用

循环引用原因 如果在Block中使用附有_ _strong修饰符的对象类型自动变量&#xff0c;那么当Block从栈复制到堆时&#xff0c;该对象为Block所持有&#xff0c;这样容易引起循环引用。 HPPerson *person [[HPPerson alloc] init];person.block ^{NSLog("person.age--- …

Redis使用场景-热点数据缓存

什么是缓存&#xff1f; 为了把一些经常访问的数据放入缓存中已减少对数据库的访问&#xff0c;从而减少数据库的压力&#xff0c;提高程序的性能。【内存中存储】-效率快 缓存的原理 什么样的数据适合放入缓存中&#xff1f; 1.查询频率高且修改频率低 2.数据安全性低 哪些组件…

《python语言程序设计》第6章第7题财务应用程序:计算未来投资,编写函数计算制定年数以给定利率

记住这里增加循环应该是以年为单位。但是添加的数是月为单位 此处需留意其实点不是1&#xff0c;1代表1年&#xff0c;这里月所以其实是12&#xff0c;这里的单位是月&#xff0c;而不是年。 python for i in range(12,monthNum12,12) 如果你把12都换成1呢&#xff1f;&…

本地生活抽佣系统搭建:如何让系统具有竞争优势?

随着本地生活的潜力不断展现&#xff0c;本地生活服务商逐渐成为新兴职业中的一大热门&#xff0c;本地生活抽佣系统搭建的热度也一直保持着飙升的状态。 抖音生活发布的《2023年数据报告》显示&#xff0c;2023年&#xff0c;抖音生活服务平台总交易额增长256%&#xff0c;抖…

监测Nginx访问日志状态码,并做相应动作

文章目录 引言I 监测 Nginx 访问日志情况,并做相应动作1.1 前提准备1.2 访问日志 502 情况,重启 bttomcat9服务1.3 其他案例:访问日志 502 情况,重启 php-fpm 服务II 将Shell 脚本check499.sh包装成systemd服务2.1 创建systemd服务2.2 配置service2.3 开机启动2.4 其他常用…

自监督学习概述(Self-Supervised Learning,SSL)

自监督学习&#xff08;Self-Supervised Learning&#xff0c;SSL&#xff09;是一种机器学习方法&#xff0c;旨在利用未标记数据进行训练。这种方法通过从数据本身生成伪标签&#xff0c;来创建监督信号&#xff0c;使得模型能够学习有效的数据表示。自监督学习在深度学习领域…

HTTP传输下载和P2P传输下载的区别?

HTTP传输下载和P2P&#xff08;Peer-to-Peer&#xff09;传输下载在多个方面存在显著的区别&#xff0c;以下是详细的分析&#xff1a; 1. 工作原理 HTTP传输下载&#xff1a; HTTP&#xff08;Hypertext Transfer Protocol&#xff09;是一种用于在Web上进行数据通信的协议&…