文章目录
- 目标
- 测试用例
- 图像矩阵如何存储在内存之中?
- 遍历方式一:高效的遍历方式
- 遍历方式二:迭代器(安全)方法
- 遍历方式三:即时地址计算,带参数返回功能
- 遍历方式四:核心函数
- 性能差异
- 建议使用代码
目标
- 如何遍历图像的每一个像素?
- 如何存储OpenCV matrix 值?
- 如何衡量我们算法的性能?
- 什么是查询表,为什么使用它们?
测试用例
让我们考虑一种简单的色彩量化(颜色空间缩减)方法。通过使用unsigned char C和C++类型存储矩阵项(图片),像素通道可以有多达256个不同的值。对于一个三通道图像,这可能会形成更多的颜色(确切地说是1600万(2563))。工作于如此多的颜色种类,可能会严重影响我们的算法性能。然而,有时使用更少的颜色种类也能得到相同的最终结果。
在这种情况下,我们通常会缩小颜色空间。这意味着我们用一个新的输入值来分割颜色空间的当前值,以得到更少的颜色。例如,0到9之间的每个值接受新值0,10到19之间的每个值接受新值10,以此类推。
当你将一个uchar (unsigned char - 值在0和255之间)用一个int值分割时,结果也是char。这些值只能是char值。因此,任何小数都将四舍五入。利用这一事实,uchar域中的运算可以表示为:
一个简单的颜色空间缩减算法只需遍历图像矩阵的每个像素并应用这个公式即可。值得注意的是,我们做了除法和乘法运算。对于一个系统来说,这些操作是非常耗时的。如果可能的话,可以通过使用耗时更少的操作来避免它们,比如一些减法、加法,或者 在最好的情况下:使用简单的赋值。此外,请注意,对于上面的操作,我们只有有限数量的输入值。在uchar系统中,这是精确的256。
因此,对于较大的图像,明智的做法是在遍历之前计算所有可能的值,并在遍历期间使用查找表进行分配。查找表是简单数组(具有一个或多个维度),对于给定的输入值变量,这些数组保存最终的输出值。它的优点是我们不需要做计算,我们只需要查结果。
我们的测试用例程序(以及下面的代码示例)将执行以下操作:
- 读取作为命令行参数传递的图像(它可以是彩色图或灰度图),
- 并使用给定的命令行参数整数值应用减少量。
- 在OpenCV中,目前有三种主要的逐像素遍历图像的方法。为了让事情更有趣一点,我们将使用这些方法扫描图像,并打印出所需的时间。
您可以在这里下载完整的源代码,或者在核心部分的cpp教程代码中OpenCV的样例目录中查找它。它的基本用法是:
how_to_scan_images imageName.jpg intValueToReduce [G]
最后一个参数是可选的。如果给定,图像将以灰度格式加载,否则使用BGR颜色空间。第一件事是计算查找表。
int divideWith = 0; // convert our input string to number - C++ style
stringstream s;
s << argv[2];
s >> divideWith;
if (!s || !divideWith)
{
cout << "Invalid number entered for dividing. " << endl;
return -1;
}
uchar table[256];
for (int i = 0; i < 256; ++i)
table[i] = (uchar)(divideWith * (i/divideWith));
在这里,我们首先使用c++ stringstream类将第三个命令行参数从文本转换为整数格式。然后,我们使用一个简单的循环和上面的公式来计算查找表。这里没有OpenCV特定的东西。
另一个问题是我们如何度量时间? OpenCV提供了两个简单的函数来实现这个:cv::getTickCount()和cv::getTickFrequency()。第一个返回系统CPU在某个事件中的时钟数(比如自启动系统以来)。第二个函数返回CPU在一秒钟内发出的时钟的次数。因此,测量两个操作之间的时间就像:
double t = (double)getTickCount();
// do something ...
t = ((double)getTickCount() - t)/getTickFrequency();
cout << "Times passed in seconds: " << t << endl;
图像矩阵如何存储在内存之中?
图像矩阵的大小取决于所使用的颜色空间系统。更准确地说,它取决于所使用的通道数量。在灰度图像的情况下,我们有:
对于多通道图像,列包含的子列数与通道数相同。例如BGR色彩系统:
注意,通道的顺序是相反的:BGR而不是RGB。
另外,因为在许多情况下,内存足够大,可以以连续的方式存储行,所以行可以一个接一个地跟随,从而创建一个单独的长行。因为所有的东西都在一个地方一个接着一个,这可能有助于加快扫描过程。我们可以使用cv::Mat::isContinuous()函数来询问矩阵是否存在这种情况。继续到下一节找到一个示例。
遍历方式一:高效的遍历方式
在性能方面,经典的C风格操作符[](指针)访问是无可比拟的。因此,我们推荐的最有效的方法是:
Mat& ScanImageAndReduceC(Mat& I, const uchar* const table)
{
// 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())
{
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;
}
这里我们只需要获取一个指向每行开头的指针然后遍历它直到它结束。在矩阵以连续方式存储的特殊情况下,我们只需要请求指针一次,然后一直到最后。我们需要遍历彩色图像:我们有三个通道,所以我们需要在每一行中遍历三倍以上的项。
还有另一种方法。Mat对象的data数据成员返回指向第一行第一列的指针。如果该指针为空,则该对象中没有有效输入。检查它 是检查图像加载是否成功的最简单方法。如果存储是连续的,我们可以使用它来遍历整个数据指针。在一个灰度图像的情况下,这看起来像:
uchar* p = I.data;
for( unsigned int i = 0; i < ncol*nrows; ++i)
*p++ = table[*p];
你会得到同样的结果。但是,这种代码可读性差。如果你学的是高级语言,你将更难理解它的涵义。此外,在实践中,您将获得相同的性能结果(因为大多数现代编译器可能会自动为您优化代码。(实现这个小小的优化技巧))。
遍历方式二:迭代器(安全)方法
在有效的情况下,你有责任确保遍历正确数量的uchar字段并跳过行之间可能出现的间隙。iterator方法被认为是一种更安全的方法,因为它从用户那里接管了这些任务。您所需要做的就是请求并获取图像矩阵的开始和结束,然后从开始迭代器 增加,直到到达结束。要获得迭代器所指向的值,请使用 *operator(在它自增之前)。
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:
{
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;
}
对于彩色图像,每列有三个uchar项。这可以被认为是uchar项的一个短载体,已经在OpenCV中以Vec3b的名字进行了封装。要访问第n个子列,我们使用简单的操作符[]获取。重要的是要记住OpenCV迭代器会遍历列并自动跳到下一行。因此,对于彩色图像,如果使用简单的uchar迭代器,就只能访问蓝色通道的值。
遍历方式三:即时地址计算,带参数返回功能
最后一种扫描方法:不推荐。它用于获取或修改图像中随机位置的元素。它的基本用法是指定要访问的项的行号和列号。在我们早期的扫描方法中,你可能已经注意到,我们正在使用的图像类型很重要。这里没有什么不同,因为您需要手动指定在自动查找时使用的类型。您可以在以下源代码的灰度图像中观察到这一点(使用+ cv::Mat::at()函数):
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_<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;
}
该函数接受您的输入类型和坐标,并计算查询项的地址。然后返回对它的引用。当您获得值时,它可能是常量,当您设置值时,它可能是非常量。作为一个安全步骤,在调试模式只有有一个检查,才能确保你的输入坐标是有效的,并且确实存在。如果不是这样,您将在标准错误输出流中得到一个很好的输出消息。与发布模式下的有效方式相比,使用此方法的唯一区别是,对于图像的每个元素,您将获得一个新的行指针,用于我们使用C操作符[]来获取列元素。
如果您需要使用此方法对一个图像进行多次查找,那么为每次访问输入类型和at关键字可能会很麻烦和耗时。为了解决这个问题,OpenCV有一个cv::Mat_数据类型。它与Mat相同,但是在定义时需要通过查看数据矩阵指定数据类型,但是作为回报,可以使用操作符()快速访问项。更好的是,它可以很容易地从通常的cv::Mat数据类型转换为cv::Mat数据类型。您可以在上面函数的彩色图像中看到它的示例用法。然而,需要注意的是,相同的操作(具有相同的运行时速度)可以在cv::Mat::at函数上完成。这只是一个更少的为懒惰的程序员编写技巧。
遍历方式四:核心函数
这是在图像中实现查找表 修改 的额外方法。在图像处理中,将给定图像的所有值修改为其他值是很常见的。OpenCV提供了一个修改图像值的函数,而不需要编写图像的扫描逻辑。我们使用核心模块的cv::LUT()函数。首先,我们构建一个查找表的Mat类型:
Mat lookUpTable(1, 256, CV_8U);
uchar* p = lookUpTable.ptr();
for( int i = 0; i < 256; ++i)
p[i] = table[i];
最后调用函数(I为输入图像,J为输出图像):
LUT(I, lookUpTable, J);
性能差异
为了得到最好的结果,最好自己编译程序并运行它。为了使差异更清晰,我使用了一张相当大的(2560 X 1600)图像。这里给出的性能是针对彩色图像的。为了得到更准确的值,我对调用该函数数百次得到的值取了平均值。
Method | Time |
Efficient Way | 79.4717 milliseconds |
Iterator | 83.7201 milliseconds |
On-The-Fly RA | 93.7878 milliseconds |
LUT function | 32.5759 milliseconds |
我们可以总结几点。 如果可能的话,使用OpenCV已经实现的功能(而不是再造这些功能)。最快的方法是LUT函数。这是因为OpenCV库是通过Intel线程构建块启用多线程的。但是,如果您需要编写一个简单的图像扫描,最好使用指针方法。迭代器是一种更安全的选择,但是速度相当慢。在调试模式下,使用即时引用访问方法进行全图像扫描的成本最高。在发布模式下,它可能优于迭代器方法,也可能劣于迭代器方法,但它肯定会为此牺牲迭代器的安全特性。
建议使用代码
int divideWith = 0; // convert our input string to number - C++ style
stringstream s;
s << argv[2];
s >> divideWith;
if (!s || !divideWith)
{
cout << "Invalid number entered for dividing. " << endl;
return -1;
}
uchar table[256];
for (int i = 0; i < 256; ++i)
table[i] = (uchar)(divideWith * (i/divideWith));
double t = (double)getTickCount();//计时开始
// do something ...
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);
t = ((double)getTickCount() - t)/getTickFrequency();//计时结束
cout << "Times passed in seconds: " << t << endl;