前言:
在计算机内存中,数字图像以矩阵的形式存储和运算,比如,在MatLab中,图像读取之后对应一个矩阵,在OpenCV中,同样也是如此。
在早期的OpenCV1.x版本中,图像的处理是通过IplImage(该名称源于Intel的另一个开源库Intel Image Processing Library ,缩写成IplImage)结构来实现的。早期的OpenCV是用C语言编写,因此提供的借口也是C语言接口,其源代码完全是C的编程风格。IplImage结构是OpenCV矩阵运算的基本数据结构。
到OpenCV2.x版本,OpenCV开源库引入了面向对象编程思想,大量源代码用C++重写,Mat类 (Matrix的缩写) 是OpenCV用于处理图像而引入的一个封装类。从功能上讲,Mat类在IplImage结构的基础上进一步增强,并且,由于引入C++高级编程特性,Mat类的扩展性大大提高,Mat类的内容在后期的版本中不断丰富,如果你查看Mat类的定义的话(OpenCV3.1\sources\modules\core\include\opencv2\core\mat.hpp),会发现其设计实现十分全面而具体,基本覆盖计算机视觉对于图像处理的基本要求。
接下来我们学习OpenCV的Mat类,并学习如何对Mat对象进行一些常见的操作。
一、Mat的常见属性
Mat对象的常用函数如下:
- data uchar型的指针。Mat类分为了两个部分:矩阵头和指向矩阵数据部分的指针,data就是指向矩阵数据的指针。
- dims 矩阵的维度,例如5*6矩阵是二维矩阵,则dims=2,三维矩阵dims=3.
- rows 矩阵的行数
- cols 矩阵的列数
- size 矩阵的大小,size(cols,rows),如果矩阵的维数大于2,则是size(-1,-1)
- channels 矩阵元素拥有的通道数,例如常见的彩色图像,每一个像素由RGB三部分组成,则channels = 3
- type
表示了矩阵中元素的类型以及矩阵的通道个数,它是一系列的预定义的常量,其命名规则为CV_(位数)+(数据类型)+(通道数)。具体的有以下值:
这里U(unsigned integer)表示的是无符号整数,S(signed integer)是有符号整数,F(float)是浮点数。CV_8UC1 CV_8UC2 CV_8UC3 CV_8UC4 CV_8SC1 CV_8SC2 CV_8SC3 CV_8SC4 CV_16UC1 CV_16UC2 CV_16UC3 CV_16UC4 CV_16SC1 CV_16SC2 CV_16SC3 CV_16SC4 CV_32SC1 CV_32SC2 CV_32SC3 CV_32SC4 CV_32FC1 CV_32FC2 CV_32FC3 CV_32FC4 CV_64FC1 CV_64FC2 CV_64FC3 CV_64FC4
例如:CV_16UC2,表示的是元素类型是一个16位的无符号整数,通道为2.
C1,C2,C3,C4则表示通道是1,2,3,4
type一般是在创建Mat对象时设定,如果要取得Mat的元素类型,则无需使用type,使用下面的depth - depth
矩阵中元素的一个通道的数据类型,这个值和type是相关的。例如 type为 CV_16SC2,一个2通道的16位的有符号整数。那么,depth则是CV_16S。depth也是一系列的预定义值,
将type的预定义值去掉通道信息就是depth值:
CV_8U CV_8S CV_16U CV_16S CV_32S CV_32F CV_64F - elemSize
矩阵一个元素占用的字节数,例如:type是CV_16SC3,那么elemSize = 3 * 16 / 8 = 6 bytes - elemSize1
矩阵元素一个通道占用的字节数,例如:type是CV_16CS3,那么elemSize1 = 16 / 8 = 2 bytes = elemSize / channels
二、图像读取
首先我们看一下测试照片,我们编写代码,读取测试图片,并将其显示出来:
#include <iostream>
#include <opencv2/opencv.hpp>
// author: 行歌
// time: 2018-4-12
using namespace cv;
using namespace std;
int main()
{
Mat img;
img = imread("test3.png");
if (img.empty())
{
printf("could not load the picture...");
}
namedWindow("原图:", CV_WINDOW_AUTOSIZE);
imshow("原图:", img);
waitKey(0);
system("PAUSE");
return 0;
}
运行程序,可以看到这张测试图片:
三、彩色图像灰度化
在OpenCV中实现将彩色像素(一个向量)转化为灰度像素(一个数值)的公式如下:
同时,OpenCV中定义了cvtColor函数实现BGR彩色空间的图像和其他颜色空间转换。cvCvtColor是Opencv里的颜色空间转换函数,可以实现RGB颜色向HSV,HSI等颜色空间的转换,也可以转换为灰度图像,其中参数CV_RGB2GRAY是RGB到gray。函数声明如下:
参数解释如下:
src表示输入的 8 - bit, 16 - bit或 32 - bit单倍精度浮点数影像。
dst表示输出的8 - bit, 16 - bit或 32 - bit单倍精度浮点数影像。
code表示色彩空间转换的模式,该code来实现不同类型的颜色空间转换。比如CV_BGR2GRAY表示转换为灰度图,CV_BGR2HSV将图片从RGB空间转换为HSV空间。其中当code选用CV_BGR2GRAY时,dst需要是单通道图片。当code选用CV_BGR2HSV时,对于8位图,需要将RGB值归一化到0 - 1之间。这样得到HSV图中的H范围才是0 - 360,S和V的范围是0 - 1。接下来,我们来实际操作,编写程序:
#include <iostream>
#include <opencv2/opencv.hpp>
// author: 行歌
// time: 2018-4-12
using namespace cv;
using namespace std;
int main()
{
Mat img;
img = imread("test3.png");
if (img.empty())
{
printf("could not load the picture...");
}
namedWindow("原图:", CV_WINDOW_AUTOSIZE);
imshow("原图:", img);
// 将彩色图转换为灰度图,常采用以下方法:
Mat gray_img;
cvtColor(img, gray_img, CV_RGB2GRAY);
namedWindow("灰度图片:", CV_WINDOW_AUTOSIZE);
imshow("灰度图片:", gray_img);
waitKey(0);
system("PAUSE");
return 0;
}
运行程序,我们得到结果:
四、分离通道
有时候我们需要将多通道矩阵分离成单通道矩阵,即将所有向量的第一个值组成的单通道矩阵作为第一通道,将所有向量的第二元素组成的单通道矩阵作为第二通道,依次类推。那么OpenCV当中如何才能够实现呢?其实很简单!
使用OpenCV提供的split函数可分离多通道,如将多通道矩阵img分离为多个单通道,这些单通道矩阵被存放在vector容器中。我们修改代码:
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace cv;
using namespace std;
int main()
{
Mat img;
img = imread("test3.png");
if (img.empty())
{
printf("could not load the picture...");
}
namedWindow("原图:", CV_WINDOW_AUTOSIZE);
imshow("原图:", img);
// 将彩色图进行通道分离,常采用以下方法:
vector<Mat> planes;
split(img, planes);
imshow("蓝色通道:", planes[0]);
imshow("绿色通道:", planes[1]);
imshow("红色通道:", planes[2]);
waitKey(0);
system("PAUSE");
return 0;
}
运行代码,得到结果:
五、访问单通道Mat对象中的值
访问Mat对象中的值,最直接的方式是使用Mat的成员函数at,如对于单通道且数据类型为CV_32F的对象m,访问它的第r行第c列的值,格式为:m.at<float>(r,c)。接下来我们就尝试用成员函数at依次访问Mat对象中的所有值,编写代码:
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace cv;
using namespace std;
int main()
{
Mat img;
img = imread("test3.png");
if (img.empty())
{
printf("could not load the picture...");
}
// 将彩色图转换为灰度图,常采用以下方法:
Mat gray_img;
cvtColor(img, gray_img, CV_RGB2GRAY);
namedWindow("灰度图片:", CV_WINDOW_AUTOSIZE);
imshow("灰度图片:", gray_img);
// 使用成员函数at访问Mat对象中的值,如对于单通道且数据类型为CV_32F的对象m,访问它的第r行第c列的值,格式为:m.at<float>(r,c)。
// 接下来我们利用函数at依次访问Mat对象当中的所有值。
for (int r = 0; r < gray_img.rows; ++r)
{
for (int c = 0; c < gray_img.cols; ++c)
{
gray_img.at<uchar>(r, c) = 255 - int(gray_img.at<uchar>(r, c)); //反差处理
}
}
namedWindow("新的图片:", CV_WINDOW_AUTOSIZE);
imshow("新的图片:", gray_img);
waitKey(0);
system("PAUSE");
return 0;
}
运行程序,如下所示:
六、访问多通道Mat对象中的值
利用成员函数at访问多通道Mat的元素值,可以将三通道Mat看作一个特殊的二维数组,只是在每一个位置上不是一个数值而是一个向量(元素)。在此之前,首先介绍一下OpenCV当中的一个重要的类——向量类Vec。
这里的向量可以理解为数学意义上的列向量,构造一个_cn*1的列向量,数据类型为_Tp,格式如下:
Vec <Typename _Tp,int _cn>
比如我们构造一个长度为3,数据类型为uchar且初始化为10,11,12的列向量,可以这样: Vec <uchar,3> vec_ (10,11,12);
那么如何访问这个列向量中的值呢?可以利用“[ ]”或者“( )”操作符访问向量中的值。如:vec_ [ 0 ]或者vec_( 0 )等都可以实现。
还有很重要的一点就是,OpenCV为向量类的声明取了一个别名,例如:
typedef Vec<uchar, 3> Vec3b;
typedef Vec<int, 2> Vec2i;
typedef Vec<float, 4> Vec4f;
typedef Vec<double, 3> Vec3d;
理解上述概念以后,我们编写代码:
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace cv;
using namespace std;
int main()
{
Mat img;
img = imread("test3.png");
if (img.empty())
{
printf("could not load the picture...");
}
namedWindow("原图:", CV_WINDOW_AUTOSIZE);
imshow("原图:", img);
//接下我们基于成员函数at和向量类vec对三个通道的Mat对象img进行访问
Mat dst;
dst = Mat(img.size(),img.type()); // 创建一个与img同类型和大小的Mat对象
int height = img.rows;
int width = img.cols;
int channel_num = img.channels();
if (channel_num ==3)
{
for (int r = 0; r < height; ++r)
{
for (int c = 0; c < width; ++c)
{
dst.at<Vec3b >(r, c)[0] = 255 - img.at<Vec3b >(r, c)[0];
dst.at<Vec3b >(r, c)[1] = 255 - img.at<Vec3b >(r, c)[1];
dst.at<Vec3b >(r, c)[2] = 255 - img.at<Vec3b >(r, c)[2];
cout << img.at<Vec3b>(r, c) << ",";
// 将三通道Mat看作一个特殊的二维数组,只是在每一个位置上不是一个数值而是一个向量(元素),依次打印每一个向量元素
}
cout << endl;
}
}
namedWindow("新的图片:", CV_WINDOW_AUTOSIZE);
imshow("新的图片:", dst);
waitKey(0);
system("PAUSE");
return 0;
}
运行程序,得到:
这是通过按行号、列号取出的每一个元素Vec3b:
这是对三通道矩阵作反差处理以后的结果: