相机标定原理:张正友标定法、内参矩阵与外参矩阵、畸变系数
相机标定,说白了就是给相机做一次「视力检查」。你得知道它的镜头到底是怎么看世界的,才能把拍歪的画面给掰正。我刚开始做图像处理那会儿,总觉得标定是个可有可无的步骤——直到有一次,我把一个没标定过的相机直接拿去测距,结果偏差大到离谱。嗯,从那以后,我再也不敢跳过这一步了。
为什么需要相机标定?
相机把三维世界投影到二维图像上,这个过程会丢失深度信息,还会引入各种变形。你想想看,一个正方形的棋盘格,拍出来可能变成梯形、桶形,甚至边缘还带点波浪。标定的目的,就是找到从「真实世界坐标」到「像素坐标」的映射关系,然后反过来把变形纠正回去。
核心目标:建立世界坐标系 → 相机坐标系 → 图像坐标系 → 像素坐标系 的完整映射链。
张正友标定法:为什么它成了行业标准?
张正友在1998年提出的这个方法,彻底改变了相机标定的玩法。在此之前,标定需要高精度的三维标定块,又贵又麻烦。张正友的方法只需要一个打印的棋盘格,拍几张不同角度的照片就行。
它的核心思路是这样的:
- 打印棋盘格——知道每个角点的物理坐标(假设Z=0)
- 从不同角度拍摄——至少3张,我建议拍10-20张
- 检测角点——找到每张图中棋盘格角点的像素坐标
- 求解单应性矩阵——每张图都有一个从物理平面到像素平面的映射
- 分解内参和外参——利用多个单应性矩阵的约束,解出相机内参
- 优化畸变系数——用非线性最小二乘法精调
我的经验:拍棋盘格时,一定要让棋盘覆盖画面的各个区域——四角、中心、边缘都要有。我曾经只拍了中心区域,结果边缘畸变校正得一塌糊涂。另外,棋盘格不要放得太远,占画面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 特别大,说明你的镜头安装可能有问题。
完整标定流程:从拍到用
我一般按这个步骤走:
- 准备棋盘格——打印一张 9×6 或 10×7 的棋盘格,贴在硬纸板上
- 拍摄照片——从不同角度拍 15-20 张,确保棋盘格出现在画面的各个位置
- 检测角点——用 OpenCV 的
findChessboardCorners函数 - 亚像素精化——用
cornerSubPix提高角点精度 - 执行标定——调用
calibrateCamera函数 - 评估结果——检查重投影误差,一般小于 0.5 像素就算合格
- 保存参数——把内参矩阵和畸变系数存成文件
下面是一个简单的标定代码片段:
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)
知识结构总览
下面这张图把整个标定体系串起来了:
标定结果怎么评估?
标定完了,怎么知道结果好不好?我最常用的指标是重投影误差:
- 用标定得到的参数,把物理坐标投影回像素坐标
- 计算投影坐标和实际检测到的角点坐标之间的欧氏距离
- 对所有角点取平均
一般来说,重投影误差小于 0.5 像素就算不错。如果超过 1 个像素,我建议检查一下:
- 棋盘格是不是贴得不平?
- 照片是不是拍糊了?
- 角点检测是不是有误匹配?
一个常见的坑:标定用的棋盘格尺寸必须精确测量。我见过有人直接用「打印出来的尺寸」当物理坐标,结果打印机缩放导致实际尺寸偏差了 2%。这个误差会直接传递到焦距和畸变系数里,最终影响校正精度。用游标卡尺量一下实际格子边长,花不了两分钟。
小结
相机标定是整个畸变校正系统的基石。张正友标定法用棋盘格和几张照片,就能解出内参、外参和畸变系数。内参矩阵描述相机本身的几何特性,外参矩阵描述相机在空间中的位姿,畸变系数则负责纠正镜头的光学缺陷。这三者配合,才能把一张歪歪扭扭的照片还原成它本该有的样子。