數據載入、顯示與保存
要對一張圖像進行處理,首先需要獲得該圖像。在日常生活中,我們可以通過相機、手機等設備獲取照片,並以某種格式存儲在硬盤中。 同樣,在計算機程序中進行圖像處理時,也需要通過特定方式獲取圖像數據,並將其以一定的數據類型存儲在相應的容器中,隨後再通過某種形式展示給用戶。 因此,本章將介紹圖像數據的載入、存儲與輸出,包括圖像與視頻的讀取、圖像存儲容器的創建與使用,以及將處理後的結果以圖像或視頻形式進行保存等內容。
圖像存儲容器
與我們日常所看到的圖像不同,數字圖像在計算機中是以矩陣的形式存儲的。矩陣中的每一個元素都用於描述圖像中的某種信息,例如亮度、顏色等,如圖 2-1 所示。 數字圖像處理的本質,就是通過一系列操作,從這些矩陣數據中提取更深層次信息的過程。因此,學習圖像處理的第一步,就是掌握如何對這些矩陣數據進行操作。 對於接觸過 C++ 編程的讀者來說,字符串通常以 string 類型存儲,整數以 int 類型存儲。同樣地,在 OpenCV 中,提供了一個 Mat 類用於存儲矩陣數據。 本節將詳細介紹 Mat 類的使用方法及其支持的運算,通過學習,可以在程序中靈活地使用 Mat 類型變量。

Mat 類介紹
在早期的 OpenCV 1.0 版本中,圖像是使用名為 IplImage 的 C 語言結構體進行存儲的。因此,在一些較老的 OpenCV 教程中,仍然可以看到它的使用。
然而,IplImage 類型存在一個明顯的缺點——需要用戶手動釋放內存。如果程序結束時仍有未釋放的 IplImage 變量,就會導致內存洩漏問題。
隨著 OpenCV 的不斷發展,庫中引入了 C++ 接口,並提供了 Mat 類用於數據存儲。Mat 類採用自動內存管理機制,有效解決了內存釋放問題。當變量不再使用時,其佔用的內存會被自動釋放。
Mat 類用於保存矩陣類型的數據,包括向量、矩陣,以及灰度圖像和彩色圖像等。
從結構上看,Mat 類由兩部分組成:矩陣頭(header)和指向實際數據的指針。矩陣頭中包含矩陣的尺寸、存儲方式、數據地址以及引用計數等信息。矩陣頭的大小是固定的,不會隨著矩陣尺寸的變化而改變。
在絕大多數情況下,矩陣頭所佔空間遠小於矩陣數據本身,因此,在圖像複製和傳遞過程中,主要的開銷來自於數據部分。
為了解決這一問題,OpenCV 在複製或傳遞圖像時,並不會複製完整的數據,而是僅複製矩陣頭以及指向數據的指針。因此,在創建 Mat 對象時,可以先創建矩陣頭,再為其賦值數據。其具體方法如代碼清單 2-1 所示。
cv::Mat a; // 创建一个名为 a 的矩阵头
a = cv::imread("test.jpg"); // 读取图像数据,使 a 指向图像像素数据
cv::Mat b = a; // 复制矩阵头,并命名为 b
上述代碼首先創建了一個名為 a 的矩陣頭,隨後讀取一張圖像,並使 a 中的矩陣指針指向該圖像的像素數據。最後,將 a 的矩陣頭複製給 b。 需要注意的是,雖然 a 和 b 各自擁有獨立的矩陣頭,但它們的矩陣指針指向的是同一塊數據。因此,通過任意一個矩陣頭對數據進行修改,另一個矩陣頭所訪問的數據也會隨之改變。 此外,當刪除變量 a 時,b 並不會變成空數據;只有當 a 和 b 都被釋放後,底層的矩陣數據才會真正被釋放。 這是因為在矩陣頭中維護了一個引用計數(reference count),用於記錄當前有多少個對象共享同一塊數據。只有當引用計數減為 0 時,該數據才會被釋放。
tips:採用引用次數來釋放存儲內容是 C++中常見的方式,用這種方式可以避免仍有某個變量引用數據時將這個數據刪除造成程序崩潰的問題,同時極大地縮減程序運行時所佔用的內存。
接下來講解 Mat 類裡可以存儲的數據類型。根據官方給出的 Mat 類繼承關係圖,如圖 2-2 所示,我們發現 Mat 類可以存儲的數據類型包含 double、float、uchar、unsigned char, 以及自定義的模板等。

我們可以通過代碼清單 2-2 的方式聲明一個存放指定類型的 Mat 類變量。
cv::Mat A = cv::Mat_<double>(3, 3); // 创建一个 3×3 的 double 类型矩阵
由於 OpenCV 中的 Mat 類主要用於存儲圖像數據,而像素值的範圍直接影響圖像的質量。如果使用 8 位無符號整數去存儲 16 位圖像數據,就會導致嚴重的顏色失真,甚至產生數據錯誤。
此外,不同位數的編譯器對數據類型長度的定義可能存在差異。為了避免在不同平臺或環境下,由於變量位數不一致而導致程序運行出現問題,OpenCV 對數據類型進行了統一規範。
因此,OpenCV 根據數值變量的存儲位數,定義了一套專用的數據類型體系。表 2-1 列出了 OpenCV 中常用的數據類型及其取值範圍。
| 數據類型 | 具體類型 | 取值範圍 |
|---|---|---|
| CV_8U | 8 位無符號整數 | 0 ~ 255 |
| CV_8S | 8 位有符號整數 | -128 ~ 127 |
| CV_16U | 16 位無符號整數 | 0 ~ 65535 |
| CV_16S | 16 位有符號整數 | -32768 ~ 32767 |
| CV_32S | 32 位有符號整數 | -2147483648 ~ 2147483647 |
| CV_32F | 32 位浮點數 | -FLT_MAX ~ FLT_MAX,INF,NaN |
| CV_64F | 64 位浮點數 | -DBL_MAX ~ DBL_MAX,INF,NaN |
僅有數據類型還不足以完整描述圖像數據,還需要定義圖像的通道(Channel)數量。例如,灰度圖像是單通道數據,而彩色圖像通常是 3 通道(RGB)或 4 通道(如 RGBA)數據。
針對這一需求,OpenCV 定義了通道數標識:C1、C2、C3、C4,分別表示單通道、雙通道、三通道和四通道。 由於每種數據類型都可能對應不同的通道數,因此,OpenCV 將“數據類型”和“通道數”結合起來,用於完整描述圖像數據類型。例如: CV_8UC1:表示 8 位無符號、單通道數據,通常用於灰度圖像 CV_8UC3:表示 8 位無符號、三通道數據,通常用於彩色圖像 我們可以通過代碼清單 2-3 的方式,創建指定數據類型和通道數的 Mat 對象。
cv::Mat a(640, 480, CV_8UC3); // 创建一个 640×480 的 3 通道矩阵(用于彩色图像)
cv::Mat b(3, 3, CV_8UC1); // 创建一个 3×3 的 8 位无符号单通道矩阵
cv::Mat c(3, 3, CV_8U); // 创建单通道矩阵,C1 标识可以省略
雖然在 64 位編譯器中,uchar 和 CV_8U 都表示 8 位無符號整數,但兩者有嚴格的區分:CV_8U 只能用於 Mat 類內部的數據類型標識。如果使用 Mat_<CV_8U>(3,3) 或 Mat a(3,3,uchar),都會提示創建錯誤。
Mat 類構造與賦值
前一小節已經介紹了 3 種構造 Mat 類變量的方法,但是後兩種沒有給變量初始化賦值,本小將重點介紹如何靈活地構造並賦值 Mat 類變量。根據 OpenCV 的源碼定義,關於 Mat 類的構造方式共有 20 餘種,然而,在平時一些簡單的應用程序中,很多複雜的構造方式並沒有太多的用武之地,因此本書重點講解作者在學習和做項目中常用的構造與賦值方式。
1. Mat 類的構造 (1) 利用默認構造函數(見代碼清單2-4)
cv::Mat::Mat();
通過代碼清單2-4,利用默認構造函數構造了一個Mat類,這種構造方式不需要輸入任何的參數,在後續給變量賦值的時候會自動判斷矩陣的類型與大小,實現靈活的存儲,常用於存儲讀取的圖像數據和某個函數運算的輸出結果。
(2) 根據輸入矩陣尺寸和類型構造(見代碼清單2-5)
cv::Mat::Mat(int rows,
int cols,
int type);
- rows: 構造矩陣的行數。
- cols: 矩陣的列數。
- type: 除CV_8UC1、CV_64FC4等從1到4通道以外,還提供了更多通道的參數,通過CV_8UC(n)中的n來構建多通道矩陣,其中n最大可以取到512。
這種構造方法我們在前文中也見過,通過輸入矩陣的行、列,以及存儲數據類型,實現構造。這種定義方式清晰、直觀,易於閱讀,常用在明確需要存儲數據尺寸和數據類型的情況下,例如相機的內參矩陣、物體的旋轉矩陣等。 利用輸入矩陣尺寸和數據類型構造Mat類的方法存在一種變形,通過將行和列組成一個Size()結構進行賦值,代碼清單2-6中給出了這種構造方法的原型。
代碼清單2-6 用Size()結構構造Mat類
cv::Mat::Mat(Size size(),
int type);
- size: 二維數組變量尺寸,通過Size(cols,rows)進行賦值。
- type: 與代碼清單2-5中的參數一致。
利用這種方式構造Mat類時要格外注意,在Size()結構裡,矩陣的行和列的順序與代碼清單2-5中的方法相反,使用Size()時(見代碼清單2-7),列在前、行在後。如果不注意,雖然同樣會構造成功Mat類,但是當我們需要查看某個元素時,我們並不知道行與列顛倒,就可能會出現數組越界的錯誤。
代碼清單2-7 用Size結構構造Mat示例
cv::Mat a(Size(480, 640), CV_8UC1); // 构造一个行为640、列为480的单通道矩阵
cv::Mat b(Size(480, 640), CV_32FC3); // 构造一个行为640、列为480的3通道矩阵
(3) 利用已有矩陣構造(見代碼清單2-8)
cv::Mat::Mat(const Mat &m);
- m: 已經構建完成的Mat類矩陣數據。
這種構造方式非常簡單,可以構造出與已有Mat類變量存儲內容一樣的變量。注意,這種構造方式只是複製了Mat類的矩陣頭,矩陣指針指向的是同一個地址,因此,如果通過某一個Mat類變量修改了矩陣中的數據,那麼另一個變量中的數據也會發生改變。
提示:如果希望複製兩個一模一樣的Mat類而彼此之間不會受影響,那麼可以使用clone()函數。
如果需要構造的矩陣尺寸比已有矩陣小,並且存儲的是已有矩陣的子內容,那麼可以用代碼清單2-9中的方法進行構建。
代碼清單2-9 構造已有Mat類的子類
cv::Mat::Mat(const Mat &m,
const Range &rowRange,
const Range &colRange = Range::all());
- m: 已經構建完成的Mat類矩陣數據。
- rowRange: 在已有矩陣中需要截取的行數範圍,是一個Range變量,例如從第2行到第5行可以表示為Range(2,5)。
- colRange: 在已有矩陣中需要截取的列數範圍,是一個Range變量,例如從第2列到第5列可以表示為Range(2,5),當不輸入任何值時,表示所有列都會被截取。
這種方式主要用於在原圖中截圖使用。不過需要注意的是,通過這種方式構造的Mat類與已有Mat類享有共同的數據,即如果兩個Mat類中有一個數據發生更改,那麼另一個也會隨之更改。
代碼清單2-10 在原Mat中截取子Mat類
cv::Mat b(a, Range(2, 5), Range(2, 5)); // 从a中截取部分数据构造b
cv::Mat c(a, Range(2, 5)); // 默认最后一个参数构造c
2. Mat類的賦值 構建完成Mat類後,變量裡並沒有數據,需要將數據賦值給它。針對不同情況,OpenCV 4.1提供了多種賦值方式,下面介紹如何給Mat類變量賦值。
(1) 構造時賦值(見代碼清單2-11)
cv::Mat::Mat(int rows,
int cols,
int type,
const Scalar &s);
- rows: 矩陣的行數。
- cols: 矩陣的列數。
- type: 存儲數據的類型。
- s: 給矩陣中每個像素賦值的參數變量,例如Scalar(0,0,255)。
該種方式是在構造的同時進行賦值(見代碼清單2-12),將每個元素要賦予的值放入Scalar結構中即可。這裡需要注意的是,用此方法會將圖像中的每個元素賦予相同的數值,例如Scalar(0,0,255)會將每個像素的3個通道值分別賦為0、0、255。
代碼清單2-12 在構造時賦值示例
cv::Mat a(2, 2, CV_8UC3, cv::Scalar(0, 0, 255)); // 创建一个3通道矩阵,每个像素都是0,0,255
cv::Mat b(2, 2, CV_8UC2, cv::Scalar(0, 255)); // 创建一个2通道矩阵,每个像素都是0,255
cv::Mat c(2, 2, CV_8UC1, cv::Scalar(255)); // 创建一个单通道矩阵,每个像素都是255
我們在程序return語句之前加上斷點進行調試,用Image Watch查看每一個Mat類變量裡的數據,結果如圖2-3所示,證明已成功構造矩陣並賦值。
提示:Scalar結構中變量的個數一定要與定義中的通道數相對應。如果Scalar結構中變量的個數大於通道數,則位置在大於通道數之後的數值將不會被讀取,例如執行a(2,2,CV_8UC2,Scalar(0,0,255))後,每個像素值都將是(0,0),而255不會被讀取;如果Scalar結構中變量的個數小於通道數,則會以0補充。

