- 📢博客主页:盾山狂热粉的博客_CSDN博客-C、C++语言,机器视觉领域博主
- 📢努力努力再努力嗷~~~✨
💡大纲
⭕一个处理图像的程序需要通过某种方式获取图像,同时以某种类型存放在某个容器中,并通过某种形式展示给用户
👉这里将介绍图像存储容器的创建与使用,将详细讲解Mat的操作方式以及其支持的运算
👉目标:在程序中灵活使用Mat类型变量
一、Mat类介绍
💡在计算机中数字图像以矩阵的形式储存,矩阵中每一个元素都代表一定的图像信息。所以想要对图像进行处理,从矩阵数据中提取更多的信息,就需要掌握如何操作矩阵信息
⚠️在早期的Opencv中,使用IplImage的C语言结构体来储存图像信息,但是该结构体在程序结束时需要手动释放,不安全
⚠️在引入C++接口后,提供Mat类来存储图像信息,利用自动内存管理技术很好的解决了旧版本的问题,可以自动分配内存,不存在内存泄漏
Mat类是一种图像数据结构。用来保存矩阵类型的数据信息,包括向量、矩阵、灰度或彩色图像等数据
Mat类分为矩阵头和指向存储数据的矩阵指针两部分
(一)矩阵头与矩阵指针
👉矩阵头中包含矩阵的尺寸、存储方法、地址和引用次数
👉矩阵头的大小是一个常数,不会随着矩阵尺寸大小而改变
👉在绝大多数情况下矩阵头大小远小于矩阵中数据量的大小,因此图像复制和传递过程中主要的开销是存放矩阵数据。为了解决这个问题,在OpenCV中复制和传递图像时,只是复制了矩阵头和指向存储数据的指针,因此在创建Mat类时可以先创建矩阵头后赋值数据
Mat a; // 创建一个名为a的矩阵头
a = imread(“test.jpg”); // 向a中赋值图像数据,并将a中的矩阵指针指向该图像的像素数据
Mat b=a; // 复制矩阵头,并命名为b
⚠️当删除a变量时,b变量并不会指向一个空数据,只有当两个变量都删除后,才会释放矩阵数据。因为矩阵头中引用次数标记了引用某个矩阵数据的次数,只有当矩阵数据引用次数为0的时候才会释放矩阵数据
(二)Mat类可以存放的数据类型
👉Mat类可以存储的数据类型包含double、float、uchar、unsigned char以及自定义的模板等
👉下列代码:声明一个存放指定double类型的Mat类变量
Mat A = Mat_<double>(3,3); // 创建一个3*3的矩阵用于存放double类型数据
⚠️ 需要注意的是,一张图片的像素值的最大值决定图片质量。如果用8位无符号整数去存储16位图像,会造成严重的图像颜色失真或造成数据错误。为了避免在不同环境下因变量位数长度不同而造成程序执行问题,OpenCV根据数值变量存储位数长度定义了数据类型
-
OpenCV中的数据类型与取值范围
数据类型 | 具体类型 | 取值范围 |
---|---|---|
CV_8U | 8位无符号整数 | 0~255 |
CV_8S | 8位符号整数 | -128~127 |
CV_16U | 16位无符号整数 | 0~65535 |
CV_16S | 16位符号整数 | -32768~32767 |
CV_32S | 32位符号整数 | -2147483648~2147483647 |
CV_32F | 32位浮点整数 | -FLT_MAX~FLT_MAX, INF, NAN |
CV_64F | 64位浮点整数 | -DBL_MAX~DBL_MAX, INF, NAN |
👉针对灰度图像与彩色图像的通道数不同,还需要再定义图像数据的通道(Channel)数
-
通道数标识:C1、C2、C3、C4,表示单通道、双通道、3通道和4通道
Mat a(640,480,CV_8UC3) // 创建一个640*480的3通道矩阵用于存放彩色图像
Mat a(3,3,CV_8UC1) // 创建一个3*3的8位无符号整数的单通道矩阵
Mat a(3,3,CV_8U) // 创建单通道矩阵C1标识可以省略
⚠️虽然CV_8U和uchar都是8位无符号整数,但CV_8U只能用在Mat类内部。Mat_<CV_8U>(3,3)和Mat a(3,3,uchar)都是不对的,正确的是Mat_<uchar>(3,3)和Mat a(3,3,CV_8U)
⚠️总结上述三种构造Mat类变量的方法
创建Mat类
Mat a; a = imread(“test.jpg”); // 通过读取图片进行初始化赋值
声明一个指定类型的Mat类
Mat A = Mat_<double>(3,3);
通过opencv数据类型创建Mat类
Mat a(640,480,CV_8UC3)
⚠️后面两种没有给变量初始化赋值
二、Mat类的构造与赋值
(一)Mat类的构造
1、利用默认构造函数
👉不需要输入任何的参数,在后续给变量赋值的时候会自动判断矩阵的类型与大小,实现灵活的存储
👉常用于存储读取的图像数据和某个函数运算输出结果
Mat::Mat();
2、根据输入矩阵尺寸和类型构造
👉常用在明确需要存储数据尺寸和数据类型的情况下,例如相机的内参矩阵、物体的旋转矩阵等
Mat::Mat(int rows, int cols, int type)
Mat a(8,8,CV_8UC3);
👉上述形式的变形。通过Size(cols, rows)进行赋值,注意列在前、行在后
Mat::Mat(Size size(), int type)
Mat a(Size(8,8),CV_8UC3);
3、利用已有矩阵构造
👉这种构造方式可以构造出与已有的Mat类变量存储内容一样的变量。只是复制了Mat类的矩阵头,矩阵指针指向的是同一个地址,因此如果通过某一个Mat类变量修改了矩阵中的数据,另一个变量中的数据也会发生改变
Mat::Mat( const Mat & m); // m:已经构建完成的Mat类矩阵数据
👉复制两个一模一样的Mat类,这样两者矩阵数据的修改相互之间不会受影响
m = a.clone()
👉如果需要构造的矩阵尺寸比已有矩阵小,并且存储的是已有矩阵的子内容
Mat::Mat(const Mat & m,
const Range & rowRange, // rowRange:在已有矩阵中需要截取的行数范围,是一个Range变量,例如从第2行到第5行可以表示为Range(2,5)
const Range & colRange = Range::all() // 当不输入任何值时表示所有列都会被截取
)
Mat b(a,Range(2,5),Range(2,5)); // 从a中截取构造b
这种方式主要用于在原图中截图使用
通过这种方式构造的Mat类与已有Mat类享有共同的数据
(二)Mat类的赋值
1、构造时赋值
👉Scalar结构中变量的个数一定要与定义中的通道数相对应。若变量个数大于通道数,则位置大于通道数之后的数值将不会被读取;若变量数小于通道数,则会以0补充
Mat::Mat(int rows, int cols, int type,
const Scalar & s // 给矩阵中每个像素赋值的参数变量
)
Mat a(2, 2, CV_8UC3, Scalar(0, 0, 255)); // 创建一个3通道矩阵,每个像素都是0,0,255
2、枚举赋值法
👉将矩阵中所有的元素都一一枚举出,并用数据流的形式赋值给Mat类
👉输入的数据个数一定要与矩阵元素个数相同,不然赋值过程会出现报错,因此本方法常用在矩阵数据比较少的情况
Mat a = (Mat_<int>(3, 3) << 1, 2, 3, 4, 5, 6, 7, 8, 9);
Mat b = (Mat_<double>(2, 3) << 1.0, 2.1, 3.2, 4.0, 5.1, 6.2);
3、循环赋值
👉对矩阵中的每一位元素进行赋值,可以对矩阵中的任意部分进行赋值
⚠️只有这一种方式是可以不在声明变量的时候进行赋值
Mat c = Mat_<int>(3, 3); // 定义一个3*3的矩阵
for (int i = 0; i < c.rows; i++) // 矩阵行数循环
{
for (int j = 0; j < c.cols; j++) // 矩阵列数循环
{
c.at<int>(i, j) = i+j; // 赋值函数中声明的变量类型要与矩阵定义时的变量类型相同
}
}
4、类方法赋值
👉快速赋值的方法,可以初始化指定的矩阵
Mat a = Mat::eye(3, 3, CV_8UC1); // eye()构建单位矩阵,如果行和列不相等,则在矩阵的 (1,1),(2,2),(3,3)等主对角位置处为1
Mat b = (Mat_<int>(1, 3) << 1, 2, 3);
Mat c = Mat::diag(b); // 构建对角矩阵,参数必须是1维变量,用来存放对角元素的数值
Mat d = Mat::ones(3, 3, CV_8UC1); // 全为0
Mat e = Mat::zeros(4, 2, CV_8UC3); // 全为1
5、利用数组进行赋值
👉类似于枚举法,但是该方法可以根据需求改变Mat类矩阵的通道数
三、Mat类支持的运算
(一)加减乘除运算
👉Mat类变量支持矩阵的加减乘除运算
👉当两个Mat类变量加减运算时必须保证矩阵中的数据类型相同
👉常数与Mat类变量的运算结果为Mat类
Mat a = (Mat_<int>(3, 3) << 1, 2, 3, 4, 5, 6, 7, 8, 9);
Mat b = (Mat_<int>(3, 3) << 1, 2, 3, 4, 5, 6, 7, 8, 9);
Mat c = (Mat_<double>(3, 3) << 1.0, 2.1, 3.2, 4.0, 5.1, 6.2, 2, 2, 2);
Mat d = (Mat_<double>(3, 3) << 1.0, 2.1, 3.2, 4.0, 5.1, 6.2, 2, 2, 2);
Mat e, f, g, h, i;
e = a + b;
f = c - d;
g = 2 * a;
h = d / 2.0;
i = a – 1; // 每个元素-1
(二)矩阵乘法、内积、位的乘法
Mat j, m;
double k;
j = c*d; // 矩阵乘法,行列相乘,结果相加。第一个矩阵的列数等于第二个矩阵的行数,保存类型为float、double
k = a.dot(b); // 行向量与列向量的点乘,结果为double类型变量,要求两个矩阵有相同数量的元素数目
m = a.mul(b); // 结果类型由这两个矩阵类型决定,常用CV_8U,范围0~255,防止数据溢出255
(三)实例程序
#include<opencv2/opencv.hpp>
#include<iostream>
using namespace std;
using namespace cv;
int main()
{
Mat img = imread("1.jpg");
if (img.empty())
{
cout << "Can't Find it!!!" << endl;
return - 1;
}
imshow("MyTest", img);
// Mat dst;
Mat dst = Mat::zeros(img.size(), img.type());
Mat m = Mat::zeros(img.size(), img.type()); // 图片大小、类型不变
m = Scalar(2, 2, 2);
// 相当于是加法操作
int w = img.cols; // 宽 列
int h = img.rows; // 高 行
int dims = img.channels(); // 通道数
for (int row = 0; row < h; row++) {
for (int col = 0; col < w; col++) {
Vec3b p1 = img.at<Vec3b>(row, col);
Vec3b p2 = m.at<Vec3b>(row, col);
dst.at<Vec3b>(row, col)[0] = saturate_cast<uchar>(p1[0] + p2[0]); // 像素范围限定,小于0为0,大于255为255
dst.at<Vec3b>(row, col)[1] = saturate_cast<uchar>(p1[1] + p2[1]);
dst.at<Vec3b>(row, col)[2] = saturate_cast<uchar>(p1[2] + p2[2]);
}
}
// dst = image + Scalar(50, 50, 50);
add(img, m, dst);
imshow("加法操作", dst); // 亮度增加
// dst = image - Scalar(50, 50, 50);
subtract(img, m, dst);
imshow("减法操作", dst); // 亮度减少
// dst = image/Scalar(2, 2, 2);
divide(img, m, dst);
imshow("除法操作", dst); // 亮度暗两倍
multiply(img, m, dst);
imshow("乘法操作", dst); // 亮度亮两倍 如果亮度像素超过255,自动截断,以255输出
waitKey(0);
return 0;
}
四、读取Mat类元素的方法
💡首先我们要明确一点,多通道Mat类矩阵是三维的,而计算机的存储是二维的,怎么将三维数据变成二维数据?采用的方法是先存储将第一个元素的每个通道数据,以此类推。每一行的元素都按照这种方式进行存储
👉Mat类矩阵的常用属性
属性 | 作用 | Mat (3, 4, CV_32FC3) |
---|---|---|
cols | 矩阵的列数 | 4 |
rows | 矩阵的行数 | 3 |
step | 以字节为单位的矩阵的有效宽度 | eleSize() * cols = 48 |
elemSize() | 每个元素的字节数 | 32 / 8 * channels() = 12 |
total() | 矩阵中元素的个数 | 12 |
channels() | 矩阵的通道数 | 3 |
(一)通过at方法进行读取
1、针对单通道的读取方法
👉通过at方法读取元素需要在后面跟上“<数据类型>”,这里的数据类型需要与矩阵定义的一样,否则会报错
👉如果矩阵定义的是uchar类型的数据,输出时需要强制转换成int类型的数据,否则输出的结果并不是整数
Mat a = (Mat_<uchar>(3, 3) << 1, 2, 3, 4, 5, 6, 7, 8, 9);
int value = (int)a.at<uchar>(0, 0);
👉该方法以坐标的形式给出需要读取的元素坐标(行,列)
2、针对多通道的读取方法
👉多通道矩阵每一个元素坐标处都是多个数据,因此引入一个变量用于表示同一元素多个数据
针对3通道矩阵,定义了cv::Vec3b、cv::Vec3s、cv::Vec3w、cv::Vec3d、cv::Vec3f、cv::Vec3i用于表示同一个元素的三个通道数据
b是uchar类型的缩写、s是short类型的缩写、w是ushort类型的缩写、d是double类型的缩写、f是float类型的缩写、i是int类型的缩写
👉Vec3b数据类型在输出每个通道数据时需要将其变量类型强制转换成int类型,因为是int类型变量在接收
Mat b(3, 4, CV_8UC3, Scalar(0, 0, 1));
Vec3b vc3 = b.at<Vec3b>(0, 0); // (0,0)指的是元素在矩阵的位置
int first = (int)vc3.val[0]; // 这里都是通道里的像素值
int second = (int)vc3.val[1];
int third = (int)vc3.val[2];
(二)通过指针ptr进行读取
-
如果找到每一行元素的起始地址位置,那么读取矩阵中每一行不同位置的元素就是将指针在起始位置向后移动若干位即可
Mat b(3, 4, CV_8UC3, Scalar(0, 0, 1));
for (int i = 0; i < b.rows; i++)
{
uchar* ptr = b.ptr<uchar>(i);
for (int j = 0; j < b.cols*b.channels(); j++) // 逐行访问每一个像素值
{
cout << (int)ptr[j] << endl;
}
}
// 当读取第2行中第3个数据时,可以用(int)b.ptr<uchar>(1)[2]这样的形式来直接访问
void QuickDemo::pixel_visit_demo(Mat &image)
{
int w = image.cols; // 宽 列
int h = image.rows; // 高 行
int dims = image.channels(); // 通道数
// 数组遍历
for (int row = 0; row < h; row++) {
for (int col = 0; col < w; col++) {
if (dims == 1) { // 灰度图像 单通道
int pv = image.at<uchar>(row, col); // 获取一个像素点像素值
image.at<uchar>(row, col) = 255 - pv; // 对获取到的像素点赋值,保证像素值在0~255范围内
}
if (dims == 3) { // 彩色图像 三通道
Vec3b bgr = image.at<Vec3b>(row, col); // 三个通道像素点像素值的获取
image.at<Vec3b>(row, col)[0] = 255 - bgr[0]; // 对获取到的像素点赋值
image.at<Vec3b>(row, col)[1] = 255 - bgr[1];
image.at<Vec3b>(row, col)[2] = 255 - bgr[2];
}
}
}
// 指针遍历
for (int row = 0; row < h; row++) {
uchar* current_row = image.ptr<uchar>(row); // 当前行的指针
for (int col = 0; col < w; col++) {
if (dims == 1) { // 灰度图像
int pv = *current_row; // 获取一个像素点像素值
*current_row++ = 255 - pv; // 保证像素值在0~255范围内
}
if (dims == 3) { // 彩色图像
*current_row++ = 255 - *current_row;
*current_row++ = 255 - *current_row;
*current_row++ = 55 - *current_row;
}
}
}
imshow("像素读写演示", image);
}
(三)通过迭代器进行读取
(四)通过矩阵元素的地址定位方式进行读取
📢欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正!