1 凸包

凸包就是将一张图片中物体的最外层的点连接起来构成的凸多边形,它能包含物体中所有的像素点。

假如在图像中有一些点:

经过凸包检测并绘制之后,应该得到的结果:

2 基础凸包检测算法

1.先找到最左边和最右边的点

2.连接上面的两个点,将点集分为上半区和下半区,以上半区为例:

3. 找到上半区中离直线最远的点。

假设直线的方程为:,则点(x0,y0)到直线的公式为:

4. 把最远的像素点与之前最左和最右的像素点连接,把这两条直线明明为y1和y2。

5. 求出y1和y2的直线方程,把上半区所有的点带入计算与y1和y2的距离(分正负):

d=0时,表示点在y1或y2上,可以忽略
d>0时,表示点在y1或y2上方,这表示需要更新凸包点
d<0时,表示点在y1和y2下方,忽略

6. 当上一步出现d>0的情况时,需要更新凸包点,重新连接,形成新的y2和y3:

7. 在新的y2和y3中重新反复计算新凸包点,直到上半区计算完成。

8. 下半区执行跟上半区相似的操作,但是判断条件改为寻找d<0的点。

上面的过程都是基于已知坐标点进行的处理,实际上对于未处理的图像,可能并不能直接获取点的坐标,特别是彩色图像,需要转换为二值化后的图像,并通过轮廓检测算法获取边界点的坐标。

3 其他检测凸包方法

以下方法,了解即可。

3.1  Graham扫描法

Graham扫描法是一种用于寻找二维平面上点集凸包的算法,也是OpenCV凸包检测的基本算法。其基本思想是先找一个基点,从这个基点出发,按照逆时针方向逐个寻找凸包点。

3.2 QuickHull法

在二维空间中,QuickHull用于计算凸包,采用分而治之的策略,通过递归将点集划分为更小的子集,并构建这些子集的凸包,用于OpenCV的大量计算加速。

3.3 Andrew扫描链法

4 代码实践

代码步骤:图片输入→灰度化→二值化→寻找轮廓→查找凸包→绘制轮廓→图像输出

4.1 寻找轮廓

轮廓指的是前景的轮廓,参数如下:

轮廓的查找方式:先使用默认参数,细节下一章讲解。

轮廓的近似办法:先使用默认参数,细节下一章讲解。

4.2 查找凸包

寻找凸包节点不提供参数

4.3 绘制轮廓

绘制轮廓实际上是绘制凸包的轮廓而不是前景的轮廓。

使用图片:tubao1.jpg

import cv2