(2) 枚舉法賦值
這種賦值方式是將矩陣中所有的元素一一列舉,並用數據流的形式賦值給Mat類。具體賦值形式如代碼清單2-13所示。
代碼清單2-13 利用枚舉法賦值示例
cv::Mat a = (cv::Mat_<int>(3, 3) << 1, 2, 3, 4, 5, 6, 7, 8, 9);
cv::Mat b = (cv::Mat_<double>(2, 3) << 1.0, 2.1, 3.2, 4.0, 5.1, 6.2);
上面第一行代碼創建了一個3×3的矩陣,矩陣中存放的是1~9的9個整數,先將矩陣中的第一行存滿,之後再存入第二行、第三行,即1、2、3存放在矩陣a的第一行,4、5、6存放在矩陣a的第二行,7、8、9存放在矩陣a的第三行。第二行代碼創建了一個2×3的矩陣,其存放方式與矩陣a相同。
提示:在採用枚舉法時,輸入的數據個數一定要與矩陣元素個數相同,例如,在代碼清單2-13中第一行代碼只輸入1~8共8個數時,賦值過程會出現報錯,因此本方法常用在矩陣數據比較少的情況下。
(3) 循環法賦值
與通過枚舉法賦值方法相似,循環法賦值也是對矩陣中的每一個元素進行賦值,但是可以不在聲明變量的時候進行賦值,而且可以對矩陣中的任意部分進行賦值。具體賦值形式如代碼清單2-14所示。
代碼清單2-14 利用循環法賦值示例
cv::Mat c = cv::Mat(3, 3, CV_8UC1); // 定义一个3*3的矩阵
for (int i = 0; i < c.rows; i++) // 矩阵行数循环
{
for (int j = 0; j < c.cols; j++) // 矩阵列数循环
{
c.at<uchar>(i, j) = i + j;
}
}
上面代碼同樣創建了一個3×3的矩陣,通過for循環的方式,對矩陣中的每一個元素進行賦值。需要注意的是,在給矩陣每個元素賦值的時候,賦值函數中聲明的變量類型要與矩陣定義時的變量類型相同,即代碼清單2-14中第1行和第6行中變量類型要相同,如果第6行代碼改成c.at<double>程序就會報錯,無法賦值。
(4) 類方法賦值
在Mat類裡,提供了可以快速賦值的方法,可以初始化指定的矩陣。例如,生成單位矩陣、對角矩陣、所有元素都為0或者1的矩陣等。具體使用方法如代碼清單2-15所示。
代碼清單2-15 利用類方法賦值示例
cv::Mat a = cv::Mat::eye(3, 3, CV_8UC1);
cv::Mat b = (cv::Mat_<int>(1, 3) << 1, 2, 3);
cv::Mat c = cv::Mat::diag(b);
cv::Mat d = cv::Mat::ones(3, 3, CV_8UC1);
cv::Mat e = cv::Mat::zeros(4, 2, CV_8UC3);
上面代碼中的每個函數的作用及參數的含義介紹如下:
- eye(): 構建一個單位矩陣,前兩個參數為矩陣的行數和列數,第三個參數為矩陣存放的數據類型與通道數。如果行和列不相等,則在矩陣的(1,1)、(2,2)、(3,3)等主對角位置處為1。
- diag(): 構建對角矩陣,其參數必須是Mat類型的一維變量,用來存放對角元素的數值。
- ones(): 構建一個全為1的矩陣,參數含義與eye()相同。
- zeros(): 構建一個全為0的矩陣,參數含義與eye()相同。
(5) 利用數組進行賦值
這種方法與枚舉法類似,但是該方法可以根據需求改變Mat類矩陣的通道數,可以看作枚舉法的拓展,在代碼清單2-16中給出了這種方法的賦值形式。
代碼清單2-16 利用數組進行賦值示例
float a[8] = {5, 6, 7, 8, 1, 2, 3, 4};
cv::Mat b = cv::Mat(2, 2, CV_32FC2, a);
cv::Mat c = cv::Mat(2, 4, CV_32FC1, a);
這種賦值方式首先將需要存入Mat類中的變量存入一個數組中,之後通過設置Mat類矩陣的尺寸和通道數將數組變量拆分成矩陣,這種拆分方式可以自由定義矩陣的通道數。當矩陣中的元素數目大於數組中的數據時,將用1.0737418e+08填充賦值給矩陣;當矩陣中元素的數目小於數組中的數據時,將矩陣賦值完成後,數組中剩餘數據將不再賦值。由數組賦值給矩陣的過程是首先將矩陣中第一個元素的所有通道依次賦值,之後再賦值下一個元素。為了更好地體會這個過程,我們將定義的b和c矩陣在圖2-4中給出。

