文章目录
前言
在python的opencv中,也就是导入:import cv2。使用的是numpy和内置ndarray来存储和处理图像数据。而对于C++的opencv是没有ndarray的,也没有numpy库,而是使用cv::Mat类来表示图像数据与进行处理的。
python是一种动态类型语言,无需显示的指定变量的类型。直接使用imread返回的就是ndarray对象,后续使用numpy库可以直接对其进行相关图像操作。
import cv2
import numpy as np
image = cv2.imread("lena.jpg")
print(type(image)) # <class 'numpy.ndarray'>
print(image.shape) # h,w,c
C++是一种静态类型语言,任何变量都需要显示地指定一个数据类型。而对于图像这种多维数组来说,就定义了Mat类来管理。
#include <opencv2/opencv.hpp>
#include<iostream>
using namespace std;
int main()
{
cv::Mat image = cv::imread("lena.jpg");
int h = image.rows; // row为高度
int w = image.cols; // col为宽度
int ch = image.channels(); // 通道
cout << h << " " << w <<" "<< ch << endl;
}
一、初识Mat类
Mat类分为矩阵头和指向存储数据的矩阵指针。矩阵头中包含矩阵的尺寸、存储方法、地址和引用次数等,只存放这几个固定的数据类型,所以矩阵头所占空间是固定的。图像复制和传递过程中主要的开销是存放矩阵数据。
一般的赋值操作,只是复制矩阵头和存放矩阵数据的指针,两者还是指向同一矩阵数据(浅拷贝),若需克隆一份新的矩阵数据,则要深拷贝。(在python中也是如此)
:
int main()
{
cv::Mat image1 // 矩阵头
image1 = cv::imread("lena.jpg"); // 矩阵指针将指向该矩阵像素
cv::Mat image2 = image1; // 浅拷贝
cv::Mat image3 = image1.clone(); // 深拷贝
}
注:Mat类利用自动内存管理技术解决了内存自动释放的问题,当变量不再需要时立即释放内存。
当发生A浅拷贝B时,两种指向同一矩阵数据。只删除A,B仍然指向该矩阵数据;当A、B都删除时,才会释放该内存。能够这么做的原因是上面说到的矩阵头中的引用次数,复制一次,引用次数+1,删除一次,引用次数-1,当引用次数为0时才会释放内存(发现了吗,和智能指针shared_ptr一样的原理)。
(2)图像数据类型
C++中,内置的数据类型有int,float,char等。我们指定,不同的编译平台,位数会发生变化。所以OpenCV中根据数值变量存储位数长度重新命名了新的数据类型,如下表:
数据类型 | 所代表类型与取值范围 |
---|---|
CV_8U | 8位无符号整数(0-——255) |
CV_8S | 8位符号整数(-128——127) |
CV_16U | 16位无符号整数(0——65535) |
CV_16S | 16位符号整数(-32768——32767) |
CV_32S | 32位无符号整数 |
CV_32F | 32位浮点整数 |
图像的单个灰度像素值为(0-255),所以CV_8U是最常用的。但图像还有RGB通道之分,所以OpenCV还定义了通道标识符:C1、C2、C3、C4,来分别表示单通道、双通道、三通道和四通道。为此,完整的图像数据类型为:CV_8UC3,表示8位的三通道数据
二、Mat类的创建(构造)与赋值
1、类的常规三种构造(默认、有参、拷贝)
(1)默认构造
Mat可以进行默认构造,这种方式不需要输入任何的参数,在后续给变量赋值的时候会自动判断矩阵的类型与大小,实现灵活的存储,常用于存储读取的图像数据或某个函数运算的输出结果
cv::Mat image;
image = cv::imread("lena.jpg");
cv::Mat zero_roi;
cv::inRange(mask2, 0, 0, zero_roi); //cv::inRange(输入图像,下界,上界,输出图像);
(2)输入矩阵尺寸和数据类型——有参构造
常规语法:
//cv::Mat image(rows, cols, type)
cv::Mat image(600, 400, CV_8UC3); // 创建一个高600,宽400,3通道的8位无符号矩阵数据
还有一种方式是采用cv::Size()进行赋值。
cv::Size 类可以方便地指定图像的尺寸,例如创建一个具有特定宽度和高度的图像,或者在图像处理过程中获取图像的尺寸信息。但要注意,Size()里的高、宽与常规语法是相反的,宽在前,高在后。
//cv::Size size(cols, rows);
cv::Size size(400, 600);
cv::Mat image(size, CV_8UC3); // 创建一个高600,宽400,3通道的8位无符号矩阵数据
(3)利用已有矩阵构造——拷贝构造
默认的拷贝是浅拷贝,若想创建一份不会影响到原数据的,需要用clone()来进行深拷贝,常用在函数参数上;如何还想指定尺寸,可以用矩阵指针的方式,常用于调整图像尺寸
cv::Size size(400, 600);
cv::Mat image1(size, CV_8UC3);
cv::Mat image2 = image1; //浅拷贝
cv::Mat image3 = image1.clone(); // 深拷贝
//cv::Mat image3(image1.clone) 拷贝构造的另一种写法,隐式构造
//当要对图片传入函数中进行处理,而又不会影响到原图
cv::Mat func(cv::Mat& images)
{
cv::Mat img_input;
ima_input = images.clone();
......
return xxx;
}
// 当要调整图像尺寸为520×520时(同样为深拷贝)
cv::Mat image4(520, 520, CV_8UC3, image1.data)
利用已有矩阵构造,还可以进行类似截图的操作,可以采用cv::Range或cv::Rect。注意cv::Rect里的高和宽与默认相反,先定义宽,再定义高。
// 1、Range指定行(高)和列(宽)的范围。下例是0到299行,共300行;0到399列,共400列
cv::Mat image2 = image1(cv::Range(0, 300), cv::Range(0, 400));
// cv::Mat image2(image1, (cv::Range(0, 300), cv::Range(0, 400)); 隐式构造
//2、Rect指定左上角坐标和矩形的宽度和高度来定义区域
cv::Mat image2(image1, cv::Rect(0, 0, 400, 300));//左上角左边(0,0),宽400,高300
2、赋值方式
上面讲述了多种构造方式,但只是创建了对象,还没有数据值赋给它。OpenCV4给予了多种赋值方式。
(1)构造时给每个通道赋值
在构造时,加上从cv::Scalar()直接赋值。这种方式是赋给一个通道的所有相同数据,如单通道时cv::Scalar(0),表示全部赋值0;三通道时,cv::Scalar(0,0,0),表示三个通道全都赋值为0。另外一提,彩色图片在OpenCV中默认三通道的顺序是B、G、R。但是,在用这种方式赋值时,实际应用中大多数都是为了得到一个全黑或全白的矩阵,来用作后续处理。
cv::Mat image(600, 400, CV_8UC3, cv::Scalar(0,0,0)); // 全黑的3通道
cv::Mat image(600, 400, CV_8UC3, cv::Scalar(255, 255, 255)); // 全白的3通道
(2)给通道内每个元素赋值
(1)中的方式是赋给一个通道(矩阵)相同的数据,也可以给矩阵内每一个元素进行赋值,如枚举、循环、数组。枚举的个数要与矩阵元素个数相同,所以此方法一般用在矩阵数据比较少的情况。但一般图像数据都较大,所以在实际应用中很少使用。
(3)使用成员函数赋值
Mat类中,自定义了可以初始化的矩阵,如eys,ones,zeros,diag,来生成单位矩阵、对角矩阵等。
前面说过,实际应用中大多数都是为了得到一个全黑或全白的矩阵,来用作后续处理。所以常用的是ones
cv::Mat mask = cv::Mat::zeros(cv::Size(400, 600), CV_8UC3); // 注意Size里的顺序(宽,高)
全黑的mask的作用:(1)因为像素都是0,所以跟其它任何像素作加法运算,得到的都是其它像素的原值,所以可用于移植其他图像
例:假设原始图片过大,先作了切割,再处理,最后要把处理结果图拼接起来。image是一个部分结果图,可以设定一个和原图大小一样的全黑mask作为底板,来依次放进部分结果图,下面是一个简单示例:
cv::Mat image(100, 100, CV_8UC3, cv::Scalar(255, 255, 255)); // 用全白image来当作部分结果图
cv::Mat mask = cv::Mat::ones(cv::Size(640, 400), CV_8UC3);
cv::Mat roi = mask(cv::Rect(0, 0, 100, 100)); // 使用Rect获得指定区域roi(左上角(0,0),宽高(100,100))
image.copyTo(roi); // 复制image到roi中(深拷贝)
cv::imshow("原图", mask);
(2)像素为0的另一个好处是方便进行逻辑运算。如求与运算(只有两个数都为1,结果才为1。所以当存在像素为0时,结果一定为0),可以将其他像素全置为0。这种操作在处理二值mask时很常见。
// 逻辑运算的参数都一样:前两个参数是要进行运算的两个图像矩阵,第三个参数是结果矩阵,第4个参数是设置范围,一般默认即可
void cv::bitwise_and(src1, src2, dst) //像素求与运算
void cv::bitwise_or(src1, src2, dst) //像素求或运算
void cv::bitwise_xor(src1, src2, dst) //像素求异或运算
void cv::bitwise_not(src1, src2, dst) //像素求非运算
此外,深度学习中使用torch张量也可以创建该类型的矩阵。如: auto zero_tensor = torch::zeros_like(index); //创建一个与index大小相同的全零张量zero_tensor。
三、Mat类的常用属性
属性 | 含义 |
---|---|
cols | 矩阵列数(图像宽度) |
rows | 矩阵行数(图像高度) |
channels() | 通道数 |
total() | 矩阵中元素的个数 |
step | 以字节为单位的矩阵的有效宽度 |
elemSize() | 每个元素的字节数 (默认情况下像素值是char类型,故灰度图=1字节,彩色图=3字节) |
data | 指向矩阵数据起始位置的指针(通常用于复制、传递图像数据) |
ptr() | data是指向整个矩阵数据的指针,ptr()可以选择特定行的数据 ,常用于有映射关系的复制操作 |
上面的“元素”并不是完全等于像素。在单通道图片中,一个元素等于一个像素;但若在多通道(如三通道中),一个元素=B、G、R三个像素,如下图。(注:Mat类的矩阵是二维的,所以计算机系统会把三通道的图片压缩成二维形式,如图所示,存储完一个元素的BGR三个像素值后,才会接着存放下一个元素)
值得注意:灰度图的总像素=total();彩色图像的总像素 = total()×channels;灰度图step=cols,彩色图step=cols×3。
对于cols 和 row,应该是系统做过处理,无论什么通道的图像,这两个就是图片的宽和高。
3.1 如何访问Mat类的元素
常用访问方式有:(1)at方法;(2)指针;(3)迭代器;(4)矩阵元素的地址定位
(2) 指针Ptr
ptr< uchar> () 是一个成员函数,属于OpenCV库的cv::Mat类。该函数返回一个指向图像某一行首元素的指针,可以方便地访问和修改图像中的每个像素。
uchar* cv::Mat::ptr<uchar>(int y) // y表示的是行的索引。返回值是一个指向第y行的uchar型指针
使用Ptr确定到每个像素
const uchar* dst = dst1.ptr<uchar>(0, 2); // 第0行第2个像素(从0开始数)
// 遍历每一个像素
for (int i = 0; i < img.rows; ++i)
{
uchar* row_ptr = img.ptr<uchar>(i);
for (int j = 0; j < img.cols; ++j)
{
uchar pixel = row_ptr[j];
cout << (int)pixel<< " ";
cout << "j=" << j << endl;
}
cout << endl;
}
上面这种遍历方式只适用于单通道CV_8UC1的图片,因为在遍历cols时,没有考虑通道,而前面说过计算机存储彩色图是是按照BGR像素连续存储的,所以若按照 j < img.cols ,实际上是没有遍历完的。下图展示了一张图片的首行遍历结果,可以看到,会依次遍历BGR三个像素。
想要遍历多通道图片的所有像素,最简单的方法就是将 j < img.cols 改为 j < img.cols *img.channels() 。但是通常情况下,不希望循环框架发生改变,所以一般是从内部处理。常见的(彩色图)逐元素复制拷贝操作参考如下:
const uchar* dst = dst1.ptr<uchar>(0, 2); // 第0行第2个像素(从0开始数)
// 遍历每一个像素
for (int i = 0; i < img.rows; ++i)
{
uchar* row_ptr = img.ptr<uchar>(i);
uchar* dst_ptr = dstImg.ptr<uchar>(i);
for (int j = 0; j < img.cols; ++j)
{
std::memcpy(dst_ptr + col * img_input.channels(), src_ptr + col * img_input.channels(), sizeof(uchar) * img_input.channels());
}
}