OpenCV学习笔记(1)CoreModel---如何快速遍历图像与建立查找表
总述
我们从以下几个问题作为引例
- 什么是查找表(look up table),为什么需要他们?
- OpenCV的矩阵值到底怎么存储的?
- 如何快速遍历到图像的每一个像素?
- 如何评价遍历算法的运行效率
查找表
让我们从最简单的颜色空间压缩开始。通过使用uchar 类型来存储数据,那么至多256个取值,三个颜色通道(RGB)就有近一千六百万种颜色组合,这么庞大的数据可能不利于压缩和传递,在这种情况下,我们希望能够将一些相近的颜色合并,来用较小的颜色种类达到近似相同的图像质量。
接下来有一个简单的算法,可以减少颜色空间,通过这种算法,可以将小数部分剔除,使得颜色相近的合并到一起(除数不一定是10,可以任意宽度)。
但我们注意到,乘法和除法的代价很高,我们不希望每一个像素都执行一遍上述的算法(因为现在的图像都很庞大),如果可以的话,应该避免使用,转而去运用简单的加法和减法或者最好是赋值运算。
这时候查找表的优势就体现出来了,查找表就是一个简单的数组,在对图像进行处理之前,把所有可能的输入像素值通过算法计算并存入相对应的空间中,在对图像进行处理时,只需要在对应位置取出对应的处理后数值即可,在这种情况下就只运用了赋值运算符。
int divideWith = 0;
stringstream s;
s << argv[2];
s >> divideWith;
if (!s || !divideWith)
{
cout << "输入的无效除数. " << endl;
return -1;
}
uchar table[256];
for (int i = 0; i < 256; ++i)
table[i] = (uchar)(divideWith * (i/divideWith));
图像存储方式
正如我们上一篇文章OpenCV学习笔记(1)CoreModel—Mat所阐述的,图像的容器是Mat ,如何存储也取决我们选取的颜色空间。就灰度图而言,它如下图所示存储
对于多通道的图像,列数包含着与通道数相同的子列。对于RGB颜色空间来说如下图所示。
注意到存储时,是以BGR的顺序,而非RGB。
遍历图像方式
读入图像时,最重要的一项工作就是检查图像是否连续(在内存中连续存储),我们可以通过cv::Mat::isContinuous() 来检查。
C风格指针调用(最有效率的方式)
Mat& ScanImageAndReduceC(Mat& I, const uchar* const table)
{
// 只接受char类型矩阵
CV_Assert(I.depth() == CV_8U);
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)
{
p = I.ptr<uchar>(i);
for ( j = 0; j < nCols; ++j)
{
p[j] = table[p[j]];
}
}
return I;
}
最后的赋值工作是和前面颜色空间压缩联系在一起的,遍历并修改了像素值。
当然你也可以利用数据成员data 方式得到数据的首地址,并且遍历。
uchar* p = I.data;
for( unsigned int i = 0; i < ncol*nrows; ++i)
*p++ = table[*p];
迭代器遍历
迭代器是被认为更安全的一种方式来遍历图像。它仅仅只需要知道从哪里开始和到哪里结束便可以完成整个遍历操作。
Mat& ScanImageAndReduceIterator(Mat& I, const uchar* const table)
{
// 只接受char类型矩阵
CV_Assert(I.depth() == CV_8U);
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;
}
值得注意的是,在通道为3(即彩色图像)的时候,使用了vec3b 迭代器而非uchar 迭代器,如果只是用uchar 迭代器,只能获取蓝色通道的像素值,请注意!
动态地址计算并返回引用
Mat& ScanImageAndReduceRandomAccess(Mat& I, const uchar* const table)
{
// 只接受char类型矩阵
CV_Assert(I.depth() == CV_8U);
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;
}
这种方式遍历图像,在debug模式下效率较低,并且每次调用该函数时,需要键入数据类型和at关键字较为麻烦,可以利用Mat_ 模板类指向该区域,直接利用括号进行索引。
通过内置函数遍历整个图像
Mat lookUpTable(1, 256, CV_8U);
uchar* p = lookUpTable.ptr();
for( int i = 0; i < 256; ++i)
p[i] = table[i];
LUT(I, lookUpTable, J);
不同算法的运行效率
为了让差别更加明显,选取了(2560×1600)大小的彩色图像
方法 | 时间 |
---|---|
C风格指针 | 79.4717 毫秒 |
迭代器 | 83.7201 毫秒 |
动态地址计算 | 93.7878 毫秒 |
LUT函数 | 32.5759 毫秒 |
如何测量时间呢?参考如下代码
double t = (double)getTickCount();
// do something ...
t = ((double)getTickCount() - t)/getTickFrequency();
cout << "时间过去了(s): " << t << endl;