如何使用OpenCV掃描圖像,查找表格和時間測量

2018-08-29 11:23 更新

目標

我們會為以下問題尋求答案:

  • 如何通過圖像的每個像素?
  • OpenCV矩陣值如何存儲?
  • 如何衡量我們的算法的性能?
  • 什么是查找表,為什么使用它們?

我們的測試用例

讓我們考慮一種簡單的減色方法。通過對矩陣項存儲使用unsigned char C和C ++類型,像素通道最多可以有256個不同的值。對于三通道圖像,這可以允許形成太多的顏色(1600萬精確)。使用如此多的色調(diào)可能會對我們的算法性能造成沉重打擊。然而,有時候,只要少一點工作能夠得到相同的最終結(jié)果就足夠了。

在這種情況下,我們通常會減少色彩空間。這意味著我們將顏色空間當前值與新的輸入值分開,以減少顏色。例如,零和九之間的每個值都將新的值為零,每個值在十到十十之間的值十等等。

當您使用int值將uchar(unsigned char-aka值在0和255之間)值分隔時,結(jié)果也將是char。這些值只能是char值。因此,任何分數(shù)將被向下舍入。利用這一事實,uchar域中的上層操作可以表示為:

QQ圖片20170829111236

簡單的顏色空間縮小算法將包括僅通過圖像矩陣的每個像素并應(yīng)用該公式。值得注意的是,我們做一個除法和乘法運算。這些操作對于系統(tǒng)來說是昂貴的。如果可能,通過使用更便宜的操作(如少量減法,添加或在最佳情況下是簡單的分配)來避免這種情況。此外,請注意,我們只有上限操作的輸入值有限。在uchar系統(tǒng)的情況下,這是256。

因此,對于較大的圖像,預(yù)先計算所有可能的值,并且在分配期間通過使用查找表來進行分配是明智的。查找表是簡單的數(shù)組(具有一個或多個維),對于給定的輸入值變量保存最終的輸出值。其實力在于我們不需要進行計算,只需要讀取結(jié)果。

我們的測試用例程序(以及此處提供的示例)將執(zhí)行以下操作:讀取控制臺線路參數(shù)圖像(可以是顏色或灰度級 - 控制臺線路參數(shù)),并使用給定的控制臺行參數(shù)整數(shù)值。在OpenCV中,目前有三種主要通過像素逐個通過圖像的方法。為了使事情更有趣,將使用所有這些方法對每個圖像進行掃描,并打印出花費多長時間。

您可以在這里下載完整的源代碼,或者在OpenCV的sample目錄中查看核心部分的cpp教程代碼。其基本用途是:

how_to_scan_images imageName.jpg intValueToReduce [G]

最后一個參數(shù)是可選的。如果給定圖像將以灰度格式加載,否則使用BGR顏色空間。首先是計算查找表。

    int divideWith = 0; // convert our input string to number - C++ style
    stringstream s;
    s << argv[2];
    s >> divideWith;
    if (!s || !divideWith)
    {
        cout << "Invalid number entered for dividing. " << endl;
        return -1;
    }
    uchar table[256];
    for (int i = 0; i < 256; ++i)
       table[i] = (uchar)(divideWith * (i/divideWith));

這里我們首先使用C ++ stringstream類將第三個命令行參數(shù)從文本轉(zhuǎn)換為整數(shù)格式。然后我們使用一個簡單的外觀和上面的公式來計算查找表。沒有OpenCV具體的東西在這里。

另一個問題是我們?nèi)绾魏饬繒r間?那么OpenCV提供了兩個簡單的函數(shù)來實現(xiàn)這個cv :: getTickCount()cv :: getTickFrequency()。第一個從某個事件返回系統(tǒng)CPU的刻度數(shù)(就像您啟動系統(tǒng)一樣)。第二次返回您的CPU在一秒鐘內(nèi)發(fā)出多少次刻錄。所以為了測量秒數(shù),兩次操作之間的時間容易如下:

