XYT·星野图
首页项目归档照片墙音乐灵境说说杂谈友链关于
封面
返回上一级

雷达 z 轴怎么编码:DrivoR 的 1 bit 与 TD25A 的高度切片

写作时间:2026-08-25 11:20
# 机器人
# 端到端
# 点云
# BEV
# DrivoR
# TD25A

状态:源码解读 + 方案定稿,非已实现功能。 DrivoR 侧 2026-08-25 对 valeoai/DrivoR@main 逐行核对; TD25A 侧几何引自 td25a.urdf.xacro 与 nav2_params.yaml。

一眼版

DrivoR 原样 问题 TD25A 的做法
高度 0.2 m 切一刀,2 通道 桌腿和桌面同通道 → 桌下一律当障碍 按车体几何切 3 档(0.03 / 0.15 / 1.01 / 2.00 m)
每格的值 点密度,截顶 5 近场全饱和成 1.0,反映距离不是障碍 3 档占据(对数)+ 全局 z_min / z_max = 5 个数
时间维 无,逐帧独立 360° 扫描转一圈障碍就闪 从 STVL 三维体素读,射线清除 + 2 s 衰减白送
验证状态 官方从没跑过(lidar_pc: [],论文纯相机) 未测试代码,不能当"已验证做法"引 全部要自己从零验

最终喂给 ViT 的是一张图,和覆盖染色图并在同一条支路:

[静态图, tanh覆盖, frontier, occ_a, occ_b, occ_c, z_min, z_max]
                  8 通道 @ 224 × 224 @ 0.05 m
              → DINOv3 ViT-S/16 → 16 个 register token

一、DrivoR 怎么处理雷达

神经网络吃不了"一堆点",它要图片那样的规整方格。所以整条流水线只干一件事:把点云变成图片。五步:

几万个 (x,y,z)
 ① 切掉 z > 100 m                       ← 死开关,什么也没滤
 ② 按 0.2 m 分成 below / above 两堆      ← 全流程唯一一次用到高度
 ③ 各自数格子里的点数
    np.histogramdd(pc[:, :2])           ← [:, :2] 丢掉 z,高度在此永久消失
 ④ 超过 5 个点一律当 5,再 ÷5 归一化
 ⑤ 两张图叠成 2 通道 → 518×518
           ↓  和相机图同路
   ViT → 16 个 register token → 解码器
lidar_min_x/max_x: ±35 m       lidar_image_size: [518, 518]   # 13.5 cm/格
lidar_split_height: 0.2        lidar_hist_max_per_pixel: 5
lidar_max_height: 100.0        lidar_pc: []                   # 默认整条支路关闭

关键在第 ③ 步。 一个格子里可能落了 9 个点,有的在 0.3 m、有的在 1.5 m——格子里只写了个 9。它们各自多高,永远查不回来了。

拿本机真实 Mid360 点云(maps/test.pcd,27,580 点)跑一遍:

DrivoR 五步处理真实 Mid360 点云

第 0 步的颜色是高度(蓝低橙高),到第 3–4 步只剩单色——高度消失的那一刻就是 [:, :2] 那一行。分堆也极不均匀:z ≤ 0.2 m 只有 2,717 点,z > 0.2 m 有 12,083 点,室内几乎所有结构(墙、家具、桌面)全挤在 above 一个通道里。

这套约定来自 Transfuser(源码注释写明),为高速公路尺度设计。

二、但这条支路官方从没跑过

DrivoR 论文是纯相机的。 lidar_pc: [] 不是"某个实验关掉了"——仓库里只有这一份 agent config,摘要与实验全部围绕 camera register token,激光一次都没出现。

两条证据说明它不只是"没启用",而是没跑通过:

① 整段抄自 TransFuser,连注释一起。

# NOTE: Code from
# https://github.com/autonomousvision/carla_garage/blob/main/team_code/data.py#L873

两个语义参数(0.2 m 分档、截顶 5)一个没动;连 max_height_lidar: 100.0 这个在源头就已经是空的死开关,也一并抄了过来。

② 有个 bug 说明它从没单独跑过。

self.lidar_scene_embeds = nn.Parameter(torch.randn(
    1, self.num_lidar, num_scene_tokens,
    self.image_backbone.num_features) * 1e-6)   # ← 引用 image_backbone

image_backbone 只在 num_cams > 0 时创建,"只开激光不开相机"会直接 AttributeError。另有 use_grid_mask = True 写死——GridMask 随机抠掉方格,用在占据栅格上等于随机擦掉障碍物,接这条支路必须先关。

