OpenCV—轮廓操作一站式详解:查找/筛选/绘制/形状描述与重心标注(Python版)

OpenCV—轮廓操作一站式详解:查找/筛选/绘制/形状描述与重心标注(Python版)

为了方便使用的Python同学,将上一篇文章用Python重写了一遍,其中一些OpenCV的Python接口说明将后续持续更新。

轮廓是定义或限定形状或对象的边或线,是机器视觉中的常用的概念,多用于目标检测、识别等任务。关于OpenCV轮廓操作,尤其是级别及如何使用轮廓级别进行筛选等问题,相关文章比较少,正好最近用到,因此将其总结成文。本文主要介绍OpenCV的查找轮廓函数findContours()绘制函数drawContours(),及其轮廓级别参数hierarchy,涉及到预处理、轮廓筛选等内容,并提供全部源代码,希望能帮助大家理解基本概念并能借鉴示例代码编写自己的算法。

本文代码:Python

Python版本3.5或以上

OpenCV版本3.4(4.1版本有一处接口修改,文中已注明)

本文包括如下内容:

  • 基本概念

1.查找和绘制轮廓函数findContous(),drawContours()

2.轮廓参数:轮廓级别、轮廓长度

3.轮廓的形状描述子:最小覆盖矩形、圆、多边形逼近、凸包

  • 编程实战

1.如何筛选轮廓:按轮廓级别和长度筛选

2.如何绘制轮廓的外接形状

3.如何获取轮廓的重心坐标并标注

本文的目标:

1.从原始图像中找到2架可回收火箭

2.标注目标的位置与重心坐标

阅读完成后,将能从原始图像中找到2架火箭,并标注其位置与坐标。如下图所示:

目录

OpenCV轮廓操作一站式详解:查找/筛选/绘制/形状描述与标注

1.查找、绘制轮廓函数

findContours()

drawContours()

2.预处理

3.查找轮廓

4.绘制轮廓

5.筛选轮廓

5.1 hierarchy轮廓级别详解

contours与hierarchy的关系

什么是层次结构hierarchy?

5.2 OpenCV中的层次结构表示

Next

Previous

First_Child

Parent

5.3 按hierarchy筛选轮廓

5.4 按长度筛选轮廓

6.联通域分析

7.标注轮廓重心


1.查找、绘制轮廓函数


findContours()

函数原型:

cv2.findContours(image, mode, method[, contours[, hierarchy[, offset ]]]) 
使用方式:
binary, contours, hierarchy = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)

函数参数:

image

输入:源图像,一个8位单通道图像,注意一定是CV_8UC1的单通道图像,否则报错。 非零像素被视为1。 零像素保持为0,因此图像被视为二进制。可以使用compare,inRange,threshold,adaptiveThreshold,Canny等来从灰度或彩色图像中创建二进制图像。如果mode为RETR_CCOMP或RETR_FLOODFILL,则输入也可以是标签的32位整数图像(CV_32SC1)。

contours

输出:检测到的轮廓。每个轮廓都存储为点向量(例如std :: vector <std :: vector <cv :: Point >>)。即由若干个cv::Point类型的点组成了单个轮廓std :: vector <cv :: Point >,再由若干个轮廓组成输入图像中的全部轮廓std::vector<std :: vector <cv :: Point >>

hierarchy

输出:轮廓级别信息。Hierarchy为可选输出变量,是std::vector<cv::Vec4i>类型的向量(每个元素都是一个4个int值构成的向量)。包含有关图像拓扑的信息。它具有与轮廓数量一样多的元素。例如,第i个轮廓, hierarchy[i][0],hierarchy[i][1],hierarchy[i][2]和hierarchy[i][3]依次为:第i个轮廓的[Next, Previous, First_Child, Parent],即轮廓i相同等级的下一轮廓、前一轮廓,第一个子轮廓和父轮廓(上一级轮廓)的索引号(即contours向量中的轮廓序号)。如果轮廓i没有下一个,前一个,父级或嵌套轮廓,则层次结构[i]的相应元素将为负数。这个参数我们将在下文中重点介绍。
mode 输入:轮廓检索模式, 详见 RetrievalModes
method 输入:轮廓近似法, 详见ContourApproximationModes
offset 输入:每个轮廓点移动的偏移量,可选参数,cv::Point()类型。如果从整幅图像的某个ROI中提取轮廓,然后又在整个图像中分析轮廓(将ROI中的轮廓坐标恢复到整幅图像中的坐标),这个偏移量非常有用,可以免去我们自己写代码转换坐标系的麻烦。

