Metadata-Version: 2.4
Name: mdrive4-json
Version: 0.0.4
Summary: Read and edit mdrive4 JSON calibration files with sensor->ego extrinsics.
License-Expression: MIT
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: numpy>=1.20.0
Requires-Dist: PyYAML>=6.0
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Requires-Dist: build; extra == "dev"
Requires-Dist: twine; extra == "dev"

# mdrive4-json

读取和编辑 mdrive4 JSON 标定目录的小工具。包名为 `mdrive4-json`，导入名为
`mdrive4_json`。

重要约定：所有 JSON 外参均按原文件解释为 `sensor->ego`，其中 `ego` 等同于
`vrf_ground`，即后轴中心接地点。本工具不自动求逆、不转换单位、不改写坐标系语义。
如需与官方 `ParamsTreeHelper` 对齐，调用 `load_numpy_calibration(..., ego_frame="vrf_ground")`。

## 官方映射参考

D11 只负责 mdrive4 JSON 的读取、编辑和 workspace 管理，不负责从官方 PB 生成
mdrive4 JSON。官方 `vehicle_config.pb.txt` 到 JSON 的映射、坐标规则和初始化模板，
以 mdrive 仓库中的 `params_convert_l2` 为准：

- 官方 Git 地址：`https://git.minieye.tech/ad/mdrive/mdrive/-/tree/xzt_new_factory_calib`
- 当前参考分支：`xzt_new_factory_calib`
- 官方转换目录：`modules/calibration/params_convert_l2/`
- 重点查询文件：`conf/params.json`、`README_params_convert_l2.md`、`src/params_convert_l2_main.cc`、`init_template/`

当前机器可参考本地路径
`/home/mini/code/mdrive_git/mdrive/modules/calibration/params_convert_l2/`，但本地仓库路径和分支可能变化。
查询官方规则时优先以 Git URL 对应分支为稳定入口。

职责边界：

- D11 不隐式复刻 `params_convert_l2` 的 PB/C01 转换逻辑。
- `params_convert_l2` 负责按官方映射和坐标规则从 `vehicle_config.pb.txt` 生成 JSON。
- 当前不调整 `create_sensor` / `update_sensor`，避免把官方转换逻辑混入 JSON 编辑 API。

## 安装

```bash
pip install .
```

开发验证建议使用仓库约定环境：

```bash
conda run -n py310 python -m pytest tests
```

## Python API

```python
from mdrive4_json import (
    build,
    load_dataset,
    load_numpy_calibration,
    parse,
    update_record,
    update_sensor,
    validate_workspace,
)

dataset = load_dataset("/home/mini/Downloads/output_21_0529")
record = dataset.get("at128p_front")
print(record.extrinsic)  # {"pos": [...], "roll": ..., "pitch": ..., "yaw": ...}

numpy_dataset = load_numpy_calibration("/home/mini/Downloads/output_21_0529")
edge = numpy_dataset.get_extrinsic("camera1")
print(edge.source_frame, edge.target_frame)  # camera1 ego
print(edge.translation)  # numpy.ndarray shape=(3,)
print(edge.rotation)     # [qx, qy, qz, qw], degree ZYX RPY

camera = numpy_dataset.get_intrinsic("camera1")
print(camera.K)   # 3x3 numpy.ndarray
print(camera.pb)  # D02.PB文件 camera_params 风格 dict

update_record(
    "/home/mini/Downloads/output_21_0529",
    "camera1",
    {"yaw": 0.2, "focal_u": 7350.0},
    output_dir="/tmp/output_21_0529_edited",
)

parse("/home/mini/Downloads/output_21_0529", "/tmp/output_21_0529_workspace")
update_sensor("/tmp/output_21_0529_workspace", "camera1", {"yaw": 0.2})
assert validate_workspace("/tmp/output_21_0529_workspace")["valid"]
build("/tmp/output_21_0529_workspace", "/tmp/output_21_0529_rebuilt")
```

公开 API：

- `load_dataset(input_dir) -> MDrive4JsonDataset`
- `load_numpy_calibration(input_dir, ego_frame="ego") -> NumpyCalibrationDataset`
- `save_dataset(dataset, output_dir=None, inplace=False)`
- `list_records(input_dir) -> list[CalibrationRecord]`
- `update_record(input_dir, sensor_id, patch, output_dir=None, inplace=False, allow_unknown=False)`
- `parse(input_dir, workspace_dir) -> tuple[dict, dict]`
- `build(workspace_dir, output_dir) -> list[str]`
- `validate_workspace(workspace_dir) -> dict`
- `list_sensors/get_sensor/create_sensor/update_sensor/delete_sensor/replace_sensors`
- `MDrive4JsonWorkspaceManager(default_workspace_dir)`

`sensor_id` 规则：

- 主 ID 优先使用 JSON `frame_id`；缺失时回退文件名 stem。
- camera JSON 的 `camera{camera_id}` 与文件名 stem 会作为查询 alias 保留，例如 `dataset.get("camera1")` 可兼容旧调用。
- alias 仅用于查询兼容；`NumpyExtrinsic.source_frame` 和 `NumpyIntrinsic.frame_id` 始终使用主 ID。

外参字段固定读取：

- `pos[x,y,z]`
- `roll`
- `pitch`
- `yaw`

camera 额外暴露常见内参字段：

- `focal_u`、`focal_v`、`cu`、`cv`
- `distort_coeffs`
- `image_width`、`image_height`
- `prj_model`、`fov`
- `affine_params`、`poly_coeffs`、`inv_poly_coeffs` 等

