3. 相机模型与标定:针孔相机模型、内参矩阵、畸变模型、张正友标定法原理与实战

说到空间感知,相机就是我们的「眼睛」。但你知道吗?相机看到的图像,和我们人类感知到的三维世界之间,其实隔着一层「数学滤镜」。这层滤镜,就是相机模型。

我刚开始做三维重建时,总觉得标定是件很麻烦的事。直到有一次,我拿着一个没标定过的相机去拍标定板,结果重建出来的物体歪歪扭扭,像个喝醉了的雕塑。嗯,从那以后,我再也不敢跳过标定了。

这一章,我们就来彻底搞懂相机模型和标定。说白了,就是搞清楚两件事:相机是怎么成像的,以及怎么修正它的「近视眼」

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

你想想看,最早的相机其实就是个暗箱。光线从一个小孔射进来,在后面的感光面上形成倒像。这就是针孔相机模型的核心思想。

数学上,我们用四个坐标系来描述这个过程:

  • 世界坐标系:物体在真实世界中的位置 (Xw, Yw, Zw)
  • 相机坐标系:以相机光心为原点 (Xc, Yc, Zc)
  • 图像物理坐标系:在成像平面上 (x, y)
  • 图像像素坐标系:在图像上 (u, v)

从世界坐标到像素坐标,其实就是一连串的矩阵乘法。我习惯把它想象成一条流水线:

世界点 → 刚体变换(外参) → 相机点 → 投影(内参) → 像点 → 离散化 → 像素点

核心公式

s * [u, v, 1]^T = K * [R|t] * [Xw, Yw, Zw, 1]^T

其中 K 是内参矩阵,[R|t] 是外参矩阵,s 是尺度因子。

3.2 内参矩阵:相机的「身份证」

内参矩阵 K 长这样:

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

这里 fx, fy 是焦距(以像素为单位),cx, cy 是主点坐标(光轴与成像平面的交点)。

我在项目中遇到过一个问题:用手机拍标定板,结果 fx 和 fy 差了好几十个像素。一开始我以为是标定出错了,后来才发现——手机摄像头的光学模组本身就不是完美的圆形,两个方向的焦距有差异很正常。

我的经验:内参矩阵一旦标定好,只要不改变焦距(比如变焦或换镜头),就可以一直用。但如果你用的是自动对焦摄像头,每次对焦后内参都会变,需要重新标定。

3.3 畸变模型:为什么直线会变弯?

现实中的镜头不是完美的针孔。光线经过透镜时会发生折射,导致图像变形。这就是畸变。

畸变主要分两种:

  • 径向畸变:光线在透镜边缘弯曲得更厉害,产生「桶形」或「枕形」效果
  • 切向畸变:透镜与成像平面不平行,产生「倾斜」效果

数学上,我们用多项式来建模畸变:

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 是切向畸变系数。

避坑指南:我曾经用一张畸变很严重的广角镜头去标定,结果 k3 怎么都收敛不了。后来发现,对于普通镜头,只用 k1, k2 就够了。k3 主要是给鱼眼镜头用的。别盲目加参数,过拟合了反而不好。

3.4 张正友标定法:从棋盘格到相机参数

张正友标定法,说白了就是「拍一张棋盘格照片,然后解方程」。这个方法巧妙就巧妙在:它不需要你知道棋盘格在三维空间中的精确位置,只需要知道棋盘格是平面的就够了。

核心步骤是这样的:

  1. 拍标定板:从不同角度拍 10-20 张棋盘格照片
  2. 提取角点:用 OpenCV 找到棋盘格的内角点
  3. 计算单应性矩阵:每张照片对应一个单应性矩阵 H
  4. 求解内参:利用 H 的约束条件,解出内参矩阵 K
  5. 求解外参:有了 K,每张照片的外参 [R|t] 就迎刃而解
  6. 优化畸变:用非线性最小二乘法优化所有参数

为什么张正友标定法这么流行?

因为它只需要一个棋盘格,不需要精密的标定设备。你打印一张纸,贴在平板上,就能标定。我当年在学校做项目时,就是用 A4 纸打印的棋盘格,效果一样好。

3.5 实战:用 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)
    
    # 4. 查找棋盘格角点
    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)

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

print("内参矩阵:\n", mtx)
print("畸变系数:\n", dist)

我的建议:拍标定板时,注意几点:

  • 让棋盘格占满画面,但不要超出边界
  • 角度变化要大:俯仰、偏航、滚转都要有
  • 距离也要变化:近的、远的都要拍
  • 至少拍 10 张,20 张更稳

3.6 标定结果评估:你的标定准不准?

标定完了,怎么知道准不准?我一般看两个指标:

  • 重投影误差:把 3D 点投影回图像,看和实际角点的距离。一般小于 0.5 像素就算不错
  • 畸变矫正效果:把一张照片做畸变矫正,看直线是不是变直了
# 计算重投影误差
mean_error = 0
for i in range(len(objpoints)):
    imgpoints2, _ = cv2.projectPoints(objpoints[i], rvecs[i], tvecs[i], mtx, dist)
    error = cv2.norm(imgpoints[i], imgpoints2, cv2.NORM_L2) / len(imgpoints2)
    mean_error += error

print("平均重投影误差: {:.3f} 像素".format(mean_error / len(objpoints)))

# 畸变矫正
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)

我曾经踩过的坑:有一次标定结果重投影误差只有 0.1 像素,我高兴坏了。结果一矫正图像,发现边缘还是弯的。后来才发现,我拍的标定板照片全是在画面中心区域,边缘信息不够。记住:标定板一定要覆盖画面的各个角落,尤其是边缘。

3.7 本章知识体系

下面这张图,是我自己总结的相机标定知识体系。你可以把它当作一个「思维导图」来用:

相机标定 针孔相机模型 内参矩阵 K 畸变模型 世界→相机→图像→像素 外参 [R|t] 矩阵 fx, fy, cx, cy 焦距、主点 径向畸变 k1,k2,k3 切向畸变 p1,p2 张正友标定法 棋盘格角点提取 单应性矩阵求解 非线性优化 评估:重投影误差 < 0.5 像素

这张图把整个标定流程串起来了。从针孔模型出发,理解内参和畸变,然后用张正友法求解,最后用重投影误差评估。每一步都环环相扣。

好了,相机标定这块就讲到这里。记住一句话:标定是空间感知的基石。标定不准,后面的一切都是空中楼阁。我建议你动手跑一遍上面的代码,用自己的手机拍几张照片试试。实践出真知。


专注资料整理