圖像的邊緣
圖像的邊緣從數學上是如何表示的呢?

圖像的邊緣上,鄰近的像素值應當顯著地改變了。而在數學上,導數是表示改變快慢的一種方法。梯度值的大變預示着圖像中內容的顯著變化了。
用更加形象的圖像來解釋,假設我們有一張一維圖形。下圖中灰度值的“躍升”表示邊緣的存在:

使用一階微分求導我們可以更加清晰的看到邊緣“躍升”的存在(這里顯示為高峰值):

由此我們可以得出:邊緣可以通過定位梯度值大於鄰域的相素的方法找到。
卷積
卷積可以近似地表示求導運算。
那么卷積是什么呢?
卷積是在每一個圖像塊與某個算子(核)之間進行的運算。
核?!
核就是一個固定大小的數值數組。該數組帶有一個錨點 ,一般位於數組中央。

可是這怎么運算啊?
假如你想得到圖像的某個特定位置的卷積值,可用下列方法計算:
- 將核的錨點放在該特定位置的像素上,同時,核內的其他值與該像素鄰域的各像素重合;
- 將核內各值與相應像素值相乘,並將乘積相加;
- 將所得結果放到與錨點對應的像素上;
- 對圖像所有像素重復上述過程。
用公式表示上述過程如下:
在圖像邊緣的卷積怎么辦呢?
計算卷積前,OpenCV通過復制源圖像的邊界創建虛擬像素,這樣邊緣的地方也有足夠像素計算卷積了。
近似梯度
比如內核為3時。
首先對x方向計算近似導數:

然后對y方向計算近似導數:

然后計算梯度:

當然你也可以寫成:

開始求梯度
#include "opencv2/imgproc/imgproc.hpp" #include "opencv2/highgui/highgui.hpp" #include <stdlib.h> #include <stdio.h> using namespace cv; int main( int argc, char** argv ){ Mat src, src_gray; Mat grad; char* window_name = "求解梯度"; int scale = 1; int delta = 0; int ddepth = CV_16S; int c; src = imread( argv[1] ); if( !src.data ){ return -1; } //高斯模糊 GaussianBlur( src, src, Size(3,3), 0, 0, BORDER_DEFAULT ); //轉成灰度圖 cvtColor( src, src_gray, CV_RGB2GRAY ); namedWindow( window_name, CV_WINDOW_AUTOSIZE ); Mat grad_x, grad_y; Mat abs_grad_x, abs_grad_y; Sobel( src_gray, grad_x, ddepth, 1, 0, 3, scale, delta, BORDER_DEFAULT ); convertScaleAbs( grad_x, abs_grad_x ); Sobel( src_gray, grad_y, ddepth, 0, 1, 3, scale, delta, BORDER_DEFAULT ); convertScaleAbs( grad_y, abs_grad_y ); addWeighted( abs_grad_x, 0.5, abs_grad_y, 0.5, 0, grad ); imshow( window_name, grad ); waitKey(0); return 0; }
Sobel函數
索貝爾算子(Sobel operator)計算。
- C++: void Sobel (InputArray src, OutputArray dst, int ddepth, int dx, int dy, int ksize=3, double scale=1, double delta=0, int borderType=BORDER_DEFAULT )
參數
- src – 輸入圖像。
- dst – 輸出圖像,與輸入圖像同樣大小,擁有同樣個數的通道。
- ddepth –
- 輸出圖片深度;下面是輸入圖像支持深度和輸出圖像支持深度的關系:
- src.depth() = CV_8U, ddepth = -1/CV_16S/CV_32F/CV_64F
- src.depth() = CV_16U/CV_16S, ddepth = -1/CV_32F/CV_64F
- src.depth() = CV_32F, ddepth = -1/CV_32F/CV_64F
- src.depth() = CV_64F, ddepth = -1/CV_64F
當 ddepth為-1時, 輸出圖像將和輸入圖像有相同的深度。輸入8位圖像則會截取頂端的導數。
- xorder – x方向導數運算參數。
- yorder – y方向導數運算參數。
- ksize – Sobel內核的大小,可以是:1,3,5,7。
- scale – 可選的縮放導數的比例常數。
- delta – 可選的增量常數被疊加到導數中。
- borderType – 用於判斷圖像邊界的模式。
代碼注釋:
//在x方向求圖像近似導數 Sobel( src_gray, grad_x, ddepth, 1, 0, 3, scale, delta, BORDER_DEFAULT ); //在y方向求圖像近似導數 Sobel( src_gray, grad_y, ddepth, 0, 1, 3, scale, delta, BORDER_DEFAULT );
如果我們打印上面兩個輸出矩陣,可以看到grad_x和grad_y中的元素有正有負。
當然,正方向遞增就是正的,正方向遞減則是負值。
這很重要,我們可以用來判斷梯度方向。
convertScaleAbs函數
線性變換轉換輸入數組元素成8位無符號整型。
- C++: void convertScaleAbs (InputArray src, OutputArray dst, double alpha=1, double beta=0 )
參數
- src – 輸入數組。
- dst – 輸出數組。
- alpha – 可選縮放比例常數。
- beta – 可選疊加到結果的常數。
對於每個輸入數組的元素函數convertScaleAbs 進行三次操作依次是:縮放,得到一個絕對值,轉換成無符號8位類型。
對於多通道矩陣,該函數對各通道獨立處理。如果輸出不是8位,將調用Mat::convertTo 方法並計算結果的絕對值,例如:
Mat_<float> A(30,30); randu(A, Scalar(-100), Scalar(100)); Mat_<float> B = A*5 + 3; B = abs(B);
為了能夠用圖像顯示,提供一個直觀的圖形,我們利用該方法,將-256 — 255的導數值,轉成0 — 255的無符號8位類型。
addWeighted函數
計算兩個矩陣的加權和。
- C++: void addWeighted (InputArray src1, double alpha, InputArray src2, double beta, double gamma, OutputArray dst, int dtype=-1 )
參數
- src1 – 第一個輸入數組。
- alpha – 第一個數組的加權系數。
- src2 – 第二個輸入數組,必須和第一個數組擁有相同的大小和通道。
- beta – 第二個數組的加權系數。
- dst – 輸出數組,和第一個數組擁有相同的大小和通道。
- gamma – 對所有和的疊加的常量。
- dtype – 輸出數組中的可選的深度,當兩個數組具有相同的深度,此系數可設為-1,意義等同於選擇與第一個數組相同的深度。
函數addWeighted 兩個數組的加權和公式如下:
在多通道情況下,每個通道是獨立處理的,該函數可以被替換成一個函數表達式:
dst = src1*alpha + src2*beta + gamma;
利用convertScaleAbs和addWeighted,我們可以對梯度進行一個可以用圖像顯示的近似表達。
這樣我們就可以得到下面的效果:

梯度方向
但有時候邊界還不夠,我們希望得到圖片色塊之間的關系,或者研究樣本的梯度特征來對機器訓練識別物體時候,我們還需要梯度的方向。
二維平面的梯度定義為:

這很好理解,其表明顏色增長的方向與x軸的夾角。
但Sobel算子對於沿x軸和y軸的排列表示的較好,但是對於其他角度表示卻不夠精確。這時候我們可以使用Scharr濾波器。
Scharr濾波器的內核為:

這樣能提供更好的角度信息,現在我們修改原程序,改為使用Scharr濾波器進行計算:
#include "opencv2/imgproc/imgproc.hpp" #include "opencv2/highgui/highgui.hpp" #include <stdlib.h> #include <stdio.h> using namespace cv; int main( int argc, char** argv ){ Mat src, src_gray; Mat grad; char* window_name = "梯度計算"; int scale = 1; int delta = 0; int ddepth = CV_16S; int c; src = imread( argv[1] ); if( !src.data ){ return -1; } GaussianBlur( src, src, Size(3,3), 0, 0, BORDER_DEFAULT ); cvtColor( src, src_gray, CV_RGB2GRAY ); namedWindow( window_name, CV_WINDOW_AUTOSIZE ); Mat grad_x, grad_y; Mat abs_grad_x, abs_grad_y; //改為Scharr濾波器計算x軸導數 Scharr( src_gray, grad_x, ddepth, 1, 0, scale, delta, BORDER_DEFAULT ); convertScaleAbs( grad_x, abs_grad_x ); //改為Scharr濾波器計算y軸導數 Scharr( src_gray, grad_y, ddepth, 0, 1, scale, delta, BORDER_DEFAULT ); convertScaleAbs( grad_y, abs_grad_y ); addWeighted( abs_grad_x, 0.5, abs_grad_y, 0.5, 0, grad ); imshow( window_name, grad ); waitKey(0); return 0; }
Scharr函數接受參數與Sobel函數相似,這里就不敘述了。
下面我們通過divide函數就能得到一個x/y的矩陣。
對兩個輸入數組的每個元素執行除操作。
- C++: void divide (InputArray src1, InputArray src2, OutputArray dst, double scale=1, int dtype=-1 )
- C++: void divide (double scale, InputArray src2, OutputArray dst, int dtype=-1 )
參數
- src1 – 第一個輸入數組。
- src2 – 第二個輸入數組,必須和第一個數組擁有相同的大小和通道。
- scale – 縮放系數。
- dst – 輸出數組,和第二個數組擁有相同的大小和通道。
- dtype – 輸出數組中的可選的深度,當兩個數組具有相同的深度,此系數可設為-1,意義等同於選擇與第一個數組相同的深度。
該函數對兩個數組進行除法:
或則只是縮放系數除以一個數組:
這種情況如果src2是0,那么dst也是0。不同的通道是獨立處理的。
被山寨的原文
Sobel Derivatives . OpenCV.org
Image Filtering . OpenCV.org





