[OpenCV2]使用指針遍歷圖像

在多數的圖像處理任務,爲了執行一個計算任務,需要遍歷圖像的所有像素.考慮到大量的像素數據需要被訪問,用一個有效率的方法去做這個事情是很有必要的.本節和下一節會用不同的方式展示如何用循環遍歷圖像.本節使用指針的方法.

Getting ready

我們會用一個簡單的任務舉例如何遍歷圖像:減少一幅圖像的顏色數.

彩色圖像是由三個通道的像素組成的.每個通道的亮度值分別對應三原色(紅綠藍).因爲這些值是8位unsigned char類型的,總共的顏色數爲256×256×256,總共超過了一千六百萬種顏色.因此,爲了減少分析圖像的複雜性,有時減少圖像的顏色數是有用的.一個簡單的方法是把RGB顏色空間再分成相等大小的空間.例如,如果圖像顏色減少爲每8個像素取一個像素值,你最終只會得到32×32×32種顏色.原始圖像中的每一種顏色被減少後的圖像新的顏色值代替,這個新的顏色值是,它原來屬於小的顏色空間的中間值.

因此,這個基本的顏色縮減算法是很簡單的.如果N是縮減因子,然後對圖像中的每個像素,然後這個像素的每個通道的值除以N(整除,因此需要注意有數據丟失).然後把這個結果乘以N,最後的結果會略小於輸入的像素值.然後再加加上N/2,會得到在兩個與N相乘相鄰區間的中間值.對於每個8位通道重複這個處理,你會得到總共256/N×256/N×256/N可能的顏色值.

How to do it ...

我們用下面的顏色縮減函數實現:

void colorReduce(cv::Mat &image, int div=64);
爲用戶提供了一個圖像和每通道的縮減係數.在這裏,這個功能是in-place的,即圖像的輸入和處理返回的結果都是一個參數.參見There's more...瞭解一些通用的輸入和輸出參數的表示方法.

這個處理方法是通過一個二重循環遍歷所有的像素值:

void colorReduce(cv::Mat &image, int div=64) {
     int nl= image.rows; // number of lines
     // total number of elements per line
     int nc= image.cols * image.channels(); 
              
     for (int j=0; j<nl; j++) {
        // get the address of row j
        uchar* data= image.ptr<uchar>(j);
        for (int i=0; i<nc; i++) {
            // process each pixel ---------------------                 
                  data[i]=    data[i]/div*div + div/2;
            // end of pixel processing ----------------
        } // end of line 
     }
}
這個功能可以使用下面的代碼片段測試

 // read the image
 image= cv::imread("boldt.jpg");
 // process the image
 colorReduce(image);
 // display the image
 cv::namedWindow("Image");
 cv::imshow("Image",image);
我們會得到如下圖像(注意觀察圖像的顏色):

How it works...

在一幅彩色圖像中,第一個三字節的圖像緩衝區給出了左上角像素的3個顏色通道值,下一個三字節表示第一行第二個像素的值.如此類推(注意,在OpenCV中,通道默認順序是BGR,所以藍色是第一個通道).一幅圖像的寬(W)和高(H),會需要佔用W×H×3 uchars的內存塊大小.然而,由於效率的原因,行的長度會被一些額外的像素填充.這是因爲一些多媒體處理芯片(如Intel MMX構架)在圖像的寬是4或者是8的整倍數時效率更高.很顯然,這些多出的像素並不顯示和保存,他們的值是被忽略的.OpenCV定義這個不定的寬度爲一個關鍵字.顯然,如果圖像如果沒有被額外的像素填充時,圖像的寬度和真實寬度是一致的.屬性cols給你圖像的寬(列數),屬性rows給出圖像的高.step屬性給出有效的寬度(用字節數表示).甚至你的圖像數據類型爲除unchar其他的類型,step仍然給出一行的字節數.像素大小通過elemSize方法給出(例如,對於一個3通道的短整型矩陣(CV_16SC3),elemSize會返回6).圖像的通道數會通過nchannels方法返回(灰度圖像爲1,彩色圖像爲3).最後,在一個矩陣中total方法會返回像素總數(圖像也是一個矩陣).

每行真實的像素數給出如下:

int nc= image.cols * image.channels(); 
爲了簡化指針的運算,cv::Mat類提供了一個方法可以獲取每一行的地址.這就是ptr方法.這是一個模版方法會返回第j行的指針

uchar* data= image.ptr<uchar>(j);

注意,在處理語句,我們能使用指針進行一列一列移動的運算.所以我們可以這樣寫:

  *data++= *data/div*div + div2;

There's more ...

這個顏色縮減的功能僅僅通過一個方法實現.我們有其他的方法可以實現同樣的功能.圖像功能函數一般允許設置不同的輸入,輸出圖像.如果考慮到圖像數據的連續性,遍歷圖像循環將會更有效率.最終,我們可以使用低等級的指針遍歷圖像數據.以下我們會討論以上內容.

Other color reduction Formulas

在我們的例子中,顏色的減少是通過利用整數的除法分層,將結果分爲最近的整數層中.

data[i]= data[i]/div*div + div/2;
這個減少顏色的功能還可以使用取模運算,放回div(1維的減少因子)最近的倍數:

data[i]=    data[i] – data[i]%div + div/2;
但是這個計算會有一點慢,因爲它需要訪問每個像素兩次.

還有一種使用按位運算的方法.如果我們限制換算係數爲2的冪,div = pow(2,n),然後掩蓋像素第一個n bits的值,會得到div最低的倍數.這個掩蓋操作是一個簡單的位運算實現的:

     // mask used to round the pixel value
     uchar mask= 0xFF<<n; // e.g. for div=16, mask= 0xF0
