# `convert_json_to_json.py` 说明文档

## 1. 脚本用途

`tools/convert_gt_to_label/convert_json_to_json.py` 的作用是：

- 读取原始 GT 标注 JSON；
- 从 `asso_list` 中提取每个目标的 `camera_mea` 和可选的 `lidar_mea`；
- 结合标定文件，将原始标注转换为项目当前使用的目标 JSON 格式；
- 在输出中同时写入：
  - 2D 框信息；
  - 相机系 3D 信息；
  - ego 系 3D 信息；
  - 车辆前后左右面的附加信息（仅部分目标类型会生成）。

这个脚本本质上是一个“原始标注格式 -> 最终训练/评估使用 JSON 格式”的转换器，而不是简单的字段重命名脚本。

---

## 2. 输入与输出

### 2.1 单文件模式

命令格式：

```bash
python3 tools/convert_gt_to_label/convert_json_to_json.py \
    input.json \
    output.json \
    --calib-path /path/to/case/calib
```

说明：

- `input.json`：原始标注文件。
- `output.json`：转换后的输出文件。
- `--calib-path`：标定目录。脚本会读取：

```text
<calib-path>/L2_calib/camera4.json
```

如果不传 `--calib-path`，代码默认使用 `input.json` 的父目录，但只有当该目录下确实存在 `L2_calib/camera4.json` 时才可正常工作。

### 2.2 批量模式

命令格式：

```bash
python3 tools/convert_gt_to_label/convert_json_to_json.py \
    --json-root /path/to/dataset_root
```

或使用封装脚本：

```bash
bash tools/convert_gt_to_label/run_convert_json_to_json.sh /path/to/dataset_root
```

批量模式下，脚本会递归查找：

```text
<json-root>/**/annotations_20260320/*.json
```

并假设每个 case 目录结构类似：

```text
case_xxx/
├── annotations_20260320/
│   └── xxx.json
├── calib/
│   └── L2_calib/camera4.json
└── labels_20260320_with_ego/
    └── xxx.json    # 脚本输出
```

注意：代码当前实际输出目录是 `labels_20260320_with_ego`。

---

## 3. 转换主流程

单个目标的大致处理链路如下：

1. 从原始标注的 `asso_list` 中逐个读取对象。
2. 从 `camera_mea` 读取：
   - `cls`
   - 2D 框位置
   - `confidence`
3. 用 `DETECTION_CLASS_MAP` 将原始类别名映射为项目内部类别 id。
4. 若目标存在 `lidar_mea`，则进一步计算：
   - 相机系 3D 中心点、尺寸、朝向；
   - ego 系 3D 信息；
   - 部分目标的前/后/左/右面信息。
5. 将最终结果组织成目标 JSON 条目，包含：
   - `type`
   - `type_name`
   - `box2d`
   - `3d_ori`
   - `3d_front/back/left/right`
   - `3d_ori_ego`
   - `3d_ori_ego_from_cam`
   - 一致性校验字段等。

---

## 4. `DETECTION_CLASS_MAP` 的作用

`DETECTION_CLASS_MAP` 是这个脚本里最关键的类别标准化入口。

代码位置：

- 定义：`tools/convert_gt_to_label/convert_json_to_json.py`
- 使用：`build_label_row()` 中先取 `raw_label_name`，再执行 `label_det_cls = DETECTION_CLASS_MAP.get(raw_label_name, -1)`

它的职责不是单纯“把字符串换成数字”，而是同时决定了下面三件事：

### 4.1 决定原始类别如何映射到输出类别

当前代码会先取原始类别名：

- 若 `camera_mea["cls"] == "vehicle"`，则使用 `camera_mea["subcls"]`
- 否则直接使用 `camera_mea["cls"]`

随后脚本会先通过 `DETECTION_CLASS_MAP` 把原始类别归一化成标准类别 id，再通过 `CLASS_MAP_MODEL` 得到标准类别名，最终写入输出中的：

