本节导读:
Writing effcient image scanning loops
除非必要,否则不能以降低代码清晰度的代价去提升性能,简单的代码易维护和调试。
实现方法
实现方法在上一节里都写得差不多了,这一块主要是做对比分析,如图所示:
| Method | Average time |
|---|---|
| data[i]= data[i]/div*div + div/2; | 37ms |
| *data++= *data/div*div + div/2; | 37ms |
| *data++= v - v%div + div/2; | 52ms |
| *data++= *data&mask + div/2; | 35ms |
| colorReduce(input, output); | 44ms |
| i<image.cols*image.channels(); | 65ms |
| MatIterator | 67ms |
| .at(j,i) | 80ms |
| 3-channel loop | 29ms |
这里书上介绍了两个函数,cv::getTickCount()和cv::getTickFrequency();用法如下:1
2
3
4
5double duration;
duration = static_cast<double>(cv::getTickCount());
colorReduce(image); // the function to be tested
duration = static_cast<double>(cv::getTickCount())-duration;
duration /= cv::getTickFrequency(); // the elapsed time in ms
不得不说代码写的还真规范,要是我这样的渣写,肯定就不会想到用static_cast<double>来强制转换,直接(double);从上面的表格中可以看出,用指针遍历快,迭代器遍历慢。at最慢,不过at的设计是用来随机存取像素值的,不适用遍历整幅图像。
迭代器的设计目标就是简化图像遍历的过程从而降低出错的机会,所以如果遍历整幅图像还是用指针,快,作为程序员,应该有避免错误的能力。
下面介绍一下书上提到的一些优化细节:
- 能提前计算的都提前计算出来
1 | int nc= image.cols * image.channels(); |
来替换1
for (int i=0; i<image.cols * image.channels(); i++) {
这样就不用每次都要来计算image.channels(),重复计算有耗时,应丢弃,提前计算好用局部变量存储起来。
- 能在一个循环里解决的就不要在折腾
比如在处理三通道图像时,之前的代码中,内层循环有是这么设计的,使循环次数为image.cols * image.channels(),书上就说了,我们可以使循环次数变成image.cols1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23void colorReduce(cv::Mat &image, int div=64)
{
int nl= image.rows; // number of lines
int nc= image.cols ; // number of columns
// is it a continous image?
if (image.isContinuous())
{
// then no padded pixels
nc= nc*nl;
nl= 1; // it is now a 1D array
}
for (int j=0; j<nl; j++)
{
// pointer to first column of line j
uchar* data= image.ptr<uchar>(j);
for (int i=0; i<nc; i++)
{
*data++= div/2; //data[i][0]
*data++= div/2; //data[i][1]
*data++= div/2; //data[i][2]
}
}
}
上面这段代码,按照书上的意思是最快的图像遍历方法了!(如果我没理解错的话,有错欢迎指正!)
OpenMP and TBB
多线程处理是另外一个提升算法效率的方式,OpenMP 与 Intel Threading Building Blocks(TBB)是两个比较流行的并行编程API。VS2013里有可以直接用OpenMP,具体用法搜索一下,我用过,基础的操作还是蛮简单的,看一下就会用了,奈何自己笔记本就两核,只提升了2倍。
Scanning an image with neighbor access
与其说这一节教你怎么进行邻域操作,倒不如说教你怎么进行图像锐化,一共两种方法,第一种原始的指针操作,第二种自定义核算子,利用OpenCV函数filter2D来操作,更加快捷,二者结果一样,甚至核的尺寸如果比较大,后者效率更高。
锐化算子的计算方式:
1 | sharpened_pixel= 5 * current - left - right - up - down; |
原始的指针操作
1 | void sharpen(const cv::Mat &image, cv::Mat &result) |
这里值得注意的两点:
cv::saturate_cast<uchar>method
他的作用是:用来对计算结果进行截断(1) 小于0的像素值截断为0;(2)大于255的截断为255;(3)输入参数是浮点数的话,会对其取整至最接近输入值的整数。
- 边缘像素的处理
由于边缘像素的邻域不完整,无法按照上述进行操作运算,所以书上就直接让其变为0。使用OpenCV提供的函数进行如下操作:
1 | result.row(0).setTo(cv::Scalar(0));//如果是一个三通道的图像,则需要使用cv::Scalar(0,0,0)来指定像素三个通道的目标值 |
row and col method :They return a special cv::Mat instance composed of a single line (or a single column) as specified in a parameter. See row and colcv::Mat::setTo method: Sets all or some of the array elements to the specified value. See setTo
使用filter2D method
尽量使用下面这种方法,相比上面那种方法,filter2D更高效简洁(书上说的):
核矩阵:1
2
30 -1 0
-1 5 -1
0 -1 0
1 | void sharpen2D(const cv::Mat &image, cv::Mat &result) |
Performing simple image arithmetic
Images can be combined in different ways. Since they are regular matrices, they can be added, subtracted, multiplied, or divided. OpenCV offers various image arithmetic operators and their use is discussed in this recipe.
简单的运算
1 | // c[i]= a[i]+b[i]; |
1 | // if (mask[i]) c[i]= a[i]+b[i]; |
如果指定了掩模(mask),运算只会在mask对应像素不为NULL的像素上进行((the mask must be 1-channel);OpenCV还定义了许多运算,接下来只列举书上出现的,具体怎么用干啥的,参见OpenCV官方文档。
1 | cv::subtract, cv::absdiff, cv::multiply, cv::divide |
Overloaded image operators
1.cv::addWeighted can be written as: result= 0.7*image1+0.9*image2;
2.大多数操作符都被重载了,如&, |, ^, ~. the comparison operators <, <=, ==,!=, >, >=;these laterreturning a 8-bit binary image.
3.矩阵乘法m1*m2(m1,m2是cv::Mat的实例),矩阵求逆(matrix inversion)m1.inv() , 矩阵转置(transpose)m1.t() , 矩阵的行列式(determinant)m1.determinant() , 向量模(vector norm)v1.norm() , 向量叉乘(cross-product)v1.cross(v2), 向量点积(dot product)v1.dot(v2) .
4.前面的循环遍历实现图像颜色缩减可以用一行代码来写:1
image = (image&cv::Scalar(mask,mask,mask)) + cv::Scalar(div/2,div/2,div/2);
但是这个耗时比较长,89ms,调用位运算与标量和预算(而且不是在同一个for循环里完成这个操作)但是因为其简洁,大多数情况考虑用他们。(不是特别注重时间性能的话)
Splitting the image channels
1 | // create vector of 3 images |
Defining regions of interest
如果想把一张小图片往大图片上贴,由于大小尺寸不同,无法直接用cv::add,这时候就需要定义感兴趣区域(ROI, regions of interest).
实现方法
定义感兴趣区域,插入图像:1
2
3
4
5// define image ROI
cv::Mat imageROI;
imageROI = image(cv::Rect(385, 270, logo.cols, logo.rows));
// add logo to image
cv::addWeighted(imageROI, 1.0, logo, 0.3, 0., imageROI);
我们知道cv::Mat赋值操作只是浅拷贝,他们共享一块内存,也就是说imageROI指向的就是原图片,改变imageROI,意味着改变原图像,从而实现插入。 但是在书上的图中我们看到,效果不明显,因为直接相加,可能像素值超过255,然后使其为255,这样就造成效果不明显,所以书上又提供了掩模来操作,直接替换原图像该位置的像素。
1 | // define ROI |
掩模的作用就是只处理原图像中对应掩模元素不为NULL的位置的元素。比如掩模(1, 1)处为NULL,那么原图像该位置就不处理,如果该位置不为NULL,那么原图像这个位置像素就处理。
定义ROI的方法
(1)上面已经给出一种方法了,使用cv::Rect:1
2cv::Mat imageROI;
imageROI = image(cv::Rect(385, 270, logo.cols, logo.rows));
(2)这里介绍第二种方法,使用cv::Range:1
cv::Mat imageROI = image(cv::Range(270,270+logo.rows), cv::Range(385,385+logo.cols));
cv::Range是指从起始索引到终止索引(不包含终止索引)的一段连续序列。
(3)第二种方法的扩展
创建原始图像特定行/列的ROI1
2cv::Mat imageROI = image.rowRange(start, end); //行
cv::Mat imageROI = image.colRange(start, end); //列