未知原始字段会保留。编辑未知字段默认拒绝，确需写入时传
`allow_unknown=True` 或 CLI `--allow-unknown`。

## numpy / PB 风格读取

`load_numpy_calibration()` 是推荐的统一读取入口：

- 外参返回 `NumpyExtrinsic`：`source_frame=sensor_id`，`target_frame=ego_frame`，`translation` 为 `(3,)`，`rotation` 为 `[qx, qy, qz, qw]`。
- 变换语义固定为 `p_root = T * p_source`，即 `source_frame->ego_frame`；不求逆、不做轴系转换、不改单位。
- 默认 `ego_frame="ego"`，文档语义为 `vrf_ground` 后轴中心接地点。
- camera 内参返回 `NumpyIntrinsic`：`K` 为 `(3,3)`，`D` 为畸变数组，并保留 D02 风格 `pb` dict。

PB dict 示例：

```python
{
    "frame_id": "camera1",
    "model_type": "PINHOLE",
    "pinhole": {
        "width": 3840,
        "height": 2160,
        "intrinsic": [focal_u, s, cu, 0.0, focal_v, cv, 0.0, 0.0, 1.0],
        "distortion": distort_coeffs,
    },
}
```

`prj_model == 5` 输出 `model_type="FISHEYE"` 和 `fisheye` block；其它已见值
`1/3` 默认输出 `model_type="PINHOLE"` 和 `pinhole` block。`s` 缺省为 `0.0`。

## YAML 工作区

`0.0.4` 提供 D02 风格中间工作区，便于多个 API 共享编辑状态并统一校验/build：

- `order_manifest.yaml`：按顺序记录 `sensor_id`、`file`、原始 `json_file`、`is_camera`、alias。
- `sensors/{sensor_id}.yaml`：每个传感器一个可编辑 YAML，字段保持原 JSON key，不做语义转换。
- `.mdrive4_json_meta.json`：记录源目录、原始文件名、alias、hash 等追踪信息。

典型流程：

```python
from mdrive4_json import MDrive4JsonWorkspaceManager

mgr = MDrive4JsonWorkspaceManager("/tmp/output_21_0529_workspace")
mgr.parse("/home/mini/Downloads/output_21_0529")
mgr.update_sensor("camera1", {"yaw": 0.2, "focal_u": 7350.0})
mgr.build("/tmp/output_21_0529_rebuilt")
```

`build()` 会先调用 `validate_workspace()`，发现 manifest 引用缺失文件、孤儿
`sensors/*.yaml`、重复 `sensor_id/json_file`、pose 非法等问题时拒绝输出。

真实样例 smoke：

```bash
cd D_pypi/D11.mdrive4-json处理/V0.0.4
conda run -n py310 python -m pytest tests
```

若 `/home/mini/Downloads/output_21_0529` 存在，测试会验证 15 条外参和 6 条 camera 内参。

## CLI

```bash
mdrive4-json summary /home/mini/Downloads/output_21_0529
mdrive4-json show /home/mini/Downloads/output_21_0529 --sensor camera1
mdrive4-json edit /home/mini/Downloads/output_21_0529 --sensor camera1 --set yaw=0.2 --output-dir /tmp/edited
mdrive4-json edit /home/mini/Downloads/output_21_0529 --sensor camera1 --set yaw=0.2 --inplace
mdrive4-json parse /home/mini/Downloads/output_21_0529 -o /tmp/output_21_0529_workspace
mdrive4-json validate -i /tmp/output_21_0529_workspace
mdrive4-json build -i /tmp/output_21_0529_workspace -o /tmp/output_21_0529_rebuilt
```

`--set` 的值优先按 JSON 解析，因此列表和布尔值可这样写：

```bash
mdrive4-json edit input --sensor camera1 --set 'pos=[1,2,3]' --set is_valid=true --output-dir output
```

## 写盘规则

- 默认不写盘；必须显式选择 `output_dir` 或 `inplace=True`。
- `output_dir` 模式要求目标目录不存在，并复制输入目录全部 JSON，再修改目标文件。
- `inplace=True` 会覆盖输入目录中的 JSON。
- 写入型 API 自动维护顶层 `calib_timestamp`：当某个传感器 JSON/YAML 除 `calib_timestamp` 外的有效内容发生变化时，写出的对应文件会刷新为运行机器本地时区当前时间，格式为 `YYYY-MM-DD-HH-MM-SS`。
- 如果 patch 后有效内容与原文件一致，不会仅为了刷新 `calib_timestamp` 而写盘。
- `calib_timestamp` 是工具维护字段，不能通过 Python API patch 或 CLI `--set` 手动修改；即使启用 `allow_unknown=True` / `--allow-unknown` 也会拒绝。
- `build()` 从 workspace 生成 JSON 时保留 sensor YAML 中已有的 `calib_timestamp`，不会因为只读构建流程额外刷新时间。
- JSON 使用 UTF-8、`ensure_ascii=False`、4 空格缩进保存。

## 校验

读取时会校验：

- 输入目录存在且包含 JSON。
- JSON 根节点必须是对象。
- 每个文件使用 `frame_id` 或文件名 stem 识别主 `sensor_id`。
- `sensor_id` 不能重复。
- `pos` 必须是 3 个数字，`roll/pitch/yaw` 必须是数字。

编辑 camera 内参时会做基础类型校验。未知 key 默认拒绝。