double t = (double)getTickCount();
// do something ...
t = ((double)getTickCount() - t)/getTickFrequency();
cout << "Times passed in seconds: " << t << endl;

圖像矩陣如何存儲在內(nèi)存中?

正如您已經(jīng)閱讀我的Mat - 基本圖像容器教程中的矩陣大小取決于使用的顏色系統(tǒng)。更準確地說,它取決于所使用的通道數(shù)量。在灰度圖像的情況下,我們有一些像:

tutorial_how_matrix_stored_1

對于多通道圖像,列包含與通道數(shù)一樣多的子列。例如在BGR顏色系統(tǒng)的情況下:

tutorial_how_matrix_stored_2

注意,通道的順序是反向的:BGR而不是RGB。因為在許多情況下,內(nèi)存足夠大以便以連續(xù)的方式存儲行,所以這些行可以一個接一個地跟隨,創(chuàng)建一個長行。因為一切都在一個地方,這可能有助于加快掃描過程。我們可以使用cv :: Mat :: isContinuous()函數(shù)來詢問矩陣是否是這種情況。繼續(xù)下一節(jié)找一個例子。

有效的方式

當涉及到性能時,你無法擊敗經(jīng)典的C風格操作符[](指針)訪問。因此,我們可以推薦使用最有效的方法進行分配:

Mat& ScanImageAndReduceC(Mat& I, const uchar* const table)
{
    // accept only char type matrices
    CV_Assert(I.depth() == CV_8U);
    int channels = I.channels();
    int nRows = I.rows;
    int nCols = I.cols * channels;
    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;
}

在這里,我們基本上只是獲取一個指向每行開頭的指針,直到它結(jié)束。在特殊情況下,矩陣以連續(xù)的方式存儲,我們只需要單次請求指針,直到最后。我們需要尋找彩色圖像:我們有三個通道,所以我們需要通過每行三次以上的項目。

還有另一種方法。Mat對象的數(shù)據(jù)數(shù)據(jù)成員返回指向第一行第一列的指針。如果此指針為空,則該對象中沒有有效的輸入。檢查這是檢查您的圖像加載是否成功的最簡單的方法。如果存儲是連續(xù)的,我們可以使用它來遍歷整個數(shù)據(jù)指針。在灰度圖像的情況下,它將如下所示:

uchar * p = I.data;
for(unsigned  int i = 0; i <ncol * nrows; ++ i)
    * p ++ = table [* p];

你會得到相同的結(jié)果。但是,這段代碼稍后閱讀很難閱讀。如果你有更先進的技術(shù),那就更難了。此外,在實踐中,我觀察到您將獲得相同的性能結(jié)果(因為大多數(shù)現(xiàn)代編譯器可能會為您自動實現(xiàn)這種小型優(yōu)化技巧)。

迭代程序(安全)方法

如果有效的方式確保您通過適量的uchar字段,并跳過行之間可能發(fā)生的差距是您的責任。迭代程序方法被認為是更安全的方式,因為它從用戶接管這些任務(wù)。所有你需要做的是要求圖像矩陣的開始和結(jié)束,然后只是增加開始迭代程序,直到你到達結(jié)束。要獲取迭代程序指向的值,使用*運算符(在它之前添加)。

Mat& ScanImageAndReduceIterator(Mat& I, const uchar* const table)
{
    // accept only char type matrices
    CV_Assert(I.depth() == CV_8U);
    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;
}

在彩色圖像的情況下,我們每列有三個uchar項目。這可能被認為是一個簡短的uchar項目向量,已經(jīng)在OpenCV中使用Vec3b名稱進行了浸禮。要訪問第n個子列,我們使用簡單的operator []訪問。重要的是要記住,OpenCV迭代程序遍歷列,并自動跳到下一行。因此,如果使用簡單的uchar迭代程序,您將只能訪問藍色通道值。

