1. 程式人生 > >OpenCV4Android開發實錄(3):數字影象基礎與OpenCV開發入門

OpenCV4Android開發實錄(3):數字影象基礎與OpenCV開發入門

俗話說:“工欲善其事,必先利其器”。數字影象處理作為專業性比較強的一門學科,也是計算機視覺的基礎課程,在開始學習OpenCV處理數字影象之前,我覺得對數字影象有一定的瞭解還是非常有必要的,尤其是對於沒有任何數字影象基礎的人。擁有紮實的數字影象基礎,將是深入研究OpenCV框架的一把利器!

1. 數字影象基礎

數字影象,又稱數點陣圖像(點陣圖),由模擬影象數字化得到,它以畫素為基本元素,是二維影象用有限畫素的表示。通俗的說,數字影象在計算機中是以數值矩陣的形式存在和處理,矩陣的每一個元素即為畫素。下圖表示一副黑白影象所對應的矩陣,每一個小方塊代表黑白影象的一個畫素,在矩陣中值為0或1。

image

1.1 畫素與灰度級

 畫素是組成數字影象最基本元素,是數字影象顯示的基本單位,它是一個邏輯尺度單位,比如一張640x480大小圖片,它由橫向640個畫素和縱向480個畫素組成。每個畫素都擁有數字值,不同的影象種類畫素值的範圍有所區別,比如二值影象(黑白影象)畫素數值只能為0或1、灰度影象畫素數值為0~255。下圖表示圖片中每一個正方形為一個畫素:
畫素
 除了畫素,在實際開發中,我們可能還會碰到諸如影象的灰度、灰度值、灰度級和灰度級數等概念,它們也算是數字影象處理的重要基礎。所謂影象的灰度、灰度值或灰度級實質是同一個概念,為表示一個畫素明暗程度的整數,範圍為0~255,其中,純白為255,純黑為0,中間值為介於純黑和純白之間的灰色。由於一副數字影象由若干個畫素組成,而每個畫素擁有自己的灰度值(注:RGB彩色影象只有R=G=B,畫素的值才能稱為灰度值,否則為顏色值),因此一副影象擁有若干不同的灰度級(灰度值),這些灰度級集中起來便成為該影象的灰度級數。灰度級數代表一副數字影象的層次,影象資料的層次越多,視覺效果就越好。下圖為一副灰度影象及其灰度值(畫素值)表示:
image

1.2 解析度

 通常,解析度可分為螢幕解析度、影象解析度、視訊解析度,它們有一個共同特徵,即包含的畫素數量越多,影象畫面的顯示就越清晰、細膩,主要區別如下:

  • 螢幕解析度

 螢幕解析度就是計算機顯示器或移動裝置的螢幕上能顯示畫素的個數,通常以水平和垂直畫素數量來衡量。這裡以華為P20 Pro為例,它的主屏解析度為2240x1080畫素,表示該機型螢幕水平方向上能夠顯示2240個畫素,垂直方向能夠顯示1080個畫素,總共2419200個畫素(>200萬),可以說華為P20 Pro螢幕解析度是相當高的,當然螢幕的顯示也就非常清晰。

  • 影象解析度

 影象解析度就是影象的尺寸,也成為影象的寬和高,即影象水平方向上的畫素個數和垂直方向上的畫素個數。比如一張解析度為640×480畫素的圖片,有480行畫素資訊,每行有640個畫素,該圖片包含的畫素個數是307 200,也就是我們常說的30萬畫素。而一個解析度為1600×1200畫素的圖片,就達到了約200萬的畫素。

  • 視訊解析度

 現實生活中,對於視訊解析度我們習慣性用標清、高清、超清或者480P、720P、1080P來形容,這裡所謂的P指的是逐行掃描,480P是表示的是視訊中畫面的解析度為640x480畫素,也就是說,視訊中每一副畫面由水平方向上的640個畫素和垂直方向上的480個畫素組成,每掃描一行就會遍歷480個畫素。

1.3 顏色空間

 根據數字影象色彩表示方式的不同,數字影象有幾種不同的顏色空間(顏色模型),如RGB顏色空間、HSV顏色空間、YUV顏色空間等。下面就具體介紹一下這幾種顏色空間:

  • RGB

 RGB色彩模式是工業界的一種顏色標準,是通過對紅(R)、綠(G)、藍(B)三個顏色通道的變化以及它們相互之間的疊加來得到各式各樣的顏色的,RGB即是代表紅、綠、藍三個通道的顏色,這個標準幾乎包括了人類視力所能感知的所有顏色,是目前運用最廣的顏色系統之一。RGB色彩模式使用RGB模型為影象中每一個畫素的RGB分量各佔8bits,且分配一個0~255範圍內的強度值。例如:純紅色R值為255,G值為0,B值為0;灰色的R、G、B三個值相等(除了0和255);白色的R、G、B都為255;黑色的R、G、B都為0。RGB影象只使用三種顏色,就可以使它們按照不同的比例混合,在螢幕上重現16777216(2^24)種顏色。

  • YUV

YUV顏色空間是歐洲電視系統使用的一種色彩編碼空間,在現代的彩色電視系統中,通過三管彩色攝影機和彩色CCD攝影機得到的彩色影象訊號,經過分色、分別放大校正得到RGB,再進一步經矩陣變換電路得到亮度訊號Y和兩個色差訊號U、V,最後傳送端對這三個訊號分別編碼,再使用同一通道發出去,這就是YUV顏色空間。

image     image

 彩色電視採用YUV空間正是為了用亮度訊號Y解決彩色電視機與黑白電視機的相容問題,使黑白電視機也能接收彩色電視訊號。YUV主要用於優化彩色視訊訊號的傳輸,使其向後相容老式黑白電視。與RGB視訊訊號傳輸相比,它最大的優點在於只需佔用極少的頻寬(RGB要求三個獨立的視訊訊號同時傳輸)。其中“Y”表示明亮度(Luminance或Luma),也就是灰階值;而“U”和“V”表示的則是色度(Chrominance或Chroma),作用是描述影像色彩及飽和度,用於指定畫素的顏色。“亮度”是透過RGB輸入訊號來建立的,方法是將RGB訊號的特定部分疊加到一起。“色度”則定義了顏色的兩個方面─色調與飽和度,分別用Cr和Cb來表示。其中,Cr反映了RGB輸入訊號紅色部分與RGB訊號亮度值之間的差異;而Cb反映的是RGB輸入訊號藍色部分與RGB訊號亮度值之同的差異。採用YUV色彩空間的重要性是它的亮度訊號Y和色度訊號U、V是分離的。如果只有Y訊號分量而沒有U、V分量,那麼這樣表示的影象就是黑白灰度影象。彩色電視採用YUV空間正是為了用亮度訊號Y解決彩色電視機與黑白電視機的相容問題,使黑白電視機也能接收彩色電視訊號。YUV相關色彩模型與RGB的轉換方程如下:

YUV和YCbCr區別:YCbCr是DVD、攝像機、數字電視等產品中常用的色彩編碼方案,它是YUV經過縮放和偏移的進化種類,由亮度分量Y、藍色色度分量Cb和紅色色度分量Cr組成,其中Y與YUV中的Y含義相同,Cb、Cr與YUV中的U、V均指色彩,只是表示方法上不同。在計算機系統中應用廣泛,JPEG、MPEG均採用此格式,並且我們所說的YUV顏色格式大多指YCbCr。RGB到YCbCr的轉換公式如下:
image

  • HSV

 HSV顏色空間由Hue(色調)、Saturation(飽和度)和Value(亮度)組成,與RGB類似,HSV顏色空間中也有三個分量:H、S、V。HSV顏色空間是1978年提出的,它是一種主觀的顏色空間。色調通常指代顏色名稱。飽和度表示摻入白光的分量,摻入白光的分量越多,則飽和度越低,即S值越小;摻入白光的分量越少,則飽和度越高,即S值越大。亮度表示摻入黑光的分量,摻入黑光的分量越多,則亮度越低,即V越小;摻入黑光的分量越少,則亮度越高,即V越大。HSV顏色空間模型:

image

1.4 影象種類

 根據影象能呈現的色彩和灰度等級,我們可以將任何影象(物理的和數字的)影象分為彩色影象、灰度影象和二值影象。它們的區別如下:

  • 二值影象

 二值影象是指影象中的畫素在只佔1bit(位),每個畫素值只能是0或1,即純黑色和純白色,中間沒有過渡色。因此,二值影象又稱為黑白影象,灰度級數為2。

image

  • 灰度影象

 灰度影象是指灰度級數大於2的影象,但它不包含彩色資訊。影象中的畫素通常佔8bits(1位元組),每個畫素值範圍為0~255,除純黑色和純白色外,中間還存在不同程度的灰色。

image

  • 彩色影象

 彩色影象是指影象中含有色彩資訊的影象,在數字影象中,每一個畫素都有相應的數值來表示該畫素的資訊,彩色影象的資訊就是顏色資訊。根據三基色原理,任何顏色都可以表示為三個基本顏色紅、綠、藍(RGB)按不同比例合成產生。此外,根據數字影象色彩的表示方式不同,彩色影象可由RGB、YUV、HSV等顏色空間來描述。

三基色原理:自然界所有顏色(色彩)均可通過紅、綠、藍三色按照不同的比例合成產生,同樣絕大多數單色光也可以分解成紅綠藍三種色光,因此,紅、綠、藍三色被稱為三基色。
image

