图像形态学:膨胀、腐蚀和边缘检测与绘制
图形膨胀和腐蚀
腐蚀
腐蚀操作会使前景物体的边界向内收缩
算法简述
用一个结构元素(通常是一个小的矩形或圆形核)在图像上滑动。只有当结构元素完全覆盖前景像素时,中心像素才保留,否则就被腐蚀(变为背景)。腐蚀操作的过程和膨胀类似,其原理可以看成最小值滤波,也就是用结构元素覆盖原图像范围内的最小值替换锚点位置的像素值。对于腐蚀来说,边界补充的时候补充的是较大值,在下面的例子中也就是1:
api接口
api接口有三个参数
第一个是要处理的图像
第二个是腐蚀核的大小
第三个是腐蚀操作次数
代码展示
腐蚀和膨胀
tu=cv2.imread('taiyang.png')
kernel=np.ones((5,5),np.uint8)
erosion = cv2.erode(tu,kernel,iterations=1)cv2.imshow('tu',tu)
cv2.imshow('erosion',erosion)
cv2.waitKey(0)
效果展示

膨胀
膨胀操作会使前景物体的边界向外扩张。
算法简述
用一个结构元素在图像上滑动。只要结构元素覆盖的区域中有一个像素是前景,中心像素就变成前景(即膨胀)。用结构元素覆盖原图像范围内的最大值替换锚点位置的像素值。对于膨胀来说,边界补充的时候补充的是较小值,在下面的例子中也就是0:
api接口
和腐蚀一样有三个接口
图片 膨胀核大小 膨胀次数
代码展示
tu=cv2.imread('taiyang.png')
kernel=np.ones((5,5),np.uint8)
dilation = cv2.dilate(tu,kernel,iterations=1)cv2.imshow('tu',tu)
cv2.imshow('dilation',dilation)
cv2.waitKey(0)
结果展示
总结
实战例子
膨胀
如果我们有一段断断续续的文字看不清楚,我们可以膨胀一下。
tu=cv2.imread('wenzi.png')kernel=np.ones((3,3),np.uint8)
dilation = cv2.dilate(tu,kernel,iterations=1)cv2.imshow('tu',tu)
cv2.imshow('dilation',dilation)
cv2.waitKey(0)
腐蚀
我们有一个指纹信息,但是有许多噪声点,这个时候我们可以腐蚀一下,然后就可以去噪了。
tu1=cv2.imread('zhiwen.png')
kernel=np.ones((2,2),np.uint8)
erosion = cv2.erode(tu1,kernel,iterations=1)cv2.imshow('tu',tu1)
cv2.imshow('erosion',erosion)
cv2.waitKey(0)
综合
开运算(先腐蚀后膨胀)
对于上面的指纹,我们腐蚀完有些部分断开了,如果我我们想连上,我们可以先腐蚀再膨胀,这样就可以达到了。
tu=cv2.imread('wenzi.png')
tu1=cv2.imread('zhiwen.png')
kernel=np.ones((2,2),np.uint8)
erosion = cv2.erode(tu1,kernel,iterations=1)
dilation = cv2.dilate(erosion,kernel,iterations=1)
cv2.imshow('tu',tu1)
cv2.imshow('erosion',erosion)
cv2.imshow('erosopn+dilation',dilation)
cv2.waitKey(0)
这个有一个集成的函数zhiwen_new = cv2.morphologyEx(zhiwen,cv2.MORPH_OPEN,kernel)
zhiwen=cv2.imread('zhiwen.png')kernel=np.ones((2,2),np.uint8)
zhiwen_new = cv2.morphologyEx(zhiwen,cv2.MORPH_OPEN,kernel)cv2.imshow('tu',zhiwen)
cv2.imshow('new',zhiwen_new)
cv2.waitKey(0)
闭运算(先膨胀后腐蚀)
有些时候我们的指纹刚开始就断断续续,这样我们就要先膨胀后腐蚀了。但效果不是很好。
zhiwen=cv2.imread('zhiwen.png')kernel=np.ones((3,3),np.uint8)
zhiwen_new = cv2.morphologyEx(zhiwen,cv2.MORPH_CLOSE,kernel)cv2.imshow('tu',zhiwen)
cv2.imshow('new',zhiwen_new)
cv2.waitKey(0)
梯度运算(膨胀-腐蚀)
这个就是把原图变细,然后再把原图变粗,减去这个细的就得出一个框了。
tu=cv2.imread('wenzi.png')kernel=np.ones((2,2),np.uint8)
erosion = cv2.erode(tu,kernel,iterations=1)
dilation = cv2.dilate(tu,kernel,iterations=1)
a=dilation-erosion
td=cv2.morphologyEx(tu, cv2.MORPH_GRADIENT, kernel)
cv2.imshow('tu',tu)
cv2.imshow('erosion',erosion)
cv2.imshow('dilation',dilation)
cv2.imshow('GRADIENT',td)
cv2.imshow('a',a)
cv2.waitKey(0)
顶帽和黑帽
顶帽 = 原始图像 - 开运算图像
黑帽 = 闭运算图像 - 原始图像
tu=cv2.imread('taiyang.png')
cv2.imshow('tu',tu)
kernel=np.ones((2,2),np.uint8)open_sun=cv2.morphologyEx(tu,cv2.MORPH_OPEN,kernel)
cv2.imshow('open_sun',open_sun)
toplat=cv2.morphologyEx(tu,cv2.MORPH_TOPHAT,kernel)
cv2.imshow('toplat',toplat)
close_sun=cv2.morphologyEx(tu,cv2.MORPH_CLOSE,kernel)
cv2.imshow('close_sun',close_sun)
blackhat=cv2.morphologyEx(tu,cv2.MORPH_BLACKHAT,kernel)
cv2.imshow('blackhat',blackhat)
erosion = cv2.erode(tu,kernel,iterations=1)
dilation = cv2.dilate(tu,kernel,iterations=1)
cv2.imshow('dilation',dilation)
cv2.imshow('erosion',erosion)
cv2.waitKey(0)
顶帽图像展示
黑帽图像展示
边缘检测
这个有许多算法,先讲Sobel算法。
api接口参数解释
cv2.Sobel(src, ddepth, dx, dy, ksize)
src: 输入图像
ddepth: 输出图像的深度,深度是指输出图像的数据类型,决定了像素值的范围。参数为负数(如-1)表示输出图像与输入图像具有相同的深度。由于Sobel算子计算梯度时会产生负数,所以通常使用浮点数类型(如cv2.CV_64F)来保留负梯度,然后再取绝对值转换回8位无符号整数(如cv2.CV_8U)。这里直接使用-1(与输入相同)可能会截断负值,所以后续常配合 cv2.convertScaleAbs
处理。
dx: x方向导数阶数
dy: y方向导数阶数
ksize: Sobel核大小
这里只列出几个常用的。
y方向
yuan=cv2.imread('yuan.png',0) #这里本身就是黑白的,所以转化灰度图对结果没啥影响,但对彩色图片是有影响的
y1=cv2.Sobel(yuan,-1,dx=0,dy=1,ksize=5)
cv2.imshow('yuan',yuan)
cv2.imshow('yuan1',y1)
cv2.waitKey(0)
这里可以看到为-1的时候只能检测一般,x方向也是只能检测一半,因为上面只能保存正值,负数舍弃了。所以我们后面引入了一个浮点数类型(如cv2.CV_64F)来保留负梯度,然后再配合绝对值来显示。如下
yuan=cv2.imread('yuan.png',0) #这里本身就是黑白的,所以转化灰度图对结果没啥影响,但对彩色图片是有影响的
y1=cv2.Sobel(yuan,-1,dx=1,dy=0,ksize=5)
cv2.imshow('yuan',yuan)
cv2.imshow('yuan1',y1)
cv2.waitKey(0)
y2=cv2.Sobel(yuan,cv2.CV_64F,dx=1,dy=0,ksize=5)#这里只能转化一边,因为这个只能保存大于0的值,这里要保存为CV_64F格式的,然后下面再取绝对值就可以了。
yuan_x_full = cv2.convertScaleAbs(y2)#转换为绝对值,负数转换为正数
cv2.imshow('yuan_x_full',yuan_x_full)
cv2.waitKey(0)
重要代码
y2=cv2.Sobel(yuan,cv2.CV_64F,dx=1,dy=0,ksize=5)#这里只能转化一边,因为这个只能保存大于0的值,这里要保存为CV_64F格式的,然后下面再取绝对值就可以了。
yuan_x_full = cv2.convertScaleAbs(y2)#转换为绝对值,负数转换为正数
优化
我们上面可以看出,如果我们x方向,那么在圆的正上方和正下方那块的左右没变化,然后就断开了,在y方向的时候,最左和最右的上下也是对应的没变化,那么也是有一个缺口,我们该怎么补全呢?
这个时候我们只要把两个有缺的加一起就可以凑出一个完整的了。
yuan=cv2.imread('yuan.png',0)
cv2.imshow('yuan',yuan)
y2=cv2.Sobel(yuan,cv2.CV_64F,dx=1,dy=0,ksize=5)。
yuan_x_full = cv2.convertScaleAbs(y2)#转换为绝对值,负数转换为正数
cv2.imshow('yuan_x_full',yuan_x_full)
yuan_y_full=cv2.Sobel(yuan,cv2.CV_64F,dx=0,dy=1,ksize=5)
yuan_y_full = cv2.convertScaleAbs(yuan_y_full)#转换为绝对值,负数转换为正数
cv2.imshow('yuan_y_full',yuan_y_full)
full=yuan_x_full+yuan_y_full
cv2.imshow('full',full)
cv2.waitKey(0)
那么有人回想,如果我x=1,y=1呢?会不会就好了,其实使得其反的,这个时候缺点全集齐了。
yuan=cv2.imread('yuan.png',0)
xy=cv2.Sobel(yuan,cv2.CV_64F,dx=1,dy=1,ksize=5)
xy=cv2.convertScaleAbs(xy)
cv2.imshow('xy',xy)
cv2.waitKey(0)
综合四种方法
先给出这四种的方法代码
# 读取图像 (灰度模式)
img = cv2.imread('zl.png', 0) # flags=0 表示灰度读取# ========== Sobel边缘检测 ==========
# X方向梯度 (检测垂直边缘)
grad_x = cv2.Sobel(img, cv2.CV_64F, 1, 0) # dx=1, dy=0
abs_grad_x = cv2.convertScaleAbs(grad_x) # 转换负数为绝对值# Y方向梯度 (检测水平边缘)
grad_y = cv2.Sobel(img, cv2.CV_64F, 0, 1) # dx=0, dy=1
abs_grad_y = cv2.convertScaleAbs(grad_y) # 转换负数为绝对值# 合并X/Y方向梯度
sobel_combined = cv2.addWeighted(abs_grad_x, 1, abs_grad_y, 1, 0)# ========== Scharr边缘检测 (更敏感) ==========
# X方向梯度
scharr_x = cv2.Scharr(img, cv2.CV_64F, 1, 0)
scharr_x_abs = cv2.convertScaleAbs(scharr_x)# Y方向梯度
scharr_y = cv2.Scharr(img, cv2.CV_64F, 0, 1)
scharr_y_abs = cv2.convertScaleAbs(scharr_y)# 合并结果
scharr_combined = cv2.addWeighted(scharr_x_abs, 1, scharr_y_abs, 1, 0)# ========== Laplacian边缘检测 ==========
laplacian = cv2.Laplacian(img, cv2.CV_64F)
laplacian_abs = cv2.convertScaleAbs(laplacian) # 二值化前需绝对值转换# ========== Canny边缘检测 (最优效果) ==========
canny_edges = cv2.Canny(img, 100, 200) # 阈值可调整# ========== 显示所有结果 ==========
cv2.imshow('1. Original', img)
cv2.imshow('2. Sobel X', abs_grad_x)
cv2.imshow('3. Sobel Y', abs_grad_y)
cv2.imshow('4. Sobel Combined', sobel_combined)
cv2.imshow('5. Scharr Combined', scharr_combined)
cv2.imshow('6. Laplacian', laplacian_abs)
cv2.imshow('7. Canny', canny_edges)cv2.waitKey(0) # 按任意键关闭窗口
cv2.destroyAllWindows()
第一种是Sobel,上面提到了,这里展示下结果
第二种是Scharr边缘检测
第三种 Laplacian边缘检测
第四种 Canny边缘检测
这个要重要说下,这个就是Sobel的3*3核时算法升级,有以下四种升级。
我们对比下两个的图像,首先噪声消除了许多,边缘正常了一些没那么粗了,还要些。。。
主要源于以下四个升级。
高斯滤波降噪 - 解决噪声敏感性
非极大值抑制(NMS) - 解决边缘粗大
双阈值检测 - 解决阈值单一问题
边缘连接 - 解决边缘断裂
边缘绘制
# 边缘检测and边缘绘制
pen1=cv2.imread('pen.png',0)
ret,pen=cv2.threshold(pen1,120,255,cv2.THRESH_BINARY)
cv2.imshow('pen',pen)
contours, hierarchy = cv2.findContours(pen,cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)print(contours)
print(len(contours))
pen1=pen.copy()
aa = cv2.cvtColor(pen1, cv2.COLOR_GRAY2BGR)
cv2.drawContours(aa,contours,-1,(0,255,0),2)# cv2.imshow('contours',contours)
cv2.imshow('pen1',aa)
cv2.waitKey(0)
通过contours, hierarchy = cv2.findContours(pen,cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)来获取轮廓contours,这个contours保存的是一个一个的坐标点。
hierarchy
参数表示轮廓之间的层级关系,它描述了图像中轮廓的嵌套结构(没啥用)
然后再通过drawContours可以进行绘制。