mode参数:

RETR_EXTERNAL 

Python: cv.RETR_EXTERNAL

仅检索极端外轮廓。 它为所有轮廓设置hierarchy [i][2] = hierarchy [i][3] = – 1。

RETR_LIST 

Python: cv.RETR_LIST

检索所有轮廓而不建立任何层次关系。

RETR_CCOMP 

Python: cv.RETR_CCOMP

检索所有轮廓并将它们组织成两级层次结构。在顶层轮廓是外部轮廓。在第二层轮廓是“洞”的轮廓。如果连接组件的洞内有另一个轮廓,它的级别仍然认定为顶层。

RETR_TREE 

Python: cv.RETR_TREE

检索所有轮廓并重建嵌套轮廓的完整层次结构。

RETR_FLOODFILL 

Python: cv.RETR_FLOODFILL

 

drawContours()

绘制轮廓轮廓或填充轮廓。

函数原型:

image = cv.drawContours(image, contours, contourIdx, color[, thickness[, lineType[, hierarchy[, maxLevel[, offset]]]]])

函数参数:

image

输入:源图像。单通道或3通道图像。

contours

输入:待绘制的轮廓。std :: vector <std :: vector <cv :: Point >>类型。
contourIdx 输入:待绘制的轮廓序号。例如:0为绘制第1个轮廓contours[0];1为绘制第2个轮廓contours[1],依次类推;-1为绘制所有轮廓。
color 输入:轮廓颜色。cv::Scalar变量,例如:cv::Scalar(0,0,255)为红色轮廓,cv::Scalar::all(0)为黑色轮廓
thickness 输入:轮廓粗细。int型变量,默认为1,值越大越粗
lineType 输入:绘制轮廓的线型。默认LINE_8,8联通线型(下一个点连接上一个点的边或角)

hierarchy

输入:待绘制的轮廓级别。std::vector<cv::Vec4i>类型的向量(每个元素都是一个4个int值构成的向量)。下一轮廓、前一轮廓,第一个子轮廓和父轮廓(上一级轮廓)的索引号。
maxLevel 输入:待绘制的轮廓最大级别。
method 输入:轮廓近似法, 详见ContourApproximationModes
offset 输入:每个轮廓点移动的偏移量,可选参数。

2.预处理


预处理目的是为轮廓查找提供高质量的输入源图像。

预处理的主要步骤包括:

  • 灰度化:使用cv2.cvtColor()
  • 图像去噪:使用高斯滤波cv2.GaussianBlur()
  • 二值化:使用cv2.threshold()
  • 形态学处理:cv2.morphologyEx()

其中灰度化可以将3通道图像转化为单通道图像,以便进行二值化门限分割;去噪可以有效剔除图像中的异常独立噪点;二值化是为轮廓查找函数提供单通道图像;形态学的某些处理通常可以剔除细小轮廓,联通断裂的轮廓。

读取图像代码如下:

# 1.载入图像
image = cv2.imread("spaceX2.jpg", 1)
# print(image.shape)
height, width, channel = image.shape
image = cv2.resize(image, (int(0.5*width), int(0.5*height)), interpolation=cv2.INTER_CUBIC)
cv2.imshow("original", image)
cv2.waitKey()

# 2.预处理
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# print(gray.shape)
cv2.imshow("gray", gray)

gray = cv2.GaussianBlur(gray, (3, 3), 1)
ret, binary = cv2.threshold(gray, 80, 255, cv2.THRESH_BINARY_INV)
cv2.imshow("binary", binary)

element = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))  # 3 * 3 正方形,8位uchar型,全1结构元素
binary = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, element)

cv2.imshow("morphology", binary)
cv2.waitKey()

3.查找轮廓


使用findContours函数查找轮廓,并输出找到的轮廓个数

# 3.查找轮廓
binary, contours, hierarchy = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)  # for opencv3.4
print("find", len(contours), "contours")

如果使用的是opencv4.1版本,则上述代码中查找轮廓函数的返回值仅有2个,如下所示

