一、相机内参标定原理:从针孔到畸变
大家好,我是老张。做传感器标定这行有十几年了。今天咱们聊聊相机内参标定。
说实话,很多人一上来就调算法、跑代码,结果标出来的参数根本不能用。为什么?因为没搞懂最基础的原理。今天我就把针孔模型、畸变模型、内参矩阵K这三个核心概念,掰开了揉碎了讲清楚。
1.1 针孔模型:最简单的相机
相机成像,说白了就是小孔成像。你想想看,一个暗箱,前面戳个小孔,光线穿过小孔在后面的感光元件上成像。这就是针孔模型。
但这里有个问题——小孔太小,进光量不够,成像太暗。所以实际相机都用透镜。不过没关系,数学上我们还是用针孔模型来近似。
我个人习惯把针孔模型拆成三步:
- 世界坐标系:物体在真实世界中的位置 (X, Y, Z)
- 相机坐标系:以相机光心为原点,Z轴指向镜头前方
- 图像坐标系:最终在像素平面上的位置 (u, v)
从世界到图像,中间有个投影关系。公式很简单:
u = f * X / Z + u0
v = f * Y / Z + v0
其中 f 是焦距,u0、v0 是光心在像素平面上的偏移。嗯,这里要注意,实际相机传感器不是完美的正方形,所以 f 在 u 和 v 方向上可能不一样。我们后面会讲到。
核心要点:针孔模型假设光线是直线传播的,没有畸变。但现实世界哪有这么完美?
1.2 畸变模型:现实总是不完美
我在项目中遇到过最头疼的问题,就是畸变。你标定出来的参数,如果没处理好畸变,那后面的三维重建、SLAM全都会飘。
畸变主要分两种:
径向畸变
说白了就是镜头边缘的「桶形」或「枕形」变形。越靠近图像边缘,变形越明显。为什么会这样?因为透镜的曲率不是完美的球面。
数学上用泰勒展开来近似:
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 是像素到光心的距离,k1、k2、k3 是径向畸变系数。一般用 k1、k2 就够了,k3 是给鱼眼镜头准备的。
我的经验:标定径向畸变时,棋盘格一定要拍到边缘。我曾经有个项目,只拍了中心区域,结果标出来的 k1、k2 全是错的,整个系统都偏了。
切向畸变
这个更隐蔽。它是因为镜头和传感器不平行造成的。你想想看,如果镜头装歪了,成像自然就歪了。
公式也不复杂:
x_distorted = x + [2*p1*x*y + p2*(r^2 + 2*x^2)]
y_distorted = y + [p1*(r^2 + 2*y^2) + 2*p2*x*y]
p1、p2 就是切向畸变系数。说实话,大部分情况下切向畸变很小,但如果你做高精度测量,绝对不能忽略。
避坑指南:我曾经有个项目,标定出来的重投影误差一直降不下去。折腾了两天,最后发现是镜头没拧紧,轻微晃动导致的切向畸变。所以,标定前先检查机械结构!
1.3 内参矩阵K:把一切装进矩阵
好了,现在我们把针孔模型和畸变模型整合起来。内参矩阵K就是干这个的。
K 矩阵长这样:
K = [fx, 0, cx
0, fy, cy
0, 0, 1]
其中:
- fx, fy:焦距在 u、v 方向上的像素尺度。注意,它们可能不一样!
- cx, cy:光心在像素平面上的偏移
为什么 fx 和 fy 可能不一样?因为传感器像素不是完美的正方形。我见过一些工业相机,像素是矩形的,fx 和 fy 能差 5%。
完整的投影公式就是:
s * [u, v, 1]^T = K * [R|t] * [X, Y, Z, 1]^T
其中 R 和 t 是外参,s 是尺度因子。这个公式就是整个相机标定的核心。
记住:内参矩阵K描述的是相机本身的属性,一旦标定好,只要不换镜头、不调焦距,K 就是固定的。外参 R、t 每次安装都会变。
1.4 标定流程:从理论到实践
说了这么多理论,咱们看看实际怎么做。我一般用张正友标定法,步骤很简单:
- 准备棋盘格:打印一张棋盘格,贴在平板上。格子大小要精确测量。
- 拍摄多张图片:从不同角度拍摄棋盘格。我建议至少 15-20 张,覆盖各个位置和角度。
- 提取角点:用 OpenCV 的 findChessboardCorners 提取棋盘格角点。
- 求解内参:用最小二乘法求解 K 矩阵和畸变系数。
- 优化:用 Levenberg-Marquardt 算法做非线性优化,最小化重投影误差。
代码示例(OpenCV Python):
import cv2
import numpy as np
# 准备棋盘格角点坐标
objp = np.zeros((6*9, 3), np.float32)
objp[:, :2] = np.mgrid[0:9, 0:6].T.reshape(-1, 2)
# 存储所有图片的角点
objpoints = [] # 世界坐标系
imgpoints = [] # 图像坐标系
# 遍历图片
for fname in images:
img = cv2.imread(fname)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, corners = cv2.findChessboardCorners(gray, (9, 6), None)
if ret:
objpoints.append(objp)
imgpoints.append(corners)
# 标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
objpoints, imgpoints, gray.shape[::-1], None, None
)
print("内参矩阵 K:\n", mtx)
print("畸变系数:\n", dist)
小技巧:标定完成后,一定要检查重投影误差。一般小于 0.5 像素就算合格。如果误差太大,检查一下棋盘格是否平整,或者图片数量是否足够。
1.5 知识体系总览
为了让你更直观地理解整个知识结构,我画了一张图:
这张图把整个知识体系串起来了。从针孔模型到畸变模型,再到内参矩阵K,最后落到标定流程上。你照着这个思路学,不会乱。
好了,这一章就到这里。记住,标定不是跑个代码就完事了。理解原理,才能用好工具。下一章我们聊聊外参标定,那个更刺激。