3、相机内参矩阵:焦距、主点、畸变参数,以及如何从相机标定获取
说到坐标变换,有个东西绕不开——相机内参矩阵。
我刚开始做目标跟踪那会儿,总觉得内参矩阵就是个黑盒子。标定完拿到几个数字,往代码里一填就完事了。后来踩了不少坑才明白,这东西要是理解不透,后面做坐标变换、做3D定位,全是空中楼阁。
今天咱们就把这个「黑盒子」拆开看看。
3.1 内参矩阵到底长什么样?
先上公式,别怕,很简单:
K = [fx 0 cx]
[ 0 fy cy]
[ 0 0 1]
这是一个 3×3 的上三角矩阵。里面就四个关键参数:
- fx, fy —— 焦距(以像素为单位)
- cx, cy —— 主点坐标(光轴与成像平面的交点)
说白了,内参矩阵干的事就是:把相机坐标系下的3D点,映射到图像平面上的2D像素坐标。
你想想看,一个三维空间里的点 (X, Y, Z),怎么变成图像上那个 (u, v) 的?中间就是内参矩阵在起作用。
核心理解:内参矩阵描述的是「相机内部的光学几何属性」。同一个相机,不管你怎么转、怎么挪,内参矩阵是不变的。
3.2 焦距 fx, fy —— 不是镜头上的那个焦距
这里有个容易混淆的地方。
镜头包装盒上写的「焦距 12mm」,那是物理焦距。而内参矩阵里的 fx、fy,是以像素为单位的等效焦距。
它们的关系是:
fx = f / dx
fy = f / dy
其中 f 是物理焦距(单位 mm),dx、dy 是每个像素的物理尺寸(单位 mm/pixel)。
举个例子:
- 物理焦距 f = 12 mm
- 像素尺寸 dx = 0.004 mm/pixel
- 那么 fx = 12 / 0.004 = 3000 像素
嗯,这里要注意:fx 和 fy 通常不相等。因为像素不一定是正方形,dx 和 dy 可能不一样。我在项目中遇到过一台工业相机,fx=2456,fy=2452,差了一点点。虽然差别不大,但做高精度跟踪时,这点差异就会导致几厘米的误差。
3.3 主点 cx, cy —— 不一定是图像中心
很多人以为主点就是图像的正中心,比如 640×480 的图像,主点就是 (320, 240)。
实际上不是这样。
主点是光轴与成像平面的交点。由于镜头安装有误差、传感器有偏移,主点往往不在正中心。我见过最离谱的一次,主点偏了将近 30 个像素。
你想想看,如果做目标跟踪时用了错误的 cx、cy,那目标在图像边缘时,坐标变换的误差会非常大。
我的习惯:每次拿到新相机,第一件事就是重新标定。不要相信出厂参数,尤其是主点坐标。
3.4 畸变参数 —— 镜头不是完美的
理想情况下,镜头是完美的针孔模型。但现实中的镜头都有畸变,主要分两种:
- 径向畸变:图像边缘向内或向外弯曲。桶形畸变、枕形畸变都属于这一类。
- 切向畸变:镜头与传感器不平行导致的扭曲。
畸变模型通常用几个参数来描述:
径向畸变:k1, k2, k3
切向畸变:p1, p2
一共 5 个畸变参数。OpenCV 的标定函数默认就是输出这 5 个。
我曾经在一个项目中,用广角镜头做行人跟踪。没做畸变校正之前,图像边缘的人脸都变形了,跟踪框根本对不准。校正之后,效果立竿见影。
避坑指南:畸变校正会改变图像尺寸和像素位置。如果你在做目标跟踪,一定要在校正后的图像上标注跟踪框,否则坐标全乱套了。
3.5 如何从相机标定获取内参?
获取内参最常用的方法就是张正友标定法。你需要一块棋盘格标定板,从不同角度拍 10-20 张照片,然后用 OpenCV 跑一遍。
核心步骤:
- 准备棋盘格标定板(打印出来贴在平板上就行)
- 从不同角度拍摄 10-20 张照片
- 用
cv2.findChessboardCorners()找到角点 - 用
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 = [] # 世界坐标系中的3D点
imgpoints = [] # 图像中的2D点
# 遍历所有标定图片
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)
跑完之后,mtx 就是内参矩阵,dist 就是畸变参数。
我的经验:标定照片不要只拍正面,要多拍倾斜角度。尤其是棋盘格靠近图像边缘的照片,对畸变参数的估计帮助很大。我一般拍 15-20 张,重投影误差控制在 0.1 像素以内才算合格。
3.6 知识体系总览
下面这张图总结了本章的核心内容:
3.7 小结
内参矩阵是相机模型的核心。它包含焦距、主点、畸变参数三大部分。通过相机标定,我们可以精确获取这些参数。
做目标跟踪时,我建议你把内参矩阵当成「一等公民」来对待。每次换相机、换镜头、甚至调整焦距后,都要重新标定。别偷懒,这个懒偷不得。
好了,内参矩阵就聊到这儿。下一节咱们聊聊外参矩阵——相机是怎么摆的,以及它和目标跟踪有什么关系。