首页 > 学院 > 开发设计 > 正文

【OpenCV学习笔记】三、操作像素

2019-11-08 02:15:17
字体:
来源:转载
供稿:网友

最近在系统地学习OpenCV,将学习的过程在此做一个记录,主要以代码+注释的方式

记录学习过程。

1.访问像素值

  要访问矩阵中的每个独立元素,只需要指定它的行号和列号。返回的对应元素可以是单个数值,也可

以是多通道图像的数值向量。给图像加入椒盐噪声(salt-and-pepper noise),来说明如何直接访问像素值。顾名思义,椒盐噪声是一个专门的噪声类型,它随机选择一些像素,把它们的颜色替换成白色或黑色。

#include <opencv2/core/core.hpp>#include <opencv2/highgui/highgui.hpp>void salt(cv::Mat image, int n){	int i, j;	for (int k = 0; k < n; k++) {		// rand()是随机数生成器		//利用cv::Mat中公共成员变量cols和rows得到图像的列数和行数		i = std::rand() % image.cols;		j = std::rand() % image.rows;		//使用type方法来区分灰度图像和彩色图像。		if (image.type() == CV_8UC1)     // 灰度图像		{ 			//利用cv::Mat的at(int y,int x)方法可以访问元素			//at方法被实现成一个模板方法,在调用时必须指定图像元素的类型			image.at<uchar>(j, i) = 255;		}		else if (image.type() == CV_8UC3)  // 彩色图像		{		/*彩色图像的每个像素对应三个部分:红色、绿色和蓝色通道。因此包		含彩色图像的cv::Mat类会返回一个向量,向量中包含三个8位的数值。 		OpenCV为这样的短向量定义了一种类型,即cv::Vec3b。这个向量包含		三个无符号字符(unsigned character)类型的数据。因此,访问彩色		像素中元素的方法如下:*/			image.at<cv::Vec3b>(j, i)[0] = 255;			image.at<cv::Vec3b>(j, i)[1] = 255;			image.at<cv::Vec3b>(j, i)[2] = 255;		}	}}/*修改图像的函数在使用图像作为参数时,都采用了值传递的方式。之所以这样做,是因为它们在复制图像时仍共享了同一块图像数据。因此在需要修改图像内容时,图像参数没必要采用引用传递的方式*/int main(){	// 打开图像	cv::Mat image = cv::imread("boldt.jpg");	// 调用函数以添加噪声	salt(image, 3000);	// 显示图像	cv::namedWindow("Image");	cv::imshow("Image", image);	cv::waitKey(0);	return 0;}

运行结果:

2.用指针遍历图像

  以减少图像中颜色的数量这个任务为例,来说明遍历图像的过程。

  彩色图像由三个通道组成,每个通道对应三原色(红、绿、蓝)之一的强度。由于每个强度值

都是用一个8位的unsigned char表示,所以全部可能的颜色数目为256 × 256 × 256, 

大于1600万个。理所当然,为了降低分析的复杂度,降低图像中的颜色数目有时是有用的。

  基本的减色算法很简单。假设N是减色因子,将图像中每个像素的每个通道的值除以N

(使用整数除法,不保留余数)。然后将结果乘以N,得到N的倍数,并且刚好不超过原始像素值。

只需加上N/2,就得到相邻的N倍数之间的中间值。对所有8位通道值重复这个过程,就会得到(256/N)× (256/N)×(256/N)种可能的颜色值。

#include <opencv2/core/core.hpp>#include <opencv2/highgui/highgui.hpp>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的地址		/*为了简化指针运算的计算过程,cv::Mat类提供ptr函数,可以		直接访问图像中任一行的地址。ptr函数是一个模板函数, 返回第j行的地址:*/		uchar* data = image.ptr<uchar>(j);		for (int i = 0; i < nc; i++) 		{			// 处理每个像素 ---------------------			data[i] = data[i] / div*div + div / 2;			// 像素处理结束 -----------			/*注意在处理语句中, 我们也可以采用另一种等价的做法, 即利用指针			运算从一列移到下一列。 因此可以使用下面的代码:*/			//*data = *data / div*div + div2; data++;		} // 一行结束	}}int main(){	// 读取图像	cv::Mat image = cv::imread("boldt.jpg");	// 处理图像	colorReduce(image, 64);	// 显示图像	cv::namedWindow("Image");	cv::imshow("Image", image);	cv::waitKey(0);	return 0;}运行结果:

