20 KiB
title, date, draft, tags, categories
| title | date | draft | tags | categories | |||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
| Chapter 02 — 四层空间记忆架构 | 2026-05-20 | false |
|
|
Chapter 02 — 四层空间记忆架构
本章目标:把机器人大脑里的"空间记忆"拆成 L1–L4 四层,逐层说清楚:存什么 / 怎么存 / 谁写 / 谁读 / 何时过期。
2.1 为什么是四层?而不是 1 层、3 层、7 层?
不能是 1 层的原因
单一表示(如纯点云)既不能高频更新(点云重写太慢),也不能直接被 LLM 查询(没有语义)。
不能是 3 层的原因
若合并 L3 拓扑与 L4 语义为一层,路径规划(需要拓扑边权)与问答(需要属性查询)会互相干扰。
不能更多(如 7 层)的原因
分层越多,层间同步成本越高。四层来自三个现实约束的交集:
flowchart TB
subgraph C["认知科学"]
direction LR
C1["Working"] <--> C2["Short-term"]
C2 <--> C3["Long-term"]
end
subgraph S["SLAM 工程"]
direction LR
S1["Metric"] <--> S2["Topological"]
S2 <--> S3["Semantic"]
end
subgraph R["机器人控制"]
direction LR
R1["Reactive (Hz 级)"] <--> R2["Deliberative (秒级)"]
end
subgraph L["PRISM 四层(三家传统的最小公倍数)"]
direction LR
L1["L1<br/>感知缓冲"] --- L2["L2<br/>度量"] --- L3["L3<br/>拓扑"] --- L4["L4<br/>语义"]
end
C -. 分化 .-> S
S -. 加感知缓冲 .-> R
R -. 取并集 .-> L
style L fill:#fff7d6,stroke:#c97a00,stroke-width:2px
四层是这三套传统在工程上的最小公倍数。
2.2 四层全景图
flowchart LR
subgraph PRISM["PRISM Spatial Memory(左:高频・易变・低抽象 → 右:低频・稳定・高抽象)"]
direction LR
L1["<b>L1 感知缓冲</b><br/>Perceptual<br/>──────────<br/>位姿环形缓冲<br/>深度环形缓冲<br/>──────────<br/>ZED 2i 主写<br/>更新:30 Hz<br/>寿命:ms 级"]
L2["<b>L2 度量</b><br/>Metric<br/>──────────<br/>占据栅格<br/>TSDF<br/>3DGS<br/>──────────<br/>iPhone+ZED 共写<br/>更新:5 Hz<br/>寿命:秒级"]
L3["<b>L3 拓扑</b><br/>Topological<br/>──────────<br/>房间节点 + 边<br/>锚点列表<br/>──────────<br/>iPhone 主写<br/>更新:低频<br/>寿命:分钟级"]
L4["<b>L4 语义</b><br/>Semantic<br/>──────────<br/>场景图 Neo4j<br/>bbox 属性<br/>──────────<br/>iPhone+ZED 共写<br/>更新:极低频<br/>寿命:小时级"]
L1 --> L2 --> L3 --> L4
end
style L1 fill:#fde2e2,stroke:#a33
style L2 fill:#fff1c1,stroke:#a87a00
style L3 fill:#d4f0d4,stroke:#2e7d32
style L4 fill:#d8e4ff,stroke:#1565c0
记住三条直觉:
- 越往右越"抽象":L1 是像素和位姿,L4 是 "the lamp is on the nightstand"
- 越往右越"慢":L1 ms 级覆盖,L4 小时级更新
- 越往右越"小":L1 几 GB/小时,L4 几 MB/全场景
2.3 L1 — 感知缓冲 (Perceptual Buffer)
2.3.1 定位
"机器人最近几秒看到的所有东西"——纯粹的工作记忆,类似人的视觉残留。
2.3.2 存什么
@dataclass
class PerceptualFrame:
timestamp: float
pose: Pose # T_robot→map (ZED VIO 输出)
rgb_left: np.ndarray # (H,W,3) 可选保存
depth: np.ndarray # (H,W)
imu_packet: List[IMUSample] # 自上一帧以来的 IMU
keypoints: Optional[np.ndarray] # ORB/SuperPoint 关键点
class L1Buffer:
capacity_seconds: float = 10.0 # 环形缓冲容量
keyframe_interval: float = 0.5 # 关键帧采样间隔
ring: Deque[PerceptualFrame] # 满则覆盖
keyframes: Deque[PerceptualFrame] # 关键帧池(保留更久)
2.3.3 写者 / 读者
| 角色 | 频率 | 操作 |
|---|---|---|
| 写:ZED 2i 驱动 | 30 Hz | ring.append(frame) |
| 读:避障 / 局部规划 | 10 Hz | 取最近 1 s 的 frames |
| 读:L2 融合器 | 5 Hz | 取一个关键帧融入 TSDF |
| 读:回环检测 | 1 Hz | 与关键帧池做相似度匹配 |
2.3.4 过期规则
- 超过
capacity_seconds的非关键帧 → 直接丢弃 - 关键帧:若已成功融入 L2 → 5 分钟后可丢弃
- 全部仅在内存,不落盘(除非 debug)
2.3.5 失效与降级
- VIO 跟丢 → L1 暂停接收 → 触发重定位(见
05_pipeline_B_relocalization.md) - IMU 异常 → 仅用视觉位姿,标记
confidence=low
2.4 L2 — 度量记忆 (Metric Memory)
⚠️ v1.5 更新:本节后续提及"L2 voxel 直接生成 L3/L4 节点内容"的写法已被 § 2.7b L2-L3 数据流:路由 vs 内容(v1.5 新原则) 取代。L2 几何在 v1.5 起仅用作路由信号,不再承担"决定节点内容"的职责。旧描述保留以备历史追溯。
2.4.1 定位
"3D 几何长什么样"——机器人能在上面规划路径、避障、渲染的稠密表示。
2.4.2 存什么(多重表示)
@dataclass
class L2Memory:
# 用途 1:导航——2D/2.5D 占据栅格
occupancy_grid: OctoMap # 5 cm 分辨率,0=空 1=占 -1=未知
# 用途 2:精细几何——TSDF / Mesh
tsdf: VoxelBlockGrid # 2 cm 分辨率
mesh_uri: str # 离线烘焙的 .glb
# 用途 3:渲染 / 视觉相似度——3DGS
gaussians_uri: Optional[str] # .ply,每房间一份
# 用途 4:先验掩膜
prior_mask: np.ndarray # 0=ZED 可写 1=iPhone 静态保护
no_update_zone: np.ndarray # 1=镜面/玻璃,禁止写入
为什么要 4 套表示?
| 表示 | 谁用 | 为什么不能替代 |
|---|---|---|
| OctoMap | 路径规划器 | 路径规划只关心"能不能走",cm 级足够 |
| TSDF | 差异检测 / 抓取 | 需要带符号距离才能算 SDF 差 |
| Mesh | 可视化 / Unity 仿真 | 渲染流水线友好 |
| 3DGS | 视觉重定位 / 新视角生成 | 比 mesh 真实得多 |
它们共享同一份原始点云,只是不同的"派生视图"。
2.4.3 写者 / 读者
| 区域 | 主写者 | 来源 | 频率 |
|---|---|---|---|
| 静态结构(墙、门、固定家具) | iPhone | RoomPlan mesh | 1 次/场景 |
| 可变区域(家具间空隙、地面) | ZED 2i | TSDF 增量 | 5 Hz 局部 |
| 新发现区(iPhone 没扫到) | ZED 2i | TSDF 新建 voxel | 5 Hz |
| 镜面/玻璃 | 都不写 | — | — |
2.4.4 关键规则:先验保护
def fuse_zed_to_l2(zed_tsdf_local, l2: L2Memory):
for voxel in zed_tsdf_local:
if l2.no_update_zone[voxel.idx]:
continue # 镜面区,跳过
if l2.prior_mask[voxel.idx]:
# 先验区:只允许微调(低权重)
l2.tsdf.update(voxel, weight=0.1)
else:
# 自由区:正常融合
l2.tsdf.update(voxel, weight=1.0)
→ iPhone 提供的墙不会被一次 ZED 噪声毁掉,但小幅度(< 5 cm)的修正可累积起效。
2.4.5 过期规则
- iPhone 写入的体素 → 长期保留,但有
last_seen字段 - ZED 写入的体素:30 天内未被再次确认 → 衰减 confidence
- 若某区域被打上
delta(家具搬走)→ 在 Consolidator 跑后才物理移除
2.5 L3 — 拓扑记忆 (Topological Memory)
⚠️ v1.5 更新:本节中"由 L2 几何(TSDF/OctoMap)反推生成 L3 节点内容"的旧写法已被 § 2.7b L2-L3 数据流:路由 vs 内容(v1.5 新原则) 取代。L3 节点的
clip_embedding/polygon/ 锚点描述等内容改为从 L1 高质量 keyframe 直接获取;L2 仅决定"写哪个节点"。旧描述保留以备历史追溯。
2.5.1 定位
"房间和走廊怎么连"——机器人长距离导航的骨架。
2.5.2 存什么
@dataclass
class L3Node:
uid: str # "room_301"
label: str # "Bedroom" / "Hallway" / "Lobby"
center: np.ndarray # (3,) 房间几何中心
polygon: np.ndarray # (N,2) 房间地面多边形
anchors: List[str] # 引用 L4 中的家具 uid(如 'bed_301')
clip_embedding: np.ndarray # (512,) 房间整体视觉指纹
@dataclass
class L3Edge:
src: str # "room_301"
dst: str # "hallway_3F"
via: str # 连接介质:'door_301a' / 'open_passage'
cost: float # 通行成本(距离 + 难度)
bidirectional: bool = True
class L3Memory:
nodes: Dict[str, L3Node]
edges: List[L3Edge]
anchor_index: Dict[str, str] # furniture_uid -> room_uid 反向索引
2.5.3 写者 / 读者
| 角色 | 何时 | 操作 |
|---|---|---|
| 写:iPhone 解析器 | 离线一次 | 按 RoomPlan 的 room 切分自动生成节点;门窗作为边 |
| 写:ZED 2i + 巡逻 | 机器人实际穿过门时 | 确认/新增边,更新 cost |
| 读:高层规划器 | 任务下发时 | 在 L3 图上跑 A* / Dijkstra 得"房间序列" |
| 读:重定位器 | 上电 / 跟丢时 | 用 clip_embedding 做粗匹配 |
2.5.4 过期规则
- 房间节点:除非装修,否则永久保留
- 边:连续 N 次(默认 3 次)巡逻都走不通 → 标记
deprecated,路径规划器跳过 - 锚点:被 ZED 检测到原家具消失 → 从
anchors中移除,但节点不删
2.5.5 与 L4 的区别(容易混淆!)
- L3 是"地理":节点是地方(房间、走廊),边是通行关系
- L4 是"物品":节点是东西(床、灯、遥控器),边是支撑/包含/邻接关系
- L3 节点
room_301通过anchors字段指向 L4 节点bed_301,tv_301...
2.6 L4 — 语义记忆 (Semantic Memory)
2.6.1 定位
"哪个东西在哪儿、长啥样、跟谁挨着"——机器人理解任务和与人对话的底座。
2.6.2 存什么
@dataclass
class L4Node:
uid: str # 'bed_301', 'lamp_301_a'
label: str # 'bed' / 'lamp' / 'remote_control'
category: str # 'furniture' / 'appliance' / 'small_item'
pose: Pose
bbox_3d: np.ndarray # (8,3) OBB
mesh_uri: Optional[str]
clip_embedding: np.ndarray # (512,)
attributes: Dict # {color, material, state(on/off/open/closed),
# mobile: bool, fragile: bool, ...}
parent_room: str # L3 房间 uid
source: Literal['iphone','zed2i','vlm','fused']
first_seen: float
last_seen: float
observation_count: int
confidence: float
@dataclass
class L4Edge:
src_uid: str
dst_uid: str
relation: Literal['on','under','in','next_to',
'inside_drawer','plugged_into',...]
confidence: float
class L4Memory:
nodes: Dict[str, L4Node]
edges: List[L4Edge]
spatial_index: Optional[KDTree] # 加速"附近的东西"查询
semantic_index: Optional[Faiss] # CLIP 向量库,加速文本查物体
2.6.3 写者 / 读者
| 角色 | 频率 | 操作 |
|---|---|---|
| 写:iPhone 解析器 | 离线一次 | 16 类家具直接落库(高 confidence) |
| 写:ZED + VLM | 2 Hz | 检出物品,与现有节点匹配或新建 |
| 写:Consolidator | 充电时 | 确认 delta 并固化 |
| 读:LLM Agent | 按需 | find("遥控器在哪") → CLIP 检索 + 关系遍历 |
| 读:抓取规划器 | 任务时 | 取目标 bbox_3d + attributes.fragile |
| 读:渲染器 | 可视化 | 取所有 mesh_uri |
2.6.4 三类节点的不同生命周期
| 类型 | 例子 | mobile | 谁写 | 多久过期 |
|---|---|---|---|---|
| 固定结构 | 墙、门、内嵌衣柜 | false | iPhone | 永不 |
| 大件家具 | 床、沙发、书桌 | half | iPhone 主,ZED 校准 | 季度级 |
| 小物品 | 遥控器、水杯、毛巾 | true | ZED + VLM 主 | 小时级 |
mobile 标签直接影响重定位是否能用它当 anchor(见 05_pipeline_B_relocalization.md)。
2.6.5 关系(Edge)的两种来源
# 1. 几何派生:通过 bbox 相对位置自动得出
def derive_edge_from_geometry(a: L4Node, b: L4Node) -> Optional[L4Edge]:
if a.bbox_3d.contains(b.bbox_3d.center):
return L4Edge(b.uid, a.uid, 'in', confidence=0.9)
if abs(b.bbox_min_z - a.bbox_max_z) < 0.05:
return L4Edge(b.uid, a.uid, 'on', confidence=0.85)
if a.distance_to(b) < 0.5:
return L4Edge(a.uid, b.uid, 'next_to', confidence=0.7)
return None
# 2. VLM 派生:让 GPT-4V / Qwen-VL 看图给关系
def derive_edge_from_vlm(rgb_image, detections) -> List[L4Edge]:
prompt = f"Given these detected objects {detections}, " \
f"list spatial relations as (a, relation, b)."
return vlm.parse_relations(rgb_image, prompt)
两路边都允许存在,按 confidence 加权融合。
2.7 层间交互规则
2.7.1 写入顺序(数据如何流入大脑)
flowchart TB
ZED["ZED 2i<br/>原始数据"]
VLM["VLM<br/>(在线感知)"]
L1["L1 感知缓冲"]
L2["L2 度量"]
DELTA[("delta/<br/>差异目录")]
L4["L4 语义"]
L3["L3 拓扑"]
CON["Consolidator<br/>(充电时跑)"]
IPHONE["iPhone<br/>(离线一次)"]
LTM[("L2 + L3 + L4<br/>永久保留")]
ZED -- "30 Hz 写入" --> L1
ZED -- "5 Hz 关键帧" --> L2
ZED -- "2 Hz" --> VLM
L2 -- "差异检测" --> DELTA
VLM -- "关联" --> L4
DELTA -- "充电时确认" --> CON
CON --> L4
L4 -- "更新 anchors" --> L3
IPHONE -- "离线一次" --> LTM
style L1 fill:#fde2e2,stroke:#a33
style L2 fill:#fff1c1,stroke:#a87a00
style L3 fill:#d4f0d4,stroke:#2e7d32
style L4 fill:#d8e4ff,stroke:#1565c0
style CON fill:#ffe9b3,stroke:#c97a00
style DELTA fill:#f5e1ff,stroke:#7b1fa2
2.7.2 查询顺序(Agent 怎么读大脑)
最常见的查询是 "找东西 + 怎么去",标准流程:
def query_find_and_navigate(text: str, memory):
# 1. L4 语义搜索:文本 → 物品 uid
obj_uid = memory.l4.semantic_search(text) # CLIP
obj = memory.l4.nodes[obj_uid]
# 2. L3 拓扑规划:当前房间 → 物品所在房间
cur_room = memory.l3.locate(memory.l1.current_pose)
path_rooms = memory.l3.astar(cur_room, obj.parent_room)
# 3. L2 度量规划:物品所在房间内的精确路径
metric_path = memory.l2.plan_path(
start=memory.l1.current_pose,
goal=obj.pose,
room_sequence=path_rooms)
# 4. L1 实时跟随 + 避障
follow_path(metric_path)
return obj
→ 从右向左下钻:抽象 → 具体,是 PRISM 查询的标准范式。
2.7.3 写入冲突的仲裁
当 iPhone 和 ZED 对同一区域有不同观测:
| 情况 | 仲裁规则 |
|---|---|
| iPhone 说有墙,ZED 说没墙 | 单次→忽略 ZED;连续 N 帧→写 delta;Consolidator 确认才删墙 |
| iPhone 没标家具,ZED 检到家具 | 立即写 L4 新节点,但 confidence 起始 0.3,多次确认才上升 |
| iPhone 标了 lamp,ZED 检到 lamp 但位置偏 30 cm | ZED 微调 pose,confidence 加权平均 |
| iPhone 标了 chair(mobile=true),ZED 没看到 | 不立即删,标记 state='moved',consolidate 决定 |
总原则:iPhone 写入的内容"假设正确直到证据充分相反"。
2.7b L2-L3 数据流:路由 vs 内容(v1.5 新原则)
v1.5 起 PRISM 借鉴 Lyra 2.0 的核心思想——"几何只做路由,不做合成"——重新厘清 L2 与 L3 之间的职责边界。详细动机与 Lyra 2.0 的对照参见
18_lyra_inspirations.md(尤其是 §18.2)。在此之前,PRISM 的 L2 既负责几何稠密表示,又承担"把 voxel 内容写入 L3 节点"的合成职责,导致 L3 节点的视觉指纹(CLIP 嵌入)、bounding box、文字描述等都受 L2 voxel 量化精度限制;新原则把这两件事彻底解耦。
2.7b.1 路由 vs 内容:两条数据流分工
flowchart LR
L1KF["<b>L1 keyframe</b><br/>full-res RGB-D<br/>+ CLIP embedding<br/>(高保真原始观测)"]
L2GEO["<b>L2 几何</b><br/>TSDF / OctoMap<br/>(低精度,可量化)"]
L3NODE["<b>L3 节点写入</b><br/>clip_embedding<br/>polygon / anchors<br/>(高保真内容)"]
L1KF -- "内容来源<br/>(高保真)" --> L3NODE
L2GEO -. "仅作路由信号<br/>(决定写哪个节点)" .-> L3NODE
style L1KF fill:#fde2e2,stroke:#a33
style L2GEO fill:#fff1c1,stroke:#a87a00
style L3NODE fill:#d4f0d4,stroke:#2e7d32
- L2 几何 = 路由信号:仅用来判断"当前观测的 3D 位置应该归属到 L3 图里的哪个节点"。即使 L2 的 voxel 是 2 cm 量化、含噪声、甚至局部缺失,只要它能指向正确的 L3 节点 uid就够了。
- L1 keyframe = 内容源:节点的
clip_embedding、视觉证据、bounding box、属性描述等实际内容必须来自 L1 缓存里那一张未经量化的高分辨率 RGB-D keyframe(或它的特征),而非从 L2 voxel 反投影回来。
一句话总结:几何精度只需要够"指向哪个节点",不需要"决定节点内容"。
2.7b.2 路由式写入伪代码
def write_to_l3(observation, l2_geometry, l3_graph):
# ① L2 只参与"路由":根据观测的 3D 位置定位 L3 节点 uid
target_node = route_via_l2(observation.position, l2_geometry)
# ② 内容来源是 L1 高保真 keyframe,而非 L2 voxel
keyframe_evidence = get_best_keyframe(observation, l3_graph[target_node])
# ③ 用 keyframe 的原始 CLIP / bbox / patch 更新节点内容
l3_graph[target_node].update(keyframe_evidence)
四行核心逻辑里,L2 只出现在第一步(route_via_l2)且只读;真正塑造 L3 节点内容的是第二步从 L1 取回的 keyframe_evidence。这一拆分让 L2 即使在 OctoMap 5 cm + TSDF 噪声的精度下仍然足以胜任,而 L3 的语义保真度由 L1 决定。
2.7b.3 对原有章节的影响一览
| 原章节 | 旧写法 | v1.5 新原则下的对应 |
|---|---|---|
| §2.4 L2 度量 | L2 既存几何又"派生"L3 内容 | L2 仅做几何 + 路由;不再向 L3 派生内容 |
| §2.5 L3 拓扑 | clip_embedding 由 voxel 颜色聚合 |
由 L1 keyframe 直接编码 |
| §2.7.1 写入顺序图 | L2 --差异检测--> DELTA 仍然成立 |
但 L3 节点内容更新的箭头改为来自 L1,L2 只贡献节点 uid 路由 |
06_pipeline_C §6.6 |
TSDF 体素直接重投影生成 patch | 见 § 6.6b keyframe-based 内容更新 |
2.8 整体存储预算
以一个 10 房间酒店楼层为例:
| 层 | 数据 | 大小 |
|---|---|---|
| L1 | 10 s 环形缓冲(深度+RGB) | ~ 2 GB 内存 |
| L2 | 10 房间 × (OctoMap 50 MB + TSDF 100 MB + 3DGS 200 MB) | ~ 3.5 GB 磁盘 |
| L3 | 10 节点 + 边 + CLIP 向量 | ~ 100 KB |
| L4 | ~ 500 nodes (10 房 × 50 件) + 关系 | ~ 50 MB |
| 总 | ~ 4 GB 磁盘 + 2 GB 内存 |
→ 完全可以在 Jetson Orin (32–64 GB) 上跑。
2.9 本章小结
| 层 | 一句话 | 主写者 | 平均寿命 |
|---|---|---|---|
| L1 感知缓冲 | 最近几秒的位姿+深度 | ZED 2i | 10 秒 |
| L2 度量 | 3D 几何(导航/渲染用) | iPhone 静态 + ZED 增量 | 月级 |
| L3 拓扑 | 房间-走廊连通图 | iPhone | 永久 |
| L4 语义 | 物品-关系场景图 | iPhone 大件 + ZED+VLM 小件 | 小时~季度 |
四层不是平行的,而是沿"频率×抽象"轴展开的连续谱。
读完本章你应能:
- ✅ 解释每一层"是什么"
- ✅ 解释每一层"由谁主写、谁主读"
- ✅ 理解层间写入冲突的仲裁规则
- ✅ 估算一个 10 房间场景的存储预算
下一章 03_data_schema.md 给出可直接复制运行的 Python dataclass schema + 序列化格式。
章节版本:v1.0 估计阅读时间:18 分钟 关键收获:四层结构的"存什么/谁写谁读/何时过期"完整规则