图像处理中的梯度计算、边缘检测与凸包特征分析技术详解

发布于:2025-04-08 ⋅ 阅读:(40) ⋅ 点赞:(0)

前言

书接上文

OpenCV图像处理实战全解析:镜像、缩放、矫正、水印与降噪技术详解-CSDN博客文章浏览阅读1.1k次,点赞38次,收藏29次。本文系统解析OpenCV图像处理五大实战场景:镜像反转的三种坐标变换模式,图像缩放的尺寸控制与插值方法,透视变换的四点定位原理与三维投影消除,基于二值化模板的水印叠加技术,以及均值/高斯/中值滤波器的噪声消除机制。通过11个完整代码示例,详解关键参数设置与算法选择策略,涵盖flip、resize、warpPerspective、bitwise_and、GaussianBlur等核心函数应用,提供从基础操作到复杂形变矫正的全栈解决方案,为计算机视觉开发提供完备的技术参考。 https://blog.csdn.net/qq_58364361/article/details/146971848?spm=1011.2415.3001.10575&sharefrom=mp_manage_link


一、图像梯度处理

1.1 图像梯度

dx和dy是x和y的微分,趋向于无穷小,这是在连续的函数中求导的过程,可以认为这就是函数某一个点的梯度。

上面的连续的函数,但是实际中图像是离散的。如果图像也能微分,可以理解为图像的像素点无穷小,图像分辨率无穷大

理想情况下图像的数据组成也应该可以进行微分的,因此存在x轴和y轴的梯度,需要同时考虑两个方向

图像在计算机中是以离散的像素点分布的,因此使用的差分


1.2 边缘计算 Filter2D

梯度计算的目的是为了寻找图像的边缘,因此把寻找图像边缘的计算称为边缘计算,也是通过卷积核实现的,下面的是一个常用于边缘计算的卷积核:

上面的核用于提取图片的垂直边缘,下图是水平梯度的计算,来提取垂直边缘。

同理,改变卷积核形态,可以把图像的水平边缘提取,进行垂直梯度的计算:

可以看到相关参数如下:

ddepth 深度-1表示保持原图位深度(通常为8位无符号整型)

kernel 卷积核

可自定义核数值

edge_ex_method 边缘检测的方向

upright 垂直 level 水平

此参数在OpenCV函数接口中并不存在

import cv2
import numpy as np

if __name__ == '__main__':
    path = "QiPan.jpg"  # 图片文件路径
    img = cv2.imread(path)  # 读取图片,返回BGR格式的numpy数组

    #构建卷积核
    kernel = np.array(
            [
                    [-1, 0, 1],  # Sobel算子X方向卷积核
                    [-2, 0, 2],  # 用于边缘检测
                    [-1, 0, 1]
                    ],
            dtype=np.float32  # 指定数据类型为32位浮点数
            )
    #边缘计算
    dts_img = cv2.filter2D(img, -1, kernel)  # 使用卷积核进行图像滤波
    """
    cv2.filter2D参数说明:
    src: 输入图像
    ddepth: 输出图像深度(-1表示与输入相同)
    kernel: 卷积核
    """

    #显示图片
    cv2.imshow("dts_img", dts_img)  # 在窗口中显示处理后的图像
    cv2.waitKey(0)  # 等待任意按键按下


1.3 Sobel算子

Sobel算子是一种特殊的Filter2D,也是最常用的一种边缘检测算法,使用特定的卷积核进行计算。

  • 使用k1,对原始图像src进行卷积操作,得到水平方向的梯度图Gx

  • 使用k2,对原始图像src进行卷积操作,得到垂直方向的梯度图Gy

  • 支持适应Gx和Gy求出总梯度,这种用法比较严格,需要同时具有水平和垂直梯度的像素才会显示比较明显

dx,dy参数分别表示是否考虑x和y方向的梯度,不支持全零。

k1k2必须使用上述参数否则不是Sobel子。除了Sober算子外,也可以更改卷积核的权重比例形成其他的算子:

import cv2
import numpy as np

