【OpenCV學習】之OpenCV如何掃描圖像、利用查找表和計時

目的

我們將探索以下問題的答案:

  • 如何遍歷圖像中的每一個像素?
  • OpenCV的矩陣值是如何存儲的?
  • 如何測試我們所實現算法的性能?
  • 查找表是什麼?爲什麼要用它?

測試用例

這裏我們測試的,是一種簡單的顏色縮減方法。如果矩陣元素存儲的是單通道像素,使用C或C++的無符號字符類型,那麼像素可有256個不同值。但若是三通道圖像,這種存儲格式的顏色數就太多了(確切地說,有一千六百多萬種)。用如此之多的顏色可能會對我們的算法性能造成嚴重影響。其實有時候,僅用這些顏色的一小部分,就足以達到同樣效果。

這種情況下,常用的一種方法是 顏色空間縮減 。其做法是:將現有顏色空間值除以某個輸入值,以獲得較少的顏色數。例如,顏色值0到9可取爲新值0,10到19可取爲10,以此類推。

uchar (無符號字符,即0到255之間取值的數)類型的值除以 int 值,結果仍是 char 。因爲結果是char類型的,所以求出來小數也要向下取整。利用這一點,剛纔提到在 uchar 定義域中進行的顏色縮減運算就可以表達爲下列形式:

                                                                           I_{new} = (\frac{I_{old}}{10}) * 10

這樣的話,簡單的顏色空間縮減算法就可由下面兩步組成:一、遍歷圖像矩陣的每一個像素;二、對像素應用上述公式。值得注意的是,我們這裏用到了除法和乘法運算,而這兩種運算又特別費時,所以,我們應儘可能用代價較低的加、減、賦值等運算替換它們。此外,還應注意到,上述運算的輸入僅能在某個有限範圍內取值,如 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 中,你或許已瞭解到,圖像矩陣的大小取決於我們所用的顏色模型,確切地說,取決於所用通道數。如果是灰度圖像,矩陣就會像這樣:

                        \newcommand{\tabItG}[1] { \textcolor{black}{#1} \cellcolor[gray]{0.8}} \begin{tabular} {ccccc} ~ & \multicolumn{1}{c}{Column 0} &   \multicolumn{1}{c}{Column 1} &   \multicolumn{1}{c}{Column ...} & \multicolumn{1}{c}{Column m}\\ Row 0 & \tabItG{0,0} & \tabItG{0,1} & \tabItG{...}  & \tabItG{0, m} \\ Row 1 & \tabItG{1,0} & \tabItG{1,1} & \tabItG{...}  & \tabItG{1, m} \\ Row ... & \tabItG{...,0} & \tabItG{...,1} & \tabItG{...} & \tabItG{..., m} \\ Row n & \tabItG{n,0} & \tabItG{n,1} & \tabItG{n,...} & \tabItG{n, m} \\ \end{tabular}

而對多通道圖像來說,矩陣中的列會包含多個子列,其子列個數與通道數相等。例如,RGB顏色模型的矩陣:

            \newcommand{\tabIt}[1] { \textcolor{yellow}{#1} \cellcolor{blue} &  \textcolor{black}{#1} \cellcolor{green} & \textcolor{black}{#1} \cellcolor{red}} \begin{tabular} {ccccccccccccc} ~ & \multicolumn{3}{c}{Column 0} &   \multicolumn{3}{c}{Column 1} &   \multicolumn{3}{c}{Column ...} & \multicolumn{3}{c}{Column m}\\ Row 0 & \tabIt{0,0} & \tabIt{0,1} & \tabIt{...}  & \tabIt{0, m} \\ Row 1 & \tabIt{1,0} & \tabIt{1,1} & \tabIt{...}  & \tabIt{1, m} \\ Row ... & \tabIt{...,0} & \tabIt{...,1} & \tabIt{...} & \tabIt{..., m} \\ Row n & \tabIt{n,0} & \tabIt{n,1} & \tabIt{n,...} & \tabIt{n, m} \\ \end{tabular}

注意到,子列的通道順序是反過來的: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模式下它的表現和迭代法相差無幾,但是從安全性角度來考慮,迭代法是更佳的選擇

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章