Mat類支持的運算
在處理數據時,需要對數據進行加減乘除運算,例如對圖像進行濾波、增強等操作都需要對像素級別進行加減乘除運算。為了方便運算,Mat類變量支持矩陣的加減乘除運算,即我們在使用Mat類變量時,將其看作普通的矩陣即可,例如Mat類變量與常數相乘遵循矩陣與常數相乘的運算法則。Mat類與常數運算時,可以直接通過加減乘除符號實現。代碼清單2-17中給出了Mat類變量與常數進行加減乘除運算的示例程序。
代碼清單2-17 Mat類的加減乘除運算
cv::Mat a = (cv::Mat_<int>(3, 3) << 1, 2, 3, 4, 5, 6, 7, 8, 9);
cv::Mat b = (cv::Mat_<int>(3, 3) << 1, 2, 3, 4, 5, 6, 7, 8, 9);
cv::Mat c = (cv::Mat_<double>(3, 3) << 1.0, 2.1, 3.2, 4.0, 5.1, 6.2, 2, 2, 2);
cv::Mat d = (cv::Mat_<double>(3, 3) << 1.0, 2.1, 3.2, 4.0, 5.1, 6.2, 2, 2, 2);
cv::Mat e, f, g, h, i;
e = a + b;
f = c - d;
g = 2 * a;
h = d / 2.0;
i = a - 1;
這裡需要注意的是,當兩個Mat類變量進行加減運算時,必須保證兩個矩陣中的數據類型是相同的,即兩個分別保存int和double數據類型的Mat類變量不能進行加減運算。與常規的乘除法不同之處在於,常數與Mat類變量運算結果的數據類型保留Mat類變量的數據類型,例如,double類型的常數與int類型的Mat類變量運算,最後結果仍然為int類型。在代碼清單2-17的最後一行代碼中,Mat類變量減去一個常數,表示的含義是Mat類變量中的每一個元素都要減去這個常數。
在對圖像進行卷積運算時,需要兩個矩陣進行乘法運算,OpenCV不但提供了兩個Mat類矩陣的乘法運算,而且定義了兩個矩陣的內積和對應位的乘法運算。代碼清單2-18中給出了兩個Mat類矩陣的乘法的代碼實現。
代碼清單2-18 兩個Mat類矩陣的乘法運算
cv::Mat j, m;
double k;
j = c * d; //乘法
k = a.dot(b); //内积
m = a.mul(b); //对位乘法
代碼清單2-18中矩陣定義和賦值與代碼清單2-17中相同。在代碼中定義了兩個Mat類變量和一個double變量,分別實現了兩個Mat類矩陣的乘法、內積和對應位乘法。
第3行代碼的""運算符表示兩個矩陣的數學乘積,例如存在兩個矩陣A₃ₓ₃和B₃ₓ₃,""運算結果為矩陣C₃ₓ₃,C₃ₓ₃中的每一個元素表示為:

需要注意的是,"*"運算要求第一個Mat類矩陣的列數必須與第二個Mat類矩陣的行數相同,而且該運算要求Mat類中的數據類型必須是CV_32FC1、CV_64FC1、CV_32FC2、CV_64FC2這4種中的一種,也就是對於一個二維的Mat類矩陣,其保存的數據類型必須是float類型或者double類型。 代碼清單2-18中的第4行代碼表示兩個Mat類矩陣的內積。根據輸出結果可以知道dot()方法結果是一個double類型的變量,該運算的目的是求取一個行向量和一個列向量點乘,例如存在兩個向量d = d₁ d₂ d₃和e = e₁ e₂ e₃,經過dot()方法運算的結果為:

需要注意的是,輸入的兩個Mat類矩陣必須具有相同的元素數目,但是無論輸入的兩個Mat類矩陣的維數是多少,都會將兩個Mat類矩陣擴展成一個行向量和一個列向量,因此dot()運算的結果永遠是一個double類型的變量。
代碼清單2-18中的第5行代碼表示兩個Mat類矩陣對應位的乘積。根據輸出結果可以知道mul()方法運算結果同樣是一個Mat類矩陣。對於兩個矩陣A₃ₓ₃和B₃ₓ₃,經過mul()方法運算的結果C₃ₓ₃中每一個元素都可以表示為:

需要注意的是,不同於前兩種乘法運算,參與mul()方法運算的兩個Mat類矩陣中保存的數據在保證相同的前提下,可以是任何一種類型,並且默認的輸出數據類型與兩個Mat類矩陣保持一致。在圖像處理領域,常用的數據類型是CV_8U,其範圍是0~255,當兩個比較大的整數相乘時,就會產生結果溢出的現象,輸出結果為255,因此,在使用mul()方法時,需要防止出現數據溢出的問題。
Mat類元素的讀取
對於Mat類矩陣的讀取與更改,我們已經在矩陣的循環賦值中介紹過如何用at方法對矩陣的每一位進行賦值,這只是OpenCV提供的多種讀取矩陣元素方式中的一種,本小節將詳細介紹如何讀取Mat類矩陣中的元素,並對其數值進行修改。
在學習如何讀取Mat類矩陣元素之前,首先需要知道Mat類變量在計算機中是如何存儲的。多通道的Mat類矩陣類似於三維數據,而計算機的存儲空間是一個二維空間,因此Mat類矩陣在計算機中存儲時是將三維數據變成二維數據,先存儲第一個元素每個通道的數據,之後再存儲第二個元素每個通道的數據。每一行的元素都按照這種方式進行存儲,因此,如果我們找到了每個元素的起始位置,那麼可以找到這個元素中每個通道的數據。圖2-5展示了一個三通道矩陣的存儲方式,其中連續的藍色、綠色和紅色方塊分別代表每個元素的3個通道。

