Mat - 图像的容器
在对图像进行处理时,首先需要将图像载入到内存中,而Mat就是图像在内存中的容器,管理着图像在内存中的数据。Mat是C++ 的一个类,由于OpenCV2中引入了内存自动管理机制,所以不必手动的为Mat开辟内存空间以及手动的释放内存。Mat中包含的数据主要由两个部分构成:矩阵头(矩阵尺寸、存储方法、存储地址等信息)和一个指向存储图像所有像素值的矩阵(根据所选的存储方法不同的矩阵可以是不同的维数)的指针。
在图像处理中,对图像的处理不可能是在一个函数中完成的,这就需要在不同的函数间传递Mat。同时,图像处理的计算量是很大,除非万不得已就不要去传递比较大的Mat。这就要求使用某种机制来实现Mat的快速传递。Mat中主要有矩阵头和一个指向矩阵的指针,矩阵头是一个常数值,但是矩阵保存了图像所有的像素值,通常会比矩阵头大几个数量级,因此传递Mat是主要的消耗是在矩阵复制上。为了解决这个问题,OpenCV中引入了计数机制。每个Mat都有自己的信息头,但是共享同一个矩阵,也就是在传递Mat时,只复制矩阵头和指向矩阵的指针。
1: Mat a,c ;
2: a = imread("d:\test.jpg",1) ;
3: Mat b(a) ; //拷贝构造函数
4: a = c ; //复制运算符
上面代码中3个Mat对象a,b,c指向同一个矩阵,由于都指向了同一个矩阵,某一个对象对矩阵进行操作时也会影响到其他对象读取到的矩阵。
多个对象同时使用一个矩阵,那么当不需要该矩阵时,谁来负责清理?简单的回答是,最后一个使用它的对象。通过引用计数机制,无论什么时候Mat对象的信息头被复制了,都会增加矩阵的引用次数加1;反之,当一个Mat的信息头被释放后,引用计数就会被减1;当计数被减到0时,矩阵就会被释放。
当然,有些时候还是需要拷贝矩阵本身的,这时候可以使用clone和 copyTo。通过clone和copyTo创建的Mat,都有自己的矩阵,修改其中一个的矩阵不会对其他的造成影响。
访问像素的三种方法
对图像像素值的访问是图像处理最基本的要求,在OpenCV中提供了三种方式来访问图像的像素值。
矩阵在内存中的存储
首先来看一下图像像素值在内存中的保存方式。前面提到,像素值是以矩阵的方式保存的,矩阵的大小取决于图像采用的颜色模型,确切的说是图像的通道数。如果是灰度图像,矩阵是这样的:
矩阵的每一个元素代表一个像素 值。而对多通道图像来说,一个像素值需要多个矩阵元素来存储,矩阵中的列会包含多个子列,其子列数和通道数目相等。以常见的RGB模型来说:
而且,如果内存比较大,图像中的各行各列就可以一行一行的连接起来,形成一个长行。连续存储有助于提升图像的扫描速度,使用iscontinuous来判断矩阵是否是连续存储的。
颜色空间缩减
如果矩阵元素存储的是单通道像素,使用8位无符号来保存每个元素,那么像素可能有256个不同的值。如果是三通道的话,就会用一千六百多种颜色。如此多的颜色在有些时候不是必须的,而且会对算法的性能造成严重的影响。在这种情况下,最常用的做法就是颜色空间的缩减,也就是将现有的颜色空间进行映射,以获得较少的颜色数。例如:颜色值0到9映射为0,10到19映射为10,以此类推。
以简单颜色空间缩减为例,使用OpenCV提供的三种方式来遍历图像像素。将各个颜色值映射关系存储到表中,在对格像素的颜色值进行处理时,直接进行查表。下面是对映射表的初始化:
1:
2: uchar table[256] ;
3: int divideWith = 10;
4: for(int i = 0 ; i < 256 ; i ++)
5: table[i] = (uchar) ( divideWith * (i / divideWith));
这里将各个像素的颜色值整除以10,然后再乘以10,这样会像上面所说的将0到9的颜色值映射为0,10到19的颜色值映射为10,以此类推。
指针遍历图像 Efficient Way
1: Mat& scanImageWithPointer(Mat &img , const uchar * const table)
2: {
3: CV_Assert(img.depth () == sizeof(uchar));
4:
5: int channels = img.channels() ;
6:
7: int rows = img.rows * channels;
8: int cols = img.cols ;
9:
10: if(img.isContinuous()) {
11: cols *= rows ;
12: rows = 1 ;
13: }
14:
15: uchar * p ;
16: for(int i = 0 ; i < rows ; i ++){
17: p = img.ptr<uchar>(i);
18: for(int j = 0 ; j < cols ; j ++){
19: p[j] = table[p[j]] ;
20: }
21: }
22: return img ;
23: }
首先使用断言,只处理使用8位无符号数保存元素值的矩阵。然后在取出举证的行数和列数,如果是多通道的话矩阵是有子列的,用通道数乘以矩阵的行数作为最终遍历时行数。另外,调用isContinuous来判断矩阵在内存中是不是连续存储的。p = img.ptr<uchar>(i); 来获取每一行开始处的指针,然后遍历至改行的末尾。如果是连续存储的,就只需要获取一次每行的开始指针,一路遍历下去即可。
Mat中的data字段会返回指向矩阵第一行第一列的指针,通过可以使用该字段来检查图像是否被载入成功了。当矩阵是连续存储时,也可以通过data来遍历整个图像。
1: uchar * p = img.data ;
2: for(unsigned int i = 0 ; i < img.rows * img.cols * img.channels() ; i ++)
3: *p ++ = table[*p] ;
但是这种代码可读性差,并且进一步操作困难,其在性能上的表现并不明显的优于上面的方法。
迭代遍历图像 Safe Method
1: Mat& scanImageWithIterator(Mat &img,const uchar * const table)
2: {
3: CV_Assert(img.depth () == sizeof(uchar));
4:
5: const int channels = img.channels() ;
6:
7: switch (channels){
8: case 1:
9: {
10: MatIterator_<uchar> it,end ;
11: end = img.end<uchar>() ;
12: for(it = img.begin<uchar>(); it != end ; it ++) {
13: *it = table[*it] ;
14: }
15: break ;
16: }
17: case 2:
18: {
19: MatIterator_<Vec3b> it,end ;
20: end = img.end<Vec3b>() ;
21: for(it = img.begin<Vec3b>(); it != end ; it ++) {
22: (*it)[0] = table[(*it)[0]] ;
23: (*it)[1] = table[(*it)[1]] ;
24: (*it)[2] = table[(*it)[2]] ;
25: }
26: break ;
27: }
28: }
29: return img ;
30: }
1: cv::MatIterator_<cv::Vec3b> it = image.begin<cv::Vec3b>();
2: cv::Mat_<cv::Vec3b>::iterator it = image.begin<cv::Vec3b>();
同样的获取图像的常量迭代器也有两种方式
1: cv::MatConstIterator_<cv::Vec3b> it = image.begin<cv::Vec3b>();
2: cv::Mat_<cv::Vec3b>::const_iterator end = image.end<cv::Vec3b>();
at<>遍历图像
这种方法不推荐用来遍历图像,它主要用来获取或更改图像的中随机元素的。基本用途是用来访问特定的矩阵元素(知道行数和列数)
1: Mat& scanImageWithAt(Mat& img,const uchar * const table)
2: {
3: CV_Assert(img.depth () == sizeof(uchar));
4: const int channels = img.channels() ;
5:
6: switch (channels){
7: case 1:
8: {
9: for (int i = 0 ; i < img.rows ; i ++)
10: for(int j = 0 ; j < img.cols ; j ++)
11: img.at<uchar>(i,j) = table[img.at<uchar>(i,j)] ;
12: break ;
13: }
14: case 2:
15: {
16: Mat_<Vec3b> I = img ;
17: for(int i = 0 ; i < I.rows ; i ++){
18: for(int j = 0 ; j < I.cols ; j ++){
19: I(i,j)[0] = table[I(i,j)[0]] ;
20: I(i,j)[1] = table[I(i,j)[1]] ;
21: I(i,j)[2] = table[I(i,j)[2]] ;
22: }
23: }
24: img = I ;
25: break ;
26: }
27: }
28: return img ;
29: }
1: //n添加椒盐噪声的个数
2: void salt(Mat& img,int n)
3: {
4: for(int k = 0 ; k < n ; k ++) {
5: int i = rand() % img.rows ;
6: int j = rand() % img.cols ;
7:
8: if(img.channels() == 1){
9: img.at<uchar>(i,j) = 255 ;
10: }else if(img.channels() == 3){
11: img.at<Vec3b>(i,j)[0] = 255 ;
12: img.at<Vec3b>(i,j)[1] = 255 ;
13: img.at<Vec3b>(i,j)[2] = 255 ;
14: }
15: }
16: }
lenna图片添加椒盐噪声后
这里要提下,在使用imshow将Mat显示到命名窗口时,需要调用waitkey()这个函数下,不然的话在命名窗口显示不出来。
LUT
在图像处理中,对图像的所有像素重新映射是很常见的,在OpenCV中提供一个函数来实现该该操作,不需要去扫描整个图像,operation on array :LUT。
1: Mat lookupTable(1,256,CV_8S);
2: uchar *p = lookupTable.data ;
3: for(int i = 0 ; i < 256 ; i ++)
4: p[i] = table[i] ;
5:
6: Mat result ;
7: LUT(img,lookupTable,result) ;
LUT的函数原型
void LUT(InputArray src, InputArray lut, OutputArray dst)
src 输入的8位矩阵
lut 256个元素的查找表,为了应对多通道输入矩阵,查找表要么是单通道(此时,输入矩阵的多个通道使用相同的查找表),要么和输入矩阵有相同的通道数
dst 输出矩阵。和输入矩阵有相同的尺寸和通道