在多數的圖像處理任務,爲了執行一個計算任務,需要遍歷圖像的所有像素.考慮到大量的像素數據需要被訪問,用一個有效率的方法去做這個事情是很有必要的.本節和下一節會用不同的方式展示如何用循環遍歷圖像.本節使用指針的方法.
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();
但是,雖然這個是可以在我們的例子中使用,但是這種方式是不推薦的.除了易於出錯,這個方法不能在感興趣的區域使用.感興趣的區域將在本章最後討論.