在瞭解了Mat類變量的存儲方式之後,我們來看Mat類具有的屬性。表2-2中列出了Mat類矩陣常用的屬性,同時詳細地介紹了每種屬性的作用。
表2-2 Mat類矩陣常用的屬性
| 屬性 | 作用 |
|---|---|
| cols | 矩陣的列數 |
| rows | 矩陣的行數 |
| step | 以字節為單位的矩陣的有效寬度 |
| elemSize() | 每個元素的字節數 |
| total() | 矩陣中元素的個數 |
| channels() | 矩陣的通道數 |
這些屬性之間互相組合可以得到多數Mat類矩陣的屬性,例如step屬性與cols屬性組合,可以求出每個元素所佔據的字節數,再與channels()屬性結合,就可以知道每個通道的字節數,進而知道矩陣中存儲的數據量的類型。下面通過一個例子具體說明每個屬性的用處:用Mat(3, 4, CV_32FC3)定義一個矩陣,這時通道數channels()為3;列數cols為4;行數rows為3;矩陣中元素的個數為3×4,結果為12;每個元素的字節數為32/8×channels(),本例最後結果為12;以字節為單位的有效長度step為elemSize()×cols,本例結果為48。
常用的Mat類矩陣的元素讀取方式包括通過at方法進行讀取、通過指針ptr進行讀取、通過迭代器進行讀取、通過矩陣元素的地址定位方式進行讀取。下面將詳細介紹這4種讀取方式。
1. 通過at方法讀取Mat類矩陣中的元素
通過at方法讀取矩陣元素分為針對單通道的讀取方法和針對多通道的讀取方法,在代碼清單2-19中給出了通過at方法讀取單通道矩陣元素的代碼。 代碼清單2-19 at方法讀取Mat類單通道矩陣元素
cv::Mat a = (cv::Mat_<uchar>(3, 3) << 1, 2, 3, 4, 5, 6, 7, 8, 9);
int value = (int)a.at<uchar>(0, 0);
通過at方法讀取元素需要在後面跟上"<數據類型>",如果此處的數據類型與矩陣定義時的數據類型不相同,就會出現因數據類型不匹配而產生的報錯信息。該方法以座標的形式給出需要讀取的元素座標(行數, 列數)。需要說明的是,如果矩陣定義的是uchar類型的數據,那麼在需要輸入數據的時候,需要強制轉換成int類型的數據進行輸出,否則輸出的結果並不是整數。
由於單通道圖像是一個二維矩陣,因此在at方法的最後給出二維平面座標即可訪問對應位置元素。而多通道矩陣每一個元素座標處都是多個數據,因此引入一個變量用於表示同一元素的多個數據。在OpenCV中,針對三通道矩陣,定義了cv::Vec3b、cv::Vec3s、cv::Vec3w、cv::Vec3d、cv::Vec3f、cv::Vec3i共6種類型用於表示同一個元素的3個通道數據。通過這6種數據類型可以總結出其命名規則,其中的數字表示通道的個數,最後一位是數據類型的縮寫,b是uchar類型的縮寫、s是short類型的縮寫、w是ushort類型的縮寫、d是double類型的縮寫、f是float類型的縮寫、i是int類型的縮寫。當然,OpenCV也為二通道和四通道定義了對應的變量類型,其命名方式也遵循這個命名規則,例如二通道和四通道的uchar類型分別用cv::Vec2b和cv::Vec4b表示。代碼清單2-20中給出了通過at方法讀取多通道矩陣的實現代碼。
代碼清單2-20 at方法讀取Mat類多通道矩陣元素
cv::Mat b(3, 4, CV_8UC3, cv::Scalar(0, 0, 1));
cv::Vec3b vc3 = b.at<cv::Vec3b>(0, 0);
int first = (int)vc3.val[0];
int second = (int)vc3.val[1];
int third = (int)vc3.val[2];
在使用多通道變量類型時,同樣需要注意at方法中數據變量類型與矩陣的數據變量類型相對應,並且cv::Vec3b類型在輸入每個通道數據時需要將其變量類型強制轉換成int類型。不過,如果將at方法讀取出的數據直接賦值給cv::Vec3i類型變量,就不需要在輸出每個通道數據時進行數據類型的強制轉換。
2. 通過指針ptr讀取Mat類矩陣中的元素
前面我們分析過Mat類矩陣在內存中的存放方式,矩陣中每一行中的每個元素都是挨著存放的,如果找到每一行元素的起始地址位置,那麼讀取矩陣中每一行不同位置的元素時將指針在起始位置向後移動若干位即可。在代碼清單2-21中,給出了通過指針ptr讀取Mat類矩陣元素的代碼實現。 代碼清單2-21 指針ptr讀取Mat類矩陣元素
cv::Mat b(3, 4, CV_8UC3, cv::Scalar(0, 0, 1));
for (int i = 0; i < b.rows; i++)
{
uchar* ptr = b.ptr<uchar>(i);
for (int j = 0; j < b.cols * b.channels(); j++)
{
cout << (int)ptr[j] << endl;
}
}
在程序裡,首先有一個大循環用來控制矩陣中每一行,之後定義一個uchar類型的指針ptr,在定義時需要聲明Mat類矩陣的變量類型,並在定義最後用小括號聲明指針指向Mat類矩陣的哪一行。第二個循環控制用於輸出矩陣中每一行所有通道的數據。根據圖2-5中所示的存儲形式,每一行中存儲的數據數量為列數與通道數的乘積,即指針可以向後移動cols×channels()位,如第7行代碼所示,指針向後移動的位數在中括號給出。程序中給出了循環遍歷Mat類矩陣中的每一個數據的方法,當我們能夠確定需要訪問的數據時,可以直接通過給出行數和指針後移的位數進行訪問,例如,當讀取第2行數據中第3個數據時,可以直接通過ptr2訪問。
3. 通過迭代器訪問Mat類矩陣中的元素
Mat類變量同時也是一個容器變量,因此,Mat類變量擁有迭代器,用於訪問Mat類變量中的數據,通過迭代器可以實現對矩陣中每一個元素的遍歷,代碼實現在代碼清單2-22中給出。 代碼清單2-22 通過迭代器讀取Mat類矩陣元素
cv::MatIterator_<uchar> it = a.begin<uchar>();
cv::MatIterator_<uchar> it_end = a.end<uchar>();
for (int i = 0; it != it_end; it++)
{
cout << (int)(*it) << " ";
if ((++i % a.cols) == 0)
{
cout << endl;
}
}
Mat類的迭代器變量類型是cv::MatIterator_<>,在定義時同樣需要在尖括號中聲明數據的變量類型。Mat類迭代器的起始是Mat.begin<>(),結束是Mat.end<>(),與其他迭代器用法相同,通過"++"運算實現指針位置向下迭代,數據的讀取方式是先讀取第一個元素的每一個通道,之後再讀取第二個元素的每一個通道,直到最後一個元素的最後一個通道。
4. 通過矩陣元素的地址定位方式訪問元素
前面3種讀取元素的方式都需要知道Mat類矩陣存儲數據的類型,而且在認知上,我們更希望能夠通過聲明"第X行第X列第X通道"的方式來讀取某個通道內的數據,代碼清單2-23中給出的就是這種讀取數據的方式。
代碼清單2-23 通過矩陣元素的地址定位方式訪問元素
(int)(*(b.data + b.step[0] * row + b.step[1] * col + channel));
代碼中row變量的含義是某個數據所在元素的行數,col變量的含義是某個數據所在元素的列數,channel變量的含義是某個數據所在元素的通道數。這種方式與我們通過指針讀取數據的形式類似,都是通過將首個數據的地址指針移動若干位後指向需要讀取的數據,只不過這種方式可以通過直接給出行、列和通道數進行讀取,不需要用戶再計算某個數據在這行數據存儲空間中的位置。
圖像的讀取與顯示
本節中將詳細介紹圖像讀取和顯示的相關功能。
圖像讀取函數imread
我們在前面已經介紹過了圖像讀取函數imread()的調用方式(見代碼清單1-1),這裡我們給出函數的原型(見代碼清單2-24)。 代碼清單2-24 imread()函數的原型
cv::Mat cv::imread(const String &filename,
int flags = IMREAD_COLOR);
- filename: 需要讀取圖像的文件名稱,包含圖像地址、名稱和圖像文件擴展名。
- flags: 讀取圖像形式的標誌,如將彩色圖像按照灰度圖讀取,默認參數是按照彩色圖像格式讀取,可選參數在表2-3中給出。
函數用於讀取指定的圖像並將其返回給一個Mat類變量,當圖像文件不存在、破損或者格式不受支持時,則無法讀取圖像,此時函數返回一個空矩陣,因此可以通過返回矩陣的data屬性是否為空或者empty()函數是否為真來判斷是否成功讀取圖像,如果讀取圖像失敗,那麼data屬性返回值為0,empty()函數返回值為1。
函數能夠讀取多種格式的圖像文件,但是,在不同操作系統中,由於使用的編解碼器不同,因此在某個系統中能夠讀取的圖像文件可能在其他系統中就無法讀取。無論在哪個系統中,BMP文件和DIB文件都是始終可以讀取的。在Windows和macOS系統中,默認情況下使用OpenCV自帶的編解碼器(libjpeg、libpng、libtiff和libjasper),因此可以讀取JPEG (jpg、jpeg、jpe)、PNG、TIFF (tiff、tif)文件,在Linux系統中,需要自行安裝這些編解碼器,安裝後同樣可以讀取這些類型的文件。
不過需要說明的是,該函數能否讀取文件數據與擴展名無關,而是通過文件的內容確定圖像的類型,例如,在將一個擴展名由png修改成exe時,該函數一樣可以讀取該圖像,但是將擴展名exe改成png,該函數不能加載該文件。 該函數第一個參數以字符串形式給出待讀取圖像的地址,第二個參數是設置讀取圖像的形式,默認的參數是以彩色圖的形式讀取,針對不同需求可以更改參數,在OpenCV 4.1中給出了13種模式讀取圖像的形式,總結起來分別是以原樣式讀取、灰度圖讀取、彩色圖讀取、多位數讀取、在讀取時將圖像縮小一定尺寸等形式,具體可選擇的參數及作用見表2-3。這裡需要指出的是,將彩色圖像轉成灰度圖通過編解碼器內部轉換,可能會與OpenCV程序中將彩色圖像轉成灰度圖的結果存在差異。這些標誌參數在功能不衝突的前提下可以同時聲明多個,不同參數之間用"|"隔開。
表2-3 imread()函數讀取圖像形式參數
| 標誌參數 | 簡記 | 作用 |
|---|---|---|
| IMREAD_UNCHANGED | -1 | 按照圖像原樣讀取,保留Alpha通道(第4通道) |
| IMREAD_GRAYSCALE | 0 | 將圖像轉成單通道灰度圖像後讀取 |
| IMREAD_COLOR | 1 | 將圖像轉換成3通道BGR彩色圖像 |
| IMREAD_ANYDEPTH | 2 | 保留原圖像的16位、32位深度,不聲明該參數則轉成8位讀取 |
| IMREAD_ANYCOLOR | 4 | 以任何可能的顏色讀取圖像 |
| IMREAD_LOAD_GDAL | 8 | 使用gdal驅動程序加載圖像 |
| IMREAD_REDUCED_GRAYSCALE_2 | 16 | 將圖像轉成單通道灰度圖像,尺寸縮小1/2。可以更改最後一位數字實現縮小1/4(最後一位改為4)和1/8(最後一位改為8) |
| IMREAD_REDUCED_COLOR_2 | 17 | 將圖像轉成3通道彩色圖像,尺寸縮小1/2。可以更改最後一位數字實現縮小1/4(最後一位改為4)和1/8(最後一位改為8) |
| IMREAD_IGNORE_ORIENTATION | 128 | 不以EXIF的方向旋轉圖像 |
注意:在默認情況下,讀取圖像的像素數目必須小於2³⁰,這個要求在絕大多數圖像處理領域是不受影響的,但是衛星遙感圖像、超高分辨率圖像的像素數目可能會超過這個閾值。可以通過修改系統變量中的OPENCV_IO_MAX_IMAGE_PIXELS參數調整能夠讀取的最大像素數目。
圖像窗口函數namedWindow
在我們之前的程序中並沒有介紹過窗口函數,因為在顯示圖像時如果沒有主動定義圖像窗口,程序會自動生成一個窗口用於顯示圖像,然而有時需要在顯示圖像之前對圖像窗口進行操作,例如添加滑動條,此時就需要提前創建圖像窗口。代碼清單2-25中給出了創建窗口函數的原型。 代碼清單2-25 namedWindow()函數的原型
void cv::namedWindow(const String &winname,
int flags = WINDOW_AUTOSIZE);
- winname: 窗口名稱,用作窗口的標識符。
- flags: 窗口屬性設置標誌。
該函數會創建一個窗口變量,用於顯示圖像和滑動條,通過窗口的名稱引用該窗口,如果在創建窗口時已經存在具有相同名稱的窗口,則該函數不會執行任何操作。創建一個窗口需要佔用部分內存資源,因此,通過該函數創建窗口後,在不需要窗口時需要關閉窗口來釋放內存資源。OpenCV提供了兩個關閉窗口資源的函數,分別是cv::destroyWindow()函數和cv::destroyAllWindows()。通過名稱我們可以知道,前一個函數是用於關閉一個指定名稱的窗口,即在括號內輸入窗口名稱的字符串即可將對應窗口關閉;後一個函數是關閉程序中所有的窗口,一般用於程序的最後。
不過事實上,在一個簡單的程序裡,我們並不需要調用這些函數,因為程序退出時會自動關閉應用程序的所有資源和窗口。雖然不主動釋放窗口也會在程序結束時釋放窗口資源,但是OpenCV 4.0版在結束時會報出沒有釋放窗口的錯誤,而OpenCV 4.1版則不會報錯。
該函數的第一個參數是聲明窗口的名稱,用於窗口的唯一識別。第二個參數是聲明窗口的屬性,主要用於設置窗口的大小是否可調、顯示的圖像是否填充滿窗口等,具體可選擇的參數及含義在表2-4中給出,默認情況下,函數加載的標誌參數為WINDOW_AUTOSIZE | WINDOW_KEEPRATIO | WINDOW_GUI_EXPANDED。
表2-4 namedWindow()函數窗口屬性標誌參數
| 標誌參數 | 簡記 | 作用 |
|---|---|---|
| WINDOW_NORMAL | 0x00000000 | 顯示圖像後,允許用戶隨意調整窗口大小 |
| WINDOW_AUTOSIZE | 0x00000001 | 根據圖像大小顯示窗口,不允許用戶調整大小 |
| WINDOW_OPENGL | 0x00001000 | 創建窗口的時候會支持OpenGL |
| WINDOW_FULLSCREEN | 1 | 全屏顯示窗口 |
| WINDOW_FREERATIO | 0x00000100 | 調整圖像尺寸以充滿窗口 |
| WINDOW_KEEPRATIO | 0x00000000 | 保持圖像的比例 |
| WINDOW_GUI_EXPANDED | 0x00000000 | 創建的窗口允許添加工具欄和狀態欄 |
| WINDOW_GUI_NORMAL | 0x00000010 | 創建沒有狀態欄和工具欄的窗口 |
圖像顯示函數imshow
我們在前面已經介紹過了圖像顯示函數imshow()的調用方式,這裡我們給出函數的原型(見代碼清單2-26)。 代碼清單2-26 imshow()函數的原型
void cv::imshow(const String &winname,
InputArray mat);
- winname: 要顯示圖像的窗口的名字,用字符串形式賦值。
- mat: 要顯示的圖像矩陣。
該函數會在指定的窗口中顯示圖像。如果在此函數之前沒有創建同名的圖像窗口,就會以WINDOW_AUTOSIZE標誌創建一個窗口,顯示圖像的原始大小;如果創建了圖像窗口,那麼會縮放圖像以適應窗口屬性。該函數會根據圖像的深度將其縮放,具體縮放規則如下:
- 如果圖像是8位無符號類型,那麼按照原樣顯示。
- 如果圖像是16位無符號類型或者32位整數類型,那麼會將像素除以256,將範圍由0,255×256映射到0,255。
- 如果圖像是32位或64位浮點類型,那麼將像素乘以255,即將範圍由0,1映射到0,255。
函數中第一個參數為圖像顯示窗口的名稱,第二個參數是需要顯示的圖像Mat類矩陣。這裡需要特殊說明的是,我們看到第二個參數並不是常見的Mat類,而是InputArray,這個是OpenCV定義的一個類型聲明引用,用作輸入參數的標識,我們在遇到它時可以認為是需要輸入一個Mat類數據。同樣,OpenCV對輸出也定義了OutputArray類型,我們同樣可以認為是輸出一個Mat類數據。
此函數運行後會繼續執行後面程序。如果後面程序執行完直接退出,那麼顯示的圖像有可能閃一下就消失,因此在需要顯示圖像的程序中,往往會在imshow()函數後跟有cv::waitKey()函數,用於將程序暫停一段時間。waitKey()函數是以毫秒計的等待時長,如果參數默認或者為"0",那麼表示等待用戶按鍵結束該函數。
顯示一張圖片的代碼:
#include "chapter2_2_show_image/inc/show_image.hpp"
#include <iostream>
#include <opencv2/opencv.hpp>
void opencv_function1(void)
{
cv::Mat picture_demo_mat = cv::imread(std::string(MEDIA_PATH) + "林星阑L.jpg");
cv::imshow("xiaoshen", picture_demo_mat);
std::cout << "成功运行OpenCV!" << std::endl;
cv::waitKey(0); // 这句确保窗口一直打开
}
顯示一張圖片的代碼(使用英偉達顯卡CUDA加速):
#include "chapter2_2_show_image/inc/show_image_CUDA.hpp"
#include <iostream>
#include <opencv2/opencv.hpp>
void opencv_function2(void)
{
cv::Mat picture_demo_mat = cv::imread(std::string(MEDIA_PATH) + "林星阑H.jpg");
cv::cuda::GpuMat gpuImage;
gpuImage.upload(picture_demo_mat);
cv::Mat result;
gpuImage.download(result);
cv::namedWindow("林星阑",cv::WINDOW_NORMAL);
cv::imshow("林星阑", result);
std::cout << "CUDA成功运行!" << std::endl;
cv::waitKey(0); // 这句确保窗口一直打开
}
視頻加載與攝像頭調用
前面已經介紹瞭如何通過程序讀取圖像數據,本節將介紹OpenCV中為讀取視頻文件和調用攝像頭而設計的VideoCapture類。
視頻數據的讀取
雖然視頻文件是由多張圖片組成的,但imread()函數並不能直接讀取視頻文件,需要由專門的視頻讀取函數進行視頻讀取,並將每一幀圖像保存到Mat類矩陣中。代碼清單2-27中給出了VideoCapture類在讀取視頻文件時的構造方式。 代碼清單2-27 讀取視頻文件VideoCapture類構造函數
cv::VideoCapture::VideoCapture(); // 默认构造函数
cv::VideoCapture::VideoCapture(const String& filename,
int apiPreference = CAP_ANY);
- filename: 讀取的視頻文件或者圖像序列名稱。
- apiPreference: 讀取數據時設置的屬性,例如編碼格式、是否調用OpenNI等。
該函數是構造一個能夠讀取與處理視頻文件的視頻流。代碼清單2-27中的第一行是VideoCapture類的默認構造函數,只是聲明瞭一個能夠讀取視頻數據的類,具體讀取什麼視頻文件,需要在使用時通過open()函數指出,例如cap.open("1.avi")是VideoCapture類變量cap讀取1.avi視頻文件。
第二種構造函數在給出聲明變量的同時也將視頻數據賦值給變量。可以讀取的文件種類包括視頻文件(例如video.avi)、圖像序列或者視頻流的URL。其中讀取圖像序列需要將多個圖像的名稱統一為"前綴+數字"的形式,通過"前綴+%02d"的形式調用,例如,在某個文件夾中有圖片img_00.jpg、img_01.jpg、img_02.jpg加載時,文件名用img_%02d.jpg表示。函數中的讀取視頻設置屬性標籤默認的是自動搜索合適的標誌,因此,在平時使用中,可以將其默認,只輸入視頻名稱。與imread()函數一樣,構造函數同樣有可能讀取文件失敗,因此需要通過isOpened()函數進行判斷。如果讀取成功,則返回值為true;如果讀取失敗,則返回值為false。 通過構造函數只是將視頻文件加載到了VideoCapture類變量中,當我們需要使用視頻中的圖像時,還需要將圖像由VideoCapture類變量裡導出到Mat類變量裡,用於後期數據處理,該操作可以通過">>"運算符將圖像按照視頻順序由VideoCapture類變量賦值給Mat類變量。當VideoCapture類變量中所有的圖像都賦值給Mat類變量後,再次賦值的時候Mat類變量會變為空矩陣,因此可以通過empty()判斷VideoCapture類變量中是否所有圖像都已經讀取完畢。
VideoCapture類變量同時提供了可以查看視頻屬性的get()函數,通過輸入指定的標誌來獲取視頻屬性,例如視頻的像素尺寸、幀數、幀率等。VideoCapture類中get()方法中的常用標誌和含義在表2-5中給出。
表2-5 VideoCapture類中get()方法中的標誌參數
| 標誌參數 | 簡記 | 作用 |
|---|---|---|
| CAP_PROP_POS_MSEC | 0 | 視頻文件的當前位置(以毫秒為單位) |
| CAP_PROP_FRAME_WIDTH | 3 | 視頻流中圖像的寬度 |
| CAP_PROP_FRAME_HEIGHT | 4 | 視頻流中圖像的高度 |
| CAP_PROP_FPS | 5 | 視頻流中圖像的幀率(每秒幀數) |
| CAP_PROP_FOURCC | 6 | 編解碼器的4字符代碼 |
| CAP_PROP_FRAME_COUNT | 7 | 視頻流中圖像的幀數 |
| CAP_PROP_FORMAT | 8 | 返回的Mat對象的格式 |
| CAP_PROP_BRIGHTNESS | 10 | 圖像的亮度(僅適用於支持的相機) |
| CAP_PROP_CONTRAST | 11 | 圖像對比度(僅適用於相機) |
| CAP_PROP_SATURATION | 12 | 圖像飽和度(僅適用於相機) |
| CAP_PROP_HUE | 13 | 圖像的色調(僅適用於相機) |
| CAP_PROP_GAIN | 14 | 圖像的增益(僅適用於支持的相機) |
為了更加熟悉VideoCapture類,在代碼清單2-28中給出了讀取視頻、輸出視頻屬性並按照原幀率顯示視頻的程序,運行結果在圖2-6中給出。
代碼清單2-28 VideoCapture.cpp 讀取視頻文件
#include "chapter2_3_video_capture/inc/read_video.hpp"
#include <cstdio>
#include <opencv2/opencv.hpp>
int opencv_function3(void)
{
cv::VideoCapture video__(std::string(MEDIA_PATH) + "hei.mp4");
if(video__.isOpened() == true) //判断视频是否导入成功
{
printf("视频中图像的宽度=%lf",video__.get(cv::CAP_PROP_FRAME_WIDTH));
printf("视频中图像的高度=%lf",video__.get(cv::CAP_PROP_FRAME_HEIGHT));
printf("视频的帧率=%lf",video__.get(cv::CAP_PROP_FPS));
printf("视频的总帧数=%lf",video__.get(cv::CAP_PROP_FRAME_COUNT));
}
else
{
printf("导入视频失败,请确认视频文件是否正确");
return 1;
}
while(true)
{
cv::Mat frame__;
video__ >> frame__;
if(frame__.empty() == true) //检测图像是不是空的,如果是空的,说明视频最后一帧也已经导入完了
{
break;
}
cv::imshow("视频播放",frame__);
cv::waitKey(1000 / video__.get(cv::CAP_PROP_FPS)); //FPS为1秒每帧(也就是帧的数量),用1000ms / 帧的数量 = 每帧所需时间
}
cv::waitKey(0); // 这句确保窗口视频播放完后一直打开,不关闭
}
圖2-6 讀取視頻程序運行結果

