一、前言
上一篇我們簡單講解了OpenCV的概念和基礎(chǔ)架構(gòu)。本篇主要向大家介紹下圖像處理中一個比較重要的概念 -- 掩膜操作。開始前我們先看下利用矩陣掩膜操作來加強圖像對比度的效果。

二、開胃菜-Mat對象

我們用眼睛看到的是圖像,而計算機卻不認(rèn)識。于是,人們使用數(shù)值的形式來記錄圖像,比如用RGB值記錄圖像的每個點,以此來表示圖像。就如上圖,我們看到的是一輛車,而計算機“看到”的是一個包含圖像值的矩陣。OpenCV的Mat對象對應(yīng)的就是矩陣。Mat提供了許多便捷的API來創(chuàng)建、操作矩陣。
Mat基礎(chǔ)操作
Mat image = Mat(240, 320, CV_8UC3);
第一個參數(shù)是rows,該矩陣的行數(shù);第二個參數(shù)是cols,該矩陣的列數(shù);第三個參數(shù)是該矩陣元素的類型。這句話表示創(chuàng)建一個大小為240×320的矩陣,里面的元素為8位unsigned型,通道數(shù)(channel)有3個。
image.create(480, 640, CV_8UC3);
分配(或重新分配)image矩陣,把大小設(shè)為480×640,類型設(shè)為CV8UC3。
Mat image = Mat(3, 3, CV_32F, Scalar(5));
定義并初始化一個3×3的32bit浮點數(shù)矩陣,每個元素都設(shè)為5。
uchar* ptr = image.ptr(row);
指針操作,表示拿到image第row行的指針
uchar* output = image.ptr(row);
output[1] = value;
利用指針修改圖像,表示修改image第row行的第2個數(shù)據(jù)為value。
Mat常用成員介紹
1、data
Mat對象中的一個指針,指向存放矩陣數(shù)據(jù)的內(nèi)存(uchar* data)
2、dims
矩陣的維度,34的矩陣維度為2維,34*5的矩陣維度為3維
3、channels
矩陣通道,矩陣中的每一個矩陣元素擁有的值的個數(shù),比如說 3 * 4 矩陣中一共 12 個元素,如果每個元素有三個值,那么就說這個矩陣是 3 通道的,即 channels = 3。常見的是一張彩色圖片有紅、綠、藍三個通道。
4、rows
矩陣的行數(shù)
5、cols
矩陣的列數(shù)
Mat與IplImage
OpenCV1使用基于C接口定義的圖像存儲格式IplImage存儲圖像。IplImage直接暴露內(nèi)存,如果忘記釋放內(nèi)存,就會造成內(nèi)存泄漏。
從OpenCV2開始,開始使用Mat類存儲圖像,具有以下優(yōu)勢:
圖像的內(nèi)存分配和釋放由Mat類自動管理
Mat類由兩部分?jǐn)?shù)據(jù)組成:矩陣頭(包含矩陣尺寸、存儲方法、存儲地址等)和一個指向存儲所有像素值的矩陣(根據(jù)所選存儲方法的不同,矩陣可以是不同的維數(shù))的指針。Mat在進行賦值和拷貝時,只復(fù)制矩陣頭,而不復(fù)制矩陣,提高效率。如果矩陣屬于多個Mat對象,則通過引用計數(shù)來判斷,當(dāng)最后一個使用它的對象,則負責(zé)釋放矩陣。
可以使用clone和copyTo函數(shù),不僅復(fù)制矩陣頭還復(fù)制矩陣。
三、掩膜操作
數(shù)字圖像處理中的掩膜的概念是借鑒于PCB制版的過程,在半導(dǎo)體制造中,許多芯片工藝步驟采用光刻技術(shù),用于這些步驟的圖形“底片”稱為掩膜(也稱作“掩模”),其作用是:在硅片上選定的區(qū)域中對一個不透明的圖形模板遮蓋,繼而下面的腐蝕或擴散將只影響選定的區(qū)域以外的區(qū)域。
圖像掩膜與其類似,用選定的圖像、圖形或物體,對處理的圖像(全部或局部)進行遮擋,來控制圖像處理的區(qū)域或處理過程。
光學(xué)圖像處理中,掩??梢允悄z片、濾光片等。數(shù)字圖像處理中,掩模為二維矩陣數(shù)組,有時也用多值圖像。
是不是概念看得一頭霧水,沒事的,我第一次看概念的也是一樣樣的。下面我以例子來輔助大家了解掩膜。
摳下鎧的頭

