想象一下:你是一位探险家,走进一个从未有人到过的黑暗洞穴。没有地图、没有 GPS,你唯一能依靠的是左手摸到的岩壁、右手握着的探路杖,还有身体对自己转向和迈步的感知。你要活着走出去,必须一边回答“我现在在洞穴的哪个位置”(定位),一边在脑中画出“洞穴内部长什么样”(建图)。更麻烦的是,这两个问题互相纠缠:没有地图你无法确定位置,不确定位置你又画不出地图。 这就是 SLAM(Simultaneous Localization and Mapping,同时定位与建图)要解决的核心难题。今天,当你在手机上打开 AR 测量工具,或戴上 VR 头显在房间里走动时,设备里就住着这样一位“探险家”——它用摄像头当眼睛,用惯性传感器当身体平衡感,一边感知移动,一边构建环境地图,最终让虚拟沙发稳稳落在真实地板上、让虚拟敌人藏在真实墙角后。 本文就用这场“黑暗洞穴探险”作为主线,带你一步步理解 SLAM 的感知、融合、地图、回环优化,以及 ARKit 和 ARCore 背后的原理。

岗位关联

对 AI/XR 从业者而言,理解 SLAM 意味着你不再只是“调用 ARKit/ARCore 的 API”,而是能诊断漂移、读懂 VIO 日志、优化锚定体验——这是从界面开发走向空间计算核心能力的第一张门票。

目录
  1. 可视化演示
  2. SLAM基本概念
  3. 感知传感器
  4. 视觉里程计
  5. 视觉惯性系统
  6. 地图表示
  7. 回环与优化
  8. 主流平台实践
  9. 开发落地趋势
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

展示视觉惯性里程计VIO的相机-IMU融合定位过程,以及回环检测与BA全局优化消除漂移的效果,切换模式查看。

本节配有交互动效(进入视口后自动加载,约 3KB)。

VIO融合定位 相机沿轨迹运动提取特征点匹配 IMU高频(200-1000Hz)提供运动先验 融合输出高精度位姿估计 VIO克服单目尺度模糊,实现稳定空间定位

SLAM基本概念

探险家最讨厌的情况是无头苍蝇式乱走。真正专业的做法是:每走一步,先根据记忆中已见过的石柱判断自己的位置,再把这个位置附近的新发现补进脑海里的地图。这一步一步“我在哪 → 地图该如何更新”的循环,就是 SLAM 的本体。它不是某个一次性算法,而是一整套“边探索边修正”的生存法则。

SLAM(Simultaneous Localization and Mapping)即同时定位与建图。位姿(位置加朝向)记为 x,环境地图记为 m,传感器观测记为 z,运动指令和体感记为 u。定位负责给出 x,建图负责给出 m,而 z 和 u 是系统不断吸入的信息。难点在于:地图不完整时,定位没有参照物;定位不准确时,地图又会被画歪。这经典的“鸡生蛋”困局,解法是交替迭代——先粗略估计位姿,更新局部地图,再回头精修位姿,如此反复。

从概率视角看,SLAM 维护的是一个后验分布。它回答的问题是:在已知全部历史观测和运动输入的前提下,当前位姿与整张地图的联合置信度有多高。每来一帧新图像、每收到一次 IMU 读数,算法就刷新一次这个分布,这就是“实时性”的本质——不是一次性算完,而是每时每刻都在更新。

所以,当你举起手机第一次扫描客厅时,它面对的其实是“一张还没画完的地图”。它敢把虚拟花瓶放到茶几上,靠的正是这个概率框架——每一次观测都在压缩不确定性,让“我在这里、地面在那里”的置信度越来越高。空间计算的第一块基石,就这样铺好了。

p(xt,m∣z1:t,u1:t)p(x_t, m \mid z_{1:t}, u_{1:t})
这个公式是 SLAM 的“总任务书”:给定历史观测和运动数据,估计此时的位置与地图。
  1. 联合概率表示位姿 x_t 与地图 m 同时取某个值的可信程度,数值越大越相信这个组合是对的。
    p(xt,m)p(x_t, m)
  2. 条件符号竖线表示“在……已知的前提下”,即我们要基于观测而不是凭空猜测。
    ∣\mid
  3. 观测集合从第 1 到第 t 时刻的全部传感器观测,例如相机拍到的每一帧图像。
    z1:t={z1,z2,…,zt}z_{1:t} = \{z_1, z_2, \dots, z_t\}
  4. 运动输入集合包括 IMU 读到的加速度、角速度,以及控制指令,对应探险家“走了几步、转了几度”的体感。
    u1:t={u1,u2,…,ut}u_{1:t} = \{u_1, u_2, \dots, u_t\}

感知传感器

