# convert_merge_tracking_bundle

## 目录说明

这个目录整理了 `convert_merge_tracking.py` 及其运行依赖，并额外包含了 `make_jl.py`，方便在独立目录中完成以下两步：

1. 把 `merge_tracking.json` 转成 `ObjectPerceptionObjectList` 的 protobuf 三件套。
2. 再把三件套转成便于查看的 JSON 文本输出。

目录中的关键文件：

- `convert_merge_tracking.py`：把 `merge_tracking.json` 转成 protobuf 输出。
- `make_jl.py`：把 `ObjectPerceptionObjectList.data.json` 对应的数据转成 JSON 文本。
- `proto_reader/reader.py`：按 `data.json + index.json + bin` 的组合方式读取二进制消息。
- `pyproto/*.py`：protobuf 生成代码，提供 `ObjectList`、`Object` 等消息定义。
- `requirements.txt`：这个独立目录所需的最小第三方 Python 依赖。

## 环境准备

建议使用 Python 3。

安装依赖：

```bash
pip install -r requirements.txt
```

`requirements.txt` 中只有两个最小外部依赖：

- `protobuf>=3.6.1,<4`：用于加载当前目录中的 `*_pb2.py` 生成代码。
- `absl-py>=0.7`：`proto_reader/reader.py` 中使用了 `absl.logging`。

之所以把 `protobuf` 限制在 `<4`，是因为当前打包的 `pyproto/*.py` 是旧风格生成代码，和 4.x 及以上版本可能不兼容。

## 使用方法

### 1) 把 merge_tracking.json 转成 protobuf 三件套

```bash
python3 convert_merge_tracking.py /path/to/merge_tracking.json -o ./out
```

如果需要强制指定相机 ID：

```bash
python3 convert_merge_tracking.py /path/to/merge_tracking.json -o ./out --cam-id 4
```

执行后会在输出目录生成：

- `ObjectPerceptionObjectList.bin`
- `ObjectPerceptionObjectList.index.json`
- `ObjectPerceptionObjectList.data.json`

这三个文件的关系如下：

- `*.bin`：连续写入的 protobuf 二进制消息。
- `*.index.json`：记录每一帧消息在 `*.bin` 里的 `offset` 和 `size`。
- `*.data.json`：入口配置文件，告诉读取程序去哪里找 `bin` 和 `index`。

### 2) 把 protobuf 三件套转成 JSON 文本

```bash
python3 make_jl.py ./out/ObjectPerceptionObjectList.data.json -o ./out/ObjectPerceptionObjectList.jl
```

如果不加 `-o`，结果会直接输出到终端：

```bash
python3 make_jl.py ./out/ObjectPerceptionObjectList.data.json
```

说明：

- 虽然脚本命名和注释中把输出叫做 `jl` 或 `JsonLines`，但当前实现会给每条记录加缩进，所以每条记录可能占多行。
- 因此它更准确地说是“逐条追加的 JSON 文本文件”，而不是严格意义上的单行 JSONL。

## 脚本功能说明

### convert_merge_tracking.py

功能：

- 读取 `merge_tracking.json`。
- 把每一帧的 `detections` 转成 `ObjectPerceptionObjectList` protobuf。
- 按帧顺序写入二进制文件，并生成配套索引文件和入口描述文件。

适用场景：

- 需要把检测结果接入已有的 `ObjectPerceptionObjectList` 数据链路。
- 需要生成能被 `make_jl.py` 继续消费的中间数据。

### make_jl.py

功能：

- 读取 `ObjectPerceptionObjectList.data.json`。
- 根据 `index.json` 和 `bin` 逐条取出 protobuf 消息。
- 将每条 `ObjectList` 转成 JSON 文本并输出到文件或标准输出。

适用场景：

- 想快速查看 protobuf 内容。
- 想把二进制结果转成更易读、更方便后处理的 JSON 文本。

### proto_reader/reader.py

功能：

- 解析 `data.json` 中记录的文件位置。
- 读取 `index.json`。
- 按 `offset + size` 从 `bin` 中精确提取每一条 protobuf 消息的原始字节。

它是 `make_jl.py` 的底层读取器。

## 每个脚本的实现逻辑

### convert_merge_tracking.py 的实现逻辑

#### 1. 加载 protobuf 定义

脚本启动时会把当前目录下的 `pyproto` 加入 `sys.path`，然后导入：

- `object_pb2`
- `geometry_pb2`
- `camera_pb2`

其中真正核心的是 `object_pb2`，它定义了 `Object` 和 `ObjectList`。

#### 2. 定义映射表

脚本内置了三类映射：

- `CLASS_ID_MAP`：把输入里的 `class_id` 映射到 `ObjectType` 枚举。
- `ANCHOR_MAP`：把 anchor 字符串映射到 `AnchorPtInfo` 枚举。
- `FACE_CLS_MAP`：把 `face_cls` 映射到 `VehiclePose` 枚举。

这些映射决定了输入字段如何落到 protobuf 枚举值上。

#### 3. 从输入帧中提取 frame_id 和 cam_id

`parse_image_name()` 会优先从 `image_name` 中解析：

