【從零學習OpenCV 4】圖像卷積
- 2019 年 12 月 24 日
- 筆記
過幾個月的努力,小白終於完成了市面上第一本OpenCV 4入門書籍《從零學習OpenCV 4》。為了更讓小夥伴更早的了解最新版的OpenCV 4,小白與出版社溝通,提前在公眾號上連載部分內容,請持續關注小白。
卷積常用在信號處理中,而圖像數據也可以看作是一種信號數據,例如圖像中的每一行可以看作測量亮度變化的信號數據,每一列也可以看作亮度變化的信號數據,因此也可以對圖像進行卷積操作。在信號處理中卷積操作需要給出一個卷積函數與信號進行計算,圖像的卷積形式與其相同,需要給出一個卷積模板與原圖像進行卷積計算。整個過程可以看成是一個卷積模板在另外一個大的圖像上移動,對每個卷積模板覆蓋的區域進行點乘,得到的值作為中心像素點的輸出值。卷積首先需要將卷積模板旋轉180°,之後從圖像的左上角開始移動旋轉後的卷積模板,從左到右,從上到下依次進行卷積計算,最終得到卷積後的圖像。卷積模板又被稱為卷積核或者內核,是一個固定大小的二維矩陣,矩陣中存放着預先設定的數值。
圖像卷積過程大致可以分為以下5個步驟:
Step1:將卷積模板旋轉180°,由於多數情況中卷積模板中的數據是中心對稱的,因此有時這步可以省略,但是如果卷積模板不是中心對稱的,必須將模板進行旋轉。
Step2:將卷積模板中心放在原圖像中需要計算卷積的像素上,卷積模板中其餘部分對應在原圖像相應的像素上,如圖5-1所示,卷積模板和待卷積矩陣中黃色區域分別是卷積模板的中心和對應點,定位結果中陰影區域為模板覆蓋的區域。

圖5-1 圖像卷積步驟Step2
Step3:用卷積模板中的係數乘以圖像中對應位置的像素數值,並對所有結果求和,針對圖5-1表示的卷積步驟,其計算過程如式所示,最終計算結果為84.
Step4:將計算結果存放在原圖像中與卷積模板中心點像對應的像素處,即圖5-1里待卷積矩陣中的黃色像素處,結果如圖5-2所示。

圖5-2 圖像卷積步驟Step4
Step5:將卷積模板在圖像中從左至右從上到下移動,重複以上3個步驟,直到處理完所有的像素值,每一次循環的處理結果如圖5-3所示。

圖5-3 圖像卷積步驟Step5
通過前面的4個步驟已經完成了圖像卷積的主要部分,不過從圖5-3中的結果可以發現這種方法只能對圖像中心區域進行卷積,而由於卷積模板中心無法放置在圖像的邊緣像素處,因此圖像邊緣區域沒有進行卷積運算。卷積模板的中心無法放置在圖像邊緣的原因是當卷積模板的中心與圖像邊緣對應時,模板中部分數據會出現沒有圖像中的像素與之對應的情況,因此為了解決這個問題,我們主動將圖像的邊緣外推出去,例如與3×3的卷積模板運算時,用0在原圖像周圍增加一層像素,從而解決模板圖像中部分數據沒有對應像素的問題。
通過卷積的計算結果可以發現,最後一個像素值已經接近CV_8U數據類型的最大值,因此如果卷積模板選取不當,極有可能造成卷積結果超出數據範圍的情況發生,因此圖像卷積操作常將卷積模板通過縮放使得所有數值的和值為1,進而解決卷積後數值越界的情況發生,例如將圖5-1中卷積模板的所有數值除以12後再進行卷積操作。
針對上面的卷積過程,OpenCV 4中提供了filter2D()函數用於實現圖像和卷積模板之間的卷積運算,該函數的函數原型在代碼清單5-1中給出。
代碼清單5-1 filter2D()函數原型 1. void cv::filter2D(InputArray src, 2. OutputArray dst, 3. int ddepth, 4. InputArray kernel, 5. Point anchor = Point(-1,-1), 6. double delta = 0, 7. int borderType = BORDER_DEFAULT 8. )
- src:輸入圖像。
- dst:輸出圖像,與輸入圖像具有相同的尺寸和通道數。
- ddepth:輸出圖像的數據類型(深度),根據輸入圖像的數據類型不同擁有不同的取值範圍,具體的取值範圍在表5-1給出,當賦值為-1時,輸出圖像的數據類型自動選擇。
- kernel:卷積核,CV_32FC1類型的矩陣。
- anchor:內核的基準點(錨點),默認值(-1,-1)代表內核基準點位於kernel的中心位置。基準點即卷積核中與進行處理的像素點重合的點,其位置必須在卷積核的內部。
- delta:偏值,在計算結果中加上偏值。
- borderType:像素外推法選擇標誌,可以選取的參數及含義已經在表3-5中給出。默認參數為BORDER_DEFAULT,表示不包含邊界值倒序填充。
該函數用於實現圖像和卷積模板之間的卷積運算,函數第一個參數為輸入的待卷積圖像,允許輸入圖像為多通道圖像,圖像中的不同通道的卷積模板是同一個卷積模板,如果需要用不同的卷積模板對不同的通道進行卷積操作,需要先使用split()函數將圖像多個通道分離之後單獨對每一個通道求取卷積運算。函數第二個參數為輸出圖像,尺寸和通道數與第一個參數保持一致。輸出圖像的數據類型由第三個參數進行選擇,根據輸入圖像數據類型的不同,可供選擇的輸出數據類型也不相同,詳細取值範圍在表5-1給出。函數第四個參數為卷積模板矩陣,多數情況下該模板都是一個奇數尺寸的模板,例如3×3、5×5等。函數第五個參數指定卷積模板的中心位置,即圖5-1里卷積模板中黃色像素,中心點的位置可以在卷積模板中任意指定。函數最後兩個參數分別為計算卷積的偏值和像素外推方法選擇的標誌,卷積偏值表示在卷積步驟Step2計算結果的基礎上再加上偏值delta作為最終結果。

