Opecv学习笔记(2)编写高效的图像扫描循环

准备工作

为了说明图像扫描的过程,我们来做一个简单的任务:减少图像中颜色的数量。
  彩色图像由三通道像素组成,每个通道表示红、绿、蓝三原色中一种颜色的亮度值,每个数值都是 8 位无符号字符类型,因此颜色总数为 256×256×256,即超过 1600 万种颜色。因此,为了降低分析的复杂性,有时需要减少图像中颜色的数量。一种实现方法是把 RGB 空间细分到大小相等的方块中。例如,如果把每种颜色数量减少到 1/8,那么颜色总数就变为 32×32×32。将旧图像中的每个颜色值划分到一个方块,该方块的中间值就是新的颜色值;新图像使用新的颜色值,颜色数就减少了。
  因此,基本的减色算法很简单。假设 N 是减色因子,将图像中每个像素的值除以 N (这里假定使用整数除法,不保留余数)。然后将结果乘以 N ,得到 N 的倍数,并且刚好不超过原始像素值。加上 N / 2,就得到相邻的 N 倍数之间的中间值。对所有 8位通道值重复这个过程,就会得到(256 / N ) × (256 / N ) × (256 / N )种可能的颜色值。(例如0-99的数,假定N是10,则最后的可能值为5,15,25,35,45,55,65,75,85,95,一下子只有十个数了)
初始图片::boldt.jpg
在这里插入图片描述

如何实现

用户提供一幅图像和每个颜色通道的减色因子。这里的处理过程是就地进行的,也就是说,函数直接修改了输入图像的像素值。也可以改成使用输入和输出参数的函数。
  处理过程很简单,只要创建一个二重循环遍历所有像素值,代码如下所示:

#include <iostream>
#include <opencv2/opencv.hpp>
#include <opencv2/highgui.hpp>

using namespace cv;
using namespace std;
//整数运算
void colorReduce1(Mat image, int div = 64) {
	int n1 = image.rows; // 行数
	// 每行的元素数量
	int nc = image.cols * image.channels();
	for (int j = 0; j < n1; j++) {
		// 取得行j的地址
		uchar* data = image.ptr<uchar>(j);  //ptr是一个模板方法,可以直接访问图像中一行的起始地址

		for (int i = 0; i < nc; i++) {
			// 处理每个像素-----------------------
			 data[i] = data[i] / div * div + div / 2;
			// 像素处理结束-----------------------
		}   // 一行结束
	}
}
int main(int argc, char* argv[]) {
	//读取图像
	Mat image = imread("boldt.jpg", 1);
	//处理图像
	colorReduce1(image, 64);
	//显示结果
	cv::namedWindow("Image Result");
	imshow("Image Result", image);
	waitKey(0);
	return 0;
}

执行后得到下面的图像。
在这里插入图片描述

实现原理

在彩色图像中,图像数据缓冲区的前 3 字节表示左上角像素的三个通道的值,接下来的 3字节表示第 1 行的第 2 个像素,以此类推(注意 OpenCV 默认的通道次序为 BGR)。一个宽 W高 H 的图像所需的内存块大小为 W×H×3 uchars 。不过出于性能上的考虑,我们会用几个额外的像素来填补行的长度。这是因为,如果行数是某个数字(例如 8)的整数倍,图像处理的性能可能会提高,因此最好根据内存配置情况将数据对齐。当然,这些额外的像素既不会显示也不被保存,它们的额外数据会被忽略。OpenCV 把经过填充的行的长度指定为有效宽度。如果图像没有用额外的像素填充,那么有效宽度就等于实际的图像宽度。用 cols和 rows 属性可得到图像的宽度和高度。与之类似,用 step 数据属性可得到单位是字节的有效宽度。即使图像的类型不是 uchar , step 仍然能提供行的字节数。我们可以通过 elemSize方法(例如一个三通道短整型的矩阵 CV_16SC3 , elemSize 会返回 6 )获得像素的大小,通过 nchannels 方法(灰度图像为 1 ,彩色图像为 3 )获得图像中通道的数量,最后用 total方法返回矩阵中的像素(即矩阵的条目)总数。
  用下面的代码可获得每一行中像素值的个数:

