目的
我們將探索以下問題的答案:
- 如何遍歷圖像中的每一個像素?
- OpenCV的矩陣值是如何存儲的?
- 如何測試我們所實現算法的性能?
- 查找表是什麼?爲什麼要用它?
測試用例
這裏我們測試的,是一種簡單的顏色縮減方法。如果矩陣元素存儲的是單通道像素,使用C或C++的無符號字符類型,那麼像素可有256個不同值。但若是三通道圖像,這種存儲格式的顏色數就太多了(確切地說,有一千六百多萬種)。用如此之多的顏色可能會對我們的算法性能造成嚴重影響。其實有時候,僅用這些顏色的一小部分,就足以達到同樣效果。
這種情況下,常用的一種方法是 顏色空間縮減 。其做法是:將現有顏色空間值除以某個輸入值,以獲得較少的顏色數。例如,顏色值0到9可取爲新值0,10到19可取爲10,以此類推。
uchar (無符號字符,即0到255之間取值的數)類型的值除以 int 值,結果仍是 char 。因爲結果是char類型的,所以求出來小數也要向下取整。利用這一點,剛纔提到在 uchar 定義域中進行的顏色縮減運算就可以表達爲下列形式:
這樣的話,簡單的顏色空間縮減算法就可由下面兩步組成:一、遍歷圖像矩陣的每一個像素;二、對像素應用上述公式。值得注意的是,我們這裏用到了除法和乘法運算,而這兩種運算又特別費時,所以,我們應儘可能用代價較低的加、減、賦值等運算替換它們。此外,還應注意到,上述運算的輸入僅能在某個有限範圍內取值,如 uchar 類型可取256個值。
由此可知,對於較大的圖像,有效的方法是預先計算所有可能的值,然後需要這些值的時候,利用查找表直接賦值即可。查找表是一維或多維數組,存儲了不同輸入值所對應的輸出值,其優勢在於只需讀取、無需計算。
我們的測試用例程序(以及這裏給出的示例代碼)做了以下幾件事:以命令行參數形式讀入圖像(可以是彩色圖像,也可以是灰度圖像,由命令行參數決定),然後用命令行參數給出的整數進行顏色縮減。目前,OpenCV主要有三種逐像素遍歷圖像的方法。我們將分別用這三種方法掃描圖像,並將它們所用時間輸出到屏幕上。我想這樣的對比應該很有意思。
你可以從 這裏 下載源代碼,也可以找到OpenCV的samples目錄,進入cpp的tutorial_code的core目錄,查閱該程序的代碼。程序的基本用法是:
how_to_scan_images imageName.jpg intValueToReduce [G]
最後那個參數是可選的。如果提供該參數,則圖像以灰度格式載入,否則使用彩色格式。在該程序中,我們首先要計算查找表。
int divideWith; // convert our input string to number - C++ style
stringstream s;
s << argv[2];
s >> divideWith;
if (!s)
{
cout << "Invalid number entered for dividing. " << endl;
return -1;
}
uchar table[256];
for (int i = 0; i < 256; ++i)
table[i] = divideWith* (i/divideWith);
這裏我們先使用C++的 stringstream 類,把第三個命令行參數由字符串轉換爲整數。然後,我們用數組和前面給出的公式計算查找表。這裏並未涉及有關OpenCV的內容。
另外有個問題是如何計時。沒錯,OpenCV提供了兩個簡便的可用於計時的函數 getTickCount() 和 getTickFrequency() 。第一個函數返回你的CPU自某個事件(如啓動電腦)以來走過的時鐘週期數,第二個函數返回你的CPU一秒鐘所走的時鐘週期數。這樣,我們就能輕鬆地以秒爲單位對某運算計時:
double t = (double)getTickCount();
// 做點什麼 ...
t = ((double)getTickCount() - t)/getTickFrequency();
cout << "Times passed in seconds: " << t << endl;
圖像矩陣是如何存儲在內存之中的?
在我的教程 基本圖像容器 - Mat 中,你或許已瞭解到,圖像矩陣的大小取決於我們所用的顏色模型,確切地說,取決於所用通道數。如果是灰度圖像,矩陣就會像這樣:
而對多通道圖像來說,矩陣中的列會包含多個子列,其子列個數與通道數相等。例如,RGB顏色模型的矩陣:
注意到,子列的通道順序是反過來的:BGR而不是RGB。很多情況下,因爲內存足夠大,可實現連續存儲,因此,圖像中的各行就能一行一行地連接起來,形成一個長行。連續存儲有助於提升圖像掃描速度,我們可以使用isContinuous() 來去判斷矩陣是否是連續存儲的. 相關示例會在接下來的內容中提供。
1.高效的方法 Efficient Way
說到性能,經典的C風格運算符[](指針)訪問要更勝一籌. 因此,我們推薦的效率最高的查找表賦值方法,還是下面的這種:
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 * channels;
int nCols = I.cols;
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;
}
這裏,我們獲取了每一行開始處的指針,然後遍歷至該行末尾。如果矩陣是以連續方式存儲的,我們只需請求一次指針、然後一路遍歷下去就行。彩色圖像的情況有必要加以注意:因爲三個通道的原因,我們需要遍歷的元素數目也是3倍。
這裏有另外一種方法來實現遍歷功能,就是使用 data , data會從 Mat 中返回指向矩陣第一行第一列的指針。注意如果該指針爲NULL則表明對象裏面無輸入,所以這是一種簡單的檢查圖像是否被成功讀入的方法。當矩陣是連續存儲時,我們就可以通過遍歷 data 來掃描整個圖像。例如,一個灰度圖像,其操作如下:
uchar* p = I.data;
for( unsigned int i =0; i < ncol*nrows; ++i)
*p++ = table[*p];
這回得出和前面相同的結果。但是這種方法編寫的代碼可讀性方面差,並且進一步操作困難。同時,我發現在實際應用中,該方法的性能表現上並不明顯優於前一種(因爲現在大多數編譯器都會對這類操作做出優化)。
2.迭代法 The iterator (safe) method
在高性能法(the efficient way)中,我們可以通過遍歷正確的 uchar 域並跳過行與行之間可能的空缺-你必須自己來確認是否有空缺,來實現圖像掃描,迭代法則被認爲是一種以更安全的方式來實現這一功能。在迭代法中,你所需要做的僅僅是獲得圖像矩陣的begin和end,然後增加迭代直至從begin到end。將*操作符添加在迭代指針前,即可訪問當前指向的內容。
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;
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;
}
對於彩色圖像中的一行,每列中有3個uchar元素,這可以被認爲是一個小的包含uchar元素的vector,在OpenCV中用 Vec3b 來命名。如果要訪問第n個子列,我們只需要簡單的利用[]來操作就可以。需要指出的是,OpenCV的迭代在掃描過一行中所有列後會自動跳至下一行,所以說如果在彩色圖像中如果只使用一個簡單的 uchar 而不是 Vec3b 迭代的話就只能獲得藍色通道(B)裏的值。
3. 通過相關返回值的On-the-fly地址計算
事實上這個方法並不推薦被用來進行圖像掃描,它本來是被用於獲取或更改圖像中的隨機元素。它的基本用途是要確定你試圖訪問的元素的所在行數與列數。在前面的掃描方法中,我們觀察到知道所查詢的圖像數據類型是很重要的。這裏同樣的你得手動指定好你要查找的數據類型。下面的代碼中是一個關於灰度圖像的示例(運用 + at() 函數):
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)];
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;
}
該函數輸入爲數據類型及需求元素的座標,返回的是一個對應的值-如果用 get 則是constant,如果是用 set 、則爲non-constant. 處於程序安全,當且僅當在 debug 模式下 它會檢查你的輸入座標是否有效或者超出範圍. 如果座標有誤,則會輸出一個標準的錯誤信息. 和高性能法(the efficient way)相比, 在 release模式下,它們之間的區別僅僅是On-the-fly方法對於圖像矩陣的每個元素,都會獲取一個新的行指針,通過該指針和[]操作來獲取列元素.
當你對一張圖片進行多次查詢操作時,爲避免反覆輸入數據類型和at帶來的麻煩和浪費的時間,OpenCV 提供了:basicstructures:Mat_ <id3> data type. 它同樣可以被用於獲知矩陣的數據類型,你可以簡單利用()操作返回值來快速獲取查詢結果. 值得注意的是你可以利用 at() 函數來用同樣速度完成相同操作. 它僅僅是爲了讓懶惰的程序員少寫點 >_< .
4. 核心函數LUT(The Core Function)
這是最被推薦的用於實現批量圖像元素查找和更該操作圖像方法。在圖像處理中,對於一個給定的值,將其替換成其他的值是一個很常見的操作,OpenCV 提供裏一個函數直接實現該操作,並不需要你自己掃描圖像,就是:operationsOnArrays:LUT() <lut> ,一個包含於core module的函數. 首先我們建立一個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 X 1600). 性能測試這裏用的是彩色圖片,結果是數百次測試的平均值.
Efficient Way | 79.4717 milliseconds |
Iterator | 83.7201 milliseconds |
On-The-Fly RA | 93.7878 milliseconds |
LUT function | 32.5759 milliseconds |
我們得出一些結論: 儘量使用 OpenCV 內置函數. 調用LUT 函數可以獲得最快的速度. 這是因爲OpenCV庫可以通過英特爾線程架構啓用多線程. 當然,如果你喜歡使用指針的方法來掃描圖像,迭代法是一個不錯的選擇,不過速度上較慢。在debug模式下使用on-the-fly方法掃描全圖是一個最浪費資源的方法,在release模式下它的表現和迭代法相差無幾,但是從安全性角度來考慮,迭代法是更佳的選擇