什么是數字圖像處理?
? ? 當今時代,數字圖像無處不在。手機拍照、安防監控、醫療檢查、地圖導航、工業質檢……我們每天都在接收、分析和處理大量圖像信息。對于計算機而言,圖像并不是一張“看得懂”的照片,而是由數值組成的矩陣。如何讓機器也具備“看圖”的能力,正是數字圖像處理的核心目標。
? ??簡而言之,數字圖像處理就是用計算機對圖像進行操作和分析,讓圖像更“清晰”、更“有用”、更“可識別”。舉例如下:
- 拍完照后用手機“自動美顏”一下,可能用到了濾波、邊緣平滑、膚色增強等圖像處理算法;
- 醫生查看 CT 或眼底圖時,圖像可能經過了對比度增強或偽彩色處理,使細節更加清晰;
- 攝像頭識別車輛車牌,需要經過顏色識別、輪廓識別、字符識別等操作;
?什么是OpenCV?
? ? OpenCV(Open Source Computer Vision Library)是一個開源、跨平臺的計算機視覺庫,最初由英特爾開發,現在已經成為業界和學術界廣泛使用的工具之一。OpenCV有如下特性:
- 跨平臺:支持 Windows、Linux、macOS
- 語言支持豐富:C++/Python 作為主流語言選擇,也有部分選擇Java、JavaScript等
- 實時性強:底層基于 C/C++,速度快,能勝任對性能要求高的實時應用
- 功能強大:從圖像讀取到復雜特征匹配,從邊緣檢測到深度學習支持等
? ? 在我們的專欄中,我們的示例主要使用C++,這是工程領域中最合適的使用方式。C++提供的卓越的性能,可以滿足很多實時性的應用需求。同時,我們也會適當給出一些Python示例,在深度學習訓練階段,Python是我們的首選語言(一般選擇pytorch框架)。OpenCV可以對深度學習進行數據預處理支持。
? ? 對于OpenCV的安裝,Python環境下只需要運行以下命令即可:
?pip install opencv-python
? ? 對于C++環境,我們一般都是從源碼直接編譯,然后再部署到自己的開發環境中。我們這里不講如何源碼編譯,大家可以在網上自行搜索。我們稍后會提供一個完整的C++項目,該項目會包含OpenCV所有的依賴庫,大家可以基于該項目進行自己的開發工作。
數字圖像基本結構
? ? 上圖為一個4行8列的矩陣,每個元素的取值范圍為[0,256)。我們可以將其看作為一個4*8的灰度圖像,灰度圖像的取值范圍為[0,256)。在現實生活中,我們更多看到的是彩色圖像,彩色圖像相對于灰度圖像來說,每個元素需要3個值表示,分別代表Red,Green和Blue,其數據矩陣如下:? ?
? ? 以上同樣為一個4行8列的矩陣,但每個元素由一個3*1的向量構成,如第0行0列的向量值為[172,47,117],這三個元素具體表示:Blue=172,Green=47,Red=117。特別注意這里的通道排列順序為BGR,而在生活中我們習慣稱呼彩色圖像為RGB圖像。
? ? OpenCV提供了函數cv::imread(),該函數可讀取多種格式圖像,如JPG, BMP, PNG等,其返回值為cv::Mat對象,該對象保存了圖像相關的所有信息。不論讀取哪種格式圖像,只要該圖像為三通道數據,讀取后的圖像在內存中的排列順序均為BGR(四通道多為BGRA,A表示Alpha通道,用于記錄半透明相關信息)。
? ??OpenCV提供了函數cv::imshow(),該函數用于顯示圖像,其核心參數為cv::Mat對象。我們通過一個實驗來加深通道排列順序的理解。
import cv2 #導入opencv,可用于讀取與顯示圖像
import matplotlib.pyplot as plt #用于圖像顯示img_bgr = cv2.imread('lena.png') #讀取圖像, 默認通道為bgrif img_bgr is None:print("圖像加載失敗,請檢查路徑是否正確。")
else:img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) # 轉換為rgb順序cv2.imshow('opencv show', img_bgr) #使用opencv顯示圖像cv2.waitKey(0) #opencv需要調用該函數已阻止程序繼續執行cv2.destroyAllWindows() #用戶關閉圖像窗口后清除資源#使用matplot顯示圖像,這里需要傳入rgb順序圖像plt.imshow(img_rgb)plt.title("matplot show")plt.show()
? ? ?以上一段python代碼首先使用OpenCV讀取一張圖像,然后分別使用OpenCV與matplot庫進行顯示。需要特別注意,在matplot庫中,默認將通道順序解讀為RGB。因此,我們調用了cvtColor函數對其進行通道轉換(cv2.COLOR_BGR2RGB),使得matplot可以正確顯示圖像顏色。以下分別給出正確通道順序顯示結果與錯誤通道順序顯示結果。


? ? 接下來我們給出一段C++代碼,該代碼實現了圖像讀取與顯示,處理語法上的差異,與python代碼基本一致。
int main()
{cv::Mat img_bgr = cv::imread("lena.png", cv::IMREAD_COLOR);cv::imshow("opencv show", img_bgr);cv::waitKey(0);cv::destroyAllWindows();return 0;
}
數字圖像元素讀取與修改?
? ? ?到目前為止,我們了解了圖像數據的基本結構,也能正確讀取和顯示圖像。那么,我們應該如何讀取或修改圖像單個元素數據呢??
? ? 有如下方法可以讀取或修改圖像像素數據(C++),如下:
- 使用cv::Mat.at<>()方法,該方法適合讀取少量數據。由于函數會進行邊界檢查,其速度較慢,在圖像處理算法實踐中,我們基本不會使用該函數讀取數據。以下給出示例代碼
{ // 讀取100行,150列數據,該數據為三通道數據cv::Vec3b val = img_bgr.at<cv::Vec3b>(100, 150); uchar b = val[0];uchar g = val[1];uchar r = val[2];// 讀取單通道數據(即灰度圖)uchar gray = img_bgr.at<uchar>(100, 150);
}
- 在實際項目中,我們總是直接訪問指針以獲得最佳的訪問效率,以下給出示例代碼
{// // 3通道圖像(bgr)訪問// 遍歷每一行for (int row = 0; row < img_bgr.rows; ++row){// 獲取每一行的起始指針cv::Vec3b* ptr = img_bgr.ptr<cv::Vec3b>(row);// 遍歷每一個元素(cv::Vec3b)for (int col = 0; col < img_bgr.cols; ++col){// 獲取每個通道的值uchar b = ptr[col][0];uchar g = ptr[col][1];uchar r = ptr[col][2];// 每個通道亮度*2// 由于每個通道取值范圍為[0,255],因此需要確保不越界!b = b * 2 > 255 ? 255 : b * 2;g = g * 2 > 255 ? 255 : g * 2;r = r * 2 > 255 ? 255 : r * 2;// 將修改后值賦給原通道ptr[col][0] = b;ptr[col][1] = g;ptr[col][2] = r;}}// // 單通道(灰度圖像)訪問// 遍歷每一行for (int row = 0; row < img_bgr.rows; ++row){// 獲取每一行的起始指針uchar* ptr = img_bgr.ptr<uchar>(row);// 遍歷每一個元素(uchar)for (int col = 0; col < img_bgr.cols; ++col){// 獲取灰度值uchar gray = ptr[col];// 每個通道亮度*2// 由于取值范圍為[0,255],因此需要確保不越界!gray = gray * 2 > 255 ? 255 : gray * 2;// 將修改灰度值賦給原圖像ptr[col] = gray;}}
}
? ?通過以上程序,我們可以得到一個亮度更高的圖像,效果如下:?

