1. 为什么工业质检需要“卡尺法”?从人工到像素的进化
在工厂的生产线上,质检员拿着游标卡尺或千分尺,对着一个个零件反复测量,记录数据,这场景你一定不陌生。这种传统方式,精度依赖人的经验和手感,速度慢,还容易疲劳出错。当生产线速度越来越快,对精度要求达到微米甚至亚微米级时,人工测量就成了瓶颈。
这时候,机器视觉就该上场了。你可能听说过用OpenCV找轮廓、画矩形框来测尺寸,比如先找到零件的整个外轮廓,然后算最小外接矩形的宽和高。这个方法对付形状规则、背景干净的零件还行,但一遇到复杂情况就抓瞎。比如零件表面有反光、有油污、边缘本身是圆弧过渡而不是陡峭的直角,或者我们只想测量零件上某两个特定特征点(比如两个小孔的中心)之间的距离,而不是整个零件的尺寸。用找轮廓的方法,很可能连边缘都找不准,或者找到的不是你想要的那条边。
这就像你想量一下书本封面上两个印刷字之间的距离,结果却量了整个书本的宽度,完全不是一回事。卡尺法,就是为了解决这种“精准定位局部边缘”的需求而生的。它的思路非常直观:想象在图像上,你手动(或自动)画了一条测量线,就像把一把虚拟的卡尺放在了零件上。然后,程序只关心这条线上像素的灰度变化,通过分析这条“一维采样线”上的信号,精确找到边缘跳变的位置。这个方法不关心零件整体长啥样,只聚焦在你画的那条线上,因此抗干扰能力强,特别适合工业场景中结构化的、重复性的高精度测量任务。
我最早接触这个方法,是因为一个检测手机中框螺丝孔距的项目。客户要求孔心距的测量精度必须达到±0.01mm,用传统的轮廓法,受螺丝孔内螺纹和光照阴影的影响,结果波动很大。后来改用卡尺法,只分析两个孔边缘之间的连线,稳定性一下子就上来了。所以,如果你也在为类似的高精度、抗干扰的测量需求头疼,那卡尺法绝对值得你花时间掌握。
2. 卡尺法核心:把二维图像“压扁”成一维信号
卡尺法的精髓,在于一次降维打击。我们把图像上一条斜着的、弯曲的(理论上可以是任何路径)测量线,通过数学变换,“拉直”成一条水平的一维灰度信号曲线。这个过程,就好比用一把刀沿着测量线把图像“切”开,然后把切面展开铺平。
2.1 构建仿射变换矩阵:为测量线建立坐标系
实现这个“拉直”操作,核心是构建一个仿射变换矩阵。别被这个词吓到,你可以把它理解为一个“搬家说明书”,它告诉程序:原来图像(源空间)里测量线上的每一个点,应该对应到新的一维信号(目标空间)里的哪个位置。
具体来说,我们需要测量线的两个端点 p0(x0, y0) 和 p1(x1, y1)。我们的目标是建立一个新坐标系:
- X轴:沿着从
p0指向p1的方向。这是我们的一维采样方向。 - Y轴:垂直于这条线的方向。在我们的一维采样中,Y轴其实被“压缩”了,因为我们只取线上点的值,但数学上需要它来完整定义变换。
build_transform 函数就是干这个的。它计算了直线的方向向量 (dx, dy) 和长度。关键参数有两个,你只需要指定一个:
stride:采样步长(单位:像素)。比如设为0.5,就是每隔0.5个像素采一个样,这就是实现亚像素精度采样的关键!传统方法只能按整像素采样,而步长0.5意味着我们能在两个物理像素之间插值出新的点,精度直接翻倍。nsamples:你希望在一维信号中得到多少个采样点。程序会根据直线长度自动计算步长。
函数内部构建了一个3x3的齐次变换矩阵 H,然后提取出前两行构成2x3的仿射变换矩阵 A。这个矩阵 A 就包含了“如何从一维坐标映射回原始图像坐标”的所有信息。我们稍后会用到它的逆变换。
import numpy as np
def build_transform(p0, p1, stride=None, nsamples=None):
"""
构建沿测量线的仿射变换矩阵。
p0, p1: 测量线端点,格式 (x, y)
stride: 采样步长(像素),用于亚像素采样。与nsamples二选一。
nsamples: 期望的采样点总数。与stride二选一。
返回: (采样点数, 2x3仿射变换矩阵)
"""
x0, y0 = p0
x1, y1 = p1
dx = x1 - x0
dy = y1 - y0
length = np.hypot(dx, dy) # 直线长度
if nsamples is not None:
# 根据总采样数计算步长
factor = 1.0 / nsamples
stride = length / nsamples
else:
if stride is None:
stride = 1.0 # 默认整像素采样
factor = stride / length
nsamples = int(round(length / stride))
# 构建齐次变换矩阵 H
H = np.eye(3, dtype=np.float64)
# X轴单位向量 (沿直线方向)
H[0:2, 0] = (dx, dy)
# Y轴单位向量 (垂直于直线,旋转90度)
H[0:2, 1] = (-dy, dx)
# 缩放因子,控制采样密度
H[0:2, 0:2] *= factor
# 平移,将原点移到p0点
H[0:2, 2] = (x0, y0)
# 确保齐次坐标部分正确
assert np.allclose(H[2], [0, 0, 1])
# 提取仿射变换部分 (2x3)
A = H[0:2, :]
return (nsamples, A)
2.2 执行亚像素采样:获取灰度信号曲线
有了变换矩阵 A,我们就能用OpenCV的 warpAffine 函数来采样了。这里有个技巧:我们提供的是从目标空间(一维线) 到源空间(原始图像) 的变换矩阵 A,但 warpAffine 通常需要源到目标的变换。所以我们需要使用 cv.WARP_INVERSE_MAP 标志,告诉函数:“我给你的这个矩阵是逆变换,你直接用就行。”
dsize=(nsamples, 1) 这个参数设置非常巧妙。它告诉OpenCV,我们要创建一个宽度为 nsamples、高度为1的图像。warpAffine 会为这个“一行像素”的图像的每一个x坐标(从0到nsamples-1),利用变换矩阵 A 反向找到原始图像中对应的亚像素位置,并通过插值(比如


136

被折叠的 条评论
为什么被折叠?



