- 從零開始:數字圖像處理的編程基礎與應用
- 彭凌西 彭紹湖 唐春明 陳統編著
- 3616字
- 2022-04-13 16:24:01
1.3 Mat圖像存儲容器
人們從現實世界中獲取數字圖像的途徑有數碼相機、掃描儀、計算機斷層掃描和核磁共振成像等。在任何情況下,人眼所看到的都是圖像。然而,實際上數字設備記錄的是圖像中每個點的數值,如圖1-39所示。

圖1-39 圖像表示
可以看到,在上述圖像中,圖像的成像只不過是一個包含所有像素點強度值的矩陣。獲取和存儲像素值的方法可能會根據不同的需要而有所不同,但最終,計算機世界里的所有圖像都可以簡化為數值矩陣信息。OpenCV是一個計算機視覺庫,可以幫助我們處理和操縱這些信息。因此,讀者需要熟悉的第一件事是OpenCV如何存儲和處理圖像。
· 1.3.1 Mat容器簡介
Mat容器是早期(2001年)OpenCV基于C語言接口建立的。為了在內存中存放圖像,當時采用的是IplImage C語言結構體。然而,采用這種方法用戶必須接受C語言所有的不足,其中最大的不足是需要手動進行內存管理,即用戶需要對開辟和銷毀內存負責。雖然對于小型的程序來說手動管理內存不是問題,但一旦代碼變得龐大,用戶就會越來越多地糾結于這個問題,而不是著力實現其開發目標。
幸運的是,C++出現了,并且帶來了類的概念。同時,由于C++與C語言完全兼容,所以在進行代碼更改時不會出現任何兼容性問題。為此,OpenCV在2.0版本中引入了一個新的C++接口,并在OpenCV中使用了Mat類,通過Mat解決了內存管理和運算符重載等問題,利用自動內存管理給出了解決問題的新方法。使用這個方法,用戶不需要糾結于如何管理內存,而且代碼會變得簡潔。C++接口唯一的不足是當前許多嵌入式開發系統只支持C語言,所以當用戶的目標不是這種開發平臺(嵌入式)時,沒有必要使用“舊”方法。
對于Mat類,首先要知道的是用戶不必再手動為其開辟空間,也不必在不需要時立即將空間釋放(但手動做這些工作還是可以的,大多數OpenCV函數仍會手動為輸出數據開辟空間)。當傳遞一個已經存在的Mat對象時,開辟好的矩陣空間會被重用。也就是說,用戶每次都使用大小正好的內存來完成任務。
Mat類由兩個數據部分組成:矩陣頭(包含矩陣尺寸、存儲方法、存儲地址等信息)和一個指向存儲所有像素值矩陣(根據所選存儲方法的不同,矩陣可以是不同的維數)的指針。矩陣頭的尺寸是常數值,但矩陣本身的尺寸會因圖像的不同而不同,通常比矩陣頭的尺寸大好幾個數量級。因此,當在程序中傳遞圖像并創建副本時,大的開銷是由矩陣造成的,而不是矩陣頭。OpenCV是一個圖像處理庫,其中包含大量的圖像處理函數。為了解決問題,通常要使用庫中的多個函數,因此經常需要在函數中傳遞圖像。同時,對于那些計算量很大的圖像處理算法,除非萬不得已,否則不應該復制“大”圖像,因為這會降低程序運行速度。
為了解決這個問題,OpenCV使用引用計數機制,其思路是讓每個Mat對象有自己的矩陣頭,但共享同一個矩陣(通過讓矩陣指針指向同一地址來實現)。當使用復制構造函數(又稱拷貝構造函數)時,只需復制矩陣頭和矩陣指針即可,而不用復制矩陣本身,代碼如下。
Mat A,C; // 只創建矩陣頭部分
A = imread(argv[1],CV_LOAD_IMAGE_COLOR); // 這里為矩陣開辟內存
Mat B(A); // 使用復制構造函數
C = A; // 賦值運算符
以上代碼中的所有Mat對象最終都指向同一個,也是唯一的數據矩陣。雖然它們的矩陣頭不同,但通過任何一個對象所做的改變都會影響其他對象。實際上,不同的對象只是訪問相同數據的不同途徑而已。這里還要提及一個強大的功能:可以創建只引用部分數據的矩陣頭。例如用戶想要創建一個感興趣區域(Region of Interest,ROI),只需要創建包含邊界信息的矩陣頭即可。
Mat D (A,Rect(10,10,100,100) ); // 使用矩陣確定感興趣區域
Mat E = A(Range:all(),Range(1,3)); // 使用行和列確定邊界,這里為A矩陣全部行的第1—3列
如果矩陣屬于多個Mat對象,那么當不再需要它時誰來負責清理?簡單的回答是最后一個使用它的對象。清理工作可以通過引用計數機制來實現。無論用戶什么時候復制一個Mat對象的矩陣頭,都會增加矩陣的引用次數;反之,當一個矩陣頭被釋放之后,這個計數被減1;當計數值為0,矩陣就會被清理。但在某些時候確實需要完全復制矩陣本身(不只是矩陣頭和矩陣指針),這時可以使用函數clone()或者copyTo()實現深復制。示例如下。
Mat F = A.clone();
Mat G;
A.copyTo(G);
此時改變F或者G就不會影響Mat矩陣頭所指向的矩陣。二者的區別在于,copyTo()是否申請新的內存空間,取決于目標頭像矩陣頭中的大小信息是否與源圖像一致,若一致則是淺復制,不申請新的空間,否則先申請空間后再進行復制;clone ()是完全的深復制,在內存中申請新的空間。
以上內容可以總結如下。
①OpenCV函數中輸出圖像的內存分配是自動完成的(如果不特別指定的話)。
②使用OpenCV的Mat類時不需要考慮內存釋放問題。
③賦值運算符和復制構造函數只復制矩陣頭。
④函數clone()或者copyTo()可用來復制表示圖像的矩陣。
· 1.3.2 存儲方法
這一小節講述如何存儲像素值。存儲像素值需要指定顏色空間和數據類型。顏色空間是指對一個給定的顏色,如何組合顏色元素以對其編碼。最簡單的顏色空間要屬灰度級空間,只處理黑色和白色,對它們進行組合可以產生不同程度的灰色。
對于彩色,則有更多種類的顏色空間。但不論哪種存儲方法,都是把顏色分成3個或者4個基元素,通過組合基元素來產生所有的顏色。RGB顏色空間是最常用的顏色空間之一,它也是人眼內部構成顏色的方式。它的基色是紅色、綠色和藍色,有時為了表示透明顏色也會加入第四個元素alpha (A)。
不同的顏色空間,各有自身的優勢,具體介紹如下。
- RGB(Red:紅,Green:綠,Blue:藍)是最常見的顏色空間之一,這是因為人眼采用相似的工作機制,所以它也被顯示設備所采用。
- HSV(Hue:色調或色相,Saturation:飽和度,Value:明度)和HLS(Hue:色調或色相,Lightness:亮度,Saturation:飽和度)把顏色分解成色調、飽和度和明度/亮度。這是描述顏色更自然的方式,比如可以通過拋棄最后一個元素,使算法對輸入圖像的光照條件不敏感。
- YcrCb(即YUV),“Y”表示明亮度(Luminance或Luma),也就是灰度值;而“U”和“V” 表示色度(Chrominance或Chroma),描述影像色彩及飽和度,用于指定像素的顏色,YcrCb在JPEG圖像格式中廣泛使用。
- CIE L*a*b*是目前最流行的測色系統之一。以明度L*和色度坐標a*、b*來表示顏色在顏色空間中的位置。L*表示顏色的明度,范圍由0到100,表示顏色從深(黑)到淺(白),a*正值表示偏紅,負值表示偏綠;b*正值表示偏黃,負值表示偏藍。
每個組成元素都有自己的定義域,具體取決于其數據類型。如何存儲一個元素決定了在該元素定義域上能夠控制的精度。最小的數據類型是char,占1字節或者8個二進制位,可以是有符號型(0到255之間)或無符號型(-127到+127之間)。盡管使用3個字符char型元素已經可以表示1600萬種可能的顏色(使用RGB顏色空間),但若使用單精度浮點數float(4字節,32位)型或雙精度浮點數double(8字節,64位)型元素則能分辨出更加精細的顏色。但增加元素的尺寸也會增加圖像所占的內存空間。
OpenCV中圖像的通道數可以是1、2、3或4。其中常見的是單通道和三通道,二通道和四通道不常見。
①單通道的是灰度圖像。
②三通道的是彩色圖像,例如RGB圖像。
③四通道的圖像是RGBA圖像,是RGB加上一個A通道,也叫alpha通道,表示透明度。PNG圖像是一種典型的四通道圖像。alpha通道可以賦值0到1,或者0到255的數字,表示從透明到不透明。
④二通道的圖像是RGB555和RGB565格式的圖像。二通道圖像在程序處理中會用到,如傅里葉變換。其中,RGB565是16位的,只需要2字節存儲每個像素點,其中第一字節的前5位是R(紅色),第一字節后3位+第二字節前3位是G(綠色),第二字節后5位是B(藍色),相對3個字節,對源圖像進行了壓縮。
- HSI(Hue,Saturation,Intensity),其中H定義顏色的頻率,稱為色調;S表示顏色的深淺程度,稱為飽和度;I表示強度或亮度。
· 1.3.3 創建Mat對象
Mat不僅是很優秀的圖像容器類,同時也是通用的矩陣類,可以用來創建和操作多維矩陣。創建一個Mat對象有多種方法,此處將通過項目1-1來介紹這一部分內容。
創建Mat對象的具體操作過程如下。
(1)創建Qt項目1-1,如圖1-40、圖1-41所示。

