如何利用OpenCV扫描图片,查表以及时间尺度
目标
我们将要找到一下问题的答案:
1、如何遍历每一个图像中的像素?
2、OpenCV矩阵的值是如何存储的?
3、如何确保我们的算法的性能?
4、什么叫做查表、为什么要用它?
测试案例
我们来考虑一下色彩消减方法。在C和C++中为矩阵中元素存储使用unsigned char类型,一个像素的通道最多会有256中不同的数值。对于一个三通道的图像来说,这就允许格式化为更多中色彩(准确的说1600万)。在这么多的颜色深浅工作可能会沉重地打击了我们的算法性能。然而,有时候需要相对很少的工作就足够得到最终相同的结果。
我们一般使用色域减少方法来处理这个问题。这就意味着我们用新输入的数据来除以(划分)当前色域最终得到少量的颜色。例如每一个在0~9之间的数值会被0代替,每一个在10~19之间的数值会被10代替等等。
当你用一个int 类型的数值除以一个uchar(unsigned char-也称为 数值区间为0~255)类型,最终的结果也会是char类型。这些数值只可能是char 类型数值。因此,任何余数将会被向下近似约掉。利用这个特性,upper operator这个操作在uchar域中可以被表示为:
这个简单的色域减少算法通过遍历图像矩阵中的每一个像素,并且将像素适用于这个方程中。这里值得注意的是我们要做一个除和乘的运算。这些运算对于系统来说要付出沉重的代价。如果有可能,利用一些例如减法、加法或者最好情况下的简单赋值运算来避免那些代价值得的。此外,注意我们在这个upper操作中只允许一定数量的输入数值。在uchar系统中这个数值精确为256.
我们的测试算法(也就是展示在这里的例子)在这里会做以下动作:读取控制台中的参数中的图像(可能是彩色的或者是灰度图案—控制台参数也是这样的),并应用给定的控制台行参数的整数值的降低。在OpenCV中,这时我们有三个主要的方法按像素的遍历整个图像。为了让事情变得有趣一些,我们将会用这三种方法来遍历每一张图片,并且输出他们所花费的时间。
你可以从这里下载所有的源代码或从OpenCV的事例文件夹的代码部分查阅cpp tutorial code。
最后的参数是可选择的。如果这个参数被传入,则图像会以灰度的格式来传入,否则就会用RGB的格式。第一件事情是计算查阅表(look up table)。
int divideWith=0; 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类将第三个命令行参数从文本类型转换成整型。然后我们利用简单的查看和upper 方程来计算查找表。没有OpenCV的东西在里面。
tips:这里我们需要注意的是程序中的几处函数:
Mat::Ptr Returns a pointer to the specified matrix row
Mat::at Returns a reference to the specified array element
LUT Transforms the source matrix into the destination matrix using the given look-up table: dst(I) = lut(src(I))
这些函数在代码中都有所提及,应当注意
另一个问题就是我们如何测量时间?OpenCV提供了两个简单的方程来完成这项工作:getTickCount()和getTickFrequency()。第一个返回CPU在一些事件(例如从你启动你的系统开始)中会返回多少数量的滴答。第二个返回你的CPU会在一秒钟返回多少次滴答。因此通过这两个操作来测量花费多少时间是很容易的:
double t =(double)getTickCount(); t=((double)getTickCount()-t)/getTickFrequency(); cout<<"Times passed in seconds:"<<t<<endl;
图形矩阵在内存中的存储方式是怎样的呢?
正如你在Mat-The Basic Image Container教程中看到的那样,矩阵的大小是跟随颜色系统的使用而变化的。更精确的说,和通道数有关。在灰度图像中,我们可以看到类似的东西:
对于多通道图像来说,矩阵中的列会包含通道数相同的子列。例如在RGB颜色系统中:
需要注意的地方是,通道的存放顺序是相反的,也就是说,是BGR而不是RGB。因为在许多情况下,内存的大小能够满足一行接一行的连续的存储方式,也就是说,在内存中存储单个长排。因为不管里面是什么,只要是放到同一个位置上(也就是存储单个长排),就会增加扫描过程的速度。我们可以使用isContinuous()这个函数来查询这个矩阵是不是这样的情况。连续的情况在下一节中会有事例提及。
高效的方法
当读取问题涉及到性能问题时,最高效的就是利用经典的C的operator[](指针)。因此我们建议最有效率的就是下面的:
Mat&ScanImageAndReduceC(Mat&I,constuchar*const table) { CV_Assert(I.depth()!=sizeof(uchar)); 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这个矩阵对象的数据成员会返回指向第一排,第一列的指针。如果这个指针为空,就说明对于这个对象输入是无效的。这是检测图像是否成功载入的最简单的办法。如果存储的方式是连续的,我们可以一次性遍历完整个data指针。在恢复范围图像中,就像下面所示:
uchar*p=I.data; for(unsigned int i=0;i<ncol+nrows;++i) *p++=table[*p];
你会得到相同的结果。然而,之后这个代码读起来相对于较难(this code is a lot harder to read later on)。如果在这里使用一些更高级的技术就会更难读懂。此外,一般来说,我已经预料到你在性能结果方面不会有所提升(因为大多数现代的编译器可能会为你的代码自动做一些小的优化措施)。
迭代器(安全)方法
高效的事例当中,你当担负起确保遍历那些应当被遍历的uchar区域并且跳过那些可能在行之间的空隙(gaps)的责任。迭代器方法被认为是一种安全的并且使用户脱离这些任务的方法。你只需要做的是查询木箱矩阵的开头和结尾然后从开头部分递增迭代器,一直到达矩阵的最尾端。想要获取被迭代器只想的数值,可以使用*(在迭代器之前加上)。
Mat &ScanImageAndReduceIterator(Mat&I,const uchar* const table) { CV_Assert(I.depth()!=sizeof(uchar)); 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类型的短的vector,这个类型在OpenCV中被命名为Vec3b。我们使用简单的[]操作来读取第n个子列里面的元素。记得使用OpenCV的迭代器遍历列并且自动跳到下一行很重要。因此,在彩色图像中,如果你使用简单的uchar类型的迭代器,你只能读取到蓝色通道中的数据。
On-the-fly使用引用返回计算地址
最后的方法不建议在扫描中使用。这在要求或者修改一些图像中的随机元素会使用到。基本的用法是指定你想要读取的元素的行和列。在之前的扫描方法中,你已经看到我们在遍历图像过程中类型的重要性。在这里指定你想要操作的特定的不管在自动查阅过程中什么类型的操作是没有困难的。你可以在下面看到遍历灰度图像的源代码(at()函数的用法)。
Mat& ScanImageAndReduceRandomAccess(Mat&I,const uchar*const table) { CV_Assert(I.depth()!=sizeof(uchar)) 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]=tables[_I(i,j)[0]]; _I(i,j)[1]=tables[_I(i,j)[1]]; _I(i,j)[2]=tables[_I(i,j)[2]]; } I=_I; break; } } return I; }
函数通过你传入的类型、坐标然后计算你想要得到的地址(翻译的不太好:The functions takes your input type and coordinates and calculates on the fly the address of the queried item)。然后返回一个对它的引用。当你获得这个值的时候,这个值可能是常量;当你设置这个值得时候,可能使用的是非常量值。作为在debug mode only 安全措施,我们做了一个确保你输入坐标存在的检查措施。如果坐标不可用,那么你将会在标准的错误输出流得到一个漂亮的错误输出消息。在release模式下相比较于高效的方法,唯一不同的地方就是在用这个方法的时候,对于图像中的每一个元素你将会得到一个新的行指针以便于我们使用C语言中[]操作符来获取列元素。
如果你需要使用这个方法对于图像进行多次查询,那可能会是繁琐的并且在读取类型和读取每一个关键词的时候时间开销较大。为了解决这个问题,OpenCV使用了Mat_数据类型。这个类型和Mat一样,都需要为了查看矩阵数据在定义时提供额外指定的数据类型,然而不同的地方在于你可以使用()操作符对矩阵数据进行快速访问。为了让事情变得更好一些,就让Mat_类型和Mat之间可以容易的进行互换。你可以在上面的彩色图象的方程中看到一个这样的用例。然而,需要着重注意的是相同的操作(拥有相同的运行速度)已经被at()函数所完成了。这只是对于懒惰的程序员使用的可以少些代码的小手段。
核心方程
在一个图像完成修改查询表是一个好的方法(bonus method)。因为在图像处理过程中OpenCV拥有以一个方程,这个方程可以不用你写遍历图像的方法来修改你想将图像中所有的值代替为其他值的功能,这很稀疏平常。LUT()函数作为我们使用的核心模块。首先我们建立一个Mat类型的查询表:
Mat lookUpTable(1,256,CV_8U); uchar*p=lookUpTable.data; for(int i=0;i<256;++i) p[i]=table[i];
最后调用这个函数(I是我们输入的图像J是输出):
LUT(I,lookUpTable,J);
性能区别
为了程序最好的编译、运行结果。为了更好的展现区别,我使用了非常大的图像(2560*1600)。彩色图像的性能在这里展现出来。为了达到更精确的数值,我已经对数百次的函数调用结果进行了平均。
我们可以得到以下结论。如果可以,请使用zaoyizai OpenCV中实现了的函数(而不是重新创造它们)。最快的方法是LUT函数。这是由于OpenCV库的可用的多线程是通过Intel 线程编译模块的。然而如果你需要写一个简单的图像扫描程序,你还是使用指针方法吧。迭代器是方法是安全的,但是很慢。使用on-the-fly引用读取方法遍历整幅图像在debug模式下是开销最大的。zairelease模式下可能会打败迭代器方法,只是可能。然而迭代器确实是由于安全的原因才牺牲了性能。
最后,你可以在YouTube 频道中观看我所传的那个程序事例。