官方文檔鏈接:https://docs.opencv.org/4.2.0/d6/d6d/tutorial_mat_the_basic_image_container.html
目標 (Goal)
我們有多種方式可以從現實世界中獲取數字圖像:數字照相機,掃描儀,計算機斷層掃描和磁共振成像等等。在任何情況下,我們看到的都是圖像。然而,當我們將其轉換爲數字設備時,我們記錄的是圖像中每個點的數值。
例如在上圖中,你可以看到汽車的後視鏡不過是一個包含所有像素點強度值的矩陣。我們獲取和存儲像素值的方式可能會根據我們的需要而有所不同,但是最終,在計算機中所有圖像都可能被簡化爲數字矩陣和描述矩陣本身的其他信息。OpenCV 是一個計算機視覺庫,它的主要功能是處理和操作這些信息。因此,首先需要熟悉的是 OpenCV 如何存儲和處理圖像。
Mat
OpenCV 從2001年就出現了。在當時,這個庫是圍繞一個 C 接口構建的,爲了將圖像存儲在內存中,前輩們使用了一個稱爲 “IplImage” 的 C 結構體。你會在很多舊的教程和教學資料中看到有關 “IplImage” 的論述。“IplImage” 的使用使得 C 語言的所有缺點都暴露出來了。**最大的問題是需要手動管理內存。**它的使用是建立在由用戶負責內存分配和釋放的假設之上。雖然對於小程序來說這不是問題,但是,一旦代碼增長,用戶將更難處理所有這些問題,很難專注於解決最初的開發目標。
幸運的是,C++的出現和 “類” 概念的提出,使得程序可以自動管理內存,極大地方便了用戶的使用。另外,C++ 完全兼容 C,所以不會產生兼容性問題。因此,OpenCV 2.0 引入了新的 C++ 接口,它提供了一種新的方式,這意味着你不需要考慮內存管理,同時使得代碼更加簡潔。C++ 接口的主要缺點是目前許多嵌入式開發系統只支持 C。因此,除非你的目標是嵌入式平臺,否則不必使用舊的方法。
關於 Mat,第一,你需要知道你不再需要手動分配內存,並且在不需要內存的時候,它會立即自動釋放。大多數 OpenCV 函數將自動分配其輸出數據。如果你傳遞一個已經存在的 Mat 對象,它已經爲矩陣分配了所需的空間,它就將被重用。換言之,我們在任何時候都只使用執行任務所需的內存。
Mat 基本上是一個包含兩個數據部分的類:
- 矩陣頭:包含矩陣的大小,存儲方法,存儲矩陣的地址等信息。
- 指針:指向被包含的像素值的矩陣指針,可根據選擇的存儲方法取任何維度。
矩陣頭大小是恆定的,但是矩陣本身的大小可能因圖像而異,並且通常以數量級增大。
OpenCV 是一個圖像處理庫。它包含了大量的圖像處理函數。爲了解決計算上的難題,大多數情況下,用戶將使用多個庫函數。因此,常用的做法是將圖像傳遞給函數。同時,圖像處理算法往往意味着相當大的計算量。我們最不想因爲不必要的大圖像拷貝而降低程序速度。
爲了解決這個問題,OpenCV 使用了引用計數系統。其思想是每個 Mat 對象都有自己的頭,但是通過讓他們的矩陣指針指向同一個地址,矩陣可以在兩個 Mat 對象之間分享。此外,複製運算符只複製頭和指向大矩陣的指針,而不復制數據本身。
cv::Mat A, C; // creates just the header parts
A = cv::imread(argv[1], IMREAD_COLOR); // here we'll know the method used (allocate matrix)
cv::Mat B(A); // Use the copy constructor
C = A; // Assignment operator
最後,所有上述的對象都指向同一個數據矩陣,使用它們當中的任何一個對象進行修改都會影響到其他的所有對象。實際上,不同的對象只是爲相同的底層數據提供不同的訪問方法。不過它們的對象頭部分是不同的。另外有趣的是,用戶可以創建只引用完整數據的頭部分。例如,要在圖像中創建感興趣的區域(ROI),只需要創建一個具有新邊界的頭即可。
cv::Mat D (A, cv::Rect(10, 10, 100, 100) ); // using a rectangle
cv::Mat E = A(cv::Range::all(), cv::Range(1,3)); // using row and column boundaries
代碼實現結果:
此處涉及到一個問題,當一個矩陣本身屬於多個 Mat 對象,這些對象不再被需要時,它們是否負責清理矩陣空間,釋放內存。答案是:最後一個使用矩陣的對象負責這項工作。這個過程是通過 引用計數 機制來處理的。每當有對象複製 Mat 對象的頭時,矩陣的計數器會增加。當矩陣的頭被釋放時,計數器會減少。當計數器變爲 0 時,矩陣被釋放。有時用戶想要複製矩陣本身,這就需要用到 OpenCV 提供的函數
- cv::Mat::clone()
- cv::Mat::copyTo()
cv::Mat F = A.clone();
cv::Mat G;
A.copyTo(G);
此時,修改 F 或者 G 將不會影響 A 的頭所指向的矩陣。你需要記住的是:
- OpenCV 函數的輸出圖像分配是自動的(除非另有說明)。
- 使用 OpenCV 的 C++ 接口,你不需要考慮內存管理。
- 賦值運算符和拷貝構造器只複製頭。
- 使用 cv::Mat::clone() 和 cv::Mat::copyTo() 函數可以複製圖像的底層矩陣數據。
存儲方法 (Storing methods)
這部分內容有關如何存儲像素值。用戶可以選擇 顏色空間 和使用的 數據類型 。顏色空間指的是如何組合顏色組件以編碼給定的顏色。最簡單的一種方式是灰度,可以使用的顏色是黑色和白色。它們的各種結合可以創造出許多種灰色的陰影。
對於色彩豐富的方式,有許多方法可供選擇。每一種都可以分解爲三個或四個基本組件,即三種或四種原色,可以使用這些原色的組合來創建出其他顏色。使用最爲廣泛的是 RGB,主要是因爲這也是我們眼睛建立顏色的方式。RGB 的底色是紅色、綠色和藍色。爲了編碼顏色的透明度,有時會添加第四個元素:alpha 即 (a)。
但是,還有很多其他的顏色系統,每個系統都有它們自己的優勢:
- RGB 是最常見的方式。因爲我們的眼睛就是使用類似的方式。但是請記住 OpenCV 標準顯示系統使用 BGR 顏色空間來合成顏色(即顏色通道的順序是 B、G、R)。
- HSV 和 HLS 將顏色分解爲色調(hue)、飽和度(saturation)和值(value)/亮度分量(luminance components),這是一種更自然的描述顏色的方法。例如,你可能會忽略最後一個組件,使算法對輸入圖像的光照條件不太敏感。
- YCrCb 被流行的 JPEG 圖像格式所使用。
- CIE L*a*b* 是一個感知上均勻的顏色空間,如果需要測量給定顏色到另一種顏色距離,它會很方便。
每個構建圖像的通道都有自己的有效域。這將導致使用的數據類型。如何存儲組件定義了用戶對其域的控制。最小的數據類型可能是 char,即 1 個字節 或 8 位,可以是無符號(可存儲 0 到 255 之間的值)或有符號(可存儲 -127 到 +127 之間的值)。儘管在三通道的情況下,已經提供了 1600 萬種可能的顏色來表示(例如 RGB),但用戶可以通過爲每個通道使用浮點(4 字節 = 32 位)或雙字節(8 字節 = 64 位)數據類型來獲得更精細的控制。不過,增加通道數值的精度也會增加內存中整個圖片的大小。
創建 Mat 對象 (Creating a Mat object explicitly)
在 “加載、修改和保存圖像” 教程中已經學習瞭如何使用 cv::imwrite() 函數將矩陣寫入圖像文件。但是,出於調試的目的,查看實際值要方便得多。可以 使用 Mat 的 << 運算符執行操作。注意,這種方法只適用於二維矩陣。
Mat 作爲圖像容器是一個通用的矩陣類。因此,可以創建和操作多維矩陣。可以通過多種方式創建 Mat 對象:
- cv::Mat::Mat Constructor
cv::Mat M(2, 2, CV_8UC3, cv::Scalar(0, 0, 255));
std::cout << "M = " << std::endl << M << std::endl << std::endl;
對於二維和多通道圖像,首先需要定義它們的尺度 size:row 和 column。
然後需要指定用於存儲元素的數據類型和每個矩陣點的通道數。爲此,根據以下約定構造了多個定義:
CV_[The number of bits per item][Signed or Unsigned][Type Prefix]C[The channel number]
例如,CV_8UC3 意味着使用 8 位長 的 無符號 字符 類型,每個像素有其中的三個值組成 3 個通道。預定義的類型中最多爲 4 個通道。cv::Scalar() 函數適用於指定矩陣點的初始值,包含四個參數,後兩個參數默認爲 0.0。如果需要更多,可以使用上部宏創建類型,在括號種設置通道號,如下所示。
- 使用 C/C++ 數組通過構造函數初始化 (Use C/C++ arrays and initialize via constructor)
int sz[3] = { 2, 2, 2 };
cv::Mat L(3, sz, CV_8UC(1), cv::Scalar::all(0));
上面的例子展示瞭如何創建一個二維以上的矩陣。指定其維度,然後傳遞一個指針,該指針包含每個維度的大小,其餘的保持不變。
- cv::Mat::create 函數
cv::Mat M;
M.create(4, 4, CV_8UC(2));
std::cout << "M = " << std::endl << M << std::endl << std::endl;
不能用此構造函數初始化矩陣值。如果新的矩陣大小不適合舊的矩陣,它將只重新分配其矩陣數據內存。
- MATLAB 形式的初始化:cv::Mat::zeros,cv::Mat::ones,cv::Mat::eye。需要指定使用的數據類型和大小:
cv::Mat E = cv::Mat::eye(4, 4, CV_64F);
std::cout << "E = " << std::endl << " " << E << std::endl << std::endl;
cv::Mat O = cv::Mat::ones(2, 2, CV_32F);
std::cout << "O = " << std::endl << " " << O << std::endl << std::endl;
cv::Mat Z = cv::Mat::zeros(3, 3, CV_8UC1);
std::cout << "Z = " << std::endl << " " << Z << std::endl << std::endl;
- 對於小矩陣,可以使用逗號分隔的初始化器或初始化列表 (在最後一種情況下需要 C++ 11 支持):
cv::Mat C = (cv::Mat_<double>(3, 3) << 0, -1, 0, -1, 5, -1, 0, -1, 0);
std::cout << "C = " << std::endl << " " << C << std::endl << std::endl;
cv::Mat C = (cv::Mat_<double>({ 0, -1, 0, -1, 5, -1, 0, -1, 0 })).reshape(3);
std::cout << "C = " << std::endl << " " << C << std::endl << std::endl;
- 爲現有的 Mat 對象創建一個新的頭,然後使用 cv::Mat::clone 或 cv::Mat::copyTo
cv::Mat RowClone = C.row(1).clone();
std::cout << "RowClone = " << std::endl << " " << RowClone << std::endl << std::endl;
注意
可以使用 cv::randu() 函數用隨機值填充矩陣。用戶需要給出隨機值的下限和上限:
cv::Mat R = cv::Mat(3, 2, CV_8UC3);
cv::randu(R, cv::Scalar::all(0), cv::Scalar::all(255));
std::cout << "R = " << std::endl << " " << R << std::endl << std::endl;
輸出格式 (Output formatting)
上面的示例中,可以看到默認的格式化選項。同時,OpenCV 還允許格式化矩陣輸出:
- 默認 (Default)
std::cout << "R (default) = " << std::endl << " " << R << std::endl << std::endl;
- Python
std::cout << "R (python) = " << std::endl << " " << cv::format(R, cv::Formatter::FMT_PYTHON) << std::endl << std::endl;
- CSV (Comma separated values)
std::cout << "R (csv) = " << std::endl << " " << cv::format(R, cv::Formatter::FMT_CSV) << std::endl << std::endl;
- Numpy
std::cout << "R (numpy) = " << std::endl << " " << cv::format(R, cv::Formatter::FMT_NUMPY) << std::endl << std::endl;
- C
std::cout << "R (C) = " << std::endl << " " << cv::format(R, cv::Formatter::FMT_C) << std::endl << std::endl;
其他一般項目輸出 (Output of other common items)
OpenCV 還支持通過 << 運算符輸出其他常見的 OpenCV 數據結構:
- 2D Point
cv::Point2f P(5, 1);
std::cout << "Point (2D) = " << P << std::endl << std::endl;
- 3D Point
cv::Point3f P3f(2, 6, 7);
std::cout << "Point (3D) = " << P3f << std::endl << std::endl;
- std::vector via cv::Mat
std::vector<float> v;
v.push_back((float)CV_PI);
v.push_back(2);
v.push_back(3.01f);
std::cout << "Vector of floats via Mat = " << cv::Mat(v) << std::endl << std::endl;
- std::vector of points
std::vector<cv::Point2f> vPoints(20);
for (size_t i = 0; i < vPoints.size(); ++i)
vPoints[i] = cv::Point2f((float)(i * 5), (float)(i % 7));
std::cout << "A vector of 2D Points = " << vPoints << std::endl << std::endl;
完整代碼
#include <iostream>
#include <vector>
#include <opencv2/core/core.hpp>
#define window "OpenCV"
int main(int argc, char** argv)
{
cv::Mat M(2, 2, CV_8UC3, cv::Scalar(0, 0, 255));
int sz[3] = { 2, 2, 2 };
cv::Mat L(3, sz, CV_8UC(1), cv::Scalar::all(0));
cv::Mat M;
M.create(4, 4, CV_8UC(2));
std::cout << "M = " << std::endl << M << std::endl << std::endl;
cv::Mat E = cv::Mat::eye(4, 4, CV_64F);
std::cout << "E = " << std::endl << " " << E << std::endl << std::endl;
cv::Mat O = cv::Mat::ones(2, 2, CV_32F);
std::cout << "O = " << std::endl << " " << O << std::endl << std::endl;
cv::Mat Z = cv::Mat::zeros(3, 3, CV_8UC1);
std::cout << "Z = " << std::endl << " " << Z << std::endl << std::endl;
cv::Mat C = (cv::Mat_<double>(3, 3) << 0, -1, 0, -1, 5, -1, 0, -1, 0);
std::cout << "C = " << std::endl << " " << C << std::endl << std::endl;
cv::Mat C = (cv::Mat_<double>({ 0, -1, 0, -1, 5, -1, 0, -1, 0 })).reshape(3);
std::cout << "C = " << std::endl << " " << C << std::endl << std::endl;
cv::Mat RowClone = C.row(1).clone();
std::cout << "RowClone = " << std::endl << " " << RowClone << std::endl << std::endl;
cv::Mat R = cv::Mat(3, 2, CV_8UC3);
cv::randu(R, cv::Scalar::all(0), cv::Scalar::all(255));
std::cout << "R (default) = " << std::endl << " " << R << std::endl << std::endl;
std::cout << "R (python) = " << std::endl << " " << cv::format(R, cv::Formatter::FMT_PYTHON) << std::endl << std::endl;
std::cout << "R (csv) = " << std::endl << " " << cv::format(R, cv::Formatter::FMT_CSV) << std::endl << std::endl;
std::cout << "R (numpy) = " << std::endl << " " << cv::format(R, cv::Formatter::FMT_NUMPY) << std::endl << std::endl;
std::cout << "R (C) = " << std::endl << " " << cv::format(R, cv::Formatter::FMT_C) << std::endl << std::endl;
cv::Point2f P(5, 1);
std::cout << "Point (2D) = " << P << std::endl << std::endl;
cv::Point3f P3f(2, 6, 7);
std::cout << "Point (3D) = " << P3f << std::endl << std::endl;
std::vector<float> v;
v.push_back((float)CV_PI);
v.push_back(2);
v.push_back(3.01f);
std::cout << "Vector of floats via Mat = " << cv::Mat(v) << std::endl << std::endl;
std::vector<cv::Point2f> vPoints(20);
for (size_t i = 0; i < vPoints.size(); ++i)
vPoints[i] = cv::Point2f((float)(i * 5), (float)(i % 7));
std::cout << "A vector of 2D Points = " << vPoints << std::endl << std::endl;
return 0;
}