Files
gaojie 0b12ff023c
Sync to site1 / sync (push) Has been cancelled
chore: update PRISM categories from worldmodel to PRISM
2026-05-21 02:25:22 +08:00

502 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: "Chapter 02 — 四层空间记忆架构"
date: 2026-05-20
draft: false
tags: ["PRISM", "世界模型", "空间记忆", "SLAM", "点云", "机器人"]
categories: ["PRISM"]
---
# Chapter 02 — 四层空间记忆架构
> 本章目标:把机器人大脑里的"空间记忆"拆成 **L1–L4 四层**,逐层说清楚:**存什么 / 怎么存 / 谁写 / 谁读 / 何时过期。**
---
## 2.1 为什么是四层?而不是 1 层、3 层、7 层?
### 不能是 1 层的原因
单一表示(如纯点云)既不能高频更新(点云重写太慢),也不能直接被 LLM 查询(没有语义)。
### 不能是 3 层的原因
若合并 L3 拓扑与 L4 语义为一层,**路径规划**(需要拓扑边权)与**问答**(需要属性查询)会互相干扰。
### 不能更多(如 7 层)的原因
分层越多,**层间同步成本越高**。四层来自三个现实约束的交集:
```mermaid
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 四层全景图
```mermaid
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
```
记住三条直觉:
1. **越往右越"抽象"**:L1 是像素和位姿,L4 是 "the lamp is on the nightstand"
2. **越往右越"慢"**:L1 ms 级覆盖,L4 小时级更新
3. **越往右越"小"**L1 几 GB/小时,L4 几 MB/全场景
---
## 2.3 L1 — 感知缓冲 (Perceptual Buffer)
### 2.3.1 定位
"机器人最近几秒看到的所有东西"——纯粹的**工作记忆**,类似人的视觉残留。
### 2.3.2 存什么
```python
@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`](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 新原则)](#27b-l2-l3-数据流路由-vs-内容v15-新原则) 取代。L2 几何在 v1.5 起仅用作**路由信号**,不再承担"决定节点内容"的职责。旧描述保留以备历史追溯。
### 2.4.1 定位
"3D 几何长什么样"——机器人能在上面**规划路径、避障、渲染**的稠密表示。
### 2.4.2 存什么(多重表示)
```python
@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 关键规则:先验保护
```python
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 新原则)](#27b-l2-l3-数据流路由-vs-内容v15-新原则) 取代。L3 节点的 `clip_embedding` / `polygon` / 锚点描述等**内容**改为从 L1 高质量 keyframe 直接获取;L2 仅决定"写哪个节点"。旧描述保留以备历史追溯。
### 2.5.1 定位
"房间和走廊怎么连"——机器人**长距离导航**的骨架。
### 2.5.2 存什么
```python
@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 存什么
```python
@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`](05_pipeline_B_relocalization.md))。
### 2.6.5 关系(Edge)的两种来源
```python
# 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 写入顺序(数据如何流入大脑)
```mermaid
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 怎么读大脑)
最常见的查询是 **"找东西 + 怎么去"**,标准流程:
```python
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 帧→写 deltaConsolidator 确认才删墙 |
| iPhone 没标家具,ZED 检到家具 | 立即写 L4 新节点,但 `confidence` 起始 0.3,多次确认才上升 |
| iPhone 标了 lampZED 检到 lamp 但位置偏 30 cm | ZED 微调 `pose`confidence 加权平均 |
| iPhone 标了 chairmobile=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_lyra_inspirations.md)(尤其是 §18.2)。在此之前,PRISM 的 L2 既负责几何稠密表示,又承担"把 voxel 内容写入 L3 节点"的合成职责,导致 L3 节点的视觉指纹(CLIP 嵌入)、bounding box、文字描述等都受 L2 voxel 量化精度限制;新原则把这两件事彻底解耦。
### 2.7b.1 路由 vs 内容:两条数据流分工
```mermaid
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 路由式写入伪代码
```python
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 内容更新](06_pipeline_C_online_perception.md#66b-v15-新写法keyframe-based-内容更新代替-l2-几何反推) |
---
## 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 (3264 GB) 上跑。
---
## 2.9 本章小结
| 层 | 一句话 | 主写者 | 平均寿命 |
|----|--------|--------|----------|
| **L1** 感知缓冲 | 最近几秒的位姿+深度 | ZED 2i | 10 秒 |
| **L2** 度量 | 3D 几何(导航/渲染用) | iPhone 静态 + ZED 增量 | 月级 |
| **L3** 拓扑 | 房间-走廊连通图 | iPhone | 永久 |
| **L4** 语义 | 物品-关系场景图 | iPhone 大件 + ZED+VLM 小件 | 小时~季度 |
四层不是平行的,而是**沿"频率×抽象"轴展开的连续谱**。
读完本章你应能:
- ✅ 解释每一层"是什么"
- ✅ 解释每一层"由谁主写、谁主读"
- ✅ 理解层间写入冲突的仲裁规则
- ✅ 估算一个 10 房间场景的存储预算
下一章 [`03_data_schema.md`](03_data_schema.md) 给出可直接复制运行的 Python `dataclass` schema + 序列化格式。
---
**章节版本**v1.0
**估计阅读时间**18 分钟
**关键收获**:四层结构的"存什么/谁写谁读/何时过期"完整规则