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

相机标定,说白了就是给相机做一次「体检」。你得知道它的脾气秉性,才能让它拍出来的照片准确反映三维世界。我刚开始做三维扫描那会儿,总觉得标定是浪费时间,直接上手扫描,结果重建出来的模型歪七扭八。后来才明白——没有标定,后面的所有算法都是空中楼阁。

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

先聊聊针孔相机模型。这是所有相机模型的基础,虽然简单,但够用。

想象一个密闭的盒子,前面戳一个小孔,光线穿过小孔,在后面的感光面上成像。这就是针孔相机。现实中我们用透镜代替小孔,但数学上,我们依然用针孔模型来近似。

为什么?因为透镜太复杂了,而针孔模型足够简洁,能解释大部分成像行为。

核心公式:

三维空间点 (X, Y, Z) 映射到图像平面 (u, v) 的关系:

u = f * X / Z + u0
v = f * Y / Z + v0

其中 f 是焦距, (u0, v0) 是主点坐标(光轴与图像平面的交点)。

嗯,这里要注意:这个公式假设相机是理想的,没有畸变。但现实中没有完美的相机,所以我们需要引入畸变模型。

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

内参矩阵描述的是相机内部的几何属性。它告诉你:焦距是多少、像素是方的还是扁的、主点偏移了多少。

内参矩阵 K 长这样:

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

其中:

  • fx, fy:x 和 y 方向的焦距(以像素为单位)。如果像素是正方形,fx = fy。
  • cx, cy:主点坐标,通常是图像中心,但实际会有偏移。

我在项目中遇到过一台工业相机,标定后发现 cx 偏离了图像中心十几个像素。一开始以为是标定错了,后来查手册才知道,那款相机的传感器封装就有偏移。所以,别迷信「中心对称」,标定一下最靠谱。

我的习惯:每次拿到新相机,第一件事就是标定内参。哪怕之前标过,只要镜头动了、焦距变了,都得重新标。别偷懒,偷懒的代价是后面花更多时间调参。

3.3 外参矩阵:相机在哪儿?在看哪儿?

外参矩阵描述的是相机在世界坐标系中的位置和朝向。说白了,就是相机在哪儿、脑袋往哪边转。

外参矩阵由旋转矩阵 R 和平移向量 t 组成:

[R | t]

它是一个 3x4 的矩阵。R 是 3x3 的旋转矩阵,t 是 3x1 的平移向量。

举个例子:你把相机放在三脚架上,拍了一张照片。外参矩阵就记录了相机相对于世界坐标系的位置和姿态。如果你移动了相机,外参就变了。

你想想看,在三维扫描中,我们经常需要从多个角度拍摄同一个物体。每次移动相机,外参都在变。但内参不变(只要焦距和镜头没动)。这就是为什么标定要分内参和外参——内参一次搞定,外参每次重新算。

3.4 畸变模型:现实总是不完美的

针孔模型是理想情况。但现实中的镜头有畸变。最常见的两种:

  • 径向畸变:光线通过透镜边缘时弯曲得更厉害,导致图像边缘的直线变弯。分为桶形畸变和枕形畸变。
  • 切向畸变:透镜和成像平面不平行导致的畸变。

畸变模型通常用多项式来近似:

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 是切向畸变系数。r 是像素到主点的距离。

我曾经踩过的坑:有一次标定完,发现畸变系数特别大,尤其是 k3。我以为是标定出错了,反复标了好几遍都一样。后来才发现,那个镜头本身就是鱼眼镜头,畸变本来就大。所以,标定前先搞清楚镜头类型,别拿鱼眼当普通镜头标。

3.5 张正友标定法:最经典的标定方法

张正友标定法,说白了就是「拍棋盘格,然后算参数」。这个方法在 1998 年提出,到现在依然是工业界最常用的方法。为什么?因为它简单、稳定、精度够用。

核心思路是这样的:

  1. 打印一张棋盘格(黑白格子交替),贴在平面上。
  2. 从不同角度拍 10-20 张照片。
  3. 检测每张照片中的角点(格子交叉点)。
  4. 利用角点的世界坐标(已知)和图像坐标(检测到的),建立方程。
  5. 解方程,得到内参、外参和畸变系数。

你可能会问:为什么是棋盘格?因为角点检测简单、稳定,而且棋盘格的几何关系已知,方便建立约束。

张正友标定法的数学本质:

它利用单应性矩阵(Homography)来建立世界平面和图像平面之间的映射关系。每张照片提供一个单应性矩阵,多个单应性矩阵联合求解,就能解出内参。

我个人习惯用 OpenCV 的 cv2.calibrateCamera() 函数来实现张正友标定。代码很简单,但要注意几个细节:

import cv2
import numpy as np

# 准备棋盘格的世界坐标
pattern_size = (9, 6)  # 内角点数量
objp = np.zeros((pattern_size[0]*pattern_size[1], 3), np.float32)
objp[:,:2] = np.mgrid[0:pattern_size[0], 0:pattern_size[1]].T.reshape(-1,2)

# 存储所有照片的角点
objpoints = []  # 世界坐标
imgpoints = []  # 图像坐标

# 遍历照片,检测角点
for fname in image_files:
    img = cv2.imread(fname)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    ret, corners = cv2.findChessboardCorners(gray, pattern_size, None)
    if ret:
        objpoints.append(objp)
        imgpoints.append(corners)

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

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

这段代码跑完,你就得到了内参矩阵和畸变系数。接下来就可以用它们去畸变、做三维重建了。

我的建议:拍照时,让棋盘格占画面 1/3 到 1/2 的面积。角度变化要大——倾斜、旋转、远近都要有。我一般拍 15-20 张,覆盖整个视场。太少的话,标定结果不稳定;太多的话,边际收益递减。

3.6 标定流程总结

整个标定流程,我习惯用一张图来概括:

相机标定流程 1. 准备棋盘格 打印并贴在平面上 2. 多角度拍摄 10-20张不同角度 3. 检测角点 提取棋盘格角点 4. 求解 计算参数 标定输出 内参矩阵 (K) 畸变系数 (k1,k2,p1,p2,k3) 外参矩阵 (R, t)

这张图把整个流程串起来了。你照着做就行,别想太多。

3.7 标定精度评估

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

  • 重投影误差:把标定得到的参数反算回去,看角点的投影位置和实际检测位置的差距。通常小于 0.5 像素就算不错。
  • 畸变校正效果:用标定结果去畸变,看图像中的直线是不是变直了。

注意:重投影误差小不一定代表标定好。如果你只拍了 3 张照片,重投影误差可能也很小,但那是过拟合。所以,照片数量要够,角度要多样。

好了,相机标定的基础就这些。说白了,就是搞清楚相机怎么成像、有什么毛病、怎么治。后面做三维扫描,每一步都离不开这些基础。你把这些搞懂了,后面的路就好走了。


公众号:蓝海资料掘金营,微信deep3321