接下來我們以代碼角度分析下究竟什么是掩膜。
// image為鎧的圖片
Mat src;
UIImageToMat(image, src);
Mat mask = Mat::zeros(src.size(), CV_8UC1);
Rect2i r = Rect2i(120, 80, 100, 100);
mask(r).setTo(255);
Mat dst;
src.copyTo(dst, mask);
第一步建立與原圖一樣大小的mask圖像,并將所有像素初始化為0,因此全圖成了一張全黑色圖。
第二步將mask圖中的r區(qū)域的所有像素值設(shè)置為255,也就是整個r區(qū)域變成了白色。
Mat mask = Mat::zeros(src.size(), CV_8UC1);
mask(r).setTo(255);

使用mask將原始圖src拷貝到目的圖dst上。
src.copyTo(dst, mask);
這個拷貝的動作完整版本是這樣的:
原圖(src)與掩膜(mask)進行與運算后得到了結(jié)果圖(dst)。
其實就是原圖中的每個像素和掩膜中的每個對應(yīng)像素進行與運算。比如1 & 1 = 1;1 & 0 = 0;
比如一個3 * 3的圖像與3 * 3的掩膜進行運算,得到的結(jié)果圖像就是:

所以,mask就是位圖,來過濾像素。如果mask像素的值是非0的,我就保留,否則就丟棄。
因為我們上面得到的mask中,感興趣的區(qū)域是白色的,表明感興趣區(qū)域的像素都是非0,而非感興趣區(qū)域都是黑色,表明那些區(qū)域的像素都是0。一旦原圖與mask圖進行與運算后,得到的結(jié)果圖只留下原始圖感興趣區(qū)域的圖像了。也正剩下鎧的頭部了。
增強對比度
矩陣的掩膜操作就是根據(jù)掩膜來重新計算每個像素的像素值,掩膜(mask)也被稱為kernel。
通過掩膜操作實現(xiàn)圖像對比度提高的公式如下。
I(i,j) = 5*I(i,j) - [I(i-1,j) + I(i+1,j) + I(i,j-1) + I(i,j+1)]
注:這里看不懂不要緊,先看具體的實現(xiàn),回頭我們再一起回顧這里。
上面的公式,轉(zhuǎn)換成矩陣就如下圖所示

紅色是中心像素,從上到下,從左到右對每個像素做同樣的處理操作,具體過程如下圖,深灰色底表示原圖像,每次移動kernel便根據(jù)公司計算新值并更新矩陣。最終得到的結(jié)果就是對比度提高之后的輸出圖像。

具體的代碼如下:
// image為鎧的圖片
Mat src;
UIImageToMat(image, src);
int cols = (src.cols-1) * src.channels();
int offset = src.channels();
int rows = src.rows;
Mat dst = Mat(src.size(), src.type());
for (int row = 1; row < rows-1; row++) {
uchar* previous = src.ptr(row-1);
uchar* current = src.ptr(row);
uchar* next = src.ptr(row+1);
uchar* output = dst.ptr(row);
for (int col = offset; col < cols; col++) {
output[col] = saturate_cast<uchar>(5*current[col] - (current[col-offset] + current[col +offset] + previous[col] + next[col]));
}
}
/*
注:
saturate_cast<uchar>(-100),返回0
saturate_cast<uchar>(288),返回255
saturate_cast<uchar>(100),返回100
這個函數(shù)的功能是確保RGB值范圍在0~255之間。
*/
效果

接下來我們來回顧下上面的那個公式
I(i,j) = 5*I(i,j) - [I(i-1,j) + I(i+1,j) + I(i,j-1) + I(i,j+1)]
其實這個公式就是5倍的中心像素減去周邊的四個像素之和。
我們舉兩個例子來看下這個公式的結(jié)果。


我們可以大致看到若是中心點的值大于周圍,則計算后的結(jié)果會將中心點與周圍的值差距拉得更大;
若是中心點的值小于周圍,則計算后的結(jié)果也會將中心點與周圍的值差距拉大。這樣“大的大,小的小”結(jié)果不就是對比明顯了嗎,也就是提高了對比度。
大家會發(fā)現(xiàn)這樣做掩膜操作也太麻煩了。這個時候我們就找OpenCV來幫個忙,看看它是怎么實現(xiàn)的。
Mat src;
UIImageToMat(image, src);
Mat dst;
Mat kernel = (Mat_<char>(3, 3) << 0, -1, 0, -1, 5, -1, 0, -1, 0);
filter2D(src, dst, src.depth(), kernel);
一個filter2D搞定!定義如下:
void filter2D( InputArray src, OutputArray dst, int ddepth,
InputArray kernel, Point anchor=Point(-1,-1),
double delta=0, int borderType=BORDER_DEFAULT );
其中src與dst是Mat類型變量、depth表示位圖深度,有32、24、8等。
四、小結(jié)
本篇主要介紹了Mat對象的基本用法,并通過兩個例子講解了掩膜操作的原理和實現(xiàn)。下一篇還是會以這樣的形式講解OpenCV的其他知識,有更好建議的朋友可以給我留言,see you later!