這個顏色縮減功能將由下式給出:

data[i]=    (data[i]&mask) + div/2;
通常,按位操作是非常有效率的,當要求效率的時候,按位操作是很有用的.

Having input and output arguments

在我們的例子中,圖像的轉化直接就修改了輸入圖像,我們稱爲in-place方式.使用這種方式,我們不需要額外的圖像接受輸出結果,直接在內存上進行操作.但是,在一些應用中,用戶希望能夠完整的保存原始圖像.這樣,用戶首先就必須先創建原始圖像的副本,然後再進行處理.注意,完全創建一幅圖像的副本,使用clone方法是最簡單的,例如:

// read the image
   image= cv::imread("boldt.jpg");
   // clone the image
   cv::Mat imageClone= image.clone();
   // process the clone
   // orginal image remains untouched
   colorReduce(imageClone);
   // display the image result
   cv::namedWindow("Image Result");
   cv::imshow("Image Result",imageClone);
通過定義了一個額外的重載函數,讓用戶選擇是否使用in-place的處理方法.函數的參數如下:

void colorReduce(const cv::Mat &image, // input image 
                 cv::Mat &result,      // output image
                 int div=64);
注意 這個輸入圖像現在被定義爲const類型,這意味着這個圖像不會被函數修改.當需要in-place處理時,可以把輸入和輸出指定爲同一個圖像.

colorReduce(image,image);
如何不需要,提供一個cv::Mat的對象,例如:

cv::Mat result;   
colorReduce(image,result);
這裏的關鍵是,首先驗證輸入和輸出圖像是否分配了相同的數據空間,大小和像素類型是否匹配.爲了方便檢測在函數內部使用cv::Mat的creat方法創建一個矩陣.使用這個方法時,需要重新指定大小和類型.如果這個矩陣已經分配了大小和類型,這個方法不會執行分配操作,該方法僅僅會返回實例.因此,我們的函數,需要首先調用create方法去創建一個和輸入圖像相同大小和類型矩陣(如果必須的話):

result.create(image.rows,image.cols,image.type());
注意:create總是創建一個連續的圖像,這是一個沒有任何填充的圖像.內存塊被分配爲total()*elemSize()的大小.這個循環使用了兩個指針:

for (int j=0; j<nl; j++) {
        // get the addresses of input and output row j
        const uchar* data_in= image.ptr<uchar>(j);
        uchar* data_out= result.ptr<uchar>(j);
        for (int i=0; i<nc; i++) {
            // process each pixel ---------------------                 
                  data_out[i]= data_in[i]/div*div + div/2;
            // end of pixel processing ----------------        } // end of line 
當輸入和輸出爲同一個圖像時,這個功能函數會和第一個提出的方法完全等價.如果輸出爲另一個圖像,這個函數將被正確的執行,無論這個圖像在函數調用之前是否被分配.

Efficient scanning of contionus images

我們先前的解釋,由於效率的原因,每一行的最後可能被額外的像素填充.然而,當圖像沒有被額外像素填充時,這個圖像可以被看成是一個總共有W×H個像素的一維數組.cv::Mat的方法可以告訴我們圖像是否被填充.如果 isContinuous 方法返回true說明沒有包含填充像素.

在一些具體的處理算法中,通過一個循環(或更多)處理圖像時,利用圖像數據的連續性具有優勢.我們的處理函數可以寫爲如下形式:

void colorReduce(cv::Mat &image, int div=64) {
     int nl= image.rows; // number of lines
     int nc= image.cols * image.channels(); 
     if (image.isContinuous()) 
     {
        // then no padded pixels
        nc= nc*nl; 
        nl= 1;  // it is now a 1D array
     }
     // this loop is executed only once
     // in case of continuous images
     for (int j=0; j<nl; j++) { 
     uchar* data= image.ptr<uchar>(j);
          for (int i=0; i<nc; i++) {
            // process each pixel ---------------------                
                  data[i]= data[i]/div*div + div/2;
            // end of pixel processing ----------------          } // end of line                   
     }
}
現在,首先測試圖像是否是連續的.如果圖像不包含填充像素,我們通過把寬爲1.把高設置爲W×H.消除外部循環.注意 ,有一個reshape的方法在這裏可以被使用.如下:

if (image.isContinuous()) 
{
   // no padded pixels
   image.reshape(1,            // new number of channels
      image.cols*image.rows) ; // new number of rows
}
int nl= image.rows; // number of lines
int nc= image.cols * image.channels(); 
這個reshape方法在沒有任何內存複製和重新分配的情況下改變了矩陣的維度.第一參數是新的通道數,第二個參數是新的行數.列數自動進行重新分配了.

在這些實現中,內層循環按順序處理所有圖像像素.這中方法的主要優勢是:當幾個小圖像同時掃描到相同的循環中.

Low-level pointer arithmetics

在cv::Mat類中,圖像數據包含了一個個unsigned char類型的內存塊.第一個內存元素的地址返回一個unchar類型的指針.所以,在你圖像循環開始,應當這樣寫:

uchar *data= image.data;

這個step方法可以給出一行總共的字節數(包含填充的數據).通常,你會包含行爲j 列爲 i 像素的地址:

// address of pixel at (j,i) that is &image.at(j,i)     
data= image.data+j*image.step+i*image.elemSize();  
但是,雖然這個是可以在我們的例子中使用,但是這種方式是不推薦的.除了易於出錯,這個方法不能在感興趣的區域使用.感興趣的區域將在本章最後討論.














發佈了57 篇原創文章 · 獲贊 95 · 訪問量 21萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章