參考返回的即時地址計算

最后的方法不推薦用于掃描。它是為了獲取或修改圖像中的某種方式的隨機元素。它的基本用法是指定要訪問的項目的行號和列號。在我們早期的掃描方法中,您可以通過我們正在查看的圖像來觀察這一點很重要。這在這里沒有什么不同,因為您需要手動指定在自動查找時要使用的類型。如果下列源代碼的灰度圖像(+ cv :: at()函數(shù)的用法),您可以觀察這一點:

Mat& ScanImageAndReduceRandomAccess(Mat& I, const uchar* const table)
{
    // accept only char type matrices
    CV_Assert(I.depth() == CV_8U);
    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;
}

這些功能需要您的輸入類型和坐標,并即時計算查詢項目的地址。然后返回一個引用。當您設(shè)置值時,獲取值和非常數(shù)時,這可能是常數(shù)。作為調(diào)試模式的安全步驟*,執(zhí)行一個檢查,您的輸入坐標是有效的并且確實存在。如果不是這樣,您將在標準錯誤輸出流上獲得一個很好的輸出消息。與釋放模式中的有效方式相比,使用此方法的唯一區(qū)別是,對于圖像的每個元素,您將獲得一個新的行指針,以便我們使用C運算符[]獲取列元素。

如果您需要使用此方法對圖像執(zhí)行多次查找,則可能會麻煩和耗時地為每個訪問輸入類型和at關(guān)鍵字。為了解決這個問題OpenCV有一個cv :: Mat_數(shù)據(jù)類型。與Mat相同,在定義中需要通過查看數(shù)據(jù)矩陣來指定數(shù)據(jù)類型,但是您可以使用operator()快速訪問項目。為了使事情變得更好,這可以很容易地從和通常的cv :: Mat數(shù)據(jù)類型轉(zhuǎn)換。您可以在上方功能的彩色圖像的情況下看到此示例的用法。然而,重要的是要注意,cv :: at()可以完成相同的操作(具有相同的運行時速度功能。對于懶惰的程序員的伎倆來說,這是一個更少的事情。

核心功能

這是在圖像中實現(xiàn)查找表修改的一種獎勵方法。在圖像處理中,很常見的是要將所有給定的圖像值修改為其他值。OpenCV提供了修改圖像值的功能,無需編寫圖像的掃描邏輯。我們使用核心模塊的cv :: LUT()函數(shù)。首先我們構(gòu)建一個Mat類型的查找表:

    Mat lookUpTable(1, 256, CV_8U);
    uchar* p = lookUpTable.ptr();
    for( int i = 0; i < 256; ++i)
        p[i] = table[i];

最后調(diào)用函數(shù)(我是我們的輸入圖像,J是輸出的一個):

        LUT(I,lookUpTable,J);

性能差異

為了最好的結(jié)果,編譯程序并以自己的速度運行它。為了使差異更加清晰,我使用了相當大的(2560 X 1600)圖像。這里呈現(xiàn)的性能是彩色圖像。為了獲得更準確的值,我將從函數(shù)調(diào)用得到的值平均為100次。

方法時間
高效的方式79.4717毫秒
迭代程序83.7201毫秒
在飛行RA93.7878毫秒
LUT功能32.5759毫秒

我們可以總結(jié)一些事情。如果可能,請使用OpenCV已經(jīng)創(chuàng)建的功能(而不是重新創(chuàng)建它們)。最快的方法是LUT功能。這是因為OpenCV庫通過Intel Threaded Building Blocks啟用多線程。但是,如果你需要編寫一個簡單的圖像掃描,喜歡指針方法。迭代程序程序

是一個更安全的賭注,但是相當慢。使用即時參考訪問方法進行全圖像掃描是調(diào)試模式中最昂貴的。在釋放模式下,它可能會擊敗迭代程序方法,但是它肯定會犧牲迭代程序的安全性能。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號