1. 程式人生 > 其它 >C++版本OpenCv教程(一)Mat—基本的影象容器

C++版本OpenCv教程(一)Mat—基本的影象容器

技術標籤:OpenCV

目錄

英文原版連結
Mat - The Basic Image Container

目標

我們有多種方法從現實世界獲取數字影象:數碼相機、掃描器、計算機斷層掃描和磁共振成像等等。在以上任何情況下,我們(人類)看到的都是影象。然而,當將其轉換到我們的數字裝置時,我們所記錄的是影象中每個點的數值。
在這裡插入圖片描述
例如,在上面的影象中,你可以看到汽車的鏡子只不過是一個包含所有畫素點的強度值的矩陣。我們獲取和儲存畫素值的方式可能會根據我們的需要而有所不同,但最終計算機世界中的所有影象都可能被簡化為數值矩陣和描述矩陣本身的其他資訊。OpenCV是一個計算機視覺庫,主要致力於處理和操作這些資訊。因此,首先需要熟悉OpenCV如何儲存和處理影象。

Mat

cv::Mat Class Reference
關於Mat,您需要知道的第一件事是,您不再需要 手動分配它的記憶體並在不需要時立即釋放它。雖然仍可以這樣做,但大多數OpenCV函式將自動分配其輸出資料。如果您傳遞一個已經存在的Mat物件(該物件已經為矩陣分配了所需的空間),那麼它將被重用。換句話說,我們在任何時候都只使用執行任務所需的記憶體。

Mat基本上是一個類,有兩個資料部分:Mat頭(包含矩陣大小、儲存方法、矩陣儲存地址等資訊)和一個指向包含畫素值矩陣的指標(採取多少維數 取決於選擇的儲存方法)。矩陣頭的大小是恆定的,然而,矩陣本身的大小可能隨著影象的不同而變化,通常會大上幾個數量級。

OpenCV是一個影象處理庫。它包含了大量的影象處理功能。為了解決計算難題,大多數時候您將使用庫的多個函式。因此,將影象傳遞給函式是一種常見的做法。我們不應該忘記我們討論的是影象處理演算法,這些演算法往往需要大量的計算。我們最不希望做的事情就是通過對可能很大的影象進行不必要的複製來進一步降低程式的速度。

為了解決這個問題,OpenCV使用了一個引用計數系統。其思想是每個Mat物件都有自己的矩陣頭,但是當兩個Mat物件之間共享一個矩陣時,可以通過讓兩個Mat物件的矩陣指標指向相同的地址。此外,複製操作符將只複製矩陣頭和指向大矩陣的指標,而不是資料本身。

Mat A, C;                          // creates just the header parts
A =
imread(argv[1], IMREAD_COLOR); // here we'll know the method used (allocate matrix) Mat B(A); // Use the copy constructor C = A; // Assignment operator

最後,上述所有物件都指向同一個資料矩陣,使用其中任何一個物件進行修改都會影響到所有其他物件。實際上,不同的物件只是為相同的底層資料提供了不同的訪問方法;然而,它們的矩陣頭部分是不同的。真正有趣的部分是,您可以建立只引用部分資料的矩陣頭。例如,要在影象中建立感興趣的區域(ROI),只需建立一個帶有新邊界的新標題:

Mat D (A, Rect(10, 10, 100, 100) ); // using a rectangle
Mat E = A(Range::all(), Range(1,3)); // using row and column boundaries

現在你可能會問-如果矩陣本身可能屬於多個Mat物件:當它不再被需要時,誰負責清理它。簡而言之:是最後一個使用它的物件。這是通過使用引用計數機制來處理的。每當有人複製一個Mat物件的矩陣頭時,就會為矩陣增加一個計數器。每當清除矩陣頭時,此計數器就會減少。當計數器達到0時,矩陣被釋放。但是,有時候還需要複製矩陣本身,所以OpenCV提供了 cv::Mat::clone() 和 cv::Mat::copyTo() 函式。

Mat F = A.clone();
Mat G;
A.copyTo(G);

現在修改F或G將不會影響A的Mat頭指向的矩陣。你需要記住的是:

  • OpenCV函式的輸出影象記憶體分配是自動的(除非另有指定)。
  • 您不需要考慮使用OpenCV的c++介面進行記憶體管理。
  • 賦值操作符和複製建構函式只複製Mat頭。
  • 可以使用cv::Mat::clone()和cv::Mat::copyTo()函式複製影象的底層(資料)矩陣。

儲存方法

這是關於如何儲存畫素值的。您可以選擇顏色空間和使用的資料型別。顏色空間指的是我們如何結合顏色元件來編碼一個給定的顏色。最簡單的一種是灰度,我們可以使用黑色和白色來製造灰度。這些組合使我們能夠建立許多灰度級。

對於建立色彩的方式,我們有更多的方式可供選擇。它們每一種都將其分解為三個或四個基本元件,我們可以使用這些元件的組合來建立其他元件。最流行的一種是RGB,主要是因為這也是我們的眼睛構建顏色的方式。它的基色是紅、綠、藍。為了編碼一種顏色的透明度,有時會新增第四個元素:alpha (透明度)(A)。

