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] — 外参矩阵

这个公式贯穿了整个视觉定位的流程。你想想看,从世界坐标到像素坐标,中间经历了四次变换:

  1. 世界坐标系 → 相机坐标系:靠外参(旋转R + 平移t)
  2. 相机坐标系 → 归一化成像平面:透视投影(除以Z)
  3. 归一化平面 → 物理成像平面:加畸变(径向+切向)
  4. 物理成像平面 → 像素坐标系:靠内参(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 核心思想

  1. 假设标定板是平面(Z=0),建立单应性矩阵 H
  2. 利用 H 的约束条件,先解出内参的闭式解
  3. 用内参反推每张图片的外参
  4. 加入畸变模型,用非线性优化(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 本章知识体系

下面这张图把相机成像与标定的核心逻辑串起来了。你可以把它当作一个「标定前必看」的思维导图:

相机成像与标定知识体系 相机成像与标定知识体系 相机成像与标定 针孔相机模型 内参矩阵 K 外参 [R|t] 畸变参数 世界→相机坐标 透视投影 像素坐标映射 fx, fy cx, cy 旋转矩阵 R 平移向量 t 径向畸变 k1,k2,k3 切向畸变 p1,p2 张正友标定法 拍摄棋盘格 角点检测 求解内参+外参 非线性优化 核心:利用平面标定板的单应性约束,无需3D坐标信息

这张图把整个章节的知识点串成了一条线:从针孔模型出发,理解内参、外参、畸变三个参数组,然后用张正友标定法把它们解出来。每一步都有对应的数学原理和实操要点。

一句话总结本章: 相机标定就是求解从「世界坐标」到「像素坐标」的映射参数。内参和外参描述了几何映射,畸变参数修正了镜头缺陷。张正友标定法用平面棋盘格,把这个问题变成了一个可工程化求解的优化问题。