3. 相机标定原理:针孔相机模型、内参矩阵与外参矩阵、畸变模型(径向与切向)、张正友标定法

相机标定,说白了就是给相机做一次「视力矫正」。

你想想看,机器人要导航,得先看懂世界。但相机拍出来的图像,天生就有畸变——就像透过一个哈哈镜看东西。我刚开始做机器人项目时,就吃过这个亏。当时用了一个廉价USB摄像头做视觉SLAM,结果地图建出来全是歪的,机器人定位偏差十几厘米。后来才发现,相机根本没标定。

嗯,这节课我们就来彻底搞懂相机标定。

3.1 针孔相机模型:最朴素的成像原理

先聊最基础的模型——针孔相机。它其实就是一个暗箱,前面戳个小孔,光线穿过小孔,在后面的感光平面上成像。

为什么用针孔模型?因为它简单。真实世界的点 (X, Y, Z) 通过小孔,映射到成像平面上的 (x, y)。这个映射关系,就是透视投影。

数学上,它长这样:

x = f * (X / Z)
y = f * (Y / Z)

其中 f 是焦距,也就是小孔到成像平面的距离。注意,这里有个 Z 在分母上——这就是「近大远小」的根本原因。

核心要点:针孔模型是线性的,但真实相机不是。所以我们需要标定来修正偏差。

我在项目中遇到过一个问题:用针孔模型算出来的坐标,在近距离(1米以内)还挺准,但到了5米开外,误差就开始明显了。为什么?因为真实镜头不是针孔,它有厚度、有曲率。

3.2 内参矩阵:相机的「基因」

内参矩阵,描述的是相机内部的固有属性。说白了,就是相机自己长什么样——焦距、像素大小、主点位置。

它的标准形式是:

K = [fx  0  cx]
    [ 0  fy  cy]
    [ 0   0   1]

这里:

  • fx, fy:分别是x和y方向的焦距(单位是像素)。注意,它们不一定相等,因为像素可能不是正方形。
  • cx, cy:主点坐标,也就是光轴与成像平面的交点。理想情况下在图像中心,但实际会有偏移。

我建议你拿到一个新相机后,第一件事就是标定内参。我曾经用过一个工业相机,标定后发现主点偏移了整整15个像素——这意味着如果直接用理论中心点,所有测量都会偏。

个人经验:内参矩阵一旦标定好,只要不碰镜头(不拧、不摔),基本不会变。所以标定一次,存档备用。

3.3 外参矩阵:相机在哪儿?在看哪儿?

外参矩阵解决的是「相机坐标系」和「世界坐标系」之间的转换关系。它包含旋转和平移。

数学形式:

[R | t]

其中 R 是3x3的旋转矩阵,t 是3x1的平移向量。

举个例子:你的机器人底盘中心是世界坐标系原点,相机装在机器人头顶。那么外参矩阵就描述了相机相对于底盘的位置和朝向。

我记得有一次做AGV导航,机器人总是撞到左边的货架。排查了半天,发现是外参标定出了问题——相机安装时歪了2度,导致视觉定位系统以为机器人偏右,于是不断向左修正。嗯,2度偏差,在10米外就是35厘米的误差。

避坑指南:外参是会变的!只要相机被碰过、螺丝松动、或者机器人结构变形,外参就需要重新标定。我曾经因为机器人撞了一次墙,外参就偏了,结果整个导航系统都乱了。

3.4 畸变模型:镜头不是完美的

真实镜头有畸变。主要分两种:径向畸变和切向畸变。

3.4.1 径向畸变

径向畸变是镜头曲率造成的。越靠近图像边缘,畸变越明显。它又分两种:

  • 桶形畸变:图像向外凸出,像木桶。常见于广角镜头。
  • 枕形畸变:图像向内凹陷。常见于长焦镜头。

数学模型用泰勒级数展开:

x_corrected = x * (1 + k1*r^2 + k2*r^4 + k3*r^6)
y_corrected = y * (1 + k1*r^2 + k2*r^4 + k3*r^6)

其中 r 是像素到主点的距离,k1, k2, k3 是径向畸变系数。

我建议你至少用3个系数(k1, k2, k3)。有些库只用k1和k2,但对于大广角镜头(比如120度以上),k3是必须的。我在一个项目中用了鱼眼镜头,只标定k1和k2,结果边缘畸变根本矫正不过来。

3.4.2 切向畸变

切向畸变是镜头和成像平面不平行造成的。说白了,就是镜头装歪了。

数学模型:

x_corrected = x + [2*p1*x*y + p2*(r^2 + 2*x^2)]
y_corrected = y + [p1*(r^2 + 2*y^2) + 2*p2*x*y]

其中 p1, p2 是切向畸变系数。

重要提醒:切向畸变通常很小,但如果你的相机是手工组装的(比如一些科研级相机),切向畸变可能很大。我见过一个相机,切向畸变系数p1达到了0.05,矫正后图像边缘偏移了3个像素。

3.5 张正友标定法:最经典的标定方法

张正友标定法,是1998年提出的。到现在20多年了,依然是工业界最常用的方法。为什么?因为它只需要一个棋盘格,拍几张照片就行。

核心思路是这样的:

  1. 打印一张棋盘格,贴在平板上。
  2. 从不同角度拍10-20张照片。
  3. 算法自动检测棋盘格的角点。
  4. 利用角点的已知物理坐标和图像坐标,求解内参和外参。
  5. 再用非线性优化,把畸变系数也求出来。

具体步骤,我习惯用OpenCV来做:

import cv2
import numpy as np

# 准备棋盘格角点的物理坐标
# 假设棋盘格是9x6的内角点,每个格子30mm
pattern_size = (9, 6)
square_size = 30.0  # mm

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 = []  # 物理坐标
imgpoints = []  # 图像坐标

# 遍历图像
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)
        
        # 可视化角点
        cv2.drawChessboardCorners(img, pattern_size, corners, ret)

# 标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
    objpoints, imgpoints, gray.shape[::-1], None, None
)

print("内参矩阵:\n", mtx)
print("畸变系数:\n", dist)

个人经验:拍照时要注意几点:

  • 棋盘格要占图像面积的1/4到1/2,太小了角点检测不准。
  • 角度要多样:正对、左偏、右偏、上仰、下俯,都要有。
  • 距离也要变化:近的、远的,覆盖整个工作范围。
  • 不要只拍一个平面,否则解算会退化。

标定完成后,记得做一次验证。拿一张新拍的棋盘格照片,用标定结果去畸变,然后看看角点是否对齐。我一般要求重投影误差小于0.5像素。如果大于1个像素,说明标定质量不行,需要重拍。

避坑指南:我曾经犯过一个低级错误——棋盘格没贴平,有褶皱。结果标定出来的畸变系数全是错的。后来换了亚克力板,把棋盘格夹在中间,才搞定。记住:棋盘格必须绝对平整。

3.6 本章知识体系

下面这张图,帮你理清相机标定的核心逻辑:

相机标定知识体系 针孔相机模型 内参矩阵 外参矩阵 畸变模型 径向畸变 切向畸变 张正友标定法 棋盘格 角点检测 非线性优化 重投影误差 < 0.5px 核心目标:建立 3D 世界点到 2D 图像点的精确映射 输入:棋盘格多角度照片 → 输出:内参 + 外参 + 畸变系数 验证标准:重投影误差 < 0.5 像素

相机标定是整个3D视觉系统的基石。没有准确的标定,后面的立体匹配、点云生成、位姿估计,全都是空中楼阁。我建议你花时间把这一步做扎实——磨刀不误砍柴工。


公众号:蓝海资料掘金营,微信deep3321