int nc= image.cols * image.channels();

为了简化指针运算的计算过程, cv::Mat 类提供了一个方法,可以直接访问图像中一行的起始地址。这就是 ptr 方法,它是一个模板方法,返回第 j 行的地址:

uchar* data= image.ptr<uchar>(j);

请注意,我们也可以在处理语句中采用另一种等价的做法,即利用指针运算从一列移到下一列。因此可以使用下面的代码:

*data++= *data/div*div + div2;

其他减色算法

使用取模运算符

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

添加函数void colorReduce2

//使用取模运算符
void colorReduce2(Mat image, int div = 64) {
	int n1 = image.rows;
	// 每行的元素数量
	int nc = image.cols * image.channels();
	for (int j = 0; j < n1; j++) {
		// 取得行j的地址
		uchar* data = image.ptr<uchar>(j);  //ptr是一个模板方法,可以直接访问图像中一行的起始地址

		for (int i = 0; i < nc; i++) {
			// 处理每个像素-----------------------
			data[i] = data[i] - data[i] % div + div / 2;
			// 像素处理结束-----------------------
		}   // 一行结束
	}
}

使用位运算符

另外还可以使用位运算符。如果把减色因子限定为 2 的指数,即 div=pow(2,n) ,那么把像素值的前 n 位掩码后就能得到最接近的 div 的倍数。可以用简单的位移操作获得掩码,代码如下所示:

//使用位运算符
void colorReduce3(Mat image, int div = 64) {
	int n1 = image.rows; // 行数
	// 每行的元素数量
	int nc = image.cols * image.channels();

	// div必须是2的幂
	int n = static_cast<int>(log(static_cast<double>(div)) / log(2.0) + 0.5);
	// 用来截取像素值的掩码
	uchar mask = 0xFF << n;  // 如果div=16,那么mask=0xF0
	uchar div2 = div >> 1; // div2 = div / 2

	for (int j = 0; j < n1; j++) {
		// 取得行j的地址
		uchar* data = image.ptr<uchar>(j);  //ptr是一个模板方法,可以直接访问图像中一行的起始地址
		
		for (int i = 0; i < nc; i++) {
			// 处理每个像素-----------------------
			*data &= mask;            // 掩码
			*data++ += div2;      // 加上div/2
			// 像素处理结束-----------------------
		}   // 一行结束
	}
}

用迭代器扫描图像

:上面都是用指针扫描图像,在面向对象编程时,我们通常用迭代器对数据集合进行循环遍历。迭代器是一种类,专门用于遍历集合的每个元素,并能隐藏遍历过程的具体细节。信息隐藏原则的应用,使扫描集合的过程变得更加容易和安全。并且不管被用于哪种类型的集合,它都能提供类似的形式。标准模板库(Standard Template Library,STL)对每个集合类都定义了对应的迭代器类,OpenCV 也提供了cv::Mat 的迭代器类,并且与 C++ STL中的标准迭代器兼容。
  要得到 cv::Mat 实例的迭代器,首先要创建一个 cv::MatIterator_ 对象。跟 cv::Mat_类似,这个下划线表示它是一个模板子类。因为图像迭代器是用来访问图像元素的,所以必须在编译时就明确返回值的类型。可以这样定义彩色图像的迭代器:

cv::MatIterator_<cv::Vec3b> it;

也可以使用在 Mat_ 模板类内部定义的 iterator 类型:

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

然后就可以使用常规的迭代器方法 begin 和 end 对像素进行循环遍历了。不同之处在于它们仍然是模板方法。

