OpenCV(26)图像分割 -- 距离变换与分水岭算法(硬币检测、扑克牌检测、车道检测)
目录
一、基础理论
1、思想
防止同化。
任何灰度图像都可以看作是一个地形表面,其中高强度表示山峰,低强度表示山谷。你开始用不同颜色的水(标签)填充每个孤立的山谷(局部最小值)。随着水位的上升,根据附近的山峰(坡度),来自不同山谷的水明显会开始合并,颜色也不同。为了避免这种情况,你要在水融合的地方建造屏障。你继续填满水,建造障碍,直到所有的山峰都在水下。然后你创建的屏障将返回你的分割结果。这就是Watershed背后的“思想”。你可以访问Watershed的CMM网页,了解它与一些动画的帮助。
但是这种方法会由于图像中的噪声或其他不规则性而产生过度分割的结果。因此OpenCV实现了一个基于标记的分水岭算法,你可以指定哪些是要合并的山谷点,哪些不是。这是一个交互式的图像分割。
2、原理
我们所做的是:给我们知道的对象赋予不同的标签。用一种颜色(或强度)标记我们确定为前景或对象的区域,用另一种颜色标记我们确定为背景或非对象的区域,最后用
0
标记我们不确定的区域,这是我们的标记。然后应用分水岭算法。然后我们的标记将使用我们给出的标签进行更新,对象的边界值将为-1
。靠近对象中心的区域是前景,而离对象中心很远的区域是背景,剩下的区域是不确定区域。
二、分水岭实战:硬币
首先看看原图:
步骤归纳
1、图像二值化
2、开运算去噪
3、确定背景区域(膨胀)(得到背景/最大连通域)
4、确定前景区域(距离变换) (分离)(得到种子/前景)
5、找到未知区域(未知区域=背景-前景)
6、根据种子标记最大连通域
7、使用分水岭算法:合并种子和不确定区域、标记边界为-1
8、涂色并显示
1、把原图像转二值图
# 转二值图像
def ToBinary():
global gray, binary
# 灰度化
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
# 二值化
ret, binary = cv.threshold(gray, 0, 255, cv.THRESH_BINARY_INV + cv.THRESH_OTSU)
cv.imshow('binary', binary)
(为了方便后面的调用,这里把灰度图和二值图都设置为了global全局变量)
2、开运算去噪
开运算,去除噪声
# 1、开运算去噪
opening = cv.morphologyEx(binary, cv.MORPH_OPEN, (3,3), iterations=2)
cv.imshow('opening', opening)
3、确定背景区域(膨胀)(得到背景/最大连通域)
膨胀之后,对象扩大,背景减小,此时对象>原对象,此时背景 < 原背景,那么此时的背景自然可以确定为原背景的一部分。(离对象中心很远的是背景)
# 2、膨胀确定背景区域
sure_bg = cv.dilate(opening, (3,3), iterations=3)
cv.imshow('sure_bg', sure_bg)
4、确定前景区域(距离变换) (分离)(得到种子/前景)
原理:距离变换,在二值图中把对象缩小,得到的就是原图的一部分,可以确定为前景。类似于分离。(不分离的话,可以不用距离变换,只用腐蚀就够了)
为什么不能直接缩小?
直接缩小会让图片也跟着变化,我们要的只是把对象缩小,图片不变的。
distanceTransform函数:计算非零像素到最近零像素点的最短距离。一般用于求解图像的骨骼(二值图)
def distanceTransform(src: Any, distanceType: Any, maskSize: Any, dst: Any = None, dstType: Any = None) -> None
distanceType :所用的求解距离的类型。
mask_size :距离变换掩模的大小,可以是 3 或 5。 对 CV_DIST_L1 或 CV_DIST_C 的情况,参数值被强制设定为 3, 因为 3×3 mask 给出 5×5 mask 一样的结果,而且速度还更快。
# 3、确定前景区域(距离变换)
# 3-1、求对象最大宽度/长度(直径)
dist_transform = cv.distanceTransform(opening, cv.DIST_L2, 5)
# 3-2、最长直径缩小一定程度,确定前景
ret, sure_fg = cv.threshold(dist_transform, 0.7 * dist_transform.max(), 255, cv.THRESH_BINARY)
# 前景 阈值函数 阈值 最大值 二值化方式
sure_fg = np.uint8(sure_fg)
cv.imshow('sure_fg', sure_fg)
sure_fg是种子 。(后面的标记需要根据种子获取标记)
5、找到未知区域(未知区域=背景-前景)
未知区域 = 确定的背景 - 确定的前景
# 4、找未知区域(未知区域 = 确定的背景-确定的前景)
unknown = cv.subtract(sure_bg, sure_fg)
cv.imshow('unknown', unknown)
6、根据种子标记最大连通域
根据种子标记最大连通域(大于1为内部区域,标记1为背景区域,0为未知区域)
# 5、根据种子标记最大连通域(大于1为内部区域,标记1为背景区域,0为未知区域)
ret, markers = cv.connectedComponents(sure_fg) #标记最大连通域
markers = markers+1 #最大连通域标记为1(背景)
markers[unknown == 255] = 0 #未知区域标记为0
显示:连通域(背景)、不确定区域、种子(前景)
#显示各区域(连通域/背景、不确定区域、种子/前景)
def Show_Markers():
mark = img.copy()
mark[markers == 1] = (255, 0, 0) #连通域/背景(蓝)
mark[markers == 0] = (0, 255, 0) #不确定区域(绿)
mark[markers > 1] = (0, 0, 255) #前景/种子(红)
mark[markers == -1] = (0, 255, 0) #边界(绿)
cv.imshow('Markers', mark)
7、使用分水岭算法:合并种子和不确定区域、标记边界为-1
# 6、使用分水岭算法,边界修改为-1,边界涂红(-1)(分界:连通域背景 -- 未知区域+种子)
markers = cv.watershed(img, markers) # 分水岭算法(修改边界为-1)
8、涂色并显示
# 7、涂色并显示(边界(markers==-1)涂色)
dst = img.copy()
dst[markers == -1] = [0, 0, 255] #边界(-1)涂色
cv.imshow('dst', dst)
总代码及效果(硬币)
# 距离变换与分水岭算法
import numpy as np
import cv2 as cv
# 转二值图像
def ToBinary():
global gray, binary
# 灰度化
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
# 二值化
ret, binary = cv.threshold(gray, 0, 255, cv.THRESH_BINARY_INV + cv.THRESH_OTSU)
cv.imshow('binary', binary)
#显示各区域(连通域/背景、不确定区域、种子/前景)
def Show_Markers():
mark = img.copy()
mark[markers == 1] = (255, 0, 0) #连通域/背景(蓝)
mark[markers == 0] = (0, 255, 0) #不确定区域(绿)
mark[markers > 1] = (0, 0, 255) #前景/种子(红)
mark[markers == -1] = (0, 255, 0) #边界(绿)
cv.imshow('Markers', mark)
# 分水岭找边界
def Watershed():
global markers
# 1、开运算去噪
opening = cv.morphologyEx(binary, cv.MORPH_OPEN, (3,3), iterations=2)
cv.imshow('opening', opening)
# 2、确定背景区域(膨胀)
sure_bg = cv.dilate(opening, (3,3), iterations=3)
cv.imshow('sure_bg', sure_bg)
# 3、确定前景区域(距离变换)(种子)
# 3-1、求对象最大宽度/长度(直径)
dist_transform = cv.distanceTransform(opening, cv.DIST_L2, 5)
# 3-2、最长直径按比例缩小,确定前景
ret, sure_fg = cv.threshold(dist_transform, 0.7 * dist_transform.max(), 255, cv.THRESH_BINARY)
# 前景 阈值函数 阈值 最大值 二值化方式
sure_fg = np.uint8(sure_fg)
cv.imshow('sure_fg', sure_fg)
# 4、找未知区域(未知区域 = 确定的背景-确定的前景)
unknown = cv.subtract(sure_bg, sure_fg)
cv.imshow('unknown', unknown)
# 5、根据种子标记最大连通域(大于1为内部区域,标记1为背景区域,0为未知区域)
ret, markers = cv.connectedComponents(sure_fg) #标记最大连通域
markers = markers+1 #背景标记为1(此为最大连通域)
markers[unknown == 255] = 0 #未知区域标记为0
Show_Markers() #显示各区域(连通域/背景、不确定区域、种子/前景)
# 6、使用分水岭算法,合并不确定区域和种子,边界修改为-1,(分界:连通域背景 -- 未知区域+种子)
markers = cv.watershed(img, markers) # 分水岭算法(修改边界为-1)
Show_Markers() #显示各区域(连通域/背景、不确定区域、种子/前景)
# 7、涂色并显示(边界(markers==-1)涂色)
dst = img.copy()
dst[markers == -1] = [0, 0, 255] #边界(-1)涂色
cv.imshow('dst', dst)
if __name__ == '__main__':
img = cv.imread('Resource/test19.jpg')
cv.imshow('img', img)
ToBinary() #转二值图
Watershed() #分水岭找边界
cv.waitKey(0)
三、分水岭实战:扑克牌
(这个是自实现,不那么规范)
原图:
步骤归纳
1、锐化
如果只是用硬币的代码,会出现这样的效果(只能分出外部的区域):
这里我们有必要在二值化前进行锐化。
一开始使用的是卷积核:
kernel = np.array([
[1, 1, 1],
[1, -8, 1],
[1, 1, 1]
])
发现边缘还是不够明显,继续升级卷积核:
kernel = np.array([
[2, 2, 2],
[2, -16, 2],
[2, 2, 2]
])
现在可以看到,二值图像边缘已经比较清晰锐利了。
2、开运算去噪
# 1、开运算去噪
opening = cv.morphologyEx(binary, cv.MORPH_OPEN, (3,3), iterations=2)
cv.imshow('opening', opening)
3、获取背景(全连通域)
这里的扑克牌只是空架子,就不膨胀处理了,
# 2、确定背景区域(这里只是空架子,就不膨胀了)
sure_bg = opening.copy()
cv.imshow('sure_bg', sure_bg)
3、确定前景区域(距离变换)(种子)
# 3、确定前景区域(距离变换)(种子)
# 3-1、求对象最大宽度/长度(直径)
dist_transform = cv.distanceTransform(opening, cv.DIST_L2, 3, 5)
# 3-2、最长直径按比例缩小,确定前景
ret, sure_fg = cv.threshold(dist_transform, 0.01 * dist_transform.max(), 255, cv.THRESH_BINARY)
# 前景 阈值函数 阈值 最大值 二值化方式
sure_fg = np.uint8(sure_fg)
cv.imshow('sure_fg', sure_fg)
4、找未知区域(未知区域 = 确定的背景-确定的前景)
# 4、找未知区域(未知区域 = 确定的背景-确定的前景)
unknown = cv.subtract(sure_bg, sure_fg)
cv.imshow('unknown', unknown)
5、根据种子标记最大连通域
# 5、根据种子标记最大连通域(大于1为内部区域,标记1为背景区域,0为未知区域)
ret, markers = cv.connectedComponents(sure_fg) #标记最大连通域
markers = markers+1 #背景标记为1(此为最大连通域)
markers[unknown == 255] = 0 #未知区域标记为0
Show_Markers() #显示各区域(连通域/背景、不确定区域、种子/前景)
6、使用分水岭算法,合并不确定区域和种子,边界修改为-1
# 6、使用分水岭算法,合并不确定区域和种子,边界修改为-1(分界:连通域背景 -- 未知区域+种子)
markers = cv.watershed(img, markers) # 分水岭算法(修改边界为-1)
Show_Markers() #显示各区域(连通域/背景、不确定区域、种子/前景)
7、涂色并显示
# 7、涂色并显示(边界(markers==-1)涂色)
dst = img.copy()
dst[markers == -1] = [0, 255, 0] #边界(-1)涂色
cv.imshow('dst', dst)
总代码及效果(扑克牌)
# 距离变换与分水岭算法
import numpy as np
import cv2 as cv
# 转二值图像
def ToBinary():
global gray, binary
# 1、锐化
kernel = np.array([
[2, 2, 2],
[2, -16, 2],
[2, 2, 2]
])
sharp = cv.filter2D(img, -1, kernel)
cv.imshow('sharp', sharp)
# 灰度化
gray = cv.cvtColor(sharp, cv.COLOR_BGR2GRAY)
cv.imshow('gray', gray)
# 二值化
ret, binary = cv.threshold(gray, 0, 255, cv.THRESH_BINARY_INV + cv.THRESH_OTSU)
cv.imshow('binary', binary)
#显示各区域(连通域/背景、不确定区域、种子/前景)
def Show_Markers():
mark = img.copy()
mark[markers == 1] = (255, 0, 0) #连通域/背景(蓝)
mark[markers == 0] = (0, 255, 0) #不确定区域(绿)
mark[markers > 1] = (0, 0, 255) #前景/种子(红)
mark[markers == -1] = (0, 255, 0) #边界(绿)
cv.imshow('Markers', mark)
# 分水岭找边界
def Watershed():
global markers
# 1、开运算去噪
opening = cv.morphologyEx(binary, cv.MORPH_OPEN, (3,3), iterations=2)
cv.imshow('opening', opening)
# 2、确定背景区域(这里只是空架子,就不膨胀了)
sure_bg = opening.copy()
cv.imshow('sure_bg', sure_bg)
# 3、确定前景区域(距离变换)(种子)
# 3-1、求对象最大宽度/长度(直径)
dist_transform = cv.distanceTransform(opening, cv.DIST_L2, 3, 5)
# 3-2、最长直径按比例缩小,确定前景
ret, sure_fg = cv.threshold(dist_transform, 0.01 * dist_transform.max(), 255, cv.THRESH_BINARY)
# 前景 阈值函数 阈值 最大值 二值化方式
sure_fg = np.uint8(sure_fg)
cv.imshow('sure_fg', sure_fg)
# 4、找未知区域(未知区域 = 确定的背景-确定的前景)
unknown = cv.subtract(sure_bg, sure_fg)
cv.imshow('unknown', unknown)
# 5、根据种子标记最大连通域(大于1为内部区域,标记1为背景区域,0为未知区域)
ret, markers = cv.connectedComponents(sure_fg) #标记最大连通域
markers = markers+1 #背景标记为1(此为最大连通域)
markers[unknown == 255] = 0 #未知区域标记为0
Show_Markers() #显示各区域(连通域/背景、不确定区域、种子/前景)
# 6、使用分水岭算法,合并不确定区域和种子,边界修改为-1(分界:连通域背景 -- 未知区域+种子)
markers = cv.watershed(img, markers) # 分水岭算法(修改边界为-1)
Show_Markers() #显示各区域(连通域/背景、不确定区域、种子/前景)
# 7、涂色并显示(边界(markers==-1)涂色)
dst = img.copy()
dst[markers == -1] = [0, 255, 0] #边界(-1)涂色
cv.imshow('dst', dst)
if __name__ == '__main__':
img = cv.imread('Resource/test20.jpg')
cv.imshow('img', img)
ToBinary() #转二值图
Watershed() #分水岭找边界
cv.waitKey(0)
四、分水岭实战:路面检测
先锐化再进行处理,会适用于很多种情况。
这里和扑克牌的类似,都是边界不够明显,需要锐化处理的。前面的扑克牌只有一个框架,没法膨胀,路面可以膨胀处理获取背景。
原图:
1、 锐化
2、锐化之后再二值化
3、膨胀获取背景
和上面扑克牌不同的就是这里,这里的路面不是空架子,可以进行膨胀。
4、距离变换获取前景
5、获取不确定区域(背景-前景)
6、显示标记情况
7、成功检测出路面的分水岭
总代码
# 距离变换与分水岭算法(路面检测)
import numpy as np
import cv2 as cv
# 转二值图像
def ToBinary():
global gray, binary
# 1、锐化
kernel = np.array([
[2, 2, 2],
[2, -16, 2],
[2, 2, 2]
])
sharp = cv.filter2D(img, -1, kernel)
cv.imshow('sharp', sharp)
# 灰度化
gray = cv.cvtColor(sharp, cv.COLOR_BGR2GRAY)
cv.imshow('gray', gray)
# 二值化
ret, binary = cv.threshold(gray, 0, 255, cv.THRESH_BINARY_INV + cv.THRESH_OTSU)
cv.imshow('binary', binary)
#显示各区域(连通域/背景、不确定区域、种子/前景)
def Show_Markers():
mark = img.copy()
mark[markers == 1] = (255, 0, 0) #连通域/背景(蓝)
mark[markers == 0] = (0, 255, 0) #不确定区域(绿)
mark[markers > 1] = (0, 0, 255) #前景/种子(红)
mark[markers == -1] = (0, 255, 0) #边界(绿)
cv.imshow('Markers', mark)
# 分水岭找边界
def Watershed():
global markers
# 1、开运算去噪
opening = cv.morphologyEx(binary, cv.MORPH_OPEN, (3,3), iterations=3)
cv.imshow('opening', opening)
# 2、确定背景区域(膨胀)
sure_bg = cv.dilate(opening, (3,3), iterations=2)
cv.imshow('sure_bg', sure_bg)
# 3、确定前景区域(距离变换)(种子)
# 3-1、求对象最大宽度/长度(直径)
dist_transform = cv.distanceTransform(opening, cv.DIST_L2, 3, 5)
# 3-2、最长直径按比例缩小,确定前景
ret, sure_fg = cv.threshold(dist_transform, 0.01 * dist_transform.max(), 255, cv.THRESH_BINARY)
# 前景 阈值函数 阈值 最大值 二值化方式
sure_fg = np.uint8(sure_fg)
cv.imshow('sure_fg', sure_fg)
# 4、找未知区域(未知区域 = 确定的背景-确定的前景)
unknown = cv.subtract(sure_bg, sure_fg)
cv.imshow('unknown', unknown)
# 5、根据种子标记最大连通域(大于1为内部区域,标记1为背景区域,0为未知区域)
ret, markers = cv.connectedComponents(sure_fg) #标记最大连通域
markers = markers+1 #背景标记为1(此为最大连通域)
markers[unknown == 255] = 0 #未知区域标记为0
Show_Markers() #显示各区域(连通域/背景、不确定区域、种子/前景)
# 6、使用分水岭算法,合并不确定区域和种子,边界修改为-1(分界:连通域背景 -- 未知区域+种子)
markers = cv.watershed(img, markers) # 分水岭算法(修改边界为-1)
Show_Markers() #显示各区域(连通域/背景、不确定区域、种子/前景)
# 7、涂色并显示(边界(markers==-1)涂色)
dst = img.copy()
dst[markers == -1] = [0, 255, 0] #边界(-1)涂色
cv.imshow('dst', dst)
if __name__ == '__main__':
img = cv.imread('Resource/road.jpg')
cv.imshow('img', img)
ToBinary() #转二值图
Watershed() #分水岭找边界
cv.waitKey(0)
参考资料
更多推荐
所有评论(0)