探险家有两样贴身装备:手电筒和身体的平衡感。手电筒照亮岩壁,让他看到远处的钟乳石形状——这是丰富的视觉信息;但当手电急速甩动,眼前会一片模糊。身体的平衡感则相反:黑暗中也能感知自己转了多少度、迈了几步,可走久了,步幅估算难免越来越飘。这就是相机与 IMU 的真实写照。

视觉相机是 SLAM 的“眼睛”:它提供像素级的高信息量数据,能辨认纹理、边缘、角点,但惧怕运动模糊、暗光和纯色墙面。惯性测量单元(IMU)则是“前庭觉”:它以 200Hz 甚至更高的频率输出加速度和角速度,完全不受光照影响,短时间内的运动估计极其准,但每次积分都会引入微小误差,长时间累积就变成“飘”。一个怕快、一个怕久,恰好互补。

IMU 的测量必须建模成数学形式,否则算法无法区分“真实运动”和“传感器瑕疵”。这个模型告诉我们:加速度计读到的数,不等于真实加速度,而是叠加了重力投影、偏置和噪声的混合信号。

在手电罢工的瞬间,平衡感顶上去;在脚步累积太多怀疑时,眼睛又帮忙校准回来。传感器融合的本质,就是让“眼睛”和“前庭觉”轮流做对方的质检员。下一节,我们看看纯靠眼睛数步子能走多远——那就是视觉里程计。

am=R⊤(a−g)+ba+naa_m = R^{\top}(a - g) + b_a + n_a
IMU 测量模型:读到的加速度 = 真实加速度去掉重力后的投影 + 偏置 + 噪声。
  1. 加速度计读数IMU 实际输出的加速度值,单位是米每平方秒。
    ama_m
  2. 去重力项用旋转矩阵把世界坐标系下的加速度转到机体坐标系,并减去重力 g 的影响——因为静止时加速度计也能读到 9.8 的重力。
    R⊤(a−g)R^{\top}(a - g)
  3. 偏置与噪声偏置 b_a 是传感器长期的系统性偏差,噪声 n_a 是每时每刻的随机扰动。
    ba+nab_a + n_a

视觉里程计

探险家在昏暗的洞壁上发现一块心形石头,走了几步再看,石头在视野里向左挪了一小段——他立刻知道自己在向右移动。连续观察一组石头的“集体位移”,就能反推出自己的步幅和转角。这就是用眼睛数步子,专业名称叫视觉里程计(Visual Odometry, VO)。

VO 是 SLAM 的前端引擎,核心流程三步走:特征提取(找出图像中那些“独一无二”的角点、斑点)、特征匹配(把相邻两帧里同一个物理点的像素对上)、位姿估计(根据匹配关系求解相机运动)。经典的 ORB、SIFT 特征,本质就是教算法“什么叫一看就认得出来的石头”。匹配点对足够多,运动就能被精确解出来。

位姿估计常用“最小化重投影误差”:把一个已知的三维路标点按当前猜测的位姿投影回像素平面,比较投影点和实际观测点之间的差距。所有点的差距平方和越小,说明猜测的位姿越接近真相。

VO 靠眼睛短时间数步子很可靠,但每帧估计都有微小误差。走上一万步,这些误差就像细沙一样聚成沙丘——设备的位置估计开始偏离真实轨迹。这还不是最糟的:更怕的是墙壁没有纹理、镜头快速转动时特征消失。要治这两个毛病,就要请出 IMU 这位搭档了。

E=∑i∥pi−Π(RPi+t)∥2E = \sum_i \left\| p_i - \Pi(R P_i + t) \right\|^2
重投影误差公式:把地图里的 3D 点投影到图像上,和实际看到的像素比差距,差距越小位姿越准。
  1. 三维路标点地图中第 i 个空间点的坐标,相当于探险家记住的一块“石头”。
    Pi=(X,Y,Z)⊤P_i = (X, Y, Z)^{\top}
  2. 旋转和平移相机从上一时刻到当前时刻的姿态变化,是我们要解的“脚步幅度”。
    R∈SO(3),t∈R3R \in SO(3), \quad t \in \mathbb{R}^3
  3. 投影函数把三维点按照相机内参投影到像素平面,得到“它应该出现在图像的哪里”。
    Π(P′)=1Z′KP′\Pi(P') = \frac{1}{Z'} K P'
  4. 像素误差的平方和把每个点的预测位置与实际观测位置的偏差求平方并累加,作为整体误差。
    ∥pi−Π(RPi+t)∥2\left\| p_i - \Pi(R P_i + t) \right\|^2

视觉惯性系统

探险家在湿滑地段猛地甩了一下手电,眼前瞬间一片白光——眼睛失灵了。但他的平衡感清楚地告诉他:身体向右转了约 90 度。几秒后眼睛恢复,他又开始怀疑自己走了多远,抬头看见那块心形石头——原来刚才少算了两个弯。眼睛和平衡感互相纠错,这套配合就是视觉惯性系统(Visual-Inertial System, VIO)。

VIO 的融合策略分两个流派。松耦合:视觉和 IMU 各自独立算出位姿,再像“两个专家投票”一样加权合并,简单但信息利用不充分。紧耦合:把图像特征的原始观测和 IMU 的原始测量放进同一个优化问题里,让两者互相约束,精度显著更高但计算量更大。VINS-Mono、ORB-SLAM3 等主流方案走的都是紧耦合路线。

紧耦合的状态向量把“探险家”的完整状况写成一个矢量:他站在哪、面朝哪、走得多快、平衡感偏了多少。IMU 偏置不是固定常数,而是会缓慢变化的——所以必须把它放进状态里,让视觉来持续校准它。

眼睛与平衡感互相校准,就诞生了 AR 设备最核心的引擎——视觉惯性里程计。它也是 ARKit 和 ARCore 内部真正日复一日跑着的程序。但有了引擎还不够,探险家还得决定画什么样的地图,这引出了地图表示的问题。

x=[R,  p,  v,  bg,  ba]⊤x = [R, \; p, \; v, \; b_g, \; b_a]^{\top}
VIO 状态向量:姿态、位置、速度、陀螺仪偏置、加速度计偏置,五者联合估计。
  1. 姿态与位置R 是旋转矩阵,p 是三维位置,共同决定探险家“面朝哪、站在哪”。
    R∈SO(3),p∈R3R \in SO(3), \quad p \in \mathbb{R}^3
  2. 速度v 是机体在三个方向的速度,IMU 靠积分加速度得到速度,但积分会飘,需要视觉修正。
    v∈R3v \in \mathbb{R}^3
  3. 双偏置陀螺仪偏置 b_g 和加速度计偏置 b_a,是 IMU 的“长期手感误差”,必须实时估计。
    bg,ba∈R3b_g, b_a \in \mathbb{R}^3

地图表示

探险家画地图可以有不同的画法:赶路时只要记几块关键石头当路标,画一张“稀疏地图”;想研究洞穴结构,就要把每一面墙、每一处起伏都描清楚,画一张“稠密地图”。而 AR 摆家具时,最关心的其实是“地平面在哪里、墙在哪里”——平面地图就够了。

稀疏地图只保存少量特征路标点的位置,节省内存且定位高效,适合长期跟踪;稠密地图逐像素重建环境表面,渲染效果好但计算和存储开销巨大;半稠密与平面地图介于两者之间,在 AR 场景中最常用。无论哪种地图,所有点都必须放在统一的坐标系下描述——这就是世界坐标系、相机坐标系之间的关系。

地图上的任意一个点,都可以在世界坐标和相机坐标之间来回转换。探险家的“洞穴地图”是绝对画布,而他的眼睛每次看到的,只是这张地图的某个局部视角。

在 AR 应用里,那句“虚拟沙发放在了地面上”,本质上就是在确定地平面在一块地图中的精确方程。地图不再是一张给用户看的图,而是一份给算法看的“空间参考资料”。不过无论地图画得多好,探险家走得久了,还是会发现自己画的地图有点对不上——这时就需要回环检测出场救场了。

