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年,张正友教授提出了一个方法——用平面棋盘格做标定。这个方法到现在还是工业界的主流。为什么?因为它只需要拍几张棋盘格照片,不需要知道相机位姿,操作简单,精度够用。

核心思路分三步:

  1. 检测角点:在每张图像上找到棋盘格的亚像素角点坐标。
  2. 求解初值:利用单应性矩阵,闭式求解内参和外参的初始值。
  3. 非线性优化:用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 知识体系总览

这一章的内容,我用一张图串起来:

摄像头成像模型与内参标定知识体系 针孔模型 内参矩阵 K (fx, fy, cx, cy) 畸变模型 径向 k1,k2,k3 + 切向 p1,p2 张正友标定法 棋盘格 + 单应性 + 非线性优化 标定流程:拍摄棋盘格 → 角点检测 → 求解初值 → 非线性优化 → 畸变校正 OpenCV 标定实战 cv2.findChessboardCorners → cv2.calibrateCamera → cv2.undistort 输出:内参矩阵 + 畸变系数 用于后续去畸变、3D重建、多摄融合 评估指标:重投影误差 目标 < 0.3 像素,否则检查数据 核心原则:标定精度决定融合上限,内参不准,后面全白搭 建议每半年或更换镜头后重新标定一次

嗯,这张图基本把这一章的核心逻辑讲清楚了。从针孔模型到畸变模型,再到张正友标定法和OpenCV实战,每一步都是下一环的基础。

个人建议:初学者不要一上来就调OpenCV参数。先拿一个摄像头,拍20张棋盘格,跑通完整流程,看看重投影误差是多少。等你有感觉了,再去做多摄像头同步标定。

标定这件事,说白了就是「用已知的3D点,去反推摄像头的内部参数」。棋盘格上的角点坐标是已知的,图像上的像素坐标是检测出来的,两者之间的映射关系,就是我们要标定的东西。

我曾经在一个车载项目里,因为标定板没贴平,导致融合后的鸟瞰图在拼接缝处有1-2个像素的错位。后来重新标定,问题就解决了。所以,别小看这一步。