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

最近在系统地学习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);

操作的结果是一个新图像,如下图所示:







评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值