然而, 不同顏色系統各有其優點:

  • RGB是最常見的,我們的眼睛使用類似的模式。但是請記住,OpenCV標準顯示系統使用BGR顏色空間組成顏色(紅色和藍色通道互換位置)。
  • HSV和HLS將顏色分解為色調、飽和度和亮度值組成部分,這是我們描述顏色更自然的方式。例如,您可能會忽略最後一個元件,使您的演算法對輸入影象的光線條件不太敏感。
  • YCrCb被流行的JPEG影象格式所使用。
  • CIE Lab*是一個感知上一致的顏色空間,如果你需要測量一種給定顏色到另一種顏色的距離,它很方便。

每個構建元件都有自己的有效域。他們直接影響所使用的資料型別。儲存元件的方式定義了我們對其域的控制。最小的資料型別可能是char,即一個位元組或8位。它可以是無符號的(因此可以儲存0到255的值)或有符號的(從-127到+127的值)。雖然在三個元件的情況下,這已經給出了1600萬種可能的顏色來表示(像在RGB的情況下),但是,我們仍可以通過使用float(4位元組= 32位)或double(8位元組= 64位)資料型別為每個元件獲得更精細的控制。不過,請記住,增加元件的大小也會增加記憶體中整個圖片的大小。

顯式建立一個Mat物件

但是,出於除錯的目的,檢視實際值要方便得多。您可以使用Mat的<<運算子來完成此操作。注意,這隻適用於二維矩陣。

雖然Mat作為影象容器工作得很好,但它也是一個通用的矩陣類。因此,建立和操作多維矩陣是可能的。你可以用多種方式建立一個Mat物件:
- cv::Mat::Mat 建構函式

#include<iostream>
//#include <stdio.h>
#include <opencv2/opencv.hpp>
#include "opencv/highgui.h"

using namespace std;
using namespace cv;

int main(int argc,char** argv) {
    Mat M(2,2,CV_8UC3,Scalar(0,0,255));
    cout<<"M = "<<endl;
    cout<<M<<endl;
    return 0;
}

結果:

M = 
[  0,   0, 255,   0,   0, 255;
   0,   0, 255,   0,   0, 255]

對於二維和多通道影象,我們首先定義它們的大小:行和列計數。

然後,我們需要指定用於儲存元素的資料型別和每個矩陣點的通道數量。為了做到這一點,我們根據下面的約定構造了多個定義:

CV_[The number of bits per item][Signed or Unsigned][Type Prefix]C[The channel number]
CV_[單元素的bits數][Signed or Unsigned][資料型別字首]C[通道數]

例如,CV_8UC3意味著我們使用8位長的無符號char型別,每個畫素有三個這樣的型別來形成三個通道。為最多4個通道預定義了型別。cv::Scalar是四個元素的 short vector。指定它,您就可以用自定義值初始化所有矩陣點。如果需要更多,可以使用上面的巨集建立型別,在括號中設定通道編號,如下所示。

- 使用C/ C++陣列並通過建構函式進行初始化

int sz[3] = {2,2,2};
Mat L(3,sz, CV_8UC(1), Scalar::all(0));

上面的例子展示瞭如何建立一個二維以上的矩陣。指定其維度,然後傳遞一個包含每個維度大小的指標,剩下的保持不變。

- cv::Mat::create 函式

M.create(4,4, CV_8UC(2));
cout << "M = "<< endl << " "  << M << endl << endl;

您不能用這種結構初始化矩陣值。它只會重新分配它的矩陣資料記憶體,如果新的大小不能適應舊的。

- MATLAB風格的初始化器:cv::Mat:: zero, cv::Mat:: ones,cv::Mat::eye。指定要使用的大小和資料型別:

Mat E = Mat::eye(4, 4, CV_64F);
cout << "E = " << endl << " " << E << endl << endl;
Mat O = Mat::ones(2, 2, CV_32F);
cout << "O = " << endl << " " << O << endl << endl;
Mat Z = Mat::zeros(3,3, CV_8UC1);
cout << "Z = " << endl << " " << Z << endl << endl;

結果:

E = 
[1, 0, 0, 0;
 0, 1, 0, 0;
 0, 0, 1, 0;
 0, 0, 0, 1]
O = 
[1, 1;
 1, 1]
Z = 
[  0,   0,   0;
   0,   0,   0;
   0,   0,   0]

- 對於小矩陣,你可以使用逗號分隔的初始化器或初始化器列表(最後一種情況需要c++ 11支援):

Mat C = (Mat_<double>(3,3) << 0, -1, 0, -1, 5, -1, 0, -1, 0);
cout << "C = " << endl << " " << C << endl << endl;
C = (Mat_<double>({0, -1, 0, -1, 5, -1, 0, -1, 0})).reshape(3);
cout << "C = " << endl << " " << C << endl << endl;

- 您可以使用cv::randu()函式用隨機值填充一個矩陣。你需要為隨機值給出一個下限和上限:

Mat R = Mat(3, 2, CV_8UC3);
randu(R, Scalar::all(0), Scalar::all(255));