1.5 數字影象基本屬性

 在瞭解影象相關屬性之前,個人覺得很有必要先搞清楚什麼是單色光、複合光、色彩和顏色。在三原色中,可見光的波長為380~780nm(納米),不同波長呈現出不同的顏色,可見波長從長到短依次為紅、橙、黃、綠、青、藍、紫。紅色的波長為700nm、綠色的波長為546.1nm、藍色的波長為435.8nm。只有單一波長成分的光稱為單色光,含有兩種以上波長成分的光稱為複合光色彩通常都是由一種單色光和白光按照一定比例混色的,色彩的三要素:色相、明度、純度。其中,色相又稱顏色,由亮度和色度共同表示,是色彩的首要特徵和區別各種不同色彩最準確的標準;明度指色彩的亮度;純度指色彩的鮮豔程度,即飽和度。

  • 色彩深度

色彩深度/顏色深度計算機圖形學領域表示在點陣圖或者視訊幀緩衝區中儲存1畫素的顏色所用的位數,它也稱為位/畫素(bpp)。色彩深度越高,可用的顏色就越多,通常儲存每個畫素所用的位數n與可選擇的顏色數量關係可表示為:顏色數量=2^n。比如二值影象中儲存每個畫素佔1位,即顏色深度為1,可選顏色為2^1=2種(黑色、白色);灰度影象中儲存每個畫素佔8位,即顏色深度為8,可選顏色為2^8=256種;真彩色影象(RGB顏色格式)中儲存每個佔24位,即顏色深度為24,可選顏色為2^24=16777216種。通常,影象的色彩深度越深,每個畫素儲存佔用空間越大,圖片就越大。

  • 影象亮度

 亮度指照射在景物或影象上光線的明暗程度。影象亮度增加時,就會顯得耀眼或刺眼,亮度越小時,影象就會顯得灰暗。如果是灰度影象,則亮度跟灰度值有關,灰度值越高則影象越亮;如果是彩色影象(RGB格式),則亮度跟R、G、B分量值有關,假設亮度為Y:Y=0.299R+0.587G+0.114B,人的主觀感受是綠光最亮,紅光其次,藍光最弱。 下圖展示的是不同亮度顯示效果:

這裡寫圖片描述

  • 影象對比度

影象對比度所指的就是一幅影象當中明暗區域最亮的白和最暗的黑之間不同亮度層級的測量,差異範圍越大代表對比度越高,差異範圍越小代表對比度數值越小,好的對比率120:1就可容易地顯示生動、豐富的色彩,當對比率高達300:1時,便可支援各階的顏色。從視覺角度上來看,對比度越大影象就越清晰醒目,從而色彩也會更加鮮明豔麗。反之,對比度越小,則會讓人感到畫面灰濛濛一片,色彩丟失也會很嚴重。下圖展示的是低、高對比度顯示的效果:

這裡寫圖片描述

  • 影象色調

色調是指色調是指色彩外觀的基本傾向。在明度、純度、色相這三個要素中,某種因素起主導作有用,可以稱之為某種色調。色調各種影象色彩模式下原色的明暗程度,級別範圍從0到255,共256級色調。例如對灰度影象,當色調級別為255時,就是白色,當級別為0時,就是黑色,中間是各種程度不同的灰色。在RGB模式中,色調代表紅、綠、藍三種原色的明暗程度,對綠色就有淡綠、淺綠、深綠等不同的色調。

這裡寫圖片描述

  • 影象飽和度

 飽和度指彩色影象顏色的濃度,或色彩的純度。飽和度越高,顏色越飽滿,表現顏色種類越多,顏色就越鮮明;飽和度越低,表現則較黯淡,當飽和度為0時,影象表現為灰度影象。
這裡寫圖片描述

色調和飽和度合稱為色度,色度表示的是色彩的純度,用來反映顏色的色調和飽和度。

  • 影象清晰度

 影象清晰度,是指影像上各細部影紋及其邊界的清晰程度。
這裡寫圖片描述

2. OpenCV資料結構

2.1 基礎影象容器Mat

  在OpenCV2.0之前,OpenCV主要使用IpImage*在記憶體中儲存影象,但由於IplImage基於C語言介面實現,這就需要我們在release之前手動釋放開闢的記憶體,否則非常容易造成記憶體洩漏。為了儘可能避免這種記憶體洩漏情況,基於C++實現的Mat類便”閃亮登場”。Mat,即矩陣Matrix的縮寫,它被引入於OpenCV2.0,是OpenCV在影象處理中儲存影象最基本的資料結構,也可以稱作是儲存影象的容器。Mat類儲存影象最大的優勢就是記憶體的自動管理,即我們無需再去手動為其開闢和釋放空間,並且當傳遞一個已經存在的Mat物件時開闢好的矩陣空間會被重用

 (1) Mat結構

 Mat資料結構由兩部分組成:矩陣頭(Header)和一個指標(Pointer)。一個指向記憶體中儲存所有畫素值的矩形的指標,其中,Header包含矩陣的大小、儲存方法和儲存地址等資訊;Pointer指向儲存在記憶體中所有畫素值的矩陣。Mat類部分原始碼如下:

class CV_EXPORTS Mat
{
public:
   // 構造方法
   Mat();
   Mat(int rows, int cols, int type);
   Mat(int rows, int cols, int type, const Scalar& s);
   ....
   // 成員方法
   Mat row(int y) const;
   Mat col(int x) const;
   Mat clone() const;
   void copyTo( OutputArray m ) const;
   static MatExpr zeros(int rows, int cols, int type);
   static MatExpr eye(int rows, int cols, int type);
   static MatExpr ones(int rows, int cols, int type);
   void create(int rows, int cols, int type);
   int depth() const;
   int channels() const;
   ....
   // 成員變數
   /*! includes several bit-fields:
         - the magic signature
         - continuity flag
         - depth
         - number of channels
    */
    int flags;
    //! the matrix dimensionality, >= 2
    int dims;
    //! the number of rows and columns 
    //or (-1, -1) when the matrix has more than 2 dimensions
    int rows, cols;
    //! pointer to the data
    uchar* data;
    ....
};

 關於影象副本在函式中的傳遞,大的開銷通常是由直接拷貝(複製)影象矩陣資料所致,這對於程式的執行效能是非常不利的。為了解決該問題,OpenCV使用引用計數機制讓每個Mat物件有自己的資訊頭,同時共享一個矩陣。也就是說,影象在函式傳遞中,只是傳遞原有Mat物件的資訊頭和矩陣指標而不是複製矩陣,讓所有Mat物件的指標指向同一地址的矩陣。常見的實現方法:

// 僅建立資訊頭部分
Mat A,C;    
// 載入圖片到記憶體,系統為該矩陣開闢一段記憶體空間
A = imread("helloOpecv.png",CV_LOAD_IMAGE_COLOR);  
// 使用構造方法拷貝A的Header和Pointer
Mat B(A);
// 使用賦值方式拷貝A的Header和Pointer
C = A;

// 以下兩種方式會建立只引用部分資料的資訊頭
// 使用矩形界定拷貝A的ROI(感興趣區域)
Mat D (A,Rect(10,10,100,100));
// 使用行和列來界定
Mat E = A(Range:all() , Range(1,3));

 從上面的程式碼來看,A、B、C物件均指向同一個資料矩陣,D、E物件會建立引用部分資料的資訊頭。當然,如果你想直接複製影象矩陣本身(不只是資訊頭和矩形指標),Mat類也提供相關的方法,即clone()或者copyTo,此時修改影象副本將不影響原始Mat資訊頭所指向的矩陣。

Mat A;    
A = imread("helloOpecv.png",CV_LOAD_IMAGE_COLOR); 
Mat F = A.clone();
Mat G;
A.copyTo(G);

 (2)  Mat物件建立方法

 Mat類既是一個影象容器,也是一個通用的矩陣類,通過Mat類可以用來建立和操作多維矩陣。除了上文中使用imread方法將影象資料儲存到Mat矩陣中,Mat物件的建立方法有多種,這裡我們只介紹下開發過程中常見的幾種:

  • 使用Mat()建構函式
/** Mat(int rows, int cols, int type, const Scalar& s);
*   說明:
        rows、type為二維矩陣的行數和列數
        type為儲存元素的資料型別及每個矩陣點通道數,如CV_8UC1、CV_8UC3...
        scalar為畫素顏色值,也可理解為初始化矩陣的初始值
*/
Mat M(3,3,CV_8UC3,Scalar(255,0,255));
cout << "M=" << endl << " " << M << endl << endl

 執行結果:
這裡寫圖片描述
 儲存元素的資料型別和每個矩陣點的通道數的通用表示式為CV_[位數][帶符號與否][型別字首]C[通道數],比如CV_8UC3表示矩陣元素資料使用8位的unsigned char型,每個畫素由三個元素組成的三通道。

  • 使用Mat的Create()函式
/** void create(int rows, int cols, int type);
*   說明:
        create方法用於建立一個新的矩陣,併為該矩陣開闢一段記憶體空間。
        需要注意的是,create方法無法為矩陣設定初值,它預設值為205.
*/
Mat M(3, 3, CV_8UC3, Scalar(255, 0, 255));
M.create(2, 2, CV_8UC2);
cout << "M=" << endl << " " << M << endl << endl;

 執行結果:
這裡寫圖片描述

  • 使用Mat的zeros()、ones()、eyes()函式
