第二章 误差来源分析:光学系统、传感器与机械振动
做视觉检测这么多年,我最大的体会就是:精度不是算出来的,是抠出来的。你算法写得再漂亮,硬件上的误差没管好,最后出来的数据就是一堆废纸。
这一章,咱们就聊聊误差到底从哪来。说白了,就三大块:光学系统、传感器、机械振动。我一个个拆开讲,每个坑都是真金白银换来的经验。
核心观点:误差分析不是事后找补,而是设计阶段就要预判的事。你越早搞清楚误差来源,后面调试的时间就越短。
2.1 光学系统误差:你看到的不一定是真的
光学系统是视觉检测的第一道关卡。镜头畸变、照明不均、景深变化,这三个问题我几乎在每个项目里都遇到过。
2.1.1 镜头畸变
镜头畸变说白了就是图像变形。你拍一个标准的棋盘格,边缘的格子看起来是弯的。为什么会这样?因为镜头是物理透镜,光线穿过时天然会弯曲。
我记得有一次做手机中框尺寸检测,客户要求精度±0.02mm。我一开始没做畸变校正,结果边缘尺寸差了0.1mm。嗯,后来老老实实标定了一遍,才把误差压下来。
畸变主要分两种:
- 桶形畸变:图像中心向外膨胀,常见于广角镜头
- 枕形畸变:图像边缘向内收缩,常见于长焦镜头
怎么处理?用张正友标定法。OpenCV里直接有函数,我贴一段我常用的代码:
import cv2
import numpy as np
# 读取标定板图像
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
objp = np.zeros((9*6, 3), np.float32)
objp[:,:2] = np.mgrid[0:9, 0:6].T.reshape(-1, 2)
objpoints = [] # 世界坐标系中的点
imgpoints = [] # 图像坐标系中的点
for fname in calibration_images:
img = cv2.imread(fname)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, corners = cv2.findChessboardCorners(gray, (9, 6), None)
if ret:
objpoints.append(objp)
corners2 = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria)
imgpoints.append(corners2)
# 标定得到畸变系数
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, gray.shape[::-1], None, None)
# 去畸变
undistorted = cv2.undistort(img, mtx, dist, None, mtx)
我的经验:标定板最好用陶瓷材质的,热稳定性好。我见过有人用打印纸标定,温度一变,纸一缩,标定结果全废了。
2.1.2 照明不均
照明不均是个隐形杀手。你看着图像好像挺亮,但实际边缘的灰度值可能差了30个等级。这对边缘检测的影响非常大——同一个特征,左边测出来是100像素,右边测出来是102像素。
我建议用以下方法处理:
- 硬件层面:用同轴光源或环形光源,尽量让光均匀
- 软件层面:做背景校正,用高斯模糊提取照明分布,然后做差分
这里有个小技巧:
# 照明不均校正
def correct_illumination(image, kernel_size=101):
# 估计背景照明
background = cv2.GaussianBlur(image, (kernel_size, kernel_size), 0)
# 做差分校正
corrected = cv2.subtract(image, background)
# 归一化到0-255
corrected = cv2.normalize(corrected, None, 0, 255, cv2.NORM_MINMAX)
return corrected
注意:kernel_size一定要选奇数,而且要比特征尺寸大。我曾经设太小,把特征本身也给减掉了,查了半天才发现。
2.2 传感器误差:像素不是无限小的
传感器是视觉系统的眼睛。但眼睛也有近视的时候。传感器误差主要来自三个方面:像素尺寸、读出噪声、暗电流。
2.2.1 像素尺寸与分辨率
像素尺寸决定了你能看到多小的细节。举个例子,你用500万像素的相机,传感器尺寸1/2.5英寸,像素大小约2.2μm。理论上你能分辨的最小尺寸就是2.2μm——但这是理想情况。
实际上,根据奈奎斯特采样定理,你至少需要2个像素才能分辨一个特征。所以实际分辨率是像素尺寸的2倍。我习惯用这个公式估算:
实际分辨率 ≈ 像素尺寸 × 2.5(留点余量)
| 传感器类型 | 像素尺寸(μm) | 理论分辨率(μm) | 实际可用(μm) |
|---|---|---|---|
| 工业相机 5MP | 2.2 | 2.2 | 5.5 |
| 工业相机 12MP | 1.55 | 1.55 | 3.9 |
| 线阵相机 8k | 5.0 | 5.0 | 12.5 |
2.2.2 读出噪声与暗电流
读出噪声是传感器在读取电荷时引入的随机误差。暗电流是即使没有光照,传感器也会产生的微弱信号。这两个东西叠加起来,就是图像上的噪点。
我做过一个实验:在完全黑暗的环境下,用同一台相机拍100张图,取平均。结果发现平均灰度值不是0,而是12左右。这就是暗电流的贡献。
处理办法:
- 暗场校正:拍一张全黑图,作为背景减掉
- 多帧平均:拍多张图取平均,噪声会按√N的比例降低
- 选择合适的增益:增益越高,噪声越大。我一般控制在增益≤10dB
避坑指南:我曾经为了省成本,用了消费级CMOS做检测。结果一到夏天,温度升高,暗电流翻倍,检测结果直接漂移。后来换了工业级传感器,问题才解决。温度每升高10°C,暗电流大约翻一倍,这个要记住。
2.3 机械振动误差:看不见的抖动最致命
机械振动是视觉检测里最容易被忽略的误差来源。你想想看,相机固定在一个支架上,旁边有台电机在转,振动通过地面传到相机——图像就模糊了。
2.3.1 振动对测量的影响
振动会导致两种问题:
- 运动模糊:曝光时间内,物体或相机发生了位移
- 位置抖动:不同帧之间,特征点的位置随机变化
我遇到过最夸张的一次,是在一个冲压车间做检测。冲床一启动,整个地面都在抖。我用激光干涉仪测了一下,相机安装位置的振幅达到了0.05mm。而我的检测精度要求是0.01mm——这根本没法做。
2.3.2 振动频率与曝光时间的关系
这里有个关键公式:
允许的最大曝光时间 = 允许的模糊像素 / (振动速度 × 放大倍率)
举个例子:
- 允许模糊:1个像素
- 振动速度:0.1 mm/s
- 放大倍率:10 μm/像素
- 最大曝光时间 = 1 / (0.1 / 0.01) = 0.1秒
也就是说,曝光时间超过100ms,图像就会模糊超过1个像素。
我的建议:如果振动无法避免,就用短曝光+强光源。我一般把曝光时间控制在10ms以内,配合高功率LED频闪灯。这样振动的影响可以忽略不计。
2.3.3 振动抑制的实用方法
我总结了几条实战经验:
- 机械隔振:相机支架用橡胶垫或气浮平台隔离
- 触发同步:在振动最小的时刻触发拍照(比如冲床的静止区间)
- 软件补偿:用加速度传感器测量振动,在图像上做反卷积补偿
- 多次测量取平均:随机振动误差可以通过多次测量降低
这里有个简单的振动检测代码:
import numpy as np
def detect_vibration(image1, image2):
"""检测两帧之间的振动位移"""
# 计算相位相关
f1 = np.fft.fft2(image1)
f2 = np.fft.fft2(image2)
cross_power = (f1 * np.conj(f2)) / np.abs(f1 * np.conj(f2) + 1e-10)
phase_corr = np.fft.ifft2(cross_power)
# 找到峰值位置
h, w = phase_corr.shape
peak = np.unravel_index(np.argmax(np.abs(phase_corr)), (h, w))
# 计算位移
dx = peak[1] - w//2
dy = peak[0] - h//2
return dx, dy
注意:相位相关法对光照变化不敏感,但对图像内容有要求。如果画面里全是均匀区域,这个方法会失效。我一般会在视场里放一个参考标记点。
小结
这一章咱们聊了三大误差来源:光学系统、传感器、机械振动。每个来源都有它的特点和处理方法。我个人觉得,误差分析最核心的思路就是:先识别,再量化,最后补偿。你不可能消除所有误差,但你可以把误差控制在可接受的范围内。
下一章咱们会讲怎么把这些误差量化成具体的数值,以及怎么用统计方法评估系统的整体精度。嗯,到时候见。
公众号:蓝海资料掘金营,微信deep3321