圖1-40 創建Qt項目1-1

圖1-41 選擇對應的Kits組件——MSVC2015 32bit
(2)修改1-1.pro文件來配置OpenCV環境。往1-1.pro文件中加入如下代碼。
# 導入頭文件
INCLUDEPATH+=D:/OpenCV/opencv/build/include
INCLUDEPATH+=D:/opencv/opencv/build/include/opencv2
# 導入庫文件
win32:CONFIG(debug, debug|release):{
LIBS+=-LD:/OpenCV/opencv/build/x64/vc14/lib\
-lopencv_world440d
}
else{
LIBS+=-LD:/OpenCV/opencv/build/x64/vc14/lib\
-lopencv_world440
}
至此,環境已經配置完畢。本書后面的環境如無特殊說明均默認使用此處的環境配置方法,不再重復說明,請讀者注意。
█ 1.采用Mat()構造函數創建Mat對象
例1-1:使用Mat()構造函數創建Mat對象。
(1)編輯main.cpp文件。
#include<iostream>
#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main()
{
//創建一個類型為8位uchar、顏色為三通道黃色的2×2 Mat對象
Mat img(2,2,CV_8UC3,Scalar(0,255,255));
cout <<"矩陣元素" << endl << img << endl;
return 0;
}
(2)運行代碼,步驟如下。本書后面的運行代碼方法如無特殊說明均默認為此處的運行方法,不再重復說明,請讀者注意。
(3)編譯程序前,需要先對工程進行QMake編譯。將鼠標指針移動到左側的工程名上并右擊,在彈出的快捷菜單中選擇“qmake”命令進行QMake編譯,結果如圖1-42所示。