/** Mat::eye(int rows, int cols, int type)
*   說明:
        建立矩陣並將矩陣初始值預設,即將row=col處元素預設為1,其他為0
    Mat::ones(int rows, int cols, int type)
    說明:
        建立矩陣並將矩陣初始值預設,即將所有元素預設為1
    Mat::zeros(int rows, int cols, int type)
    說明:
        建立矩陣並將矩陣初始值預設,即將所有元素預設為0
*/
Mat eyeM = Mat::eye(4,4,CV_64F);
Mat eyeO = Mat::ones(2, 2, CV_32F);
Mat eyeZ = Mat::zeros(4, 4, CV_8UC1);
cout << "eyeM=" << endl << " " << eyeM << endl << endl;
cout << "eyeO=" << endl << " " << eyeO << endl << endl;
cout << "eyeZ=" << endl << " " << eyeZ << endl << endl;

 執行結果
這裡寫圖片描述

  • 對小矩陣使用逗號分隔式賦值
Mat C = (Mat_<double>(3, 3) << 0, -1, 0, -1, 5, -1, 0, -1, 0);
cout << "C=" << endl << " " << C << endl << endl;

 Mat_繼承於Mat,Mat_(int _rows, int _cols)等價於Mat(_rows, _cols, DataType<_Tp>::type)

  • 為已存在的物件建立新資訊頭
// 使用clone(0)或者copyTo()為一個已存在的Mat物件建立一個新的資訊頭
Mat M = (Mat_<double>(3, 3) << 0, -1, 0, -1, 5, -1, 0, -1, 0);
Mat rowClone = M.row(1).clone();
cout << "M=" << endl << " " << M << endl << endl;
cout << "rowClone=" << endl << " " << rowClone << endl << endl;

 執行結果:
這裡寫圖片描述

2.2 OpenCV中常用資料結構

 (1) 點的表示:Point類

 Point類資料結構表示二維座標系下的點,即由其影象座標x和y指定的2維座標點。用法如下:

Point point = Point(xo,yo);  
// 或者  
// Point point;  
// point.x = xo;  
// point.y = yo;

 除此之外,常見的還有Point2f、Point3f等,分別表示浮點型2D點和3維座標點。通過檢視OpenCV原始碼,實質上Point_、Point2i、Point為相互等價的整型座標點,Point_、Point2f為互相等價的浮點型座標點。

 (2) 顏色的表示:Scalar類

 Scalar類表示的是影象畫素的顏色值,在OpenCV中被大量用於傳遞畫素值。它是一個具有4個元素的陣列,但一般第四個引數無需寫出來。對於RGB顏色格式來說,c表示紅色分量、b表示綠色分量、c表示藍色分量,它們共同決定畫素值或稱畫素顏色值。 Scalar類原始碼如下:

template<typename _Tp> class Scalar_ : public Vec<_Tp, 4>
{
public:
    //! various constructors
    Scalar_();
    Scalar_(_Tp v0, _Tp v1, _Tp v2=0, _Tp v3=0);
    Scalar_(_Tp v0);

    template<typename _Tp2, int cn>
    Scalar_(const Vec<_Tp2, cn>& v);

    //! returns a scalar with all elements set to v0
    static Scalar_<_Tp> all(_Tp v0);

    //! conversion to another data type
    template<typename T2> operator Scalar_<T2>() const;

    //! per-element product
    Scalar_<_Tp> mul(const Scalar_<_Tp>& a, double scale=1 ) const;

    // returns (v0, -v1, -v2, -v3)
    Scalar_<_Tp> conj() const;

    // returns true iff v1 == v2 == v3 == 0
    bool isReal() const;
};

typedef Scalar_<double> Scalar;

 從Scalar原始碼可知,Scalar繼承於Vec<_Tp,4>,Scalar、Scalar_<double>是等價的。Scalar提供了Scalar_(_Tp v0, _Tp v1, _Tp v2=0, _Tp v3=0)構造方法,引數v0、v1、v2、v3資料型別為double。Scalar提供了all(_Tp v0)函式用於將所有引數賦值為v0;conj()函式用於引數取反。

 (3) 尺寸的表示:Size類

 Size類表示影象的尺寸或者矩陣的大小,它的使用非常簡單,即Size(int w,int h),其中,w表示影象或矩陣的寬度,h表示影象或矩陣的高度。wSize類原始碼如下:

template<typename _Tp> class Size_
{
public:
    typedef _Tp value_type;

    //! various constructors
    Size_();
    Size_(_Tp _width, _Tp _height);
    Size_(const Size_& sz);
    Size_(const Point_<_Tp>& pt);

    Size_& operator = (const Size_& sz);
    //! the area (width*height)
    _Tp area() const;
    //! true if empty
    bool empty() const;

    //! conversion of another data type.
    template<typename _Tp2> operator Size_<_Tp2>() const;

    _Tp width, height; // the width and the height
};