// 用迭代器扫描图像
void colorReduce4(Mat image, int div = 64) {
	// div必须是2的幂
	int n = static_cast<int>(log(static_cast<double>(div)) / log(2.0) + 0.5);
	// 用来截取像素值的掩码
	uchar mask = 0xFF << n;  // 如果div=16,那么mask=0xF0
	uchar div2 = div >> 1; // div2 = div / 2

	// 迭代器
	Mat_<Vec3b>::iterator it = image.begin<Vec3b>();
	Mat_<Vec3b>::iterator itend = image.end<Vec3b>();

	//扫描全部像素
	for (; it != itend; ++it) {
		(*it)[0] &= mask;
		(*it)[0] += div2;
		(*it)[1] &= mask;
		(*it)[1] += div2;
		(*it)[2] &= mask;
		(*it)[2] += div2;
		//这里处理的是一个彩色图像,因此迭代器返回cv::<Vec3b>实例。可以用取值运算符[]访问每个颜色通道的元素,也可以使用cv::<Vec3b>的重载运算符,
	}
}

使用at方法访问像素

//使用at方法访问像素的函数
void colorReduce5(Mat image, int div = 64) {
	int n1 = image.rows; // 行数
	int nc = image.cols; //列数
	int div2 = div / 2;
	for (int j = 0; j < n1; j++) {
		for (int i = 0; i < nc; i++) {
			image.at<Vec3b>(j, i)[0] = image.at<Vec3b>(j, i)[0] / div * div + div2;
			image.at<Vec3b>(j, i)[1] = image.at<Vec3b>(j, i)[1] / div * div + div2;
			image.at<Vec3b>(j, i)[2] = image.at<Vec3b>(j, i)[2] / div * div + div2;
		}
	}
}

效率对比

在编写图像处理函数时,你需要充分考虑运行效率。在设计函数时,你要经常检查代码的运行效率,找出处理过程中可能使程序变慢的瓶颈。
  但是有一点非常重要,除非确实必要,不要以牺牲代码的清晰度来优化性能。简洁的代码总是更容易调试和维护。只有对程序效率至关重要的代码段,才需要进行重度优化。

如何实现对比

OpenCV 有一个非常实用的函数可以用来测算函数或代码段的运行时间,它就是 cv::getTickCount() ,该函数会返回从最近一次计算机开机到当前的时钟周期数。在代码开始和结束时记录这个时钟周期数,就可以计算代码的运行时间。若想得到以秒为单位的代码运行时间,可使用另一个方法 cv::getTickFrequency() ,它返回每秒的时钟周期数,这里假定 CPU的频率是固定的(对于较新的 CPU,频率并不一定是固定的)。为了获得某个函数(或代码段)的运行时间,通常需使用这样的程序模板:

const int64 start = cv::getTickCount();
colorReduce(image); // 调用函数
// 经过的时间(单位:秒)
double duration = (cv::getTickCount()-start)/cv::getTickFrequency();

main函数修改后如下

int main(int argc, char* argv[]) {
	// 读取图像
	Mat image = imread("boldt.jpg", 1);
	// 处理图像
	const int64 start = cv::getTickCount();
	colorReduce1(image, 64);
	// 经过的时间
	double duration = (cv::getTickCount() - start) / cv::getTickFrequency();
	cout << duration << endl;
	//显示结果
	cv::namedWindow("Image Result");
	imshow("Image Result", image);
	waitKey(0);
	return 0;
}

实现原理

本问的 colorReduce 函数有几种实现方式,此处将列出每种方式的运行时间,实际的数据跟你使用的计算机有关(我的计算机配置为64位的Intel Core i5-6300HQ@2.3GHz 四核)。观察运行时间的相对差距更有意义。此外,测试结果也跟生成可执行文件的具体编译器有关。我们采用 320×240 的图像,测试减色操作的平均运行时间。编译器为Window7下visual Studio 2017。分别在主函数中将函数colorReduce1-5测试一遍,结果如下