if __name__ == '__main__':
    # 1. 图片输入
    path = 'tubao1.jpg'
    image_np = cv2.imread(path)
    # 2. 灰度化
    image_np_gray = cv2.cvtColor(image_np, cv2.COLOR_BGR2GRAY)

    # 3. 二值化
    ret, image_np_thresh = cv2.threshold(
        image_np_gray,  # 输入的灰度图像
        127,  # 阈值(127),但使用OTSU方法时会自动计算最佳阈值
        255,  # 最大值(255),当像素值超过阈值时赋予的值
        cv2.THRESH_BINARY + cv2.THRESH_OTSU
    )
    # cv2.THRESH_BINARY:二值化,大于阈值的设为最大值,其余设为0
    # cv2.THRESH_OTSU:使用大津算法自动确定最佳阈值
    print('thresh:', ret)
    # ret:实际使用的阈值
    # image_np_thresh:二值化后的图像

    # 4.寻找轮廓
    # 返回值1:轮廓的点坐标,使用array存储n个轮廓的信息
    # 返回值2:多个轮廓之间的层级关系
    contours, hierarchy = cv2.findContours(
        image_np_thresh,  # 二值化图像
        cv2.RETR_EXTERNAL,  # 轮廓的查找方式,cv2.RETR_EXTERNAL:只检测最外层轮廓
        cv2.CHAIN_APPROX_SIMPLE  # 轮廓近似办法,cv2.CHAIN_APPROX_SIMPLE:压缩水平、垂直和对角方向的元素,只保留端点
    )
    print(len(contours))  # 轮廓的数量,contours:轮廓的列表,每个轮廓是一个点集
    # print(contours)
    # print(hierarchy)

    # 拿到第一个轮廓的数据
    cnt = contours[0]  # 获取第一个轮廓
    # print(cnt)
    # 1394表示前景轮廓的像素点,1表示占位,2表示二维点
    print(cnt.shape)  # (1394, 1, 2)

    # 5. 查找凸包
    # 返回轮廓凸包点
    hull = cv2.convexHull(cnt)  # 参数也可以替换为其他的轮廓,cnt是第一个轮廓
    print(hull)
    print(hull.shape)  # 包含凸包点数量
    """
    cv2.convexHull()函数计算轮廓的凸包
    凸包是包含轮廓所有点的最小凸多边形
    参数:轮廓点集
    返回值:凸包的点集
    """

    # 6. 绘制轮廓
    # 绘制多条线
    cv2.polylines(
        image_np,  # 在哪个图上画线
        [hull],  # 绘制的轮廓列表
        isClosed=True,  # 轮廓是否封闭
        color=(0, 0, 255),  # 颜色
        thickness=2  # 粗细
    )
    """
    cv2.polylines()函数在图像上绘制多边形

    参数1:要绘制的图像
    参数2:要绘制的轮廓列表(需要是列表形式)
    参数3:是否闭合轮廓(True表示连接首尾点)
    参数4:颜色(BGR格式,这里是红色)
    参数5:线宽
    """

    # 7. 图片输出
    cv2.imshow('image_np', image_np)
    cv2.waitKey(0)
    cv2.imwrite('tubao1-1.png', image_np)
"""
代码逻辑总结:
        读取图像文件
        转换为灰度图像
        二值化处理,分离前景和背景
        在二值图像中查找轮廓
        计算第一个轮廓的凸包
        在原图上绘制凸包
        显示结果图像
"""

代码运行后结果:tubao1-1.png

练习:同时绘制tubao2的两个凸包轮廓,分别使用不同的颜色。

图片为:tubao2.jpg

"""同时绘制tubao2的两个凸包轮廓,分别使用不同的颜色。"""
import cv2

