相机标定原理:张正友标定法、内参矩阵与外参矩阵、畸变系数

相机标定,说白了就是给相机做一次「视力检查」。你得知道它的镜头到底是怎么看世界的,才能把拍歪的画面给掰正。我刚开始做图像处理那会儿,总觉得标定是个可有可无的步骤——直到有一次,我把一个没标定过的相机直接拿去测距,结果偏差大到离谱。嗯,从那以后,我再也不敢跳过这一步了。

为什么需要相机标定?

相机把三维世界投影到二维图像上,这个过程会丢失深度信息,还会引入各种变形。你想想看,一个正方形的棋盘格,拍出来可能变成梯形、桶形,甚至边缘还带点波浪。标定的目的,就是找到从「真实世界坐标」到「像素坐标」的映射关系,然后反过来把变形纠正回去。

核心目标:建立世界坐标系 → 相机坐标系 → 图像坐标系 → 像素坐标系 的完整映射链。

张正友标定法:为什么它成了行业标准?

张正友在1998年提出的这个方法,彻底改变了相机标定的玩法。在此之前,标定需要高精度的三维标定块,又贵又麻烦。张正友的方法只需要一个打印的棋盘格,拍几张不同角度的照片就行。

它的核心思路是这样的:

  1. 打印棋盘格——知道每个角点的物理坐标(假设Z=0)
  2. 从不同角度拍摄——至少3张,我建议拍10-20张
  3. 检测角点——找到每张图中棋盘格角点的像素坐标
  4. 求解单应性矩阵——每张图都有一个从物理平面到像素平面的映射
  5. 分解内参和外参——利用多个单应性矩阵的约束,解出相机内参
  6. 优化畸变系数——用非线性最小二乘法精调

我的经验:拍棋盘格时,一定要让棋盘覆盖画面的各个区域——四角、中心、边缘都要有。我曾经只拍了中心区域,结果边缘畸变校正得一塌糊涂。另外,棋盘格不要放得太远,占画面1/3到2/3比较合适。

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

内参矩阵描述了相机内部的几何特性。它只跟相机本身有关,跟你拍什么、怎么拍没关系。一旦标定好,只要不换镜头、不变焦距,内参就是固定的。

内参矩阵 K 的数学形式:

K = [fx  0  cx]
    [ 0  fy  cy]
    [ 0   0   1]
参数 含义 典型值(以1080p相机为例)
fx, fy 焦距(以像素为单位) 约 1000-2000
cx, cy 主点坐标(光轴与成像平面的交点) 约 (960, 540)

这里有个容易混淆的点:fx 和 fy 不是物理焦距 f,而是 f 除以像素尺寸。所以即使同一个镜头,换不同分辨率的传感器,fx 和 fy 也会变。

外参矩阵:相机在空间中的「位置和姿态」

外参矩阵描述的是相机相对于世界坐标系的旋转和平移。每次移动相机,外参都会变。它由两部分组成:

  • 旋转矩阵 R(3×3)——描述相机的朝向
  • 平移向量 t(3×1)——描述相机的位置

外参矩阵的齐次形式:

[R | t] = [r11 r12 r13 | tx]
         [r21 r22 r23 | ty]
         [r31 r32 r33 | tz]

注意:外参矩阵中的 R 是正交矩阵,它的逆等于它的转置。如果你标定出来的 R 不满足这个性质,说明标定结果有问题。我曾经遇到过因为棋盘格检测角点顺序错乱,导致 R 矩阵完全不对的情况——检查了三天才发现是角点排序的 bug。

畸变系数:镜头的光学「缺陷」

没有镜头是完美的。畸变主要分两类:

畸变类型 表现 数学模型
径向畸变 直线变弯,越靠近边缘越明显 k1, k2, k3(泰勒展开系数)
切向畸变 镜头与成像平面不平行导致的偏移 p1, p2

径向畸变又分两种:

  • 桶形畸变——画面中间向外鼓,常见于广角镜头
  • 枕形畸变——画面边缘向内凹,常见于长焦镜头

畸变校正的数学公式(以径向畸变为例):

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

其中 r 是像素点到主点的距离。你看,离中心越远,r 越大,畸变就越明显。

避坑指南:我曾经在标定一个鱼眼镜头时,只用了 k1 和 k2 两个参数,结果边缘校正效果很差。后来加上 k3,效果立竿见影。对于普通镜头,k1 和 k2 就够了;对于广角或鱼眼,一定要用 k3。另外,切向畸变 p1、p2 通常很小,如果标定结果里 p1、p2 特别大,说明你的镜头安装可能有问题。

完整标定流程:从拍到用

我一般按这个步骤走:

  1. 准备棋盘格——打印一张 9×6 或 10×7 的棋盘格,贴在硬纸板上
  2. 拍摄照片——从不同角度拍 15-20 张,确保棋盘格出现在画面的各个位置
  3. 检测角点——用 OpenCV 的 findChessboardCorners 函数
  4. 亚像素精化——用 cornerSubPix 提高角点精度
  5. 执行标定——调用 calibrateCamera 函数
  6. 评估结果——检查重投影误差,一般小于 0.5 像素就算合格
  7. 保存参数——把内参矩阵和畸变系数存成文件

下面是一个简单的标定代码片段:

import cv2
import numpy as np

# 准备棋盘格角点的物理坐标
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)

# 存储所有图片的角点
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)
        corners2 = cv2.cornerSubPix(gray, corners, (11,11), (-1,-1), 
                                     criteria)
        imgpoints.append(corners2)

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

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

知识结构总览

下面这张图把整个标定体系串起来了:

相机标定知识体系 棋盘格多角度图像 张正友标定法(单应性矩阵分解) 内参矩阵 K fx, fy, cx, cy 外参矩阵 [R|t] 旋转 + 平移 畸变系数 k1, k2, k3, p1, p2 畸变校正 & 图像质量调优 💡 关键约束 • 内参矩阵:相机固有属性,标定一次即可复用 • 外参矩阵:每次移动相机都需要重新标定 • 畸变系数:与镜头相关,更换镜头必须重新标定

标定结果怎么评估?

标定完了,怎么知道结果好不好?我最常用的指标是重投影误差:

  • 用标定得到的参数,把物理坐标投影回像素坐标
  • 计算投影坐标和实际检测到的角点坐标之间的欧氏距离
  • 对所有角点取平均

一般来说,重投影误差小于 0.5 像素就算不错。如果超过 1 个像素,我建议检查一下:

  • 棋盘格是不是贴得不平?
  • 照片是不是拍糊了?
  • 角点检测是不是有误匹配?

一个常见的坑:标定用的棋盘格尺寸必须精确测量。我见过有人直接用「打印出来的尺寸」当物理坐标,结果打印机缩放导致实际尺寸偏差了 2%。这个误差会直接传递到焦距和畸变系数里,最终影响校正精度。用游标卡尺量一下实际格子边长,花不了两分钟。

小结

相机标定是整个畸变校正系统的基石。张正友标定法用棋盘格和几张照片,就能解出内参、外参和畸变系数。内参矩阵描述相机本身的几何特性,外参矩阵描述相机在空间中的位姿,畸变系数则负责纠正镜头的光学缺陷。这三者配合,才能把一张歪歪扭扭的照片还原成它本该有的样子。