整数运算模运算符位运算符迭代器at方法访问像素
0.0012634 0.00137406 0.000934553 0.0300995 0.0566162
有趣的是,使用了位运算符的方法要比其他方法快得多,而整数运算和取模运算符运行时间非常接近。因此,要在图像循环中计算出结果,花些时间找出效率最高的方法十分重要,其净影响会非常明显。用迭代器的减色函数,它的运行时间更长,使用迭代器的主要目的是简化图像扫描过程,降低出错的可能性。at方运行速度较慢,应该在需要随机访问像素的时候使用,绝不要在扫描图像时使用。对于可以预先计算的数值,要避免在循环中做重复计算,继而浪费时间。例如,这样写减色函数是很不明智的:
for (int i=0; i<image.cols * image.channels(); i++) {
*data &= mask;
*data++ += div/2;
}

上面的代码需要反复计算每行的像素数量和 div/2 的结果。改进后的代码为:

int nc= image.cols * image.channels();
uchar div2= div>>1;
for (int i=0; i<nc; i++) {
*(data+i) &= mask;
*(data+i) += div2;
}

一般来说,需要重复计算的代码会比优化后的代码慢 10 倍。但是要注意,有些编译器能够对此类循环进行优化,仍会生成高效的代码。

使用图像运算符

OpenCV的大多数运算函数都有对应的重载运算符,其中包括位运算符 & 、 | 、 ^ 、 ~ 和函数 min 、 max 、 abs 。比较运算符 < 、 <= 、 == 、 != 、 > 和 >= 也已被重载,它们返回一个 8位的二值图像。此外还有矩
阵乘法 m1*m2 (其中 m1 和 m2 都是 cv::Mat 实例)、矩阵求逆 m1.inv() 、变位 m1.t() 、行列式 m1.determinant() 、求范数 v1.norm() 、叉乘 v1.cross(v2) 、点乘 v1.dot(v2) ,等等。在理解这点后,你就会使用相应的组合赋值符了(例如 += 运算符)。
  针对输入图像的运算符简单地重写这个函数,代码如下

// 使用针对输入图像的运算符
void colorReduce8(cv::Mat image, int div = 64) {

	int n = static_cast<int>(log(static_cast<double>(div)) / log(2.0) + 0.5);
	uchar mask = 0xFF << n;

	// 由于被操作的是彩色图像,因此使用了 cv::Scalar。使用图像运算符可以简化代码、提高开发效率,因此在大多数场合都应考虑采用
	image = (image&cv::Scalar(mask, mask, mask)) + cv::Scalar(div / 2, div / 2, div / 2);
}

时间为:0.00184083

连续图像的高效扫描

前面解释过,为了提高性能,可以在图像的每行末尾用额外的像素进行填充。有趣的是,在去掉填充后,图像仍可被看作一个包含 W×H 像素的长一维数组。用 cv::Mat 的 isContinuous方法可轻松判断图像有没有被填充。如果图像中没有填充像素,它就返回 true 。我们还能这样测试矩阵的连续性:

// 检查行的长度(字节数)与“列的个数×单个像素”的字节数是否相等
image.step == image.cols*image.elemSize();

为确保完整性,测试时还需要检查矩阵是否只有一行;如果是,这个矩阵就是连续的。但是不管哪种情况,都可以用 isContinuous 方法检查矩阵的连续性。在一些特殊的处理算法中,你可以充分利用图像的连续性,在单个(更长)循环中处理图像。处理函数改为

