opencv--影象的基本操作【2】
1、影象的表示
1.1、數字影象
一副尺寸為 M × N 的影象可以用一個 M × N 的矩陣來表示,矩陣元素的值表示這個位置上的畫素的亮度,一般來說畫素值越大表示該點越亮。
如圖影象,我們看到的是 Lena 的頭像,但是計算機看來,這副影象只是一堆亮度各異的點。
圖中白色圓圈內的區域,進行放大並仔細檢視,將會如圖所示。
1.2、一般來說,灰度圖用 2 維矩陣表示,彩色(多通道)影象用 3 維矩陣(M× N × 3)表示。
對於影象顯示來說,目前大部分裝置都是用無符號 8 位整數(型別為CV_8U)表示畫素亮度。
1.3、影象資料在計算機記憶體中的儲存順序為以影象最左上點(也可能是最左下點)開始,儲存如表所示。
灰度影象的儲存示意圖:
I ij 表示第 i 行 j 列的畫素值。如果是多通道影象,比如 RGB 影象,則每個
畫素用三個位元組表示。在 OpenCV 中,RGB 影象的通道順序為 BGR ,儲存如
表 所示
2、Mat類
早期的 OpenCV 中,使用 IplImage 和 CvMat 資料結構來表示影象。IplImage和CvMat 都是 C 語言的結構。使用這兩個結構的問題是記憶體需要手動管理,開發者必須清楚知道何時需要申請記憶體,何時需要釋放記憶體。這個開發者帶來了一定的負擔,開發者應將更多精力用於演算法設計,因此在新版本的 OpenCV 中引入了 Mat 類。
新加入的 Mat 類能夠自動管理記憶體。使用 Mat 類,你不再需要花費大量精力在記憶體管理上。而且你的程式碼會變得很簡潔,程式碼行數會變少。但 C++介面唯一的不足是當前一些嵌入式開發系統可能只支援 C 語言,如果你的開發平臺支援C++,完全沒有必要再用 IplImage 和 CvMat。在新版本的 OpenCV 中,開發者依然可以使用 IplImage 和 CvMat,但是一些新增加的函式只提供了 Mat 介面。
2.1、Mat 類的定義如下所示,關鍵的屬性如下方程式碼所示:
class CV_EXPORTS Mat
{
public:
//一系列函式
...
/* flag 引數中包含許多關於矩陣的資訊,如:
-Mat 的標識
-資料是否連續
-深度
-通道數目
*/
int flags;
//矩陣的維數,取值應該大於或等於 2
int dims;
//矩陣的行數和列數,如果矩陣超過 2 維,這兩個變數的值都為-1
int rows, cols;
//指向資料的指標
uchar* data;
//指向引用計數的指標
//如果資料是由使用者分配的,則為 NULL
int* refcount;
//其他成員變數和成員函式
...
};
2.2、建立Mat物件
Mat 是一個非常優秀的影象類,它同時也是一個通用的矩陣類,可以用來建立和操作多維矩陣。有多種方法建立一個 Mat 物件。
建構函式方法
Mat 類提供了一系列建構函式,可以方便的根據需要建立 Mat 物件。
下面是一個使用建構函式建立物件的例子。
Mat M(3,2, CV_8UC3, Scalar(0,0,255)); cout << "M = " << endl << " " << M << endl;
第一行程式碼建立一個行數(高度)為 3,列數(寬度)為 2 的影象,影象元素是 8 位無符號整數型別,且有三個通道。
影象的所有畫素值被初始化為(0, 0,255)。由於 OpenCV 中預設的顏色順序為 BGR,因此這是一個全紅色的影象。
第二行程式碼是輸出Mat類的例項M的所有畫素值。Mat重定義了<<操作符,使用這個操作符,可以方便地輸出所有畫素值,而不需要使用 for 迴圈逐個畫素輸出。
- 常用的建構函式有:
Mat::Mat()
無引數構造方法;
Mat::Mat(int rows, int cols, int type)
建立行數為 rows,列數為 col,型別為 type 的影象;
Mat::Mat(Size size, int type)
建立大小為 size,型別為 type 的影象;
Mat::Mat(int rows, int cols, int type, const Scalar& s)
建立行數為 rows,列數為 col,型別為 type 的影象,並將所有元素初始化為值 s;
Mat::Mat(Size size, int type, const Scalar& s)
建立大小為 size,型別為 type 的影象,並將所有元素初始化為值 s;
Mat::Mat(const Mat& m)
將 m 賦值給新建立的物件,此處不會對影象資料進行復制,m 和新物件共用影象資料;
Mat::Mat(int rows, int cols, int type, void* data, size_t step=AUTO_STEP)
建立行數為 rows,列數為 col,型別為 type 的影象,此建構函式不建立影象資料所需記憶體,而是直接使用 data 所指記憶體,影象的行步長由 step指定。
Mat::Mat(Size size, int type, void* data, size_t step=AUTO_STEP)
建立大小為 size,型別為 type 的影象,此建構函式不建立影象資料所需記憶體,而是直接使用 data 所指記憶體,影象的行步長由 step 指定。
Mat::Mat(const Mat& m, const Range& rowRange, const Range& colRange)
建立的新影象為 m 的一部分,具體的範圍由 rowRange 和 colRange 指定,此建構函式也不進行影象資料的複製操作,新影象與 m 共用影象資料;
Mat::Mat(const Mat& m, const Rect& roi)
建立的新影象為 m 的一部分,具體的範圍 roi 指定,此建構函式也不進行影象資料的複製操作,新影象與 m 共用影象資料。
1. 這些建構函式中,很多都涉及到型別type。type可以是CV_8UC1,CV_16SC1,…,CV_64FC4 等。
2. 裡面的 8U 表示 8 位無符號整數,16S 表示 16 位有符號整數,64F表示 64 位浮點數(即 double 型別);
3. C 後面的數表示通道數,例如 C1 表示一個通道的影象,C4 表示 4 個通道的影象,以此類推。如果你需要更多的通道數,需要用巨集 CV_8UC(n),例如:
Mat M(3,2, CV_8UC(5));//建立行數為 3,列數為 2,通道數為 5 的影象
create()函式建立物件
除了在建構函式中可以建立影象,也可以使用 Mat 類的 create()函式建立影象。如果 create()函式指定的引數與影象之前的引數相同,則不進行實質的記憶體申請操作;如果引數不同,則減少原始資料記憶體的索引,並重新申請記憶體。
Mat M(2,2, CV_8UC3);//建構函式建立影象
M.create(3,2, CV_8UC2);//釋放記憶體重新建立影象
需要注意的時,使用 create()函式無法設定影象畫素的初始值
Matlab 風格的建立物件方法
OpenCV 2 中提供了 Matlab 風格的函式,如 zeros(),ones()和 eyes()。這種方法使得程式碼非常簡潔,使用起來也非常方便。
使用這些函式需要指定影象的大小和型別,使用方法如下:
Mat Z = Mat::zeros(2,3, CV_8UC1);
cout << "Z = " << endl << " " << Z << endl;
Mat O = Mat::ones(2, 3, CV_32F);
cout << "O = " << endl << " " << O << endl;
Mat E = Mat::eye(2, 3, CV_64F);
cout << "E = " << endl << " " << E << endl;
有些 type 引數如 CV_32F 未註明通道數目,這種情況下它表示單通道
上面程式碼的輸出結果如圖所示:
例項
#include"opencv2/opencv.hpp"
#include<iostream>
using namespace std;
using namespace cv;
int main() {
Mat src;
src = imread("D:/111.jpg");
if (src.empty()) {
cerr << "open error" << endl;
return -1;
}
namedWindow("原圖", WINDOW_AUTOSIZE);
imshow("原圖", src);
//構造 Mat::Mat(Size size,int type),沒有資料
Mat dst;
dst = Mat(src.size(), src.type());
dst = Scalar(127, 0, 255); //B=127 G=0 R=255
namedWindow("new1", WINDOW_AUTOSIZE);
imshow("new1", dst);
//和原圖一模一樣
//1. Mat.clone()
//和原圖一模一樣
Mat dst2;
dst2 = src.clone();
namedWindow("new2", WINDOW_AUTOSIZE);
imshow("new2", dst2);
//2. Mat.copyTo(output)
Mat dst3;
src.copyTo(dst3);
namedWindow("new3", WINDOW_AUTOSIZE);
imshow("new3", dst3);
//3. cvtColor構造圖片
Mat dst4;
cvtColor(src,dst4, CV_BGR2GRAY);
cout << "dst4.cols=" << dst4.cols << endl ;
cout << "dst4.rows=" << dst4.rows << endl ;
cout << "src.channels=" << src.channels() << endl ;
cout << "dst4.channels=" << dst4.channels() << endl;
const uchar* p = dst4.ptr<uchar>(0);
cout << "dst4 first pixel value=" << &p << endl;
namedWindow("new4", WINDOW_AUTOSIZE);
imshow("new4", dst4);
//圖片內容為空
//Mat::Mat(int rows, int cols, int type, void* data, size_t step=AUTO_STEP)
Mat dst5(3,3, CV_8UC3, Scalar(0, 0, 255));
cout << "dst5=" << endl << dst5 << endl;
namedWindow("new5", WINDOW_AUTOSIZE);
imshow("new5", dst5);
//Mat
Mat dst6;
dst6.create(src.size(), src.type());
dst6 = Scalar(0, 0, 255);
namedWindow("new6", WINDOW_AUTOSIZE);
imshow("new6", dst6);
//
Mat dst7;
dst7=Mat::zeros(src.size(), src.type());
namedWindow("new7", WINDOW_AUTOSIZE);
imshow("new7", dst7);
waitKey(0);
}
3、矩陣的基本元素表達
對於單通道影象,其元素型別一般為 8U(即 8 位無符號整數),當然也可以是 16S、32F 等;這些型別可以直接用 uchar、short、float 等 C/C++語言中的基本資料型別表達。
如果多通道影象,如 RGB 彩色影象,需要用三個通道來表示。在這種情況下,如果依然將影象視作一個二維矩陣,那麼矩陣的元素不再是基本的資料型別。
OpenCV 中有模板類 Vec,可以表示一個向量。OpenCV 中使用 Vec 類預定義了一些小向量,可以將之用於矩陣元素的表達。
typedef Vec<uchar, 3> Vec3b;
typedef Vec<uchar, 4> Vec4b;
typedef Vec<short, 2> Vec2s;
typedef Vec<short, 3> Vec3s;
typedef Vec<short, 4> Vec4s;
typedef Vec<int, 2> Vec2i;
typedef Vec<int, 3> Vec3i;
typedef Vec<int, 4> Vec4i;
typedef Vec<float, 2> Vec2f;
typedef Vec<float, 3> Vec3f;
typedef Vec<float, 4> Vec4f;
typedef Vec<float, 6> Vec6f;
typedef Vec<double, 2> Vec2d;
typedef Vec<double, 3> Vec3d;
typedef Vec<double, 4> Vec4d;
typedef Vec<double, 6> Vec6d;
例如 :
- 8U 型別的 RGB 彩色影象可以使用 Vec3b,
- 3 通道 float 型別的矩陣可以使用 Vec3f。
對於 Vec 物件,可以使用[]符號如運算元組般讀寫其元素,如:
Vec3b color; //用 color 變數描述一種 RGB 顏色
color[0]=255; //B 分量
color[1]=0; //G 分量
color[2]=0; //R 分量
5、選取影象區域性區域
Mat 類提供了多種方便的方法來選擇影象的區域性區域。使用這些方法時需要注意,這些方法並不進行記憶體的複製操作。如果將區域性區域賦值給新的 Mat 物件,新物件與原始物件共用相同的資料區域,不新申請記憶體,因此這些方法的執行速度都比較快。
5.1、單行或單列選擇
提取矩陣的一行或者一列可以使用函式row()或col()。
函式的宣告如下:
Mat Mat::row(int i) const
Mat Mat::col (int j) const
引數i和j分別是行標和列標。例如取出A矩陣的第i行可以使用如下程式碼:
Mat line=A.row(i);
例如取出 A 矩陣的第 i 行,將這一行的所有元素都乘以 2,然後賦值給第 j行,可以這樣寫:
A.row(j) = A.row(i)*2;
5.2、用Range選擇多行或多列
Range 是 OpenCV 中新增的類,該類有兩個關鍵變數 star 和 end。Range 對
象可以用來表示矩陣的多個連續的行或者多個連續的列。其表示的範圍為從 start
到 end,包含 start,但不包含 end。Range 類的定義如下:
class Range
{
public:
...
int start, end;
};
Range 類還提供了一個靜態方法 all(),這個方法的作用如同 Matlab 中的“:”,表所有的行或者所有的列。
建立一個單位陣
Mat A=Mat::eye(10,10,CV_32S);
提取第1到3列(不包含3列)
Mat B=A(Range::all(),Range(1,3));
提取 B 的第 5 至 9 行(不包括 9)
Mat C = B(Range(5, 9), Range::all());
其實等價於 C = A(Range(5, 9), Range(1, 3))
5.3、 感興趣區域
從影象中提取感興趣區域(Region of interest)有兩種方法:
- 使用建構函式
//建立寬度為 320,高度為 240 的 3 通道影象
Mat img(Size(320,240),CV_8UC3);
//roi 是表示 img 中 Rect(10,10,100,100)區域的物件
Mat roi(img, Rect(10,10,100,100));
//使用建構函式
Mat roi4(img, Range(10,100),Range(10,100));
- 使用括號運算子
Mat roi2 = img(Rect(10,10,100,100));
當然也可以使用 Range 物件來定義感興趣區域,
如下使用括號運算子:
Mat roi3 = img(Range(10,100),Range(10,100));
5.4、取對角線元素
矩陣的對角線元素可以使用 Mat 類的 diag()函式獲取,該函式的定義如下:
Mat Mat::diag(int d) const
- 引數 d=0 時,表示取主對角線;
- 當引數 d>0 是,表示取主對角線下方的次對角線,
如 d=1 時,表示取主對角線下方,且緊貼主多角線的元素; - 當引數 d<0 時,表示取主對角線上方的次對角線。
如同row()和col()函式,diag()函式也不進行記憶體複製操作,其複雜度也是O(1)。
6、Mat表示式
用 C++中的運算子過載,OpenCV 2 中引入了 Mat 運算表示式。這一新特點使得使用 C++進行程式設計時,就如同寫 Matlab 指令碼,程式碼變得簡潔易懂,也便於維護。
如果矩陣 A 和 B 大小相同,則可以使用如下表達式:
C = A + B + 1;
其執行結果是 A 和 B 的對應元素相加,然後再加 1,並將生成的矩陣賦給 C變數。
下面給出 Mat 表示式所支援的運算。下面的列表中使用 A 和 B 表示 Mat 型別的物件,使用 s 表示 Scalar 物件,alpha 表示 double 值。
- 加法,減法,取負:A+B,A-B,A+s,A-s,s+A,s-A,-A
- 縮放取值範圍:A*alpha
- 矩陣對應元素的乘法和除法: A.mul(B),A/B,alpha/A
- 矩陣乘法:A*B (注意此處是矩陣乘法,而不是矩陣對應元素相乘)
- 矩陣轉置:A.t()
- 矩陣求逆和求偽逆:A.inv()
- 矩陣比較運算:A cmpop B,A cmpop alpha,alpha cmpop A。
此處 cmpop可以是>,>=,==,!=,<=,<。如果條件成立,則結果矩陣(8U 型別矩陣)的對應元素被置為 255;否則置 0。 - 矩陣位邏輯運算:A logicop B,A logicop s,s logicop A,~A,
此處 logicop可以是&,|和^。 - 矩陣對應元素的最大值和最小值:min(A, B),min(A, alpha),max(A, B),max(A, alpha)。
- 矩陣中元素的絕對值:abs(A)
- 叉積和點積:A.cross(B),A.dot(B)
下面例程展示了 Mat 表示式的使用方法,例程的輸出結果如圖所示。
#include <iostream>
#include "opencv2/opencv.hpp"
using namespace std;
using namespace cv;
int main(int argc, char* argv[])
{
//建立了一個4*4的單位矩陣,單通道,32位
Mat A = Mat::eye(4, 4, CV_32SC1);
cout << "A=" << A << endl;
//單位矩陣的每個元素都是*3+1;
Mat B = A * 3 + 1;
//B矩陣的主對角線【4,4,4,4】與B矩陣的第二列元素【1,4,1,1】對應相加
Mat C = B.diag(0) + B.col(1);
//輸出元素:
cout << "A = " << A << endl << endl;
cout << "B = " << B << endl << endl;
cout << "C = " << C << endl << endl;
cout << "A = " << A << endl << endl;
//C矩陣與B的主對角線的點積 5*4+8*4+5*4+5*4;
cout << "C .* diag(B) = " << C.dot(B.diag(0)) << endl;
return 0;
}
7、Mat_類
Mat_類是對 Mat 類的一個包裝,其定義如下:
template<typename _Tp> class Mat_ : public Mat
{
public:
//只定義了幾個方法
//沒有定義新的屬性
};
這是一個非常輕量級的包裝,既然已經有 Mat 類,為何還要定義一個 Mat_?
下面我們看這段程式碼:
Mat M(600, 800, CV_8UC1);
for( int i = 0; i < M.rows; ++i){
uchar * p = M.ptr<uchar>(i);
for( int j = 0; j < M.cols; ++j ){
double d1 = (double) ((i+j)%255);
M.at<uchar>(i,j) = d1;
double d2 = M.at<double>(i,j);//此行有錯
}
}
在讀取矩陣元素時,以及獲取矩陣某行的地址時,需要指定資料型別。這樣首先需要不停地寫“”,讓人感覺很繁瑣,在繁瑣和煩躁中容易犯錯,如上面程式碼中的錯誤,用 at()獲取矩陣元素時錯誤的使用了 double 型別。這種錯誤不是語法錯誤,因此在編譯時編譯器不會提醒。在程式執行時,at()函式獲取到的不是期望的(i,j)位置處的元素,資料已經越界,但是執行時也未必會報錯。這樣的錯誤使得你的程式忽而看上去正常,忽而彈出“段錯誤”,特別是在程式碼規模很大時,難以查錯。
如果使用 Mat_類,那麼就可以在變數宣告時確定元素的型別,訪問元素時不再需要指定元素型別,即使得程式碼簡潔,又減少了出錯的可能性。上面程式碼可以用 Mat_實現.實現程式碼如下面例程裡的第二個雙重 for 迴圈。
#include <iostream>
#include "opencv2/opencv.hpp"
#include <stdio.h>
using namespace std;
using namespace cv;
int main(int argc, char* argv[])
{
Mat M(600, 800, CV_8UC1);
for (int i = 0; i < M.rows; ++i)
{
//獲取指標時需要指定型別
uchar * p = M.ptr<uchar>(i);
for (int j = 0; j < M.cols; ++j)
{
double d1 = (double)((i + j) % 255);
//用 at()讀寫畫素時,需要指定型別
M.at<uchar>(i, j) = d1;
//下面程式碼錯誤,應該使用 at<uchar>()
//但編譯時不會提醒錯誤
//執行結果不正確,d2 不等於 d1
//double d2 = M.at<double>(i, j);
}
}
//在變數宣告時指定矩陣元素型別
Mat_<uchar> M1 = (Mat_<uchar>&)M;
for (int i = 0; i < M1.rows; ++i)
{
//不需指定元素型別,語句簡潔
uchar * p = M1.ptr(i);
for (int j = 0; j < M1.cols; ++j)
{
double d1 = (double)((i + j) % 255);
//直接使用 Matlab 風格的矩陣元素讀寫,簡潔
M1(i, j) = d1;
double d2 = M1(i, j);
}
}
return 0;
}
8、Mat類的記憶體管理
使用 Mat 類,記憶體管理變得簡單,不再像使用 IplImage 那樣需要自己申請和釋放記憶體。雖然不瞭解 Mat 的記憶體管理機制,也無礙於 Mat 類的使用,但是如果清楚瞭解 Mat 的記憶體管理,會更清楚一些函式到底操作了哪些資料。
Mat 是一個類,由兩個資料部分組成:矩陣頭(包含矩陣尺寸,儲存方法,儲存地址等資訊)和一個指向儲存所有畫素值的矩陣的指標。矩陣頭的尺寸是常數值,但矩陣本身的尺寸會依影象的不同而不同,通常比矩陣頭的尺寸大數個數量級。複製矩陣資料往往花費較多時間,因此除非有必要,不要複製大的矩陣。
為了解決矩陣資料的傳遞,OpenCV 使用了引用計數機制。其思路是讓每個Mat 物件有自己的矩陣頭資訊,但多個 Mat 物件可以共享同一個矩陣資料。讓矩陣指標指向同一地址而實現這一目的。很多函式以及很多操作(如函式引數傳值)只複製矩陣頭資訊,而不復制矩陣資料。
前面提到過,有很多中方法建立 Mat 類。如果 Mat 類自己申請資料空間,那麼該類會多申請 4 個位元組,多出的 4 個位元組儲存資料被引用的次數。引用次數儲存於資料空間的後面,refcount 指向這個位置,如圖所示。當計數等於 0時,則釋放該空間。
關於多個矩陣物件共享同一矩陣資料,我們可以看這個例子:
Mat A(100,100, CV_8UC1);
Mat B = A;
Mat C = A(Rect(50,50,30,30));
上面程式碼中有三個 Mat 物件,分別是 A,B 和 C。這三者共有同一矩陣資料:
9、輸出
從前面的例程中,可以看到 Mat 類過載了<<操作符,可以方便得使用流操作來輸出矩陣的內容。預設情況下輸出的格式是類似 Matlab 中矩陣的輸出格式。除了預設格式,Mat 也支援其他的輸出格式。程式碼如下:
- 首先建立一個矩陣,並用隨機數填充。填充的範圍由 randu()函式的第二個引數和第三個引數確定,下面程式碼是介於 0 到 255 之間。
建立了3*2的一個影象,有3個通道
Mat R = Mat(3, 2, CV_8UC3);
三通道的BGR取值都是隨機數 0~255
randu(R, Scalar::all(0), Scalar::all(255));
預設格式輸出的程式碼如下:
cout << "R (default) = " << endl << R << endl << endl;
python 格式輸出的程式碼如下:
//cout << "r(Python風格_openCV2) = " << format(r, “python”) << “;” << endl << endl;
cout << "r(Python風格_openCV3) = " << endl << format(r, Formatter::FMT_PYTHON) << endl << endl;
以逗號分割的輸出的程式碼如下:
//cout << "r(,分隔風格_openCV2) = " << format(r, “csv”) << “;” << endl << endl;
cout << "r(,分隔風格_openCV3) = " << endl << format(r, Formatter::FMT_CSV) << endl << endl;
numpy 格式輸出的程式碼如下:
//cout << "r(numpy風格_openCV2) = " << format(r, “numpy”) << “;” << endl << endl;
cout << "r(numpy風格_openCV3) = " << endl << format(r, Formatter::FMT_NUMPY)<< endl << endl;