• 如果您觉得本站非常有看点,那么赶紧使用Ctrl+D 收藏吧

opencv2学习笔记(1)

互联网 diligentman 1周前 (11-18) 4次浏览

cv::Mat 类是用于保存图像以及其他矩阵数据的数据结构。默认情况下,它们的尺寸为0,但是也可以指定初始尺寸。

cv::Mat ima(240,320,CV_8U,cv::Scalar(100));

同时指定矩阵中元素的类型,这里的CV_8U对应的是单字节的像素图像。字母U意味着无符号的(Unsigned)。也可以使用字母S声明带符号的类型。
对于彩色图像,需要指定三个通道(CV_8UC3).也可以声明16位或32位的整数图像,或者浮点数图像。
当CV::Mat对象离开作用域后,分配的内存将被自动释放,防止内存溢出。
cv::Mat还实现了引用计数以及浅拷贝,当图像之间进行赋值时,图像数据并没有发生赋值,两个对象都指向同一块的内存块。这也可用于参数传值的图像,以及返回值传值的图像。引用计数的作用是只有当所有引用内存数据的对象都被析构后,才会释放内存块。
可以使用copyTo()方法。

cv::Mat image2,image3;
image2 = result;
result.copyTo(image3);
//创建新的对象实现拷贝
cv::Mat function()
//作为类 返回图像 自动实现浅拷贝
{
	cv::Mat ima(240,320,CV_8U,cv::Scalar(100));
	return ima;
}

//得到灰度图
cv::Mat gray = function();

gray变量将包含函数中创建的图像内容,而不涉及额外的内存分配。

然而在类中使用时需要谨慎,避免返回类中的图像成员