void colorReduce6(Mat image, int div = 64) {
	int n1 = image.rows; // 行数
	// 每行的元素数量
	int nc = image.cols * image.channels();

	if (image.isContinuous()) {
		//没有填充的像素
		nc = nc * n1;
		n1 = 1;  //它现在成了一个一维数组
	}

	// div必须是2的幂
	int n = static_cast<int>(log(static_cast<double>(div)) / log(2.0) + 0.5);
	// 用来截取像素值的掩码
	uchar mask = 0xFF << n;  // 如果div=16,那么mask=0xF0
	uchar div2 = div >> 1; // div2 = div / 2

	//对于连续图像,这个循环只执行一次
	for (int j = 0; j < n1; j++) {
		// 取得行j的地址
		uchar* data = image.ptr<uchar>(j);  //ptr是一个模板方法,可以直接访问图像中一行的起始地址

		for (int i = 0; i < nc; i++) {
			// 处理每个像素-----------------------
			*data &= mask;            // 掩码
			*data++ += div2;      // 加上div/2
			// 像素处理结束-----------------------
		}   // 一行结束
	}
}

时间为:0.000942552,和位运算符差不多,暂时看不出来区别
  如果连续性测试结果表明图像中没有填充像素,我们就把宽度设为 1,高度设为 W × H ,从而:去除外层的循环。注意,这里还需要用 reshape 方法。本例中需要这样写:

void colorReduce7(Mat image, int div = 64) {
	if (image.isContinuous()) {
		//没有填充的像素
		image.reshape(1,  // 新的通道数
			         1);  // 新的行数
		//如果使用reshape方法修改矩阵的维数,就不需要复制内存或重新分配内存了。
		//第一个参数是新的通道数,第二个参数是新的行数。列数会进行相应的修改
	}
	int nc = image.cols * image.channels(); //列数

	// div必须是2的幂
	int n = static_cast<int>(log(static_cast<double>(div)) / log(2.0) + 0.5);
	// 用来截取像素值的掩码
	uchar mask = 0xFF << n;  // 如果div=16,那么mask=0xF0
	uchar div2 = div >> 1; // div2 = div / 2

	uchar* data = image.ptr<uchar>(0);

	for (int i = 0; i < nc; i++) {
	// 处理每个像素-----------------------
		*data &= mask;            // 掩码
		*data++ += div2;      // 加上div/2
		// 像素处理结束-----------------------
	}   // 一行结束
}

注意,如果是用 reshape 方法修改矩阵的维数,就不需要复制内存或重新分配内存了。第一个参数是新的通道数,第二个参数是新的行数。列数会进行相应的修改。
  测试结果为:1.55537e-05,即为:0.0000155537
  即使处理的元素总数相同,使用较短的循环和多条语句通常也要比使用较长的循环和单条语句的运行效率高。与之类似,如果你要对一个像素执行 N 个不同的计算过程,那就在单个循环中执行全部计算,而不是写 N 个连续的循环,每个循环执行一个计算。
  针对连续图像生成一个循环,而不是对行和列运行常规的二重循环,使运行速度提高了很多。通常情况下,这种策略是非常好的,因为它会使速度明显提高。
  还有一个提高算法运行效率的方法是采用多线程,尤其是在使用多核处理器时。OpenMP、Intel 线程构建模块(Threading Building Block,TBB)和 Posix 是比较流行的并发编程 API,用于创建和管理线程。而且现在 C++11 本身就支持多线程。

在彩色图像上应用查找表

实际上,更高效的做法是预先计算好所有的减色值,然后用查找表修改每个像素,这很容易实现。下面是新的减
色函数:

void colorReduce9(cv::Mat &image, int div = 64) {
	// 创建一维查找表
	cv::Mat lookup(1, 256, CV_8U);
	// 定义减色查找表的值
	for (int i = 0; i < 256; i++)
		lookup.at<uchar>(i) = i / div * div + div / 2;
	// 对每个通道应用查找表
	cv::LUT(image, lookup, image);
}

测试结果为::0.00087371
  这种减色方案之所以能起作用,是因为在多通道图像上应用一维查找表时,同一个查找表会独立地应用在所有通道上。如果查找表超过一个维度,那么它和所用图像的通道数必须相同。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值