opencv笔记(八)——OpenCV中Mat总结

转自:

https://www.cnblogs.com/justkong/p/7278579.html

https://blog.csdn.net/xiaowei_cqu/article/details/7771760

一、数字图像存储概述

数字图像存储时,我们存储的是图像每个像素点的数值,对应的是一个数字矩阵。Mat和Matlab里的数组格式有点像,但一般是二维向量,如果是灰度图,一般存放<uchar>类型;如果是RGB彩色图,存放<Vec3b>类型。

单通道灰度图数据存放格式:

多通道的图像中,每列并列存放通道数量的子列,如RGB三通道彩色图:

注意通道的顺序反转了:BGR。通常情况内存足够大的话图像的每一行是连续存放的,也就是在内存上图像的所有数据存放成一行,这中情况在访问时可以提供很大方便。可以用 isContinuous()函数来判断图像数组是否为连续的。

二、Mat的存储

1、OpenCV1基于C接口定义的图像存储格式IplImage*,直接暴露内存,如果忘记释放内存,就会造成内存泄漏。

2、从OpenCV2开始,开始使用Mat类存储图像,具有以下优势:

(1)图像的内存分配和释放由Mat类自动管理

(2)Mat类由两部分数据组成:矩阵头(包含矩阵尺寸、存储方法、存储地址等)和一个指向存储所有像素值的矩阵(根据所选存储方法的不同,矩阵可以是不同的维数)的指针。Mat在进行赋值和拷贝时,只复制矩阵头,而不复制矩阵,提高效率。如果矩阵属于多个Mat对象,则通过引用计数来判断,当最后一个使用它的对象,则负责释放矩阵。

(3)可以使用clone和copyTo函数,不仅复制矩阵头还复制矩阵。

三、Mat创建

1、使用Mat构造函数

Mat test(2,2,CV_8UC3,Scalar(0,0,255));

2、使用Mat构造函数2

int sizes[3] = {2,2,2};

Mat test(3,sizes,CV_8UC3,Scalar::all(0));

3、为已存在的IplImage指针创建信息头

IplImage* img = cvLoadImage("1.jpg",1);

Mat test(img);

4、利用create函数

Mat test;

test.create(4,4,CV_8UC2);

5、采用Matlab形式的初始化方式

(1)Mat me = Mat::eye(4,4,CV_64F);

(2)Mat mo = Mat::ones(2,2,CV_32F);

(3)Mat mz = Mat::zeros(3,3,CV_8UC1);

注:元素类型,即CV_[位数][带符号与否][类型前缀]C[通道数]

四、Mat中相关成员的意义

1、data

Mat对象中的一个指针,指向存放矩阵数据的内存(uchar* data)

2、dims

矩阵的维度,3*4的矩阵维度为2维,3*4*5的矩阵维度为3维

3、channels

矩阵通道,矩阵中的每一个矩阵元素拥有的值的个数,比如说 3 * 4 矩阵中一共 12 个元素,如果每个元素有三个值,那么就说这个矩阵是 3 通道的,即 channels = 3。常见的是一张彩色图片有红、绿、蓝三个通道。

4、depth

深度,即每一个像素的位数,也就是每个通道的位数。在opencv的Mat.depth()中得到的是一个0 – 6的数字,分别代表不同的位数:enum { CV_8U=0, CV_8S=1, CV_16U=2, CV_16S=3, CV_32S=4, CV_32F=5, CV_64F=6 },可见 0和1都代表8位, 2和3都代表16位,4和5代表32位,6代表64位。

5、elemSize

矩阵中每个元素的大小,每个元素包含channels个通道。如果Mat中的数据的数据类型是CV_8U那么elemSize = 1;是CV_8UC3那么elemSize = 3,是CV_16UC2那么elemSize = 4。

6、elemSize1

矩阵中数据类型的大小,即elemSize/channels,也就是depth对应的位数。

7、step

是一个数组,定义了矩阵的布局,参考下图

若矩阵有n维,则step数组大小为n

step[n-1] = elemSize(每个矩阵元素的数据大小)

step[n-2] = size(1维)*elemSize

step[n-3] = size(2维)*size(1维)*elemSize

...

step[0] = size(n-1维)*size(n-2维)*...size(1维)*elemSize

8、step1

step1也是一个数组,为step/elemSize1,若矩阵有n维,则step1[n-1] = channels。

9、type

矩阵元素的类型,即创建Mat时传递的类型,例如CV_8UC3、CV_16UC2等。

五、访问图像中的像素

高效的方法:C操作符[ ]

最快的是直接用C风格的内存访问操作符[]来访问:

Mat& ScanImageAndReduceC(Mat& I, const uchar* const table)
{
	// accept only char type matrices
	CV_Assert(I.depth() != sizeof(uchar));
	int channels = I.channels();
	int nRows = I.rows ;
	int nCols = I.cols* channels;
	if (I.isContinuous())
	{
		nCols *= nRows;
		nRows = 1;
	}
	int i,j;
	uchar* p;
	for( i = 0; i < nRows; ++i)
	{
		p = I.ptr<uchar>(i);
		for ( j = 0; j < nCols; ++j)
		{
			p[j] = table[p[j]];
		}
	}
	return I;
}

