相机模型与标定基础:针孔 vs 鱼眼、内参/外参/畸变、张正友法

做环视拼接,第一步就是把相机“摸透”。你连镜头怎么成像、图像怎么扭曲的都不清楚,后面做拼接肯定踩坑。这一章咱们就把相机模型和标定基础讲透。

针孔模型:最朴素的成像原理

针孔模型是相机成像的“理想国”。光线穿过一个小孔,在后面的感光面上倒立成像。说白了,就是初中物理的小孔成像实验。

数学上,它用一个 3×4 投影矩阵 描述:

s * [u, v, 1]^T = K * [R | t] * [X, Y, Z, 1]^T

这里面:

  • K 是内参矩阵(焦距、主点)
  • [R | t] 是外参(旋转+平移)
  • s 是尺度因子

内参矩阵长这样:

K = [fx,  0, cx;
      0, fy, cy;
      0,  0,  1]

fx, fy 是焦距(像素单位),cx, cy 是主点(光轴与像面交点)。

我的经验: 主点不一定在图像正中心。我遇到过一批工业相机,主点偏了十几个像素,直接用默认值做拼接,结果全景图对不齐。标定一下就好了。

鱼眼模型:为什么针孔不够用?

针孔模型假设光线直线传播。但鱼眼镜头为了看到 180° 甚至更大视野,故意让光线弯曲。你想想看,如果按针孔模型算,180° 视场角的像平面得无限大——这不可能。

所以鱼眼模型用 投影函数 代替直线投影。常见的有:

  • 等距投影:r = f * θ
  • 等立体角投影:r = 2f * sin(θ/2)
  • 正交投影:r = f * sin(θ)
  • 体视投影:r = 2f * tan(θ/2)

其中 θ 是入射光线与光轴的夹角,r 是像点到主点的距离。

市面上大部分鱼眼相机用的是 等距投影 或 等立体角投影。我拆过几款车载环视摄像头,基本都是等距模型。

注意: 不同厂家对鱼眼模型的实现有差异。标定时一定要确认相机使用的投影模型,否则矫正出来会变形。

畸变系数:现实总是不完美

即使有了投影模型,镜头加工和装配还会引入额外畸变。主要分两类:

畸变类型 产生原因 数学表示
径向畸变 透镜曲面不完美 k1, k2, k3(泰勒展开系数)
切向畸变 镜头与像面不平行 p1, p2

径向畸变让直线变弯——桶形畸变(负值)或枕形畸变(正值)。鱼眼镜头主要是桶形畸变,而且非常严重。

切向畸变相对小,但也不能忽略。我曾经调试一套环视系统,发现左右拼接缝总差几个像素,查了半天——是切向畸变没标好。

内参、外参、畸变系数:三者的关系

这三个概念容易搞混,我画个图帮你理清:

相机标定参数体系 内参 (Intrinsics) fx, fy, cx, cy 外参 (Extrinsics) R (旋转), t (平移) 畸变系数 k1,k2,k3,p1,p2 内参 + 畸变 → 描述“相机本身” 外参 → 描述“相机在空间中的位置” 标定板图像 角点提取 求解参数

简单说:内参 + 畸变 描述相机本身的光学特性,外参 描述相机在世界坐标系中的位姿。标定的目标就是同时求出这三组参数。

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

张正友在 1998 年提出的方法,到现在还是最主流的标定方案。它的核心思路是:

  1. 拍几张棋盘格照片(不同角度、不同位置)
  2. 提取角点(棋盘格内角点)
  3. 利用单应性矩阵 建立 3D 世界点与 2D 图像点的关系
  4. 闭式解 + 非线性优化 得到内参、外参、畸变系数

为什么它这么流行?因为:

  • 只需要一个打印的棋盘格,不需要精密设备
  • 算法稳定,OpenCV 直接封装好了
  • 对标定环境要求低,室内室外都能做
关键点: 张正友法假设棋盘格是平面(Z=0),所以至少需要 3 张不同角度的照片才能解出所有参数。我一般拍 15-20 张,覆盖视野各个区域。

标定流程实操:从拍照到出参数

我用 OpenCV 做标定的标准流程:

# 1. 准备棋盘格图像
# 2. 提取角点
ret, corners = cv2.findChessboardCorners(gray, (cols, rows), None)

# 3. 亚像素精炼
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
corners2 = cv2.cornerSubPix(gray, corners, (11,11), (-1,-1), criteria)

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

# 5. 矫正图像
dst = cv2.undistort(img, mtx, dist, None, newcameramtx)

这里面 objpoints 是棋盘格的世界坐标(Z=0),imgpoints 是对应的图像角点坐标。

避坑指南: 我曾经用 8x6 的棋盘格,但 OpenCV 默认角点数是 (7,5)——少了一行一列。结果标定出来的内参完全不对。检查一下你的棋盘格尺寸和代码里的参数是否一致。

标定质量评估:怎么看结果好不好?

标定完不能直接拿去用,得先看看质量。我一般看三个指标:

指标 正常范围 说明
重投影误差 < 0.5 像素 将标定点反投影回图像,计算与原始角点的距离
内参稳定性 多次标定差异 < 1% 换几组照片重新标定,看 fx, fy 是否稳定
矫正后直线度 肉眼观察无明显弯曲 用矫正后的图像看场景中的直线是否变直

重投影误差小于 0.3 像素算优秀,0.5 以内可接受。超过 1 像素就要检查标定板是否平整、照片是否模糊。

我的习惯: 标定完我会把棋盘格放在视野边缘再拍一张,用标定结果矫正,看边缘的格子是否变形。如果边缘格子还是歪的,说明标定板没覆盖到那个区域,需要补拍。

鱼眼标定的特殊之处

鱼眼镜头畸变大,直接用针孔模型标定会出问题。OpenCV 提供了专门的鱼眼标定接口:

# 鱼眼标定
ret, mtx, dist, rvecs, tvecs = cv2.fisheye.calibrate(
    objpoints, imgpoints, gray.shape[::-1], None, None)

# 鱼眼矫正
map1, map2 = cv2.fisheye.initUndistortRectifyMap(
    mtx, dist, np.eye(3), newcameramtx, size, cv2.CV_16SC2)
dst = cv2.remap(img, map1, map2, cv2.INTER_LINEAR)

区别在于:

  • 鱼眼模型的畸变系数是 4 个(k1,k2,k3,k4),不是针孔的 5 个
  • 投影模型不同,矫正映射的计算方式也不同
  • 鱼眼标定需要更多照片(我一般拍 25-30 张),覆盖更大的视场角范围

嗯,这里要注意:鱼眼标定对棋盘格在图像中的大小有要求。如果棋盘格太小(比如只占图像 5% 的区域),角点提取精度会下降,标定结果就不准了。