3. 相机成像模型与标定:针孔模型、畸变校正、张正友标定法、相机内参矩阵
相机标定,说白了就是搞清楚相机到底是怎么「看」世界的。你想想看,一个三维空间里的点,怎么就能映射到二维图像上?中间经历了哪些变换?又引入了哪些误差?
我刚开始做视觉抓取时,总觉得标定是件麻烦事,随便用个默认参数就开干。结果呢?抓取精度惨不忍睹,机械臂总是差那么几毫米。后来我才明白——相机标定是整个视觉系统的地基,地基不稳,上层建筑全是空中楼阁。
3.1 针孔模型:最朴素的成像原理
针孔模型是最经典的相机成像模型。它假设光线沿直线传播,穿过一个小孔后在成像平面上形成倒立的像。
核心思想:三维世界中的点 P(X, Y, Z) 通过光心 O 投影到成像平面上的点 p(x, y)。
数学上,这个投影关系可以写成:
x = f * (X / Z)
y = f * (Y / Z)
其中 f 是焦距,也就是光心到成像平面的距离。
但实际相机不是针孔,而是用透镜来聚光。这就引出了一个问题——透镜会引入畸变。
3.2 畸变校正:让直线变回直线
实际相机拍出来的图像,边缘的直线往往是弯的。这就是畸变。畸变主要分两种:
| 畸变类型 | 表现 | 产生原因 |
|---|---|---|
| 径向畸变 | 直线向外或向内弯曲 | 透镜形状不完美 |
| 切向畸变 | 图像倾斜变形 | 透镜与成像平面不平行 |
我记得有一次在工厂调试,发现抓取精度总是偏,排查了半天才发现是相机畸变没校正。边缘的工件位置偏差达到了 5 个像素,换算成物理距离就是好几毫米。
畸变校正的数学模型是这样的:
// 径向畸变
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)
// 切向畸变
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]
其中 k1、k2、k3 是径向畸变系数,p1、p2 是切向畸变系数。这些系数就是我们要标定出来的。
我曾经踩过的坑:畸变系数不是越多越好。对于普通工业相机,用 k1、k2 两个径向系数就够了。加上 k3 反而容易过拟合,尤其是标定板图片不够多的时候。
3.3 相机内参矩阵:从世界到像素的「翻译官」
内参矩阵 K 是相机标定的核心输出。它描述了从相机坐标系到像素坐标系的映射关系。
K = [fx 0 cx]
[ 0 fy cy]
[ 0 0 1]
其中:
- fx, fy:焦距在 x 和 y 方向上的像素尺度
- cx, cy:光心在像素坐标系中的位置
为什么 fx 和 fy 可能不一样?因为像素不一定是正方形的。我见过一些工业相机,fx 和 fy 能差 5% 以上。
完整的投影过程是这样的:
1. 世界坐标系 → 相机坐标系(外参变换)
2. 相机坐标系 → 归一化平面(针孔投影)
3. 归一化平面 → 像素坐标系(内参矩阵)
我的习惯:拿到一个新相机,第一件事就是标定内参。把 fx、fy、cx、cy 记下来,贴在相机上。这样换相机时能快速定位问题。
3.4 张正友标定法:最实用的标定方案
张正友标定法是目前最主流的相机标定方法。它的核心思路是:用一张棋盘格标定板,从不同角度拍 10-20 张图片,然后通过数学优化求解内参和畸变系数。
为什么是棋盘格?因为棋盘格的角点容易检测,而且角点之间的物理距离是已知的。
标定流程如下:
- 准备标定板:打印一张棋盘格,贴在平整的硬板上
- 采集图片:从不同角度拍 15-20 张,覆盖视野的各个区域
- 角点检测:用 OpenCV 的 findChessboardCorners 检测角点
- 求解内参:用张正友算法求解内参矩阵和畸变系数
- 优化:用非线性最小二乘法优化所有参数
关键点:标定板要占图像面积的 1/4 到 1/3,太小了角点检测不准,太大了容易超出视野。
OpenCV 实现代码:
import cv2
import numpy as np
# 准备标定板的角点坐标
pattern_size = (9, 6) # 内角点数量
square_size = 0.025 # 方格边长,单位:米
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)
# 标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
objpoints, imgpoints, gray.shape[::-1], None, None
)
print("内参矩阵:\n", mtx)
print("畸变系数:\n", dist)
我曾经踩过的坑:标定板一定要平整!有一次我用了一张有点皱的打印纸,标定出来的内参偏差很大。后来换了亚克力板贴标定纸,效果立竿见影。
3.5 标定质量评估
标定完了怎么知道好不好?我一般看三个指标:
| 指标 | 好 | 一般 | 差 |
|---|---|---|---|
| 重投影误差 | < 0.3 像素 | 0.3 - 0.8 像素 | > 0.8 像素 |
| 图片覆盖 | 视野四角 + 中心 | 覆盖大部分区域 | 集中在中心 |
| 图片数量 | 15-20 张 | 10-15 张 | < 10 张 |
重投影误差是最直观的指标。它表示:我们把检测到的角点通过标定结果反投影回三维空间,再投影回图像,看和原始角点差了多少像素。
我的习惯:标定完成后,我会用 cv2.projectPoints 把标定板的角点投影到图像上,画出来看看。如果投影点和检测点重合得很好,说明标定没问题。
3.6 本章知识体系
下面这张图总结了相机成像模型与标定的核心逻辑:
嗯,到这里相机成像模型和标定的核心内容就讲完了。记住一句话:标定不是一次性工作,换镜头、调焦距、甚至温度变化都可能影响标定结果。我建议每次项目开始前都重新标定一次,花 10 分钟换来整个系统的精度保障,这笔账怎么算都划算。