? ? 雖然直接訪問指針可以獲得最佳的運行效率,然而我們也可能因為訪問不當而產生以下不良后果,典型錯誤為內存越界錯誤,這可能導致整個程序崩潰。所以,在實際項目中,我們需要慎重使用指針,確保代碼正確性以避免內存越界錯誤!
? ? 另外,一些性能優化的常識可以讓我們避免一些極端低效的代碼,如下代碼大大降低運行效率:
{// 該代碼運行效率會非常低,由于違背了內存連續性訪問原則,// 導致頻繁的緩存命中失敗,嚴重降低數據訪問效率!// 遍歷每一列for (int col = 0; col < img_bgr.cols; ++col){// 遍歷每一行for (int row = 0; row < img_bgr.rows; ++row){cv::Vec3b val = img_bgr.ptr<cv::Vec3b>(row)[col];uchar b = val[0];uchar g = val[1];uchar r = val[2];}}
}
? ? 觀察以上代碼,我們for循環順序發生了改變,該代碼對圖像元素的訪問順序為:
? ? 0行0列->1行0列->2行0列...->0行1列->1行1列->2行1列....
? ? 也就是說在列方向上遍歷,而圖像元素在行方向上連續存儲,從而每次訪問都可能導致緩存命中失敗,從而嚴重影響訪問效率!
? ? ?一般情況下,C++提供了非常靈活的圖像數據讀取方式,有時候我們可能也會使用Python進行少量的數據讀取操作,以下給出使用Python讀取圖像數據的方法:
(b, g, r) = img_bgr[100, 150] #獲取第100行第150列的B、G、R通道值
blue_channel = img_bgr[:, :, 0] #獲取藍通道數據
cv::Mat關鍵元素?
? ? cv::Mat
是 OpenCV中最核心的數據結構之一,用于表示圖像、視頻幀、矩陣等二維數據。理解 cv::Mat
的內部結構對于高效圖像處理非常關鍵。早期的C接口使用IplImage結構,除了兼容需求,我們不再使用IplImage接口了。
? ? 以下是cv::Mat的基本數據結構:
? ? cv::Mat
? ? ?├── data? ? ? ? ? ?→ 指向圖像數據的指針
? ? ?├── rows? ? ? ? ? → 行數(即圖像高度)
? ? ?├── cols? ? ? ? ? ?→ 列數(即圖像寬度)
? ? ?├── step? ? ? ? ? ?→ 每行占用的字節數(stride)
? ? ?├── channels? ? → 通道數(通過 type 解析)
? ? ?├── type? ? ? ? ? ? → 數據類型和通道數的編碼
? ? ?├── depth()? ? ? ?→ 每個通道的數據類型(如 CV_8U)
? ? ?├── refcount? ? ? → 引用計數指針(實現共享內存)
? ? ?└── others? ? ? ? ?→ flags、allocator 等
? ? data為一個uchar*類型數據,指向圖像像素數據的首地址,可以直接通過指針操作像素,如:
uchar* p = img_bgr.data;? p[0] = 255; p[1] = 255;
? ? rows和cols分別代表圖像的行數與列數,也即圖像的高度與寬度。
? ? step表示圖像每一行占用的總字節數,利用該數據可以準確跳轉到每行數據首指針上,以下兩種寫法均可以跳轉到第10行首指針處,故data1與data2為相等指針。
cv::Vec3b* data1 = (cv::Vec3b*)(img_bgr.data + img_bgr.step * 10);
cv::Vec3b* data2?= img_bgr.ptr<cv::Vec3b>(10);
? ? type()函數返回一個整數,該整數編碼了通道數與數據類型信息。一般情況下,我們可以分別調用channels()與depth()函數來分別獲取通道數與數據類型。
? ? 在常規數字圖像中,通道數一般返回為1,3,4通道數據,分別表示灰度圖,真彩色,帶Alpha通道真彩色。當然,在其他應用中,也可以返回任意通道,如2通道可以編碼圖像梯度信息。
? ? 圖像數據類型主要定義了數據精度與數據符號,如CV_8U為8位無符號整數,CV_8S為8位有符號整數,CV_16U/CV_16S定義了16位整數,CV_32S定義了32位有符號整數(注意沒有CV_32U!),CV_32F/CV_64F分別定義了單精度與雙精度浮點類型。
int depth = img_bgr.depth();
int channels = img_bgr.channels();
? ? elemSize()表示一個像素占用的字節數,elemSize1()表示一個通道占用的字節數,使用elemSize() / elemSize1()可計算處通道數,等價于channnels()函數。?
? ??refcount作為內存引用計數,在淺拷貝時共享內存數據,僅增加引用計數,代碼如下:
cv::Mat img = cv::imread("lena.png", cv::IMREAD_COLOR);int* ref = img.refcount; // 引用計數為1cv::Mat img2 = img; //淺拷貝,img與img2公用內存int* ref2 = img2.refcount; // 淺拷貝后引用計數增加到2img.release(); // 釋放img,引用計數減1int* ref3 = img2.refcount; // 釋放img后,引用計數減少到1
? ? 除了淺拷貝之外,我們在很多時候有深拷貝需求(即不共享內存數據),函數copyTo()與clone()均可實現該目標,代碼如下:
// 方式 1:clone(返回新對象)
cv::Mat img_clone = img.clone();// 方式 2:copyTo(拷貝到已有對象)
cv::Mat img_copy;
img.copyTo(img_copy);
結語?
? ? 通過介紹數字圖像處理與OpenCV的基本知識,我們理解了數字圖像的基本結構,以及如何高效的訪問圖像中的任意元素。同時對通道順序以及內存連續性問題進行特別講解,使得我們可以在工程實踐中避免一些微妙的錯誤,提升程序的效率。最后,我們講解了OpenCV中最為重要的數據結構cv::Mat,通過該數據結構,可以實現圖像數據的所有基本操作。
? ? 在工程應用中,為了運行效率我們一般會選擇OpenCV的C++接口。然而在某些情況下,Python接口也發揮了重要的作用。如在深度學習的訓練過程中,我們一般使用pytorch框架。此時,使用OpenCV的Python接口進行數據預處理是非常必要的。因此,在博文中,我們同步給出了C++與Python代碼片段,以適應不同應用場景需求。