注意:书中这段代码是有问题的,前面写成了 

int nRows = I.rows * channels;
int nCols = I.cols;

一般情况 isContinous为true,运行不会出错,但你可以注释掉那个if,会有访问越界的问题。

这种访问形式就是在每行定义一个指针,然后在内存上直接连续访问。如果整个数组在内存上都是连续存放的,那么只需要定义一个指针就可以访问所有的数据!如单通道的灰度图访问方式如下:

uchar* p = I.data;
for( unsigned int i =0; i < ncol*nrows; ++i)
	*p++ = table[*p];

安全的方法:迭代器iterator

相比用指针直接访问可能出现越界问题,迭代器绝对是非常安全的方法:

Mat& ScanImageAndReduceIterator(Mat& I, const uchar* const table)
{
	// accept only char type matrices
	CV_Assert(I.depth() != sizeof(uchar));
	const int channels = I.channels();
	switch(channels)
	{
	case 1:
		{
			MatIterator_<uchar> it, end;
			for( it = I.begin<uchar>(), end = I.end<uchar>(); it != end; ++it)
				*it = table[*it];
			break;
		}
	case 3:
		{
			MatIterator_<Vec3b> it, end;
			for( it = I.begin<Vec3b>(), end = I.end<Vec3b>(); it != end; ++it)
			{
				(*it)[0] = table[(*it)[0]];
				(*it)[1] = table[(*it)[1]];
				(*it)[2] = table[(*it)[2]];
			}
		}
	}
	return I;
}

这里我们只定义了一个迭代器,用了一个for循环,这是因为在OpenCV里迭代器会访问每一列然后自动跳到下一行,不用管在内存上是否isContinous。另外要注意的是在三通道图像中我们定义的是 <Vec3b>格式的迭代器,如果定义成uchar,则只能访问到B即蓝色通道的值。
这种方式虽然安全,但是挺慢的,一会儿就知道了。

更慢的方法:动态地址计算

这种方法在需要连续扫描所有点的应用时并不推荐,因为它更实用与随机访问。这种方法最基本的用途是访问任意的某一行某一列:

Mat& ScanImageAndReduceRandomAccess(Mat& I, const uchar* const table)
{
	// accept only char type matrices
	CV_Assert(I.depth() != sizeof(uchar));
	const int channels = I.channels();
	switch(channels)
	{
	case 1:
		{
			for( int i = 0; i < I.rows; ++i)
				for( int j = 0; j < I.cols; ++j )
					I.at<uchar>(i,j) = table[I.at<uchar>(i,j)];
			break;
		}
	case 3:
		{
			Mat_<Vec3b> _I = I;
 
			for( int i = 0; i < I.rows; ++i)
				for( int j = 0; j < I.cols; ++j )
				{
					_I(i,j)[0] = table[_I(i,j)[0]];
					_I(i,j)[1] = table[_I(i,j)[1]];
					_I(i,j)[2] = table[_I(i,j)[2]];
				}
				I = _I;
				break;
		}
	}
	return I;
}

因为这种方法是为随机访问设计的,所以真的是奇慢无比。。。
 

减小颜色空间 color space reduction

现在来介绍下上述函数对每个元素的操作,也就是用table更改像素值。这里其实是做了个减小颜色空间的操作,这在一些识别之类的应用中会大大降低运算复杂度。类如uchar类型的三通道图像,每个通道取值可以是0~255,于是就有 256*256个不同的值。我们可以通过定义:
0~9 范围的像素值为 0
10~19 范围的像素值 为 10
20~29 范围的像素值为 20
。。。。。。
着这样的操作将颜色取值降低为 26*26*26 种情况。这个操作可以用一个简单的公式:

来实现,因为C++中int类型除法操作会自动截余。 类如 Iold=14; Inew=(Iold/10)*10=(14/10)*10=1*10=10;
在处理图像像素时,每个像素需要进行一遍上述计算也需要一定的时间花销。但我们注意到其实只有 0~255 种像素,即只有256种情况。进一步可以把256种计算好的结果提前存在表中 table 中,这样每种情况不需计算直接从 table 中取结果即可。

int divideWith=10; 
uchar table[256];
for (int i = 0; i < 256; ++i)
	table[i] = divideWith* (i/divideWith);

于是table[i]存放的是值为i的像素减小颜色空间的结果,这样也就可以理解上述方法中的操作:

p[j] = table[p[j]];

LUT : Look up table

OpenCV 很聪明的有个 LUT 函数就是针对这种 Look up talbe 的操作:

Mat lookUpTable(1, 256, CV_8U);
uchar* p = lookUpTable.data;
for( int i = 0; i < 256; ++i)
	p[i] = table[i];
for (int i = 0; i < times; ++i)
	LUT(I, lookUpTable, J);

算法计时

为了验证几种方法的效率,可以用一个简单的计时和输出:

double t;
t = (double)getTickCount();
t = 1000*((double)getTickCount() - t)/getTickFrequency();
t /= times;

实验结果

原图:

降低颜色空间结果:

算法时间:

更清楚的时间对比表:

猜你喜欢

转载自blog.csdn.net/qq_37764129/article/details/81272046