# 3.查找轮廓
contours, hierarchy = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)  # for opencv4.1
print("find", len(contours), "contours")

4.绘制轮廓


为了方便查看轮廓查找结果,使用drawContours()来绘制轮廓并显示。编写函数drawMyContours()函数用于在白色背景中或者原图上查看轮廓。

函数参数:

  • 窗口名字;
  • 原始图像以及轮廓变量
  • 白色背景上还是在原图上绘制轮廓的标志位
# 4. 绘制轮廓函数
# 自定义绘制轮廓的函数(为简化操作)
# 输入1:winName:窗口名
# 输入2:image:原图
# 输入3:contours:轮廓
# 输入4:draw_on_blank:绘制方式,True在白底上绘制,False:在原图image上绘制
def drawMyContours(winName, image, contours, draw_on_blank):
    # cv2.drawContours(image, contours, index, color, line_width)
    # 输入参数:
    # image:与原始图像大小相同的画布图像(也可以为原始图像)
    # contours:轮廓(python列表)
    # index:轮廓的索引(当设置为-1时,绘制所有轮廓)
    # color:线条颜色,
    # line_width:线条粗细
    # 返回绘制了轮廓的图像image
    if (draw_on_blank): # 在白底上绘制轮廓
        temp = np.ones(image.shape, dtype=np.uint8) * 255
        cv2.drawContours(temp, contours, -1, (0, 0, 0), 2)
    else:
        temp = image.copy()
        cv2.drawContours(temp, contours, -1, (0, 0, 255), 2)
    cv2.imshow(winName, temp)
    cv2.waitKey()

在main函数中调用drawMyContours()如下:

# 4.绘制原始轮廓
drawMyContours("find contours", image, contours, True)

首次查找的轮廓变量contours中有21个向量,即找到21个轮廓。调试展开contours变量,可以看到每个元素都是一个由一系列轮廓上的点组成的。

调试展开hierarchy变量,可以看到其类型为ndarray的n维数组,维度为(1, 21, 4),第二维度对应轮廓的索引号(共21个轮廓),第三维度为1×4的轮廓级别信息(每个轮廓包含4个级别描述量[Next,Previous,First_Child,Parent])。

通过绘制轮廓,可以看到这21个轮廓,除了两个目标之外,还有云、地面背景以其中的“洞”轮廓。

5.筛选轮廓


查找到大轮廓显然有许多不符合要求,因此可以通过某些准则进行轮廓筛选。通过观察,发现上图中有许多轮廓包含“洞”,即子轮廓,而这些子轮廓显然也有父级轮廓,因此我们可以使用findContours的hierarchy轮廓级别参数删除那些有子轮廓也有父轮廓的轮廓。

5.1hierarchy轮廓级别详解

contours与hierarchy的关系

使用findContours()函数将返回contours轮廓向量以及对应的hierarchy轮廓级别向量(可选项)。两者有相同的长度即contours.size() = hierarchy.size(),并且向量的序号表示找到的轮廓索引,且一一对应。

什么是层次结构hierarchy?

通常我们使用findContours()函数来检测图像中的对象。有时对象位于不同的位置。但在某些情况下,某些形状在其他形状内(类似嵌套)。在这种情况下,我们将外部轮廓称为父级轮廓,将内部轮廓称为子轮廓。这样,图像中的轮廓彼此之间存在某种关系。并且我们可以指定一个轮廓如何相互连接,例如,它是某个其他轮廓的子项,还是父项等。此关系的表示就称为层次结构hierarchy。

考虑下面的示例图片:

在上图中,有一些形状,我们从0-5编号这5个形状。图中2和2a表示最外侧矩形的外部和内部轮廓。

轮廓0,1,2是最外部轮廓,三者为同一级别。我们可以说,它们在层次结构0中,或者只是它们处于相同的层次结构级别。

接下来是轮廓-2a可以被认为是轮廓-2的子轮廓(或者相反,轮廓-2是轮廓-2a的父级轮廓)所以让它在层次结构-1中。 类似地,轮廓-3是轮廓-2a的子轮廓,它进入下一层次。 最后,轮廓4,5是轮廓-3a的子轮廓,它们位于最后的层次结构级别。 从编号框的方式,可以说轮廓-4是轮廓-3a的第一个子轮廓,当然轮廓-5也是轮廓-3a的子轮廓。