攝像頭的直接調用
VideoCapture類還可以調用攝像頭,構造方式如代碼清單2-29所示。
代碼清單2-29 VideoCapture類調用攝像頭構造函數
cv::VideoCapture::VideoCapture(int index,
int apiPreference = CAP_ANY);
通過與代碼清單2-27對比,調用攝像頭與讀取視頻文件相比,只有第一個參數不同。調用攝像頭時,第一個參數為要打開的攝像頭設備的ID,ID的命名方式從0開始。從攝像頭中讀取圖像數據的方式與從視頻中讀取圖像數據的方式相同,通過">>"符號讀取當前時刻相機拍攝到的圖像。並且,在讀取視頻時,VideoCapture類具有的屬性同樣可以使用。我們將代碼清單2-28中的視頻文件改成攝像頭ID(0),再次運行修改後的代碼清單2-28中的程序,運行結果如圖2-7所示。
Linux查詢攝像頭ID:
ls /dev/video*
然後輸出
/dev/video0 /dev/video1 /dev/video2 /dev/video3
這裡的4個攝像頭設備不一定都是真攝像頭,在我這邊,0是筆記本自帶的攝像頭,而2是我外接的USB攝像頭,都試一下就可以了。
圖2-7 調用攝像頭程序運行結果

數據保存
在圖像處理過程中,會生成新的圖像,例如將模糊的圖像經過算法變得更加清晰,將彩色圖像變成灰度圖像,同時需要將處理的結果以圖像或者視頻的形式保存成文件。本節將詳細講述如何將Mat類矩陣保存成圖像或者視頻文件。
圖像的保存
OpenCV提供imwrite()函數用於將Mat類矩陣保存成圖像文件,該函數的原型在代碼清單2-30中給出。
代碼清單2-30 imwrite()函數原型
bool cv::imwrite(const String& filename,
InputArray img,
const std::vector<int>& params = std::vector<int>());
- filename: 保存圖像的地址和文件名,包含圖像格式。
- img: 將要保存的Mat類矩陣變量。
- params: 保存圖片格式屬性設置標誌。 該函數用於將Mat類矩陣保存成圖像文件,如果成功保存,則返回true,否則返回false。可以保存的圖像格式參考imread()函數能夠讀取的圖像文件格式,通常使用該函數只能保存8位單通道圖像和3通道BGR彩色圖像,但是可以通過更改第三個參數保存成不同格式的圖像。不同圖像格式能夠保存的圖像位數如下:
- 16位無符號(CV_16U)圖像可以保存成PNG、JPEG、TIFF格式文件;
- 32位浮點(CV_32F)圖像可以保存成PFM、TIFF、OpenEXR和Radiance HDR格式文件;
- 4通道(Alpha通道)圖像可以保存成PNG格式文件。 該函數第三個參數在一般情況下不需要填寫,保存成指定的文件格式只需要直接在第一個參數後面更改文件後綴,但是當需要保存的Mat類矩陣中數據比較特殊時(如16位深度數據),則需要設置第三個參數。第三個參數的設置方式如代碼清單2-31所示,常見的可選擇設置標誌在表2-6中給出。
代碼清單2-31 imwrite()函數中第三個參數設置方式
vector<int> compression_params;
compression_params.push_back(IMWRITE_PNG_COMPRESSION);
compression_params.push_back(9);
imwrite(filename, img, compression_params);
表2-6 imwrite()函數第三個參數可選擇的標誌及作用
| 標誌參數 | 簡記 | 作用 |
|---|---|---|
| IMWRITE_JPEG_QUALITY | 1 | 保存成JPEG格式的文件的圖像質量,分成0~100等級,默認95 |
| IMWRITE_JPEG_PROGRESSIVE | 2 | 增強JPEG格式,啟用為1,默認值為0 (False) |
| IMWRITE_JPEG_OPTIMIZE | 3 | 對JPEG格式進行優化,啟用為1,默認參數為0 (False) |
| IMWRITE_JPEG_LUMA_QUALITY | 5 | JPEG格式文件單獨的亮度質量等級,分成0~100,默認為0 |
| IMWRITE_JPEG_CHROMA_QUALITY | 6 | JPEG格式文件單獨的色度質量等級,分成0~100,默認為0 |
| IMWRITE_PNG_COMPRESSION | 16 | 保存成PNG格式文件壓縮級別,0~9,值越大意味著更小尺寸和更長的壓縮時間,默認值為1(最佳速度設置) |
| IMWRITE_TIFF_COMPRESSION | 259 | 保存成TIFF格式文件壓縮方案 |
為了更好地理解imwrite()函數的使用方式,在代碼清單2-32中給出了生成帶有Alpha通道的矩陣,並保存成PNG格式圖像的程序。程序運行後會生成一個保存了4通道的PNG格式圖像,為了更直觀地看到圖像結果,我們在圖2-8中給出了Image Watch插件中看到的圖像和保存成PNG格式的圖像。
代碼清單2-32 imgWriter.cpp 保存圖像
#include "chapter2_4_save_media_file/inc/save_image.hpp"
#include <cstdio>
#include <opencv2/opencv.hpp>
void AlphaMat(cv::Mat *mat);
int opencv_function6(void)
{
cv::Mat mat__(480,640,CV_8UC4);
AlphaMat(&mat__);
std::vector<int> compression_params;
compression_params.push_back(cv::IMWRITE_PNG_COMPRESSION); //PNG图像压缩标志
compression_params.push_back(9); //设置最高压缩质量
if(cv::imwrite(std::string(SRCSRC_PATH) + "chapter2_4_save_media_file/save_files/save_image_test1.png",mat__,compression_params) == false)
{
printf("保存成PNG图像失败");
return 1;
}
printf("保存成PNG图像成功");
return 0;
}
void AlphaMat(cv::Mat *mat)
{
CV_Assert(mat->channels() == 4); //如果通道不等于4,那么抛出异常
for(int i = 0;i < mat->rows;i++) //行
{
for(int j = 0;j < mat->cols;j++) //列
{
/*
cv::Vec4b bgra = mat->at<cv::Vec4b>(i,j);
bgra.val[0] = cv::saturate_cast<uint8_t>(255); //B:255 //该函数防止溢出,里面的数只能是0-255
bgra.val[1] = cv::saturate_cast<uint8_t>(0); //G:0
bgra.val[2] = cv::saturate_cast<uint8_t>(0); //R:0
bgra.val[3] = cv::saturate_cast<uint8_t>(180); //α:180
*/
cv::Vec4b &bgra = mat->at<cv::Vec4b>(i,j);
bgra[0] = cv::saturate_cast<uint8_t>(255); //B:255 //该函数防止溢出,里面的数只能是0-255
bgra[1] = cv::saturate_cast<uint8_t>(0); //G:0 //重载运算符[]
bgra[2] = cv::saturate_cast<uint8_t>(0); //R:0
bgra[3] = cv::saturate_cast<uint8_t>(90); //α:90
}
}
}
圖2-8 程序中和保存後的4通道圖像(左:Image Watch,右:PNG文件)