- `type`：类别 id
- `type_name`：标准类别名

例如：

- `tanker -> 6 -> truck`
- `large_truck -> 6 -> truck`
- `motorcyclist -> 10 -> bicyclist`
- `tricyclist -> 12 -> tricycle`

这说明 `DETECTION_CLASS_MAP` 做的是“原始类别标准化”，不是“原样透传”。

### 4.2 决定目标是否被直接过滤

如果映射结果为 `-1`，目标会被直接丢弃，不写入输出。

当前脚本中：

- `carton -> -1`

对应逻辑在 `build_label_row()` 中：

- 若 `label_det_cls < 0`，函数直接返回 `None`
- 上层 `generate_label_rows()` 会跳过该目标

也就是说，`DETECTION_CLASS_MAP` 同时承担了“保留/过滤白名单”的作用。

### 4.3 间接决定是否生成 3D 信息

在 `build_3d_label()` 中，存在如下规则：

- 若 `label_lidar is None`，不生成有效 3D；
- 若类别 id 不在 `SUPPORTED_3D_CLASS_IDS` 中，不生成有效 3D；
- 若类别 id 在 `COMPLETE_3D_CLASS_IDS` 中，只生成 `3d_ori`，不生成 `3d_front/back/left/right`；

因此，`DETECTION_CLASS_MAP` 还会影响 3D 处理分支。

结合当前类别定义，可理解为：

- `0~8`：`face_3d_classes`，可进入 3D 处理，且会额外尝试生成 `3d_front/back/left/right`
- `9~12`：`complete_3d_classes`，可进入 3D 处理，但只保留 `3d_ori`
- `13~16`：只保留 2D 或空 3D 占位
- `-1`：直接过滤，不输出

所以，`DETECTION_CLASS_MAP` 不只是“类别映射表”，它还在事实上定义了哪些类别属于当前 3D 支持范围。

---

## 5. 当前 `DETECTION_CLASS_MAP` 解释

当前映射如下：

| 原始类别 / 子类 | 映射后 id | 输出标准类 | 3D 行为 |
| --- | ---: | --- |
| `car` | 0 | `car` | `face_3d` |
| `suv` | 1 | `suv` | `face_3d` |
| `pickup` | 2 | `pickup` | `face_3d` |
| `medium_car` | 3 | `medium_car` | `face_3d` |
| `van` | 4 | `van` | `face_3d` |
| `bus` | 5 | `bus` | `face_3d` |
| `truck` / `tanker` / `large_truck` / `construction_vehicle` | 6 | `truck` | `face_3d` |
| `special_vehicle` | 7 | `special_vehicle` | `face_3d` |
| `unknown` | 8 | `unknown` | `face_3d` |
| `pedestrian` | 9 | `pedestrian` | `complete_3d` |
| `bicyclist` / `motorcyclist` | 10 | `bicyclist` | `complete_3d` |
| `bicycle` / `motorcycle` | 11 | `bicycle` | `complete_3d` |
| `tricycle` / `tricyclist` | 12 | `tricycle` | `complete_3d` |
| `traffic_sign` | 13 | `traffic_sign` | 仅 2D |
| `wheel` | 14 | `wheel` | 仅 2D |
| `plate` | 15 | `plate` | 仅 2D |
| `face` | 16 | `face` | 仅 2D |

可以看出，这张表体现了三类策略：

### 5.1 一对一映射

例如：

- `pedestrian -> 9`
- `wheel -> 14`

适用于原始类别与项目标准类别完全一致的情况。

### 5.2 多对一归并

例如：

- `tanker`
- `large_truck`
- `construction_vehicle`

都被归并为 `truck(6)`。

这意味着训练/评估侧不再区分这些细粒度障碍物，而是把它们当成同一大类处理。

### 5.3 显式过滤

例如：

- `carton -> -1`

说明该类别虽然可能出现在原始标注中，但当前转换目标并不希望保留它。

---

## 6. `DETECTION_CLASS_MAP` 的维护原则

后续如果原始 GT 中新增了 `camera_mea.cls` 类别，最先需要检查的就是这张表。

### 6.1 如果原始类别未出现在映射表中

当前代码会直接执行：

```python
label_det_cls = DETECTION_CLASS_MAP.get(raw_label_name, -1)
```

这意味着：

- 未知类别会被映射为 `-1`；
- `build_label_row()` 会直接返回 `None`，该目标不会写入输出。

因此，只要数据源里出现新类别，就必须同步更新 `DETECTION_CLASS_MAP`，否则会被静默过滤。

### 6.2 建议先用统计脚本盘点类别

同目录已经提供：

```text
tools/convert_gt_to_label/stat_gt_categories.py
```

它可以统计原始 GT 中所有 `camera_mea.cls` 的取值，并生成映射模板，适合在更新 `DETECTION_CLASS_MAP` 前先做盘点。

建议流程：

1. 先运行 `stat_gt_categories.py` 统计现网类别；
2. 确认每个原始类别应该：
   - 映射到哪个标准类；
   - 还是直接过滤；
3. 再更新 `DETECTION_CLASS_MAP`；
4. 最后运行 `convert_json_to_json.py` 做验证。

### 6.3 修改映射时要同步关注 3D 语义

例如把某个新类别映射到：

- `0~8`：表示该类别会进入 `face_3d` 分支；
- `9~12`：表示该类别会进入 `complete_3d` 分支；
- `13~16`：表示该类别只保留 2D 或空 3D 占位；
- `-1`：表示完全过滤。

所以改这张表时，不能只从“类别名是否合理”来判断，还要同时确认：

- 是否希望输出 3D；
- 是否希望参与车辆面信息计算；
- 是否会影响下游训练/评估类别定义。

---

## 7. 输出类别与 `CLASS_MAP` 的关系

脚本中还有一张表：

```python
CLASS_MAP = {
    "car": 0,
    "suv": 1,
    "pickup": 2,
    ...
}
```

它和 `DETECTION_CLASS_MAP` 的关系可以理解为：

- `DETECTION_CLASS_MAP`：原始类别名 / 原始 `subcls` -> 内部标准 id
- `CLASS_MAP_MODEL`：内部标准 id -> 规范化后的标准类别名
- `CLASS_MAP`：标准类别名 -> 输出类别 id

两者配合后，最终输出对象里会写入：

- `type`: 例如 `6`
- `type_name`: 例如 `truck`

---

## 8. 使用这个脚本时最容易踩的点

### 8.1 类别未配置会被直接过滤

如果原始类别或车辆 `subcls` 出现新值，但 `DETECTION_CLASS_MAP` 没更新，该目标会被跳过，不会写入输出。

### 8.2 批量模式只扫描固定目录名

批量模式只查找：

```text
annotations/*.json
```

如果目录名变化，需要修改 `find_annotation_files()`。

### 8.3 批量模式输出目录是 `labels_json`

批量模式当前实际写入的是：

```text
labels_json
```

使用时应以代码实际行为为准。

### 8.4 并非所有类别都会生成有效 3D

即使原始数据里存在 `lidar_mea`，也只有：

- `0~8` 类会生成 `face_3d`
- `9~12` 类会生成 `complete_3d`
- `13~16` 类会写成空 3D

### 8.5 输出类别名是归一化后的标准类名

例如：

- `tanker` 最终输出为 `truck`
- `motorcyclist` 最终输出为 `bicyclist`
- `tricyclist` 最终输出为 `tricycle`

---

## 9. 一句话总结

如果只抓一个重点来理解这个脚本，那么就是：

`DETECTION_CLASS_MAP` 是原始 GT 到项目标准标签体系的总入口，它同时决定了“类别如何归一化成标准输出、目标是否过滤、以及目标进入哪种 3D 分支”。  
理解这张表，基本就理解了这个脚本一半以上的业务语义。
