图像处理的基础是对图像每一个像素点的遍历,即图像扫描。在本节中,将介绍几种不同的图像遍历方式,为了对比不同方法的效率,我们不是单纯的遍历,而是对图像做更多的处理。在此,我们测试的是一种简单的颜色缩减方法。为了比较不同遍历算法的运行时间,你还将看到 OpenCV 中计时函数的用法。
1. 概述
图像处理的基础是对图像每一个像素点的遍历,即图像扫描。在本节中,将介绍几种不同的图像遍历方式,为了对比不同方法的效率,我们不是单纯的遍历,而是对图像做更多的处理。在此,我们测试的是一种简单的颜色缩减方法。为了比较不同遍历算法的运行时间,你还将看到 OpenCV 中计时函数的用法。
2. 图像存储方式
在进行下面的论述之前,先对图像矩阵在内存中的存储方式简单介绍。对于单通道灰度图像:
而对于多通道图像来说,矩阵中的列会包含多个子列,子列数与通道数相等,如 BGR 颜色模型的矩阵为:
3. 颜色缩减
何谓颜色缩减?对于元素类型为 uchar 的单通道图像矩阵,每个像素点有 256 个灰度值,但是对于三通道图像,每个像素点的颜色种类达 16777216 种(256 的三次方)。如此多的颜色可能会对算法性能造成严重影响,我们往往只需要颜色的一部分,也能满足要求,因此引入了颜色缩减。
如上图所示,左侧为颜色缩减前,右侧为颜色缩减后,数字表示灰度值。可以看出,灰度值 92 到 114 映射为 92;115 到 137 映射为 115,以此类推,左侧 164 种颜色缩减为右侧的 7 种颜色。实现方法也很简单:
//color1输入,color2输出
color2 = (color1 / 23) * 23;
但是,如果直接对图像的每个像素进行上述除法和乘法,这样效率是很低的。一个较好的办法是事先生成一张颜色缩减的查找表,表中缩减前后的值都明确给定,这样遍历图像时,利用查找表直接对相应像素点进行赋值即可。其优势在于只需读取、无需计算。以下代码生成颜色查找表 color_table:
// 生成颜色查找表
vector<int> color_table;
int width = 20;
for (int i = 0; i < 256; i++)
{
color_table.push_back(i / width * width);
}
4. 图像遍历
有了颜色查找表后,我们便可以对图像进行遍历并对像素点进行颜色缩减了。我们采用了几种不同的图像遍历方法,为了对比它们的效率,采用 OpenCV 提供的两个简单的计时器函数 getTickCount() 和 getTickFrequency(), 它们分别返回 CPU 走过的时钟周期数和 CPU 一秒的时钟周期数。因此,可以这样来计时(单位:秒):
double time_begin = (double)getTickCount();
// do something
double time_end = (double)getTickCount();
double time = (time_end - time_begin) / getTickFrequency();
4.1 利用指针遍历
Mat& ScanImageAndReduceC(Mat& I, vector<int> color_table)
{
//只接收字符型矩阵
CV_Assert(I.depth() != sizeof(uchar));
int channels = I.channels(); //获取图像通道数
int row = I.rows * channels;
int col = I.cols;
uchar* p;
if (I.isContinuous()) //判断像素是否连续存储
{
col *= row;
row = 1;
}
for (int i = 0; i < row; i++)
{
p = I.ptr<uchar>(i);
for (int j = 0; j < col; j++)
{
p[j] = color_table[p[j]];
}
}
return I;
}
颜色缩减结果(根据查找表的width设置缩减的程度,在此 width = 20)
算法执行时间为 0.0134007 秒。
4.2 利用迭代器遍历
Mat& ScanImageAndReduceIterator(Mat& I, vector<int> color_table)
{
//只接收字符型矩阵
CV_Assert(I.depth() != sizeof(uchar));
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 = color_table[*it];
break;
}
case 3:
{
MatIterator_<Vec3b> it, end;
for (it = I.begin<Vec3b>(), end = I.end<Vec3b>(); it != end; it++)
{
(*it)[0] = color_table[(*it)[0]];
(*it)[1] = color_table[(*it)[1]];
(*it)[2] = color_table[(*it)[2]];
}
break;
}
}
return I;
}
算法执行时间 0.0693951 秒。用迭代器遍历速度稍慢,但是更加安全。上述代码中,我们首先对图像的通道数进行判断,通道数为 1 时,直接对灰度值赋值;对于三通道彩色图像,每个像素可以看做一个包含三个 uchar 元素的 vector, 在 OpenCV 中用 Vec3b 命名。对于彩色图像,如果我们仅仅使用 uchar 而不是 Vec3b 迭代的话就只能获得蓝色通道的值(BGR模型中的第一个通道)。
4.3 通过相关返回值的 On-the-fly 地址遍历
Mat& ScanImageAndReduceRadomAccess(Mat& I, vector<int> color_table)
{
//只接收字符型矩阵
CV_Assert(I.depth() != sizeof(uchar));
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) = color_table[I.at<uchar>(i, j)];
}
}
break;
}
case 3:
{
for (int i = 0; i < I.rows; i++)
{
for (int j = 0; j < I.cols; j++)
{
I.at<Vec3b>(i, j)[0] = color_table[I.at<Vec3b>(i, j)[0]];
I.at<Vec3b>(i, j)[1] = color_table[I.at<Vec3b>(i, j)[1]];
I.at<Vec3b>(i, j)[2] = color_table[I.at<Vec3b>(i, j)[2]];
}
}
break;
}
}
return I;
}
通过 at() 函数获取并更改图像中的元素。事实上,这种方法并不推荐呗用来进行图像扫描。
4.4 核心函数 LUT (The Core Function)
核心函数 LUT 是最被推荐用于实现批量图像元素查找和更改的方法,它并不需要你自己去扫描图像。我们先建立一个查找表:
Mat table(1, 256, CV_8U);
uchar* p = table.data;
for(int i = 0; i < 256; i++)
p[i] = color_table[i];
然后调用函数:
//image是输入,image_reduce是输出
LUT(image, table, image_reduce);
4.5 结论
尽量使用 OpenCV 内置函数,调用 LUT 函数可以获得最快的速度,这是因为 OpenCV 库可以通过英特尔线程架构启用多线程,当然,迭代器也是一个不错的选择,优点是安全,缺点是速度较慢,on-the-fly方法不推荐使用。