如何使用OpenCV扫描图像,查找表格和时间测量
目标
我们会为以下问题寻求答案:
- 如何通过图像的每个像素?
- OpenCV矩阵值如何存储?
- 如何衡量我们的算法的性能?
- 什么是查找表,为什么使用它们?
我们的测试用例
让我们考虑一种简单的减色方法。通过对矩阵项存储使用unsigned char C和C ++类型,像素通道最多可以有256个不同的值。对于三通道图像,这可以允许形成太多的颜色(1600万精确)。使用如此多的色调可能会对我们的算法性能造成沉重打击。然而,有时候,只要少一点工作能够得到相同的最终结果就足够了。
在这种情况下,我们通常会减少色彩空间。这意味着我们将颜色空间当前值与新的输入值分开,以减少颜色。例如,零和九之间的每个值都将新的值为零,每个值在十到十十之间的值十等等。
当您使用int值将uchar(unsigned char-aka值在0和255之间)值分隔时,结果也将是char。这些值只能是char值。因此,任何分数将被向下舍入。利用这一事实,uchar域中的上层操作可以表示为:
简单的颜色空间缩小算法将包括仅通过图像矩阵的每个像素并应用该公式。值得注意的是,我们做一个除法和乘法运算。这些操作对于系统来说是昂贵的。如果可能,通过使用更便宜的操作(如少量减法,添加或在最佳情况下是简单的分配)来避免这种情况。此外,请注意,我们只有上限操作的输入值有限。在uchar系统的情况下,这是256。
因此,对于较大的图像,预先计算所有可能的值,并且在分配期间通过使用查找表来进行分配是明智的。查找表是简单的数组(具有一个或多个维),对于给定的输入值变量保存最终的输出值。其实力在于我们不需要进行计算,只需要读取结果。
我们的测试用例程序(以及此处提供的示例)将执行以下操作:读取控制台线路参数图像(可以是颜色或灰度级 - 控制台线路参数),并使用给定的控制台行参数整数值。在OpenCV中,目前有三种主要通过像素逐个通过图像的方法。为了使事情更有趣,将使用所有这些方法对每个图像进行扫描,并打印出花费多长时间。
您可以在这里下载完整的源代码,或者在OpenCV的sample目录中查看核心部分的cpp教程代码。其基本用途是:
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;
图像矩阵如何存储在内存中?
正如您已经阅读我的Mat - 基本图像容器教程中的矩阵大小取决于使用的颜色系统。更准确地说,它取决于所使用的通道数量。在灰度图像的情况下,我们有一些像:
对于多通道图像,列包含与通道数一样多的子列。例如在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对象的数据数据成员返回指向第一行第一列的指针。如果此指针为空,则该对象中没有有效的输入。检查这是检查您的图像加载是否成功的最简单的方法。如果存储是连续的,我们可以使用它来遍历整个数据指针。在灰度图像的情况下,它将如下所示:
uchar * p = I.data;
for(unsigned int i = 0; i <ncol * nrows; ++ i)
* p ++ = table [* p];
你会得到相同的结果。但是,这段代码稍后阅读很难阅读。如果你有更先进的技术,那就更难了。此外,在实践中,我观察到您将获得相同的性能结果(因为大多数现代编译器可能会为您自动实现这种小型优化技巧)。
迭代程序(安全)方法
如果有效的方式确保您通过适量的uchar字段,并跳过行之间可能发生的差距是您的责任。迭代程序方法被认为是更安全的方式,因为它从用户接管这些任务。所有你需要做的是要求图像矩阵的开始和结束,然后只是增加开始迭代程序,直到你到达结束。要获取迭代程序指向的值,使用*运算符(在它之前添加)。
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个子列,我们使用简单的operator []访问。重要的是要记住,OpenCV迭代程序遍历列,并自动跳到下一行。因此,如果使用简单的uchar迭代程序,您将只能访问蓝色通道值。
参考返回的即时地址计算
最后的方法不推荐用于扫描。它是为了获取或修改图像中的某种方式的随机元素。它的基本用法是指定要访问的项目的行号和列号。在我们早期的扫描方法中,您可以通过我们正在查看的图像来观察这一点很重要。这在这里没有什么不同,因为您需要手动指定在自动查找时要使用的类型。如果下列源代码的灰度图像(+ cv :: 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相同,在定义中需要通过查看数据矩阵来指定数据类型,但是您可以使用operator()快速访问项目。为了使事情变得更好,这可以很容易地从和通常的cv :: Mat数据类型转换。您可以在上方功能的彩色图像的情况下看到此示例的用法。然而,重要的是要注意,cv :: 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];
最后调用函数(我是我们的输入图像,J是输出的一个):
LUT(I,lookUpTable,J);
性能差异
为了最好的结果,编译程序并以自己的速度运行它。为了使差异更加清晰,我使用了相当大的(2560 X 1600)图像。这里呈现的性能是彩色图像。为了获得更准确的值,我将从函数调用得到的值平均为100次。
方法 | 时间 |
---|---|
高效的方式 | 79.4717毫秒 |
迭代程序 | 83.7201毫秒 |
在飞行RA | 93.7878毫秒 |
LUT功能 | 32.5759毫秒 |
我们可以总结一些事情。如果可能,请使用OpenCV已经创建的功能(而不是重新创建它们)。最快的方法是LUT功能。这是因为OpenCV库通过Intel Threaded Building Blocks启用多线程。但是,如果你需要编写一个简单的图像扫描,喜欢指针方法。迭代程序程序
是一个更安全的赌注,但是相当慢。使用即时参考访问方法进行全图像扫描是调试模式中最昂贵的。在释放模式下,它可能会击败迭代程序方法,但是它肯定会牺牲迭代程序的安全性能。