python探测图像中边框_边缘检测,框出物体的轮廓(使用opencv-python)

边缘检测、边缘探测、轮廓绘制、多边形、区域分割、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

你可能感兴趣的:(python探测图像中边框)