視頻的保存
有時我們需要將多幅圖像生成視頻,或者直接將攝像頭拍攝到的數據保存成視頻文件。OpenCV中提供了VideoWriter類用於實現多張圖像保存成視頻文件,該類構造函數的原型在代碼清單2-33中給出。
代碼清單2-33 保存視頻文件VideoWriter類構造函數
cv::VideoWriter::VideoWriter(); // 默认构造函数
cv::VideoWriter::VideoWriter(const String& filename,
int fourcc,
double fps,
Size frameSize,
bool isColor = true);
- filename: 保存視頻的地址和文件名,包含視頻格式。
- fourcc: 壓縮幀的4字符編解碼器代碼,詳細參數在表2-7中給出。
- fps: 保存視頻的幀率,即視頻中每秒圖像的張數。
- frameSize: 視頻幀的尺寸。
- isColor: 保存視頻是否為彩色視頻。
代碼清單2-33中的第一行默認構造函數的使用方法與VideoCapture()相同,都是創建一個用於保存視頻的數據流,後續通過open()函數設置保存文件名稱、編解碼器、幀數等一系列參數。第二種構造函數需要輸入的第一個參數是需要保存的視頻文件名稱,第二個參數是編解碼器的代碼,可以設置的編解碼器選項在表2-7中給出,如果賦值"-1",則會自動搜索合適的編解碼器,需要注意的是,其在OpenCV 4.0版和OpenCV 4.1版中的輸入方式有一些差別,具體差別在表2-7給出。第三個參數為保存視頻的幀率,可以根據需求自由設置,例如實現原視頻二倍速播放、原視頻慢動作播放等。第四個參數是設置保存的視頻文件的尺寸,這裡需要注意的是,在設置時一定要與圖像的尺寸相同,不然無法保存視頻。最後一個參數是設置保存的視頻是否是彩色的,程序中,默認的是保存為彩色視頻。
該函數與VideoCapture()有很大的相似之處,都可以通過isOpened()函數判斷是否成功創建一個視頻流,可以通過get()查看視頻流中的各種屬性。在保存視頻時,我們只需要將生成視頻的圖像一幀一幀地通過"<<"操作符(或者write()函數)賦值給視頻流,最後使用release()關閉視頻流。
表2-7 視頻編碼格式
| OpenCV 4.1版本標誌 | OpenCV 4.0版本標誌 | 作用 |
|---|---|---|
VideoWriter::fourcc('D', 'I', 'V', 'X') | CV_FOURCC('D', 'I', 'V', 'X') | MPEG-4編碼 |
VideoWriter::fourcc('P', 'I', 'M', '1') | CV_FOURCC('P', 'I', 'M', '1') | MPEG-1編碼 |
VideoWriter::fourcc('M', 'J', 'P', 'G') | CV_FOURCC('M', 'J', 'P', 'G') | JPEG編碼(運行效果一般) |
VideoWriter::fourcc('M', 'P', '4', '2') | CV_FOURCC('M', 'P', '4', '2') | MPEG-4.2編碼 |
VideoWriter::fourcc('D', 'I', 'V', '3') | CV_FOURCC('D', 'I', 'V', '3') | MPEG-4.3編碼 |
VideoWriter::fourcc('U', '2', '6', '3') | CV_FOURCC('U', '2', '6', '3') | H263編碼 |
VideoWriter::fourcc('I', '2', '6', '3') | CV_FOURCC('I', '2', '6', '3') | H263I編碼 |
VideoWriter::fourcc('F', 'L', 'V', '1') | CV_FOURCC('F', 'L', 'V', '1') | FLV1編碼 |
為了更好地理解VideoWriter()類的使用方式,代碼清單2-34中給出了利用已有視頻文件數據或者直接通過攝像頭生成新的視頻文件的例程。讀者需要重點體會VideoWriter()類和VideoCapture()類的相似之處和使用時的注意事項。
代碼清單2-34 VideoWriter.cpp 保存視頻文件
#include "chapter2_4_save_media_file/inc/save_video.hpp"
#include <cstdio>
#include <opencv2/opencv.hpp>
template <typename T>
bool Save_Video(T file_opened_name_or_index,const std::string & file_saved_name);
int opencv_function5(void)
{
// 截取视频里的一段视频并保存到本地
Save_Video<const std::string &>(std::string(MEDIA_PATH) + "hei.mp4",std::string(SRCSRC_PATH) + "chapter2_4_save_media_file/save_files/save_video_test1.mp4");
// 截取摄像头里的一段视频并保存到本地
// Save_Video<int>(2,std::string(SRCSRC_PATH) + "chapter2_4_save_media_file/save_files/save_video_test2.mp4");
return 0;
}
template <typename T>
bool Save_Video(T file_opened_name_or_index,const std::string & file_saved_name)
{
cv::Mat img;
cv::VideoCapture video(file_opened_name_or_index);
if(video.isOpened() == true)
{
printf("调用摄像头或打开视频成功!");
printf("视频中图像的宽度=%lf",video.get(cv::CAP_PROP_FRAME_WIDTH));
printf("视频中图像的高度=%lf",video.get(cv::CAP_PROP_FRAME_HEIGHT));
printf("视频的帧率=%lf",video.get(cv::CAP_PROP_FPS));
printf("视频的总帧数=%lf",video.get(cv::CAP_PROP_FRAME_COUNT));
}
else
{
printf("调用摄像头或者视频失败,请检查摄像头是否连接成功或者视频文件是否存在");
return false;
}
video >> img;
if(img.empty() == true)
{
printf("帧图像获取失败!");
return false;
}
cv::VideoWriter writer;
auto file_name = file_saved_name;
int codec = writer.fourcc('M','J','P','G');
double fps = 30.0;
auto size = img.size();
bool isColor = (img.type() == CV_8UC3);
writer.open(file_name,codec,fps,size,isColor);
if(writer.isOpened() == true) //判断视频流是否创建成功!
{
printf("视频流创建成功!");
}
else
{
printf("视频流创建失败,请确认是否为合法输入!");
return false;
}
while(true)
{
if(video.read(img) == false) //判断是否还能够继续从摄像头或者视频中读出一帧图像
{
printf("摄像头断开连接或者视频读取完成!");
break;
}
writer << img; //writer.write(img);
cv::namedWindow("Live");
cv::imshow("Live",img);
int8_t keyborad_value = cv::waitKey(1000 / video.get(cv::CAP_PROP_FPS)); //FPS为1秒每帧(也就是帧的数量),用1000ms / 帧的数量 = 每帧所需时间
if(keyborad_value == 27) //ESC键的ASCII码值为27,按下ESC键退出循环
{
break;
}
}
video.release();
writer.release();
return true;
}
從視頻中保存圖片:
#include "chapter2_4_save_media_file/inc/save_image_in_video.hpp"
#include <cstdio>
#include <opencv2/opencv.hpp>
int opencv_function7(void)
{
cv::VideoCapture video__(0);
cv::Mat frame__;
if(video__.isOpened() == true) //判断视频是否导入成功
{
printf("视频中图像的宽度=%lf",video__.get(cv::CAP_PROP_FRAME_WIDTH));
printf("视频中图像的高度=%lf",video__.get(cv::CAP_PROP_FRAME_HEIGHT));
printf("视频的帧率=%lf",video__.get(cv::CAP_PROP_FPS));
printf("视频的总帧数=%lf",video__.get(cv::CAP_PROP_FRAME_COUNT));
}
else
{
printf("导入摄像头视频失败,请确认摄像头是否正常打开");
return 1;
}
printf("按下空格键截图!");
printf("按下ESC结束播放!");
while(true)
{
video__ >> frame__;
cv::imshow("Live",frame__);
int32_t key_boards_val = cv::waitKey(1000/video__.get(cv::CAP_PROP_FPS));
if(key_boards_val==32) //检测到按下了空格键
{
if(cv::imwrite(std::string(SRCSRC_PATH) + "chapter2_4_save_media_file/save_files/save_image_test2.png",frame__) == false)
{
printf("截图失败!");
}
else
{
printf("截图成功!");
}
if(frame__.empty() == true) //检测图像是不是空的,如果是空的,说明视频最后一帧也已经导入完了
{
printf("视频播放结束,请选择截图或是按ESC退出!");
}
}
else if(key_boards_val==27) //检测到按下了ESC按键
{
break;
}
}
video__.release();
printf("播放被终止或已结束!");
return 0;
}
保存和讀取XML和YAML文件
除圖像數據之外,有時程序中的尺寸較小的Mat類矩陣、字符串、數組等數據也需要進行保存,這些數據通常保存成XML文件或者YAML文件。本小節中將介紹如何利用OpenCV 4中的函數將數據保存成XML文件或者YAML文件,以及如何讀取這兩種文件中的數據。
XML是一種元標記語言。所謂元標記,就是使用者可以根據自身需求定義自己的標記,例如可以用<age>、<color>等標記來定義數據的含義,如用<age>24</age>來表示age數據的數值為24。XML是一種結構化的語言,通過XML語言可以知道數據之間的隸屬關係,例如<color><red>100</red><blue>150</blue></color>表示在color數據中含有兩個名為red和blue的數據,兩者的數值分別是100和150。通過標記的方式,無論以什麼形式保存數據,只要文件滿足XML格式,讀取出來的數據就不會出現混淆和歧義。
YAML是一種以數據為中心的語言,通過"變量:數值"的形式來表示每個數據的數值,通過不同的縮進來表示不同數據之間的結構和隸屬關係。YAML可讀性高,常用來表達資料序列的格式,它參考了多種語言,包括XML、C語言、Python、Perl等。
OpenCV 4中提供了用於生成和讀取XML文件和YAML文件的FileStorage類,類中定義了初始化類、寫入數據和讀取數據等方法。我們在使用FileStorage類時首先需要對其進行初始化,初始化可以理解為聲明需要操作的文件和操作類型。OpenCV 4提供了兩種初始化的方法,分別是不輸入任何參數的初始化(可以理解為只定義,並未初始化),以及輸入文件名稱和操作類型的初始化。後一種方法初始化構造函數的原型在代碼清單2-35中給出。
代碼清單2-35 FileStorage()函數原型
cv::FileStorage::FileStorage(const String &filename,
int flags,
const String &encoding = String());
- filename: 打開的文件名稱。
- flags: 對文件進行的操作類型標誌,常用參數及含義在表2-8中給出。
- encoding: 編碼格式,目前不支持UTF-16 XML編碼,需要使用UTF-8 XML編碼。 表2-8 FileStorage()構造函數中對文件操作類型常用標誌及含義
| 標誌參數 | 簡記 | 含義 |
|---|---|---|
| READ | 0 | 讀取文件中的數據 |
| WRITE | 1 | 向文件中重新寫入數據,會覆蓋之前的數據 |
| APPEND | 2 | 向文件中繼續寫入數據,新數據在原數據之後 |
| MEMORY | 4 | 將數據寫入或者讀取到內部緩衝區 |
該函數是FileStorage類的構造函數,用於聲明打開的文件名稱和操作的類型。該函數第一個參數是打開的文件名稱,參數是字符串類型,文件的擴展名是".xml"、".yaml"(或者".yml")。打開的文件可以已經存在或者未存在,但是,當對文件進行讀取操作時,需要是已經存在的文件。第二個參數是對文件進行的操作類型標誌,例如對文件進行讀取操作、寫入操作等,常用參數及含義在表2-8中給出,由於該標誌量在FileStorage類中,因此在使用時需要加上類名作為前綴,例如"FileStorage::WRITE"。最後一個參數是文件的編碼格式,目前不支持UTF-16 XML編碼,需要使用UTF-8 XML編碼,通常情況下使用該參數的默認值即可。
打開文件後,可以通過FileStorage類中的isOpened()函數判斷是否成功打開文件。如果成功打開文件,那麼該函數返回true;如果打開文件失敗,那麼該函數返回false。
由於FileStorage類中默認構造函數沒有任何參數,因此沒有聲明打開的文件和操作的類型,此時需要通過FileStorage類中的open()函數單獨進行聲明。該函數的原型在代碼清單2-36中給出。
代碼清單2-36 open()函數原型
virtual bool cv::FileStorage::open(const String &filename,
int flags,
const String &encoding = String());
- filename: 打開的文件名稱。
- flags: 對文件進行的操作類型標誌,常用參數及含義在表2-8中給出。
- encoding: 編碼格式,目前不支持UTF-16 XML編碼,需要使用UTF-8 XML編碼。 該函數解決了默認構造函數沒有聲明打開文件的問題,函數可以指定FileStorage類打開的文件。如果成功打開文件,則返回值為true,否則為false。該函數中所有的參數及含義與代碼清單2-35中的相同,因此這裡不再贅述。同樣,通過該函數打開文件後仍然可以通過FileStorage類中的isOpened()函數判斷是否成功打開文件。 打開文件後,類似C++中創建的數據流,可以通過"<<"操作符將數據寫入文件中,或者通過">>"操作符從文件中讀取數據。除此之外,還可以通過FileStorage類中的write()函數將數據寫入文件中,該函數的原型在代碼清單2-37中給出。 代碼清單2-37 write()函數原型
void cv::FileStorage::write(const String &name,
int val);
- name: 寫入文件中的變量名稱。
- val: 變量值。
該函數能夠將不同數據類型的變量名稱和變量值寫入文件中。該函數的第一個參數是寫入文件中的變量名稱。第二個參數是變量值,代碼清單2-37中的變量值是int類型,但是在FileStorage類中提供了write()函數的多個重載函數,分別用於實現將double、String、Mat、vector<String>
使用操作符向文件中寫入數據時與write()函數類似,都需要聲明變量名和變量值,例如變量名為"age",變量值為"24",可以通過file << "age" << 24來實現。如果某個變量的數據是一個數組,可以用""將屬於同一個變量的變量值標記出來,例如file << "age" << "" << 24 << 25 << ""。如果某些變量隸屬於某個變量,可以用"{}"表示變量之間的隸屬關係,例如file << "age" << "{" << "Xiaoming" << 24 << "Wanghua" << 25 << "}"。
讀取數據時,可以通過file"x" >> xRead讀取變量名為x的變量值。但是,當某個變量中含有多個數據或者含有子變量時,就需要通過FileNode節點類型和迭代器FileNodeIterator進行讀取,例如某個變量的變量值是一個數組,首先需要定義一個形如file"age"的FileNode節點類型變量,之後通過迭代器遍歷其中的數據。另一種方法可以不使用迭代器,通過在變量後邊添加""(地址)的形式讀取數據,例如FileNode0表示數組變量中的第一個數據,FileNode"Xiaoming"表示"age"變量中的"Xiaoming"變量的數據,依次向後添加""(地址)實現多節點數據的讀取。
為了瞭解如何生成和讀取XML文件和YAML文件,在代碼清單2-38中給出了實現文件寫入和讀取的示例程序。該程序中使用write()函數和"<<"操作符兩種方式向文件中寫入數據,使用迭代器和""(地址)兩種方式從文件中讀取數據。數據的寫入和讀取方法在前面已經介紹,在代碼清單2-38中需要重點了解如何通過程序實現寫入與讀取。該程序生成的XML文件和YAML文件中的數據在圖2-9中給出,讀取文件數據的結果在圖2-10中給出。
代碼清單2-38 myXMLandYAML.cpp 保存和讀取XML和YAML文件
#include "chapter2_4_save_media_file/inc/save_XMLandYMAL.hpp"
#include <cstdio>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int opencv_function8(void)
{
system("color F0"); //修改运行程序背景和文字颜色
// string fileName = std::string(SRCSRC_PATH) + "chapter2_4_save_media_file/save_files/datas.xml"; //文件的名称
string fileName = std::string(SRCSRC_PATH) + "chapter2_4_save_media_file/save_files/datas.yaml"; //文件的名称
//以写入的模式打开文件
cv::FileStorage fwrite(fileName, cv::FileStorage::WRITE);
//存入矩阵Mat类型的数据
Mat mat = Mat::eye(3, 3, CV_8U);
fwrite.write("mat", mat); //使用write()函数写入数据
//存入浮点型数据,节点名称为x
float x = 100;
fwrite << "x" << x;
//存入字符串型数据,节点名称为str
String str = "Learn OpenCV 4";
fwrite << "str" << str;
//存入数组,节点名称为number_array
fwrite << "number_array" << "[" <<4<<5<<6<< "]";
//存入多node节点数据,主名称为multi_nodes
fwrite << "multi_nodes" << "{" << "month" << 8 << "day" << 28 << "year"
<< 2019 << "time" << "[" << 0 << 1 << 2 << 3 << "]" << "}";
//关闭文件
fwrite.release();
//以读取的模式打开文件
cv::FileStorage fread(fileName, cv::FileStorage::READ);
//判断是否成功打开文件
if (!fread.isOpened())
{
cout << "打开文件失败,请确认文件名称是否正确!" << endl;
return -1;
}
//读取文件中的数据
float xRead;
fread["x"] >> xRead; //读取浮点型数据
cout << "x=" << xRead << endl;
//读取字符串数据
string strRead;
fread["str"] >> strRead;
cout << "str=" << strRead << endl;
//读取含多个数据的number_array节点
FileNode fileNode = fread["number_array"];
cout << "number_array=[";
//循环遍历每个数据
for (FileNodeIterator i = fileNode.begin(); i != fileNode.end(); i++)
{
float a;
*i >> a;
cout << a<<" ";
}
cout << "]" << endl;
//读取Mat类型数据
Mat matRead;
fread["mat="] >> matRead;
cout << "mat=" << mat << endl;
//读取含有多个子节点的节点数据,不使用FileNode和迭代器进行读取
FileNode fileNode1 = fread["multi_nodes"];
int month = (int)fileNode1["month"];
int day = (int)fileNode1["day"];
int year = (int)fileNode1["year"];
cout << "multi_nodes:" << endl
<< " month=" << month << " day=" << day << " year=" << year;
cout << " time=[";
for (int i = 0; i < 4; i++)
{
int a = (int)fileNode1["time"][i];
cout << a << " ";
}
cout << "]" << endl;
//关闭文件
fread.release();
return 0;
}
圖2-9 myXMLandYAML.cpp程序生成的XML文件和YAML文件

圖2-10 myXMLandYAML.cpp程序文件讀取結果

本章小結
在本章中,我們首先介紹了OpenCV 4中用於存放圖像數據的Mat類的使用方式,之後介紹了圖像的讀取和顯示,視頻加載與攝像頭的調用,最後介紹瞭如何保存圖像、視頻文件,以及XML、YAML文件的保存與讀取。
本章主要函數清單
| 函數名稱 | 函數說明 | 代碼清單 |
|---|---|---|
imread() | 讀取圖像文件 | 2-24 |
namedWindow() | 創建一個顯示圖像的窗口 | 2-25 |
imshow() | 在指定窗口中顯示圖像 | 2-26 |
VideoCapture() | 調用攝像頭或者讀取、保存視頻文件 | 2-27 |
imwrite() | 保存圖像到文件 | 2-30 |
VideoWriter() | 將多幀圖像保存成視頻文件 | 2-33 |
FileStorage() | 讀取或者保存XML、YAML文件 | 2-35 |