本次作業將實作三項主題:
Denoise: 使用cv::GaussianBlur()、cv::blur()、cv::medianBlur()將圖片中的雜訊濾除
Edge Detection: 使用adaptiveThreshold()、Laplacian()、Sobel()描繪出圖形的邊緣
Canny Detector: 利用cv::Canny()執行邊緣偵測
作業環境:
Windows 11
OpenCV 4.6.0
Visual Studio 2022
雜訊產生於傳遞過程中受到干擾,去除雜訊的方法有很多種,最常見的方法是連續拍大量的照片,因為雜訊是隨機產生的,因此將所有的像數點取平均後放回,雜訊就能有效的被弭平甚至去除。本節主要介紹三種filter,將圖片平滑化以去除雜訊,分別為cv::GaussianBlur()、cv::blur()、cv::medianBlur()。
在這節我使用三種不同輸入影像來觀察在每種filter的效果。
三種影像為:salt-and-pepper noise、彩色雜訊、高ISO 照片
salt-and-pepper noise
在圖片中隨機加入黑色和白色pixel。因為是以程式寫入,優點是可以控制雜訊點數量,以觀察每種filter 的效果。我選擇單調的字體圖片來操作,因為可以容易觀察到邊界被模糊的效果,且圖片的顏色可以讓黑四和白色雜訊點明顯被辨認出來。
圖片來源:Pinterest
首先,使用 std::uniform_int_distribution產生亂數,先判斷圖片的大小,並產生row-1和column-1的亂數,且區間內選定的亂數值機率均相同。
接下來,設定n1, n2, b1, b2 兩個變數分別用來存取橫軸(row)和縱軸(column)的亂數,k用作計數器,用來控制迭代的圈數需小於n。
接著,判斷圖片是灰階還是彩色照片。
最後,將n1, n2所選到的pixel替換成白點(255),b1, b2 所選到的pixel替換成黑點(0)。
彩色雜訊
上一個處理的是黑色與白色點雜訊,所以我想測試彩色雜訊的效果,我將一張RGB的彩色雜訊圖片和影像做疊加來完成。
圖片來源:Pinterest、How to Effectively Reduce Noise Using Lightroom 3 Noise Reduction , Pinterest
使用cv::addWeighted()將圖片點對點相加,兩張圖的比重是各半做相加。
salt-and-pepper noise雜訊
salt(image, 5000);
彩色雜訊
高ISO 照片
3*3 gaussian filter 如右圖,由二維Gaussian方程式求得,特徵是中間高起兩邊低,將圖片和Gaussian Filter做卷積將會達到濾除雜訊、低通、模糊化的效果。和平均濾波器不同的是,平均濾波器因為權重都一樣會有圖像失真的問題,為了降低圖像失真,所以增加圖像中心點的權重。
使用gaussian filter 來模糊圖片已將低雜訊的程式碼如左。
cv::GaussianBlur(InputArray, OutputArray, kernalsize, sigmaX, sigmaY, borderType)
我選用9*9 的 kernal,讓模糊效果明顯以方便觀察結果,並在每種方法的前後加入 getTickCount()以計算處理照片所需的時間。
以下我改變cv::GaussianBlur()的參數觀察每個參數所帶來的效果。
第一張gif. 我改變不同kernal大小,sigma為預設值(sigmaX=0, sigmaY=0),並將全部製成一張gif. 以方便觀察,一共有八張照片3*3、9*9、13*13、19*19*、23*23、29*29、33*33、39*39,由結果可以觀察到kernal越大相片越模糊,去雜訊能力越好。
第二張圖,kernal大小為9*9,sigmaX=2, sigmaY=2,和使用預設值的做比較,使sigma=2的視覺上模糊效果較差,單顆雜訊顆粒的範圍較廣。
第三張圖,kernal大小為9*9,sigmaX=1, sigmaY=9,和第二張圖做比較,模糊效果較好,視覺上雜訊顆粒被左右(x軸方向)拉長。
cv::GaussianBlur(image, result, cv::Size(), cv::BORDER_DEFAULT);
cv::GaussianBlur(image, result, cv::Size(9, 9), 2, 2, cv::BORDER_DEFAULT);
cv::GaussianBlur(image, result, cv::Size(9, 9), 9, 1, cv::BORDER_DEFAULT);
cv::blur()為一個low-pass filter 可以將高頻的訊號去除,高頻訊號指的是邊緣、雜訊等,所以利用這個特性,雖然邊緣會變得模糊,但可以達到去除雜訊得效果。3*3 blur filter 如右圖,概念是將像數點和周圍的像數值取平均。
使用以下指令,我將kernal設定在9*9和GaussianBlur()設定相同,以便後面可以比較結果。
cv::blur(image, result, cv::Size(9, 9));
以下為模擬結果,隨著kernal的大小變大,字體的邊界變得越來越模糊,且視覺上雜訊和背景漸漸融為一體,雜訊漸漸降低。
cv::blur(image, result, cv::Size(3, 3));
cv::blur(image, result, cv::Size(9, 9));
cv::blur(image, result, cv::Size(11, 11));
cv::medianBlur()的概念是取kernal範圍內的中位數數值,以取代中心點的像數值,而且操作在salt-and-pepper noise的效果很顯著,因為是取中位數,而salt-and-pepper noise 是的像數值是0 和 255是整張圖的極值,不會被取值到。
使用以下指令,我將kernal的大小設定和前面相同,以方便比較。
cv::medianBlur(image, result, 9);
結果如下,由左至右分別將kernal 的大小調大,第一章途中還有零星小點雜訊,但後面兩張圖中幾乎看不到雜訊,從字體的角落會發現隨著kernal大小變大,原本直角處變得越來越圓弧,但視覺上畫面沒有變模糊,若繼續將kernal調大如是最後一張圖,圖中字母M和B看起來快要連在一起,像是墨水暈開的效果。
結果如下,從左至右依次增加了kernel的大小。在第一張圖中,還有一些零星的小點雜訊,但是在後面的兩張圖中,幾乎看不到雜訊。從字體的角落可以發現,隨著kernel大小的增加,原本直角處變得越來越圓弧,然而視覺上整個畫面並沒有變得模糊。如果繼續增加kernel大小,如最後一張圖,字母M和B看起來就快要連在一起,呈現的效果就像墨水暈開似的。
cv::medianBlur(image,result,3);
cv::medianBlur(image,result,9);
cv::medianBlur(image,result,13);
使用三種不同的影像,salt-and-pepper noise、彩色雜訊、高ISO 照片,重新模擬並相互比較結果。
首先,時間的部分如右圖,此數據使用的相片是加入salt-and-pepper noise 的照片
time(Gaussian) = 0.0873417ms
time(blur) = 0.0326096ms
time(medianBlur) = 0.138498ms
三種filter所需的時間長短:blur() > GaussianBlur() > medianBlur()
以下三張圖是前面出現過的結果,我將它們放大比較,每種filter的kernal均為9*9。
由結果判斷,使用cv::medianBlur()去雜訊的效果最好,畫面看起來也沒有變模糊。使用GaussianBlur()和blur()的圖片,使用相同的kernal大小所產生的像果非常接近,但視覺上我認為使用blur()的濾雜訊能力比使用GaussianBlur()好,blur()的畫面也比較模糊。
cv::GaussianBlur()
cv::blur()
cv::medianBlur()
右圖是須濾雜訊的原圖,以下三張為模擬結果,右側三張圖所使用filter的kernal均為9*9,我將結果放大比較。
三張結果圖的濾雜訊能力視覺上分不出優劣,均有一些些彩色雜訊點和原圖比較已經雜訊降低許多,像一塊塊的彩色色塊。經過GaussianBlur()和blur()的圖片看起來結果相近,但blur()又比GaussianBlur()看起來模糊。最後一張圖,看起來最不模糊,但字體暈開和相鄰的字母連在一起,原本菱角的部分便的圓滑,和前面的結果比較,印證前面所提及的,medianBlur()對於消除salt-and-pepper noise的效果較佳。
加入彩色雜訊
cv::GaussianBlur()
cv::blur()
cv::medianBlur()
以下我欲了解真實情況下的濾雜訊效果,右圖是須濾雜訊的原圖,以下三張為模擬結果,右側三張圖所使用filter的kernal均為9*9,我將結果放大比較。
從三張結果圖可以觀察到,視覺上雜訊均被消除,畫面也變得模糊,原本有線條的地方變的平滑,像是眼周附近的紋路被彌平,髮絲也僅看得出大致的走向,畫面呈現一格格像是解析度不足的效果,又以使用 medianBlur()的相片看起來最不模糊,另外兩張視覺上效果一樣。
cv::GaussianBlur()
cv::blur()
cv::medianBlur()
透過邊緣檢測可大幅減少資料量,篩選出相關資訊並保留影像的結構性,而一個物件的邊緣代表一張圖片的顏色變化強烈的地方,是高頻訊號,循著這個概念,求得邊緣的方法有:將圖片作微分,微分後不為零的地方為邊緣,或是將低頻訊號濾除求得邊緣。因為邊緣和雜訊均為高頻訊號,所以做邊緣偵測會放大雜訊。
以下將實作三種邊緣偵測的方法:
Adaptive Threshold:取顏色變化劇烈的地方
Sobel:近似Gaussian 一階微分
Scharr:比Sobel更接近Gaussian 一階微分
Laplace:二階微分
這部分我選擇兩張圖來操作,第一張邊緣多為水平或垂直線條,測試Sobel分別取水平線和垂直線的特性,第二張為有雜訊的高ISO圖片,以測試每種方法的抗雜訊能力。
在Adaptive Thresholding 中的threshold值的計算方式是 :T = mean(blockSize) – threshold ,區域像數值平均後減去一個常數。代表區域中顏色變化大的部分會被留下來,因此可以拿來做邊緣偵測,但雜訊是高頻,和四周顏色差異大,也會被留下,所以無濾雜訊的效果。
int blockSize = 3; // size of the neighborhood
int threshold = 15;// pixel will be(mean-threshold)
cv::adaptiveThreshold(image, // input image
binaryAdaptive, // output binary image
255, // max value for output
cv::ADAPTIVE_THRESH_MEAN_C, // adaptive method
cv::THRESH_BINARY, // threshold type
blockSize, // size of the block
threshold); // threshold used
我將取值範圍設定在3*3和之後要操作的function設定相同以比較結果。
將區域像數值平均後減去一個常數後得到的值改為255其餘的像數值改為0,所以顏色變化的就會呈現一條黑線。
以下我改變不同的取值區域範圍,發現一次取越多像數算平均,呈現出來影像的邊緣就越粗。
int blockSize = 3;
int threshold = 15;
int blockSize = 7;
int threshold = 15;
int blockSize = 9;
int threshold = 15;
Sobel() 近似Gaussian 的一次微分,原理如下,邊緣是影像顏色梯度變化大的部分,將影像做一次微分不為零的部份便是邊緣,因為sobel近似Gaussian因此有去雜訊的功能。
一次微分
----------->
對x軸做偏微分後會得到垂直方向的邊緣,此kernal為右側
對y軸做偏微分會得到水平方向的邊緣,此kernal如右
取得兩種方向的邊緣後,相加就可以得到完整的邊緣圖。
程式碼如左圖
Sobel(InputArray , OutputArray , depth, dx, dy,
ksize = 3, scale, delta, borderType=BORDER_DEFAULT)
dx: x方向的微分階數 dy:y方向的微分階數
scale: 輸出結果縮放係數
delta: 輸出結果位移係數
先分別將水平方向的邊緣和垂直方向的邊緣取出來後,在使用cv::addWeighted()將兩張圖合成一張完整的邊緣圖。
Scharr
因為 kernal為3*3的sobel() 和Gaussian的誤差過大,所以OpenCV還提供了另一種filter,cv::Scharr()更精確的Gaussian一階微分filter。
對x軸做偏微分後會得到垂直方向的邊緣,此kernal為右側
對y軸做偏微分會得到水平方向的邊緣,此kernal如右
函式使用的方法和Sobel()一樣
Scharr(InputArray , OutputArray , depth, dx, dy, scale=1, delta=0, borderType)
先分別找出水平方向和垂直方向的邊緣,再使用cv::addWeighted()將者張圖和在一起
以下六張圖為模擬結果,從地磚可以觀察到,對x軸做微分和對y軸做微分的效果。將Sobel_Result和Scharr_Result兩張圖做比較,可以清楚得發現,使用scharr()的影像取到更多的細節,看起來較雜亂。
cv::Sobel(image, sobelX, CV_8U, 1, 0, 3, 0.4, 128); //垂直邊緣
cv::Sobel(image, sobelY, CV_8U, 0, 1, 3, 0.4, 128); //水平邊緣
Sobel_Result
00
cv::Scharr(image, scharrX, CV_8U, 1, 0, 0.4, 128); //垂直邊緣
cv::Scharr(image, scharrX, CV_8U, 0, 1, 0.4, 128); //水平邊緣
Scharr_Result
00
以下改變Sobel() 裡面的參數值,以了解每個參數所代表的意義。第一張圖,將kernal改為5*5,取到的邊緣有更多的細節,看起來更為雜亂。第二張圖,將輸出結果縮放係數改為0.1,會改變影像中的邊緣顏色,降低比例後顏色變淡變的不明顯。第三張圖,改變輸出結果位移係數改為0,會發現除了邊緣部分其餘的顏色變成黑色(0)。
kernal = 5*5
scale = 0.1
delta = 0
接續Sobel的觀念,若對影像做二次微分,結果如右圖,同樣的二階微分不為0的地方就是邊緣,但和義街無诶分不同的是,二階微分有波峰波谷,因此二次微分時就選擇零點交叉 ( Zero-crossing ) 的位置來描述出判斷邊界。
Laplacian() 的kernal如右圖,長相呈現中間高而兩邊是負值,到四角則回到0。
我將 Laplacian()裡的參數設定和Sobel()一樣,以方便觀察差異。 Laplacian()的使用方法和Sobel()很像
Laplacian(InputArray,OutputArray, depth, ksize, scale,delta,
borderType = BORDER_DEFAULT)
程式碼如下:
cv::Mat laplace;
cv::Laplacian(image, laplace, CV_8U, 3, 0.4, 128);
cv::namedWindow("laplace");
cv::imshow("laplace", laplace);
我改變不同的kernal大小,觀察kernal帶來的影響。結果如下,隨著kernal size增加邊緣看起來加粗,偵測到的細節也增加,畫面看起來變的雜亂。
cv::Laplacian(image, laplace, CV_8U, 3, 0.4, 128);
cv::Laplacian(image, laplace, CV_8U, 5, 0.4, 128);
cv::Laplacian(image, laplace, CV_8U, 7, 0.4, 128);
以下為設定相同參數但使用不同filter所產生的結果。adaptiveThreshold()的圖呈現以點狀來描繪圖形,所以邊緣會有些許不連續的斷點,呈現的圖形沒有另外兩個清晰。將Sobel()和Laplacian()的圖形做比較,視覺上是使用Laplacian()的圖形呈現較多的細節,例如圖片中的地磚上的紋路,Laplacian()呈現的比較錯綜複雜。Sobel()和Laplacian()兩張圖參數均設定為 scale = 0.4、delta = 128,背景色呈現相同,但邊緣白線使用Laplacian()較白。
cv::adaptiveThreshold()
cv::Sobel()
cv::Laplacian()
以下我將輸入改為一張帶有雜訊的高IOS圖,相互比較adaptiveThreshold()的抗雜訊能力最差,Laplacian()次之,Sobel()的抗雜訊能力最好,雜訊點變得比邊緣線淡,所以圖形的輪廓比另外兩張圖還清晰。
cv::adaptiveThreshold()
cv::Sobel()
cv::Laplacian()
Canny Edge Detection 運用廣泛,一個完整的Canny Edge Detection主要有四個步驟:
使用 2D Gaussian平滑圖形來降低雜訊,邊緣和雜訊均為高頻,在邊緣偵測前須先降低雜訊。
數學是可以寫成:S(a, y)=G(0, sigma)*f(x,y),在這個步驟中,若sigma 越小,邊緣則越多細節。
使用Sobel 來計算梯度的方向和強度大小,圖形中越亮的部分大小就越大。
因為在一個 edge 附近的每個 pixel 都會具有非零的梯度值,如果將這些 pixel 都當作 edge,最後就會產生很粗的 edge,因此這一步的目的是在密集的區域中,找出最大值,再把其他去掉。實作的方式是把每個pixel 和梯度方向的鄰居比較梯度值,如果不是最大的,就去除(pixel = 0)。也可以利用二次微分的特性,對上一步的結果做1D的Laplace ,就可以得到 strong zero-crossing,便是邊緣位置。
最後這個步驟會需要輸入兩個參數值threshold high和threshold low ,這個步驟的目的是為了讓結果的邊緣清晰,需消除假邊(spurious edge) 接著相互做比較:
> threshold high:確定就是 edge(strong edge)
> threshold high && < threshold low:再下一步判斷(weak edge)
< threshold low:不是 edge
最後讓在邊緣附近的線段連續,從strong edge出發透過檢驗 weak edge 是否可以連到 strong edge 來判斷這個 edge 是否保留。
以下為Canny Edge Detection的流程圖:
右圖為使用canny() 來做邊緣偵測的程式碼,一共操作兩次,第一次為純做邊緣偵測,第二次加入salt-and-pepper noise 觀察雜訊對邊緣爭測的影響,根據理論可以理解每個參數所代表的意思
Canny(InputArray,OutputArray,threshold1 ,threshold2,Sobel kernal Size,
bool L2gradient=false)
threshold1, threshold2用來區分 strong edge 和 weak edge,範圍都是 0 ~ 255,通常選擇 threshold1 / threshold2 = 1/2 ~ 1/3
我選擇圖的方向是希望有明顯的色塊差異,且有部分的圖較雜亂如草地,以觀察在不同複雜度的效果。
圖片來源:12 breeds that make great house cows , Pinterest
以下比較雜訊帶來的影響,加入salt-and-pepper noise後再做Canny edge detection,視覺上雜訊被放大,驗證前面有提及過的原理,因為雜訊和邊緣均為高頻,所以做完邊緣偵測,雜訊會被放大,雖然Canny edge detection的第一步是濾雜訊,從結果可以發覺,3*3的Gaussian filter 對salt-and-pepper noise的濾雜訊能力不足。
無加入雜訊結果
加入salt-and-pepper noise
加入雜訊結果
以下改變不同的參數,以觀察不同參數對結果的影響。將threshold1對 threshold2 比值從三分之一提升到二分之一,從畫面中草地的部分可以觀察到,比值提升後,邊緣變少偵測到的細節減少。將Sobel kernal size從3改為5,畫面中的線條變得很雜亂。
threshold1 / threshold2 = 1/3
threshold1 / threshold2 = 1/2
kernal = 5
在執行最後一部份 Canny edge detection時,一開始我打算不使用 Canny() 函式,按照理論逐步編寫程式碼,並觀察每個步驟產生的效果。我找到了可參考的程式碼,但最終未能成功,因為從網站上獲取的程式碼都是片段資訊,我對程式撰寫的熟練度也不足,所以我無法修改這些片段程式碼成能執行的狀態,另外也可能是版本不相容,導致一些函式無法呼叫。即便如此,我根據程式碼畫出了流程圖 (放在 Canny Detector 中)後發覺,參考別人撰寫的程式碼比僅閱讀網站上的文字敘述,更能理解 Canny 邊緣檢測的原理。
Image Filtering , OpenCV 4.6.0, Open Source Computer Vision
Operations on arrays , OpenCV 4.6.0, Open Source Computer Vision
Image Denoising , OpenCV 4.6.0, Open Source Computer Vision
Smoothing Images , OpenCV 4.6.0, Open Source Computer Vision
[Python]Gaussian Filter-概念與實作 , Dec 3, 2018, Medium
OpenCV Python Tutorial For Beginners 18 - Smoothing Images | Blurring Images OpenCV , ProgrammingKnowledge , 2019.5.14 , YouTube
邊緣偵測 - 索伯算子 ( Sobel Operator ) , Jan 22, 2021 , Medium
Sobel Edge Detector ,©2003 R. Fisher, S. Perkins, A. Walker and E. Wolfart. , HIPR2
Edge Detection Using OpenCV , LearnOpenCV
Sobel Derivatives , OpenCV 4.6.0, Open Source Computer Vision
Laplace Operator , OpenCV 4.6.0, Open Source Computer Vision
Feature Detection , OpenCV 4.6.0, Open Source Computer Vision
Canny Edge Detector | Edge Detection, Shree K. Nayar, First Principles of Computer Vision, Mar 3, 2021, YouTube
單元六、邊緣偵測, 陳慶瀚, 2004-11-03
Canny edge detector 實作(OpenCV), Aug 4, 2018, Medium