typedef Size_<int> Size2i;
typedef Size_<int64> Size2l;
typedef Size_<float> Size2f;
typedef Size_<double> Size2d;
typedef Size2i Size;

 從Size類原始碼可知,Size、Size2i、Size_<int>是等價的,構造方法中的_width、_height變數資料型別為int型,除此之外,根據_width_height資料型別的不同,Szie類還有幾個變種,即Size_<int64>和Size2l為長整型、Size_<float>和Size2f為浮點型、Size_和Size2d為double型。Size類還提供了area()函式用來計算尺寸的面積;empty()函式判斷該Size區域是否為空,即是否包含影象資料;width和height成員變數用來返回影象或矩陣的寬高。

 (4) 矩形的表示:Rect類

 Rect類表示一個矩形,它有x, y, width, height幾個成員變數,其中,x,y表示矩形左上角的座標,width,height表示矩形的寬和高。除此之外,Rect類還提供了area()返回矩形面積;contains(Point)判斷點是否在矩形內;tl()返回左上角點座標等函式。同Point、Size類似,Rect也使用模板方法提供了多種型別的Rect物件,比如Rect_和Rect2f表示資料型別為浮點型。Rect原始碼如下:

template<typename _Tp> class Rect_
{
public:
    typedef _Tp value_type;

    //! various constructors
    Rect_();
    Rect_(_Tp _x, _Tp _y, _Tp _width, _Tp _height);
    Rect_(const Rect_& r);
    Rect_(const Point_<_Tp>& org, const Size_<_Tp>& sz);
    Rect_(const Point_<_Tp>& pt1, const Point_<_Tp>& pt2);

    Rect_& operator = ( const Rect_& r );
    //! the top-left corner
    Point_<_Tp> tl() const;
    //! the bottom-right corner
    Point_<_Tp> br() const;

    //! size (width, height) of the rectangle
    Size_<_Tp> size() const;
    //! area (width*height) of the rectangle
    _Tp area() const;
    //! true if empty
    bool empty() const;

    //! conversion to another data type
    template<typename _Tp2> operator Rect_<_Tp2>() const;

    //! checks whether the rectangle contains the point
    bool contains(const Point_<_Tp>& pt) const;
    //< the top-left corner, as well as width and
    //height of the rectangle
    _Tp x, y, width, height;  
};

typedef Rect_<int> Rect2i;
typedef Rect_<float> Rect2f;
typedef Rect_<double> Rect2d;
typedef Rect2i Rect;
2.3 影象中的畫素儲存與訪問

(1)畫素值的儲存

  • 灰度影象儲存格式

image

 從上圖可知,灰度影象的儲存格式是一個二維矩陣,由n行*m列畫素組成,每個畫素佔1位元組(8bits),取值為0(純黑)~255(純白)。因此,灰度影象的一個畫素佔1個位元組,共有2^8=265種灰度色。

  • 彩色影象儲存格式

image

 從上圖可知,彩色影象的儲存格式也是一個二維矩陣,由n行*m列畫素組成,每個畫素由B(藍色)、G(綠色)、R(紅色)三個通道(分量)共同決定(OpenCV預設顏色空間為BGR)。對於OpenCV來說,它會將B、G、R三個分量看成一列,其中B、G、R分量各佔1位元組(8bits),取值為0~255之間。因此,彩色影象的一個影象佔3個位元組,共有2^8x2^8x2^8=16777216種。

影象矩陣的大小取決於所用的顏色模型,因為不同的顏色模型每個畫素佔的通道數不一樣,對於多通道影象而言,矩陣中的列會包含多個子列,其子列個數與通道數相等。

 (2) 影象中的畫素訪問
 在OpenCV中,提供了三種訪問影象中(每個)畫素的方法:

  • 指標訪問
Mat srcImage = imread("jinmao.jpg");
int rowNumber = srcImage.rows; // 行數
int colNumber = srcImage.cols; //列數
// 遍歷影象所有畫素
for(int i=0; i<rowNumber; i++) {
    // 獲取第i行的首地址,Mat類提供了ptr函式可以得到影象任意行的首地址
    uchar* data = srcImage.ptr<uchar>(i);
    // 遍歷第i行的所有畫素
    for(int j=0 ; j<colNumber; j++ ) {
        // 開始處理每一個畫素
        data[j] = ...;
    }
}
  • 迭代器iterator
     使用迭代器操作畫素的原理:先獲得影象矩陣的begin和end,然後增加迭代至從begin到end,將*操作符新增在迭代指標前,即可訪問當前指向的內容。相比用指標直接訪問可能出現越界問題,迭代器是一種非常安全的方法。
