1. 数字图像的含义
OpenCV 中的图像,其实指的是数字图像。在介绍图像这个概念之前,先介绍几个基础的概念:
- 像素(Pixel)是图像的基本单元或者基本元素,亦或者是图像最小的单位。图像中的像素点包含不同的像素值。对于灰白图像而言,像素值是介于0-255之间的值;对于拥有 RGB 3个通道的彩色图像而言,每个通道的像素值为0-255;对于二维黑白图片而言,这些像素点构成了一个二维矩阵;对于二维彩色图片而言,这些像素点则是一个多维矩阵。
- 数字图像处理(Digital Image Processing),它被称作计算机图像处理技术。
简单介绍完这些基础概念之后,接下来会介绍 OpenCV 中的 Mat 类。从本文开始,使用 OpenCV 的主要语言是 C++,当然只要有一些常见高级语言的编程基础,理解起来也不是什么难事。
2. Mat 的基本结构
自 OpenCV 2.x 之后,Mat 是 OpenCV 最基本也是最重要的类。Mat 是 Matrix 的简称,表示矩阵的意思。它是图像的容器,是一个二维向量。在 OpenCV 中 Mat 用于表示图像的类。对于 C++ 版本而言,Mat 支持自动内存管理,无须申请和释放内存,所以对程序员非常友好。(当然,也可以手工地去创建)
Mat 不仅可以存储图像,还可以存储矩阵。
Mat 由两个数据部分组成:矩阵头和指针。
class CV_EXPORTS Mat{
public:
// ... a lot of methods ...
...
/*! includes several bit-fields:
- the magic signature
- continuity flag
- depth
- number of channels
*/
int flags;
//! the array dimensionality, >= 2
int dims;
//! the number of rows and columns or (-1, -1) when the array has more than 2 dimensions
int rows, cols;
//! pointer to the data
uchar* data;
//! pointer to the reference counter;
// when array points to user-allocated data, the pointer is NULL
int* refcount;
// other members
...
};
- 矩阵头:包含矩阵尺寸、存储方法、存储地址等信息。对应 Mat 类中的 flags、dims、rows,cols、data 指针、refcount 指针。所以矩阵头的大小是恒定的。
- 指针:指向包含像素值的矩阵(可以根据选择不同的存储方法,采用任何维度进行存储数据),也就是 data 指针指向的空间。通常,矩阵比矩阵头大几个数量级。
在图像处理中,我们日常使用的最简单的操作就是创建、传递、拷贝 Mat 对象。
图像在创建过程中,大的开销主要来自于矩阵。如果要拷贝、赋值 Mat 对象,并且使用深拷贝的话,效率会大大地降低。恰好 OpenCV 本身采用引用计数机制(可以看到 Mat 类中的 refcount 指针),即 Mat 对象的矩阵指针指向同一地址。这一机制可以让我们使用浅拷贝。
Mat 的构造函数、赋值运算符只拷贝矩阵头和矩阵指针 ,而不拷贝矩阵本身。
3. Mat 的读取与创建
接下来,我们来认识如何使用 Mat 对象,并通过它来读取和创建图像。
3.1 图像的读取
在图像创建之前,先介绍一下图像的读取。我们可以从图片中、视频中等读取 Mat 对象。例如,下面的代码是从图片文件中读取 Mat 对象。
String fileName = "https://b2.7b2.com/Users/tony/images/test.jpg";
Mat srcImage;
srcImage = imread(fileName);
//判断图像是否加载成功
if (srcImage.empty()){
cout << "图像加载失败" << endl;
return -1;
}
cv::imread() 函数用于从文件中读取 Mat 对象。其中,第一个参数 fileName 是包含了文件绝对路径的文件名。
CV_EXPORTS_W Mat imread( const String& filename, int flags = IMREAD_COLOR );
它的第二个参数表示图像读取的模式,默认是 IMREAD_COLOR ,它表示将图像转换成三通道 BGR 彩色图像。下面是 imread 函数可以用到的 flags:
enum ImreadModes {
IMREAD_UNCHANGED = -1, //按原样返回加载的图像(会带上alpha通道)。忽略EXIF方向。
IMREAD_GRAYSCALE = 0, //将图像转为单通道灰度图
IMREAD_COLOR = 1, //将图像转为BGR三通道彩色图像
IMREAD_ANYDEPTH = 2, //如果图像深度为16-bit/32-bit则会返回该深度图像,否则返回8-bit图像
IMREAD_ANYCOLOR = 4, //按照任意颜色图像格式读取
IMREAD_LOAD_GDAL = 8, //使用gdal驱动程序加载图像
IMREAD_REDUCED_GRAYSCALE_2 = 16, //将图像转为单通道灰度图且图像尺寸变为1/2
IMREAD_REDUCED_COLOR_2 = 17, //将图像转为BGR三通道彩色图像且图像尺寸变为1/2
IMREAD_REDUCED_GRAYSCALE_4 = 32, //将图像转为单通道灰度图且图像尺寸变为1/4
IMREAD_REDUCED_COLOR_4 = 33, //将图像转为BGR三通道彩色图像且图像尺寸变为1/4
IMREAD_REDUCED_GRAYSCALE_8 = 64, //将图像转为单通道灰度图且图像尺寸变为1/8
IMREAD_REDUCED_COLOR_8 = 65, //将图像转为BGR三通道彩色图像且图像尺寸变为1/8
IMREAD_IGNORE_ORIENTATION = 128 //忽略EXIF中的方向标识,不旋转图像
};
如果想以原本类型读取图片,我们可以选择 IMREAD_UNCHANGED,这样图像原本的类型和读进来的类型会保持一致。
有一个 imreadmulti 函数它与 imread 函数类似,用于从一个文件中读取多幅图像。例如,从某个 tiff 文件中读取多个 Mat 对象。
CV_EXPORTS_W bool imreadmulti(const String& filename, CV_OUT std::vector<Mat>& mats, int flags = IMREAD_ANYCOLOR);
3.2 Mat 的创建
Mat 类有很多构造函数,其自身也有很多函数可以用于创建 Mat 对象。本文以及以后的内容并不打算对每个函数进行详细的解释,因此这里只列举实际使用中常见的场景。
3.2.1 使用构造函数创建
创建一个 3*3 的 3 通道矩阵,每个像素点的值为(0,0,255)。
cv::Mat src(3, 3, CV_8UC3, cv::Scalar(0, 0, 255));
std::cout << "src = " << std::endl << src << std::endl;
输出结果:
src =
[ 0, 0, 255, 0, 0, 255, 0, 0, 255;
0, 0, 255, 0, 0, 255, 0, 0, 255;
0, 0, 255, 0, 0, 255, 0, 0, 255]
在这里,CV_8UC3 表示为 3 通道 Unsigned 8bits 格式的矩阵,即 BGR 3 通道。
稍微整理一下,矩阵数据类型:
CV_<bit_depth>(S|U|F)C<number_of_channels>
- bit_depth,比特数,例如 8 bits,16 bits,32 bits,64 bits
- S|U|F
S:signed int,有符号整形
U:unsigned int,无符号整形
F:float,单精度浮点型 - C<number_of_channels>代表一张图片的通道数,例如:
1:单通道图像,表示灰度图片。
3:3 通道图像,表示 RGB 彩色图像 。
4:4 通道图像,表示带 Alpha (透明度)通道的 RGB 图像。
在 OpenCV 中,类似的矩阵数据类型还有 CV_16SC3、CV_32FC3、CV_64FC3 等等。在下一篇,我们会详细介绍矩阵数据类型相关的内容。
再举一个例子,使用构造函数创建一个指定大小(400*400)的 Mat 对象,并指定每个像素点的颜色值(0,0,255)。其实,它会展示一张红色的图。
cv::Mat src(cv::Size(400,400), CV_8UC3, cv::Scalar(0,0,255));
imshow("red",src);
输出结果:
Scalar 字面意思是标量,它是从 Vec 派生的 4 个向量元素的模板类。Scalar 类型在 OpenCV 中广泛用于传递像素值。
Scalar 常见的构造函数为
Scalar_();
Scalar_(_Tp v0, _Tp v1, _Tp v2=0, _Tp v3=0);
Scalar_(_Tp v0);
Scalar_(const Scalar_& s);
Scalar_(Scalar_&& s) CV_NOEXCEPT;
当 Scalar 表示颜色时,单通道图像使用下标 [0] 表示,三通道图像使用下标 [0]、[1]、[2] 表示 B、G、R 通道。所以,cv::Scalar(0,0,255) 对应的就是红色。
3.2.2 使用数组创建
同样,使用构造函数创建一个 3*3 的矩阵。
int array[2] = { 3, 3 };
cv::Mat src(2, array, CV_8UC1, cv::Scalar::all(0));
std::cout << "src = " << std::endl << src << std::endl;
输出结果:
src =
[ 0, 0, 0;
0, 0, 0;
0, 0, 0]
该构造函数的第一个参数表示矩阵的维数,第二个参数表示指定 n 维数组形状的整数数组。所以,这里的 array 数组表示的是每一维数的数量。
3.2.3 使用 create 函数创建
使用 create() 函数创建一个 3*3 的二维单通道矩阵。
cv::Mat src;
src.create(3, 3, CV_8UC1);
std::cout << "src = " << std::endl << src << std::endl;
输出结果:
src =
[ 0, 0, 51;
2, 0, 96;
0, 0, 64]
create() 函数只能创建一个指定大小、指定矩阵数据类型的矩阵,并不能为矩阵设置初始值。它在改变矩阵尺寸时,为矩阵数据重新分配了内存,因此其所创建的矩阵中每个数据都是一个随机值。
3.2.4 特殊矩阵创建
OpenCV 中有类似 MATLAB 那样可以快速赋值、创建矩阵的函数,生成全 0 矩阵、单位矩阵、对角矩阵。
cv::Mat mat_zeros = cv::Mat::zeros(3,3,CV_8UC1); // 全 0 矩阵
cout<<"mat_zeros="<<endl<<mat_zeros<<endl;
cv::Mat mat_ones = cv::Mat::ones(3,3,CV_8UC1); // 单位矩阵
cout<<"mat_ones="<<endl<<mat_ones<<endl;
cv::Mat mat_eye = cv::Mat::eye(3,3,CV_8UC1); // 对角矩阵
cout<<"mat_eye="<<endl<<mat_eye<<endl;
输出结果:
mat_zeros=
[ 0, 0, 0;
0, 0, 0;
0, 0, 0]
mat_ones=
[ 1, 1, 1;
1, 1, 1;
1, 1, 1]
mat_eye=
[ 1, 0, 0;
0, 1, 0;
0, 0, 1]
3.2.5 使用自定义矩阵 Mat 创建
我们也可以自己定义一些数据量比较小的矩阵,例如:
cv::Mat src = (cv::Mat_<double>(3, 3) << 1, 0, 0, 0, 1, 0, 0, 0, 1);
std::cout << "src = " << std::endl << src << std::endl;
输出结果:
src =
[1, 0, 0;
0, 1, 0;
0, 0, 1]
3.2.6 提取某个 Mat 对象的 ROI
ROI(Region Of Interest),表示感兴趣的区域。通常,提取 ROI 能够便于进一步分析图像。
常用的提取 ROI 区域的方法包括:
- 使用 cv::Rect 指定矩形的左上角的坐标,以及它的宽和高,提取 ROI 区域。
Mat src = imread("https://b2.7b2.com/Users/tony/images/test.jpg");
Mat roi = src(Rect(300, 400, 200, 300));//Rect 四个形参分别表示 x 坐标,y 坐标,宽,高
- 使用 cv::Range 指定感兴趣的行或列的范围,提取 ROI 区域。
Mat src = imread("https://b2.7b2.com/Users/tony/images/test.jpg");
Mat roi = src(Range(150, 150 + 100), Range(250, 250 + 100));//Range两个形参分别是:起始行或列,起始行或列+偏移量
再举个例子,读取一张图片,并显示图像。然后提取图像的 ROI,并显示该 ROI。
cv::Mat src = imread("...");
imshow("src",src);
Mat roi = src(Rect(1200, 800, 1500, 2500));
imshow("roi",roi);
输出结果:
4. Mat 的赋值
4.1 浅拷贝与深拷贝
浅拷贝:只复制指向某个对象的指针,而不复制对象本身。新旧对象会共享同一块内存,修改任何一方都会影响到另一方。
深拷贝:创造一个一模一样的对象,新旧对象不共享内存,修改任何一方不会影响到另一方。
之前提到过,Mat 的拷贝构造函数、赋值运算符都是浅拷贝,另外 ROI 的提取也是浅拷贝。例如:
Mat a = imread("https://b2.7b2.com/Users/tony/images/test.jpg");
Mat b(a); // 拷贝构造函数
Mat c = a; // 赋值运算符
当删除 a、b、c 中任何一个对象,其余两个对象都不会指向一个空数据。只有当三个对象都删除时,才会真正释放矩阵数据。前面提到过,这依赖于 Mat 对象的引用计数机制(refcount 指针),它的本质是用于指向相同数据地址的不同类对象的内存管理,只有当矩阵数据引用次数为 0 的时候才会真正释放矩阵数据。
4.2 clone() 与 copyTo()
有时候,我们还是需要创建一个全新的 Mat 对象,拷贝矩阵数据本身。那么我们可以使用 clone() 、 copyTo() 函数实现深拷贝。
例如:
Mat a;
Mat b = a.clone();// 对 a 进行克隆
Mat c;
a.copyTo(c);// 将 a 拷贝到 c 对象
当更改 a、b、c 中任何一个对象,其余两个对象都不会受到影响。
copyTo() 函数有两种形式:
- srcImage.copyTo(dstImage):将 srcImage 的内容复制到 dstImage;
- srcImage.copyTo(dstImage, mask):mask 是一个掩模,当 srcImage 与 mask 进行运算后,将得到的结果拷贝给 dstImage。其中,mask 必须为 CV_8U 类型,大小与 srcImage、dstImage 保持一致。
掩模的运算规则:
在图像的任意位置(x,y),如果 mask 的像素值等于 1,则 dstImage(x,y) = srcImage(x,y)。如果 mask 的像素值等于 0,则 dstImage(x,y) = 0
因此,使用 copyTo() 函数时,将原先的 srcImage 在 mask 上不为 0 的,所对应的像素点进行拷贝。拷贝的结果复制到目标对象 dstImage 上。
举个 copyTo() 函数并且使用 mask 的例子:
cv::Mat a = (cv::Mat_<double>(3, 3) << 0, 0, 0, 0, 0, 0, 0, 240, 0);
std::cout << "a = " << std::endl << a << std::endl;
Mat mask = Mat::eye(3,3,CV_8UC1);
std::cout << "mask = " << std::endl << mask << std::endl;
Mat roi;
a.copyTo(roi,mask);
std::cout << "roi = " << std::endl << roi << std::endl;
输出结果:
a =
[0, 0, 0;
0, 0, 0;
0, 240, 0]
mask =
[ 1, 0, 0;
0, 1, 0;
0, 0, 1]
roi =
[0, 0, 0;
0, 0, 0;
0, 0, 0]
5. 总结
本文作为入门的准备,简单介绍了图像相关的基础知识、Mat 的基本结构、Mat 的创建/读取/赋值。
在此基础上也引申出很多知识,比如矩阵的数据类型、掩模等,这些内容都是非常重要的。因此,后续的内容都会用到它们,因此也会会更加进一步详细地介绍它们。