Mat的内部结构?
本质:内存里面的矩阵+管理它的“脑子”
指向像素数据的指针(Pointer to the Matrix):这是一个指针,它指向了内存中真正存储所有像素数值的那块区域,这块数据区域的大小是根据图像的大小和类型变化,通常非常大
矩阵头(matrix header):里面包含了矩阵的大小,用于存储的方法,矩阵存储的地址信息)矩阵头的大小是很恒定的,{矩阵的尺寸(行数,列数,维度),存储方法(数据类型,通道数),矩阵数据存储的内存地址,引用计数器(Reference Counter)}
类比:就像一本书,矩阵头就像是书的目录页,记录了书有多少章节(尺寸),用什么纸张印刷的(存储方法),正文是从那一页开始的(内存地址)等信息,指向像素数据的指针则告诉你正文内容存放再图书光的哪个书架上,而真正的内容(像素数据)就是书架上的那本书本身。
Mat的共享数据与引用计数(Reference Counter)
目的
图像的处理通常涉及很多的步骤,一个图像通常会作为输入传递给多个函数,或者复制给多个变量,如果每次传递或者是赋值都需要完整的复制整个图像数据,会极其的浪费内存空,所以使用引用计数来表明这个图像被传递或者是赋值多少次。
机制:Opencv使用引用计数来解决这个问题
这个想法是每个Mat对象都有他们自己的矩阵头,一个矩阵可以在两个矩阵对象之间共享,使他们的矩阵指针指向同一个地址,此外,复制运算符仅仅只会复制函数头和指向存储像素的指针,而不是数据本身。
Mat A, C; // creates just the header parts
A = imread(argv[1], IMREAD_COLOR); // here we'll know the method used (allocate matrix)
Mat B(A); // Use the copy constructor
C = A; // Assignment operator
当我创建了A,C矩阵,此时我仅仅创建了函数头,里面包含头部信息和指向数据的指针,这个时候这块数据只有A在使用,此时引用计数为1。然后我使用拷贝构造函数MatB(A)或者复制操作C = A,Opencv只会默认的只复制函数矩阵头,而不是复制庞大的像素数据本身。新的B,C会获得一个和A完全一样的指向数据的指针,同时这块像素数据的的引用计数会增加。
共享数据:因为ABC都指向同一块数据,所以修改其中任何一个对象所指向的数据都会更改这一块像素数据,因为他们指向的是同一块数据
创建子部分的标题:我们不想去只复制矩阵头,想另外开辟一块内存空间,可以在图像中创建一块自己感兴趣的部分(ROI),我只需要创建一个具有新边界的新标题:
// 创建一个矩形区域作为 ROI
Mat D(A, Rect(10, 10, 100, 100)); // D 的头指向 A 的数据中从 (10,10) 开始的 100x100 区域
// 使用行和列范围创建 ROI
Mat E = A(Range::all(), Range(1, 3)); // E 的头指向 A 的所有行,但只包括第 1 和第 2 列 (Range 是半开区间 [1, 3) )
自动释放:当一个Mat对象需要被销毁的时候(比如离开作用域)它首先将它指向的数据块的引用计数减一,只有当引用计数减到0的时候,这块区域才会真正被释放。
真正的复制图像数据(Deep Copy)
如果我想完全得到一个独立的副本,我必须使用深拷贝。Opencv主要提供了两个函数来实现
cv::Mat::clone():创建一个新的对象,它是原始对象的一个完整的独立的克隆。
Mat F = A.clone(); // F 是 A 的一个全新副本,修改 F 不会影响 A
cv::Mat::copyTo():将原始的对象数据复制到另一个已经存在的Mat对象中去,如果目标对象的尺寸或者类型不合适,copyTo会为其首先重新分配内存
Mat G;
A.copyTo(G); // G 现在包含了 A 的数据的副本,修改 G 不会影响 A
创建感兴趣的区域(Region of Interest,ROI)
机制
创建ROI实际上是创建了一个新的矩阵头,这个头部的尺寸信息定义了我感兴趣的区域,但是它的数据指针仍然指向了原始图像数据块中对应的起始位置,它不复制任何像素数据。
Mat D(A, Rect(10, 10, 100, 100)); // D 的头指向 A 的数据中从 (10,10) 开始的 100x100 区域
// 使用行和列范围创建 ROI
Mat E = A(Range::all(), Range(1, 3)); // E 的头指向 A 的所有行,但只包括第 1 和第 2 列 (Range 是半开区间 [1, 3) )
共享数据
因为ROI仍然指向的原始数据,所以修改ROI就会修改原始的数据对应的区域,ROI是一种高效访问和操作图像局部区域的方式,它不产生数据拷贝,但修改ROI就会修改原图。
像素值的存储方式(Storing Methods)
颜色空间(Color Space)如何组合基本颜色成分来表示一个颜色
矩阵中的每个像素值是由两个方面去表示:颜色空间和数据类型
灰度(Grayscale):最简单的,只有黑白和不同程度的灰色。每个像素通常用一个数值表示亮度。
RGB(Red ,Green, Blue):最常见的一种,符合人眼感知颜色的方式。每个像素用三个值分别表示红、绿、蓝的强度。有时会加上第四个 Alpha (A) 通道表示透明度 (Transparency),变成 RGBA。
BGR:非常重要的一点!OpenCV 中默认加载和显示的彩色图像,其颜色通道顺序是 BGR(蓝、绿、红),而不是通常的 RGB。 如果你从其他地方获取了 RGB 图像数据,或者要将 OpenCV 的图像传递给需要 RGB 的系统,需要进行通道转换(例如使用 cvtColor
函数)。
HSV/HLS (Hue, Saturation, Value/Luminance): 将颜色分解为色调、饱和度、明度/亮度。这种方式更符合人类描述颜色的直觉。例如,只关注色调和饱和度可以降低光照变化对算法的影响。
数据类型(Data Type)
定义每个颜色分量用什么类型的数据来存储,这决定了颜色的精度和内存占用。
命名规则
OpenCV 使用一套约定的宏来定义数据类型和通道数: CV_[位数][类型][类型前缀]C[通道数]
[位数]
: 每个基础元素占用的比特数,如 8
, 16
, 32
, 64
。
[类型]
: U
(Unsigned Integer - 无符号整数), S
(Signed Integer - 有符号整数), F
(Float - 浮点数)。[类型前缀]
: 通常省略,但有时用于区分(如 CV_8UC1
的 U
)。
C[通道数]
: C
后面的数字代表每个像素点包含多少个通道(颜色分量)。
示例
CV_8UC1
: 每个像素有 1 个通道,每个通道是 8 位无符号字符型 (unsigned char, 0-255)。通常用于灰度图像。
CV_8UC3
: 每个像素有 3 个通道,每个通道是 8 位无符号字符型。最常用于BGR 彩色图像。每个像素需要 3 字节。
CV_32FC1
: 每个像素 1 个通道,每个通道是 32 位浮点型 (float)。用于需要更高精度的单通道图像(如深度图或算法中间结果)。
CV_32FC3
: 每个像素 3 个通道,每个通道是 32 位浮点型。用于需要高精度颜色表示的彩色图像。
cv::Scalar
: 这是一个可以表示包含 1 到 4 个元素的短向量,常用于给 Mat
对象的所有像素赋初
始值。例如 Scalar(0, 0, 255)
在 CV_8UC3
类型的 Mat
中表示蓝色 (因为 BGR 顺序,Blue=255, Green=0, Red=0)。Scalar::all(0)
表示所有分量都是 0。
权衡: 使用更高精度的数据类型(如 float
, double
)可以表示更细微的颜色或数值变化,但会显著增加内存占用。unsigned char
(8U) 对于大多数显示需求来说足够了(每个通道 256 级,3 通道就有 256256256 ≈ 1677 万种颜色)。
关键点: 理解 CV_...C...
的命名规则,知道它定义了像素数据的类型和通道数,并了解不同类型在精度和内存上的取舍。
显示创建Mat对象(Creating a Mat Object Explicitly)
cv::Mat::Mat 构造函数:最直接的方式
// 创建一个 2x2 的 Mat, 每个像素 3 通道 (CV_8UC3), 初始值设为红色 (BGR: 0,0,255)
Mat M(2, 2, CV_8UC3, Scalar(0, 0, 255));
// 创建一个三维 Mat (2x2x2), 单通道 (CV_8UC1), 初始值为 0
int sz[3] = {2, 2, 2};
Mat L(3, sz, CV_8UC(1), Scalar::all(0)); // 注意 CV_8UC(1) 的写法
需要指定维度,数据类型,通道数,以及可选的初始值(Scalar)。
cv::Mat:creata函数:用于创建或者重新分配Mat的内存;
Mat M; // 先创建一个空的 Mat 头
M.create(4, 4, CV_8UC(2)); // 分配一个 4x4, 双通道 (CV_8UC2) 的数据区
与构造函数不同,create
不会设置初始值。一个重要的特性是:如果调用 create
时,M
原有的数据区大小和类型已经满足新的要求,它就不会重新分配内存,而是复用旧的内存。只有当尺寸或类型不匹配,需要更大空间时,才会释放旧内存(如果引用计数为 0)并分配新内存。
MATLAB风格初始化器:zeros,ones ,eye
Mat E = Mat::eye(4, 4, CV_64F); // 创建一个 4x4 的单位矩阵 (对角线为1, 其余为0), 数据类型为 double (64F)
Mat O = Mat::ones(2, 2, CV_32F); // 创建一个 2x2 的全1矩阵, 数据类型为 float (32F)
Mat Z = Mat::zeros(3, 3, CV_8UC1); // 创建一个 3x3 的全0矩阵, 数据类型为 uchar (8UC1)
可以创建一些特定的矩阵