if __name__ == '__main__':
    # 1. 图片输入
    path = 'tubao2.jpg'
    image_np = cv2.imread(path)
    # 2. 灰度化
    image_np_gray = cv2.cvtColor(image_np, cv2.COLOR_BGR2GRAY)

    # 3. 二值化
    ret, image_np_thresh = cv2.threshold(
        image_np_gray,  # 输入的灰度图像
        127,  # 阈值(127),但使用OTSU方法时会自动计算最佳阈值
        255,  # 最大值(255),当像素值超过阈值时赋予的值
        cv2.THRESH_BINARY + cv2.THRESH_OTSU
    )
    # cv2.THRESH_BINARY:二值化,大于阈值的设为最大值,其余设为0
    # cv2.THRESH_OTSU:使用大津算法自动确定最佳阈值
    print('thresh:', ret)

    # 4.寻找轮廓
    # 返回值1:轮廓的点坐标,使用array存储n个轮廓的信息
    # 返回值2:多个轮廓之间的层级关系
    contours, hierarchy = cv2.findContours(
        image_np_thresh,  # 二值化图像
        cv2.RETR_EXTERNAL,  # 轮廓的查找方式,cv2.RETR_EXTERNAL:只检测最外层轮廓
        cv2.CHAIN_APPROX_SIMPLE  # 轮廓近似办法,cv2.CHAIN_APPROX_SIMPLE:压缩水平、垂直和对角方向的元素,只保留端点
    )
    print('找到的轮廓数量:', len(contours))  # 打印轮廓的数量
    # 轮廓的数量,contours:轮廓的列表,每个轮廓是一个点集

    # 5. 处理每个轮廓并绘制凸包
    # 定义不同颜色用于绘制不同轮廓的凸包
    colors = [
        (0, 0, 255),  # 红色 (BGR格式)
        (0, 255, 255),  # 黄色
        (0, 255, 0),  # 绿色
        (255, 0, 0),  # 蓝色
        (255, 0, 255),  # 粉色
        (255, 255, 0)  # 青色
    ]

    # 遍历所有找到的轮廓
    for i, cnt in enumerate(contours):
        # # 计算当前轮廓的面积,过滤掉太小的轮廓(可能是噪声)
        # area = cv2.contourArea(cnt)
        # if area < 100:  # 忽略面积小于100像素的轮廓
        #     continue
        """
        for循环:遍历contours列表中的所有轮廓
        enumerate(contours):同时获取轮廓的索引i和轮廓数据cnt    
        i:当前轮廓在列表中的索引(从0开始)    
        cnt:当前轮廓的点集数据(NumPy数组)
        
        cv2.contourArea(cnt):计算当前轮廓的面积
        这个函数接收一个轮廓点集作为参数,返回该轮廓所包围区域的面积        
        面积单位是像素,表示轮廓内部区域的大小
        """

        # 计算当前轮廓的凸包
        hull = cv2.convexHull(cnt)
        """
        cv2.convexHull()函数计算轮廓的凸包
        凸包是包含轮廓所有点的最小凸多边形
        参数:轮廓点集
        返回值:凸包的点集
        """
        # 选择颜色(循环使用预定义的颜色)
        # color = colors[i]  # 当i≥6时会报IndexError
        color = colors[i % len(colors)]  # 0%6=0...7%6=1(循环)

        # 6.绘制当前轮廓的凸包
        cv2.polylines(
            image_np,  # 在原图上绘制
            [hull],    # 要绘制的凸包点集
            isClosed=True,  # 闭合轮廓
            color=color,    # 使用选择的颜色
            thickness=2     # 线宽为2像素
        )
        """
        cv2.polylines()函数在图像上绘制多边形
    
        参数1:要绘制的图像
        参数2:要绘制的轮廓列表(需要是列表形式)
        参数3:是否闭合轮廓(True表示连接首尾点)
        参数4:颜色(BGR格式,这里是红色)
        参数5:线宽
        """

    # 7. 图片输出
    cv2.imshow('轮廓凸包检测结果', image_np)  # 显示处理后的图像
    cv2.waitKey(0)  # 等待按键,0表示无限等待
    cv2.destroyAllWindows()  # 关闭所有OpenCV窗口
    cv2.imwrite('tubao2-2.jpg', image_np)

运行结果:tubao2-2.jpg

练习:

给下面的图像绘制凸包轮廓,灰色是背景。

