参考:
1、https://docs.opencv.org/3.2.0/
2、https://github.com/opencv/opencv/
如何使用OpenCV扫描图像,查找表和时间测量
- 如何通过图像的每个像素?
- OpenCV矩阵值如何存储?
- 如何衡量我们算法的性能?
- 什么是查找表,为什么使用它们?
Our test case
我们的测试用例程序(和这里提供的示例)将执行以下操作:读取控制台行参数图像(可能是彩色或灰度级 - 控制台行参数),并将缩减应用于给定的控制台行参数整数值。 在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; //文本转成int
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); //设定图像的数值类型为uint8
int channels = I.channels();
int nRows = I.rows;
int nCols = I.cols * channels; //列x通道数
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;
}
核心功能
这是一个在图像中实现查找表修改的额外方法。 因为在图像处理中,您想要将所有给定的图像值替换为其他值,这是相当常见的。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 Threaded Building Blocks启用的多线程。 但是,如果您需要编写一个简单的图像扫描更喜欢指针的方法。 迭代器是一个更安全的赌注,但相当慢。 在调试模式下,使用实时参考访问方法进行全图像扫描是成本最高的。 在释放模式下,它可能会打败迭代器的方法,但它肯定会牺牲迭代器的安全特性。
// 执行方式:xxx.exe <imageNameToUse> <divideWith> [G]
#include <opencv2/core.hpp>
#include <opencv2/core/utility.hpp>
#include "opencv2/imgcodecs.hpp"
#include <opencv2/highgui.hpp>
#include <iostream>
using namespace std;
using namespace cv;
int main(int argc, char** argv)
{
if (argc < 3)
{
cout << "Not enough parameters" << endl;
return -1;
}
Mat I, J;
if (argc == 4 && !strcmp(argv[3], "G"))
I = imread(argv[1], IMREAD_GRAYSCALE);
else
I = imread(argv[1], IMREAD_COLOR);
if (I.empty())
{
cout << "The image" << argv[1] << " could not be loaded." << endl;
return -1;
}
//! [dividewith]
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));
Mat lookUpTable(1, 256, CV_8U);
uchar* p = lookUpTable.ptr();
for (int i = 0; i < 256; ++i)
p[i] = table[i];
double t;
t = (double)getTickCount();
LUT(I, lookUpTable, J);
imwrite("D:\\j.bmp", J);
t = 1000 * ((double)getTickCount() - t) / getTickFrequency();
cout << "time:" << t << endl;
return 0;
}