OpenCV 中的图像,其实指的是数字图像。在介绍图像这个概念之前,先介绍几个基础的概念:
像素(Pixel)是图像的基本单元或者基本元素,亦或者是图像最小的单位。图像中的像素点包含不同的像素值。对于灰白图像而言,像素值是介于0-255之间的值;对于拥有 RGB 3个通道的彩色图像而言,每个通道的像素值为0-255;对于二维黑白图片而言,这些像素点构成了一个二维矩阵;对于二维彩色图片而言,这些像素点则是一个多维矩阵。
数字图像处理(Digital Image Processing),它被称作计算机图像处理技术。
简单介绍完这些基础概念之后,接下来会介绍 OpenCV 中的 Mat 类。从本文开始,使用 OpenCV 的主要语言是 C++,当然只要有一些常见高级语言的编程基础,理解起来也不是什么难事。
自 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 的构造函数、赋值运算符只拷贝矩阵头和矩阵指针 ,而不拷贝矩阵本身。
接下来,我们来认识如何使用 Mat 对象,并通过它来读取和创建图像。
在图像创建之前,先介绍一下图像的读取。我们可以从图片中、视频中等读取 Mat 对象。例如,下面的代码是从图片文件中读取 Mat 对象。
String fileName = "/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& mats, int flags = IMREAD_ANYCOLOR);
Mat 类有很多构造函数,其自身也有很多函数可以用于创建 Mat 对象。本文以及以后的内容并不打算对每个函数进行详细的解释,因此这里只列举实际使用中常见的场景。
创建一个 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,比特数,例如 8 bits,16 bits,32 bits,64 bits
S|U|F S:signed int,有符号整形 U:unsigned int,无符号整形 F:float,单精度浮点型
C
在 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*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 数组表示的是每一维数的数量。
使用 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() 函数只能创建一个指定大小、指定矩阵数据类型的矩阵,并不能为矩阵设置初始值。它在改变矩阵尺寸时,为矩阵数据重新分配了内存,因此其所创建的矩阵中每个数据都是一个随机值。
OpenCV 中有类似 MATLAB 那样可以快速赋值、创建矩阵的函数,生成全 0 矩阵、单位矩阵、对角矩阵。
cv::Mat mat_zeros = cv::Mat::zeros(3,3,CV_8UC1); // 全 0 矩阵
cout<<"mat_zeros="<
输出结果:
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]
我们也可以自己定义一些数据量比较小的矩阵,例如:
cv::Mat src = (cv::Mat_(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]
ROI(Region Of Interest),表示感兴趣的区域。通常,提取 ROI 能够便于进一步分析图像。
常用的提取 ROI 区域的方法包括:
使用 cv::Rect 指定矩形的左上角的坐标,以及它的宽和高,提取 ROI 区域。
Mat src = imread("/Users/tony/images/test.jpg");
Mat roi = src(Rect(300, 400, 200, 300));//Rect 四个形参分别表示 x 坐标,y 坐标,宽,高
使用 cv::Range 指定感兴趣的行或列的范围,提取 ROI 区域。
Mat src = imread("/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);
浅拷贝:只复制指向某个对象的指针,而不复制对象本身。新旧对象会共享同一块内存,修改任何一方都会影响到另一方。深拷贝:创造一个一模一样的对象,新旧对象不共享内存,修改任何一方不会影响到另一方。
之前提到过,Mat 的拷贝构造函数、赋值运算符都是浅拷贝,另外 ROI 的提取也是浅拷贝。例如:
Mat a = imread("/Users/tony/images/test.jpg");
Mat b(a); // 拷贝构造函数
Mat c = a; // 赋值运算符
当删除 a、b、c 中任何一个对象,其余两个对象都不会指向一个空数据。只有当三个对象都删除时,才会真正释放矩阵数据。前面提到过,这依赖于 Mat 对象的引用计数机制(refcount 指针),它的本质是用于指向相同数据地址的不同类对象的内存管理,只有当矩阵数据引用次数为 0 的时候才会真正释放矩阵数据。
有时候,我们还是需要创建一个全新的 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_(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]
本文作为入门的准备,简单介绍了图像相关的基础知识、Mat 的基本结构、Mat 的创建/读取/赋值。在此基础上也引申出很多知识,比如矩阵的数据类型、掩模等,这些内容都是非常重要的。因此,后续的内容都会用到它们,因此也会会更加进一步详细地介绍它们。
【Java与Android技术栈】公众号
关注 Java/Kotlin 服务端、桌面端 、Android 、机器学习、端侧智能
更多精彩内容请关注: