用尽量便宜、稳定、可并行的办法,持续拿到"空间分布好、时间上可跟踪、几何上可三角化"的特征。
cuVSLAM 本身就是一套偏 GPU 加速、多相机、实时视觉/视觉惯性 SLAM 的系统,支持多相机输入,并强调在边缘平台上实时运行。(arXiv)
1)RGB 先转灰度:因为前端要的是亮度结构,不是颜色
角点检测、LK/KLT 光流、本质都依赖:
- 图像梯度
- 小 patch 的亮度变化
- 结构张量
这些都是灰度强度场上的计算。
所以先 RGB → 灰度,不是"降级",而是:
- 降维,计算更便宜
- 避免颜色通道带来的冗余
- 让梯度/光流模型更稳定
本质原理就是:
特征跟踪关心的是"哪里有可观测的亮度变化",不是"这个点是什么颜色"。
2)GFTT / Shi-Tomasi:在找"两个方向都好跟踪"的点
Shi-Tomasi 的核心不是找边,而是找角点。
因为光流要稳定,要求一个 patch 在 x/y 两个方向都有足够梯度。
如果只有一个方向有变化,那就是边,沿边方向会退化,光流会漂。
所以结构张量的最小特征值 ( \lambda_{\min} ) 大,表示:
- 这个 patch 在两个方向都有信息
- 位移估计条件更好
- 更适合 LK/KLT 跟踪
你这段:
cpp
float eMin = (T - D) / 2.f;
return my_log1p(eMin);
本质上是:
- 先算最小特征值
- 再用近似 log 压缩动态范围
为什么要压缩动态范围?
因为真实图像里,强角点和普通角点响应可能差很多。
如果不压缩:
- 极强角点会"统治排序"
- 配额分配会过度偏向少数区域
- 数值范围大,不利于后续工程实现
所以这不是数学本质变化,而是工程 trick:
保留"强点更强"的排序趋势,但削弱极端值的支配力。
3)8×8 分箱不是"均匀撒点",而是"受控的不均匀"
你这段最核心。
很多人以为空间均匀化 = 每个 bin 固定拿一样多的点。
但你这里不是。你这里是:
- 先按空间切 8×8
- 每个 bin 统计自己的总 GFTT 强度
accGFTT - 再按
accGFTT / sumGFTT分配 quota
所以它的本质不是"绝对均匀",而是:
在全局覆盖和局部质量之间做折中。
为什么这么设计?
如果完全按响应强度全局选:
- 点会全堆在纹理最丰富的区域
- 大片区域没点
- 后端几何条件差
如果完全每格平均分:
- 平坦区域也会硬塞垃圾点
- 浪费预算
- 跟踪质量差
所以这个配额原理可以概括为:
空间正则化 + 纹理自适应分配
也就是:
- 不让特征过度聚集
- 但也不强迫无纹理区凑数
这就是为什么你说天空不会浪费配额,边缘/角点密集区会拿更多点。
4)burn mask:本质是在做"硬核最小间距约束"
已有点先 burn 进 mask,选中新点后再 burn 一次,原理很直接:
作用 1:防止重复检测到同一局部结构
否则同一个角附近可能连续出多个非常接近的点。
作用 2:控制特征最小间距
让点云更"铺开"。
作用 3:提高独立性
太近的点观测信息高度相关,对 BA / F 矩阵 / PnP 的增益有限。
所以 burn mask 的本质是:
用一个简单的几何排斥机制,替代复杂的全局优化选点。
这是很典型的工程近似:
用一个 O(1) 的局部规则,逼近"分布均匀 + 去冗余"的目标。
5)3×3 NMS:确保拿到的是局部峰值,不是平台边缘
最大堆只能保证"候选值大",
但不能保证它在局部真的是最突出那个点。
所以还要做 3×3 NMS 检查。
原理就是:
- 如果这个点不是局部极大值
- 那它往往只是强响应区域的边缘/肩部
- 不是最稳定的中心点
这样做的好处:
- 点更稳定
- patch 中心更合理
- 跟踪和亚像素 refinement 更靠谱
6)亚像素拟合:因为前端精度不够,后端会被拖死
你这里的抛物线拟合,本质是:
- 不满足于整数像素点位
- 用局部响应曲面拟合峰值位置
- 得到更精细的中心
为什么这重要?
因为整数像素误差看起来小,但对:
- 三角化
- 重投影误差
- 小基线深度估计
- IMU 紧耦合优化
都会累积。
所以这一步本质是:
在前端用很小代价,降低后端长期的几何误差。
7)懒计算梯度金字塔:本质是"按需求值,不提前烧算力"
这段:
cpp
const int levels = min(gradPyramid.getLevelsCount(), (int)log1p(search_radius_px));
背后的逻辑是:
- 搜索范围小,不需要很多金字塔层
- 运动大,才需要从更粗层开始
- 梯度层不提前全算,只有真正跟踪用到时才算
原理上这叫:
计算复杂度和运动不确定性匹配。
也就是:
- 简单情况少算
- 困难情况多算
- 不浪费 GPU / CPU
这很符合实时系统哲学。
8)多目 primary / secondary:本质是"把多相机问题分层"
这部分不是数学原理,而是系统架构原理。
为什么不让每个相机都独立完整跟踪?
因为那样开销很大,而且多相机之间会有大量重复工作。
所以它把相机分成两类:
Primary
负责:
- 独立提特征
- 帧间跟踪
- 维护 track 生命周期
Secondary
负责:
- 只在关键帧时,从 primary 的点出发做跨相机关联
本质上就是:
先在主相机上建立"时间连续性",再在副相机上补"空间几何约束"。
这样拆分后:
- 时间维度:primary 负责
- 多视角维度:secondary 补充
- 性能大幅下降不了
- 几何信息还保住了
这是非常典型的"主从式多相机 front-end"。
9)FrustumIntersectionGraph:本质是自动发现"谁和谁真的看同一块地方"
你说的视锥体重叠建图,本质就是在回答一个问题:
哪些相机之间真的有足够共同视野,值得建立匹配关系?
因为多相机 rig 不一定是标准双目。
可能前视、侧视、斜视、深度相机混搭。
如果靠手工指定:
- 麻烦
- 不通用
- 一变 rig 就要改代码
所以它根据视锥体重叠自动建图:
- 重叠大 → 更可能共享特征
- 度数高 → 更适合作主相机
这本质上是一个基于观测覆盖率的拓扑推断。
10)跨相机 LK 的"主点偏移初始化":本质是给优化一个更接近的初值
LK/KLT 是局部迭代法。
迭代法最怕初值太差。
你这里:
cpp
offset = intrinsicsS.getPrincipal() - intrinsicsP.getPrincipal();
其实是在利用一个经验事实:
- 对已校正双目,相对位移主要沿视差方向
- 主点差大致给出一个合理初始偏移
这不一定是严格几何真值,
但足够把初始搜索窗口推到"更可能收敛"的地方。
本质原理:
局部优化算法的收敛,很大程度依赖初值。
所以这是个典型的几何先验 + 工程近似。
11)关键帧才做跨相机跟踪:本质是"不是每一条约束都值得实时计算"
跨相机跟踪有价值,但很贵。
非关键帧时,系统主要需要的是:
- 连续 odometry
- 当前局部位姿稳定
这时 primary 内部的帧间跟踪已经够用了。
而跨相机约束主要增益在于:
- 三角化
- 深度初始化
- 多视角几何稳定性
这些并不需要每一帧都做。
所以只在关键帧做,原理上是在做:
把"高价值但昂贵"的计算,放在状态更新最关键的时刻触发。
这是实时 SLAM 里非常常见的思想。
12)多 CUDA Stream:本质是把"相机对之间的独立性"变成并行性
如果 A→X、B→Y、C→Z 这些跟踪彼此独立,
那就没必要串行。
所以每个相机对单独一个 stream,本质是:
- 利用任务之间无数据依赖
- 提高 GPU 占用率
- 隐藏 kernel / memory latency
这不是算法原理,而是并行系统原理:
把图结构中的独立边,映射成 GPU 的并行执行单元。
13)三角化只做一次并持久化:本质是"3D 初始化"和"3D 更新"分离
这个也很关键。
很多人会下意识觉得:
每帧都能看到这个点,那就每帧都重新三角化一下?
但工程上没必要。
因为:
- 一旦某个 track 在较好视角/基线下已经三角化出稳定 3D 点
- 后续更多帧主要是"继续观测它"
- 这些观测应该进入后端优化更新 3D 点
- 而不是前端反复从头几何求交
所以这套做法的本质是:
首次三角化
做 初始化
后续复用
做 状态延续
真正更新
交给 BA / 后端优化
这就是为什么"一次三角化终身复用"是合理的。
不是说 3D 永远不变,而是说:
前端不重复做初始化求解,后端再统一优化。
一句话总原理
你这整套东西,背后的统一原理可以概括成 4 个词:
1. 可观测性
只选适合跟踪、适合几何求解的点。
2. 分布性
点不能全扎堆,要覆盖视野。
3. 延续性
track 要能跨时间持续,而不是每帧重来。
4. 计算受控
只在必要时做昂贵操作,并且尽量并行。
更直白地说
它不是在追求:
- 每一帧找最多点
- 每个相机都全量处理
- 每一时刻都做最完整几何
而是在追求:
用最小实时成本,持续维护一批"质量够高、分布够好、寿命够长、能支持后端优化"的 track。
这就是 cuVSLAM 这类工程系统的核心哲学。
官方文档和论文也都强调它面向多相机、GPU 加速、实时运行,并把多相机观测和 landmark 图结构作为鲁棒性的关键来源。(arXiv)