图像处理-008图像梯度
图像可以用平面空间的二维函数f(x,y)来表示,x,y表示空间平面的坐标。图像梯度则是度量图像亮度在x,y方向上的变化程度。梯度提供了两部分信息:一是梯度值的大小展示了图像亮度的变化快慢程度,二是梯度提供了亮度变化的方向。
为便于解释,可用地形来类比。将图像视为地形,图像中的亮度值看做地形中的高度,则地形中任意点的坡度方向为图像的梯度方向,沿坡度前进时地形高度的变化程度则为图像梯度的变化程度。
鉴于梯度具有方向、大小,则用向量来表示梯度。向量的方向即为梯度的方向,向量的长度为梯度的大小。梯度的方向是函数f(x,y)变化最快的方向,沿着梯度的方向容易找到最大值。
从数学的角度来讲,梯度是图像亮度在x,y方向上求导。
其中:表示x方向导数,即x方向的梯度,表示y方向的导数,即y方向的梯度。
坐标(x,y)梯度方向由公式008-2计算
坐标(x,y)梯度大小由公式008-3计算
公式008-3计算时计算量比较大,为提升计算效率,采用公式008-4来计算点(x,y)处的梯度。
梯度计算时,源图像与大小为ksize*ksize的kernel做卷积运算,卷积时,卷积核分水平和垂直两个方向。卷积核有如下特点:
- 卷积核以奇数型矩阵的形式存在,奇数一般为3,5,7;
- 卷积核中同行(行梯度)或同列(列梯度)的系数以互为相反数的形式对称分布;
- 卷积核中各系数和为0;
- 卷积核中系数大小及排班顺序决定了对图像区域处理的类型。
图008-1 图像卷积运算
图像中P5位置由P5邻域元素与卷积核系数乘积和来确定,P5值越大则表示P5与周围邻域元素差异越大。
若图像中某像素点的像素值与其邻域的像素值差异大,则该点是边缘的可能性越大,因此,梯度被用做图像边缘检测。
计算像素点(x,y)水平方向梯度时
图008-2 计算水平方向梯度
计算像素点(x,y)垂直方向梯度时
图008-3 计算垂直方向梯度
从公式008-2, 008-3可以得出:水平方向梯度计算后得到垂直方向边界,垂直方向梯度计算后得到水平方向梯度。
opencv中提供了 cv.Sobel(), cv.Scharr(), cv.Laplacian()算子来处理图像梯度。
Prewitt
prewitt是一种一阶微分算子的边缘检测,它依据其四邻域中上下、左右点灰度像素差值来计算,在边缘处差值达到最大。prewitt提供两种梯度算子:
沿x轴方向的水平梯度算子;
沿y轴方向的垂直梯度算子。
图008-4 prewitt垂直(y轴)kernel(左)和水平(x轴)kernel(右)
Soble
soble结合高斯模糊与微分求导,利用局部差分寻找边缘,所得的值是梯度的近似值,与prewitt相比,sobel强调与边缘相邻的像素点对边缘的影响。
同prewitt算子一样,sobel算子也按x,y轴分水平梯度和垂直梯度。
图008-5 sobel垂直(y轴)kernel(左)和水平(x轴)kernel(右)
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所示:
图008-7 sobel算子梯度效果
Scharr
scharr与sobel一样用来计算(x,y)处的梯度,与sobel相比仅kernel存在差异。scharr算子如图008-8所示:
图008-8 scharr水平(x轴)kernel(左)和(y轴)kernel(右)垂直
从图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
图008-9 scharr算子计算后的图像边界
Laplacian
sobel与scharr算子都具有方向性,需要分别计算x轴,y轴方向的梯度后,再整合计算出点(x,y)处的梯度。laplacian算子定义如公式008-8所示:
laplacian算子卷积核如图008-10所示:
图008-10 laplacian算子kernel
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 laplacian算子计算图像梯度效果
从图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()
图008-12 高斯滤波后laplacian算子计算图像梯度效果
从滤波后再使用laplacian计算梯度得到的效果来看,滤波后得到的图像边缘更平滑,噪点更少。
参考文献:
- https://docs.opencv.org/4.6.0/d5/d0f/tutorial_py_gradients.html
- https://handwiki.org/wiki/Image_gradient