在开发图像处理项目时,会遇到访问图像的每个像素的情况。本节主要内容是OpenCV如何访问像素,怎样提高效率,如何评价算法的性能。
目标
- 遍历图像的每个像素
- 内存中矩阵数据的存储
- 测量算法性能
- lookup table是什么
- 原文网址How to scan images, lookup tables and time measurement with OpenCV
- 本地目录D:opencvsourcesdoctutorialscorehow_to_scan_images
- 代码目录D:opencvsourcessamplescpptutorial_codecorehow_to_scan_images
- GitHub 有相应文档和OpenCV源代码
- 版本OpenCV4.1.2(版本兼容性见英文原文,部分文档适用于OpenCV2.0和3.0)
- 环境Windows、C++、VS2019 Community
测试案例 Our test case
一个颜色量化的例子。对于RGB图像的一个像素,有三个通道,每个通道的数据是unsigned char,那么组合起来就是1600万中颜色。在某些应用场景中这么多颜色会严重影响算法性能,并且去掉大多数颜色不会对最终结果造成影响。这种方法称为颜色空间退化,也就是把某些颜色用同一种颜色代替。比如数值0~9用0代替,10~19用10代替。对于unsigned char(0~255),除以int型,结果类型不变,会向下圆整,用下面公式可实现。
对于大型图像,每个像素进行一次除法和乘法,执行起来效率很低。如果对于给定的像素值,不再计算,而是查找对应的量化值,那么效率就会提升。这里就用到了lookup table。下面是创建lookup table的方法。
int divideWith = 10; //量化分辨率
uchar table[256];
for (int i = 0; i < 256; ++i)
table[i] = (uchar)(divideWith * (i/divideWith));
那么如何测量执行时间,OpenCV提供了下面的方法。
double t = (double)getTickCount();
// 待测试的代码
t = ((double)getTickCount() - t)/getTickFrequency();
cout << "运行时间(秒): " << t << endl;
图像矩阵在内存中如何存储 How is the image matrix stored in memory?
对于灰度图像:
对于RGB图像,每个子列与通道数(3个)一致,OpenCV的顺序是BGR:
通常内存足够大,可以保证一行接一行的连续存储。这样有助于加速访问每个像素。可以用isContinuous函数来判断矩阵是否是存储在连续的一块内存区域。下面是一个例子。
高效的方法 The efficient way
说到访问的访问内存的效率,C指针是第一。所有推荐下面的方法。
Mat& ScanImageAndReduceC(Mat& I, const uchar* const table)
{
// 只接受uchar类型的数据 accept only char type matrices
CV_Assert(I.depth() == CV_8U);//
int channels = I.channels();//通道数
int nRows = I.rows;//行数
int nCols = I.cols * channels;//列数与通道数乘积
if (I.isContinuous())//若图像是连续的,整个矩阵看做一行数据,行数为1,列长度为原行数乘以原列数
{
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]];//用lookup table赋值
}
}
return I;
}
另外一种方法,可实现同样的结果。但是代码阅读性不强。
uchar* p = I.data;//如果图像导入Mat,那么I.data就是图像数据的首地址,指针指向改地址
for( unsigned int i =0; i < ncol*nrows; ++i)
*p++ = table[*p];
迭代器(安全)方法The iterator (safe) method
为了防止访问越界或无效,采用迭代器可以安全访问像素,只需要指定begin和end。
Mat& ScanImageAndReduceIterator(Mat& I, const uchar* const table)
{
// accept only char type matrices
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:
{
//对于三通道图像,每个像素点可以视为一个三个元素的向量,OpenCV利用Vec3b表示
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;
}
引用返回的动态地址计算(翻译的不准确)On-the-fly address calculation with reference returning
这种方法不推荐用于遍历图像。是一种比较直观灵活的方式,可以通过设置行列坐标方式任意访问像素。
Mat& ScanImageAndReduceRandomAccess(Mat& I, const uchar* const table)
{
// accept only char type matrices
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_与Mat类似,但是用来Mat_可以简化代码,一般用不到,用Mat需要用到Mat::at函数
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;
}
核心函数 The Core Function
这是核心模块提供的用lookup table修改图像的一种方式。因为在图像处理中,经常会用给定值来修改图像的像素值。所以OpenCV提供了实现的函数。用核心模块的cv::LUT() 函数。
首先创建一个Mat类型的lookup table:
Mat lookUpTable(1, 256, CV_8U);//创建并且初始化lookup table
uchar* p = lookUpTable.ptr();
for( int i = 0; i < 256; ++i)
p[i] = table[i];
然后调用函数 (I、J是输入、输出图像):
LUT(I, lookUpTable, J);
性能差异 Performance Difference
为了测试以上方法的性能差异,用了大小为 (2560 X 1600) 的图像。这里是彩色图像,为了比较结果,测试了上百次求平均值。
可以得出结论,尽可能使用OpenCV提供的函数,而不是自己编写。最快最简洁的是LUT函数,是因为OpenCV库使用了Intel TBB实现多线程。然而如果确实需要自己实现,推荐使用指针(还是离不开C)。迭代器安全但是速度慢,debug模式下的on-the-fly reference access是最慢的。在release模式下差不多,但是安全性不如迭代器。(个人看来,如果不用LUT函数,性能没有太大区别,哪个顺手用哪个,除非开销紧张)
YouTube 有官方的演示效果video posted 。