# 模型输入输出与数据预处理流程详解

> 论文：[Cross Fusion of Point Cloud and Learned Image for Loop Closure Detection](Cross_Fusion_of_Point_Cloud_and_Learned_Image_for_Loop_Closure_Detection.pdf)

---

## 1. 整体架构概览

```
┌──────────────────────────────────────────────────────────────────────┐
│                          Fusion (总模型)                              │
│                                                                      │
│  ┌──────────────┐   ┌──────────────┐   ┌─────────────────────────┐  │
│  │   ImgHead    │   │   BEVHead    │   │     FusionHead          │  │
│  │  (图像分支)   │   │  (点云分支)   │   │   (跨模态融合)          │  │
│  │              │   │              │   │                         │  │
│  │  ALNet 提取  │   │  RICNN 提取  │   │  Cross-Attention        │  │
│  │  图像特征     │   │  BEV特征     │   │  融合多模态特征          │  │
│  └──────┬───────┘   └──────┬───────┘   └───────────┬─────────────┘  │
│         │                  │                       │                │
│         └──────────────────┴───────────────────────┘                │
│                            │                                        │
│                    ┌───────▼────────┐                               │
│                    │  NetVLAD 聚合   │                               │
│                    │  全局描述子     │                               │
│                    └───────┬────────┘                               │
│                            │                                        │
│                    ┌───────▼────────┐                               │
│                    │  UOT 位姿估计   │                               │
│                    │ (训练时)        │                               │
│                    └────────────────┘                               │
└──────────────────────────────────────────────────────────────────────┘
```

**三种运行模式**（由 `config.yaml` 中的 `flag` 控制）：
- `bev`: 仅使用点云BEV分支
- `img`: 仅使用图像分支
- `fusion`: 点云+图像融合（完整模式）

---

## 2. 数据预处理流水线

### 2.1 KITTI 数据集结构

```
KITTI/
└── sequences/
    └── XX/
        ├── velodyne/          # 激光雷达点云 (.bin, 每文件一行4个float: x,y,z,intensity)
        ├── image_2/           # 相机图像 (.png, 原始尺寸约~1242×375)
        ├── calib.txt          # 标定矩阵 (P2投影矩阵, Tr相机到雷达)
        ├── poses.txt          # GPS真实轨迹 (每行12个float, 3×4位姿矩阵)
        └── loop_GT_4m.pickle  # 闭环真值标注 (由 loop_gt.py 生成)
```

### 2.2 KITTI360 到 KITTI 格式转换 (`preparedataset.py`)

KITTI360 使用不同目录结构（`2013_05_28_drive_XXXX_sync`），通过 `preparedataset.py` 的 `k3602k()` 函数转换为标准 KITTI 格式：
- 序列 ID 映射: KITTI360 src (0,3,4,5,6,7,9,10) → KITTI tgt (50,53,54,55,56,57,59,60)
- 姿态从 `cam0_to_world.txt` 读取并通过 `cam0_to_velo` 外参转换为 Velodyne 坐标系
- 点云和图像通过符号链接组织

### 2.3 闭环真值生成 (`loop_gt.py`)

**输入**: 序列中所有帧的 4×4 位姿矩阵

**处理流程**:
1. 使用 KDTree 在 3D 位置空间 (x,y,z) 上建立索引
2. 对每一帧，查询距离 ≤ 4m 的其他帧作为正样本（闭环）
3. 排除时间上太近的帧（前后各 50 帧）
4. 根据方向角差判断是否反向：角度差 > 90° 标记为反向闭环
5. 距离 > 15m 的帧作为负样本

**输出**: `loop_GT_4m.pickle`，每帧的格式：
```python
{
    'idx': 帧索引,
    'positive_idxs': [正样本帧索引列表],  # 距离 < 4m 且不在时间窗口内
}
```

### 2.4 KittiDataset 训练数据生成 (`dataset.py`)

#### 2.4.1 数据加载 (train模式)

对每个训练样本（一个闭环对），加载 query 帧和 positive 帧：

