前言
书接上文
一、图像梯度处理
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方向的梯度,不支持全零。
k1和k2必须使用上述的参数,否则就不是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. 遍历完所有点,栈中剩下的点就是凸包的顶点。按照栈的顺序(从栈底到栈顶,即逆时针顺序)输出所有的凸包点。
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代码演示了实际应用。全文通过理论推导与代码实践相结合,为图像处理中的特征提取提供了全面的技术指导。