图像处理-008图像梯度
图像可以用平面空间的二维函数f(x,y)来表示,x,y表示空间平面的坐标。图像梯度则是度量图像亮度在x,y方向上的变化程度。梯度提供了两部分信息:一是梯度值的大小展示了图像亮度的变化快慢程度,二是梯度提供了亮度变化的方向。
为便于解释,可用地形来类比。将图像视为地形,图像中的亮度值看做地形中的高度,则地形中任意点的坡度方向为图像的梯度方向,沿坡度前进时地形高度的变化程度则为图像梯度的变化程度。
鉴于梯度具有方向、大小,则用向量来表示梯度。向量的方向即为梯度的方向,向量的长度为梯度的大小。梯度的方向是函数f(x,y)变化最快的方向,沿着梯度的方向容易找到最大值。
从数学的角度来讲,梯度是图像亮度在x,y方向上求导。
∇ f = [ g x g y ] = [ ∂ f ∂ x ∂ f ∂ y ] (008-1) \displaystyle{ \nabla f=\begin{bmatrix} g_{x} \\ g_{y} \end{bmatrix} = \begin{bmatrix} \frac{\partial f}{\partial x} \\ \frac{\partial f}{\partial y} \end{bmatrix} \tag{008-1} } ∇f=[gxgy]=[∂x∂f∂y∂f](008-1)
其中: ∂ f ∂ x \displaystyle{ \textstyle\frac{\partial f}{\partial x} } ∂x∂f表示x方向导数,即x方向的梯度, ∂ f ∂ y \displaystyle{ \textstyle\frac{\partial f}{\partial y} } ∂y∂f表示y方向的导数,即y方向的梯度。
坐标(x,y)梯度方向由公式008-2计算
θ = t a n − 1 [ g y g x ] (008-2) \displaystyle{ \theta = \operatorname{tan{^-}{^1}} \left [ \frac{g_{y}}{g_{x}} \right ] \tag{008-2} } θ=tan−1[gxgy](008-2)
坐标(x,y)梯度大小由公式008-3计算
g y 2 + g x 2 (008-3) \displaystyle{ \sqrt{g_{y}^{2} + g_{x}^{2}} \tag{008-3} } gy2+gx2(008-3)
公式008-3计算时计算量比较大,为提升计算效率,采用公式008-4来计算点(x,y)处的梯度。
∣ g x ∣ + ∣ g y ∣ (008-4) |g_x| + |g_y| \tag{008-4} ∣gx∣+∣gy∣(008-4)
梯度计算时,源图像与大小为ksize*ksize的kernel做卷积运算,卷积时,卷积核分水平和垂直两个方向。卷积核有如下特点:
-
卷积核以奇数型矩阵的形式存在,奇数一般为3,5,7;
-
卷积核中同行(行梯度)或同列(列梯度)的系数以互为相反数的形式对称分布;
-
卷积核中各系数和为0;
-
卷积核中系数大小及排班顺序决定了对图像区域处理的类型。
P 5 = c 1 ∗ p 1 + c 2 ∗ P 2 + c 3 ∗ p 3 + c 4 ∗ p 4 + c 5 ∗ p 5 + c c 6 ∗ p 6 + c 7 ∗ p 7 + c 8 ∗ p 8 + c 9 ∗ p 9 P5=c1*p1+c2*P2 + c3*p3+c4*p4+c5*p5+cc6*p6+c7*p7+c8*p8+c9*p9 P5=c1∗p1+c2∗P2+c3∗p3+c4∗p4+c5∗p5+cc6∗p6+c7∗p7+c8∗p8+c9∗p9
图像中P5位置由P5邻域元素与卷积核系数乘积和来确定,P5值越大则表示P5与周围邻域元素差异越大。
若图像中某像素点的像素值与其邻域的像素值差异大,则该点是边缘的可能性越大,因此,梯度被用做图像边缘检测。
计算像素点(x,y)水平方向梯度时
P 5 x = ( p 3 − p 1 ) ∗ 1 + ( p 6 − p 4 ) ∗ 1 + ( p 9 − − p 7 ) ∗ 1 (008-6) P5_x=(p3-p1) * 1 + (p6-p4) * 1 + (p9--p7) * 1 \tag{008-6} P5x=(p3−p1)∗1+(p6−p4)∗1+(p9−−p7)∗1(008-6)
计算像素点(x,y)垂直方向梯度时
P 5 y = ( p 7 − p 1 ) ∗ 1 + ( p 8 − p 2 ) ∗ 1 + ( p 9 − − p 3 ) ∗ 1 (008-7) P5_y=(p7-p1) * 1 + (p8-p2) * 1 + (p9--p3) * 1 \tag{008-7} P5y=(p7−p1)∗1+(p8−p2)∗1+(p9−−p3)∗1(008-7)
从公式008-2, 008-3可以得出:水平方向梯度计算后得到垂直方向边界,垂直方向梯度计算后得到水平方向梯度。
opencv中提供了 cv.Sobel(), cv.Scharr(), cv.Laplacian()算子来处理图像梯度。
Prewitt
prewitt是一种一阶微分算子的边缘检测,它依据其四邻域中上下、左右点灰度像素差值来计算,在边缘处差值达到最大。prewitt提供两种梯度算子:
沿x轴方向的水平梯度算子;
沿y轴方向的垂直梯度算子。
Soble
soble结合高斯模糊与微分求导,利用局部差分寻找边缘,所得的值是梯度的近似值,与prewitt相比,sobel强调与边缘相邻的像素点对边缘的影响。
同prewitt算子一样,sobel算子也按x,y轴分水平梯度和垂直梯度。
opencv提供函数cv.Sobel()来处理sobel变换,其详情如下:
c/c++
void cv::Sobel(InputArray src, #输入图像
OutputArray dst, #输出图像,与输入图像同类型,同大小
int ddepth, #图像深度
int dx, #x轴的导数阶数
int dy, #y轴的导数阶数
int ksize = 3, #卷积核大小,一般为奇数 如:3,5,7,9
double scale = 1,#计算导数时值时的缩放因子
double delta = 0,#输入dst前,添加到dst上的值
int borderType = BORDER_DEFAULT #边界像素展拓类型
)
Python:
cv.Sobel(src, ddepth, dx, dy[, dst[, ksize[, scale[, delta[, borderType]]]]]) -> dst
实现代码: C++
/**
* sobel算子
* @param origin_img
* @return
*/
int ImageGradient::sobel(const Mat &origin_img) {
cout << origin_img << endl;
Mat sobel_x, sobel_y;
Sobel(origin_img, sobel_x, CV_64F, 1, 0, 5);
imshow("sobel_x", sobel_x);
convertScaleAbs(sobel_x, sobel_x);
imshow("sobel_x_1", sobel_x);
Sobel(origin_img, sobel_y, CV_64F, 0, 1, 5);
imshow("sobel_y", sobel_y);
convertScaleAbs(sobel_y, sobel_y);
imshow("sobel_y_1", sobel_y);
Mat sobel_xy;
Sobel(origin_img, sobel_xy, CV_64F, 1, 1, 5);
imshow("sobel_xy", sobel_xy);
convertScaleAbs(sobel_xy, sobel_xy);
imshow("sobel_xy_1", sobel_xy);
return EXIT_SUCCESS;
}
实现代码: python
def sobel(origin_img):
titles = ["origin_image"]
images = [origin_img]
sobel_x = cv.Sobel(origin_img, cv.CV_64F, 1, 0, 5)
titles.append("sobel_x")
images.append(sobel_x)
sobel_y = cv.Sobel(origin_img, cv.CV_64F, 0, 1, 5)
titles.append("sobel_y")
images.append(sobel_y)
sobel_xy = cv.Sobel(origin_img, cv.CV_64F, 1, 1, 5)
titles.append("sobel_xy")
images.append(sobel_xy)
sobel_x_abs = cv.convertScaleAbs(sobel_x)
titles.append("sobel_x_abs")
images.append(sobel_x_abs)
sobel_y_abs = cv.convertScaleAbs(sobel_y)
titles.append("sobel_y_abs")
images.append(sobel_y_abs)
sobel_xy_abs = cv.convertScaleAbs(sobel_xy)
titles.append("sobel_xy_abs")
images.append(sobel_xy_abs)
for i in range(7):
plt.subplot(2, 4, i + 1)
plt.imshow(images[i], 'gray', vmin=0, vmax=255)
plt.title(titles[i])
plt.xticks(), plt.yticks([])
plt.show()
效果如图008-7所示:
Scharr
scharr与sobel一样用来计算(x,y)处的梯度,与sobel相比仅kernel存在差异。scharr算子如图008-8所示:
从图008-8可以看出,scharr卷积核中的系数与sobel中的卷积核系数不同。scharr算子增大了锚点邻域的权重,使得scharr算子更易于探测边缘不太明显的图像的边缘。
opencv提供cv.Scharr()函数用于scharr梯度的计算,其详细描述如下:
c/c++
void cv::Scharr(InputArray src, #输入图像
OutputArray dst, #输出图像,与输入图像同类型,同大小
int ddepth, #图像深度
int dx, #x轴的导数阶数 0(不计算), 1(一阶导数), 2(二阶导数)
int dy, #y轴的导数阶数 0, 1, 2
double scale = 1, #计算导数时值时的缩放因子
double delta = 0, #输入dst前,添加到dst上的值
int borderType = BORDER_DEFAULT #边界像素展拓类型
)
Python:
cv.Scharr(src, ddepth, dx, dy[, dst[, scale[, delta[, borderType]]]])->dst
实现代码: C/C++
/**
* scharr 算子
* @param origin_img
* @return
*/
int ImageGradient::scharr(const Mat &origin_img) {
Mat kernel_x, kernel_y;
Mat scharr_x, scharr_y;
Scharr(origin_img, scharr_x, CV_64F, 1, 0, 7);
imshow("scharr_x", scharr_x);
convertScaleAbs(scharr_x, scharr_x);
imshow("scharr_x_1", scharr_x);
Scharr(origin_img, scharr_y, CV_64F, 0, 1, 7);
imshow("scharr_y", scharr_y);
convertScaleAbs(scharr_y, scharr_y);
imshow("scharr_y_1", scharr_y);
return EXIT_SUCCESS;
}
实现代码: python
# scharr
def scharr(origin_img):
titles = ["origin_image"]
images = [origin_img]
scharr_x = cv.Scharr(origin_img, cv.CV_64F, 1, 0)
titles.append("scharr_x")
images.append(scharr_x)
scharr_y = cv.Scharr(origin_img, cv.CV_64F, 0, 1)
titles.append("scharr_y")
images.append(scharr_y)
scharr_x_abs = cv.convertScaleAbs(scharr_x)
titles.append("scharr_x_abs")
images.append(scharr_x_abs)
scharr_y_abs = cv.convertScaleAbs(scharr_y)
titles.append("scharr_y_abs")
images.append(scharr_y_abs)
for i in range(5):
plt.subplot(2, 3, i + 1)
plt.imshow(images[i], 'gray', vmin=0, vmax=255)
plt.title(titles[i])
plt.xticks(), plt.yticks([])
plt.show()
效果图如图008-9
Laplacian
sobel与scharr算子都具有方向性,需要分别计算x轴,y轴方向的梯度后,再整合计算出点(x,y)处的梯度。laplacian算子定义如公式008-8所示:
Δ s r c = ∂ 2 s r c ∂ x 2 + ∂ 2 s r c ∂ y 2 (008-8) \Delta src = \frac{\partial ^2{src}}{\partial x^2} + \frac{\partial ^2{src}}{\partial y^2} \tag{008-8} Δsrc=∂x2∂2src+∂y2∂2src(008-8)
laplacian算子卷积核如图008-10所示:
opencv提供函数cv.Laplacian()用作laplacian运算,其描述如下:
c/c++
void cv::Laplacian(InputArray src, #输入图像
OutputArray dst, #输出图像,与输入图像同类型,同大小
int ddepth, #图像深度
int ksize = 1, #卷积核大小,一般为奇数
double scale = 1,#计算导数时值时的缩放因子
double delta = 0,#输入dst前,添加到dst上的值
int borderType = BORDER_DEFAULT #边界像素展拓类型
)
Python:
cv.Laplacian(src, ddepth[, dst[, ksize[, scale[, delta[, borderType]]]]]) ->dst
实现代码: C/C++
/**
* laplacian算子
* @param origin_img
* @return
*/
int ImageGradient::laplacian(const Mat &origin_img) {
Mat kernel_1;
Laplacian(origin_img, kernel_1, CV_64F, 1);
imshow("kernel_1", kernel_1);
convertScaleAbs(kernel_1, kernel_1);
imshow("kernel_1_abs", kernel_1);
Mat kernel_3;
Laplacian(origin_img, kernel_3, CV_64F, 3);
imshow("kernel_3", kernel_3);
convertScaleAbs(kernel_3, kernel_3);
imshow("kernel_3_abs", kernel_3);
Mat kernel_5;
Laplacian(origin_img, kernel_5, CV_64F, 5);
imshow("kernel_5", kernel_5);
convertScaleAbs(kernel_5, kernel_5);
imshow("kernel_5_abs", kernel_5);
// 滤波处理
Mat blur;
GaussianBlur(origin_img, blur, Size(1, 1), 0);
Laplacian(blur, kernel_1, CV_64F, 1);
imshow("kernel_1_blur", kernel_1);
convertScaleAbs(kernel_1, kernel_1);
imshow("kernel_1_abs_blur", kernel_1);
GaussianBlur(origin_img, blur, Size(3, 3), 0);
Laplacian(blur, kernel_3, CV_64F, 3);
imshow("kernel_3_blur", kernel_3);
convertScaleAbs(kernel_3, kernel_3);
imshow("kernel_3_abs_blur", kernel_3);
GaussianBlur(origin_img, blur, Size(5, 5), 0);
Laplacian(blur, kernel_5, CV_64F, 5);
imshow("kernel_5_blur", kernel_5);
convertScaleAbs(kernel_5, kernel_5);
imshow("kernel_5_abs_blur", kernel_5);
return EXIT_SUCCESS;
}
实现代码:python
# laplacian
def laplacian(origin_img):
titles = ["origin_image"]
images = [origin_img]
kernel_1 = cv.Laplacian(origin_img, cv.CV_64F, None, 1)
titles.append("kernel_1")
images.append(kernel_1)
kernel_1_abs = cv.convertScaleAbs(kernel_1)
titles.append("kernel_1_abs")
images.append(kernel_1_abs)
kernel_3 = cv.Laplacian(origin_img, cv.CV_64F, None, 3)
titles.append("kernel_3")
images.append(kernel_3)
kernel_3_abs = cv.convertScaleAbs(kernel_3)
titles.append("kernel_3_abs")
images.append(kernel_3_abs)
kernel_5 = cv.Laplacian(origin_img, cv.CV_64F, None, 5)
titles.append("kernel_5")
images.append(kernel_5)
kernel_5_abs = cv.convertScaleAbs(kernel_5)
titles.append("kernel_5_abs")
images.append(kernel_5_abs)
for i in range(7):
plt.subplot(2, 4, i + 1)
plt.imshow(images[i], 'gray', vmin=0, vmax=255)
plt.title(titles[i])
plt.xticks(), plt.yticks([])
plt.show()
图像经laplacian算子计算后的梯度如图008-11所示:
从图008-11可以看出kernel size越大,图像边缘越明显。但边缘处存在大量的噪声。为消除噪声,对图像进行滤波处理。
# laplacian
def laplacian_after_blur(origin_img):
titles = ["origin_image"]
images = [origin_img]
blur_image = cv.GaussianBlur(origin_img, (1, 1), 0)
# titles.append("kernel_1_blur")
# images.append(blur_image)
kernel_1 = cv.Laplacian(blur_image, cv.CV_64F, None, 1)
titles.append("kernel_1")
images.append(kernel_1)
kernel_1_abs = cv.convertScaleAbs(kernel_1)
titles.append("kernel_1_abs")
images.append(kernel_1_abs)
blur_image = cv.GaussianBlur(origin_img, (3, 3), 0)
# titles.append("kernel_3_blur")
# images.append(blur_image)
kernel_3 = cv.Laplacian(blur_image, cv.CV_64F, None, 3)
titles.append("kernel_3")
images.append(kernel_3)
kernel_3_abs = cv.convertScaleAbs(kernel_3)
titles.append("kernel_3_abs")
images.append(kernel_3_abs)
blur_image = cv.GaussianBlur(origin_img, (5, 5), 0)
# titles.append("kernel_5_blur")
# images.append(blur_image)
kernel_5 = cv.Laplacian(origin_img, cv.CV_64F, None, 5)
titles.append("kernel_5")
images.append(kernel_5)
kernel_5_abs = cv.convertScaleAbs(kernel_5)
titles.append("kernel_5_abs")
images.append(kernel_5_abs)
for i in range(7):
plt.subplot(3, 3, i + 1)
plt.imshow(images[i], 'gray', vmin=0, vmax=255)
plt.title(titles[i])
plt.xticks(), plt.yticks([])
plt.show()
从滤波后再使用laplacian计算梯度得到的效果来看,滤波后得到的图像边缘更平滑,噪点更少。
参考文献:
-
https://docs.opencv.org/4.6.0/d5/d0f/tutorial_py_gradients.html
-
https://handwiki.org/wiki/Image_gradient