第二章 相机选型与标定实战:工业相机、镜头、滤光片选型,张正友标定法原理与OpenCV实现
做3D视觉系统集成,第一步不是写代码,而是选对硬件。我见过太多项目,算法调得再好,相机选错了,最后全白搭。说白了,相机选型就像给系统配眼镜——度数不对,看啥都模糊。
2.1 工业相机选型:别只看像素
很多人一上来就问「我要多少万像素的相机?」。嗯,像素确实重要,但绝不是唯一指标。我个人习惯,先问三个问题:
- 检测精度要求:你要看多小的特征?0.1mm还是0.01mm?
- 视野大小:一次要拍多大面积?
- 运动速度:物体是静止还是高速运动?
举个例子。我之前做锂电池极片检测项目,客户要求检测0.05mm的划痕。视野是50mm×50mm。算一下:
分辨率 = 视野 / 精度 = 50mm / 0.05mm = 1000像素。考虑到亚像素定位,实际需要200万像素以上。我最后选了500万像素的相机,留了余量。
除了像素,还有几个关键参数:
- 传感器类型:CMOS还是CCD?现在主流是CMOS,性价比高。但弱光环境CCD仍有优势。
- 帧率:运动物体需要高帧率。我做过一个项目,传送带速度2m/s,30fps根本不够,最后换了120fps的相机。
- 接口:USB3.0、GigE、Camera Link。GigE最常用,传输距离远。USB3.0速度快但线长受限。
2.2 镜头选型:焦距、光圈、畸变
镜头是相机的「眼睛」。选错了镜头,再好的相机也白搭。
焦距决定视野。焦距越短,视野越大,但畸变也越严重。计算公式:
焦距 = 工作距离 × 传感器尺寸 / 视野
比如工作距离200mm,传感器尺寸1/2英寸(6.4mm×4.8mm),视野100mm×75mm:
焦距 = 200 × 6.4 / 100 = 12.8mm
实际选12mm或16mm的镜头。
光圈影响进光量和景深。光圈越大(F值越小),进光越多,但景深越浅。做3D测量时,我一般用F8-F11,景深大,成像锐利。
畸变是3D测量的大敌。普通镜头畸变1%-3%,远心镜头可以做到0.1%以下。做高精度3D测量,我强烈建议用远心镜头。虽然贵,但值。
2.3 滤光片选型:别忽视光照
滤光片很多人不重视,其实它直接影响成像质量。我遇到过最头疼的问题——反光。金属表面、玻璃、塑料,反光能把图像毁掉。
滤光片的作用:
- 带通滤光片:只让特定波长的光通过。配合特定波长的光源,可以滤掉环境光干扰。
- 偏振片:消除反光。我做过一个手机玻璃盖板检测项目,不加偏振片,反光区域完全看不清缺陷。加了之后,效果立竿见影。
- 中性密度滤光片:均匀减光。光源太强时用。
2.4 张正友标定法原理
相机标定,说白了就是搞清楚相机内部参数(焦距、畸变系数)和外部参数(相机在世界坐标系中的位置)。张正友标定法是目前最主流的方法,用棋盘格就能搞定。
核心原理其实不复杂:
- 拍摄多张不同角度的棋盘格图像
- 检测棋盘格角点
- 利用角点的世界坐标和图像坐标,求解单应性矩阵
- 分解单应性矩阵,得到内参和外参
- 优化所有参数,最小化重投影误差
为什么需要多张不同角度的图像?因为单张图像只能提供有限约束。我一般拍15-20张,覆盖不同角度和位置。
标定结果包括:
- 内参矩阵:焦距、主点坐标
- 畸变系数:径向畸变(k1,k2,k3)、切向畸变(p1,p2)
- 重投影误差:评估标定质量,一般小于0.5像素就算不错
2.5 OpenCV实现:从理论到代码
理论讲完了,上代码。OpenCV的标定流程非常成熟,我直接贴核心代码:
import cv2
import numpy as np
import glob
# 棋盘格参数
CHECKERBOARD = (9, 6) # 内角点数量
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
# 准备世界坐标
objp = np.zeros((CHECKERBOARD[0]*CHECKERBOARD[1], 3), np.float32)
objp[:,:2] = np.mgrid[0:CHECKERBOARD[0], 0:CHECKERBOARD[1]].T.reshape(-1,2)
objpoints = [] # 世界坐标
imgpoints = [] # 图像坐标
images = glob.glob('calib_images/*.jpg')
for fname in images:
img = cv2.imread(fname)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 查找棋盘格角点
ret, corners = cv2.findChessboardCorners(gray, CHECKERBOARD, None)
if ret:
objpoints.append(objp)
corners2 = cv2.cornerSubPix(gray, corners, (11,11), (-1,-1), criteria)
imgpoints.append(corners2)
# 可视化
cv2.drawChessboardCorners(img, CHECKERBOARD, corners2, ret)
cv2.imshow('img', img)
cv2.waitKey(500)
cv2.destroyAllWindows()
# 标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
objpoints, imgpoints, gray.shape[::-1], None, None
)
print("内参矩阵:\n", mtx)
print("畸变系数:\n", dist)
print("重投影误差:", ret)
这段代码我用了很多年,基本没改过。有几个细节要注意:
- 角点检测:cv2.findChessboardCorners的第三个参数是内角点数量,不是格子数量。9×6的棋盘格,内角点是8×5。
- 亚像素优化:cv2.cornerSubPix让角点精度达到亚像素级别,标定结果更准。
- 图像数量:少于10张标定结果不稳定。我一般拍20张左右。
2.6 标定结果验证
标定完别急着用,先验证。我一般做两件事:
- 重投影误差检查:小于0.5像素算合格。如果大于1像素,说明标定有问题,重新拍。
- 实际测量验证:放一个已知尺寸的物体,用标定结果测量。误差在允许范围内才算通过。
# 畸变校正
img = cv2.imread('test.jpg')
h, w = img.shape[:2]
newcameramtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w,h), 1, (w,h))
dst = cv2.undistort(img, mtx, dist, None, newcameramtx)
# 裁剪有效区域
x, y, w, h = roi
dst = dst[y:y+h, x:x+w]
cv2.imshow('undistorted', dst)
cv2.waitKey(0)
这里有个小技巧:getOptimalNewCameraMatrix的alpha参数控制保留像素范围。alpha=1保留所有像素(边缘可能有黑边),alpha=0裁剪到最大有效区域。我一般用alpha=0.5,平衡视野和有效区域。
2.7 知识体系总览
下面这张图总结了本章的核心逻辑:
从图中可以看出,相机选型和标定是一个完整的闭环。硬件选型决定了图像质量的上限,标定则把硬件误差降到最低。两者缺一不可。
好了,这一章的内容就到这里。选型标定做扎实了,后面的3D重建、手眼标定才能顺利进行。记住一句话:标定做不好,算法再牛也白搭。