但管道本身是通的。 激光和相机用同一个类 ImgEncoder,出口完全对称,模型里一行 torch.cat 拼起来,下游不区分 token 来自哪个传感器。改 in_chans 也不用手动做 patch_embed 手术,timm 已处理。

技术拆解可信(逐行核对过),但它是未测试代码——我们的改法要自己从零验。

三、为什么这套对室内不成立

TD25A 的真实几何:车顶 1.008 m · Livox 装在 0.951 m · 清扫机构幅宽 0.736 m · STVL 最低障碍高度 0.03 m。

可穿越判据由此确定:净空 > 1.01 m 才钻得过去。

而 0.2 m 一刀切,把"拖鞋"和"桌子"放进了同一个通道。看一张桌子:

DrivoR 的 above 通道       真实情况
┌──────────────┐          ┌──────────────┐
│██████████████│          │▓            ▓│   ▓ = 桌腿
│██████████████│    vs    │              │   空白 = 桌下(能钻)
│██████████████│          │▓            ▓│
└──────────────┘          └──────────────┘
     一整块实心              只有四角是实的

桌腿和桌面都在 0.2 m 以上,桌面被投影下来把整个桌下涂满了。于是 0.75 m 的矮桌(钻不过去)和 1.20 m 的高桌(该进去扫)在网络眼里长得一模一样——区分它们的唯一信息在第 ③ 步就被丢了。

结果是所有桌下床下一律当障碍绕开,永远不扫,而这正是室内漏扫的大头。

四、TD25A 的方案(定稿)

4.1 处理步骤

输入一帧点云(就是一个 .pcd,N × 3),输出一个 (5, 224, 224) 的 float32 张量:

输入:P = {(x, y, z)}          livox_frame
     地面平面 (n, d)           ← fusion_node 已有,不用自己拟合

① 转到车体系      P_b = T(livox → base_footprint) · P        静态 TF
② 算对地高度      h = n · p + d                              点到地面的有符号距离
                 丢掉 h < 0.03 或 h ≥ 3.0 的点               地面回波 / 天花板
③ 落格(车头朝上) row = floor((5.6 − x_b) / 0.05)            车头 → 图像上方
                 col = floor((5.6 − y_b) / 0.05)            ROS +y 是左
                 丢掉 row/col 不在 [0,224) 的点
④ 每格聚合       遍历一次,每个点更新它落到的那一格:
                   k = bucket(h)      a:0.03–0.15   机构撞击带
                                      b:0.15–1.01   车体碰撞带
                                      c:1.01–2.00   低悬空物
                   n[k][row][col] += 1
                   zmin[row][col] = min(zmin, h)
                   zmax[row][col] = max(zmax, h)
⑤ 数值化         occ_k = log(1 + n_k) / log(21)              → [0,1]
                 z_min = clip(zmin / 2.0, 0, 1)   空格填 1.0
                 z_max = clip(zmax / 2.0, 0, 1)   空格填 0.0
⑥ 输出           stack → (5, 224, 224) float32

注意 ③ 不需要旋转——点已经在车体系,车头就是 +x,只是轴的重排。要旋转的是全局层(4.3)。

4.2 五个通道与分档依据

通道 内容 回答什么
occ_a 0.03 – 0.15 m 占据 线缆、袜子 —— 会卷进清扫机构
occ_b 0.15 – 1.01 m 占据 车体碰撞带 —— 撞哪儿都一样,绕
occ_c 1.01 – 2.00 m 占据 低悬空物(桌面、吧台、横梁)—— 能过去
z_min 该格最低回波的对地高度 净空余量还剩多少
z_max 该格最高回波的对地高度 4 cm 门槛还是 14 cm 线缆

判据:

occ_a = occ_b = 0  且  occ_c > 0   →   悬空,可钻
occ_a 或 occ_b > 0                 →   实体障碍,绕

分档依据得诚实说:只有两条边界是几何决定的。

边界 依据
0.03 STVL min_obstacle_height,传感器下限 ✅
1.01 车顶 1.008 m —— 过不过得去的唯一阈值 ✅
2.00 天花板截断(见 4.3 的实测) ⚠️
0.15 推的,理由是它对应一个不同的失效模式:线缆卷进机构 ≠ 车体蹭到椅腿 ⚠️

早先的四档版本在 0.60 还切了一刀,分开"车体下半/上半"——砍掉了: 撞在 0.3 m 还是 0.8 m,对平面机器人结果完全相同,切了不产生新动作。

