一、从一张照片说起:相机到底在做什么?
你有没有想过一个问题——为什么手机拍出来的照片,能“记住”现实世界的三维信息?
说白了,相机就是一个把三维世界“压扁”成二维图像的设备。但这个“压扁”的过程,是有数学规律的。我们做AR/VR开发,本质上就是在反向操作:从二维图像里,把三维信息再“还原”出来。
我个人习惯把相机模型比作一个“投影魔法盒”。你站在真实世界里,光线穿过镜头,打在传感器上,形成一张照片。这个过程中,有几个关键参数决定了“投影”的准确性。这些参数,就是我们今天要聊的——相机内参和外参。
核心观点:没有准确的相机标定,AR/VR应用中的虚拟物体就像“飘”在现实场景上,而不是“长”在现实场景里。我见过太多项目,标定没做好,后期花了几倍的时间去调位置偏移。
二、针孔相机模型:最朴素的成像原理
先别被“针孔”两个字吓到。其实它特别简单。
想象一个完全封闭的盒子,前面戳一个小孔,后面放一张感光纸。光线从物体上反射过来,穿过小孔,在感光纸上形成一个倒立的像。这就是针孔相机模型。
数学上怎么描述?看这个关系:
物体高度 / 物体到小孔的距离 = 像的高度 / 小孔到成像面的距离
写成公式就是:
-x / f = X / Z
其中:
- X 是物体在三维空间中的高度
- Z 是物体到相机的距离(深度)
- x 是物体在成像平面上的高度(像素坐标)
- f 是焦距(小孔到成像面的距离)
嗯,这里要注意:实际相机用的是透镜,不是小孔。但数学原理是一样的。透镜的好处是能进更多光,坏处是会产生畸变——这个我们后面会聊到。
我的经验:在AR/VR开发中,我建议你始终把相机想象成一个“针孔”。这样推导公式时思路会清晰很多。实际项目中,我经常用针孔模型做快速原型验证,等效果稳定了再考虑畸变补偿。
三、内参矩阵:相机自己的“身份证”
每个相机都有自己的“性格”。有的广角,有的长焦,有的像素高,有的像素低。内参矩阵就是描述这些“性格”的数学工具。
内参矩阵 K 长这样:
K = [fx, 0, cx]
[ 0, fy, cy]
[ 0, 0, 1]
每个参数的含义:
| 参数 | 含义 | 单位 |
|---|---|---|
| fx, fy | 焦距在x和y方向上的像素表示 | 像素 |
| cx, cy | 图像中心点(主点)的像素坐标 | 像素 |
为什么 fx 和 fy 可能不一样?因为传感器上的像素不一定是正方形的。我在项目中遇到过一台工业相机,它的像素是矩形的,导致 fx 和 fy 差了将近 5%。如果不标定出来,虚拟物体在水平方向和垂直方向上的缩放就不一致,看起来会“变形”。
内参矩阵的作用,就是把三维空间中的点,映射到二维像素坐标上。公式是:
pixel_coords = K * camera_coords
其中 camera_coords 是相机坐标系下的三维点坐标。
避坑指南:我曾经在做一个AR导航项目时,直接用了手机厂商提供的相机参数。结果虚拟箭头总是偏了10个像素。后来重新标定才发现,厂商给的 cx, cy 是理论值,实际生产中有偏差。记住:每个相机个体都不一样,必须单独标定。
四、外参矩阵:相机在哪儿?在看哪儿?
内参告诉了我们“相机长什么样”,外参告诉了我们“相机在哪儿、在看哪儿”。
外参矩阵由两部分组成:
- 旋转矩阵 R:描述相机朝向(3x3矩阵)
- 平移向量 t:描述相机位置(3x1向量)
合在一起,外参矩阵 [R | t] 是一个 3x4 的矩阵:
[R | t] = [r11, r12, r13, t1]
[r21, r22, r23, t2]
[r31, r32, r33, t3]
它的作用是把世界坐标系下的点,转换到相机坐标系下:
camera_coords = R * world_coords + t
你想想看,在AR/VR里,外参矩阵有多重要?
- 你拿着手机走动,手机的位置和朝向一直在变——外参矩阵每帧都在更新
- 虚拟物体要“固定”在现实世界的某个位置——需要外参矩阵把虚拟物体从世界坐标转换到屏幕坐标
我习惯把外参矩阵叫做“相机的手眼协调器”。它决定了虚拟物体是不是真的“长”在了现实场景里。
五、张正友标定法:为什么它是行业标准?
好了,现在我们知道需要标定内参和外参。但怎么标定呢?
张正友教授在1998年提出的方法,至今仍是工业界最常用的相机标定方法。为什么?因为它只需要你打印一张棋盘格,拍几张照片,就能算出所有参数。
核心思路是这样的:
- 准备棋盘格:打印一张黑白相间的棋盘格,贴在平面上
- 拍摄多张照片:从不同角度、不同距离拍摄棋盘格(我建议至少拍15-20张)
- 检测角点:用算法找到棋盘格上所有黑白格子的角点
- 建立对应关系:每个角点在三维空间中的坐标是已知的(棋盘格是平的,Z=0),在图像上的像素坐标也是已知的
- 求解参数:通过最小二乘法,解出内参和外参
数学上,张正友标定法利用了“单应性矩阵”的概念。简单说,就是找到一个矩阵 H,能把棋盘格上的点映射到图像上:
pixel_coords = H * world_coords
然后从多个 H 中,分解出内参 K 和外参 [R | t]。
关键点:张正友标定法假设棋盘格是绝对平面的。所以打印棋盘格时,一定要贴在平整的硬板上,不要有褶皱。我曾经用贴在纸箱上的棋盘格做标定,结果畸变参数完全不对,浪费了一整天。
六、标定实现:从理论到代码
理论说完了,咱们来点实际的。OpenCV 里已经实现了张正友标定法,我们直接调用就行。
基本流程:
# 1. 准备棋盘格参数
棋盘格内角点数 = (9, 6) # 9列6行内角点
方格大小 = 25.0 # 毫米
# 2. 准备世界坐标系下的角点坐标
objp = np.zeros((6*9, 3), np.float32)
objp[:,:2] = np.mgrid[0:9, 0:6].T.reshape(-1,2) * 25.0
# 3. 遍历所有标定图片
for img in 标定图片列表:
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, corners = cv2.findChessboardCorners(gray, (9,6), None)
if ret:
objpoints.append(objp) # 世界坐标
imgpoints.append(corners) # 像素坐标
# 4. 执行标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
objpoints, imgpoints, gray.shape[::-1], None, None
)
# mtx 就是内参矩阵
# dist 是畸变系数
# rvecs, tvecs 是每张图片的外参
这段代码看起来简单,但有几个坑要注意:
- 棋盘格内角点数量:是内部角点,不是格子数。比如 9x6 的棋盘格,实际有 10x7 个格子
- 方格大小:单位不重要(毫米、厘米都行),但所有图片必须统一
- 图片数量:少于10张,标定结果不稳定。我一般拍20张,覆盖不同角度和距离
我的小技巧:标定完成后,一定要做“重投影误差”检查。就是把标定得到的参数,把世界坐标点重新投影到图像上,看和实际检测到的角点差多少像素。误差小于0.5像素就算不错了。如果超过1个像素,说明标定质量有问题,需要重新拍。
七、知识体系总览
为了让你对本章内容有个整体把握,我画了一张结构图:
这张图把整个知识体系串起来了。从针孔模型出发,到内参和外参,最后用张正友标定法把它们全部求解出来。你在做AR/VR开发时,可以把这个图贴在工位上,随时提醒自己:相机标定是基础中的基础。
好了,这一章的内容就到这里。记住:相机标定不是一次性的工作。换镜头、调焦距、甚至温度变化,都可能让标定参数失效。我建议你在项目启动时做一次完整标定,之后每周做一次快速验证。
下一章,我们会聊到特征点提取与匹配——这是三维重建和SLAM的基石。到时候见。