# 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`