if __name__ == '__main__':
    path = "QiPan.jpg"  # 图片文件路径
    img = cv2.imread(path)  # 读取图片,返回BGR格式的numpy数组

    # 灰度图转换
    # 将BGR格式图像转换为灰度图
    img_h = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)  # 转换为灰度图

    # Sobel算子边缘检测
    """
    使用Sobel算子计算图像梯度
    参数:
        img_h: 输入灰度图像
        cv2.CV_16S: 输出图像深度(16位有符号整型)
        1: x方向上的差分阶数
        1: y方向上的差分阶数
        ksize=3: Sobel核大小
    返回:
        包含梯度值的图像(可能有负值)
    """
    img_sobel = cv2.Sobel(
            img_h,
            cv2.CV_16S,
            1,  # x方向上的差分阶数
            1,  # y方向上的差分阶数
            ksize=3  # Sobel算子大小
            )

    # 梯度绝对值处理
    img_j = np.abs(img_sobel)  # 取梯度绝对值,消除负值

    # 归一化处理
    max = np.max(img_j)  # 获取图像最大梯度值
    img_g = (img_j / max) * 255  # 归一化到0-255范围

    # 类型转换
    img_end = img_g.astype(np.uint8)  # 转换为8位无符号整型

    # 显示结果
    cv2.imshow("img_end", img_end)  # 显示处理后的图像
    cv2.waitKey(0)  # 等待用户按键


1.4 Laplacian(拉普拉斯)算子

上面算子都是一阶导数这些极值地方二阶倒数0因此梯度计算可以通过二阶倒数

推理过程如下

import cv2

if __name__ == '__main__':
    # 图片文件路径
    path = "QiPan.jpg"
    # 读取图片,返回BGR格式的numpy数组
    # @param path 图片路径
    # @return numpy.ndarray BGR格式的图像数组
    img = cv2.imread(path)

    # 拉普拉斯边缘检测
    # @param img 输入图像
    # @param ddepth 输出图像深度(-1表示与输入相同)
    # @return numpy.ndarray 边缘检测结果
    img_l = cv2.Laplacian(
            img,
            -1
            )
    # 显示处理后的图像
    # @param winname 窗口名称
    # @param mat 要显示的图像
    cv2.imshow("Laplacian", img_l)
    # 等待键盘输入
    # @param delay 延迟时间(毫秒),0表示无限等待
    cv2.waitKey(0)

拉普拉斯是二阶算子的典型代表,相比于一阶算子各有优缺点:

综上所述,一阶求导和二阶求导算法在图像梯度处理中各有优缺点,在实际应用中,应根据具体需求选择合适的方法。


二、图像边缘检测

上一章中仅仅进行梯度计算,如果要进行边缘检测需要继续优化流程

本实验使用的算法为Canny算法,此算法进行边缘检测被誉为最优方法。

Canny算法的输入图像应该为二值化图像,包括以下步骤:

1. 高斯滤波

2. 计算图像的梯度和方向

3. 非极大值抑制

4. 双阈值筛选


2.1 高斯滤波

边缘检测属于对噪点比较敏感的算法,因此需要降噪,对图像进行平滑处理,这里直接使用一个5*5的高斯核对图像降噪:

经过高斯滤波后,图下会变得更加模糊,边缘会变粗。


2.2 计算图像梯度与方向

内部使用的是Sobel算子来计算图像的梯度值,分为水平和垂直方向的核:

同时计算两个方向的梯度Gx和Gy,然后根据欧式距离(L2距离)来计算具体的梯度值:

根据三角函数,可以基于直角三角形的边长求出对应的角度:

这个角度θ就是总梯度放在哪个,按照以下规定制定角度:

根据上面的设定,可以得到每个边缘像素点所在的边缘方向:


2.3 非极大值抑制

之前使用高斯滤波会让画面的边缘变粗,但是变粗的同时边缘的也会变得模糊,因此这种图像经过梯度计算后,得到的边缘像素点会比原始图像更多,因此需要对这些像素点进行过滤。

非极大值抑制就是过滤边缘像素点的一种方法,可以让边缘变细。假设当前的像素点为(x, y),其梯度方向是0°,梯度值为G(x, y),需要比较G(x, y)与两个相邻像素点的梯度值:G(x, y-1)和G(x, y+1)。如果G(x, y)是这三个梯度值中最大的,就保留该梯度值,否则直接抑制为0。

经过了非极大值抑制之后,还需要经过双阈值筛选。如果阈值设置的太低,就会出现假边缘;如果阈值设置的太高,一些较弱的边缘就会被丢弃,因此适应双阈值再次筛选,需要设定高低阈值的比例。

双阈值对应的像素梯度值可以分为以下几种情况:

  • 强边缘 → A

如果像素的梯度值超过高阈值maxVal时,该像素必然是边缘像素。

  • 弱边缘

如果像素的梯度值低于高阈值maxVal且高于低阈值minVal时,分为以下两种情况:

(a) 如果该点能连接到一个强边缘点,该像素为边缘像素。→ C

(b) 如果该点不能连接到一个强边缘点,该像素不是边缘像素,被称为伪边缘点。 → B

  • 非边缘

如果像素的梯度值低于低阈值minVal时,该像素必然不是边缘点。

 

import cv2
import numpy as np