| 步骤 | 操作 | 输入维度 | 输出维度 |
|------|------|---------|---------|
| 1 | 读图像 | `(H,W,3)` ~1242×375 | `(H,W,3)` 原始BGR |
| 2 | Cropped & Scaled | `(H,W,3)` | `(192, 576, 3)` 即 HR=384×0.5, WR=1152×0.5 |
| 3 | 读点云 | `.bin` 每点4个float | `(N,4)` [x,y,z,intensity] |
| 4 | 投影到图像 | `(N,4)` → 通过 P2×Tr 变换 | `(N,6)` [x,y,z,i, u_pixel, v_pixel] |
| 5 | 体素化BEV | `(N,6)` → | `(h_bev, w_bev, 7)` BEV特征图 |

#### 2.4.2 数据增强（训练时）

对 positive 帧施加随机变换（`rt_mat()`）模拟位姿不确定性：
```python
rand = np.random.random(6) * 2 - 1  # [-1, 1]
Rt = rt_mat(rand[0]*3°/180π,   # ±3° roll
            rand[1]*3°/180π,   # ±3° pitch
            rand[2]*180°/180π,  # ±180° yaw
            rand[3]*3m,         # ±3m x
            rand[4]*3m,         # ±3m y
            rand[5]*0.3m)       # ±0.3m z
```

#### 2.4.3 点云到BEV体素化 (`pointcloud_encoder()`)

**输入**: `(N, 4)` 或 `(N, 6)` 点云

**配置参数** (config.yaml):
```yaml
bev_range: -32,-32,-2.5,32,32,1.5    # [xmin,ymin,zmin,xmax,ymax,zmax]
bev_resolution: 0.2                    # 体素分辨率 (m)
voxel_max_points: 100                  # 每体素最大点数
voxel_num: 15000                       # 最大体素数
voxel_sample: 'top'                    # 按z高度排序采样
```

**体素化过程**:
1. **范围滤波**: 过滤 `bev_range` 外的点
2. **排序**: 按 z 高度降序排列（优先保留高点）
3. **体素赋值**: 使用 Numba JIT 加速的 `_points_to_voxel_reverse_kernel()`
   - Z轴分辨率 = `bev_range[5] - bev_range[2]` = 4m（单个体素覆盖全高度）
   - 有效格网尺寸: `(64/0.2, 64/0.2)` = `(320, 320)` → 输出 BEV 图尺寸

**输出BEV特征图**: `(320, 320, 7)`
```
通道0: max_z / resolution_z   # 每体素最大z高度
通道1: mean_intensity         # 平均反射强度
通道2: density                # log(点数) 密度
通道3: voxel_center_x         # 体素中心x
通道4: voxel_center_y         # 体素中心y
通道5: voxel_center_z         # 体素中心z  
通道6: voxel_intensity_center  # 体素中心反射强度
```

**Relation (像素-BEV关联)**: 当点云包含像素投影信息（`shape[1]==6`）时：
```
relation.shape = (M, 每个体素内的投影点数, 2)
relation[:,:,0] = [u_pixel, v_pixel]    # 图像像素坐标
relation[:,:,-1] = [bev_row, bev_col]    # BEV格网坐标
```

#### 2.4.4 标签分数 (`label_score`)

用于训练 score 损失。通过在 query 和 positive 之间计算体素级匹配关系：

**输入**: query 体素中心 `(x,y)` 坐标、positive 体素中心 `(x,y)` 坐标

**流程**:
1. 通过 `pose_to_frame` 将 query 体素变换到 positive 坐标系
2. 做 NN 匹配（欧氏距离）
3. 距离 < 阈值（max(2, 前30%分位数)）的体素对标记为匹配（score=1）

**输出**: `(h_bev, w_bev, 2)` — 两个通道分别对应 query 和 positive 的匹配标签

### 2.5 图像预处理细节

**输入尺寸**: 原始 KITTI 图像 ~1242×375
**处理流程**:
1. `crop_image()` 裁剪到 `1152×384`（居中裁剪，按需零填充）
2. 降采样到 `576×192`（scale=0.5）

**最终输入图像**: `(192, 576, 3)` 或 batch 中 `(B, 3, 192, 576)`

---

## 3. 模型输入输出详解

### 3.1 batch_dict 完整结构

训练时，`KittiTotalLoader` 的 collate 函数构造 `train()` 函数的输入 `data`:

#### data 的原始字段（来自Dataset）

| 字段 | 维度 | 说明 |
|------|------|------|
| `sequence` | `(B,)` Tensor 或 int | 序列编号 |
| `id_query` | `(B,)` | query帧索引 |
| `id_positive` | `(B,)` | positive帧索引 |
| `bev_query` | `(B, 320, 320, 7)` | query BEV特征图 |
| `bev_positive` | `(B, 320, 320, 7)` | positive BEV特征图 |
| `img_query` | `(B, 192, 576, 3)` | query图像 (numpy→tensor) |
| `img_positive` | `(B, 192, 576, 3)` | positive图像 |
| `pose_query` | `(B, 4, 4)` | query位姿矩阵 |
| `pose_positive` | `(B, 4, 4)` | positive位姿矩阵 |
| `pose_to_frame` | `(B, 4, 4)` | query到positive的相对位姿 = inv(pose_pos) @ pose_query |
| `label_score` | `(B, 320, 320, 2)` | 匹配标签分数 |
| `relation` | `(B, max_len, 每个体素点数, 2)` | 像素-BEV关联（变长，由collate padding） |

#### train() 预处理后送入 net 前

```python
bev = cat([bev_query, bev_positive], dim=0)    # (2B, 320, 320, 7)
bev = bev.permute(0, 3, 1, 2)                   # (2B, 7, 320, 320)
img = cat([img_query, img_positive], dim=0)     # (2B, 192, 576, 3)
img = img.permute(0, 3, 1, 2)                   # (2B, 3, 192, 576)
```

**送入模型的 batch_dict**:

| 字段 | 维度 | 说明 |
|------|------|------|
| `bev` | `(2B, 7, 320, 320)` | BEV输入（前B个query，后B个positive）|
| `img` | `(2B, 3, 192, 576)` | 图像输入 |
| `relation` | `(2B, max_len, K, 2)` | 像素-BEV关联 |
| `id_query` | `(B,)` | query索引 |
| `id_positive` | `(B,)` | positive索引 |
| `sequence` | `(B,)` | 序列号 |
| `pose_to_frame` | `(B, 4, 4)` | 相对位姿 |
| `pose_query` | `(B, 4, 4)` | query位姿 |
| `pose_positive` | `(B, 4, 4)` | positive位姿 |
| `label_score` | `(B, 320, 320, 2)` | 匹配标签 |
| `batch_size` | int | `2B` |

### 3.2 ImgHead 图像分支

```
输入: img (2B, 3, 192, 576)
     归一化: x = img[:, 0:3] / 255.0  → (2B, 3, 192, 576)
```

**ALNet 骨干网络**（类似 ALIKE 架构）:

| 层 | 输入 | 输出 | 操作 |
|---|------|------|------|
| block1 | `(2B,3,192,576)` | `(2B,16,192,576)` | ConvBlock(3→16, ReLU+BN) |
| pool2+block2 | `(2B,16,192,576)` | `(2B,32,96,288)` | MaxPool2d(2)+ResBlock(16→32) |
| pool4+block3 | `(2B,32,96,288)` | `(2B,64,24,72)` | MaxPool2d(4)+ResBlock(32→64) |
| pool4+block4 | `(2B,64,24,72)` | `(2B,128,6,18)` | MaxPool2d(4)+ResBlock(64→128) |
| 特征聚合 | 4层特征拼接 | `(2B,128,192,576)` | 1×1 conv + 上采样 + concat |
| 输出头 | `(2B,128,192,576)` | score `(2B,1,192,576)` + feat `(2B,128,192,576)` | Conv1x1(128→129) |

**ImgHead 输出**:

| 字段 | 维度 | 说明 |
|------|------|------|
| `score_img` | `(2B, 1, 192, 576)` | 逐像素关键点得分 (sigmoid激活) |
| `fea_img` | `(2B, 128, 192, 576)` | 密集描述子图 |
| `key_pixels` | `(2B, 150, 2)` | Top-150 关键点像素坐标 [row, col] |
| `fea_kpl` | `(2B, 128, 150)` | Top-150 关键点处描述子 (从fea_img采样) |

