mdrive4-json 0.0.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ include README.md
@@ -0,0 +1,231 @@
1
+ Metadata-Version: 2.4
2
+ Name: mdrive4-json
3
+ Version: 0.0.4
4
+ Summary: Read and edit mdrive4 JSON calibration files with sensor->ego extrinsics.
5
+ License-Expression: MIT
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: numpy>=1.20.0
9
+ Requires-Dist: PyYAML>=6.0
10
+ Provides-Extra: dev
11
+ Requires-Dist: pytest; extra == "dev"
12
+ Requires-Dist: build; extra == "dev"
13
+ Requires-Dist: twine; extra == "dev"
14
+
15
+ # mdrive4-json
16
+
17
+ 读取和编辑 mdrive4 JSON 标定目录的小工具。包名为 `mdrive4-json`,导入名为
18
+ `mdrive4_json`。
19
+
20
+ 重要约定:所有 JSON 外参均按原文件解释为 `sensor->ego`,其中 `ego` 等同于
21
+ `vrf_ground`,即后轴中心接地点。本工具不自动求逆、不转换单位、不改写坐标系语义。
22
+ 如需与官方 `ParamsTreeHelper` 对齐,调用 `load_numpy_calibration(..., ego_frame="vrf_ground")`。
23
+
24
+ ## 官方映射参考
25
+
26
+ D11 只负责 mdrive4 JSON 的读取、编辑和 workspace 管理,不负责从官方 PB 生成
27
+ mdrive4 JSON。官方 `vehicle_config.pb.txt` 到 JSON 的映射、坐标规则和初始化模板,
28
+ 以 mdrive 仓库中的 `params_convert_l2` 为准:
29
+
30
+ - 官方 Git 地址:`https://git.minieye.tech/ad/mdrive/mdrive/-/tree/xzt_new_factory_calib`
31
+ - 当前参考分支:`xzt_new_factory_calib`
32
+ - 官方转换目录:`modules/calibration/params_convert_l2/`
33
+ - 重点查询文件:`conf/params.json`、`README_params_convert_l2.md`、`src/params_convert_l2_main.cc`、`init_template/`
34
+
35
+ 当前机器可参考本地路径
36
+ `/home/mini/code/mdrive_git/mdrive/modules/calibration/params_convert_l2/`,但本地仓库路径和分支可能变化。
37
+ 查询官方规则时优先以 Git URL 对应分支为稳定入口。
38
+
39
+ 职责边界:
40
+
41
+ - D11 不隐式复刻 `params_convert_l2` 的 PB/C01 转换逻辑。
42
+ - `params_convert_l2` 负责按官方映射和坐标规则从 `vehicle_config.pb.txt` 生成 JSON。
43
+ - 当前不调整 `create_sensor` / `update_sensor`,避免把官方转换逻辑混入 JSON 编辑 API。
44
+
45
+ ## 安装
46
+
47
+ ```bash
48
+ pip install .
49
+ ```
50
+
51
+ 开发验证建议使用仓库约定环境:
52
+
53
+ ```bash
54
+ conda run -n py310 python -m pytest tests
55
+ ```
56
+
57
+ ## Python API
58
+
59
+ ```python
60
+ from mdrive4_json import (
61
+ build,
62
+ load_dataset,
63
+ load_numpy_calibration,
64
+ parse,
65
+ update_record,
66
+ update_sensor,
67
+ validate_workspace,
68
+ )
69
+
70
+ dataset = load_dataset("/home/mini/Downloads/output_21_0529")
71
+ record = dataset.get("at128p_front")
72
+ print(record.extrinsic) # {"pos": [...], "roll": ..., "pitch": ..., "yaw": ...}
73
+
74
+ numpy_dataset = load_numpy_calibration("/home/mini/Downloads/output_21_0529")
75
+ edge = numpy_dataset.get_extrinsic("camera1")
76
+ print(edge.source_frame, edge.target_frame) # camera1 ego
77
+ print(edge.translation) # numpy.ndarray shape=(3,)
78
+ print(edge.rotation) # [qx, qy, qz, qw], degree ZYX RPY
79
+
80
+ camera = numpy_dataset.get_intrinsic("camera1")
81
+ print(camera.K) # 3x3 numpy.ndarray
82
+ print(camera.pb) # D02.PB文件 camera_params 风格 dict
83
+
84
+ update_record(
85
+ "/home/mini/Downloads/output_21_0529",
86
+ "camera1",
87
+ {"yaw": 0.2, "focal_u": 7350.0},
88
+ output_dir="/tmp/output_21_0529_edited",
89
+ )
90
+
91
+ parse("/home/mini/Downloads/output_21_0529", "/tmp/output_21_0529_workspace")
92
+ update_sensor("/tmp/output_21_0529_workspace", "camera1", {"yaw": 0.2})
93
+ assert validate_workspace("/tmp/output_21_0529_workspace")["valid"]
94
+ build("/tmp/output_21_0529_workspace", "/tmp/output_21_0529_rebuilt")
95
+ ```
96
+
97
+ 公开 API:
98
+
99
+ - `load_dataset(input_dir) -> MDrive4JsonDataset`
100
+ - `load_numpy_calibration(input_dir, ego_frame="ego") -> NumpyCalibrationDataset`
101
+ - `save_dataset(dataset, output_dir=None, inplace=False)`
102
+ - `list_records(input_dir) -> list[CalibrationRecord]`
103
+ - `update_record(input_dir, sensor_id, patch, output_dir=None, inplace=False, allow_unknown=False)`
104
+ - `parse(input_dir, workspace_dir) -> tuple[dict, dict]`
105
+ - `build(workspace_dir, output_dir) -> list[str]`
106
+ - `validate_workspace(workspace_dir) -> dict`
107
+ - `list_sensors/get_sensor/create_sensor/update_sensor/delete_sensor/replace_sensors`
108
+ - `MDrive4JsonWorkspaceManager(default_workspace_dir)`
109
+
110
+ `sensor_id` 规则:
111
+
112
+ - 主 ID 优先使用 JSON `frame_id`;缺失时回退文件名 stem。
113
+ - camera JSON 的 `camera{camera_id}` 与文件名 stem 会作为查询 alias 保留,例如 `dataset.get("camera1")` 可兼容旧调用。
114
+ - alias 仅用于查询兼容;`NumpyExtrinsic.source_frame` 和 `NumpyIntrinsic.frame_id` 始终使用主 ID。
115
+
116
+ 外参字段固定读取:
117
+
118
+ - `pos[x,y,z]`
119
+ - `roll`
120
+ - `pitch`
121
+ - `yaw`
122
+
123
+ camera 额外暴露常见内参字段:
124
+
125
+ - `focal_u`、`focal_v`、`cu`、`cv`
126
+ - `distort_coeffs`
127
+ - `image_width`、`image_height`
128
+ - `prj_model`、`fov`
129
+ - `affine_params`、`poly_coeffs`、`inv_poly_coeffs` 等
130
+
131
+ 未知原始字段会保留。编辑未知字段默认拒绝,确需写入时传
132
+ `allow_unknown=True` 或 CLI `--allow-unknown`。
133
+
134
+ ## numpy / PB 风格读取
135
+
136
+ `load_numpy_calibration()` 是推荐的统一读取入口:
137
+
138
+ - 外参返回 `NumpyExtrinsic`:`source_frame=sensor_id`,`target_frame=ego_frame`,`translation` 为 `(3,)`,`rotation` 为 `[qx, qy, qz, qw]`。
139
+ - 变换语义固定为 `p_root = T * p_source`,即 `source_frame->ego_frame`;不求逆、不做轴系转换、不改单位。
140
+ - 默认 `ego_frame="ego"`,文档语义为 `vrf_ground` 后轴中心接地点。
141
+ - camera 内参返回 `NumpyIntrinsic`:`K` 为 `(3,3)`,`D` 为畸变数组,并保留 D02 风格 `pb` dict。
142
+
143
+ PB dict 示例:
144
+
145
+ ```python
146
+ {
147
+ "frame_id": "camera1",
148
+ "model_type": "PINHOLE",
149
+ "pinhole": {
150
+ "width": 3840,
151
+ "height": 2160,
152
+ "intrinsic": [focal_u, s, cu, 0.0, focal_v, cv, 0.0, 0.0, 1.0],
153
+ "distortion": distort_coeffs,
154
+ },
155
+ }
156
+ ```
157
+
158
+ `prj_model == 5` 输出 `model_type="FISHEYE"` 和 `fisheye` block;其它已见值
159
+ `1/3` 默认输出 `model_type="PINHOLE"` 和 `pinhole` block。`s` 缺省为 `0.0`。
160
+
161
+ ## YAML 工作区
162
+
163
+ `0.0.4` 提供 D02 风格中间工作区,便于多个 API 共享编辑状态并统一校验/build:
164
+
165
+ - `order_manifest.yaml`:按顺序记录 `sensor_id`、`file`、原始 `json_file`、`is_camera`、alias。
166
+ - `sensors/{sensor_id}.yaml`:每个传感器一个可编辑 YAML,字段保持原 JSON key,不做语义转换。
167
+ - `.mdrive4_json_meta.json`:记录源目录、原始文件名、alias、hash 等追踪信息。
168
+
169
+ 典型流程:
170
+
171
+ ```python
172
+ from mdrive4_json import MDrive4JsonWorkspaceManager
173
+
174
+ mgr = MDrive4JsonWorkspaceManager("/tmp/output_21_0529_workspace")
175
+ mgr.parse("/home/mini/Downloads/output_21_0529")
176
+ mgr.update_sensor("camera1", {"yaw": 0.2, "focal_u": 7350.0})
177
+ mgr.build("/tmp/output_21_0529_rebuilt")
178
+ ```
179
+
180
+ `build()` 会先调用 `validate_workspace()`,发现 manifest 引用缺失文件、孤儿
181
+ `sensors/*.yaml`、重复 `sensor_id/json_file`、pose 非法等问题时拒绝输出。
182
+
183
+ 真实样例 smoke:
184
+
185
+ ```bash
186
+ cd D_pypi/D11.mdrive4-json处理/V0.0.4
187
+ conda run -n py310 python -m pytest tests
188
+ ```
189
+
190
+ 若 `/home/mini/Downloads/output_21_0529` 存在,测试会验证 15 条外参和 6 条 camera 内参。
191
+
192
+ ## CLI
193
+
194
+ ```bash
195
+ mdrive4-json summary /home/mini/Downloads/output_21_0529
196
+ mdrive4-json show /home/mini/Downloads/output_21_0529 --sensor camera1
197
+ mdrive4-json edit /home/mini/Downloads/output_21_0529 --sensor camera1 --set yaw=0.2 --output-dir /tmp/edited
198
+ mdrive4-json edit /home/mini/Downloads/output_21_0529 --sensor camera1 --set yaw=0.2 --inplace
199
+ mdrive4-json parse /home/mini/Downloads/output_21_0529 -o /tmp/output_21_0529_workspace
200
+ mdrive4-json validate -i /tmp/output_21_0529_workspace
201
+ mdrive4-json build -i /tmp/output_21_0529_workspace -o /tmp/output_21_0529_rebuilt
202
+ ```
203
+
204
+ `--set` 的值优先按 JSON 解析,因此列表和布尔值可这样写:
205
+
206
+ ```bash
207
+ mdrive4-json edit input --sensor camera1 --set 'pos=[1,2,3]' --set is_valid=true --output-dir output
208
+ ```
209
+
210
+ ## 写盘规则
211
+
212
+ - 默认不写盘;必须显式选择 `output_dir` 或 `inplace=True`。
213
+ - `output_dir` 模式要求目标目录不存在,并复制输入目录全部 JSON,再修改目标文件。
214
+ - `inplace=True` 会覆盖输入目录中的 JSON。
215
+ - 写入型 API 自动维护顶层 `calib_timestamp`:当某个传感器 JSON/YAML 除 `calib_timestamp` 外的有效内容发生变化时,写出的对应文件会刷新为运行机器本地时区当前时间,格式为 `YYYY-MM-DD-HH-MM-SS`。
216
+ - 如果 patch 后有效内容与原文件一致,不会仅为了刷新 `calib_timestamp` 而写盘。
217
+ - `calib_timestamp` 是工具维护字段,不能通过 Python API patch 或 CLI `--set` 手动修改;即使启用 `allow_unknown=True` / `--allow-unknown` 也会拒绝。
218
+ - `build()` 从 workspace 生成 JSON 时保留 sensor YAML 中已有的 `calib_timestamp`,不会因为只读构建流程额外刷新时间。
219
+ - JSON 使用 UTF-8、`ensure_ascii=False`、4 空格缩进保存。
220
+
221
+ ## 校验
222
+
223
+ 读取时会校验:
224
+
225
+ - 输入目录存在且包含 JSON。
226
+ - JSON 根节点必须是对象。
227
+ - 每个文件使用 `frame_id` 或文件名 stem 识别主 `sensor_id`。
228
+ - `sensor_id` 不能重复。
229
+ - `pos` 必须是 3 个数字,`roll/pitch/yaw` 必须是数字。
230
+
231
+ 编辑 camera 内参时会做基础类型校验。未知 key 默认拒绝。
@@ -0,0 +1,217 @@
1
+ # mdrive4-json
2
+
3
+ 读取和编辑 mdrive4 JSON 标定目录的小工具。包名为 `mdrive4-json`,导入名为
4
+ `mdrive4_json`。
5
+
6
+ 重要约定:所有 JSON 外参均按原文件解释为 `sensor->ego`,其中 `ego` 等同于
7
+ `vrf_ground`,即后轴中心接地点。本工具不自动求逆、不转换单位、不改写坐标系语义。
8
+ 如需与官方 `ParamsTreeHelper` 对齐,调用 `load_numpy_calibration(..., ego_frame="vrf_ground")`。
9
+
10
+ ## 官方映射参考
11
+
12
+ D11 只负责 mdrive4 JSON 的读取、编辑和 workspace 管理,不负责从官方 PB 生成
13
+ mdrive4 JSON。官方 `vehicle_config.pb.txt` 到 JSON 的映射、坐标规则和初始化模板,
14
+ 以 mdrive 仓库中的 `params_convert_l2` 为准:
15
+
16
+ - 官方 Git 地址:`https://git.minieye.tech/ad/mdrive/mdrive/-/tree/xzt_new_factory_calib`
17
+ - 当前参考分支:`xzt_new_factory_calib`
18
+ - 官方转换目录:`modules/calibration/params_convert_l2/`
19
+ - 重点查询文件:`conf/params.json`、`README_params_convert_l2.md`、`src/params_convert_l2_main.cc`、`init_template/`
20
+
21
+ 当前机器可参考本地路径
22
+ `/home/mini/code/mdrive_git/mdrive/modules/calibration/params_convert_l2/`,但本地仓库路径和分支可能变化。
23
+ 查询官方规则时优先以 Git URL 对应分支为稳定入口。
24
+
25
+ 职责边界:
26
+
27
+ - D11 不隐式复刻 `params_convert_l2` 的 PB/C01 转换逻辑。
28
+ - `params_convert_l2` 负责按官方映射和坐标规则从 `vehicle_config.pb.txt` 生成 JSON。
29
+ - 当前不调整 `create_sensor` / `update_sensor`,避免把官方转换逻辑混入 JSON 编辑 API。
30
+
31
+ ## 安装
32
+
33
+ ```bash
34
+ pip install .
35
+ ```
36
+
37
+ 开发验证建议使用仓库约定环境:
38
+
39
+ ```bash
40
+ conda run -n py310 python -m pytest tests
41
+ ```
42
+
43
+ ## Python API
44
+
45
+ ```python
46
+ from mdrive4_json import (
47
+ build,
48
+ load_dataset,
49
+ load_numpy_calibration,
50
+ parse,
51
+ update_record,
52
+ update_sensor,
53
+ validate_workspace,
54
+ )
55
+
56
+ dataset = load_dataset("/home/mini/Downloads/output_21_0529")
57
+ record = dataset.get("at128p_front")
58
+ print(record.extrinsic) # {"pos": [...], "roll": ..., "pitch": ..., "yaw": ...}
59
+
60
+ numpy_dataset = load_numpy_calibration("/home/mini/Downloads/output_21_0529")
61
+ edge = numpy_dataset.get_extrinsic("camera1")
62
+ print(edge.source_frame, edge.target_frame) # camera1 ego
63
+ print(edge.translation) # numpy.ndarray shape=(3,)
64
+ print(edge.rotation) # [qx, qy, qz, qw], degree ZYX RPY
65
+
66
+ camera = numpy_dataset.get_intrinsic("camera1")
67
+ print(camera.K) # 3x3 numpy.ndarray
68
+ print(camera.pb) # D02.PB文件 camera_params 风格 dict
69
+
70
+ update_record(
71
+ "/home/mini/Downloads/output_21_0529",
72
+ "camera1",
73
+ {"yaw": 0.2, "focal_u": 7350.0},
74
+ output_dir="/tmp/output_21_0529_edited",
75
+ )
76
+
77
+ parse("/home/mini/Downloads/output_21_0529", "/tmp/output_21_0529_workspace")
78
+ update_sensor("/tmp/output_21_0529_workspace", "camera1", {"yaw": 0.2})
79
+ assert validate_workspace("/tmp/output_21_0529_workspace")["valid"]
80
+ build("/tmp/output_21_0529_workspace", "/tmp/output_21_0529_rebuilt")
81
+ ```
82
+
83
+ 公开 API:
84
+
85
+ - `load_dataset(input_dir) -> MDrive4JsonDataset`
86
+ - `load_numpy_calibration(input_dir, ego_frame="ego") -> NumpyCalibrationDataset`
87
+ - `save_dataset(dataset, output_dir=None, inplace=False)`
88
+ - `list_records(input_dir) -> list[CalibrationRecord]`
89
+ - `update_record(input_dir, sensor_id, patch, output_dir=None, inplace=False, allow_unknown=False)`
90
+ - `parse(input_dir, workspace_dir) -> tuple[dict, dict]`
91
+ - `build(workspace_dir, output_dir) -> list[str]`
92
+ - `validate_workspace(workspace_dir) -> dict`
93
+ - `list_sensors/get_sensor/create_sensor/update_sensor/delete_sensor/replace_sensors`
94
+ - `MDrive4JsonWorkspaceManager(default_workspace_dir)`
95
+
96
+ `sensor_id` 规则:
97
+
98
+ - 主 ID 优先使用 JSON `frame_id`;缺失时回退文件名 stem。
99
+ - camera JSON 的 `camera{camera_id}` 与文件名 stem 会作为查询 alias 保留,例如 `dataset.get("camera1")` 可兼容旧调用。
100
+ - alias 仅用于查询兼容;`NumpyExtrinsic.source_frame` 和 `NumpyIntrinsic.frame_id` 始终使用主 ID。
101
+
102
+ 外参字段固定读取:
103
+
104
+ - `pos[x,y,z]`
105
+ - `roll`
106
+ - `pitch`
107
+ - `yaw`
108
+
109
+ camera 额外暴露常见内参字段:
110
+
111
+ - `focal_u`、`focal_v`、`cu`、`cv`
112
+ - `distort_coeffs`
113
+ - `image_width`、`image_height`
114
+ - `prj_model`、`fov`
115
+ - `affine_params`、`poly_coeffs`、`inv_poly_coeffs` 等
116
+
117
+ 未知原始字段会保留。编辑未知字段默认拒绝,确需写入时传
118
+ `allow_unknown=True` 或 CLI `--allow-unknown`。
119
+
120
+ ## numpy / PB 风格读取
121
+
122
+ `load_numpy_calibration()` 是推荐的统一读取入口:
123
+
124
+ - 外参返回 `NumpyExtrinsic`:`source_frame=sensor_id`,`target_frame=ego_frame`,`translation` 为 `(3,)`,`rotation` 为 `[qx, qy, qz, qw]`。
125
+ - 变换语义固定为 `p_root = T * p_source`,即 `source_frame->ego_frame`;不求逆、不做轴系转换、不改单位。
126
+ - 默认 `ego_frame="ego"`,文档语义为 `vrf_ground` 后轴中心接地点。
127
+ - camera 内参返回 `NumpyIntrinsic`:`K` 为 `(3,3)`,`D` 为畸变数组,并保留 D02 风格 `pb` dict。
128
+
129
+ PB dict 示例:
130
+
131
+ ```python
132
+ {
133
+ "frame_id": "camera1",
134
+ "model_type": "PINHOLE",
135
+ "pinhole": {
136
+ "width": 3840,
137
+ "height": 2160,
138
+ "intrinsic": [focal_u, s, cu, 0.0, focal_v, cv, 0.0, 0.0, 1.0],
139
+ "distortion": distort_coeffs,
140
+ },
141
+ }
142
+ ```
143
+
144
+ `prj_model == 5` 输出 `model_type="FISHEYE"` 和 `fisheye` block;其它已见值
145
+ `1/3` 默认输出 `model_type="PINHOLE"` 和 `pinhole` block。`s` 缺省为 `0.0`。
146
+
147
+ ## YAML 工作区
148
+
149
+ `0.0.4` 提供 D02 风格中间工作区,便于多个 API 共享编辑状态并统一校验/build:
150
+
151
+ - `order_manifest.yaml`:按顺序记录 `sensor_id`、`file`、原始 `json_file`、`is_camera`、alias。
152
+ - `sensors/{sensor_id}.yaml`:每个传感器一个可编辑 YAML,字段保持原 JSON key,不做语义转换。
153
+ - `.mdrive4_json_meta.json`:记录源目录、原始文件名、alias、hash 等追踪信息。
154
+
155
+ 典型流程:
156
+
157
+ ```python
158
+ from mdrive4_json import MDrive4JsonWorkspaceManager
159
+
160
+ mgr = MDrive4JsonWorkspaceManager("/tmp/output_21_0529_workspace")
161
+ mgr.parse("/home/mini/Downloads/output_21_0529")
162
+ mgr.update_sensor("camera1", {"yaw": 0.2, "focal_u": 7350.0})
163
+ mgr.build("/tmp/output_21_0529_rebuilt")
164
+ ```
165
+
166
+ `build()` 会先调用 `validate_workspace()`,发现 manifest 引用缺失文件、孤儿
167
+ `sensors/*.yaml`、重复 `sensor_id/json_file`、pose 非法等问题时拒绝输出。
168
+
169
+ 真实样例 smoke:
170
+
171
+ ```bash
172
+ cd D_pypi/D11.mdrive4-json处理/V0.0.4
173
+ conda run -n py310 python -m pytest tests
174
+ ```
175
+
176
+ 若 `/home/mini/Downloads/output_21_0529` 存在,测试会验证 15 条外参和 6 条 camera 内参。
177
+
178
+ ## CLI
179
+
180
+ ```bash
181
+ mdrive4-json summary /home/mini/Downloads/output_21_0529
182
+ mdrive4-json show /home/mini/Downloads/output_21_0529 --sensor camera1
183
+ mdrive4-json edit /home/mini/Downloads/output_21_0529 --sensor camera1 --set yaw=0.2 --output-dir /tmp/edited
184
+ mdrive4-json edit /home/mini/Downloads/output_21_0529 --sensor camera1 --set yaw=0.2 --inplace
185
+ mdrive4-json parse /home/mini/Downloads/output_21_0529 -o /tmp/output_21_0529_workspace
186
+ mdrive4-json validate -i /tmp/output_21_0529_workspace
187
+ mdrive4-json build -i /tmp/output_21_0529_workspace -o /tmp/output_21_0529_rebuilt
188
+ ```
189
+
190
+ `--set` 的值优先按 JSON 解析,因此列表和布尔值可这样写:
191
+
192
+ ```bash
193
+ mdrive4-json edit input --sensor camera1 --set 'pos=[1,2,3]' --set is_valid=true --output-dir output
194
+ ```
195
+
196
+ ## 写盘规则
197
+
198
+ - 默认不写盘;必须显式选择 `output_dir` 或 `inplace=True`。
199
+ - `output_dir` 模式要求目标目录不存在,并复制输入目录全部 JSON,再修改目标文件。
200
+ - `inplace=True` 会覆盖输入目录中的 JSON。
201
+ - 写入型 API 自动维护顶层 `calib_timestamp`:当某个传感器 JSON/YAML 除 `calib_timestamp` 外的有效内容发生变化时,写出的对应文件会刷新为运行机器本地时区当前时间,格式为 `YYYY-MM-DD-HH-MM-SS`。
202
+ - 如果 patch 后有效内容与原文件一致,不会仅为了刷新 `calib_timestamp` 而写盘。
203
+ - `calib_timestamp` 是工具维护字段,不能通过 Python API patch 或 CLI `--set` 手动修改;即使启用 `allow_unknown=True` / `--allow-unknown` 也会拒绝。
204
+ - `build()` 从 workspace 生成 JSON 时保留 sensor YAML 中已有的 `calib_timestamp`,不会因为只读构建流程额外刷新时间。
205
+ - JSON 使用 UTF-8、`ensure_ascii=False`、4 空格缩进保存。
206
+
207
+ ## 校验
208
+
209
+ 读取时会校验:
210
+
211
+ - 输入目录存在且包含 JSON。
212
+ - JSON 根节点必须是对象。
213
+ - 每个文件使用 `frame_id` 或文件名 stem 识别主 `sensor_id`。
214
+ - `sensor_id` 不能重复。
215
+ - `pos` 必须是 3 个数字,`roll/pitch/yaw` 必须是数字。
216
+
217
+ 编辑 camera 内参时会做基础类型校验。未知 key 默认拒绝。
@@ -0,0 +1,55 @@
1
+ from .core import (
2
+ CalibrationRecord,
3
+ MDrive4JsonDataset,
4
+ MDrive4JsonWorkspaceManager,
5
+ NumpyCalibrationDataset,
6
+ NumpyExtrinsic,
7
+ NumpyIntrinsic,
8
+ build,
9
+ build_camera_sensor_payload,
10
+ create_camera_sensor,
11
+ create_sensor,
12
+ delete_sensor,
13
+ get_sensor,
14
+ load_dataset,
15
+ load_numpy_calibration,
16
+ list_records,
17
+ list_sensors,
18
+ parse,
19
+ read_order_manifest,
20
+ replace_sensors,
21
+ rpy_degrees_zyx_to_quaternion,
22
+ save_dataset,
23
+ update_sensor,
24
+ update_record,
25
+ validate_workspace,
26
+ )
27
+
28
+ __all__ = [
29
+ "CalibrationRecord",
30
+ "MDrive4JsonDataset",
31
+ "MDrive4JsonWorkspaceManager",
32
+ "NumpyCalibrationDataset",
33
+ "NumpyExtrinsic",
34
+ "NumpyIntrinsic",
35
+ "build",
36
+ "build_camera_sensor_payload",
37
+ "create_camera_sensor",
38
+ "create_sensor",
39
+ "delete_sensor",
40
+ "get_sensor",
41
+ "load_dataset",
42
+ "load_numpy_calibration",
43
+ "list_records",
44
+ "list_sensors",
45
+ "parse",
46
+ "read_order_manifest",
47
+ "replace_sensors",
48
+ "rpy_degrees_zyx_to_quaternion",
49
+ "save_dataset",
50
+ "update_sensor",
51
+ "update_record",
52
+ "validate_workspace",
53
+ ]
54
+
55
+ __version__ = "0.0.4"
@@ -0,0 +1,134 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import sys
6
+ from typing import Any
7
+
8
+ from .core import (
9
+ build as api_build,
10
+ iter_summary_rows,
11
+ load_dataset,
12
+ parse as api_parse,
13
+ update_record,
14
+ validate_workspace as api_validate_workspace,
15
+ )
16
+
17
+
18
+ def main(argv: list[str] | None = None) -> int:
19
+ parser = _build_parser()
20
+ args = parser.parse_args(argv)
21
+ try:
22
+ args.func(args)
23
+ except ValueError as exc:
24
+ parser.exit(2, f"error: {exc}\n")
25
+ return 0
26
+
27
+
28
+ def _build_parser() -> argparse.ArgumentParser:
29
+ parser = argparse.ArgumentParser(
30
+ prog="mdrive4-json",
31
+ description="Read and edit mdrive4 JSON calibration files. Extrinsics are sensor->ego.",
32
+ )
33
+ subparsers = parser.add_subparsers(dest="command", required=True)
34
+
35
+ summary = subparsers.add_parser("summary", help="List sensors and sensor->ego poses")
36
+ summary.add_argument("input_dir")
37
+ summary.set_defaults(func=_cmd_summary)
38
+
39
+ show = subparsers.add_parser("show", help="Print one sensor JSON")
40
+ show.add_argument("input_dir")
41
+ show.add_argument("--sensor", required=True, help="sensor_id, e.g. camera1 or at128p_front")
42
+ show.set_defaults(func=_cmd_show)
43
+
44
+ edit = subparsers.add_parser("edit", help="Edit one sensor JSON")
45
+ edit.add_argument("input_dir")
46
+ edit.add_argument("--sensor", required=True, help="sensor_id, e.g. camera1 or at128p_front")
47
+ edit.add_argument("--set", dest="sets", action="append", required=True, metavar="KEY=VALUE")
48
+ edit.add_argument("--output-dir")
49
+ edit.add_argument("--inplace", action="store_true")
50
+ edit.add_argument("--allow-unknown", action="store_true")
51
+ edit.set_defaults(func=_cmd_edit)
52
+
53
+ parse = subparsers.add_parser("parse", help="Parse JSON directory into a YAML workspace")
54
+ parse.add_argument("input_dir")
55
+ parse.add_argument("-o", "--output", dest="workspace_dir", required=True)
56
+ parse.set_defaults(func=_cmd_parse)
57
+
58
+ build = subparsers.add_parser("build", help="Build JSON directory from a YAML workspace")
59
+ build.add_argument("-i", "--input", dest="workspace_dir", required=True)
60
+ build.add_argument("-o", "--output", dest="output_dir", required=True)
61
+ build.set_defaults(func=_cmd_build)
62
+
63
+ validate = subparsers.add_parser("validate", help="Validate a YAML workspace")
64
+ validate.add_argument("-i", "--input", dest="workspace_dir", required=True)
65
+ validate.set_defaults(func=_cmd_validate)
66
+ return parser
67
+
68
+
69
+ def _cmd_summary(args: argparse.Namespace) -> None:
70
+ dataset = load_dataset(args.input_dir)
71
+ print("sensor_id\tfile\tis_valid\tpos\troll\tpitch\tyaw")
72
+ for sensor_id, file_name, is_valid, pos, roll, pitch, yaw in iter_summary_rows(dataset.records):
73
+ pos_text = ",".join(f"{value:g}" for value in pos)
74
+ print(f"{sensor_id}\t{file_name}\t{is_valid}\t[{pos_text}]\t{roll:g}\t{pitch:g}\t{yaw:g}")
75
+
76
+
77
+ def _cmd_show(args: argparse.Namespace) -> None:
78
+ record = load_dataset(args.input_dir).get(args.sensor)
79
+ print(json.dumps(record.raw, ensure_ascii=False, indent=4))
80
+
81
+
82
+ def _cmd_edit(args: argparse.Namespace) -> None:
83
+ patch = _parse_sets(args.sets)
84
+ update_record(
85
+ args.input_dir,
86
+ args.sensor,
87
+ patch,
88
+ output_dir=args.output_dir,
89
+ inplace=args.inplace,
90
+ allow_unknown=args.allow_unknown,
91
+ )
92
+
93
+
94
+ def _cmd_parse(args: argparse.Namespace) -> None:
95
+ _, report = api_parse(args.input_dir, args.workspace_dir)
96
+ print(json.dumps(report, ensure_ascii=False, indent=2))
97
+
98
+
99
+ def _cmd_build(args: argparse.Namespace) -> None:
100
+ warnings = api_build(args.workspace_dir, args.output_dir)
101
+ if warnings:
102
+ print(json.dumps({"warnings": warnings}, ensure_ascii=False, indent=2))
103
+
104
+
105
+ def _cmd_validate(args: argparse.Namespace) -> None:
106
+ report = api_validate_workspace(args.workspace_dir)
107
+ print(json.dumps(report, ensure_ascii=False, indent=2))
108
+ if not report.get("valid", False):
109
+ raise ValueError(f"workspace is invalid: {report.get('errors')}")
110
+
111
+
112
+ def _parse_sets(items: list[str]) -> dict[str, Any]:
113
+ patch: dict[str, Any] = {}
114
+ for item in items:
115
+ if "=" not in item:
116
+ raise ValueError(f"--set must be KEY=VALUE, got {item!r}")
117
+ key, raw_value = item.split("=", 1)
118
+ key = key.strip()
119
+ if not key:
120
+ raise ValueError(f"--set key is empty in {item!r}")
121
+ patch[key] = _parse_value(raw_value)
122
+ return patch
123
+
124
+
125
+ def _parse_value(value: str) -> Any:
126
+ text = value.strip()
127
+ try:
128
+ return json.loads(text)
129
+ except json.JSONDecodeError:
130
+ return text
131
+
132
+
133
+ if __name__ == "__main__":
134
+ sys.exit(main())