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

1.1.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),都會提示建立錯誤。
1.2.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中給出。

1.3.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()方法時,需要防止出現數據溢位的問題。
1.4.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個數據時,可以直接通過ptr[2]訪問。
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變數的含義是某個資料所在元素的通道數。這種方式與我們通過指標讀取資料的形式類似,都是通過將首個數據的地址指標移動若干位後指向需要讀取的資料,只不過這種方式可以通過直接給出行、列和通道數進行讀取,不需要使用者再計算某個資料在這行資料儲存空間中的位置。
2.影像的讀取與顯示
本節中將詳細介紹影像讀取和顯示的相關功能。
2.1.影像讀取函式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引數調整能夠讀取的最大畫素數目。
2.2.影像視窗函式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 | 建立沒有狀態列和工具欄的視窗 |
2.3.影像顯示函式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); // 这句确保窗口一直打开
}
3.影片載入與攝像頭呼叫
前面已經介紹瞭如何通過程式讀取影像資料,本節將介紹OpenCV中為讀取影片檔案和呼叫攝像頭而設計的VideoCapture類。
3.1.影片資料的讀取
雖然影片檔案是由多張圖片組成的,但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 讀取影片程式執行結果

3.2.攝像頭的直接呼叫
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 呼叫攝像頭程式執行結果

4.資料儲存
在影像處理過程中,會生成新的影像,例如將模糊的影像經過演算法變得更加清晰,將彩色影像變成灰度影像,同時需要將處理的結果以影像或者影片的形式儲存成檔案。本節將詳細講述如何將Mat類矩陣儲存成影像或者影片檔案。
4.1.影像的儲存
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檔案)

4.2.影片的儲存
有時我們需要將多幅影像生成影片,或者直接將攝像頭拍攝到的資料儲存成影片檔案。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;
}
4.3.儲存和讀取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節點型別變數,之後通過迭代器遍歷其中的資料。另一種方法可以不使用迭代器,通過在變數後邊新增"[]"(地址)的形式讀取資料,例如FileNode[0]表示陣列變數中的第一個資料,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程式檔案讀取結果

5.本章小結
在本章中,我們首先介紹了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 |