Mat srcImage = imread("jinmao.jpg");
// 獲取初始位置的迭代器
Mat_<Vec3b>::iterator it = srcImage.begin<Vec3b>();
// 獲取終止位置的迭代器
Mat_<Vec3b>::iterator itEnd = srcImage.end<Vec3b>(); 
// 存取彩色影象畫素
for(;it != itEnd;++it) {
    // 開始處理每個畫素,包含三個通道
    (*it)[0] = ..;  // 通道1
    (*it)[1] = ..;  // 通道2
    (*it)[2] = ..;  // 通道3
    //.....
}
  • 動態地址計算
     動態地址計算,實質上是通過Mat類的at方法直接取出畫素的單通道進行計算。
Mat srcImage = imread("jinmao.jpg");
int rowNumber = srcImage.rows; // 行數
int colNumber = srcImage.cols; //列數
// 遍歷影象所有畫素
for(int i=0; i<rowNumber; i++) {
    // 遍歷第i行的所有畫素
    for(int j=0 ; j<colNumber; j++ ) {
        // 開始處理每一個畫素
        srcImage.at<Vec3b>(i,j)[0] = ...;//通道1
        srcImage.at<Vec3b>(i,j)[1] = ...;//通道2
        srcImage.at<Vec3b>(i,j)[2] = ...;//通道3
        //... 
    }
}
2.4 顏色空間及其通道分離、合併

 在數字影象基礎部分中,我們較為詳細地講解了數字影象諸如RGB、YUV、HSL等常見的顏色空間,本節在此基礎上介紹OpenCV中如何使用ctvColor函式進行顏色格式之間的轉換,以及顏色通道的分離、合併。
 (1) 顏色空間轉換
 對於OpenCV來說,影象預設的顏色格式為BRG,但由於開發需要,我們往往需要將其轉換成灰度影象、RGB格式彩色影象或YUV格式彩色影象等,這就需要用到cvtColor函數了。cvtColor函式原型如下:

/* cvtColor函式原型:
     src:輸入影象;
     dst:輸出影象(轉換後的影象);
     code:影象轉換的型別,比如COLOR_BGR2GRAY、COLOR_BGR2RGB等等(OpenCV3版);
     dstCn:目標影象的通道數,dstCn=0表示目標影象的通道數與源影象一致
*/
void cvtColor(InputArray src, OutputArray dst, int code, int dstCn=0 )  

 說明:
  InputArrayOutputArray兩個類都是代理資料型別,用來接收Mat和Vector<>作為輸入引數,OutputArray繼承自InputArray,從某些程度來說,InputArray和OutputArray可看成是Mat物件或Vector<>物件。InputArray作為輸入引數的時候,傳入的引數加了const限定符,即它只接收引數作為純輸入引數,無法更改輸入引數的內容。而OutputArray則沒有加入限定符,可以對引數的內容進行更改。

  • 示例:影象灰度化(BGR->GRAY)
Mat grayImage;
Mat srcImage = imread("jinmao.jpg");
if (! srcImage.data) {
    printf("open src image failed");
}
cvtColor(srcImage, grayImage, COLOR_BGR2GRAY);

namedWindow("srcImage", CV_WINDOW_AUTOSIZE);
imshow("srcImage", srcImage);
imshow("grayImage", grayImage);

效果演示:
這裡寫圖片描述
 (2) 顏色通道分離與合併
 在影象處理過程中,為了更好地觀察影象的特徵,需要對R、G、B三個顏色通道的分量進行分離或合併。為此,OpenCV為我們提供了split和merge方法,前者用於講一個多通道陣列分離成幾個單通道陣列/陣列;後者將多個單通道數組合併成一個多通道的陣列/陣列。

  • 顏色通道分離

 a) 函式原型

/* split函式原型(指標模式):
     src:輸入被分離的多通道陣列/陣列,這裡傳入的是多通道陣列地址;
     mvBegin:輸出分離後的陣列,mvBegin指向陣列的起始地址;
*/
void split(const Mat& src,Mat *mvBegin);  

/* split函式原型(vector模式):
     m:輸入被分離的多通道陣列/陣列;
     mv:輸出分離後的vector容器;
*/
void split(InputArray m, OutputArrayOfArrays mv);  

 b) 示例程式碼:

Mat srcImage = imread("jinmao.jpg");
Mat imageBlue, imageGreen, imageRed;
Mat mergeImage;
//定義一個Mat向量容器儲存拆分後的資料  
vector<Mat> channels;
//判斷檔案載入是否正確  
assert(srcImage.data != NULL);
namedWindow("image", CV_WINDOW_AUTOSIZE);
//通道的拆分  
split(srcImage, channels);
//提取藍色通道的資料  
imageBlue = channels.at(0);
//提取綠色通道的資料  
imageGreen = channels.at(1);
//提取紅色通道的資料  
imageRed = channels.at(2);

imshow("srcImage", srcImage);
imshow("imageBlue", imageBlue);
imshow("imageGreen", imageGreen);
imshow("imageRed", imageRed);

 效果演示:
這裡寫圖片描述
 也許你會問為什麼三個單通道分量的影象都為灰度影象,而不是紅色分量的影象偏紅色、綠色分量偏綠色以及藍色分量偏藍色?這是因為指定分量被分離出來後,其他兩個分量會預設取同被分離分量一樣的值,根據前部分提到,當R=G=B,畫素值為灰度值比如當畫素的紅色分量為255時BRG = (0,0,255),紅色分量被分離後,畫素值變為(255,255,255),變為純白色;當畫素的紅色分量為125時(假設其他兩個分量任意值)BRG=(b,r,125),畫素值變為(125,125,125),變為灰色。也就是說,單通道分量的值越大,分離後單通道顏色畫素的灰度值越大,越接近白色,反之,越接近黑色。

  • 顏色通道合併

 a) 函式原型

/* merge:
     src:輸入要被合併的vector容器陣列/陣列,所有矩陣的尺寸和深度要一樣;
     count:輸入單通道矩陣的個數;
     dst:輸出合併的多通道陣列/陣列,矩陣的尺寸和深同輸入的一樣);
*/
void merge(const Mat& src,size_t count,OutputArray dst);  

/* merge:
     mv:輸入要被合併的vector容器陣列/陣列,所有矩陣的尺寸和深度要一樣;
     dst:輸出合併的多通道陣列/陣列,矩陣的尺寸和深同輸入的一樣;
*/
void merge(InputArrayOfArrays mv, OutputArray dst);  

 b) 示例程式碼:

void mergeImage() {
    Mat catImage;
    vector<Mat> channels;
    Mat srcImage;
    Mat blueChannelImage;
    // 插入影象
    catImage = imread("cat.jpg");
    srcImage = imread("jinmao.jpg");
    if (!catImage.data) {
        printf("open logo image failed");
    }
    if (!srcImage.data) {
        printf("open src image failed");
    }
    // 分離色彩(BRG)通道
    split(srcImage, channels);
    // 提取藍色通道分量
    blueChannelImage = channels.at(0);
    // 選取原圖的藍色通道的(10,10)座標下方的一塊ROI區域
    Mat ROIImage = blueChannelImage(Rect(10, 10, catImage.cols, catImage.rows));
    // 獲取cat的灰度影象,將ROI區域和cat影象進行混合
    // 然後把得到的結果儲存到blueChannelImage中
    cvtColor(catImage, catImage, COLOR_BGR2GRAY);
    addWeighted(ROIImage, 1.0, catImage,1.0, 0.0, ROIImage);

    // 將三個通道重新合併為一個三通道
    merge(channels, srcImage);

    namedWindow("srcImage", CV_WINDOW_AUTOSIZE);
    imshow("catImage", catImage);
    imshow("ROIImage", ROIImage);
    imshow("srcImage", srcImage);
}

 效果演示:
這裡寫圖片描述
 需要注意的是,示例中”blueChannelImage = channels.at(0)”是將原圖的藍色通道的引用返回給blueChannelImage,所謂“引用”,即為等價,無論修改blueChannelImage還是channels.at(0),它們共同指向的內容(矩陣)是跟著變的,這是影象處理中較為重要的技術基礎。另外,例子中還提到了兩個重要的概念,即ROI(感興趣)區域和影象混合疊加。

  • 感興趣區域(ROI,region of interest)

 在影象處理中,為了提供分析的精度和減少處理時間,我們往往會選擇從影象中圈定一個利於目標處理的影象區域進行處理,這個區域被稱之為感興趣區域ROI。定義影象的ROI區域有兩種方法,即一種為使用表示矩形區域的Rect;另一種為指定感興趣行或列的範圍。

// 方法一:使用Rect
Mat imageROI = srcImage(Rect(10, 15, catImage.cols, catImage.rows));

// 方法二:使用Range
Mat imageROI = srcImage(Range(15,15+catImage.rows),Range(10,10+catImage.cols));

//注:Range是指從起始索引到終止索引(不包括終止索引)的一連段連續序列。
  • 線性混合操作(影象疊加)
/**addWeighted函式原型:
    InputArray型別的src1,表示需要加權的第一個陣列,常常填一個Mat。
    alpha,表示第一個陣列的權重
    src2,表示第二個陣列,它需要和第一個陣列擁有相同的尺寸和通道數(注意)
    beta,表示第二個陣列的權重值。
    dst,輸出的陣列,它和輸入的兩個陣列擁有相同的尺寸和通道數(注意)。
    gamma,一個加到權重總和上的標量值。看下面的式子自然會理解。
    dtype,輸出陣列的可選深度,有預設值-1。;當兩個輸入陣列具有相同的深度時  
        這個引數設定為-1(預設值),即等同於src1.depth()
*/
void addWeighted(InputArray src1, double alpha, InputArray src2,
       double beta, double gamma, OutputArray dst, int dtype = -1);