if __name__ == '__main__':
    # 1. 图片输入
    path = 'QiPan.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, 40, 255, cv2.THRESH_BINARY)

    # 4.高斯滤波(可能多次效果更好)
    no_noise_image = cv2.GaussianBlur(image_np_thresh, (7, 7), 0)

    # Canny算法: 5.计算梯度与方向 + 6. 非极大值抑制 + 7. 双阈值筛选
    edges_image = cv2.Canny(
            image_np_thresh,  # 源图像
            30,  # 低阈值
            70  # 高阈值
            )

    # 8. 图片输出
    cv2.imshow('edges_image', edges_image)
    cv2.waitKey(0)

三、凸包特征检测

3.1 凸包

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

假如在图像中有一些点:

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

可以看到整个物体所有的像素点均涵盖在这个凸多边形中。


3.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的点。

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

在实际图像处理中,通常会直接调用OpenCV的函数接口提取轮廓点并应用凸包算法,不需要手动实现上述过程。这些算法已经内置,可以更加高效。


3.3 Graham扫描

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

算法的主要步骤如下:

1. 寻找基点(P0)

通常选择y坐标最小的点,如果有多个这样的点,则选择x坐标更小的点作为基点P0,以P0为原点构建二维直角坐标系。

2. 计算剩下所有点相对P0的极角α(即该点与P0连线与x轴正方向的夹角)。

按照极角从小到大的顺序对点进行排序。如果极角相同,则比较与P0的距离,距离更近的排在前面。

3. 初始化一个栈来记录凸包点,把P0和P1入栈。

4. 扫描点集

把P2入栈,将栈顶的两个点相连(P1和P2),得到一条直线,观察下一个点P3在此直线的右侧还是左侧

  • 如果在右侧

说明P2不是凸包点,P2出栈,重新执行第四步(下一个点入栈)。

  • 如果在左侧或直线上

说明下一个点P3凸包点,P3入栈,重新执行第四步(下一个点入栈)。

5. 遍历完所有点,栈中剩下的点就是凸包的顶点。按照栈的顺序(从栈底到栈顶,即逆时针顺序)输出所有的凸包点。

Graham-24111张奥博https://docs.qq.com/doc/DTXJ2TWJuU2pPbkFL


3.4 QuickHull

在二维空间中,QuickHull用于计算凸包,采用分而治之的策略,通过递归将点集划分为更小的子集,并构建这些子集的凸包,如下所示:


3.5 Andrew扫描链

import cv2
import numpy as np

if __name__ == '__main__':
    # 1. 图片输入
    path = 'test.png'
    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, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
    # cv2.imshow('image_np_thresh', image_np_thresh)

    # 4. 寻找轮廓
    # 返回值1:所有轮廓的点坐标,是一个list列表
    # 返回值2:轮廓的层级关系
    contours, hierarchy = cv2.findContours(
        image_np_thresh,  # 二值化之后的图像
        cv2.RETR_EXTERNAL,  # 默认的轮廓查找方式
        cv2.CHAIN_APPROX_SIMPLE  # 默认的轮廓近似办法
    )
    print(len(contours))
    # print(contours)
    # print(hierarchy)
    # 拿到第一个轮廓的数据
    cnt = contours[0]
    print(cnt.shape)  # (296, 1, 2)表示由296个点组成,1占位,2表示xy坐标2维

    # 5. 查找凸包
    # 参数:当前轮廓的点数据(此处送入的是第一个轮廓)
    # 返回值:轮廓的凸包点
    hull = cv2.convexHull(cnt)
    print(hull.shape)  # (12, 1, 2)
    print(hull)

    # 6. 绘制轮廓
    image_np = cv2.polylines(
        image_np,  # 在哪个图像上画轮廓
        [hull],  # 凸包轮廓点的列表
        isClosed=True,  # TODO 是否闭合
        color=(0, 0, 255),  # 线段颜色
        thickness=2  # 线段粗度
    )

    # 7. 图片输出
    cv2.imshow('image_np',image_np)
    cv2.waitKey(0)

总结 

        本文系统介绍了图像处理中的梯度计算、边缘检测和凸包特征检测三大核心技术。首先,通过微分和差分理论解释了图像梯度的概念,并对比了Sobel算子和Laplacian算子在边缘检测中的优缺点。其次,详细解析了Canny算法的四步流程(高斯滤波、梯度计算、非极大值抑制、双阈值筛选),强调了其在边缘检测中的高效性。最后,深入探讨了凸包检测的三种经典算法(基础凸包检测、Graham扫描法、QuickHull法和Andrew扫描链法),结合OpenCV代码演示了实际应用。全文通过理论推导与代码实践相结合,为图像处理中的特征提取提供了全面的技术指导。