图片:2.jpg  (# 使用灰度化、二值化后,颜色浅的 无法选中)

"""给下面的图像绘制凸包轮廓,灰色是背景。"""  # 凸包特征检测
# 1.读取图像 → 2. 转换为灰度图 → 3. 二值化处理 (图像操作必要步骤)
# → 4. 查找轮廓 → 5. 绘制轮廓 → 6. 显示结果

import cv2
import numpy as np

if __name__ == '__main__':
    # 1.图片输入
    image_np = cv2.imread('2.jpg')

    # 使用灰度化、二值化后,颜色浅的 无法选中
    # 2.HSV空间转换
    hsv_image_np = cv2.cvtColor(image_np, cv2.COLOR_BGR2HSV)  # BGR转HSV

    # 3. 制作掩膜
    # 定义红色范围1
    red_low = np.array([0, 43, 46])  # 红色下限(Hmin, Smin, Vmin)
    red_high = np.array([10, 255, 255])  # 红色上限(Hmax, Smax, Vmax)

    mask1 = cv2.inRange(  # 创建掩膜
        hsv_image_np,  # 基于哪个图像, 输入HSV图像
        red_low,  # 颜色下限
        red_high  # 颜色上限
    )
    # 定义红色范围2
    red_low = np.array([156, 43, 46])  # 红色第二范围下限(深红色)
    red_high = np.array([180, 255, 255])  # 第二范围上限
    # 制作掩膜
    mask2 = cv2.inRange(  # 创建第二个掩膜
        hsv_image_np,
        red_low,
        red_high
    )

    # 定义绿色范围
    red_low = np.array([35, 43, 46])  # 红色第二范围下限(深红色)
    red_high = np.array([77, 255, 255])  # 第二范围上限
    # 制作掩膜
    mask3 = cv2.inRange(  # 创建第二个掩膜
        hsv_image_np,
        red_low,
        red_high
    )

    # 定义黄色范围
    red_low = np.array([26, 43, 46])  # 红色第二范围下限(深红色)
    red_high = np.array([34, 255, 255])  # 第二范围上限
    # 制作掩膜
    mask4 = cv2.inRange(  # 创建第二个掩膜
        hsv_image_np,
        red_low,
        red_high
    )

    # 定义紫色范围
    red_low = np.array([125, 43, 46])  # 红色第二范围下限(深红色)
    red_high = np.array([155, 255, 255])  # 第二范围上限
    # 制作掩膜
    mask5 = cv2.inRange(  # 创建第二个掩膜
        hsv_image_np,
        red_low,
        red_high
    )

    # 合并掩膜
    mask_image_np1 = cv2.bitwise_or(mask1, mask2)  # 红
    mask_image_np2 = cv2.bitwise_or(mask_image_np1, mask3)  # 红+绿
    mask_image_np3 = cv2.bitwise_or(mask_image_np2, mask4)  # 红+绿+黄
    mask_image_np4 = cv2.bitwise_or(mask_image_np3, mask5)  # 红+绿+黄+紫

    cv2.imshow('mask_image_np4', mask_image_np4)
    cv2.imwrite('mask_image_np4', mask_image_np4)

    # 4.寻找轮廓
    contours, hierarchy = cv2.findContours(  # cv2.findContours(): 查找图像中的轮廓
        mask_image_np4,
        cv2.RETR_EXTERNAL,  # 轮廓检索模式,只检测最外层轮廓
        cv2.CHAIN_APPROX_SIMPLE,  # 轮廓近似方法,压缩水平、垂直和对角线段,只保留它们的端点
    )
    print(len(contours))  # print(len(contours)):打印找到的轮廓数量

    # 5. 处理每个轮廓并绘制凸包
    # 定义不同颜色用于绘制不同轮廓的凸包
    colors = [  # 在cv2中,BGR格式
        (255, 255, 0),  # 青色
        (0, 0, 255),  # 红色
        (255, 0, 0),  # 蓝色
        (0, 255, 255),  # 黄色
        (0, 255, 0),  # 绿色
    ]

    # 遍历所有找到的轮廓
    for i, cnt in enumerate(contours):
        # 计算当前轮廓的面积,过滤掉太小的轮廓(可能是噪声)
        area = cv2.contourArea(cnt)
        if area < 100:  # 忽略面积小于100像素的轮廓
            continue

        # 计算当前轮廓的凸包
        hull = cv2.convexHull(cnt)

        # 选择颜色(循环使用预定义的颜色)
        color = colors[i]  # 当i≥6时会报IndexError
        # color = colors[i % len(colors)]  # 0%6=0...7%6=1(循环)

        # 6.绘制当前轮廓的凸包
        cv2.polylines(
            image_np,  # 在原图上绘制
            [hull],  # 要绘制的凸包点集
            isClosed=True,  # 闭合轮廓
            color=color,  # 使用选择的颜色
            thickness=2  # 线宽为2像素
        )
    # 7. 图片输出
    cv2.imshow('image_np', image_np)  # 显示处理后的图像
    cv2.waitKey(0)  # 等待按键,0表示无限等待
    cv2.destroyAllWindows()  # 关闭所有OpenCV窗口
    cv2.imwrite('color.png', image_np)

运行掩膜图像:mask_image_np4.png

运行结果:color.png

更多推荐