如果上面的描述看着头晕,不要紧,一开始都这样。

5.2 OpenCV中的层次结构表示

OpenCV中每个轮廓都有自己的信息,关于它是什么层次结构,谁是它的子轮廓,谁是它的父轮廓等.OpenCV将它表示为四个int值的数组,Python接口中类型为1个1×4数组:

[Next,Previous,First_Child,Parent]

Next

Next表示同一级别的下一个轮廓索引。例如,在我们的图片中取出轮廓-0。同一水平的下一个轮廓是轮廓-1。 所以简单地说Next = 1。类似地,对于轮廓-1,next是轮廓-2。 所以Next = 2。

轮廓-2的同一级别没有下一个轮廓,所以轮廓-2的Next = -1。轮廓-4呢?它与轮廓-5处于同一水平。所以它的下一个轮廓是轮廓-5,所以轮廓-4的Next = 5。

Previous

Previous表示同一级别的上一个轮廓索引。例如,轮廓-1的上一个轮廓在同一级别中为轮廓-0。 类似地,对于轮廓-2,它的上一个轮廓是轮廓-1。而对于轮廓-0,没有先前的,所以把它的Previous = -1。

First_Child

First_Child表示当前轮廓的第一个子轮廓索引。例如,对于轮廓-2,子轮廓是轮廓-2a。因此轮廓-2的First_Child为轮廓-2a的相应索引值。轮廓-3a呢?它有两个子轮廓。但hierarchy参数只记录第一个子轮廓,因此它是轮廓-4的索引值。因此,对于轮廓-3a,First_Child = 4。

Parent

Parent表示当前轮廓的父轮廓索引。对于轮廓-4和轮廓-5,它们的父轮廓都是轮廓-3a。对于轮廓-3a,它的父轮廓是轮廓-3,依此类推。

注意:

  • Previous表示同一层级的前一个轮廓的索引;
  • Parent表示其父轮廓的索引;
  • 如果某个轮廓没有子轮廓项或父轮廓,则对应的字段=-1.

5.3hierarchy筛选轮廓

有了上述对轮廓层级的理解,下面就可以根据需要筛选轮廓了。例如本文任务是找到两个火箭,而首次查找轮廓有许多中间有空洞的轮廓不符合要求,下面就通过遍历每一个轮廓的hierarchy级别参数的第3第4个参数来找到那些有子轮廓或者有父轮廓的轮廓,并删除之。注意使用Python接口操作轮廓时,hierarchy级别参数与轮廓变量contours为不同类型的数据。处理时需要分别对待。

首先自定义一个专门用于删除指定序号轮廓的函数delete_contours(),以方便后续操作:

#  自定义函数:用于删除列表指定序号的轮廓
#  输入 1:contours:原始轮廓
#  输入 2:delete_list:待删除轮廓序号列表
#  返回值:contours:筛选后轮廓
def delet_contours(contours, delete_list):
    delta = 0
    for i in range(len(delete_list)):
        # print("i= ", i)
        del contours[delete_list[i] - delta]
        delta = delta + 1
    return contours

然后使用级别筛选与长度筛选分别获得待删除的轮廓序号列表delete_list,再调用上述函数进行处理,以确保操作时轮廓序号不越界。

# 5.筛选轮廓
# 5.1使用层级结构筛选轮廓
# hierarchy[i]: [Next,Previous,First_Child,Parent]
# 要求没有父级轮廓
delete_list = []   # 新建待删除的轮廓序号列表
c, row, col = hierarchy.shape
for i in range(row):
    if hierarchy[0, i, 2] > 0 or hierarchy[0, i, 3] > 0:  # 有父轮廓或子轮廓
    delete_list.append(i)

# 根据列表序号删除不符合要求的轮廓
contours = delet_contours(contours, delete_list)

print(len(contours), "contours left after hierarchy filter")
drawMyContours("contours after hierarchy filtering", image, contours, True)

筛选过后的轮廓如下图所示,左上角和右下角的云层与地面的带有空洞的轮廓都被删除了。

5.4 按长度筛选轮廓