圖1-42 QMake編譯結果輸出
(4)QMake編譯通過之后,繼續將鼠標指針移動到左側的工程名上并右擊,在彈出的快捷菜單中選擇“構建”命令。同樣,此時下方的“編譯輸出”窗口沒有出現異常錯誤提示,表示編譯通過,此時才真正生成了可執行的.exe文件,如圖1-43所示。

圖1-43 編譯輸出
(5)編譯生成.exe文件之后,可以單擊Qt Creator窗口左下角的三角形按鈕,運行編譯通過的測試程序,程序運行結果如圖1-44所示。注意其中的行數為2,而列數為2×3=6。

圖1-44 例1-1程序運行結果
由此可知,在創建Mat對象時,對于二維多通道圖像,首先要定義其尺寸,即行數和列數。然后需要指定存儲元素的數據類型,以及每個矩陣點的通道數。為此,依據下面的規則有多種定義方法。
CV_[The number of bits per item][Signed or Unsigned][Type Prefix]C[The channel number]
例如前面例1-1中的“CV_8UC3”表示使用8個二進制位;U表示Unsigned int,即無符號整型;C代表所存儲圖像的通道;3代表所存儲圖像的通道數,每個像素由3個元素組成三通道。預先定義的通道數可以多達4個。Scalar是一個short整型的vector容器,指定這個參數能夠使用指定的定制化值來初始化矩陣。
在C/C++中通過構造函數進行初始化,此處基于前面例1-1的方法進行了修改。
#include<iostream>
#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main()
{
//創建一個類型為8位uchar、顏色為三通道黃色的2×2 Mat對象
// Mat img(2,2,CV_8UC3,Scalar(0,255,255));
// cout <<"matrix element"<< endl << img << endl;
//創建一個超過二維的矩陣
int sz[3] ={ 2,2,2};
//三維的Mat對象(2×2×2),元素全部為0
Mat array2(3,sz,CV_8UC1,Scalar(0));
//因為是三維的,所以不能用DOS命令行界面顯示
return 0;
}
上面演示了如何創建一個超過二維的矩陣:指定維數,然后傳遞一個指向一個數組的指針,這個數組包含每個維度的尺寸;其余的參數含義參考例1-1,此處為單通道圖像,所以是CV_8UC1,Scalar(0)。
█ 2.采用create()函數創建Mat對象
#include<opencv2/opencv.hpp>
#include<iostream>
using namespace cv;
using namespace std;
int main()
{
//創建一個類型為8位uchar、顏色為三通道黃色的2×2 Mat對象
// Mat img(2,2,CV_8UC3,Scalar(0,255,255));
// cout << "matrix element" << endl << img << endl;
//... 中間注釋省略前面介紹的代碼
//用create()函數實現對Mat對象的初始化
Mat img;
img.create(4,4,CV_8UC(2));
cout <<"M = " << endl << img << endl;
return 0;
}
程序運行結果如圖1-45所示。

圖1-45 create()函數創建的Mat對象
注意,這個創建Mat對象的方法不能為矩陣設初值,只是在改變尺寸時重新為矩陣數據開辟內存。
█ 3.采用MATLAB樣式初始化器cv::Mat::zeros、cv::Mat::ones、cv::Mat::eye創建Mat對象
用這種方法創建Mat對象時,需要指定要使用的矩陣大小和數據類型。
#include<iostream>
#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main()
{
//創建一個類型為8位uchar、顏色為三通道黃色的2×2Mat對象
// Mat img(2,2,CV_8UC3,Scalar(0,255,255));
// cout <<"matrix element"<< endl << img << endl;
//... 中間注釋省略前面介紹的代碼
Mat array1 = Mat::eye(4,4,CV_64F); // 對角矩陣
Mat array2 = Mat::ones(4,4,CV_32F); //全1矩陣
Mat array3 = Mat::zeros(4,4,CV_8UC1); //全0矩陣
cout <<"Diagonal matrix"<< endl << array1 << endl;
cout << "full one matrix" << endl << array2 << endl;
cout << "full zero matrix" << endl << array3 << endl;
return 0;
}
程序運行結果如圖1-46所示。

圖1-46 MATLAB樣式初始化器創建的Mat對象
█ 4.在小矩陣中可以用逗號分隔的初始化函數
//在小矩陣中可以用逗號分隔的初始化函數
#include<iostream>
#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main()
{
//創建一個類型為8位uchar、顏色為三通道黃色的2×2 Mat對象
// Mat img(2,2,CV_8UC3,Scalar(0,255,255));
// cout << "matrix element" << endl << img << endl;
//... 中間注釋省略前面介紹的代碼
Mat array = (Mat_<double>(3,3) << 0,-1,5,-1,5,-1,0,-1,0);
cout << "matrix" << endl << array << endl;
return 0;
}
程序運行結果如圖1-47所示。

圖1-47 在小矩陣中使用逗號分隔的初始化函數
█ 5.使用clone()或者copyTo()函數為一個存在的Mat對象創建一個新的信息頭
#include<iostream>
#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main()
{
//創建一個類型為8位uchar、顏色為三通道黃色的2×2 Mat對象
// Mat img(2,2, CV_8UC3,Scalar(0,255,255));
// cout << "matrix element" << endl << img << endl;
//... 中間注釋省略前面介紹的代碼
//使用clone()或者copyTo()函數為一個存在的Mat對象創建一個新的信息頭
Mat srcImage(3,3,CV_8UC3,Scalar(0,0,255));
Mat copyImage;
srcImage.copyTo(copyImage);
Mat newImage = srcImage.row(1).clone();
cout << "matrix"<< endl << newImage << endl;
return 0;
}
程序運行結果如圖1-48所示。

圖1-48 使用clone()或者copyTo()函數為一個存在的Mat對象創建一個新的信息頭
- 自己動手實現Lua:虛擬機、編譯器和標準庫
- Power Up Your PowToon Studio Project
- FFmpeg入門詳解:音視頻流媒體播放器原理及應用
- Mastering Python High Performance
- R的極客理想:工具篇
- Python機器學習編程與實戰
- Android驅動開發權威指南
- Instant Debian:Build a Web Server
- MINECRAFT編程:使用Python語言玩轉我的世界
- Oracle Data Guard 11gR2 Administration Beginner's Guide
- Android Sensor Programming By Example
- AI自動化測試:技術原理、平臺搭建與工程實踐
- Flink入門與實戰
- ASP.NET本質論
- A/B 測試:創新始于試驗