分几档最终应该由消融定(第五节),不该拍脑袋。

4.3 真跑一遍

test.pcd 走完上面六步,出来的 (5, 224, 224) 长这样:

雷达处理的五通道输出

五张全部由真实点云算出。第六张绿框的不是输出通道,是拿判据算出来的结果,放这儿方便对照。 建图点云抽稀过(窗口内 3,078 点、2,157 格有回波),实车单帧局部会密得多。

这五个通道就是雷达支路的全部产出。它们之后才和覆盖染色图拼在一起(见 4.5)—— 那四个通道是任务记忆,不是雷达处理的产物,本节不涉及。

这一跑发现一个必须改的定义。 顶档原本写的是"> 1.01 m"——室内每一格头上都有天花板, 这个通道会被点亮 83% 的格子,等于没有信息:

顶档定义 命中格数(有回波 4,909 格中)
> 1.01 m(原定义) 4,088(83%)—— 其中 73% 的最低回波在 3 m 以上,就是天花板
1.01 – 2.00 m(改后) 733(19%)—— 真正的低悬空物

所以顶档必须封顶。点云进来前也要按 STVL 的 max_obstacle_height: 3.0 截掉天花板, 否则 z_min 会被天花板顶满,同样失去区分度。

这条只有真跑一遍才看得出来——纸上推的时候"> 1.01 m 就是可穿越"看着完全合理。

4.4 三个数值约定的理由

各一句:

  • 占据取对数不取截顶 —— 截顶 5 在近场全饱和成 1.0(2.4 的问题);二值化又会丢掉"证据强不强"
  • 高度必须对地不对雷达 —— 1.2° 建图倾斜摊到 ±5.6 m 是 5.6 × tan(1.2°) ≈ 0.117 m,跟 occ_a 整条带一样宽,绝对阈值在边缘会串档
  • z_min 空格填 1.0 不填 0 —— 填 0 会被读成"最低点贴地",网络以为有障碍
  • 只留 2 个高度通道,不是每档一套(12 个) —— 分档已经解决"分类",数值只需回答"余量多大",一个全局 z_min 就够

4.5 和覆盖染色图并成一条支路

覆盖染色是全局的(map 系),雷达是局部的(odom 系滚动窗口)。不是叠加,是两条路搬进同一个 ego 坐标系:

全局层  静态图 / tanh覆盖 / frontier(3)→ warpAffine 裁 + 转 ─┐
                                                             ├→ 224² ego 对齐 → stack → 8 通道
局部层  occ_a / occ_b / occ_c / z_min / z_max(5)→ 以车为中心,只需转 ─┘

一次 patch_embed,一个 ViT,不拆两条支路。 因为"这格没扫过 且 这格能钻"是个逐格的与运算:同一张图上,16×16 卷积核第一层就同时看到两者;拆开就得从两组各 16 个全局摘要里反推逐格对应关系——压缩之后"哪一格"已经没了。

为什么没有"实时占据"这一层

早先的契约里还有第四个全局通道——直接取 local costmap 的实时占据。已砍。

它不是冗余(occ_a..c 已经是同一份实时雷达占据,只是按高度分了档),而是有害:

inflation_layer:
  inflation_radius: 0.55     # ÷ 0.05 m = 11 格
  cost_scaling_factor: 3.0

costmap 出来的占据带膨胀,每个障碍被抹开 11 格。桌腿之间的空隙、椅腿间能不能钻—— 恰恰是膨胀会抹掉的东西,而这正是高度分档想暴露的东西。 两个通道放在一起, 一个在造信息一个在毁信息。

顺带说清一个会被问到的问题:膨胀是规划器的概念,不是感知的概念。 TransFuser、BEVFormer、占据网络喂进网络的全是原始几何,没有一个先做膨胀。

那"按车体几何分高度档"不也是把机器人几何烘进表示里了吗?不一样:

膨胀 高度分档
烘进去的 车的水平尺寸 车的垂直尺寸
方式 破坏性——自由格被改写成代价,原边界不可恢复 非破坏性——每格照实报告,几何只决定分箱边界放哪
能还原真实占据 否 是

膨胀留在原地——给 06 的安全否决层用,不进策略输入。

4.6 和现有系统怎么接

查 nav2_params.yaml,三条比预想的顺:

现有配置 需求 差距
局部代价地图 11 × 11 m @ 0.05 = 220² 224² 差 2 格/边,改成 11.2 即对齐
雷达标记半径 obstacle_range: 4.0 ±5.6 m 外圈 1.6 m 无实时数据 → 靠静态图兜底
地面参考 fusion_node 已按实时地面平面过滤 需要对地高度 已存在,取平面参数即可
三维体素 voxel_size: 0.05 · voxel_decay: 2.0 · publish_voxel_map: false 带记忆的高度分档 一直在算,只是不往外发,打开开关即可

最后一条最省事:射线清除和 2 秒衰减白送,整条自建点云管线可以不做。

一条要记账的风险:覆盖染色在 map 系、局部层在 odom 系,map→odom 一漂移,"已扫区"和"看到的障碍"就错位,网络会看到墙压在已扫格子上。这给 ego 向量里那一维定位置信度提供了具体的失效形式。

五、贡献点与待定

值得写进论文的不是"我们加了通道",而是:

BEV 的高度分层边界由机器人本体几何唯一确定,使"可穿越性"成为输入的显式可读量,而非需要网络从密度分布中反推的隐含量。

得先让出两块:RangeViT(CVPR 2023)证明了图像预训练 ViT 能读激光栅格;ViCo3D(arXiv 2607.12959, 2026-07)已经用 dinov2_vits14 读 3 通道 BEV 图,其中一个通道就是每格最高点高度。所以"点云栅格喂预训练 ViT"和"每格存最高点高度"都不新了。

能立住的是更窄的那句:按本体几何切成离散高度带、使可穿越性成为判据可直接读出的量,面向室内覆盖任务。

骨干选 DINOv3 ViT-S/16 —— 但不是改个名字那么简单。

2026-08-25 在服务器上用真实 BEV 张量实跑了一遍(timm 1.0.22 + torch 2.2;当时按 9 通道跑的,通道数不影响下面的结论):

DINOv2 ViT-S/14 DINOv3 ViT-S/16
patch_embed.proj (384, 9, 14, 14) ✅ timm 自动适配 (384, 9, 16, 16) ✅
patch 数 @224 16×16 = 256 14×14 = 196
pos_embed (1, 256, 384) None —— DINOv3 用 RoPE
输出 token(含 16 scene) 277 ✅ 196 —— scene token 被丢了 ❌
CPU 单帧 65 ms 62 ms

问题出在 DrivoR 自己的 _pos_embed 第一行:

def _pos_embed(self, x, scene_tokens=None):
    if self.pos_embed is None:
        return x.view(x.shape[0], -1, x.shape[-1])   # ← 早退,scene_tokens 直接被忽略

DINOv3 没有可学习的绝对位置编码(用 RoPE),于是这一行早退,16 个 scene token 根本没拼进去。 而且不报错——下游 tokens[:, :16] 会取到前 16 个 patch(图像左上角那一块), 当成"全局场景摘要"接着往下算。

DrivoR 论文的核心机制(register token)在 DINOv3 上是静默失效的。 白名单里放着 DINOv3,但这条路显然也没人跑过。

要用 DINOv3,得先补 _pos_embed 的 RoPE 分支:pos_embed 为 None 时跳过加法, 但仍要拼 cls / reg / scene token。几行的事,但必须补。

(卫星版 sat493m 视角更对,但官方只发布了 ViT-L 和 ViT-7B,没有 small,上不上得了 Orin NX 待测。)

待定三项:

  • 分几档——现取 3 档,中间那刀 0.15 是推的。应做 2 / 3 / 4 档消融,用桌下覆盖率对比定
  • 悬空物在真实清扫里占多大比例——上面那个贡献点成不成立全看这个数,待实机统计
  • 打开 publish_voxel_map 后的开销、体素 topic 格式够不够直接分档,待 Jetson 实测

参考

  • navsim/agents/drivoR/:drivor_features.py::_get_lidar_feature · drivor_model.py · layers/image_encoder/dinov2_lora.py · drivoR.yaml(valeoai/DrivoR@main,2026-08-25 核对)
  • TransFuser 源出处:autonomousvision/carla_garage(Jaeger et al., ICCV 2023)
  • td25a_description/urdf/td25a.urdf.xacro · turn_on_td25a_robot/config/nav2_params.yaml
  • Ando et al. RangeViT, CVPR 2023 · ViCo3D, arXiv:2607.12959, 2026-07
avatar

XYT

以文字为星,以思考为野,绘一幅属于自己的星野图。

RECOMMENDED

DrivoR 怎么处理相机:4 张图到 64 个 token

2026-08-25 18:10

想法博客

2026-08-27 23:50

机器人姿态输入对比:DrivoR 的 11 维与 TD25A 的 4 维

2026-08-26 15:30

Table of Contents