相机模型与标定:从理论到实战

相机标定这件事,说白了就是搞清楚相机到底是怎么看世界的。我刚开始做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位置,只需要知道格子大小就行。

基本流程是这样的:

  1. 打印一张棋盘格标定板,贴在平整的硬板上
  2. 从不同角度拍摄10-20张照片
  3. 检测每张照片中的角点
  4. 用闭式解估算内参和外参的初值
  5. 用非线性优化(比如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个不同的位姿,覆盖工作空间的主要区域。

知识体系总览

下面这张图概括了本章的核心内容:

相机模型与标定知识体系 相机模型 标定方法 手眼标定 针孔相机模型 世界→相机→图像→像素 内参矩阵 K 外参矩阵 [R|t] 畸变模型 径向畸变 k1,k2,k3 切向畸变 p1,p2 张正友标定法 棋盘格标定板 角点检测 闭式解+非线性优化 标定流程 拍照→角点检测→求解→优化 重投影误差评估 Eye-in-Hand 相机固定在机械臂末端 求解 A*X = X*B 适合精密装配 Eye-to-Hand 相机固定在工作台 求解 Y*A = B*Y 核心目标:建立2D像素与3D空间的精确映射关系 精度决定3D重建的上限,标定是3D视觉系统的基石

相机标定是整个3D视觉系统的基石。标定精度直接决定了后续3D重建、定位、测量的上限。我个人习惯在项目开始时先花一天时间把标定做扎实,后面调试起来会省很多事。

记住一句话:标定不是一次性的工作。相机镜头松动、温度变化、机械臂碰撞,都可能导致标定参数偏移。定期做标定校验,是保证系统长期稳定运行的关键。


专注资料整理