OPENCV库是一个应用非常广泛的计算机视觉与机器学习库,而对矩阵的访问也是最常见.的操作。尽管OPENCV已经升级了N多次,最新版本是2.4.6,但对数据的访问一直还是延续OPENCV1.X中讲解的效率最高的访问方式,也即指针方式。今天偶来兴致,想测测自己频繁使用访问数组方式的各种效率,结果令人惊讶(本测试是在OPENCV2.4.6库下进行的)。
首先准备下面三段代码(函数代码很简单,就是逐个访问矩阵中的元素):
1)根据矩阵的连续性进行访问(一直以为是效率最高的方式)
void computeWithContinious( Mat& _src, Mat& _dst, float scale) { _dst = cv::Scalar::all(0); int i, j; Size size = _src.size(); int chns = _src.channels(); if( _src.isContinuous() && _dst.isContinuous() ) { size.width *= size.height; size.height = 1; } for( i = 0; i < size.height; i++ ) { const unsigned char* src = (const unsigned char*)(_src.data + _src.step*i); unsigned char* dst = ( unsigned char*)(_dst.data + _dst.step*i); for( j =0; j < size.width; j++ ) { if ( chns ==1) { dst[j] = src[j]; } else { dst[j*chns] = ( unsigned char)(src[j*chns]*scale); dst[j*chns+1] = ( unsigned char)(src[j*chns+1]*scale); dst[j*chns+2] = ( unsigned char)(src[j*chns+2]*scale); } } } }2)传统指针的方式(OPENCV 1.X中最优效率的方式)
void computeWithPointer( Mat& _src, Mat& _dst, float scale)
{ _dst = cv::Scalar::all(0); int i, j; int height = _src.rows; int width = _src.cols; Size size = _src.size(); int chns = _src.channels(); for( i = 0; i < height; i++ ) { const unsigned char* src = (const unsigned char*)(_src.data + _src.step*i); unsigned char* dst = ( unsigned char*)(_dst.data + _dst.step*i); for( j =0; j < width; j++ ) { if ( chns ==1) { dst[j] = src[j]; } else { dst[j*chns] = ( unsigned char)(src[j*chns]*scale); dst[j*chns+1] = ( unsigned char)(src[j*chns+1]*scale); dst[j*chns+2] = ( unsigned char)(src[j*chns+2]*scale); } } } }3)Mat的at函数访问(一直以为是效率最低的方式)
void computeWithAt( Mat& _src, Mat& _dst, float scale)
{ _dst = cv::Scalar::all(0); int i, j; int height = _src.rows; int width = _src.cols; Size size = _src.size(); int chns = _src.channels(); for( i = 0; i < height; i++ ) { for( j =0; j < width; j++ ) { if ( chns ==1) _dst.at<uchar>(i,j) = _src.at<uchar>(i,j); else _dst.at<Vec3b>(i,j) = _src.at<Vec3b>(i,j)*scale; } } }4)Main函数测试部分
int main( int argc, char* argv[])
{ const char* file_path = "E:\\Video\\lena.jpg"; double frequency = getTickFrequency()/1000; Mat src = imread(file_path); Mat dst(src.rows, src.cols, src.type()); double t0 = getTickCount(); computeWithAt(src, dst,0.8f); double t1 = getTickCount(); computeWithPointer(src, dst,0.8f); double t2 = getTickCount(); computeWithContinious(src, dst,0.8f); double t3 = getTickCount(); double dt1 = (t1-t0)/frequency; double dt2 = (t2-t1)/frequency; double dt3 = (t3-t2)/frequency; cout<<"computeWithAt time:"<<dt1<<"ms"<<endl; cout<<"computeWithPointer time:"<<dt2<<"ms"<<endl; cout<<"computeWithContinious time:"<<dt3<<"ms"<<endl; system("pause"); return 1; }到这里,准备工作已经完成,大家可以猜猜哪种方式效率最高(思考30秒)??思考完了,再来看看下面的测试图: (奇怪了??结果与公司里的电脑测试的结果不同,公司电脑测试的结果是computeWithAt最快,computeWithContinious最慢,可能公司电脑比较老,对TBB和SSE指令支持不太好,有兴趣的读者可以自己测试下)。但这次测试结果与预期的相同,先前个人分析了先这三种矩阵的访问方式,其实at方式的访问在底层采用的也是指针,但是在对矩阵进行逐一访问,每次都要对地址重新计算和定位,因此相对于指针访问方式来说(指针访问方式是通过移动指针来定位下一个元素的位置),效率就有慢;而对于本次测试中效率最高的Continious方式来说,因为这种访问方式由于矩阵数据的存放是连续的(这里又有疑问呢?怎么样的才是连续呢? 连续就是指矩阵中行与行之间是对齐的,即行的末尾没有gap(因为内存中的数据存储时为了提高效率,一般都是安字节对齐的)),将两层循环编程一层循环(rows =1),减少了循环时间,而且通过指针移动来逐一对数据进行访问,自然速度会比指针的方式快。
综上所述,在对矩阵进行访问时,computerWithContinious是最快的,其次是computeWithPointer,最后是computeWithAt。但这也因机器而有些差别。在公司的奔腾系列电脑上的测试结果与上面是相反的(at最快,指针次之,最后是continious),但在我四核的本本上的测试结果与预期是一致的。
最后,读者有兴趣可以去看看底层的关于这几种访问方式的实现,通过对源代码的分析也可以为今后写出高效率的代码提供一些技巧和窍门。