**关键点选择**: NMS (radius=2, iter=2) + Top-K (k=150 由 `kpts_number_img` 配置)

### 3.3 BEVHead 点云分支

```
输入: bev (2B, 7, 320, 320)
     x = bev[:, 0:3, :, :]     → (2B, 3, 320, 320)   可视BEV (RGB-like)
     points = bev[:, 3:7, :, :] → (2B, 4, 320, 320)   体素中心xyzi
     guider = (bev[:, 2:3] > 0) → (2B, 1, 320, 320)   有效区域mask
```

**RICNN 骨干网络**（旋转不变CNN）:

| 层 | 输入 | 输出 | 操作 |
|---|------|------|------|
| block1 | `(2B,3,320,320)` | `(2B,16,320,320)` | RIConvBlock(3→16) |
| pool2+block2 | `(2B,16,320,320)` | `(2B,32,160,160)` | RIMaxPool(2)+RIResBlock(16→32) |
| pool4+block3 | `(2B,32,160,160)` | `(2B,64,40,40)` | RIMaxPool(5)+RIResBlock(32→64) |
| pool4+block4 | `(2B,64,40,40)` | `(2B,128,10,10)` | RIMaxPool(5)+RIResBlock(64→128) |
| 特征聚合 | 4层拼接 | `(2B,128,320,320)` | 1×1 conv + 上采样 + concat |
| 输出头 | `(2B,128,320,320)` | score `(2B,1,320,320)` + feat `(2B,128,320,320)` | Conv1x1(128→129) |

> **旋转不变性**: RICNN 使用 RIConv2d / RIMaxpool2d / RIAvgpool2d，根据像素距离中心点的欧氏距离（而非固定kernel位置）分权重组，在推理时可用 `disable_ri()` 转为标准 Conv2d/MaxPool2d

**关键点选择**（默认 `select='maxpool'`）:
1. NMS (radius=3) 选出局部最大值
2. 每个batch元素取 Top-K (k=150 = `kpts_number_bev`) 得分最高像素
3. 提取对应的体素中心坐标和描述子

**BEVHead 输出**:

| 字段 | 维度 | 说明 |
|------|------|------|
| `score_bev` | `(2B, 320, 320)` | 逐像素关键点得分 |
| `fea_bev` | `(2B, 128, 320, 320)` | 密集描述子图 |
| `key_points` | `(2B, 150, 4)` | Top-150 关键点体素坐标 [x,y,z,i] |
| `pixels_kpt` | `(2B, 150, 2)` | 关键点BEV像素坐标 [row, col] |
| `fea_kpt_original` | `(2B, 128, 150)` | 关键点描述子 |
| `vlad_bev` | `(2B, 256)` | BEV NetVLAD全局描述子 |

#### EncodePosition（位置编码）

BEV 关键点位置编码：
```python
# 输入: kpts (B, 150, 4)
# 计算150×150的距离矩阵
# 距离直方图 (bins=16, range=[1,80]m)
# → MLP(16→64→64→128) → 残差加到描述子上
```

### 3.4 FusionHead 跨模态融合

#### 3.4.1 双向特征生成与转换

**(1) 图像→BEV 特征采样**:
```python
# relation: (2B, max_len, K, 2)
# pixel_img: 图像像素位置 → grid_sample(fea_img) → fea_pl_dual
fea_pl_dual.shape = (2B, 128, max_len, K)  # 从图像特征图采样的像素特征
# LocalPool: Conv1x1(100→10) + MaxPool(1,10) 聚合每体素内多个像素点
fea_pl_dual → LocalPool → (2B, 128, max_len, 1) → squeeze → (2B, 128, max_len)
# Converter (cvt_bev): Self-Attention + Conv1d残差
fea_pl_dual → cvt_bev → fea_pt_dual_gen (2B, 128, max_len)
```

**(2) BEV→图像特征采样** (训练时，有 pose_to_frame 时才执行)**:
```python
# pixel_bev: BEV格网坐标 → grid_sample(fea_bev) → fea_pt_dual
fea_pt_dual → cvt_img → fea_pl_dual_gen (2B, 128, max_len)
```

#### 3.4.2 全景特征生成 (Generator)