Pc=TcwPwP_c = T_{cw} P_w
坐标变换公式:把世界坐标系下的点,通过一次旋转加平移,变成相机坐标系下的点。
  1. 两个坐标系下的坐标同一个三维点,在世界系中叫 P_w,在相机系中叫 P_c。
    Pw=(X,Y,Z,1)⊤,Pc=(X′,Y′,Z′,1)⊤P_w = (X, Y, Z, 1)^{\top}, \quad P_c = (X', Y', Z', 1)^{\top}
  2. 齐次变换矩阵T 把旋转和平移打包成一个 4×4 矩阵,一次矩阵乘法完成坐标转换。
    Tcw=[Rcwtcw01]T_{cw} = \begin{bmatrix} R_{cw} & t_{cw} \\ 0 & 1 \end{bmatrix}
  3. 齐次坐标把三维点补成四维(最后一维为 1),让旋转和平移能写进同一个矩阵。
    P~=(X,Y,Z,1)⊤\tilde{P} = (X, Y, Z, 1)^{\top}

回环与优化

探险家在洞里绕了整整一大圈,突然看到石壁上那个自己出发时刻下的记号——“我又转回来了!”这一瞬间,他之前所有“路线可能走歪了”的怀疑都被唤醒。他摊开地图,把偏离的路线一点点拽回正确的位置。这一刻的名字,叫回环检测与全局优化。

累积漂移是 SLAM 的天敌:每一步微小的角度误差,走久了就可能偏出几米。回环检测利用图像的全局描述子,判断“当前看到的场景是不是以前来过的”。一旦认定是旧场景,就增加一条强约束:当前位姿应该和历史位姿重合。接下来,位姿图优化把这条长程约束均匀地传播到中间所有关键帧上——就像把一根松弛的橡皮筋两端拉紧,中间每个节点都被重新摆正。

回环约束的数学表达是“两个位姿之间的不一致程度”。T_i 和 T_j 是两个关键帧的位姿,ΔT_ij 是回环检测告诉我们的“它们本应相差多少”,如果实际位姿算出来的相对关系和这个差值不符,就产生残差。

回环像记忆的“回闪”,优化则把散落的脚印用橡皮筋重新拉成整体一致的路线。有了这一步,探险家绕洞穴十圈,地图仍然自洽。而在今天的手机上,这场回环每天都在数以千计的设备上悄然发生,被包装成开发者熟悉的 API 名:ARSession、Anchor、Google Play Services for AR。

eij=log⁡(ΔTij−1  Ti−1Tj)e_{ij} = \log\left(\Delta T_{ij}^{-1} \; T_i^{-1} T_j\right)
回环残差公式:衡量两个关键帧的相对位姿与回环约束之间的偏差,优化器会不断减小这个偏差。
  1. 两个关键帧位姿T_i 和 T_j 都是 SE(3) 群中的刚体变换,表示探险家前后两次的“脚印”。
    Ti,Tj∈SE(3)T_i, T_j \in SE(3)
  2. 相对变换的期望ΔT_ij 表示回环检测给出的“两处脚印本应相差多少”。
    ΔTij=Ti−1Tj\Delta T_{ij} = T_i^{-1} T_j
  3. 对数映射把 SE(3) 中的复合误差映射到六维向量空间,让优化器可以按普通向量方向调整。
    log⁡(⋅):SE(3)→R6\log(\cdot): SE(3) \to \mathbb{R}^6

主流平台实践

如今,洞穴探险家被塞进了手机芯片,成为一个不知疲倦的“隐形小人”。当你在 iPhone 上打开测距仪,ARKit 的视觉惯性里程计就以每秒几十次的频率重复着“看石头 → 数步子 → 画地图 → 认旧路”的循环。Android 侧的 ARCore 运动追踪,遵循的是同一套洞穴探险法则。

ARKit 使用设备相机加 IMU 做视觉惯性融合,输出六自由度(6DoF)位姿,并在此之上做平面检测、世界锚定;ARCore 的运动追踪同样基于视觉特征点和 IMU 的融合。两者输出的核心就是那个 6DoF 的位姿矩阵 T——它告诉渲染引擎:“虚拟物体应该画在屏幕哪个像素上”,而这背后的关键一步是三维到二维的投影。

任何在 AR 中渲染的虚拟物体,都要经历“世界坐标 → 相机坐标 → 图像坐标”的旅程。VIO 提供了位姿,相机内参提供了镜头本身的焦距和光心,两者一配合,虚拟物体就被准确地钉在了真实世界里。

开发者要做的,其实是给这个隐形小人一座好走的洞穴:保持现场光线充足、避免大范围纯色墙面、别让手完全遮挡镜头。它才能稳稳地把虚拟世界黏在真实世界上。理解了这个原理,你就能明白为什么苹果和谷歌的 AR API 都强烈建议“移动设备而不是转动设备”来初始化。

p=KTwcPp = K T_{wc} P
投影公式:世界坐标系的点,经相机位姿变换和镜头内参投影,变成屏幕像素坐标。
  1. 世界系下的三维点例如虚拟花瓶顶端的坐标。
    P=(X,Y,Z,1)⊤P = (X, Y, Z, 1)^{\top}
  2. 相机位姿从世界系到相机系的刚体变换,这就是 VIO 实时估计的六自由度结果。
    Twc=[Rt01]T_{wc} = \begin{bmatrix} R & t \\ 0 & 1 \end{bmatrix}
  3. 相机内参矩阵包含焦距 f_x、f_y 和主点 c_x、c_y,负责把光线方向和像素位置对应起来。
    K=[fx0cx0fycy001]K = \begin{bmatrix} f_x & 0 & c_x \\ 0 & f_y & c_y \\ 0 & 0 & 1 \end{bmatrix}

开发落地趋势

一名成熟的探险家会规划体力分配,什么地形大步走,什么地段稳住重心,还会不断升级装备——尼龙绳换成激光测距仪。AR/VR 开发者也是如此:管理好算法流水线、盯住性能预算,再跟上 LiDAR 和 AI 的新装备,才能让定位越来越稳。

落地一个 SLAM 应用,通常要经历:传感器标定 → 前端视觉/惯性里程计 → 建图 → 回环检测与全局优化 → 地图持久化与锚定。在性能预算上,60 帧的体验要求单帧算法耗时控制在 16 毫秒内,常见的优化手段有降低特征点数、关键帧策略、多线程并行、仅在必要时触发回环搜索。趋势上,iPhone Pro 的 LiDAR 为深度估计提供了硬件先验,端侧 NPU 让语义识别跑在本地,"AI SLAM"用神经网络提取特征、甚至端到端回归位姿——装备在升级,但探险法则不变。

评估一个 SLAM 系统到底准不准,业界有一个标准指标:ATE(绝对轨迹误差)。它把算法估计的轨迹和真值逐帧比较,算出平均偏离程度,数字越小说明定位越准。

装备从手电升级到激光雷达,算法从特征点升级到神经网络,但那条主线——感知、估计、回环、优化——从未改变。掌握了这条主线,未来面对 LiDAR SLAM、语义 SLAM、云端 SLAM,你手里握着的仍然是同一张洞穴地图,只是画得更细、走得更稳。

RMSE=1N∑i=1N∥p^i−pi∥2\mathrm{RMSE} = \sqrt{\frac{1}{N} \sum_{i=1}^{N} \left\| \hat{p}_i - p_i \right\|^2}
RMSE 公式:估计位姿与真实位姿的均方根误差,是 SLAM 论文里最常见的精度指标。
  1. 估计值与真值p̂_i 是算法输出的第 i 帧位置,p_i 是由外部设备(如动捕系统)测得的真值。
    p^i,  pi∈R3\hat{p}_i, \; p_i \in \mathbb{R}^3
  2. 欧氏距离计算每个时刻“偏了多少米”,三个坐标差的平方和再开方。
    ∥p^i−pi∥=Δx2+Δy2+Δz2\left\| \hat{p}_i - p_i \right\| = \sqrt{\Delta x^2 + \Delta y^2 + \Delta z^2}
  3. 均方根平均先平方再平均最后开方,让大的误差更突出,能敏锐反映偶尔的漂移尖峰。
    1N∑i=1Nei2\sqrt{\frac{1}{N}\sum_{i=1}^{N} e_i^2}

AI 应用场景

AR 家居试摆:让虚拟沙发踩上真实地板

阶段:空间感知与定位建图

解决的问题:传统线上买家具最大的痛点是“不知道摆家里合不合适”。AI 用 SLAM 实时构建房间的水平平面与墙面的粗略位置,让虚拟沙发在真实地板上稳定锚定,用户绕一圈就能感受尺寸和风格是否匹配。

具体案例:用户举起手机走进客厅,ARCore 提取墙面和地面特征,IMU 同步记录手机移动轨迹,系统在几百毫秒内检测出地平面。随后用户在手机屏幕上拖出沙发模型,沙发稳稳“落”在地面上,甚至随用户绕行保持位置不漂移。如果出现漂移,回环检测会在用户重新扫到冰箱角落时自动修正锚点位置。

VR 大空间定位:让玩家在房间里自由行走

阶段:实时追踪与交互反馈

解决的问题:VR 玩家在自家客厅走动时没有 GPS 可用,头显需要知道“我走了几步、朝哪个方向转身”,否则虚拟世界会和真实脚步错位。VIO 把头盔相机和 IMU 数据融合,实现六自由度的实时追踪。

具体案例:玩家戴着 PICO 头显玩密室逃脱,在三米乘三米的范围内弯腰、绕圈、快速转头。快速转头瞬间相机图像可能模糊,但 IMU 以 200Hz 的频率记录角速度,填补了图像帧之间的空白,画面不会闪跳。当玩家绕一圈回到原点,回环检测识别出书架区域的特征,系统悄悄把累计的角度误差清零,虚拟走廊始终与真实脚步同步。

Python 演示

python95 行
import numpy as np  class EkfSlam:    """一个极简 2D 扩展卡尔曼滤波 SLAM 演示类."""     def __init__(self, num_landmarks=4):        self.num_landmarks = num_landmarks        self.state_len = 3 + 2 * num_landmarks        self.state = np.zeros(self.state_len)  # x, y, theta, 路标x, 路标y, ...        self.cov = np.eye(self.state_len) * 1e-4        self.cov[:3, :3] = np.diag([1e-3, 1e-3, 1e-3])        self.known_ids = []  # 已初始化路标的 id        self.gc_count = 0   # 建图计数     def predict(self, v, w, dt):        x, y, th = self.state[:3]        self.state[0] = x + v * np.cos(th) * dt        self.state[1] = y + v * np.sin(th) * dt        self.state[2] = th + w * dt        x, y, th = self.state[:3]        F = np.eye(self.state_len)        F[0, 2] = -v * np.sin(th) * dt        F[1, 2] = v * np.cos(th) * dt        Q = np.eye(self.state_len) * 1e-8        Q[0, 0] = Q[1, 1] = (v * dt) ** 2 * 0.01        Q[2, 2] = (w * dt) ** 2 * 0.01        self.cov = F @ self.cov @ F.T + Q     def add_landmark(self, lm_id, r, phi):        x, y, th = self.state[:3]        idx = 3 + 2 * lm_id        self.state[idx] = x + r * np.cos(th + phi)        self.state[idx + 1] = y + r * np.sin(th + phi)        self.known_ids.append(lm_id)        self.gc_count += 1     def observe(self, lm_id, r, phi):        if lm_id not in self.known_ids:            self.add_landmark(lm_id, r, phi)        idx = 3 + 2 * lm_id        x, y, th = self.state[:3]        lx, ly = self.state[idx], self.state[idx + 1]        dx, dy = lx - x, ly - y        q = dx * dx + dy * dy        d = np.sqrt(q)        z_pred = np.array([d, np.arctan2(dy, dx) - th])        z = np.array([r, phi])        innov = z - z_pred        innov[1] = np.arctan2(np.sin(innov[1]), np.cos(innov[1]))        H = np.zeros((2, self.state_len))        H[0, 0] = -dx / d        H[0, 1] = -dy / d        H[0, idx] = dx / d        H[0, idx + 1] = dy / d        H[1, 0] = dy / q        H[1, 1] = -dx / q        H[1, 2] = -1.0        H[1, idx] = -dy / q        H[1, idx + 1] = dx / q        R = np.diag([0.05, 0.02])        S = H @ self.cov @ H.T + R        K = self.cov @ H.T @ np.linalg.inv(S)        self.state = self.state + K @ innov        self.cov = (np.eye(self.state_len) - K @ H) @ self.cov     def run(self, steps=120, noise=True):        """小车沿半径 4 的圆形轨迹运动, 观测 4 个路标."""        true_lm = [(4.0, 0.0), (0.0, 4.0), (-4.0, 0.0), (0.0, -4.0)]        v, w, dt = 2.0, 0.5, 0.1        for t in range(steps):            self.predict(v, w, dt)            x, y, th = self.state[:3]            for lm_id, (lx, ly) in enumerate(true_lm):                dx, dy = lx - x, ly - y                r_true = np.sqrt(dx * dx + dy * dy)                if r_true < 6.0:                    phi_true = np.arctan2(dy, dx) - th                    r = r_true + (np.random.randn() * 0.04 if noise else 0.0)                    phi = phi_true + (np.random.randn() * 0.01 if noise else 0.0)                    self.observe(lm_id, r, phi)            if t % 40 == 39:                est = self.state[:3]                true_x = 4.0 * np.cos(w * (t + 1) * dt)                true_y = 4.0 * np.sin(w * (t + 1) * dt)                err = np.sqrt((est[0] - true_x) ** 2 + (est[1] - true_y) ** 2)                print("t=%-3d 估计=(%6.2f,%6.2f) 真值=(%6.2f,%6.2f) 误差=%.3f m"                      % (t + 1, est[0], est[1], true_x, true_y, err))        print("建图完成: 已初始化 %d 个路标, 认识 %d/%d 个路标"              % (self.gc_count, len(self.known_ids), self.num_landmarks))  if __name__ == "__main__":    slam = EkfSlam(num_landmarks=4)    slam.run(steps=120, noise=True)
目的:用扩展卡尔曼滤波(EKF)实现一个最简 SLAM——小车沿圆形轨迹运动,一边估计自身位姿,一边把观测到的路标加入地图,直观展示“定位与建图同时进行”。
  1. self.state = np.zeros(self.state_len)状态向量同时装下位姿(x, y, 朝向)和所有路标坐标,这就是“建图”的载体。
  2. self.cov = F @ self.cov @ F.T + Q运动预测后先扩大不确定性,对应探险家“走一步后更没底”。
  3. H[1, 2] = -1.0观测方程对朝向角的偏导,把“看到路标的方向”与自身朝向联系起来。
  4. innov[1] = np.arctan2(np.sin(innov[1]), np.cos(innov[1]))角度残差归一化,防止 -π 和 π 之间的大跳变把滤波器带偏。
  5. K = self.cov @ H.T @ np.linalg.inv(S)卡尔曼增益决定“更信运动模型还是更信观测”,是视觉与惯性融合的数学化身。
  6. self.state = self.state + K @ innov用观测残差修正状态,相当于看一眼路标后悄悄调整自己的位置和姿态。
  7. if r_true < 6.0模拟传感器的有效感知范围,探险家只能看到身边的石头。

常见误区

误区我觉得 SLAM 和 GPS 差不多,就是让设备告诉你一个精确的绝对坐标,室内的 GPS 信号弱,所以用相机来替代卫星。

正解SLAM 没有卫星基准,它只关心“相对上一次位置,我移动了多少”,像探险家数步子。它输出的是局部坐标系下的相对位姿,这个坐标系的原点往往是应用启动时自己定的。AR 里的“世界原点”就是手机开机那一刻的位置。SLAM 要的是连续一致的相对运动,而不是经纬度。

为何常见:日常导航大量使用 GPS 绝对定位,导致人们默认位置服务都是“告诉我在哪”,忽略了定位还有“相对定位”这条路径。

误区手机相机帧率有 30fps,够高了。多加一个 IMU 只会增加计算负担,纯视觉算法不是照样能定位吗?

正解相机在快速旋转下会产生运动模糊,在暗光或白墙场景直接失去特征;IMU 以 200Hz 以上频率输出角速度和加速度,不依赖光照和纹理,能够填补相机帧与帧之间的空白。VIO 的精度普遍比纯视觉高一个量级,这正是 ARKit/ARCore 都内置 IMU 的原因。

为何常见:人凭直觉觉得“眼睛最强”,很难体会到身体里的“前庭觉”在关键时刻兜底的价值。

误区AR 虚拟物体放久了总会漂移,那我让用户重启应用重新扫描不就解决了?SLAM 算法本身没有纠错能力吧。

正解回环检测和位姿图优化就是专门对抗漂移的机制。当设备重新识别到曾经见过的场景,算法会建立回环约束并通过全局优化一次性修正整段轨迹。优秀的 AR 应用会设计“重定位”逻辑,在漂移发生后自动找回锚点,而不是用重启这种粗暴手段打断用户。

为何常见:普通用户只见过漂移的果,没见过回环修正那个“啪”的一瞬间,误以为算法没有自我纠错能力。

误区SLAM 不是要建图吗?那 AR 里屏幕上显示的 3D 模型、渲染出来的画面,应该就是 SLAM 建出来的地图吧。

正解SLAM 输出的地图是给定位算法用的几何参考:稀疏路标点、半稠密平面、位姿图。它可能只是一堆三维坐标和描述子,连颜色都没有。真正用于渲染的高质量网格通常来自单独的三维重建管线(如 LiDAR 扫描或摄影测量)。AR 中的地图服务于“锚定”和“追踪”,不负责“好看”。

为何常见:“地图”这个词天然让人联想到可视化图形,而 SLAM 的“地图”是面向数学优化的抽象表示。

误区ARKit/ARCore 启动时,我随便对着房间晃两下,它就能开始追踪了,初始化不重要吧?

正解VIO 初始化要为视觉特征和 IMU 建立初始尺度、重力方向和零偏估计。初始化质量直接影响后续几十秒的稳定性。官方文档建议缓慢平移扫描,让视差充分拉开——这和多视角几何里“基线越宽,三角化越准”的原理一致。

为何常见:初始化过程被系统自动完成,用户看不到内部估计是否收敛,容易误以为“触发过就行”。

面试题

Q1. SLAM 中“定位”与“建图”的关系是什么?

单选题 · easy

  • A.先建完整地图再进行定位
  • B.先确定位置再慢慢建图
  • C.定位与建图互相依赖同时进行← 正确答案
  • D.两者完全独立互不影响

答案:C。SLAM 的核心悖论是“定位需要地图,建图需要知道位姿”,真实解法像探险家边走边画地图,交替迭代。A 和 B 都把两者当成了先后步骤,D 直接违背 SLAM 的定义。正确选项 C 准确概括了这种相互依赖的关系。

Q2. 视觉相机与 IMU 的互补关系主要体现在哪里?

单选题 · easy

  • A.相机易模糊,IMU能扛快速转动← 正确答案
  • B.相机不模糊,IMU扛不了快转
  • C.两者都只能测量慢速运动
  • D.两者都提供绝对定位信息

答案:A。相机靠成像,快速旋转会产生运动模糊,而 IMU 直接在 200Hz 以上频率测量角速度和加速度,不受模糊影响,能在相机失效的瞬间顶住。C 和 D 都是对传感器能力的错误描述,B 把两者特性说反了。

Q3. “六自由度(6DoF)”追踪指的是什么?

单选题 · easy

  • A.六个摄像头的自由组合
  • B.三个平移加三个旋转自由度← 正确答案
  • C.六个地面标记点的定位
  • D.六倍于普通定位精度

答案:B。三维空间中的刚体共有 6 个独立自由度:沿 x、y、z 三个方向的平移,以及绕三个轴的旋转。AR 设备需要同时追踪这 6 个量才能让虚拟物体正确贴合真实世界。A、C、D 都误解了"DoF"的含义。

Q4. 视觉里程计中“重投影误差”的作用是什么?

单选题 · mid

  • A.消除镜头本身的畸变参数
  • B.通过像素匹配误差反推相机运动← 正确答案
  • C.直接测量物体的绝对位置
  • D.提高图像采集的传感器帧率

答案:B。重投影误差把地图里的三维点按当前位姿猜测投影回像素平面,与真实观测像素比较偏差,偏差越小位姿越准,通过最小化这个误差来求解相机运动。A 是标定环节的事,C 错在“绝对位置”,D 与帧率无关。

Q5. VIO 中“紧耦合”与“松耦合”的核心区别是什么?

单选题 · mid

  • A.紧耦合统一优化原始测量精度更高← 正确答案
  • B.紧耦合只用视觉松耦合只用 IMU
  • C.紧耦合实现容易但精度更低
  • D.两者没有本质层面的区别

答案:A。紧耦合把图像特征和 IMU 原始测量放进同一个优化问题联合求解,信息利用充分,精度高但计算量大;松耦合先让视觉和 IMU 各自估计位姿再融合结果,实现简单但精度受限。B、C、D 都与实际定义相悖。

Q6. 回环检测主要解决的问题是什么?

单选题 · mid

  • A.扩大地图覆盖的空间范围
  • B.消除累积漂移带来的全局不一致← 正确答案
  • C.加速每帧特征提取的速度
  • D.降低单个传感器的测量噪声

答案:B。回环检测识别“当前场景曾经来过”,建立当前位姿与历史位姿的强约束,再通过位姿图优化把误差分散到整段轨迹,消除累积漂移造成的全局不一致。A 是探索范围问题,C 是前端优化问题,D 是硬件噪声问题,都不是回环的职责。

Q7. 为什么 VIO 要把 IMU 偏置(bias)纳入状态向量?

单选题 · hard

  • A.偏置是常数,不影响定位精度
  • B.偏置会缓慢漂移,需在线估计补偿← 正确答案
  • C.偏置只在出厂时标定一次即可
  • D.偏置只影响旋转不影响平移

答案:B。IMU 偏置受温度和时间影响会缓慢变化,如果当作常数,积分误差会逐渐累积到速度和位置上。把偏置放进状态向量,让视觉观测在每次更新时反向校正偏置,这种“在线估计”是 VIO 紧耦合的经典设计。A、C 忽略了偏置的时变性,D 对偏置影响的描述不完整。

Q8. ATE 指标(均方根轨迹误差)数值偏大,说明什么?

单选题 · hard

  • A.系统整体轨迹与真值偏差较大← 正确答案
  • B.单帧特征匹配耗时过长
  • C.设备电池续航严重不足
  • D.地图包含太多三维点需要压缩

答案:A。ATE 是估计轨迹与真值轨迹逐帧比较的均方根误差,它直接反映定位精度的整体水平。数值偏大说明轨迹出现了明显漂移或偏差,需要检查初始化、回环触发频率或传感器标定。B 是耗时问题,C 是功耗问题,D 是地图容量问题,都与 ATE 无关。

Q9. 稀疏地图和稠密地图的区别有哪些?

多选题 · mid

  • A.稀疏地图只保存少量特征路标← 正确答案
  • B.稠密地图逐像素重建环境表面← 正确答案
  • C.稀疏地图一定比稠密地图更精确
  • D.AR 平面检测需要平面或半稠密信息← 正确答案

答案:ABD。稀疏地图只保留少量特征点,轻量高效;稠密地图重建逐像素的深度和表面,效果好但开销大。C 错误——稀疏不一定更精确,精度取决于算法而非地图密度。AR 场景中平面检测依赖半稠密或平面表示,D 正确。

Q10. 位姿图优化中需要同时满足哪些约束?

多选题 · hard

  • A.相邻关键帧相对位姿约束← 正确答案
  • B.回环检测提供的全局约束← 正确答案
  • C.图像每个像素的颜色一致性
  • D.让总残差尽量全局最小化← 正确答案

答案:ABD。位姿图优化维护两类核心约束:相邻帧间的顺序约束(A)和回环长程约束(B),目标是让所有约束的残差总和尽可能小(D)。C 是直接法/光度误差关心的问题,不在位姿图优化框架的经典定义内,因此不选。