2.2 OpenCV中遍历图像,查找表和时间测量
查找表(look up table),以少量空间节约大量时间
原理百度 查找表
时间测量:
使用OpenCV提供的两个函数getTickCount()和getTickFrequency()。
double t = (double)getTickCount();
// do something ...
t = ((double)getTickCount() - t)/getTickFrequency();
cout << t << endl;
图像矩阵在内存中的表示:
RGB图像在内存中存放时颜色通道的顺序是 BGR。
许多情况下图像矩阵在内存中是一行一行连续存储的,这样有助于加快图像的遍历过程。可以使用 isContinuous()函数检测图像矩阵是不是连续存储的(为了效率)。如果是连续存储的,实际上图像矩阵数据就是存储为内存中的一行。
高效的方法(The Efficient Way):
为了性能不得不使用C风格的运算符 [ ] 进行数据访问,推荐的内存访问做法如下:
Mat& ScanImageAndReduceC(Mat& I, const uchar* const table)
{
// accept only char type matrices
CV_Assert(I.depth() != sizeof(uchar));
int channels = I.channels();
int nRows = I.rows;
int nCols = I.cols * channels;
if (I.isContinuous())
{
nCols *= nRows;
nRows = 1;
}
int i,j;
uchar* p;
for( i = 0; i < nRows; ++i)<span style="white-space:pre"> </span>// 核心思想
{
p = I.ptr<uchar>(i);
for ( j = 0; j < nCols; ++j)
{
p[j] = table[p[j]];
}
}
return I;
}
如果图像矩阵是连续存储在内存中的,只要知道数据起始的指针和数据大小,就能高效率地访问了。
还有一种方法,Mat对象的data数据成员返回指向图像矩阵首行首列的指针。判断指针是否为空可以知道对象是否有效,从而检查图像是否成功读入Mat对象中。如果图像是连续存储,可用如下方式访问图像数据矩阵(灰度图,单通道):
uchar* p = I.data;
for( unsigned int i =0; i < ncol*nrows; ++i)
*p++ = table[*p];
但是实现高效率的同时这种方法也是不安全的,需要程序员保证得到了正确的数据大小(小心越界),以及跳过了正确的存储空隙(不连续的情况下)。
迭代器(安全的)方法(Iterator):
使用迭代器,程序员就不用考虑上面的问题了。只需要获得图像矩阵的begin和end迭代器,传统方式一直 ++ 即可。迭代器会自动跳到下一行并跳过内存中不连续的地方。使用解引用运算符 * 获得访问值。但是这种方法会损失效率。
Mat& ScanImageAndReduceIterator(Mat& I, const uchar* const table)
{
// accept only char type matrices
CV_Assert(I.depth() != sizeof(uchar));
const 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 = table[*it];
break;
}
case 3:
{
MatIterator_<Vec3b> it, end;
for( it = I.begin<Vec3b>(), end = I.end<Vec3b>(); it != end; ++it)
{
(*it)[0] = table[(*it)[0]];
(*it)[1] = table[(*it)[1]];
(*it)[2] = table[(*it)[2]];
}
}
}
return I;
}
上述代码中,彩色图像对应的三通道情况下,使用vector存储每个像素三个通道的值,OpenCV中专门有一个Vec3b数据类型,相当于 vector<unsigned char>。
随机引用访问(On-the-fly address calculation with reference returning):
随机访问方法不推荐用于全图像的遍历。它只是用来随机访问图像中任意一个像素,可以读取或修改该像素,这里用到 at() 函数。无论用何用方法访问图像矩阵,都需要指定数据类型。因此,需要给函数提供访问数据类型,数据所在的行号和列号,代码如下:
Mat& ScanImageAndReduceRandomAccess(Mat& I, const uchar* const table)
{
// accept only char type matrices
CV_Assert(I.depth() != sizeof(uchar));
const 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) = table[I.at<uchar>(i,j)];
break;
}
case 3:
{
Mat_<Vec3b> _I = I;
for( int i = 0; i < I.rows; ++i)
for( int j = 0; j < I.cols; ++j )
{
_I(i,j)[0] = table[_I(i,j)[0]];
_I(i,j)[1] = table[_I(i,j)[1]];
_I(i,j)[2] = table[_I(i,j)[2]];
}
I = _I;
break;
}
}
return I;
}
at() 函数输入数据类型和坐标,返回相应像素值的
引用。读操作是一个const函数,写操作是非const函数。
仅在debug模式下,函数会检查输入的坐标是否存在和有效,无效则通过标准错误输出流给出错误提示信息。相比于在release模式的高效率(第一种)方法,使用该函数的唯一的区别是,对图像中的每一个元素,你会得到一个行指针,以便于我们用 C风格的 [ ] 运算符获得列元素。
因为每次查找都要输入数据类型和坐标,用这种方法在一幅图像中多次执行查找操作是麻烦和费时的。为了解决这个问题,OpenCV提供了一个
Mat_ 的数据类型。它和 Mat 数据类型唯一的不同是需要额外指定 Mat_ 对象存储的数据矩阵的数据类型。它可以使用 () 运算符来快速访问元素,也可以容易地与 Mat 对象相互转换类型。上述代码中对彩色图像的处理(case 3)就是使用的 Mat_ 对象。
使用核心函数(The Core Function)
图像处理中经常需要将一幅图像中的所有值都替换成其他值,通过查找表可以高效实现这种替换。OpenCV提供了一个函数,使程序员不用手动地遍历图像,查表,写入替换值,而是直接 利用 LUT() 实现 。实现代码如下:
Mat lookUpTable(1, 256, CV_8U);
uchar* p = lookUpTable.data;
for( int i = 0; i < 256; ++i)
p[i] = table[i];
LUT(I, lookUpTable, J);
首先,创建一个1行256列的Mat对象用来存储查找表,然后调用
LUT() 函数实现转换。函数的参数中I是输入图像,J是输出图像。函数进一步的具体用法可查看 build/doc 目录中的opencv2refman.pdf 。
性能比较
教程中对上述四种方法进行了性能比较,采用了大小为 2560×1600 的彩色图像进行遍历替换,每种方法运行了上百次取平均值,得到如下结果:
Efficient Way | 79.4717 ms |
Iterator | 83.7201 ms |
On-The-Fly RA | 93.7878 ms |
LUT function | 32.5759 ms |
总结:
OpenCV自身提供的函数
LUT()
速度最快,这主要是因为OpenCV的库是通过Intel Threaded Building Blocks实现了多线程。如果是简单的图像遍历,可以使用第一种方法。迭代器方法损失了速度获得了安全性。Debug模式下的随机引用访问是最耗时的,Release模式下的随机引用访问效率和迭代器方法相当,但是它牺牲了迭代器具有的安全性。