```python
# 输入: fea_pt_dual_gen (B, 128, N)
# Self-Attention → ConvTranspose1d(k3,s3) → AdaptiveMaxPool1d(150)
fea_pt_dual_gen → gen_pan → fea_kpt_original_gen (B, 128, 150)
# 注意：这个模块从图像特征生成与BEV关键点数量(150)对齐的特征
```

#### 3.4.3 双路径转换器

```python
# 路径1: BEV关键点特征 → cvt_img → fea_kpt (B, 128, 150)  残留在图像空间的特征
fea_kpt_original → cvt_img → fea_kpl_gen (B, 128, 150)

# 路径2: 路径1输出 → cvt_bev → 再回到BEV空间
fea_kpl_gen → cvt_bev → fea_kpt_gen_gen (B, 128, 150)
```

#### 3.4.4 FusionHead Attention 融合

```python
# 拼接4种特征: [original, gen, gen_gen, kpl_gen]
fea_kpts = cat([fea_kpt_original, fea_kpt_original_gen,
                fea_kpt_gen_gen, fea_kpl_gen], dim=2)
# 维度: (B, 128, 150, 4)

# 对每对匹配点 (共3对: 原始-生成, 原始-回环生成, 原始-图像残差)
# Self-Attention across pairs → 取max → Cross-Attention with kpl_gen
# 最终输出: fea_kpt_fusion (B, 128, 150)
```

#### 3.4.5 FusionHead 输出

| 字段 | 维度 | 说明 |
|------|------|------|
| `fea_kpt_original` | `(2B, 128, 150)` | BEV原始关键点特征 |
| `fea_kpt_fusion` | `(2B, 128, 150)` | **融合后**关键点特征（当前实现等同original） |
| `fea_kpl` | `(2B, 128, 150)` | 图像关键点特征 |
| `fea_pt_dual` | `(2B, 128, max_len)` | 从BEV特征图采样的匹配点特征 |
| `fea_pl_dual` | `(2B, 128, max_len)` | 从图像特征图采样的匹配点特征 |
| `fea_pt_dual_gen` | `(2B, 128, max_len)` | 图像特征生成的点云特征 |
| `fea_pl_dual_gen` | `(2B, 128, max_len)` | 点云特征生成的图像特征 |
| `fea_kpt_original_gen` | `(2B, 128, 150)` | 全景生成器输出 |
| `fea_kpt_gen_gen` | `(2B, 128, 150)` | 双路径转换器输出 |
| `fea_kpl_gen` | `(2B, 128, 150)` | Converter从BEV特征生成的图像空间特征 |

### 3.5 NetVLAD 全局描述子

#### NetVLAD (标准版，实际使用)

```python
输入: fea_kpt_fusion.unsqueeze(3)  # (2B, 128, 150, 1)
      soft_assign = Conv2d(128→16)(x) → ReLU → Softmax  # (2B, 16, 150, 1)
      残差 = x - centroids[16×128]                         # (2B, 16, 150, 128)
      VLAD = sum(soft_assign * 残差, dim=2)               # (2B, 16, 128)
      归一化 → flatten → 归一化                            # (2B, 2048)
```

**注意**: 当前代码中 NetVLAD 的 `fea_size=128`, `cluster_num=16`，输出维度应为 `16 × 128 = 2048`。但在 `Fusion` 的 `__init__` 中：
```python
self.netvlad_fusion = NetVLAD(feature_size, cfg['cluster_num_fusion'])
```
而 `cluster_num_fusion: 16`，所以 VLAD 输出维度 = `16 × 128 = 2048`。

但实际上看训练代码中 VLAD 维度配置为 `vlad_size: 256`，这个参数在 `NetVLAD` 类中没有被使用（不使用 NetVLADLoupe 时）。当前实现中 VLAD 维度固定为 `cluster_num × fea_size = 2048`。

**最终 VLAD 融合**:
```python
vlads = sigmoid(w) * vlad_fusion + (1 - sigmoid(w)) * vlad_bev
# w 是可学习参数
```

### 3.6 UOT (Unbalanced Optimal Transport) 位姿估计

**仅在训练时**（`pose_to_frame` 存在时）执行：

