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.
- mdrive4_json-0.0.4/MANIFEST.in +1 -0
- mdrive4_json-0.0.4/PKG-INFO +231 -0
- mdrive4_json-0.0.4/README.md +217 -0
- mdrive4_json-0.0.4/mdrive4_json/__init__.py +55 -0
- mdrive4_json-0.0.4/mdrive4_json/cli.py +134 -0
- mdrive4_json-0.0.4/mdrive4_json/core.py +1177 -0
- mdrive4_json-0.0.4/mdrive4_json.egg-info/PKG-INFO +231 -0
- mdrive4_json-0.0.4/mdrive4_json.egg-info/SOURCES.txt +13 -0
- mdrive4_json-0.0.4/mdrive4_json.egg-info/dependency_links.txt +1 -0
- mdrive4_json-0.0.4/mdrive4_json.egg-info/entry_points.txt +2 -0
- mdrive4_json-0.0.4/mdrive4_json.egg-info/requires.txt +7 -0
- mdrive4_json-0.0.4/mdrive4_json.egg-info/top_level.txt +3 -0
- mdrive4_json-0.0.4/pyproject.toml +35 -0
- mdrive4_json-0.0.4/setup.cfg +4 -0
- mdrive4_json-0.0.4/tests/test_mdrive4_json.py +770 -0
|
@@ -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())
|