直线检测
直线检测在于检测轮廓图像中的点对齐。 直线检测的常用方法是霍夫变换 [Hough 1962]。 与傅里叶变换类似,它将图像从空间域转换到另一个空间, 在这个空间中,感兴趣的信息以不同的方式表示: 空间域中的直线被转换为霍夫空间中的点。
第一个参数化¶
在空间坐标系 中,一条直线由其系数 和 参数化, 方程为 。 因此,任何直线都可以由数对 唯一确定。 这就是霍夫的思想:图像中的每条直线都可以由霍夫空间 中的一个点来表示。 霍夫空间因此被称为参数空间。
Figure 1:霍夫变换将图像中的一条直线转换为参数空间中的一个点。
反过来,图像中的一个点在参数空间中表示为一条直线。
特别地,参数空间中的一条常数直线 对应于图像中横坐标为 的点。
如果图像中的几个点在一条直线上,它们在参数空间中各自对应的直线会相交于一点, 该点定义了连接它们的直线的方程。
在计算机上,参数空间是有限且离散的(它被称为“累加器”)。 因此,图像中的某些直线无法表示, 例如斜率为 的垂直线。 因此,在实践中需要使用另一种参数化方法。
新的参数化方法¶
为了避免前面提到的参数空间 的问题,直线由另外两个参数定义:
连接原点与直线上最近点的线段的距离 (该线段垂直于直线),
该线段与x轴的夹角 。
因此,图像中的每条直线都由一对参数 来参数化,这对应于参数空间 中的一个点。
Figure 5:霍夫变换的新参数化方法。
我们可以证明,对于图像中的每个点 ,在空间 中都关联着一个正弦曲线,其方程为 。 同一条直线上的点对应的正弦曲线在参数空间中相交于一个唯一的点。
霍夫变换算法如下:
对图像进行边缘检测
初始化累加器(作为参数的离散空间)
对于边缘中的每个像素:
确定与这些点对应的正弦曲线
沿着这条正弦曲线递增累加器
在累加器中搜索最大值:它们对应于图像中直线的参数
Figure 7 给出了一个在表示正方形的图像上进行霍夫变换的例子。
Figure 7:与左侧图像相关联的霍夫变换。 这些正弦曲线在四个(非常亮的)点相交,每个点都与带有相同字母的直线相关联。
Figure 8 给出了在真实照片上进行霍夫变换的例子。
Figure 8:与左侧图像相关联的霍夫变换。
此外,霍夫变换对噪声和遮挡具有鲁棒性(它可以检测部分被覆盖的物体)。
其他形状的霍夫变换¶
霍夫变换不仅限于直线,还可以用于检测任何具有数学参数化的形状:圆形、椭圆等。
要使用霍夫变换检测其他参数化对象, 必须根据数学参数化调整参数空间。 例如,一个圆形由三个参数(圆心坐标和半径)参数化, 那么相应的霍夫空间是三维的。
因此,由于累加器的维度等于参数的数量, 该方法的主要缺点是,对于某些形状,计算时间和使用的内存会迅速变得非常庞大。
代码示例¶
本示例使用霍夫变换在合成图像中检测直线。它演示了图像空间中的直线如何对应于霍夫空间中的峰值。
import numpy as np
from skimage.transform import hough_line, hough_line_peaks
from skimage.feature import canny
from skimage.draw import line
from skimage import data
import matplotlib.pyplot as plt
# 配置中文字体支持
plt.rcParams['font.sans-serif'] = [
'Noto Sans CJK SC', 'Noto Sans CJK JP', 'SimHei',
'Microsoft YaHei', 'WenQuanYi Micro Hei', 'DejaVu Sans'
]
plt.rcParams['axes.unicode_minus'] = False
# 构建一个合成图像
image = np.zeros((200, 200))
idx = np.arange(25, 175)
image[idx, idx] = 255
image[line(45, 25, 25, 175)] = 255
image[line(25, 135, 175, 155)] = 255
# 经典的直线霍夫变换
h, theta, d = hough_line(image)
# 绘图
fig, axes = plt.subplots(1, 3, figsize=(15, 6))
ax = axes.ravel()
ax[0].imshow(image, cmap='gray')
ax[0].set_title('输入图像')
ax[0].set_axis_off()
# 计算 theta 和距离轴的步长
# 用于为霍夫空间可视化设置适当的边界
angle_step = 0.5 * np.diff(theta).mean()
d_step = 0.5 * np.diff(d).mean()
# 定义 imshow extent 的边界:[左, 右, 下, 上]
# 将 theta 从弧度转换为度数并添加填充
bounds = [np.rad2deg(theta[0] - angle_step),
np.rad2deg(theta[-1] + angle_step),
d[-1] + d_step, d[0] - d_step]
ax[1].imshow(np.log(1 + h), extent=bounds, cmap='gray', aspect=1 / 1.5)
ax[1].set_title('霍夫变换')
ax[1].set_xlabel('角度 (度)')
ax[1].set_ylabel('距离 (像素)')
ax[1].axis('image')
ax[2].imshow(image, cmap='gray')
ax[2].set_ylim((image.shape[0], 0))
ax[2].set_axis_off()
ax[2].set_title('检测到的直线')
for _, angle, dist in zip(*hough_line_peaks(h, theta, d)):
(x0, y0) = dist * np.array([np.cos(angle), np.sin(angle)])
ax[2].axline((x0, y0), slope=np.tan(angle + np.pi/2))
plt.tight_layout()
plt.show()