```python
输入:
  fea_kpt (2B, 128, 150)  # 关键点特征
  key_points (2B, 150, 4) # 关键点坐标

流程:
  1. 前B个为query, 后B个为positive
  2. Sinkhorn Unbalanced OT:
     - Cost matrix C = 1 - cosine_sim(feat1, feat2)  # (B, 150, 150)
     - 迭代5次 (sinkhorn_iter=5)
     - epsilon = exp(learnable) + 0.03 (熵正则)
     - gamma = exp(learnable) (质量正则)
  3. 得到 Transport Plan T (B, 150, 150)
  4. project_kpts = T @ key_points2 / sum(T)  # 加权投影
  5. compute_rigid_transform (加权SVD):
     - 计算加权中心
     - SVD分解协方差矩阵
     - 输出 transformation (B, 3, 4)  # R|t 刚体变换

输出:
  transformation_original (B, 3, 4)  # 估计的相对位姿 [R|t]
  project_kpts_original (B, 150, 3)  # 投影后的关键点
  correspondences_feature (B, 150, 150)  # 匹配矩阵
```

**注意**: `UOTHead` 对原始特征和融合特征各执行一次（如果 `name='original'` 和 `name='fusion'` 都有），分别输出 `transformation_original` 和 `transformation_fusion`。

---

## 4. 模型最终输出汇总

### 4.1 推理模式 (test/eval, 无 pose_to_frame 时)

| 字段 | 维度 | 说明 |
|------|------|------|
| `vlads` | `(1, 2048)` | 全局描述子（融合 + BEV加权） |
| `vlad_bev` | `(1, 256)` | BEV全局描述子 |
| `score_bev` | `(1, 320, 320)` | BEV得分图 |
| `fea_bev` | `(1, 128, 320, 320)` | BEV描述子图 |
| `key_points` | `(1, 150, 4)` | BEV关键点坐标 [x,y,z,i] |
| `pixels_kpt` | `(1, 150, 2)` | BEV关键点像素坐标 |
| `fea_kpt_original` | `(1, 128, 150)` | BEV关键点特征 |
| `key_pixels` | `(1, 150, 2)` | 图像关键点像素坐标 |
| `fea_kpl` | `(1, 128, 150)` | 图像关键点特征 |
| `fea_img` | `(1, 128, 192, 576)` | 图像密集描述子图 |
| `score_img` | `(1, 1, 192, 576)` | 图像得分图 |
| `fea_kpt_fusion` | `(1, 128, 150)` | 融合特征（当前=BEV原始） |

### 4.2 训练模式 (有 pose_to_frame 时额外输出)

| 字段 | 维度 | 说明 |
|------|------|------|
| `transformation_original` | `(B, 3, 4)` | UOT估计的相对位姿（原始特征） |
| `transformation_fusion` | `(B, 3, 4)` | UOT估计的相对位姿（融合特征） |
| `project_kpts_original` | `(B, 150, 3)` | 投影关键点（原始） |
| `project_kpts_fusion` | `(B, 150, 3)` | 投影关键点（融合） |
| `correspondences_feature` | `(B, 150, 150)` | 特征匹配矩阵 |
| `fea_pt_dual` | `(2B, 128, max_len)` | BEV匹配点特征 |
| `fea_pl_dual` | `(2B, 128, max_len)` | 图像匹配点特征 |
| `fea_pt_dual_gen` | `(2B, 128, max_len)` | 生成的点云特征 |
| `fea_pl_dual_gen` | `(2B, 128, max_len)` | 生成的图像特征 |
| `fea_kpt_original_gen` | `(2B, 128, 150)` | 全景生成器输出 |
| `fea_kpt_gen_gen` | `(2B, 128, 150)` | 双路径转换器输出 |
| `fea_kpl_gen` | `(2B, 128, 150)` | BEV→图像特征 |

---

## 5. 损失函数 (`loss.py`)

总损失 = 各子损失加权求和：

