Python实现边缘提取的示例代码
作者:小嵌同学
复习
(1)梯度: 梯度的本意是一个向量(矢量),表示某一函数在该点处的方向导数沿着该方向取得最大值,即函数在该点处沿着该方向(此梯度的方向)变化最快,变化率最大(为该梯度的模)
(2)线性滤波 可以说是 图像处理 最基本的方法,它可以允许我们对图像进行处理,产生很多不同的效果
一、边缘提取
1、什么是边缘
图象的边缘是指图象局部区域亮度变化显著的部分,该区域的灰度剖面一般可以看作是一个阶跃,一个灰度值在很小的缓冲区域内急剧变化到另一个灰度相差较大的灰度值。
边缘有正负之分,就像导数有正值也有负值一样:由暗到亮为正,由亮到暗为负.
求边缘幅度的算法: sobel、 Roberts、 prewitt、 Laplacian、 Canny算子,Canny算子效果比其他的都要好,但是实现起来有点麻烦
2、什么是边缘提取
边缘提取,指数字图像处理中,对于图片轮廓的一个处理。对于边界处,灰度值变化比较剧烈的地方,就定义为边缘。也就是拐点,拐点是指函数发生凹凸性变化的点。二阶导数为零的地方。并不是一阶导数,因为一阶导数为零,表示是极值点。
边缘提取:边缘检测的基本思想首先是利用边缘增强算子,突出图像中的局部边缘,然后定义象素的“边缘强度”,通过设置阈值的方法提取边缘点集。由于噪声和模糊的存在,监测到的边界可能会变宽或在某点处发生间断。因此,边界检测包括两个基本内容:
(1)用边缘算子提取出反映灰度变化的边缘点集。
(2)在边缘点集合中剔除某些边界点或填补边界间断点,并将这些边缘连接成完整的线。
边缘定义:图像灰度变化率最大的地方(图像灰度值变化最剧烈的地方)。图像灰度在表面法向变化的不连续造成的边缘。一般认为边缘提取是要保留图像的灰度变化剧烈的区域,这从数学上看,最直观的方法就是微分(对于数字图像来说就是差分),在信号处理的角度来看,也可以说是用高通滤波器,即保留高频信号。
边缘信息包含两个方面:
1.像素的坐标
2.边缘的方向
(1)边缘检测
边缘检测 主要 是图象的灰度变化的度量、检测和定位。下边两幅图展现出了边缘检测的效果。
边缘检测的应用:语义分割和实例分割
语义分割:
实例分割:
(2)高频信号&低频信号
图像中的低频信号和高频信号也叫做低频分量和高频分量。
简单一点说,图像中的高频分量,指的是图像强度(亮度/灰度)变化剧烈的地方,也就是边缘(轮廓);
图像中的低频分量,指的是图像强度(亮度/灰度)变换平缓的地方,也就是大片色块的地方。
人眼对图像中的高频信号更为敏感。
(3)边缘检测的原理和步骤
1)滤波:边缘检测的算法主要是基于图像强度的一阶和二阶导数,但导数通常对噪声很敏感,因此必须采用滤波器来改善与噪声有关的边缘检测器的性能。常见的滤波方法主要有高斯滤波。
2)增强:增强边缘的基础是确定图像各点邻域强度的变化值。增强算法可以将图像灰度点邻域强度值有显著变化的点凸显出来。在具体编程实现时,可通过计算梯度幅值来确定。
3)检测:经过增强的图像,往往邻域中有很多点的梯度值比较大,而在特定的应用中,这些点并不是我们要找的边缘点,所以应该采用某种方法来对这些点进行取舍。实际工程中,常用的方法是通过阈值化方法来检测。
检测原理:关于边缘检测的基础来自于一个事实, 即在边缘部分,像素值出现”跳跃“或者较大的变化。 如果在此边缘部分求取一阶导数,就会看到极值的出现。而在一阶导数为极值的地方,二阶导数为0,基于这个原理,就可以进行边缘检测。
在边缘部分求取一阶导数,你会看到极值的出现:
如果在边缘部分求二阶导数会出现什么情况?
从上例中我们可以推论检测边缘可以通过定位梯度值大于邻域的相素的方法找到(或者推广到大于一个阀值).从以上分析中,我们推论二阶导数可以用来检测边缘 。因为图像是 “2维”, 我们需要在两个方向求导。
(4)图像锐化
图像锐化(image sharpening)是补偿图像的轮廓,增强图像的边缘及灰度跳变的部分,使图像变得清晰。
图像锐化是为了突出图像上的物的边缘、轮廓,或某些线性目标要素的特征。这种滤波方法提高了地物边缘与周围像元之间的反差,因此也被称为边缘增强。
图像锐化常使用的是拉普拉斯变换核函数:
图中右边的模板它是根据上下左右四个90度方向的像素值来计算二阶导的。如果想要加入斜对角上的像素来计算,可使用左边的模板。
(5)图像平滑
图像平滑是指用于突出图像的宽大区域、低频成分、主干部分或抑制图像噪声和干扰高频成分的图像处理方法,目的是使图像亮度平缓渐变,减小突变梯度,改善图像质量。
用Gx来卷积下面这张图的话,就会在中间黑白边界获得比较大的值。
二、Sobel算子
Sobel算子是典型的基于一阶导数的边缘检测算子,由于该算子中引入了类似局部平均的运算,因此对噪声具有平滑作用,能很好的消除噪声的影响。Sobel算子对于像素的位置的影响做了加权,因此与Prewitt算子相比效果更好。
Sobel算子包含两组3x3的矩阵,分别为横向及纵向模板,将之与图像作平面卷积,即可分别得出横向及纵向的亮度差分近似值。实际使用中,常用如下两个模板来检测图像边缘
缺点是Sobel算子并没有将图像的主题与背景严格地区分开来,换言之就是Sobel算子并没有基于图像灰度进行处理,由于Sobel算子并没有严格地模拟人的视觉生理特征,所以提取的图像轮廓有时并不能令人满意。
噪声和边缘都是灰度变化剧烈的地方,所以本质相同,滤波不能过度,否则边缘就减损了
Prewitt算子是一种一阶微分算子的边缘检测,利用像素点上下、左右邻点的灰度差,在边缘处达到极值检测边缘,去掉部分伪边缘,对噪声具有平滑作用 。其原理是在图像空间利用两个方向模板与图像进行邻域卷积来完成的,这两个方向模板一个检测水平边缘,一个检测垂直边缘。其原理与sobel算子一样
三、Canny边缘检测算法
Canny是目前最优秀的边缘检测算法( 在传统的人工智能中不包括深度学习 ),其目标为找到一个最优的边缘,其最优边缘的定义为:
1、好的检测:算法能够尽可能的标出图像中的实际边缘
2、好的定位:标识出的边缘要与实际图像中的边缘尽可能接近
3、最小响应:图像中的边缘只能标记一次
1、算法步骤
1. 对图像进行灰度化
2. 对图像进行高斯滤波:
根据待滤波的像素点及其邻域点的灰度值按照一定的参数规则进行加权平均。这样
可以有效滤去理想图像中叠加的高频噪声。
3. 检测图像中的水平、垂直和对角边缘(如Prewitt, Sobel算子等)。
4. 对梯度幅值进行非极大值抑制
5. 用双阈值算法检测和连接边缘
2、高斯平滑
高斯平滑水平和垂直方向呈现高斯分布,更突出了中心点在像素平滑后的权重,相比于均值滤波而言,有着更好的平滑效果。
重要的是需要理解, 高斯卷积核大小的选择将影响Canny检测器的性能:尺寸越大,检测器对噪声的敏感度越低,但是边缘检测的定位误差也将略有增加。 一般5x5是一个比较不错的trade off
3、非极大值抑制
非极大值抑制,简称为NMS算法,英文为Non-Maximum Suppression。其思想是搜素局部最大值,抑制非极大值。NMS算法在不同应用中的具体实现不太一样,但思想是一样的。
为什么要采用非极大值抑制?以目标检测为例:目标检测的过程中在同一目标的位置上会产生大量的候选框,这些候选框相互之间可能会有重叠,此时我们需要利用非极大值抑制找到最佳的目标边界框,消除冗余的边界框。
对于重叠的候选框,计算他们的重叠部分,若大于规定阈值,则删除;低于阈值则保留。对于无重叠的候选框,都保留。
非极大值抑制:通俗意义上是指寻找像素点局部最大值,将非极大值点所对应的灰度值置为0,这样可以剔除掉一大部分非边缘的点。
1) 将当前像素的梯度强度与沿正负梯度方向上的两个像素进行比较。
2) 如果当前像素的梯度强度与另外两个像素相比最大,则该像素点保留为边缘点,否则该像素点将被抑制(灰度值置为0)。
dTmp1和dTmp2是两个虚拟像素,也有称亚像素点,其由像素点通过双线性插值算法求得的。
4、双阈值检测
用双阈值算法检测(滞后阈值):完成非极大值抑制后,会得到一个二值图像,非边缘的点灰度值均为0,可能为边缘的局部灰度极大值点可设置其灰度为128(或其他)。这样一个检测结果还是包含了很多由噪声及其他原因造成的假边缘。因此还需要进一步的处理。
• 如果边缘像素的梯度值高于高阈值,则将其标记为强边缘像素;
• 如果边缘像素的梯度值小于高阈值并且大于低阈值,则将其标记为弱边缘像素;
• 如果边缘像素的梯度值小于低阈值,则会被抑制。
大于高阈值为强边缘,小于低阈值不是边缘。介于中间是弱边缘。阈值的选择取决于给定输入图像的内容,也无法指定确定的值,只能去调试得到效果比较好的阈值。
抑制孤立低阈值点:到目前为止,被划分为强边缘的像素点已经被确定为边缘,因为它们是从图像中的真实边缘中提取出来的。然而,对于弱边缘像素,将会有一些争论,因为这些像素可以从真实边缘提取也可以是因噪声或颜色变化引起的。
为了获得准确的结果,应该抑制由后者引起的弱边缘:
• 通常,由真实边缘引起的弱边缘像素将连接到强边缘像素,而噪声响应未连接。
• 为了跟踪边缘连接,通过查看弱边缘像素及其8个邻域像素,只要其中一个为强边缘像素,则该弱边缘点就可以保留为真实的边缘。
四、相关代码
1、canny_detail.py
import numpy as np import matplotlib.pyplot as plt import math if __name__ == '__main__': pic_path = 'lenna.png' img = plt.imread(pic_path) if pic_path[-4:] == '.png': # .png图片在这里的存储格式是0到1的浮点数,所以要扩展到255再计算 img = img * 255 # 还是浮点数类型 img = img.mean(axis=-1) # 取均值就是灰度化了 # 1、高斯平滑 #sigma = 1.52 # 高斯平滑时的高斯核参数,标准差,可调 sigma = 0.5 # 高斯平滑时的高斯核参数,标准差,可调 dim = int(np.round(6 * sigma + 1)) # round是四舍五入函数,根据标准差求高斯核是几乘几的,也就是维度 if dim % 2 == 0: # 最好是奇数,不是的话加一 dim += 1 Gaussian_filter = np.zeros([dim, dim]) # 存储高斯核,这是数组不是列表了 tmp = [i-dim//2 for i in range(dim)] # 生成一个序列 n1 = 1/(2*math.pi*sigma**2) # 计算高斯核 n2 = -1/(2*sigma**2) for i in range(dim): for j in range(dim): Gaussian_filter[i, j] = n1*math.exp(n2*(tmp[i]**2+tmp[j]**2)) Gaussian_filter = Gaussian_filter / Gaussian_filter.sum() dx, dy = img.shape img_new = np.zeros(img.shape) # 存储平滑之后的图像,zeros函数得到的是浮点型数据 tmp = dim//2 img_pad = np.pad(img, ((tmp, tmp), (tmp, tmp)), 'constant') # 边缘填补 for i in range(dx): for j in range(dy): img_new[i, j] = np.sum(img_pad[i:i+dim, j:j+dim]*Gaussian_filter) plt.figure(1) plt.imshow(img_new.astype(np.uint8), cmap='gray') # 此时的img_new是255的浮点型数据,强制类型转换才可以,gray灰阶 plt.axis('off') # 2、求梯度。以下两个是滤波求梯度用的sobel矩阵(检测图像中的水平、垂直和对角边缘) sobel_kernel_x = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]]) sobel_kernel_y = np.array([[1, 2, 1], [0, 0, 0], [-1, -2, -1]]) img_tidu_x = np.zeros(img_new.shape) # 存储梯度图像 img_tidu_y = np.zeros([dx, dy]) img_tidu = np.zeros(img_new.shape) img_pad = np.pad(img_new, ((1, 1), (1, 1)), 'constant') # 边缘填补,根据上面矩阵结构所以写1 for i in range(dx): for j in range(dy): img_tidu_x[i, j] = np.sum(img_pad[i:i+3, j:j+3]*sobel_kernel_x) # x方向 img_tidu_y[i, j] = np.sum(img_pad[i:i+3, j:j+3]*sobel_kernel_y) # y方向 img_tidu[i, j] = np.sqrt(img_tidu_x[i, j]**2 + img_tidu_y[i, j]**2) img_tidu_x[img_tidu_x == 0] = 0.00000001 angle = img_tidu_y/img_tidu_x plt.figure(2) plt.imshow(img_tidu.astype(np.uint8), cmap='gray') plt.axis('off') # 3、非极大值抑制 img_yizhi = np.zeros(img_tidu.shape) for i in range(1, dx-1): for j in range(1, dy-1): flag = True # 在8邻域内是否要抹去做个标记 temp = img_tidu[i-1:i+2, j-1:j+2] # 梯度幅值的8邻域矩阵 if angle[i, j] <= -1: # 使用线性插值法判断抑制与否 num_1 = (temp[0, 1] - temp[0, 0]) / angle[i, j] + temp[0, 1] num_2 = (temp[2, 1] - temp[2, 2]) / angle[i, j] + temp[2, 1] if not (img_tidu[i, j] > num_1 and img_tidu[i, j] > num_2): flag = False elif angle[i, j] >= 1: num_1 = (temp[0, 2] - temp[0, 1]) / angle[i, j] + temp[0, 1] num_2 = (temp[2, 0] - temp[2, 1]) / angle[i, j] + temp[2, 1] if not (img_tidu[i, j] > num_1 and img_tidu[i, j] > num_2): flag = False elif angle[i, j] > 0: num_1 = (temp[0, 2] - temp[1, 2]) * angle[i, j] + temp[1, 2] num_2 = (temp[2, 0] - temp[1, 0]) * angle[i, j] + temp[1, 0] if not (img_tidu[i, j] > num_1 and img_tidu[i, j] > num_2): flag = False elif angle[i, j] < 0: num_1 = (temp[1, 0] - temp[0, 0]) * angle[i, j] + temp[1, 0] num_2 = (temp[1, 2] - temp[2, 2]) * angle[i, j] + temp[1, 2] if not (img_tidu[i, j] > num_1 and img_tidu[i, j] > num_2): flag = False if flag: img_yizhi[i, j] = img_tidu[i, j] plt.figure(3) plt.imshow(img_yizhi.astype(np.uint8), cmap='gray') plt.axis('off') # 4、双阈值检测,连接边缘。遍历所有一定是边的点,查看8邻域是否存在有可能是边的点,进栈 lower_boundary = img_tidu.mean() * 0.5 high_boundary = lower_boundary * 3 # 这里我设置高阈值是低阈值的三倍 zhan = [] for i in range(1, img_yizhi.shape[0]-1): # 外圈不考虑了 for j in range(1, img_yizhi.shape[1]-1): if img_yizhi[i, j] >= high_boundary: # 取,一定是边的点 img_yizhi[i, j] = 255 zhan.append([i, j]) elif img_yizhi[i, j] <= lower_boundary: # 舍 img_yizhi[i, j] = 0 while not len(zhan) == 0: temp_1, temp_2 = zhan.pop() # 出栈 a = img_yizhi[temp_1-1:temp_1+2, temp_2-1:temp_2+2] if (a[0, 0] < high_boundary) and (a[0, 0] > lower_boundary): img_yizhi[temp_1-1, temp_2-1] = 255 # 这个像素点标记为边缘 zhan.append([temp_1-1, temp_2-1]) # 进栈 if (a[0, 1] < high_boundary) and (a[0, 1] > lower_boundary): img_yizhi[temp_1 - 1, temp_2] = 255 zhan.append([temp_1 - 1, temp_2]) if (a[0, 2] < high_boundary) and (a[0, 2] > lower_boundary): img_yizhi[temp_1 - 1, temp_2 + 1] = 255 zhan.append([temp_1 - 1, temp_2 + 1]) if (a[1, 0] < high_boundary) and (a[1, 0] > lower_boundary): img_yizhi[temp_1, temp_2 - 1] = 255 zhan.append([temp_1, temp_2 - 1]) if (a[1, 2] < high_boundary) and (a[1, 2] > lower_boundary): img_yizhi[temp_1, temp_2 + 1] = 255 zhan.append([temp_1, temp_2 + 1]) if (a[2, 0] < high_boundary) and (a[2, 0] > lower_boundary): img_yizhi[temp_1 + 1, temp_2 - 1] = 255 zhan.append([temp_1 + 1, temp_2 - 1]) if (a[2, 1] < high_boundary) and (a[2, 1] > lower_boundary): img_yizhi[temp_1 + 1, temp_2] = 255 zhan.append([temp_1 + 1, temp_2]) if (a[2, 2] < high_boundary) and (a[2, 2] > lower_boundary): img_yizhi[temp_1 + 1, temp_2 + 1] = 255 zhan.append([temp_1 + 1, temp_2 + 1]) for i in range(img_yizhi.shape[0]): for j in range(img_yizhi.shape[1]): if img_yizhi[i, j] != 0 and img_yizhi[i, j] != 255: img_yizhi[i, j] = 0 # 绘图 plt.figure(4) plt.imshow(img_yizhi.astype(np.uint8), cmap='gray') plt.axis('off') # 关闭坐标刻度值 plt.show()
2、借助opencv库实现的:canny.py
#!/usr/bin/env python # encoding=gbk import cv2 import numpy as np ''' cv2.Canny(image, threshold1, threshold2[, edges[, apertureSize[, L2gradient ]]]) 必要参数: 第一个参数是需要处理的原图像,该图像必须为单通道的灰度图; 第二个参数是滞后阈值1; 第三个参数是滞后阈值2。 ''' img = cv2.imread("lenna.png", 1) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) cv2.imshow("canny", cv2.Canny(gray, 200, 300)) cv2.waitKey() cv2.destroyAllWindows()
3、优化后的程序:canny_track.py
#!/usr/bin/env python # encoding=gbk ''' Canny边缘检测:优化的程序 ''' import cv2 import numpy as np def CannyThreshold(lowThreshold): detected_edges = cv2.GaussianBlur(gray,(3,3),0) #高斯滤波 detected_edges = cv2.Canny(detected_edges, lowThreshold, lowThreshold*ratio, apertureSize = kernel_size) #边缘检测 # just add some colours to edges from original image. dst = cv2.bitwise_and(img,img,mask = detected_edges) #用原始颜色添加到检测的边缘上 cv2.imshow('canny demo',dst) lowThreshold = 0 max_lowThreshold = 100 ratio = 3 kernel_size = 3 img = cv2.imread('lenna.png') gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY) #转换彩色图像为灰度图 cv2.namedWindow('canny demo') #设置调节杠, ''' 下面是第二个函数,cv2.createTrackbar() 共有5个参数,其实这五个参数看变量名就大概能知道是什么意思了 第一个参数,是这个trackbar对象的名字 第二个参数,是这个trackbar对象所在面板的名字 第三个参数,是这个trackbar的默认值,也是调节的对象 第四个参数,是这个trackbar上调节的范围(0~count) 第五个参数,是调节trackbar时调用的回调函数名 ''' cv2.createTrackbar('Min threshold','canny demo',lowThreshold, max_lowThreshold, CannyThreshold) CannyThreshold(0) # initialization if cv2.waitKey(0) == 27: #wait for ESC key to exit cv2 cv2.destroyAllWindows()
到此这篇关于Python实现边缘提取的示例代码的文章就介绍到这了,更多相关Python边缘提取内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!