class Test{
	cv::Mat ima;
	public:
	//构造函数
		Test() :ima(240,320,CV_8U,cv::Scalar(100))
		//初始化
		{}
		cv::Mat method()
		{return ima;}

如果调用了类的这个方法,返回值和属性共用一块内存,这样修改返回值时很容易引起对象属性的改变,影响侯敏的计算。
所以应该对返回的变量进行深拷贝。

第二章
操作像素

存取像素值
为了存取矩阵元素,需要在代码中指定元素所在的行和列。程序会返回相应的元素。如果图像时单通道的,返回值时单个数值,如果图像时多通道的,返回值则是一组向量。

椒盐噪点:随机的将部分像素设置为白色或者黑色。在传输过程中,如果部分像素值丢失,那么这种早点就会出现。

首先对一个图像随机修改一些白色像素点。

void salt(cv::Mat &image, int n)
{
	for(int k=0; k<n ;k++)
	{
	rand()
	//生成随机数
	int i = rand()%image.cols;
	int j = rand()%image.rows;
	if(image.channels() == 1)
	//判断是否为灰度图
	{
	image.at<uchar>(j,i) = 255;
	}
	else if(image.channels() == 3)
	//判断是否为彩色图
	{
	image.at<cv::Vec3b>(j,i)[0] = 255;
	image.at<cv::Vec3b>(j,i)[1] = 255;
	image.at<cv::Vec3b>(j,i)[2] = 255;
	//对于彩色图像需要将每个通道的值都设为白色,才能得到一个白色像素
	}
}
}

//打开图像
cv::Mat image = cv::imread("boldt.jpg");
salt(image,3000);
cv::namedWindow("image");
cv::imshow("image",image);

类cv::Mat 有若干成员函数可以获取图像的属性,公有属性cols和rows给出了图像的宽和高。
成员函数at(inty,intx)可以用来存储图像元素。但是必须在编译期已知图像的类型。
因为cv::Mat可以存放任意数据类型的元素。
所以需要先指定数据类型

image.at<uchar>(j,i) = 255;

at方法不会进行任何数据类型的转换。
opencv将彩色图像类向量定义为cv::Vec3b,即由三个unsigned char的向量组成。

使用指针遍历图像
使用双重循环遍历所有的像素值

void colorReduce(cv::Mat &image,int div=64)
{
	int nl = image.rows;
	int nc = image.cols*image.channels();
	for(int j=0;j<nl;j++)
	{
		//得到第j行的首地址
		uchar* data = image.ptr<uchar>(j);
		for(int i = 0;i < nc;i++)
		{
			data[i] = data[i]/div*div + div/2;
		}
	}
}
//主函数
image = cv::imread("boldt.jpg");
colorReduce(image);
cv::namedWindow("Image");
cv::imshow("Image",image);

cv::Mat中的ptr函数可以得到图像任意行的首地址。
可以等效的使用指针运算从一列移动到下一列。

*data++ = *data/div*div + div/2;

其它的颜色缩减公式
也可以通过模运算来计算最接近被除数的除数(缩减因子,div)的整数倍数。

data[i]=  data[i] - data[i]%div + div/2;

但是这个计算方式会偏慢,因为它需要存取每个像素两次。

另一个选择是使用位运算。如果我们限制缩减因子为2的幂次,即div = pow(2,n), 那么,只取像素值的前n位即可得到不大于该值的关于缩减因子的最大整数倍数。运算掩模可以通过简单的移位操作得到。

uchar mask = 0xFF<<n;
//for div = 16, mask = 0xF0;

颜色缩减的计算

data[i] = (data[i]&mask) + div/2;

通常而言,位运算非常搞笑,所以在需要考虑效率的情况下位运算是一个强大的备选方式。

使用输入和输出参数
颜色变换在上文中,是直接作用于输入图像的,我们称之为inplace变换,这种方式不需要额外的图像来保存输出结果。
但是有些时候不希望改变原图像,所以调用之前应当创建原始图像的拷贝。
最简单的深拷贝就是使用clone函数。

image = cv::imread("boldt.jpg");
cv::Mat imageClone = image.clone();
colorReduce(imageClone);
cv::namedWindow("Image result");
cv::imshow("Image result", imageClone);

在具体实现过程中,可以给用户选择是否采用in-place的处理方式。函数的实现如下。

void colorReduce(const cv::Mat &image,
cv::Mat &result,int div = 64);

这里输入图像是通过常量传递的,函数不会修改原图像。
但是当选择In-place的处理方式时,用户可以将输入输出指定为同一个变量:

coloeReduce(image,image);

否则,用户必须提供另一个cv::Mat实例

cv::Mat result;
colorReduce(image,result);

注意:
这里必须检查输入和输出图像的大小和元素数据类型是否一致。
cv::Mat成员函数create内置了这个检查操作。
如果需要新的尺寸和数据类型对一个矩阵进行重新分配,那么我们就可以调用create成员函数。而且,如果新指定的尺寸和数据类型与原有的一样,create函数会直接返回。
首先,利用create来创建一个与输入图像的尺寸和类型相同的矩阵:

result.create(image.rows,image.cols,image.type());

注意:create函数创建的图像的内存都是自连续的,create函数不会对图像的行进行填补。分配的内存大小为total()*elemSize()。循环使用两个指针完成。

for(int j = 0;j<nl,j++)
{
	const uchar*data_in = image.ptr<uchar>(j);
	//获取第j行的首地址
	uchar* data_out = result.ptr<uchar>(j);
	for(int i = 0;i < nc; i++)
	{
		data_out[i] = data_in[i]/div*div
 + div/2;
 	}
}

高效遍历连续图像
考虑到效率 图像有可能会在行尾扩大若干个像素,但是当不对图像进行填补时,图像可以被视为一个一维数组,可以通过cv::Mat的isContinuous方法来判断这个图像是否进行了填补。如果isContinuous方法返回值为真的话,说明没有进行过填补。
在一些图像处理算法中,我们可以利用图像的连续性,把整个处理过程使用一个循环完成。颜色缩减函数可以重写为

void colorReduce(cv::Mat &image, int div = 64)
{
	int nl = image.rows;
	int nc = image.cols*image.channels();
	if(image.isContinous())
	//没有额外的填补像素
	{
		nc = nc*nl;
		nl = 1//是一个一维数组,将图片展平
	}
	//对于连续图像,本循环只执行一次
	for(int j = 0;j < nl;j++)
	{
		uchar*data = image.ptr<uchar>(j);
		for(int i = 0;i < nc;i++)
		{
			data[i] = data[i]/div*div + div/2;
		}
	}
}

当通过isContinuous得知图像没有对行进行填补后,就可以将宽设置为1,高度设置为W×H,从而消除外层循环。
注意:也可以使用reshape方法来重写这段代码。

if(image.isContinous())
{
	//确定没有填补
	image.reshape(1,image.cols*image.rows);
}
int nl = image.rows;
int nc = image.cols*umage.channels();

reshape不需要内存拷贝或者重新分配就能改变矩阵的维度。两个参数分别为新的通道数和新的行数,矩阵的列数可以根据新的通道数和行数来自适应。
在这些实现中,内层循环依次处理图像的全部像素。这个方法在同时处理若干个小图像时会很有优势。

底层指针运算
在类cv::Mat中,图像数据以unsigned char的形式保存在一块内存中,这块内存的首地址可以通过data成员变量得到。data是一个unsigned char型的指针,所以循环可以以如下方式开始。

uchar* data = image.data;

从当前行到下一行可以通过对指针加上行宽完成。

data += image.step;

step代表图像的行宽(包括填补像素)。
通常可以通过如下方式获取第j行第i列的像素的地址

data = image.data + j*image.step + i*image.elemszie();
//或
data = &image.at(j,i);

但是这种方法容易出错,且不适用于待遇感兴趣取余的图像。

使用迭代器遍历图像
一个cv::Mat实例的迭代器可以通过创建一个cv::Mat Iterator_的实例来得到。下划线意味着它是一个模板类。
因为通过迭代器来存取图像的元素,必须在编译期知道图像元素的数据类型。

cv::Mat Iterator_<cv::Vec3b> it;

也可以使用定位在Mat_内部的迭代器类型:

cv::Mat_<cv::Vec3b>::iterator it;


喜欢 (0)