| 损失 | 权重 | 说明 |
|------|------|------|
| `l_score` | 1.0 | BEV score与label_score的MSE |
| `l_pose` | 1.0 | UOT估计位姿投影后的关键点与真值位姿投影的L1误差 |
| `l_match` | 0.05 | Sinkhorn匹配投影关键点误差 |
| `l_triplet` | 1.0 | 全局描述子三元组损失 (margin=0.5) |
| `l_gb` | 1.0 | 生成BEV特征与原始BEV特征的cosine相似度损失 |
| `l_gi` | 1.0 | 生成图像特征与原始图像特征的cosine相似度损失 |
| `l_gpa` | 1.0 | 全景生成特征与原始关键点特征的cosine相似度损失 |
| `l_kpl` | 1.0 | 关键点级别生成损失 |

**三元组选择策略** (由 `negetative_selsector` 配置):
- `random`: 随机选择违反margin的负样本
- `semihard`: 半难负样本（距离在 margin 内）
- `hardest`: 最难负样本

---

## 6. 推理与评估流程

### 6.1 闭环检测 (`evaluate_lcd.py`)

```
1. 遍历测试序列所有帧，提取VLAD + 局部特征
2. 对每帧：排除前后50帧，在剩余帧中NN搜索最相似VLAD（Top-1）
3. 用RANSAC验证局部特征匹配（EuclideanTransform, min_samples=15）
4. 统计AP、Recall@100、F1
```

### 6.2 位姿估计评估 (`evaluate_pose.py`)

```
1. 对所有闭环对（真值距离<4m）
2. RANSAC: 局部特征NN匹配 + RANSAC估计2D刚体变换
3. UOT:   Sinkhorn OT + Weighted SVD估计3D刚体变换
4. 计算平移误差(m)和旋转误差(deg)
```

---

## 7. 维度速查表

### 关键维度常量

| 参数 | 值 | config字段 |
|------|-----|-----------|
| BEV分辨率 | 0.2m | `bev_resolution` |
| BEV范围 | [-32, -32, -2.5, 32, 32, 1.5] | `bev_range` |
| BEV图尺寸 (H,W) | 320×320 | 64/0.2 |
| BEV通道数 | 7 | — |
| BEV关键点数 | 150 | `kpts_number_bev` |
| 图像分辨率 | 192×576 | (384×1152)×0.5 |
| 图像关键点数 | 150 | `kpts_number_img` |
| 特征维度 | 128 | ALNet `dim` |
| VLAD聚类数 | 16 | `cluster_num_fusion/bev/img` |
| VLAD输出维度 | 2048 | 16×128 |
| Sinkhorn迭代 | 5 | `sinkhorn_iter` |
| batchsize | 6 | `batchsize` |
| 训练序列 | 0,5,6,7,9 | `train` |
| 验证序列 | 8,50,54,55,56,59 | `validate` |
| 测试序列 | 8,50,54,55,56,59 | `test` |

### 数据流维度变化

```
原始点云:  (N, 4) [x,y,z,intensity]
                ↓ 体素化
BEV特征图: (320, 320, 7)
                ↓ permute
BEV输入:   (1, 7, 320, 320)
                ↓ RICNN
score_bev: (1, 1, 320, 320)
fea_bev:   (1, 128, 320, 320)
key_points:(1, 150, 4)
fea_kpt:   (1, 128, 150)
                ↓ NetVLAD
vlad_bev:  (1, 2048)


原始图像:  (H, W, 3) ~1242×375
                ↓ crop + resize
图像输入:  (192, 576, 3)
                ↓ permute
Img输入:   (1, 3, 192, 576)
                ↓ ALNet
score_img: (1, 1, 192, 576)
fea_img:   (1, 128, 192, 576)
key_pixels:(1, 150, 2)
fea_kpl:   (1, 128, 150)


fusion VLAD = sigmoid(w)*vlad_fusion + (1-sigmoid(w))*vlad_bev
vlads:     (1, 2048)
```

---

## 8. 模型参数量

```python
# 从代码中: sum(p.numel() for p in model.parameters()) / 1e6
# 完整 Fusion 模式 (BEV + Img + FusionHead):
#   ALNet ×2 (BEV/Img共享一个但各自实例化) + RICNN + NetVLAD ×2 + UOT + Converters + Generator + FusionHead
#   约 10-15M 参数
```

---

*文档基于代码版本: commit c3d268f "位姿可视化"*
