边缘检测、边缘探测、轮廓绘制、多边形、区域分割、edge_detection、object_segmentation,使用opencv-python的函数cv2.findContours(),框出物体的轮廓
(关键词↑)
图片处理效果预览↑(就是封面图片),从左到右依次是:原图
阈值图(第一行)、Canny边缘提取(第二行)
蓝色矩形、绿色最小矩形、红色最小圆形
蓝色等高线轮廓、绿色贴合轮廓、红色包围轮廓
核心代码预览:
thresh, contours, hierarchy = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
approx = cv2.approxPolyDP(cnt, epsilon, True)
hull = cv2.convexHull(cnt)
x, y, w, h = cv2.boundingRect(cnt)
min_rect = cv2.minAreaRect(cnt)
(x, y), radius = cv2.minEnclosingCircle(cnt)2018-06-30 初版 Yonv1943
2018-07-02 删除图片水印,其他小改动
2018-08-15 修复GitHub链接,感谢 知乎用户Yozora 在评论中指出错误
2018-11-24 回复评论内容,增加对多边形的筛选
我不满足于用一个矩形将图片中的目标框出,所以我寻找可以将目标的轮廓框出来的算法,最终我在opencv库里面找到了这个函数cv2.findContours(),参考另外一篇非常好的资料后(那篇非常好的资料→)OpenCV for detecting Edges, lines and shapes,我写出来本篇文章。
想要在自己电脑上运行的话,直接去上面这里↑复制、保存为*.py文件,并准备一张背景简单的测试图片,比如说这一张:test.png
下面是 TUTR_edge_detection.py 的流程图↓edge_detection.py 使用ProcessOn 绘制流程图
edge_detection.py
读取图片,并处理为二值图
cv2.imread('test.png') # a black objects on white image is better
# gray = cv2.cvtColor(image.copy(), cv2.COLOR_BGR2GRAY)
# ret, thresh = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
thresh = cv2.Canny(image, 128, 256)
得到二值图后,使用cv2.findContours() 得到等高线 contours,关于它的三个参数,一般不需要改动:第一个是传入的二值图,不等于0的像素将参与等高线的计算
第二个决定hierarchy 采取什么样的格式输出,具体格式看→Contours Hierarchy
thresh, contours, hierarchy = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
函数返回的thresh 其实就是传入的 thresh,我不清楚返回一个二值图的作用。
contours是一个三维,它储存了:同一圈等高线的点的列表的列表
for contour in contours:
for (x, y) in countour:
hierarchy是一个三维数组,它储存了等高线的情况,如果你使用了我提供的测试图片 test.png,那么你的hierarchy(等高线阶层) 将会是像下面↓这样的:
[[[ 1 -1 -1 -1]
[ 2 0 -1 -1]
[ 3 1 -1 -1]
[-1 2 -1 -1]]] :hierarchy # cv2.threshold()
[[[-1 -1 -1 -1]]] :hierarchy # cv2.Canny()使用不同二值图对等高线检测的影响
黄色箭头标记出阈值处理和边缘检测两种方法对结果的影响,阈值处理的结果,多出来的红色小圆与多出来的蓝色等高线不是我们想要的。
寻找等高线的时候,为何我推荐使用边缘检测而非阈值处理作为二值图?
我参考的博客里面使用阈值函数cv2.threshold()处理图片得到二值图,不过我建议:在性能允许的情况下,当我们需要框出的是目标的轮廓,那么使用cv2.Canny() 更好,理由:提取边缘与阈值处理不同,边缘提取可以识别图片中目标的形状、轮廓,而不是简单的区分出图片中的高光与暗调,可以简单地提图片中颜色分布位于中间调的上目标;
使用Canny边缘检测,提取结果的白点数量更少,对等高线检测的混淆因素减少(猜测);
我对比了一些边缘检测算法,Canny边缘检测效果好(虽然对性能要求高);中间调是指色阶值接近中值(128)的 图像像素。
这里的边缘检测算法可以不使用cv2.Canny()函数,使用其他函数亦可,Canny边缘检测是我用得比较熟悉的,比如cv2.Sobel(),cv2.Scharr(),cv2.Laplacian()etc.,你可以去OpenCV-Python 的Documentation看一下。
我其实已经对比了许多边缘检测算法,才选择了Canny边缘检测的,它的效果不错。另外关于cv2.Canny()边缘检测,你可以看我的另外一篇文章——关于自动调节Canny目标检测函数的两个参数,实现autoCanny()(←我还没写)
得到合适的目标轮廓(等高线)后,可以开始绘制轮廓,虽然使用cv2.CHAIN_APPROX_SIMPLE 取代 cv2.CHAIN_APPROX_NONE 已经尽可能忽略掉不必要的点,但是目标轮廓还可以变得更简单一点。
对输出的目标轮廓,我们可以做一些优化,比如使用:贴合轮廓(近似多边形) cv2.approxPolyDP()
包围轮廓(船壳多边形)cv2.convexHull()
↑ 其实这些是我自己给它们取的中文名称
def draw_approx_hull_polygon(img, cnts):
# img = np.copy(img)
img = np.zeros(img.shape, dtype=np.uint8)
cv2.drawContours(img, cnts, -1, (255, 0, 0), 2) # blue
epsilion = img.shape[0]/32
approxes = [cv2.approxPolyDP(cnt, epsilion, True) for cnt in cnts]
cv2.polylines(img, approxes, True, (0, 255, 0), 2) # green
hulls = [cv2.convexHull(cnt) for cnt in cnts]
cv2.polylines(img, hulls, True, (0, 0, 255), 2) # red
# 我个人比较喜欢用上面的列表解析,我不喜欢用for循环,看不惯的,就注释上面的代码,启用下面的
# for cnt in cnts:
# cv2.drawContours(img, [cnt, ], -1, (255, 0, 0), 2) # blue
#
# epsilon = 0.01 * cv2.arcLength(cnt, True)
# approx = cv2.approxPolyDP(cnt, epsilon, True)
# cv2.polylines(img, [approx, ], True, (0, 255, 0), 2) # green
#
# hull = cv2.convexHull(cnt)
# cv2.polylines(img, [hull, ], True, (0, 0, 255), 2) # red
return img
img = draw_min_rect_circle(image, contours)
cv2.imshow("contours", img)
cv2.waitKey(1943)
epsilon是近似轮廓的最小边长,我选取的是图片边长的32分之一,而原博客里面用的方法是取等高线周长(通过cv2.arcLength()计算周长)的百分之一。下图黄色的显示了多边形某条边的长度。多边形中,小于epsilon的边都会被跳过。
绿色贴合轮廓(近似多边形) 与 红色包围轮廓(船壳多边形)的区别是“是否凹陷”函数draw_approx_hull_polygon() 的结果图片
如果只是希望框出目标物体,那么可以用更加简单的方法,比如用矩形或者圆形将目标框出就好了,这里顺便介绍我参考的博客里面介绍的框选方法:蓝色矩形 cv2.boundingRect(cnt)
绿色最小矩形 cv2.minAreaRect(cnt)
红色最小圆形 cv2.minEnclosingCircle(cnt)
def draw_min_rect_circle(img, cnts): # conts = contours
img = np.copy(img)
for cnt in cnts:
x, y, w, h = cv2.boundingRect(cnt)
cv2.rectangle(img, (x, y), (x + w, y + h), (255, 0, 0), 2) # blue
min_rect = cv2.minAreaRect(cnt) # min_area_rectangle
min_rect = np.int0(cv2.boxPoints(min_rect))
cv2.drawContours(img, [min_rect], 0, (0, 255, 0), 2) # green
(x, y), radius = cv2.minEnclosingCircle(cnt)
center, radius = (int(x), int(y)), int(radius) # for the minimum enclosing circle
img = cv2.circle(img, center, radius, (0, 0, 255), 2) # red
return img
img = draw_approx_hull_polygon(image, contours)
cv2.imshow("contours", img)
cv2.waitKey(1943)函数 draw_min_rect_circle() 的结果图片
函数 draw_min_rect_circle() 的结果图片像机器学习目标检测一样,他们一般使用水平的矩形将结果图框出
不过我认为,这样不太“自然”,深度学习的目标检测应该把物体的轮廓框出,计算mAP的时候也应该使用多边形轮廓,而不是使用矩形,在处理一些比较圆润的物体的时候还行,如果处理一些有棱角的目标,用矩形的话就显得太粗糙了。
虽然在目标检测的发展前期,使用矩形简化了结果输出(只需输出标签加上4个坐标表示矩形),但是这样肯定是不够的,未来的发展方向是应该更加精细(所以要用多边形轮廓)
然而随着FCN (Fully Convolution Networks 全卷积网络)以及何凯明的 Mask-RCNN的发展,似乎这些都已经解决了。
def run():
image = cv2.imread('test.png') # a black objects on white image is better
# gray = cv2.cvtColor(image.copy(), cv2.COLOR_BGR2GRAY)
# ret, thresh = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
thresh = cv2.Canny(image, 128, 256)
thresh, contours, hierarchy = cv2.findContours(thresh, cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE)
# print(hierarchy, ":hierarchy")
"""
[[[-1 -1 -1 -1]]] :hierarchy # cv2.Canny()
[[[ 1 -1 -1 -1]
[ 2 0 -1 -1]
[ 3 1 -1 -1]
[-1 2 -1 -1]]] :hierarchy # cv2.threshold()
"""
imgs = [
image, thresh,
draw_min_rect_circle(image, contours),
draw_approx_hull_polygon(image, contours),
]
for img in imgs:
# cv2.imwrite("%s.jpg" % id(img), img)
cv2.imshow("contours", img)
cv2.waitKey(1943)
if __name__ == '__main__':
run()
pass
在评论区指出的问题,我会修改到正文中,并注明贡献者的名字。
在评论区提出的问题,我可能会尝试解答,并添加到正文中。
交流可以促进社区与自身成长,欢迎评论,谢谢大家!
对 @dorado 评论的回复:请问在图像找到轮廓后,怎么做一个筛选?过滤掉一些面积比较小的区域,或者边缘的干扰呢?有什么好的方法么?谢谢
对该文件中的函数draw_approx_hull_polygon( )稍作修改,仅添加几行代码,就可以实现这个功能。为了性能考虑,不建议用多变形的面积进行筛选,而是对多变形边长进行筛选,目的是筛选出比较大的多边形,请看下面的注释:
def draw_approx_hull_polygon(img, cnts):
# img = np.copy(img)
img = np.zeros(img.shape, dtype=np.uint8)
cv2.drawContours(img, cnts, -1, (255, 0, 0), 2) # blue
min_side_len = img.shape[0] / 32 # 多边形边长的最小值 the minimum side length of polygon
min_poly_len = img.shape[0] / 16 # 多边形周长的最小值 the minimum round length of polygon
min_side_num = 3 # 多边形边数的最小值
min_area = 16.0 # 多边形面积的最小值
approxs = [cv2.approxPolyDP(cnt, min_side_len, True) for cnt in cnts] # 以最小边长为限制画出多边形
approxs = [approx for approx in approxs if cv2.arcLength(approx, True) > min_poly_len] # 筛选出周长大于 min_poly_len 的多边形
approxs = [approx for approx in approxs if len(approx) > min_side_num] # 筛选出边长数大于 min_side_num 的多边形
approxs = [approx for approx in approxs if cv2.contourArea(approx) > min_area] # 筛选出面积大于 min_area_num 的多边形
# Above codes are written separately for the convenience of presentation.
cv2.polylines(img, approxs, True, (0, 255, 0), 2) # green
hulls = [cv2.convexHull(cnt) for cnt in cnts]
cv2.polylines(img, hulls, True, (0, 0, 255), 2) # red
另外,关于边缘检测框出移动物体的应用,可以看我的:曾伊言:感兴趣区域的移动物体检测,框出移动物体的轮廓 (固定摄像头, opencv-python)zhuanlan.zhihu.com