边缘检测是图像处理与计算机视觉中的重要技术之一。其目的是检测识别出图像中亮度变化剧烈的像素点构成的集合。图像边缘的正确检测对于分析图像中的内容、实现图像中物体的分割、定位等具有重要的作用。边缘检测大大减少了源图像的数据量,剔除了与目标不相干的信息,保留了图像重要的结构属性。
图像的边缘指的是图像中像素灰度值突然发生变化的区域,如果将图像的每一行像素和每一列像素都描述成一个关于灰度值的函数,那么图像的边缘对应在灰度值函数中是函数值突然变大的区域。函数值的变化趋势可以用函数的导数描述。当函数值突然变大时,导数也必然会变大,而函数值变化较为平缓区域,导数值也比较小,因此可以通过寻找导数值较大的区域去寻找函数中突然变化的区域,进而确定图像中的边缘位置。
边缘检测的方法大致可分为两类:基于搜索和基于零交叉。
基于搜索的边缘检测方法:首先计算边缘强度,通常用一阶导数表示,例如梯度模,然后,计算估计边缘的局部方向,通常采用梯度的方向,并利用此方向找到局部梯度模的最大值。
基于零交叉的边缘检测方法:找到由图像得到的二阶导数的零交叉点来定位边缘,通常用拉普拉斯算子或非线性微分方程的零交叉点。
滤波作为边缘检测的预处理通常是必要的,通常采用高斯滤波。
一阶微分为基础的边缘检测,通过计算图像的梯度值来检测图像的边缘,如Roberts算子、Prewitt算子和Sobel算子等。
Roberts算子是一种最简单的算子,它利用局部差分算子寻找边缘。采用对角线相邻两像素之差近似梯度幅值检测边缘,检测垂直边缘的效果比斜向边缘要好,定位精度高,但对噪声比较敏感,无法抑制噪声的影响。
Roberts算子是一个2x2的模板,采用的是对角方向相邻的两个像素之差,如下的2个卷积核形成了Roberts算子,图像中的每一个点都用这2个核做卷积:
若对于输入图像f(x,y),使用Roberts算子后输出的目标图像为g(x,y),则
在Python中,Roberts算子主要是通过Numpy定义模板,再调用OpenCV的filter2D()函数实现边缘提取。该函数主要是利用内核实现对图像的卷积运算,其函数原型如下:
dst = filter2D(src, ddepth, kernel, dts, anchor,delta, borderType)
参数说明:
src:表示输入图像;
ddepth: 表示目标图像所需的深度;
kernel: 表示卷积核,一个单通道浮点型矩阵;
anchor: 表示内核的基准点,其默认值为(-1, -1),位于中心位置;
delta:表示在存储目标图像前可选的添加到像素的值,默认值为0;
borderType:表示边框模式。
实验代码如下:
def Roberts(srcImg_path):
img = cv2.imread(srcImg_path)
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
grayImage = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Roberts算子
kernelx = np.array([[1, 0], [0, -1]], dtype=int)
kernely = np.array([[0, -1], [1, 0]], dtype=int)
x = cv2.filter2D(grayImage, cv2.CV_16S, kernelx)
y = cv2.filter2D(grayImage, cv2.CV_16S, kernely)
# 转成uint8
absX = cv2.convertScaleAbs(x)
absY = cv2.convertScaleAbs(y)
Roberts = cv2.addWeighted(absX, 0.5, absY, 0.5, 0)
# 显示图形
titles = ["Original Image", "Roberts Image"]
images = [img, Roberts]
for i in range(2):
plt.subplot(1, 2, i+1)
plt.imshow(images[i], "gray")
plt.title(titles[i])
plt.axis('off')
plt.show()
效果如下:
Prewitt是一种图像边缘检测的微分算子,其原理是利用特定区域内像素值产生的差分实现边缘检测。由于Prewitt算子采用3x3模板对区域内的像素值进行计算,而Roberts算子的模板为2x2,故Prewitt算子的边缘检测结果在水平和垂直方向均比Roberts算子更加明显。Prewitt算子适合用来识别噪声较多,灰度渐变的图像。
Prewitt算子卷积核如下:
实验代码如下:
def Prewitt(srcImg_path):
img = cv2.imread(srcImg_path)
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
grayImage = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Prewitt算子
kernelx = np.array([[1, 1, 1], [0, 0, 0], [-1, -1, -1]], dtype=int)
kernely = np.array([[-1, 0, 1], [-1, 0, 1], [-1, 0, 1]], dtype=int)
x = cv2.filter2D(grayImage, cv2.CV_16S, kernelx)
y = cv2.filter2D(grayImage, cv2.CV_16S, kernely)
# 转成uint8
absX = cv2.convertScaleAbs(x)
absY = cv2.convertScaleAbs(y)
Prewitt = cv2.addWeighted(absX, 0.5, absY, 0.5, 0)
# 显示图形
titles = ["Original Image", "Prewitt Image"]
images = [img, Prewitt]
for i in range(2):
plt.subplot(1, 2, i+1)
plt.imshow(images[i], "gray")
plt.title(titles[i])
plt.axis('off')
plt.show()
效果如下:
在边缘检测中,常用的一种模板是Sobel算子。Sobel算子有两个卷积核,一个是检测水平边缘的;另一个是检测垂直边缘的。与Prewitt算子相比,Sobel算子对于像素的位置的影响做了加权,可以降低边缘模糊程度,因此效果更好。
Sobel算子卷积核如下:
在opencv-python中定义了Sobel算子,其函数原型如下:
dst = Sobel(src, ddepth, dx, dy, dst,ksize, scale, delta, borderType)
参数说明:
src:表示输入图像;
dst:表示输出的边缘图,其大小和通道数与输入图像相同;
ddepth:表示目标图像所需的深度,针对不同的输入图像,输出目标图像有不同的深度;
dx:表示x方向上的差分阶数,取值1或0;
dy:表示y方向上的差分阶数,取值1或0;
ksize:表示Sobel算子的大小,其值必须是正数和奇数;
scale:表示缩放导数的比例常数,默认情况下没有伸缩系数;
delta:表示将结果存入目标图像之前,添加到结果中的可选增量值。
实验代码如下:
def Sobel_demo(srcImg_path):
img = cv2.imread(srcImg_path)
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
grayImage = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Sobel算子
x = cv2.Sobel(grayImage, cv2.CV_16S, 1, 0)
y = cv2.Sobel(grayImage, cv2.CV_16S, 0, 1)
# 转成uint8
absX = cv2.convertScaleAbs(x)
absY = cv2.convertScaleAbs(y)
Sobel = cv2.addWeighted(absX, 0.5, absY, 0.5, 0)
# 显示图形
titles = ["Original Image", "Sobel Image"]
images = [img, Sobel]
for i in range(2):
plt.subplot(1, 2, i+1)
plt.imshow(images[i], "gray")
plt.title(titles[i])
plt.axis('off')
plt.show()
效果如下:
二阶微分为基础的边缘检测,通过寻求二阶导数中的过零点来检测边缘,如Laplacian算子和Canny算子等。
Laplacian算子是n维欧几里德空间中的一个二阶微分算子,常用于图像增强和边缘提取。它通过灰度差分计算邻域内的像素,基本流程是:判断图像中心像素灰度值与它周围其他像素的灰度值,如果中心像素的灰度更高,则提升中心像素的灰度;反之降低中心像素的灰度,从而实现图像锐化操作。在算法实现过程中,Laplacian算子通过对邻域中心像素的四方向或八方向求梯度,再将梯度相加起来判断中心像素灰度与邻域内其他像素灰度的关系,最后通过梯度运算的结果对像素灰度进行调整。
在opencv-python中,Laplacian算子封装在Laplacian()函数中,其函数原型如下:
dst = Laplacian(src, ddepth[, dst[, ksize[, scale[, delta[, borderType]]]]])
参数说明:
src:表示输入图像;
dst:表示输出的边缘图,其大小和通道数与输入图像相同;
ddepth:表示目标图像所需的深度;
ksize:表示用于计算二阶导数的滤波器的孔径大小,其值必须是正数和奇数,且默认值为1;
scale:表示计算拉普拉斯算子值的可选比例因子,默认值为1;
delta:表示将结果存入目标图像之前,添加到结果中的可选增量值,默认值为0;
borderType:表示边框模式。
当ksize=1时,Laplacian()函数采用3x3模板(四邻域)进行变换处理。下面的实验代码是采用ksize=3的Laplacian算子进行图像锐化处理:
def Laplacian_demo(srcImg_path):
img = cv2.imread(srcImg_path)
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
grayImage = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Laplacian算子
Laplacian = cv2.Laplacian(grayImage, cv2.CV_16S, ksize=3)
# 转成uint8
Laplacian = cv2.convertScaleAbs(Laplacian)
# 显示图形
titles = ["Original Image", "Laplacian Image"]
images = [img, Laplacian]
for i in range(2):
plt.subplot(1, 2, i+1)
plt.imshow(images[i], "gray")
plt.title(titles[i])
plt.axis('off')
plt.show()
效果如下:
Canny算子由John F. Canny在1986年提出,由于它出色的检测和容错能力,至今一直被广泛使用。Canny边缘检测具有以下特点:
(1)较低的错误率 - 只有真实存在的边缘才会被检测到。
(2)较好的边缘定位 - 检测出来的结果和图像中真实的边缘在距离上的误差很小。
(3)没有重复的检测 - 对于每一条边缘,只会返回一个与之对应的结果。
Canny算子的计算步骤大概分成以下几步:
在Python Opencv接口中,提供了Canny函数,其函数原型如下:
canny = cv2.Canny(image,threshold1,threshold2)
参数说明:
image:灰度图;
threshold1:minval,较小的阈值将间断的边缘连接起来;
threshold2:maxval,较大的阈值检测图像中明显的边缘。
实验代码如下:
def Canny_demo(srcImg_path):
img = cv2.imread(srcImg_path)
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 高斯滤波
img_GaussianBlur = cv2.GaussianBlur(gray, (3,3), 0)
# Canny算子
Canny = cv2.Canny(img_GaussianBlur, 0, 100)
# 显示图形
titles = ["Original Image", "Canny Image"]
images = [img, Canny]
for i in range(2):
plt.subplot(1, 2, i+1)
plt.imshow(images[i], "gray")
plt.title(titles[i])
plt.axis('off')
plt.show()
效果如下: