上次例程中簡單提到了3種遍歷圖像像素的方式,但對於他們遍歷的性能我們卻一無所知。這次將詳細介紹下opencv中遍歷圖像像素的方法,例程對應爲 (TUTORIAL) how_to_scan_images,該例程將這3種方法分別用於圖像的像素量化時候,通過測量運行100次的平均時間,進行性能對比,還和opencv自帶的LUT函數進行對比,以此分析三種遍歷方法的性能。由於源碼較長,這次就不貼全部的源碼,只挑出其中用的函數進行分析。
我們先來看下程序的運行結果,如下圖所示。
可以發現,例程中展示了4種圖像遍歷的方法,運行結果給出了他們各自運行100次的時間,測試用的圖片爲512x512大小的彩色Lena圖片。
1. C 下標訪問的方法,耗時1.62311毫秒, 可以看出除了Opencv自帶的LUT函數,這個方式是最快的
2. 迭代器遍歷,耗時2.19939毫秒
3. on-the-fly,隨機 遍歷,耗時2.61471毫秒, 耗時較多,這是因爲at訪問帶邊界檢測,爲了保證了安全性能,犧牲了一定的速度
4. LUT 函數,這是opencv自帶的函數,耗時1.03029毫秒,這是圖像像素遍歷的方式,比c的下標訪問還要快37%
下面我們來看下各種方法是如何實現的。
1. C下標訪問的方法,對應例程中的 ScanImageAndReduceC 函數,該函數源代碼如下
Mat& ScanImageAndReduceC(Mat& I, const uchar* const table)
{
// accept only char type matrices
CV_Assert(I.depth() != sizeof(uchar));
int channels = I.channels();
int nRows = I.rows;
int nCols = I.cols * channels; // 彩色圖像和灰度圖像遍歷通用,彩色圖像的像素點默認是按BGR順序連續排列的
if (I.isContinuous()) //判斷圖像是否連續存儲,如果連續存儲,就將2維像素點遍歷問題轉換成1維像素點遍歷
{
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;
}
2. Mat 迭代器訪問的方式
Mat& ScanImageAndReduceIterator(Mat& I, const uchar* const table)
{
// accept only char type matrices
CV_Assert(I.depth() != sizeof(uchar));
const int channels = I.channels(); // 需要根據通道個數選擇操作
switch(channels)
{
case 1:
{
MatIterator_<uchar> it, end; // 類似於std的迭代器操作,需要指定數據類型,這裏爲uchar
for( it = I.begin<uchar>(), end = I.end<uchar>(); it != end; ++it)
*it = table[*it];
break;
}
case 3:
{
MatIterator_<Vec3b> it, end; // 3通道的迭代器,類似操作
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;
}
3.on-the-fly,隨機 遍歷,需要注意的是,at(i,j) 其中 i 對應的是y座標,即第i列,j 纔對應的是 x 座標,即第 j 行,這個是比較容易忽略而造成錯誤的地方。
Mat& ScanImageAndReduceRandomAccess(Mat& I, const uchar* const table)
{
// accept only char type matrices
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)]; //單通道直接用 at 訪問
break;
}
case 3:
{
Mat_<Vec3b> _I = I; // 三通道時,利用一個Mat_<Vec3b>類型進行訪問,該類型提供(i,j)下標訪問操作,
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;
}
4.LUT函數
void cv::LUT( InputArray _src, InputArray _lut, OutputArray _dst, int interpolation )
{
Mat src = _src.getMat(), lut = _lut.getMat();
CV_Assert( interpolation == 0 );
int cn = src.channels();
int lutcn = lut.channels();
CV_Assert( (lutcn == cn || lutcn == 1) &&
lut.total() == 256 && lut.isContinuous() &&
(src.depth() == CV_8U || src.depth() == CV_8S) );
_dst.create( src.dims, src.size, CV_MAKETYPE(lut.depth(), cn));
Mat dst = _dst.getMat();
LUTFunc func = lutTab[lut.depth()]; // lutTab 是一個函數指針數組,根據傳人的圖像數據類型,選擇相應的函數進行操作,這是opencv中比較高明的寫法。
CV_Assert( func != 0 );
const Mat* arrays[] = {&src, &dst, 0};
uchar* ptrs[2];
NAryMatIterator it(arrays, ptrs); // ptrs[0], ptrs[1] 分別指向 *array[0] , *array[1] 的數據池,即 ptrs[0] = src.data , ptrs[1] = dst.data
int len = (int)it.size;
for( size_t i = 0; i < it.nplanes; i++, ++it )
func(ptrs[0], lut.data, ptrs[1], len, cn, lutcn); // 用函數指針調用函數
}
可以看出,opencv對自帶的LUT函數進行了一定的優化,通過調用不同類型的查表函數進行操作,而通過函數指針和函數指針數組實現了在一個函數中根據數據類型的不同調用不同函數的操作手法,這個十分高明,值得我們學習,但LUT是針對查表法的一個函數,數據類型只支持CV_8U和CV_8S,因此限制了他的使用範圍。
另3種遍歷的方法,由於c 下標訪問不需要迭代器,也不做越界檢測,顯然是效率最高的。而迭代的寫法與std的迭代器的寫法十分類似,at訪問帶越界檢測,不過需要注意i,j對應的分別是y,x的座標。