尽管上一个步骤已经剔除了天空与地面背景轮廓,但仍然残留这一些细小的轮廓。下一步可以使用轮廓长度contours[i].size()来滤除过小或过大的轮廓:

 # 5.2使用轮廓长度滤波
min_size = 20
max_size = 500
delete_list = []
for i in range(len(contours)):
    if (cv2.arcLength(contours[i], True) < min_size) or (cv2.arcLength(contours[i], True) > max_size):
    delete_list.append(i)

# 根据列表序号删除不符合要求的轮廓
contours = delet_contours(contours, delete_list)

print(len(contours), "contours left after length filter")
drawMyContours("contours after length filtering", image, contours, False)

再次感觉越来越接近真理了:)

6.联通域分析


连通区域通常代表了场景中的某个物体。为了识别该物体,或将它与其他图像元素比较,需要对此区域进行测量,以提取部分特征。本节介绍opencv的形状描述子,用于描述连通区域的形状。OpenCV中用于形状描述的函数有很多。我们把其中几个用到上节提取到的区域。

(1)矩形框x, y, w, h = cv2.boundingRect()

在表示和定位图像中的区域方法中,边界框可能是最简洁的。它的定义是:能完整包含该形状的最小垂直矩形。返回值是方框左上角坐标以及方框的宽和高x,y,  w,  h.

(2)最小覆盖圆cv2.minEnclosingCircle()

最小覆盖圆通常用在只需要区域尺寸和位置的近似值的情况。

(3)多边形逼近cv2.approxPolyDP()

如果要更紧凑地表示区域的形状,可以采用多边形逼近。在创建时需要设置精度参数,表示形状与对应的简化多边形之间能接受的最大距离。它是cv::approxPolyDP(contours[1],poly,5,true)函数的第四个参数。返回结果是cv::Point类型的向量,表示多边形顶点个数。在画这个多边形时,要迭代遍历整个向量,并在顶点之间画直线,把它们逐个连接起来。

(4)凸包cv2.convexHll()

凸包是包含该形状的最小凸多边形。可以把它看作一条绕在区域周围的橡皮筋。在形状轮廓中凹进去的位置,凸包轮廓会与原始轮廓发生偏离。

下面使用上述分析方法中的凸包与最小覆盖矩形两种方法分别对2架火箭的提取轮廓进行分析。

# 6.形状描述子
# 6.1 最小覆盖矩形
result = image.copy()
x, y, w, h = cv2.boundingRect(contours[0])  #(x,y)为矩形左上角的坐标,(w,h)是矩形的宽和高
cv2.rectangle(result, (x, y), (x+w, y+h), (0, 255, 255), 1)

# 6.2 凸包
hull = cv2.convexHull(contours[1])
cv2.polylines(result, [hull], True, (0, 255, 0), 1)

得到的结果如下图所示:轮廓1为最小覆盖矩形(黄色线条),轮廓2为凸包(绿色线条)

7.标注轮廓重心

终于到最后一步了。下面先求2个轮廓的重心,然后使用cv::Circle()与cv::putText()函数将重心位置与坐标标注到画面上。

# 7.画重心
for i in range(len(contours)):
    mom = cv2.moments(contours[i])
    pt = (int(mom['m10'] / mom['m00']), int(mom['m01'] / mom['m00']))  # 使用前三个矩m00, m01和m10计算重心
    cv2.circle(result, pt, 2, (0, 0, 255), 2)  # 画红点
    text = "(" + str(pt[0]) + ", " + str(pt[1]) + ")"
    cv2.putText(result, text, (pt[0]+10, pt[1]+10), cv2.FONT_HERSHEY_PLAIN, 1.5, (255, 255, 255), 1, 8, 0);

cv2.imshow("center", result)
cv2.waitKey()

最终结果如下图所示。

参考链接:

https://docs.opencv.org/3.1.0/d9/d8b/tutorial_py_contours_hierarchy.html

https://docs.opencv.org/3.4.1/d3/dc0/group__imgproc__shape.html

本文更新链接:https://blog.csdn.net/iracer/article/details/90695914

转载请注明出处。


新书终于面市啦,《机器学习原理与编程实战》连接原理与实战:

https://blog.csdn.net/iracer/article/details/116051674?spm=1001.2014.3001.5501

© 版权声明
THE END
喜欢就支持一下吧
点赞261 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容