• python --opencv图像处理轮廓(寻找轮廓、绘制轮廓)详解


    什么是轮廓?

    轮廓是一系列相连的点组成的曲线,代表了物体的基本外形,相对于边缘,轮廓是连续的,边缘并不全部连续。

    寻找轮廓

    寻找轮廓 OpenCV 为我们提供了一个现成的函数 findContours()

    import cv2 as cv
    
    img = cv.imread("black.png")
    gray_img = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
    # 降噪
    ret, thresh = cv.threshold(gray_img, 127, 255, 0)
    # 寻找轮廓
    contours, hierarchy = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_NONE)
    
    print(len(contours[0]))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    这段代码先用 threshold() 对图像进行降噪处理,它的原型函数如下:

    retval, dst = cv.threshold(src, thresh, maxval, type[, dst] )
    
    • 1
    • dst:结果图像。
    • src:原图像。
    • thresh:当前阈值。
    • maxVal:最大阈值,一般为255。
    • type:阈值类型,可选值如下:

    enum ThresholdTypes {
    THRESH_BINARY = 0, # 大于阈值的部分被置为 255 ,小于部分被置为 0
    THRESH_BINARY_INV = 1, # 大于阈值部分被置为 0 ,小于部分被置为 255
    THRESH_TRUNC = 2, # 大于阈值部分被置为 threshold ,小于部分保持原样
    THRESH_TOZERO = 3, # 小于阈值部分被置为 0 ,大于部分保持不变
    THRESH_TOZERO_INV = 4, # 大于阈值部分被置为 0 ,小于部分保持不变
    THRESH_OTSU = 8, # 自动处理,图像自适应二值化,常用区间 [0,255] };

    查找轮廓使用的函数为 findContours() ,它的原型函数如下:

    cv2.findContours(image, mode, method[, contours[, hierarchy[, offset ]]])  
    
    • 1
    • image:源图像。
    • mode:表示轮廓检索模式。

    cv2.RETR_EXTERNAL 表示只检测外轮廓。
    cv2.RETR_LIST 检测的轮廓不建立等级关系。
    cv2.RETR_CCOMP建立两个等级的轮廓,上面的一层为外边界,里面的一层为内孔的边界信息。如果内孔内还有一个连通物体,这个物体的边界也在顶层。
    cv2.RETR_TREE 建立一个等级树结构的轮廓。

    • method:表示轮廓近似方法。

    cv2.CHAIN_APPROX_NONE 存储所有的轮廓点。
    cv2.CHAIN_APPROX_SIMPLE压缩水平方向,垂直方向,对角线方向的元素,只保留该方向的终点坐标,例如一个矩形轮廓只需4个点来保存轮廓信息。

    这里可以使用 print(len(contours[0])) 函数将包含的点的数量打印出来,比如在上面的示例中,使用参数 cv2.CHAIN_APPROX_NONE 轮廓点有 1382 个,而使用参数 cv2.CHAIN_APPROX_SIMPLE 则轮廓点只有 4 个。

    在这里插入图片描述

    绘制轮廓

    绘制轮廓使用到的 OpenCV 为我们提供的 drawContours() 这个函数,下面是它的三个简单的例子:

    # To draw all the contours in an image:
    cv2.drawContours(img, contours, -1, (0,255,0), 3)
    # To draw an individual contour, say 4th contour:
    cv2.drawContours(img, contours, 3, (0,255,0), 3)
    # But most of the time, below method will be useful:
    cnt = contours[4]
    cv2.drawContours(img, [cnt], 0, (0,255,0), 3)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    drawContours() 函数中有五个参数:

    • 第一个参数是源图像。
    • 第二个参数是应该包含轮廓的列表。
    • 第三个参数是列表索引,用来选择要绘制的轮廓,为-1时表示绘制所有轮廓。
    • 第四个参数是轮廓颜色。
    • 第五个参数是轮廓线的宽度,为 -1 时表示填充。

    我们接着前面的示例把使用 findContours() 找出来的轮廓绘制出来:

    import cv2 as cv
    
    img = cv.imread("black.png")
    gray_img = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
    cv.imshow("img", img)
    # 降噪
    ret, thresh = cv.threshold(gray_img, 127, 255, 0)
    # 寻找轮廓
    contours, hierarchy = cv.findContours(gray_img, cv.RETR_TREE, cv.CHAIN_APPROX_NONE)
    
    print(len(contours[0]))
    
    # 绘制绿色轮廓
    cv.drawContours(img, contours, -1, (0,255,0), 3)
    
    cv.imshow("draw", img)
    
    cv.waitKey(0)
    cv.destroyAllWindows()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    在这里插入图片描述

    特征矩

    特征矩可以帮助我们计算一些图像的特征,例如物体的质心,物体的面积等,使用的函数为 moments()

    在这里插入图片描述

    moments() 函数会将计算得到的矩以字典形式返回。

    import cv2 as cv
    
    img = cv.imread("number.png")
    
    gray_img = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
    # 降噪
    ret, thresh = cv.threshold(gray_img, 127, 255, 0)
    # 寻找轮廓
    contours, hierarchy = cv.findContours(gray_img, cv.RETR_TREE, cv.CHAIN_APPROX_NONE)
    
    cnt = contours[0]
    # 获取图像矩
    M = cv.moments(cnt)
    print(M)
    
    # 质心
    cx = int(M['m10'] / M['m00'])
    cy = int(M['m01'] / M['m00'])
    
    print(f'质心为:[{cx}, {cy}]')
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    这时,我们取得了这个图像的矩,矩 M 中包含了很多轮廓的特征信息,除了示例中展示的质心的计算,还有如 M[‘m00’] 表示轮廓面积。

    轮廓面积

    area = cv.contourArea(cnt)
    print(f'轮廓面积为:{area}')
    
    • 1
    • 2

    这里取到的轮廓面积和上面 M[‘m00’] 保持一致。

    轮廓周长

    perimeter = cv.arcLength(cnt, True)
    print(f'轮廓周长为:{perimeter}')
    
    • 1
    • 2

    参数 True 表示轮廓是否封闭,我们这里的轮廓是封闭的,所以这里写 True 。

    轮廓外接矩形

    轮廓外接矩形分为正矩形和最小矩形。使用 cv2.boundingRect(cnt) 来获取轮廓的外接正矩形,它不考虑物体的旋转,所以该矩形的面积一般不会最小;使用 cv.minAreaRect(cnt) 可以获取轮廓的外接最小矩形。

    在这里插入图片描述

    两者的区别如上图,绿线代表的是外接正矩形,红线代表的是外接最小矩形,代码如下:

    import cv2 as cv
    import numpy as np
    
    img = cv.imread("number.png")
    
    gray_img = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
    # 降噪
    ret, thresh = cv.threshold(gray_img, 127, 255, 0)
    # 寻找轮廓
    contours, hierarchy = cv.findContours(gray_img, cv.RETR_TREE, cv.CHAIN_APPROX_NONE)
    
    cnt = contours[0]
    
    # 外接正矩形
    x, y, w, h = cv.boundingRect(cnt)
    cv.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0), 2)
    
    # 外接最小矩形
    min_rect = cv.minAreaRect(cnt)
    print(min_rect)
    
    box = cv.boxPoints(min_rect)
    box = np.int0(box)
    cv.drawContours(img, [box], 0, (0, 0, 255), 2)
    
    cv.imshow("draw", img)
    
    cv.waitKey(0)
    cv.destroyAllWindows()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    boundingRect() 函数的返回值包含四个值,矩形框左上角的坐标 (x, y) 、宽度 w 和高度 h 。
    minAreaRect() 函数的返回值中还包含旋转信息,返回值信息为包括中心点坐标 (x,y) ,宽高 (w, h) 和旋转角度。

    轮廓近似

    根据我们指定的精度,它可以将轮廓形状近似为顶点数量较少的其他形状。它是由 Douglas-Peucker 算法实现的。

    OpenCV 提供的函数是 approxPolyDP(cnt, epsilon, True) ,第二个参数 epsilon 用于轮廓近似的精度,表示原始轮廓与其近似轮廓的最大距离,值越小,近似轮廓越拟合原轮廓。第三个参数指定近似轮廓是否是闭合的。具体用法如下:

    import cv2 as cv
    
    img = cv.imread("number.png")
    
    gray_img = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
    # 降噪
    ret, thresh = cv.threshold(gray_img, 127, 255, 0)
    # 寻找轮廓
    contours, hierarchy = cv.findContours(gray_img, cv.RETR_TREE, cv.CHAIN_APPROX_NONE)
    
    cnt = contours[0]
    
    # 计算 epsilon ,按照周长百分比进行计算,分别取周长 1% 和 10%
    epsilon_1 = 0.1 * cv.arcLength(cnt, True)
    epsilon_2 = 0.01 * cv.arcLength(cnt, True)
    
    # 进行多边形逼近
    approx_1 = cv.approxPolyDP(cnt, epsilon_1, True)
    approx_2 = cv.approxPolyDP(cnt, epsilon_2, True)
    
    # 画出多边形
    image_1 = cv.cvtColor(gray_img, cv.COLOR_GRAY2BGR)
    image_2 = cv.cvtColor(gray_img, cv.COLOR_GRAY2BGR)
    
    cv.polylines(image_1, [approx_1], True, (0, 0, 255), 2)
    cv.polylines(image_2, [approx_2], True, (0, 0, 255), 2)
    
    cv.imshow("image_1", image_1)
    cv.imshow("image_2", image_2)
    cv.waitKey(0)
    cv.destroyAllWindows()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    在这里插入图片描述
    第一张图是 epsilon 为原始轮廓周长的 10% 时的近似轮廓,第二张图中绿线就是 epsilon 为原始轮廓周长的 1% 时的近似轮廓。

    轮廓凸包

    凸包外观看起来与轮廓逼近相似,只不过它是物体最外层的「凸」多边形。

    如下图,红色的部分为手掌的凸包,双箭头部分表示凸缺陷(Convexity Defects),凸缺陷常用来进行手势识别等。

    在这里插入图片描述

    import cv2 as cv
    
    img = cv.imread("number.png")
    gray_img = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
    # 降噪
    ret, thresh = cv.threshold(gray_img, 127, 255, 0)
    # 寻找轮廓
    contours, hierarchy = cv.findContours(gray_img, cv.RETR_TREE, cv.CHAIN_APPROX_NONE)
    cnt = contours[0]
    # 绘制轮廓
    image = cv.cvtColor(gray_img, cv.COLOR_GRAY2BGR)
    cv.drawContours(image, contours, -1, (0, 0 , 255), 2)
    
    # 寻找凸包,得到凸包的角点
    hull = cv.convexHull(cnt)
    
    # 绘制凸包
    cv.polylines(image, [hull], True, (0, 255, 0), 2)
    
    cv.imshow("image", image)
    cv.waitKey(0)
    cv.destroyAllWindows()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    在这里插入图片描述
    还有一个函数,是可以用来判断图形是否凸形的:

    print(cv.isContourConvex(hull)) # True
    
    • 1

    它的返回值是 True 或者 False 。

    最小闭合圈

    接下来,使用函数 cv.minEnclosingCircle() 查找对象的圆周。它是一个以最小面积完全覆盖物体的圆。

    import cv2 as cv
    
    img = cv.imread("number.png")
    gray_img = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
    # 降噪
    ret, thresh = cv.threshold(gray_img, 127, 255, 0)
    # 寻找轮廓
    contours, hierarchy = cv.findContours(gray_img, cv.RETR_TREE, cv.CHAIN_APPROX_NONE)
    cnt = contours[0]
    
    # 绘制最小外接圆
    (x, y), radius = cv.minEnclosingCircle(cnt)
    center = (int(x), int(y))
    radius = int(radius)
    cv.circle(img, center, radius, (0, 255, 0), 2)
    
    cv.imshow("img", img)
    cv.waitKey(0)
    cv.destroyAllWindows()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    在这里插入图片描述

    下一个是把一个椭圆拟合到一个物体上。它返回内接椭圆的旋转矩形。

    ellipse = cv.fitEllipse(cnt)
    cv.ellipse(img, ellipse, (0, 255, 0), 2)
    
    • 1
    • 2

    在这里插入图片描述

  • 相关阅读:
    Spark - 第12章 弹性分布式数据集
    Java冒泡排序
    【LeetCode-中等题】39. 组合总和
    21天Python进阶学习计划
    【Javaweb】会话跟踪技术Cookie&Session
    文件上传与下载
    Postman如何做接口测试
    Java容器(arraylist+vector源码+stack)
    私仓库Nexus
    CSDN竞赛12期题解
  • 原文地址:https://blog.csdn.net/weixin_44634704/article/details/126489355