相机模型与标定:从理论到实战
相机标定这件事,说白了就是搞清楚相机到底是怎么看世界的。我刚开始做3D视觉时,觉得标定就是个黑盒子,跑个OpenCV函数就完事了。直到有一次项目里重建出来的点云全是扭曲的,排查了三天才发现是标定板没放平。嗯,从那以后我再也不敢小看这一步了。
针孔相机模型:最朴素的成像原理
针孔相机模型是最基础的成像模型。你想想看,光线通过一个小孔投射到感光平面上,就形成了图像。这个模型虽然简单,但足够描述大多数工业相机的成像过程。
数学上,它用四个坐标系来描述:世界坐标系、相机坐标系、图像坐标系和像素坐标系。从3D世界点到2D像素点,经历了一系列的刚体变换和投影变换。
核心公式:
s * [u, v, 1]^T = K * [R | t] * [X, Y, Z, 1]^T
其中K是内参矩阵,[R|t]是外参矩阵。s是尺度因子。
内参矩阵K长这样:
K = [fx, 0, cx;
0, fy, cy;
0, 0, 1]
fx、fy是焦距在像素单位下的表示,cx、cy是主点坐标。我在项目中遇到过,有些廉价工业相机的主点偏移特别大,如果不标定直接用理论值,重建误差能到厘米级。
畸变模型:现实总是不完美的
针孔模型是理想情况。实际镜头都有畸变,主要是径向畸变和切向畸变。
径向畸变:说白了就是镜头边缘的弯曲效果。桶形畸变让直线向外弯,枕形畸变让直线向内弯。我见过最夸张的一次,一个广角镜头在边缘处畸变超过了50个像素。
切向畸变:这通常是因为镜头和传感器没有完全平行。嗯,组装工艺的问题。
畸变模型通常用多项式来描述:
x_distorted = x * (1 + k1*r^2 + k2*r^4 + k3*r^6) + 2*p1*x*y + p2*(r^2 + 2*x^2)
y_distorted = y * (1 + k1*r^2 + k2*r^4 + k3*r^6) + p1*(r^2 + 2*y^2) + 2*p2*x*y
这里k1、k2、k3是径向畸变系数,p1、p2是切向畸变系数。r是点到主点的距离。
我的经验:标定时畸变系数不要给太多阶。一般k1、k2就够了,k3只在鱼眼镜头或大畸变镜头时用。给太多参数反而容易过拟合,标定结果不稳定。
张正友标定法:工业界的标准答案
张正友标定法是目前最主流的相机标定方法。它的核心思想是:用一张棋盘格标定板,从不同角度拍十几张照片,然后通过数学优化求解内外参。
为什么它这么流行?说白了就是操作简单、精度高。你不需要知道标定板的精确3D位置,只需要知道格子大小就行。
基本流程是这样的:
- 打印一张棋盘格标定板,贴在平整的硬板上
- 从不同角度拍摄10-20张照片
- 检测每张照片中的角点
- 用闭式解估算内参和外参的初值
- 用非线性优化(比如Levenberg-Marquardt)做全局优化
避坑指南:我曾经因为标定板没贴平,导致标定结果反复横跳。后来学乖了,用玻璃板加亚光纸,效果稳定很多。另外,照片要覆盖整个视场,别只拍中间区域。
OpenCV里实现起来很简单:
import cv2
import numpy as np
# 准备标定板参数
pattern_size = (9, 6) # 内角点数量
square_size = 0.025 # 格子边长,单位米
# 准备世界坐标系点
objp = np.zeros((pattern_size[0]*pattern_size[1], 3), np.float32)
objp[:, :2] = np.mgrid[0:pattern_size[0], 0:pattern_size[1]].T.reshape(-1, 2)
objp *= square_size
# 存储所有图片的角点
objpoints = [] # 3D点
imgpoints = [] # 2D点
# 遍历图片
for fname in image_files:
img = cv2.imread(fname)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, corners = cv2.findChessboardCorners(gray, pattern_size, None)
if ret:
objpoints.append(objp)
imgpoints.append(corners)
# 标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
objpoints, imgpoints, gray.shape[::-1], None, None
)
标定完成后,记得做重投影误差检查。一般误差小于0.5像素就算不错了。我见过有人标定出0.05像素的误差,那真是神仙操作。
手眼标定:让相机和机械臂协同工作
手眼标定解决的是相机坐标系和机械臂基坐标系之间的变换关系。根据相机安装位置不同,分为两种:
Eye-in-Hand(眼在手上)
相机固定在机械臂末端,跟着机械臂一起动。这种配置的好处是视野灵活,可以近距离观察目标。缺点是每次移动后都需要重新计算相机位姿。
数学上,Eye-in-Hand标定求解的是相机到机械臂末端的变换矩阵X:
A * X = X * B
其中A是机械臂末端的位姿变化,B是相机观测到的标定板位姿变化。
Eye-to-Hand(眼在手外)
相机固定在工作台上,机械臂在相机视野内运动。这种配置视野固定,适合全局监控。我做过一个焊接项目,用的就是Eye-to-Hand,因为需要实时观察整个焊接过程。
Eye-to-Hand标定求解的是相机到机械臂基座的变换矩阵Y:
Y * A = B * Y
两种方式的对比:
| 特性 | Eye-in-Hand | Eye-to-Hand |
|---|---|---|
| 视野范围 | 灵活,可近距离观察 | 固定,全局视野 |
| 标定复杂度 | 中等 | 中等 |
| 适用场景 | 精密装配、抓取 | 焊接、喷涂、检测 |
| 精度影响因素 | 机械臂重复定位精度 | 相机分辨率、标定板精度 |
手眼标定的代码实现,我习惯用OpenCV的calibrateHandEye函数:
import cv2
import numpy as np
# 假设我们已经有了机械臂位姿和相机观测位姿
# R_gripper2base, t_gripper2base: 机械臂末端到基座的旋转和平移
# R_target2cam, t_target2cam: 标定板到相机的旋转和平移
R_cam2gripper, t_cam2gripper = cv2.calibrateHandEye(
R_gripper2base, t_gripper2base,
R_target2cam, t_target2cam,
method=cv2.CALIB_HAND_EYE_TSAI
)
我的建议:手眼标定时,机械臂的运动范围要大一些,姿态变化要丰富。只平移不旋转,或者只旋转不平移,都会导致标定结果不稳定。我一般会让机械臂走20-30个不同的位姿,覆盖工作空间的主要区域。
知识体系总览
下面这张图概括了本章的核心内容:
相机标定是整个3D视觉系统的基石。标定精度直接决定了后续3D重建、定位、测量的上限。我个人习惯在项目开始时先花一天时间把标定做扎实,后面调试起来会省很多事。
记住一句话:标定不是一次性的工作。相机镜头松动、温度变化、机械臂碰撞,都可能导致标定参数偏移。定期做标定校验,是保证系统长期稳定运行的关键。