4. 相机成像与标定:针孔相机模型、内参/外参/畸变参数、张正友标定法实操
相机标定,说白了就是给机器人的眼睛配一副「度数准确的眼镜」。
我刚开始做视觉抓取时,总觉得标定是件麻烦事,随便拿个棋盘格拍几张就完事。结果机械臂抓一个矿泉水瓶,误差能偏出去两厘米。后来老老实实重新标定,精度直接提升了一个数量级。嗯,从那以后我再也不敢糊弄标定了。
4.1 针孔相机模型——机器人的眼睛怎么「看」世界
先理解一个最基础的问题:三维世界里的一个点,是怎么跑到二维图像上的?
针孔相机模型给出了答案。它假设光线从物体出发,穿过一个小孔,投射到成像平面上。这个模型虽然简单,但足够描述大多数工业相机的工作方式。
核心公式(齐次坐标形式):
s * [u, v, 1]^T = K * [R | t] * [X, Y, Z, 1]^T
其中:
s — 尺度因子(深度信息)
[u, v] — 像素坐标
[X, Y, Z] — 世界坐标
K — 内参矩阵
[R | t] — 外参矩阵
这个公式贯穿了整个视觉定位的流程。你想想看,从世界坐标到像素坐标,中间经历了四次变换:
- 世界坐标系 → 相机坐标系:靠外参(旋转R + 平移t)
- 相机坐标系 → 归一化成像平面:透视投影(除以Z)
- 归一化平面 → 物理成像平面:加畸变(径向+切向)
- 物理成像平面 → 像素坐标系:靠内参(fx, fy, cx, cy)
我个人习惯把内参理解为「相机自身的出厂参数」,外参理解为「相机放在哪、朝哪看」。这两个概念搞混了,后面标定一定会出问题。
4.2 内参、外参、畸变参数——三个必须搞清楚的参数组
| 参数类型 | 包含内容 | 物理含义 | 标定后是否变化 |
|---|---|---|---|
| 内参 | fx, fy, cx, cy | 焦距、光心偏移 | 固定(除非变焦) |
| 外参 | R(3×3), t(3×1) | 相机在世界中的位姿 | 每次移动都变 |
| 畸变参数 | k1, k2, k3, p1, p2 | 镜头径向/切向畸变 | 固定 |
我在项目中遇到过最坑的事:用变焦镜头做抓取,标定完内参后不小心碰了镜头变焦环,结果所有抓取点全偏了。所以工业场景下,我强烈建议用定焦镜头,或者标定完就把变焦环用胶带固定住。
4.2.1 内参矩阵 K 的细节
K = [fx, 0, cx
0, fy, cy
0, 0, 1]
fx, fy 是焦距在像素单位下的值,cx, cy 是光心在像素坐标系中的位置。理想情况下光心在图像正中心,但实际装配总有偏差。这个偏差如果不标定,你算出来的抓取位姿就会带系统性误差。
4.2.2 畸变模型
镜头不是完美的针孔,它会让图像产生变形。主要分两类:
- 径向畸变:光线在透镜边缘弯曲更厉害,造成「桶形」或「枕形」失真。用 k1, k2, k3 描述。
- 切向畸变:透镜与成像平面不平行,造成「梯形」失真。用 p1, p2 描述。
我的经验: 大多数工业镜头,k1 和 k2 就够用了。k3 一般用于鱼眼镜头。如果你用普通镜头强行标定 k3,反而可能过拟合,导致边缘区域校正过度。
4.3 张正友标定法——为什么它是工业界标配?
张正友在1998年提出的这个方法,解决了标定领域一个核心痛点:不需要知道标定板的三维坐标。
你只需要打印一张棋盘格,从不同角度拍十几张照片,算法就能自动解出内参、外参和畸变参数。这比传统方法(需要精密加工的3D标定块)方便太多了。
4.3.1 核心思想
- 假设标定板是平面(Z=0),建立单应性矩阵 H
- 利用 H 的约束条件,先解出内参的闭式解
- 用内参反推每张图片的外参
- 加入畸变模型,用非线性优化(Levenberg-Marquardt)做全局精调
为什么会这样设计?因为平面标定板容易制作,而且单应性矩阵提供了足够的几何约束。你想想看,一个平面上的四个点,就能确定一个单应性矩阵,而一个单应性矩阵又能给内参提供两个约束方程。三张以上图片,内参就超定可解了。
4.3.2 实操步骤(我用OpenCV的流程)
import cv2
import numpy as np
import glob
# 1. 准备棋盘格角点的世界坐标(假设Z=0)
pattern_size = (9, 6) # 内角点数
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)
# 2. 存储所有图片的角点
objpoints = [] # 世界坐标
imgpoints = [] # 像素坐标
images = glob.glob('calib_images/*.jpg')
for fname in images:
img = cv2.imread(fname)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 3. 查找棋盘格角点
ret, corners = cv2.findChessboardCorners(gray, pattern_size, None)
if ret:
objpoints.append(objp)
# 亚像素精度优化
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
corners2 = cv2.cornerSubPix(gray, corners, (11,11), (-1,-1), criteria)
imgpoints.append(corners2)
# 4. 执行标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
objpoints, imgpoints, gray.shape[::-1], None, None
)
print("内参矩阵:\n", mtx)
print("畸变系数:\n", dist)
我曾经踩过的坑:
- 棋盘格打印后没有贴在平整的硬板上,导致标定板本身有弯曲——标定出的畸变参数全是错的。
- 拍摄时棋盘格占画面比例太小(小于1/3),角点检测不稳定。
- 所有图片的棋盘格都朝向同一个方向——外参缺乏多样性,内参解算会退化。
4.4 标定实操的「黄金法则」
我标定过不下50台相机,总结了几条硬规矩:
| 项目 | 推荐值 | 原因 |
|---|---|---|
| 图片数量 | 15~25张 | 太少约束不足,太多边际效益递减 |
| 棋盘格姿态 | 倾斜角 < 45° | 太大角度角点检测容易失败 |
| 棋盘格占画面 | 1/3 ~ 2/3 | 太小精度差,太大边缘畸变影响大 |
| 光照 | 均匀、无高光 | 高光会让角点检测偏移 |
| 重投影误差 | < 0.5 pixel | 超过这个值说明标定质量有问题 |
快速检查标定质量的方法: 标定完成后,用 cv2.undistort() 校正一张图片,看看棋盘格的直线是否真的变直了。如果边缘的直线还是弯的,说明畸变参数没标对。
4.5 本章知识体系
下面这张图把相机成像与标定的核心逻辑串起来了。你可以把它当作一个「标定前必看」的思维导图:
这张图把整个章节的知识点串成了一条线:从针孔模型出发,理解内参、外参、畸变三个参数组,然后用张正友标定法把它们解出来。每一步都有对应的数学原理和实操要点。
一句话总结本章: 相机标定就是求解从「世界坐标」到「像素坐标」的映射参数。内参和外参描述了几何映射,畸变参数修正了镜头缺陷。张正友标定法用平面棋盘格,把这个问题变成了一个可工程化求解的优化问题。