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 年提出,到现在依然是工业界最常用的方法。为什么?因为它简单、稳定、精度够用。
核心思路是这样的:
- 打印一张棋盘格(黑白格子交替),贴在平面上。
- 从不同角度拍 10-20 张照片。
- 检测每张照片中的角点(格子交叉点)。
- 利用角点的世界坐标(已知)和图像坐标(检测到的),建立方程。
- 解方程,得到内参、外参和畸变系数。
你可能会问:为什么是棋盘格?因为角点检测简单、稳定,而且棋盘格的几何关系已知,方便建立约束。
张正友标定法的数学本质:
它利用单应性矩阵(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 标定流程总结
整个标定流程,我习惯用一张图来概括:
这张图把整个流程串起来了。你照着做就行,别想太多。
3.7 标定精度评估
标定完了,怎么知道结果好不好?我一般看两个指标:
- 重投影误差:把标定得到的参数反算回去,看角点的投影位置和实际检测位置的差距。通常小于 0.5 像素就算不错。
- 畸变校正效果:用标定结果去畸变,看图像中的直线是不是变直了。
注意:重投影误差小不一定代表标定好。如果你只拍了 3 张照片,重投影误差可能也很小,但那是过拟合。所以,照片数量要够,角度要多样。
好了,相机标定的基础就这些。说白了,就是搞清楚相机怎么成像、有什么毛病、怎么治。后面做三维扫描,每一步都离不开这些基础。你把这些搞懂了,后面的路就好走了。
公众号:蓝海资料掘金营,微信deep3321