- 帧号 `frame_id`
- 相机号 `cam_id`

同时脚本也支持：

- 从 detection 的 `frame_id` 或 `frameId` 中覆盖帧号。
- 用 `--cam-id` 强制覆盖相机号。

#### 4. 把单个 detection 的公共字段填入 protobuf

`populate_object_fields()` 负责把一个 detection 的常用字段写入目标 `Object`，包括：

- 类别和枚举映射
- `track_id`
- `frame_id`
- `timestamp`
- `lane_assignment`
- 图像框信息 `bbox`
- 世界坐标下的 `pos / size / yaw`
- `face_cls` 对应的姿态
- 可选的 `model_3d` 信息

这个函数是整个转换逻辑的公共字段装配器。

#### 5. 构建 Mono3D 量测组件

`build_mono_measure_component()` 会从 `object_3d_ego` 中取出：

- `x, y, z`
- `l, h, w`
- `yaw`

再构造一个 `measure_type = kMeasureMono3D` 的 `Object`，作为量测组件挂到上层对象里。

#### 6. 构建最终 Object 的层次结构

`build_object()` 会生成两层核心对象，并形成嵌套关系：

- `source_obj`：表示单目来源对象，包含图像框、模型 3D、Mono3D 量测等信息。
- `obj`：最终输出对象，把 `source_obj` 作为 `key_components` 挂进去。

如果 `object_3d_ego` 存在，还会额外挂入一个更底层的 Mono3D 量测 `Object`。

因此最终会形成一种层次化组织：

- 最终目标对象
- 来源单目对象
- Mono3D 量测对象

#### 7. 按帧生成 ObjectList

`build_object_list()` 对单帧做处理：

- 创建一个 `ObjectList`
- 设置帧号和相机号
- 遍历该帧所有 `detections`
- 调用 `build_object()` 把每个 detection 追加到 `ObjectList.list`

如果第一条 detection 里带了 `version`，也会写到 `ObjectList.version`。

#### 8. 序列化并生成三件套文件

`convert()` 会遍历所有帧：

- 把每帧 `ObjectList` 序列化成字节串
- 依次写入 `ObjectPerceptionObjectList.bin`
- 记录每条消息的 `frame_id / offset / size`
- 最后生成 `ObjectPerceptionObjectList.index.json`
- 再生成 `ObjectPerceptionObjectList.data.json`

这样输出结果既能高效存储，也能被后续工具顺序读取。

### make_jl.py 的实现逻辑

#### 1. 解析命令行参数

脚本接收：

- 位置参数 `data_json`
- 可选参数 `-o / --output`

其中 `data_json` 就是 `ObjectPerceptionObjectList.data.json`。

#### 2. 通过 Reader 定位真实数据文件

`make_jsonl()` 内部先创建：

```python
reader = pb_reader.Reader(data_json_path)
```

`Reader` 会：

- 读取 `data.json`
- 找到其中声明的 `index` 和 `data`
- 拼出真实的 `index.json` 和 `bin` 路径

#### 3. 逐条读取 protobuf 消息

`reader.each()` 会遍历 `index.json` 中的每个索引项：

- 从索引中拿到 `offset`
- 从索引中拿到 `size`
- 在 `bin` 中 seek 到对应位置
- 读取这一条消息的原始字节

#### 4. 把字节反序列化成 ObjectList

读取到的每个 chunk 会被解析成：

```python
object_pb2.ObjectList.FromString(chunk)
```

这样就把原始二进制恢复为 protobuf 对象。

#### 5. 转成 JSON 并输出

随后脚本调用：

```python
json_format.MessageToDict(objs, including_default_value_fields=True)
```

把 protobuf 对象转成 Python 字典，并保留默认值字段。

最后再用 `json.dumps(..., indent=2)` 输出到：

- 指定文件 `-o`
- 或标准输出

### proto_reader/reader.py 的实现逻辑

#### 1. 读取 data.json

`Reader.__init__()` 会先打开 `data.json`，读取：

- `index[0]`
- `data[0]`

并把它们解析成相对于 `data.json` 所在目录的真实路径。

#### 2. 加载 index.json

接着脚本把 `index.json` 读入内存，保存为 `self.object_index`。

索引中的每一项通常是：

```json
[frame_id, offset, size]
```

后续真正读取数据时主要使用 `offset` 和 `size`。

#### 3. 迭代返回原始消息字节

`each()` 打开二进制文件后，会对每条索引执行：

- `seek(offset)`
- `read(size)`
- `yield bts`

因此上层调用方可以把它当作一个“按条产出 protobuf 消息”的生成器。

## 推荐使用顺序

```bash
python3 convert_merge_tracking.py /path/to/merge_tracking.json -o ./out
python3 make_jl.py ./out/ObjectPerceptionObjectList.data.json -o ./out/ObjectPerceptionObjectList.jl
```

如果你只想验证转换是否成功，优先检查：

- `./out/ObjectPerceptionObjectList.data.json`
- `./out/ObjectPerceptionObjectList.index.json`
- `./out/ObjectPerceptionObjectList.bin`

如果你想看可读结果，再检查：

- `./out/ObjectPerceptionObjectList.jl`
