2. 摄像头成像模型与内参标定:针孔模型、畸变模型、张正友标定法、OpenCV标定实战
各位同学,欢迎来到多摄像头系统的第一道硬门槛——成像模型与内参标定。
说实话,我见过太多项目在融合阶段翻车,最后排查下来,根子都在标定上。两个摄像头各自的内参差几个像素,外参算得再准也没用。所以这一章,咱们把地基打牢。
2.1 针孔模型:摄像头在数学上长什么样?
摄像头成像,说白了就是一个「三维世界到二维平面」的投影过程。最经典的简化模型就是针孔模型。
你想想看,小孔成像的原理初中就学过——光线穿过一个小孔,在后面的平面上形成倒像。摄像头里的镜头组虽然复杂,但数学上我们把它等效成一个针孔。
核心公式(齐次坐标形式):
s * [u, v, 1]^T = K * [R|t] * [X, Y, Z, 1]^T
其中 K 就是内参矩阵,[R|t] 是外参。s 是尺度因子。
内参矩阵 K 长这样:
K = [fx, 0, cx;
0, fy, cy;
0, 0, 1]
fx, fy 是焦距(以像素为单位),cx, cy 是主点偏移。这四个参数,决定了空间点怎么映射到像素坐标。
我个人习惯把内参理解成「摄像头自身的出厂设置」。同一个摄像头,内参是固定的;换了镜头或者动了CMOS,就得重新标。
2.2 畸变模型:为什么直线会变弯?
理想很丰满,现实很骨感。针孔模型假设镜头是完美的,但实际镜头都有畸变。
我在项目中遇到过最典型的场景:用广角摄像头拍棋盘格,边缘的格子全弯了。如果不做畸变校正,后续的特征点匹配根本对不上。
畸变主要分两类:
| 畸变类型 | 产生原因 | 视觉效果 |
|---|---|---|
| 径向畸变 | 镜头曲率不完美 | 直线向外凸(桶形)或向内凹(枕形) |
| 切向畸变 | 镜头与CMOS不平行 | 图像倾斜、梯形失真 |
数学上,我们用多项式来建模畸变:
x_distorted = x * (1 + k1*r^2 + k2*r^4 + k3*r^6) + 2*p1*x*y + p2*(r^2 + 2*x^2)
y_distorted = y * (1 + k1*r^2 + k2*r^4 + k3*r^6) + p1*(r^2 + 2*y^2) + 2*p2*x*y
其中 k1, k2, k3 是径向畸变系数,p1, p2 是切向畸变系数。一共5个畸变参数。
注意:k3 通常只在鱼眼镜头或畸变很大的广角镜头中使用。普通镜头标定 k1, k2 就够了,强行加 k3 反而可能过拟合。
2.3 张正友标定法:为什么它是工业标准?
好了,模型有了,参数怎么求?
1998年,张正友教授提出了一个方法——用平面棋盘格做标定。这个方法到现在还是工业界的主流。为什么?因为它只需要拍几张棋盘格照片,不需要知道相机位姿,操作简单,精度够用。
核心思路分三步:
- 检测角点:在每张图像上找到棋盘格的亚像素角点坐标。
- 求解初值:利用单应性矩阵,闭式求解内参和外参的初始值。
- 非线性优化:用Levenberg-Marquardt算法,最小化重投影误差,精调所有参数。
我曾经在一个项目里,用张正友法标定6个摄像头,每个拍20张图,总共花了不到半小时。效果嘛,重投影误差控制在0.15像素以内,后续的融合拼接非常顺。
避坑指南:棋盘格不要用反光材质,打印时最好贴在平整的硬板上。我见过有人用普通A4纸贴墙上,结果照片一多纸皱了,角点检测全偏。
2.4 OpenCV标定实战:从代码到结果
理论讲完了,咱们直接上手。OpenCV的标定流程非常成熟,核心就几个函数。
先看代码骨架:
import cv2
import numpy as np
import glob
# 1. 准备棋盘格参数
CHECKERBOARD = (9, 6) # 内角点数量
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
# 2. 准备世界坐标系中的点
objp = np.zeros((CHECKERBOARD[0]*CHECKERBOARD[1], 3), np.float32)
objp[:,:2] = np.mgrid[0:CHECKERBOARD[0], 0:CHECKERBOARD[1]].T.reshape(-1,2)
objpoints = [] # 3D点
imgpoints = [] # 2D点
# 3. 遍历所有标定图像
images = glob.glob('calib_images/*.jpg')
for fname in images:
img = cv2.imread(fname)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, corners = cv2.findChessboardCorners(gray, CHECKERBOARD, None)
if ret:
objpoints.append(objp)
corners2 = cv2.cornerSubPix(gray, corners, (11,11), (-1,-1), criteria)
imgpoints.append(corners2)
# 4. 执行标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
objpoints, imgpoints, gray.shape[::-1], None, None)
# 5. 畸变校正
img = cv2.imread('test.jpg')
h, w = img.shape[:2]
newcameramtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w,h), 1, (w,h))
dst = cv2.undistort(img, mtx, dist, None, newcameramtx)
这段代码我用了很多年,基本没大改过。有几个关键点要留意:
- 棋盘格尺寸:9x6 指的是内角点数量,不是格子数量。实际棋盘格是10x7个格子。
- 图像数量:我建议至少拍15-20张,覆盖不同角度和距离。太少的话,标定结果不稳定。
- 重投影误差:标定完成后,可以用
cv2.projectPoints计算误差。一般0.1-0.3像素算正常,超过0.5就要检查数据了。
标定结果解读:
内参矩阵 mtx:
[[532.12, 0. , 324.56],
[ 0. , 531.89, 245.78],
[ 0. , 0. , 1. ]]
畸变系数 dist:
[[-0.283, 0.082, -0.001, 0.002, -0.021]]
fx≈532,说明焦距约532像素;cx≈324,主点略偏右。畸变系数k1为负,说明是桶形畸变。
2.5 知识体系总览
这一章的内容,我用一张图串起来:
嗯,这张图基本把这一章的核心逻辑讲清楚了。从针孔模型到畸变模型,再到张正友标定法和OpenCV实战,每一步都是下一环的基础。
个人建议:初学者不要一上来就调OpenCV参数。先拿一个摄像头,拍20张棋盘格,跑通完整流程,看看重投影误差是多少。等你有感觉了,再去做多摄像头同步标定。
标定这件事,说白了就是「用已知的3D点,去反推摄像头的内部参数」。棋盘格上的角点坐标是已知的,图像上的像素坐标是检测出来的,两者之间的映射关系,就是我们要标定的东西。
我曾经在一个车载项目里,因为标定板没贴平,导致融合后的鸟瞰图在拼接缝处有1-2个像素的错位。后来重新标定,问题就解决了。所以,别小看这一步。