注意
filter2D()函數不會將卷積模板進行旋轉,如果卷積模板不對稱,需要首先將卷積模板旋轉180°後再輸入給函數的第四個參數。
表5-1 fillter2D()函數輸出圖像數據類型與輸入圖像數據類型的聯繫
輸入圖像數據類型 |
輸出圖像可選數據類型 |
---|---|
CV_8U |
-1 / CV_16S / CV_32F / CV_64F |
CV_16U / CV_16S |
-1 / CV_32F / CV_64F |
CV_32F |
-1 / CV_32F / CV_64F |
CV_64F |
-1 / CV_64F |
為了了解函數filter2D()使用方式,在代碼清單5-2中給出了圖5-1中的兩個矩陣之間卷積的代碼實現方法,並且對卷積模板進行了歸一化操作。由於給出的卷積模板是中心對稱的,因此可以省略卷積過程中模板旋轉180°的操作。程序卷積計算的結果如圖5-4所示,未歸一化的卷積結果與圖5-3給出的結果一致,歸一化後矩陣中的每個元素的數值都在一定的範圍內。另外在例程中利用相同的卷積模板對彩色圖像進行卷積,輸出結果在圖5-5給出,雖然卷積前後圖像內容一致,但是圖像整體變得模糊一些,可見圖像卷積具有對圖像模糊的作用。
代碼清單5-2 myFillter.cpp圖像卷積 1. #include <opencv2opencv.hpp> 2. #include <iostream> 3. 4. using namespace cv; 5. using namespace std; 6. 7. int main() 8. { 9. //待卷積矩陣 10. uchar points[25] = { 1,2,3,4,5, 11. 6,7,8,9,10, 12. 11,12,13,14,15, 13. 16,17,18,19,20, 14. 21,22,23,24,25 }; 15. Mat img(5, 5, CV_8UC1, points); 16. //卷積模板 17. Mat kernel = (Mat_<float>(3, 3) << 1, 2, 1, 18. 2, 0, 2, 19. 1, 2, 1); 20. Mat kernel_norm = kernel / 12; //卷積模板歸一化 21. //未歸一化卷積結果和歸一化卷積結果 22. Mat result, result_norm; 23. filter2D(img, result, CV_32F, kernel, Point(-1, -1), 2, BORDER_CONSTANT); 24. filter2D(img, result_norm, CV_32F, kernel_norm, Point(-1,-1),2, BORDER_CONSTANT); 25. cout << "result:" << endl << result << endl; 26. cout << "result_norm:" << endl << result_norm << endl; 27. //圖像卷積 28. Mat lena = imread("lena.png"); 29. if (lena.empty()) 30. { 31. cout << "請確認圖像文件名稱是否正確" << endl; 32. return -1; 33. } 34. Mat lena_fillter; 35. filter2D(lena, lena_fillter, -1, kernel_norm, Point(-1, -1), 2, BORDER_CONSTANT); 36. imshow("lena_fillter", lena_fillter); 37. imshow("lena", lena); 38. waitKey(0); 39. return 0; 40. }

圖5-4 myFillter.cpp程序中矩陣卷積結果

圖5-5 myFillter.cpp程序中圖像結果