第二章 相机选型与标定实战:工业相机、镜头、滤光片选型,张正友标定法原理与OpenCV实现

做3D视觉系统集成,第一步不是写代码,而是选对硬件。我见过太多项目,算法调得再好,相机选错了,最后全白搭。说白了,相机选型就像给系统配眼镜——度数不对,看啥都模糊。

2.1 工业相机选型:别只看像素

很多人一上来就问「我要多少万像素的相机?」。嗯,像素确实重要,但绝不是唯一指标。我个人习惯,先问三个问题:

  • 检测精度要求:你要看多小的特征?0.1mm还是0.01mm?
  • 视野大小:一次要拍多大面积?
  • 运动速度:物体是静止还是高速运动?

举个例子。我之前做锂电池极片检测项目,客户要求检测0.05mm的划痕。视野是50mm×50mm。算一下:

分辨率 = 视野 / 精度 = 50mm / 0.05mm = 1000像素。考虑到亚像素定位,实际需要200万像素以上。我最后选了500万像素的相机,留了余量。

我的经验:选型时像素数至少留20%余量。别卡着边界选,现场光照、震动都会影响实际效果。

除了像素,还有几个关键参数:

  • 传感器类型: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的镜头。

注意:镜头和相机要匹配!1/2英寸的传感器配1/2英寸的镜头。我曾经犯过这个错——1英寸镜头配1/3英寸传感器,画面边缘全是暗角。

光圈影响进光量和景深。光圈越大(F值越小),进光越多,但景深越浅。做3D测量时,我一般用F8-F11,景深大,成像锐利。

畸变是3D测量的大敌。普通镜头畸变1%-3%,远心镜头可以做到0.1%以下。做高精度3D测量,我强烈建议用远心镜头。虽然贵,但值。

2.3 滤光片选型:别忽视光照

滤光片很多人不重视,其实它直接影响成像质量。我遇到过最头疼的问题——反光。金属表面、玻璃、塑料,反光能把图像毁掉。

滤光片的作用:

  • 带通滤光片:只让特定波长的光通过。配合特定波长的光源,可以滤掉环境光干扰。
  • 偏振片:消除反光。我做过一个手机玻璃盖板检测项目,不加偏振片,反光区域完全看不清缺陷。加了之后,效果立竿见影。
  • 中性密度滤光片:均匀减光。光源太强时用。
避坑指南:我曾经在一条产线上,白天晚上图像亮度不一样。查了半天,发现是环境光干扰。加了带通滤光片+红外光源,问题解决。

2.4 张正友标定法原理

相机标定,说白了就是搞清楚相机内部参数(焦距、畸变系数)和外部参数(相机在世界坐标系中的位置)。张正友标定法是目前最主流的方法,用棋盘格就能搞定。

核心原理其实不复杂:

  1. 拍摄多张不同角度的棋盘格图像
  2. 检测棋盘格角点
  3. 利用角点的世界坐标和图像坐标,求解单应性矩阵
  4. 分解单应性矩阵,得到内参和外参
  5. 优化所有参数,最小化重投影误差

为什么需要多张不同角度的图像?因为单张图像只能提供有限约束。我一般拍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张左右。
常见错误:标定图像中棋盘格要占画面30%-70%。太小了角点检测不准,太大了边缘畸变信息不足。我习惯让棋盘格在画面中移动,覆盖中心和边缘。

2.6 标定结果验证

标定完别急着用,先验证。我一般做两件事:

  1. 重投影误差检查:小于0.5像素算合格。如果大于1像素,说明标定有问题,重新拍。
  2. 实际测量验证:放一个已知尺寸的物体,用标定结果测量。误差在允许范围内才算通过。
# 畸变校正
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,平衡视野和有效区域。

我的习惯:标定完成后,把内参和畸变系数保存到YAML文件。下次直接用,不用重新标定。除非更换镜头或相机。

2.7 知识体系总览

下面这张图总结了本章的核心逻辑:

相机选型与标定知识体系 相机选型与标定 硬件选型 工业相机 镜头 滤光片 张正友标定法 棋盘格检测 单应性矩阵 参数优化 OpenCV实现 角点检测 标定函数 畸变校正 输出:内参 + 畸变系数 + 重投影误差 用于3D测量、手眼标定、视觉定位

从图中可以看出,相机选型和标定是一个完整的闭环。硬件选型决定了图像质量的上限,标定则把硬件误差降到最低。两者缺一不可。

好了,这一章的内容就到这里。选型标定做扎实了,后面的3D重建、手眼标定才能顺利进行。记住一句话:标定做不好,算法再牛也白搭。


专注资料整理