3.用迭代器遍历图像  在面向对象编程时,我们通常用迭代器对数据集合进行循环遍历。迭代器是一种类,

专门用于遍历集合的每个元素,隐藏了遍历过程的具体细节。标准模板库(STL)对容器类型

都定义了对应的迭代器,OpenCV也提供了cv::Mat的迭代器,并且与C++ STL中的标准迭代器兼容。

  依然以减色程序为例。

#include <opencv2/core/core.hpp>#include <opencv2/highgui/highgui.hpp>void colorReduce(cv::Mat &image, int div = 64) {	// 在初始位置获得迭代器	/*要得到cv::Mat实例的迭代器,首先要创建一个cv::MatIterator_对象。 	跟cv::Mat_类似,这个下划线表示它是一个模板子类。 因为图像迭代器是用来	访问图像元素的,所以必须在编译时就明确返回值的类型。 可以这样定义迭代器:*/	cv::Mat_<cv::Vec3b>::iterator it;	/*然后就可以使用常规的迭代器方法begin和end对像素进行循环遍历了。	不同之处在于它们仍然是模板方法。*/	it = image.begin<cv::Vec3b>(); 	// 获得结束位置	cv::Mat_<cv::Vec3b>::iterator itend =		image.end<cv::Vec3b>();	// 循环遍历所有像素	for (; it != itend; ++it) 	{		// 处理每个像素 ---------------------		/*注意这里处理的是一个彩色图像, 因此迭代器返回cv::Vec3b实		例。 你可以用取值运算符[]访问每个颜色通道的元素。*/		(*it)[0] = (*it)[0] / div*div + div / 2;		(*it)[1] = (*it)[1] / div*div + div / 2;		(*it)[2] = (*it)[2] / div*div + div / 2;		// 像素处理结束 ----------------	}}int main(){	// 读取图像	cv::Mat image = cv::imread("boldt.jpg");	// 处理图像	colorReduce(image, 64);	// 显示图像	cv::namedWindow("Image");	cv::imshow("Image", image);	cv::waitKey(0);	return 0;}

不管扫描的是哪种类型的集合,使用迭代器时总是遵循同样的模式。

首先你要使用合适的专用类创建迭代器对象,在本例中是cv::Mat_<cv::Vec3b>:: iterator,

然后可以用begin方法,在开始位置(本例中为图像的左上角)初始化迭代器。对于cv::Mat实例,

可以使用image.begin<cv::Vec3b>()。 

还可以在迭代器上使用数学计算,例如若要从图像的第二行开始,可以用

image.begin<cv::Vec3b>()+image.cols初始化cv::Mat迭代器。 

获得集合结束位置的方法也类似,只是改用end方法。但是,用end方法得到的迭代器已经超出了

集合范围,因此必须在结束位置停止迭代过程。结束的迭代器也能使用数学计算,例如,如果你想在最后一行前就结束迭代, 可使用image.end<cv::Vec3b>()-image.cols。初始化迭代器后,建立一个循环遍历所有元素,直到与结束迭代器相等。 

典型的while循环就像这样:

while (it!= itend) {// 处理每个像素 ---------------------// 像素处理结束 ---------------------++it;}

你可以用运算符++来移动到下一个元素,也可以指定更大的步幅。例如用it+=10, 

对每10个像素处理一次。最后,在循环内部使用取值运算符*来访问当前元素,你可以用它来读(例如element= *it;)或写(例如*it= element;)。

运行结果(同2中指针遍历的效果):

4.检查代码运行效率为了衡量函数或代码段的运行时间,OpenCV有一个非常实用的函数,即cv::getTickCount(),

该函数返回从最近一次电脑开机到当前的时钟周期数。因为我们希望得到以秒为单位的代码运行时间,所以要使用另一个方法,即cv::getTickFrequency(),这个方法返回每秒的时钟周期数。

为了获得某个函数(或代码段)的运行时间,通常需使用这样的程序模板:

const int64 start = cv::getTickCount();colorReduce(image); // 调用函数// 经过的时间( 单位: 秒)double duration = (cv::getTickCount()-start)/cv::getTickFrequency();5.遍历图像和邻域操作

在图像处理中计算像素值时,经常需要用它的相邻像素的值。

以对图像进行锐化为例,在图像处理领域有一个众所周知的结论:如果从图像中减去拉普拉斯算子部分,图像的边缘就会放大,因而图像会变得更加尖锐。

用以下方法计算锐化的数值:

sharpened_pixel= 5*current-left-right-up-down;

这里不能使用就地处理,使用者必须提供一个输出图像。图像扫描中使用了三个指针,一个表示当前行, 一个表示上面的行,另外一个表示下面的行。另外,在计算每一个像素时都需要访问与它相邻的像素,因此有些像素的值是无法计算的,包括第一行、最后一行、第一列、最后一列的像素。这个循环可以这样写:

#include <opencv2/core/core.hpp>#include <opencv2/highgui/highgui.hpp>void sharpen(const cv::Mat &image, cv::Mat &result) {	// 判断是否需要分配图像数据。 如果需要, 就分配	result.create(image.size(), image.type());	int nchannels = image.channels(); // 获得通道数	// 处理所有行( 除了第一行和最后一行)	for (int j = 1; j < image.rows - 1; j++) {		const uchar* PRevious =			image.ptr<const uchar>(j - 1); // 上一行		const uchar* current =			image.ptr<const uchar>(j); // 当前行		const uchar* next =			image.ptr<const uchar>(j + 1); // 下一行		uchar* output = result.ptr<uchar>(j); // 输出行		for (int i = nchannels; i < (image.cols - 1)*nchannels; i++) {			*output++ = cv::saturate_cast<uchar>(				5 * current[i] - current[i - nchannels] -				current[i + nchannels] - previous[i] - next[i]);		}	} // 把未处理的像素设为0		result.row(0).setTo(cv::Scalar(0));	result.row(result.rows - 1).setTo(cv::Scalar(0));	result.col(0).setTo(cv::Scalar(0));	result.col(result.cols - 1).setTo(cv::Scalar(0));}int main(){	// 读取图像	cv::Mat image = cv::imread("boldt.jpg");	cv::Mat result;	// 处理图像	sharpen(image, result);	// 显示图像	cv::namedWindow("Image");	cv::imshow("Image", result);	cv::waitKey(0);	return 0;}

5+.卷积操作

在对像素邻域进行计算时, 通常用一个核心矩阵来表示。 这个核心矩阵展现了为得到预期结果, 如何将计算相关的像素组合起来。 针对本节使用的锐化滤波器, 核心矩阵可以是这样的:

鉴于滤波是图像处理中常见的操作,OpenCV专门为此定义了一个函数, 即cv::filter2D。 要使用这个函数, 只需要定义一个内核( 以矩阵的形式) , 调用函数并传入图像和内核, 即可返回滤波后的图像。 因此, 使用这个函数可以很容易地重新定义锐化函数:

#include <opencv2/core/core.hpp>#include <opencv2/highgui/highgui.hpp>void sharpen2D(const cv::Mat &image, cv::Mat &result) {	// 构造内核( 所有入口都初始化为0)	cv::Mat kernel(3, 3, CV_32F, cv::Scalar(0));	// 对内核赋值	kernel.at<float>(1, 1) = 5.0;	kernel.at<float>(0, 1) = -1.0;	kernel.at<float>(2, 1) = -1.0;	kernel.at<float>(1, 0) = -1.0;	kernel.at<float>(1, 2) = -1.0;	// 对图像滤波	cv::filter2D(image, result, image.depth(), kernel);}int main(){	// 读取图像	cv::Mat image = cv::imread("boldt.jpg");	cv::Mat result;	// 处理图像	sharpen2D(image, result);	// 显示图像	cv::namedWindow("Image");	cv::imshow("Image", result);	cv::waitKey(0);	return 0;}但是这段代码报错:“filter2D”: 不是“cv”的成员。不知为何。

6.实现简单的图像运算

图像就是普通的矩阵,可以进行加、减、乘、除运算,我们使用算法运算符,将第二个图像与输入图像进行组合。下面就是第二个图像:这里我们把两个图像相加,用于创建特效图或覆盖图像中的信息。 我们可以使用cv::add函数

来实现相加功能。现在我们想得到加权和,因此使用更精确的cv::addWeighted函数:

cv::addWeighted(image1,0.7,image2,0.9,0.,result);操作的结果是一个新图像,如下图所示:


发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表