mcap2img 0.1.0__py3-none-any.whl
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.
- mcap2img/__init__.py +3 -0
- mcap2img/__main__.py +4 -0
- mcap2img/cli.py +246 -0
- mcap2img/overlay.py +100 -0
- mcap2img/pb/CameraCalibration_pb2.py +26 -0
- mcap2img/pb/CompressedImage_pb2.py +26 -0
- mcap2img/pb/Pose_pb2.py +28 -0
- mcap2img/pb/Quaternion_pb2.py +26 -0
- mcap2img/pb/RobotInfo_pb2.py +31 -0
- mcap2img/pb/Vector3_pb2.py +26 -0
- mcap2img/pb/__init__.py +1 -0
- mcap2img/pb/proto/CameraCalibration.proto +12 -0
- mcap2img/pb/proto/CompressedImage.proto +7 -0
- mcap2img/pb/proto/Pose.proto +10 -0
- mcap2img/pb/proto/Quaternion.proto +9 -0
- mcap2img/pb/proto/RobotInfo.proto +20 -0
- mcap2img/pb/proto/Vector3.proto +8 -0
- mcap2img/progress.py +41 -0
- mcap2img/projection.py +293 -0
- mcap2img/proto_utils.py +11 -0
- mcap2img/reader.py +227 -0
- mcap2img/timestamp_index.py +47 -0
- mcap2img-0.1.0.dist-info/METADATA +460 -0
- mcap2img-0.1.0.dist-info/RECORD +27 -0
- mcap2img-0.1.0.dist-info/WHEEL +5 -0
- mcap2img-0.1.0.dist-info/entry_points.txt +2 -0
- mcap2img-0.1.0.dist-info/top_level.txt +1 -0
mcap2img/__init__.py
ADDED
mcap2img/__main__.py
ADDED
mcap2img/cli.py
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""CLI entry point for mcap2img."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import logging
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import cv2
|
|
11
|
+
|
|
12
|
+
from mcap2img.overlay import draw_robot_axes_overlay
|
|
13
|
+
from mcap2img.progress import ProgressReporter
|
|
14
|
+
from mcap2img.reader import CAMERA_HOST_ROBOT_ID, McapReader
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _parse_camera_ids(raw: str) -> list[int]:
|
|
20
|
+
ids = [int(x.strip()) for x in raw.split(",") if x.strip()]
|
|
21
|
+
if not ids:
|
|
22
|
+
raise ValueError("At least one camera id is required")
|
|
23
|
+
return ids
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _parse_robot_ids(raw: str) -> list[str]:
|
|
27
|
+
ids = [x.strip() for x in raw.split(",") if x.strip()]
|
|
28
|
+
if not ids:
|
|
29
|
+
raise ValueError("At least one overlay robot id is required")
|
|
30
|
+
return ids
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _resolve_mcap_inputs(input_path: Path) -> tuple[list[Path], bool]:
|
|
34
|
+
"""Return MCAP file list and whether input is a batch directory."""
|
|
35
|
+
if input_path.is_file():
|
|
36
|
+
return [input_path], False
|
|
37
|
+
if input_path.is_dir():
|
|
38
|
+
files = sorted(input_path.glob("*.mcap"))
|
|
39
|
+
return files, True
|
|
40
|
+
return [], False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _output_dir_for_mcap(base_output: Path, mcap_path: Path, *, batch: bool) -> Path:
|
|
44
|
+
if batch:
|
|
45
|
+
return base_output / mcap_path.stem
|
|
46
|
+
return base_output
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def build_arg_parser() -> argparse.ArgumentParser:
|
|
50
|
+
parser = argparse.ArgumentParser(
|
|
51
|
+
description="Export MCAP camera frames to PNG with robot axis overlays.",
|
|
52
|
+
)
|
|
53
|
+
parser.add_argument(
|
|
54
|
+
"--input",
|
|
55
|
+
required=True,
|
|
56
|
+
help="Path to a local .mcap file, or a directory of .mcap files for batch export",
|
|
57
|
+
)
|
|
58
|
+
parser.add_argument("--output", default="./output", help="Output directory")
|
|
59
|
+
parser.add_argument(
|
|
60
|
+
"--cameras",
|
|
61
|
+
default="2",
|
|
62
|
+
help="Comma-separated camera indices (default: 2)",
|
|
63
|
+
)
|
|
64
|
+
parser.add_argument(
|
|
65
|
+
"--overlay-robots",
|
|
66
|
+
default="robot1,robot2",
|
|
67
|
+
help="Comma-separated robot ids for axis overlay (default: robot1,robot2)",
|
|
68
|
+
)
|
|
69
|
+
parser.add_argument(
|
|
70
|
+
"-v",
|
|
71
|
+
"--verbose",
|
|
72
|
+
action="store_true",
|
|
73
|
+
help="Enable debug logging",
|
|
74
|
+
)
|
|
75
|
+
return parser
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def run(
|
|
79
|
+
input_path: Path,
|
|
80
|
+
output_dir: Path,
|
|
81
|
+
camera_ids: list[int],
|
|
82
|
+
overlay_robot_ids: list[str],
|
|
83
|
+
*,
|
|
84
|
+
batch: bool = False,
|
|
85
|
+
) -> int:
|
|
86
|
+
if not input_path.is_file():
|
|
87
|
+
logger.error("Input file not found: %s", input_path)
|
|
88
|
+
return 1
|
|
89
|
+
|
|
90
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
91
|
+
for cam_id in camera_ids:
|
|
92
|
+
(output_dir / f"camera{cam_id}").mkdir(parents=True, exist_ok=True)
|
|
93
|
+
|
|
94
|
+
reader = McapReader(
|
|
95
|
+
mcap_path=input_path,
|
|
96
|
+
camera_ids=camera_ids,
|
|
97
|
+
overlay_robot_ids=overlay_robot_ids,
|
|
98
|
+
)
|
|
99
|
+
logger.info("Scanning %s ...", input_path)
|
|
100
|
+
scan = reader.scan_and_decode()
|
|
101
|
+
|
|
102
|
+
stats = {
|
|
103
|
+
"frames_saved": 0,
|
|
104
|
+
"overlay_drawn": 0,
|
|
105
|
+
"overlay_skipped": 0,
|
|
106
|
+
"missing_calib": 0,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
cam_host_index = scan.robot_pose_index.get(CAMERA_HOST_ROBOT_ID)
|
|
110
|
+
|
|
111
|
+
total_frames = sum(len(scan.camera_frames.get(cam_id, [])) for cam_id in camera_ids)
|
|
112
|
+
export_progress = ProgressReporter("Exporting PNG", total_frames)
|
|
113
|
+
logger.info("Exporting %d frames across %d cameras ...", total_frames, len(camera_ids))
|
|
114
|
+
|
|
115
|
+
for cam_id in camera_ids:
|
|
116
|
+
frames = scan.camera_frames.get(cam_id, [])
|
|
117
|
+
info_index = scan.camera_info_index.get(cam_id)
|
|
118
|
+
|
|
119
|
+
for ts_ns, image in frames:
|
|
120
|
+
out_path = output_dir / f"camera{cam_id}" / f"{ts_ns}.png"
|
|
121
|
+
frame = image.copy()
|
|
122
|
+
|
|
123
|
+
camera_info = info_index.nearest(ts_ns) if info_index else None
|
|
124
|
+
cam_robot_info = cam_host_index.nearest(ts_ns) if cam_host_index else None
|
|
125
|
+
|
|
126
|
+
overlay_robots = []
|
|
127
|
+
for rid in overlay_robot_ids:
|
|
128
|
+
idx = scan.robot_pose_index.get(rid)
|
|
129
|
+
if idx is not None:
|
|
130
|
+
overlay_robots.append((rid, idx.nearest(ts_ns)))
|
|
131
|
+
|
|
132
|
+
if camera_info is None or cam_robot_info is None:
|
|
133
|
+
stats["missing_calib"] += 1
|
|
134
|
+
stats["overlay_skipped"] += 1
|
|
135
|
+
else:
|
|
136
|
+
drew = draw_robot_axes_overlay(
|
|
137
|
+
frame,
|
|
138
|
+
camera_info,
|
|
139
|
+
cam_robot_info,
|
|
140
|
+
overlay_robots,
|
|
141
|
+
)
|
|
142
|
+
if drew:
|
|
143
|
+
stats["overlay_drawn"] += 1
|
|
144
|
+
else:
|
|
145
|
+
stats["overlay_skipped"] += 1
|
|
146
|
+
|
|
147
|
+
if not cv2.imwrite(str(out_path), frame):
|
|
148
|
+
logger.error("Failed to write %s", out_path)
|
|
149
|
+
return 1
|
|
150
|
+
stats["frames_saved"] += 1
|
|
151
|
+
export_progress.advance()
|
|
152
|
+
|
|
153
|
+
export_progress.finish()
|
|
154
|
+
|
|
155
|
+
logger.info(
|
|
156
|
+
"Done: saved=%d overlay_drawn=%d overlay_skipped=%d missing_calib=%d",
|
|
157
|
+
stats["frames_saved"],
|
|
158
|
+
stats["overlay_drawn"],
|
|
159
|
+
stats["overlay_skipped"],
|
|
160
|
+
stats["missing_calib"],
|
|
161
|
+
)
|
|
162
|
+
for cam_id in camera_ids:
|
|
163
|
+
n = len(scan.camera_frames.get(cam_id, []))
|
|
164
|
+
logger.info(" camera%d: %d frames -> %s", cam_id, n, output_dir / f"camera{cam_id}")
|
|
165
|
+
return 0
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def run_batch(
|
|
169
|
+
input_dir: Path,
|
|
170
|
+
output_dir: Path,
|
|
171
|
+
camera_ids: list[int],
|
|
172
|
+
overlay_robot_ids: list[str],
|
|
173
|
+
) -> int:
|
|
174
|
+
mcap_files, batch = _resolve_mcap_inputs(input_dir)
|
|
175
|
+
if not mcap_files:
|
|
176
|
+
logger.error("No .mcap files found in: %s", input_dir)
|
|
177
|
+
return 1
|
|
178
|
+
|
|
179
|
+
logger.info("Batch mode: %d MCAP file(s) in %s", len(mcap_files), input_dir)
|
|
180
|
+
exit_code = 0
|
|
181
|
+
for index, mcap_path in enumerate(mcap_files, start=1):
|
|
182
|
+
logger.info(
|
|
183
|
+
"Processing [%d/%d] %s",
|
|
184
|
+
index,
|
|
185
|
+
len(mcap_files),
|
|
186
|
+
mcap_path.name,
|
|
187
|
+
)
|
|
188
|
+
per_file_output = _output_dir_for_mcap(output_dir, mcap_path, batch=batch)
|
|
189
|
+
code = run(
|
|
190
|
+
input_path=mcap_path,
|
|
191
|
+
output_dir=per_file_output,
|
|
192
|
+
camera_ids=camera_ids,
|
|
193
|
+
overlay_robot_ids=overlay_robot_ids,
|
|
194
|
+
batch=batch,
|
|
195
|
+
)
|
|
196
|
+
if code != 0:
|
|
197
|
+
exit_code = code
|
|
198
|
+
return exit_code
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def main(argv: list[str] | None = None) -> int:
|
|
202
|
+
parser = build_arg_parser()
|
|
203
|
+
args = parser.parse_args(argv)
|
|
204
|
+
|
|
205
|
+
logging.basicConfig(
|
|
206
|
+
level=logging.DEBUG if args.verbose else logging.INFO,
|
|
207
|
+
format="%(levelname)s: %(message)s",
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
try:
|
|
211
|
+
camera_ids = _parse_camera_ids(args.cameras)
|
|
212
|
+
overlay_robot_ids = _parse_robot_ids(args.overlay_robots)
|
|
213
|
+
except ValueError as exc:
|
|
214
|
+
logger.error("%s", exc)
|
|
215
|
+
return 2
|
|
216
|
+
|
|
217
|
+
input_path = Path(args.input)
|
|
218
|
+
mcap_files, batch = _resolve_mcap_inputs(input_path)
|
|
219
|
+
if not mcap_files:
|
|
220
|
+
if input_path.is_file():
|
|
221
|
+
logger.error("Input file not found: %s", input_path)
|
|
222
|
+
elif input_path.is_dir():
|
|
223
|
+
logger.error("No .mcap files found in: %s", input_path)
|
|
224
|
+
else:
|
|
225
|
+
logger.error("Input path not found: %s", input_path)
|
|
226
|
+
return 1
|
|
227
|
+
|
|
228
|
+
if batch:
|
|
229
|
+
return run_batch(
|
|
230
|
+
input_dir=input_path,
|
|
231
|
+
output_dir=Path(args.output),
|
|
232
|
+
camera_ids=camera_ids,
|
|
233
|
+
overlay_robot_ids=overlay_robot_ids,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
return run(
|
|
237
|
+
input_path=mcap_files[0],
|
|
238
|
+
output_dir=Path(args.output),
|
|
239
|
+
camera_ids=camera_ids,
|
|
240
|
+
overlay_robot_ids=overlay_robot_ids,
|
|
241
|
+
batch=False,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
if __name__ == "__main__":
|
|
246
|
+
sys.exit(main())
|
mcap2img/overlay.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Draw projected robot coordinate axes on images."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import cv2
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from mcap2img.projection import (
|
|
11
|
+
AxisSegment,
|
|
12
|
+
build_world_to_camera_optical,
|
|
13
|
+
project_robot_axes,
|
|
14
|
+
resolve_intrinsics_mode,
|
|
15
|
+
robot_info_has_projectable_base_link,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _hex_to_bgr(hex_color: str) -> tuple[int, int, int]:
|
|
20
|
+
hex_color = hex_color.lstrip("#")
|
|
21
|
+
r = int(hex_color[0:2], 16)
|
|
22
|
+
g = int(hex_color[2:4], 16)
|
|
23
|
+
b = int(hex_color[4:6], 16)
|
|
24
|
+
return b, g, r
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
LABEL_FILL_BGR = _hex_to_bgr("#f8f8f8")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def draw_axis_segments(
|
|
31
|
+
image: np.ndarray,
|
|
32
|
+
segments: list[AxisSegment],
|
|
33
|
+
robot_id: str,
|
|
34
|
+
) -> None:
|
|
35
|
+
if not segments:
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
origin_uv = segments[0].origin_uv
|
|
39
|
+
ox, oy = int(round(origin_uv[0])), int(round(origin_uv[1]))
|
|
40
|
+
|
|
41
|
+
for seg in segments:
|
|
42
|
+
tx, ty = int(round(seg.tip_uv[0])), int(round(seg.tip_uv[1]))
|
|
43
|
+
cv2.line(image, (ox, oy), (tx, ty), seg.color_bgr, 4, cv2.LINE_AA)
|
|
44
|
+
cv2.putText(
|
|
45
|
+
image,
|
|
46
|
+
seg.tag.lower(),
|
|
47
|
+
(tx + 4, ty + 14),
|
|
48
|
+
cv2.FONT_HERSHEY_SIMPLEX,
|
|
49
|
+
0.55,
|
|
50
|
+
seg.color_bgr,
|
|
51
|
+
2,
|
|
52
|
+
cv2.LINE_AA,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
cv2.putText(
|
|
56
|
+
image,
|
|
57
|
+
robot_id,
|
|
58
|
+
(ox, oy + 22),
|
|
59
|
+
cv2.FONT_HERSHEY_SIMPLEX,
|
|
60
|
+
0.55,
|
|
61
|
+
LABEL_FILL_BGR,
|
|
62
|
+
2,
|
|
63
|
+
cv2.LINE_AA,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def draw_robot_axes_overlay(
|
|
68
|
+
image: np.ndarray,
|
|
69
|
+
camera_info: Any,
|
|
70
|
+
cam_robot_info: Any,
|
|
71
|
+
overlay_robots: list[tuple[str, Any]],
|
|
72
|
+
) -> bool:
|
|
73
|
+
"""Draw axes for overlay robots. Returns True if any axes were drawn."""
|
|
74
|
+
if camera_info is None or cam_robot_info is None:
|
|
75
|
+
return False
|
|
76
|
+
if resolve_intrinsics_mode(camera_info) is None:
|
|
77
|
+
return False
|
|
78
|
+
if not robot_info_has_projectable_base_link(cam_robot_info):
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
world_to_camera = build_world_to_camera_optical(camera_info, cam_robot_info)
|
|
82
|
+
if world_to_camera is None:
|
|
83
|
+
return False
|
|
84
|
+
|
|
85
|
+
img_h, img_w = image.shape[:2]
|
|
86
|
+
drew_any = False
|
|
87
|
+
for robot_id, robot_info in overlay_robots:
|
|
88
|
+
if not robot_info_has_projectable_base_link(robot_info):
|
|
89
|
+
continue
|
|
90
|
+
segments = project_robot_axes(
|
|
91
|
+
camera_info,
|
|
92
|
+
world_to_camera,
|
|
93
|
+
robot_info,
|
|
94
|
+
img_w,
|
|
95
|
+
img_h,
|
|
96
|
+
)
|
|
97
|
+
if segments:
|
|
98
|
+
draw_axis_segments(image, segments, robot_id)
|
|
99
|
+
drew_any = True
|
|
100
|
+
return drew_any
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
|
3
|
+
# source: CameraCalibration.proto
|
|
4
|
+
# Protobuf Python Version: 4.25.0
|
|
5
|
+
"""Generated protocol buffer code."""
|
|
6
|
+
from google.protobuf import descriptor as _descriptor
|
|
7
|
+
from google.protobuf import descriptor_pool as _descriptor_pool
|
|
8
|
+
from google.protobuf import symbol_database as _symbol_database
|
|
9
|
+
from google.protobuf.internal import builder as _builder
|
|
10
|
+
# @@protoc_insertion_point(imports)
|
|
11
|
+
|
|
12
|
+
_sym_db = _symbol_database.Default()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x17\x43\x61meraCalibration.proto\x12\x08\x66oxglove\"|\n\x11\x43\x61meraCalibration\x12\r\n\x05width\x18\x02 \x01(\x07\x12\x0e\n\x06height\x18\x03 \x01(\x07\x12\x18\n\x10\x64istortion_model\x18\x04 \x01(\t\x12\t\n\x01\x44\x18\x05 \x03(\x01\x12\t\n\x01K\x18\x06 \x03(\x01\x12\t\n\x01P\x18\x08 \x03(\x01\x12\r\n\x05T_b_c\x18\n \x03(\x01\x62\x06proto3')
|
|
18
|
+
|
|
19
|
+
_globals = globals()
|
|
20
|
+
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
|
21
|
+
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'CameraCalibration_pb2', _globals)
|
|
22
|
+
if _descriptor._USE_C_DESCRIPTORS == False:
|
|
23
|
+
DESCRIPTOR._options = None
|
|
24
|
+
_globals['_CAMERACALIBRATION']._serialized_start=37
|
|
25
|
+
_globals['_CAMERACALIBRATION']._serialized_end=161
|
|
26
|
+
# @@protoc_insertion_point(module_scope)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
|
3
|
+
# source: CompressedImage.proto
|
|
4
|
+
# Protobuf Python Version: 4.25.0
|
|
5
|
+
"""Generated protocol buffer code."""
|
|
6
|
+
from google.protobuf import descriptor as _descriptor
|
|
7
|
+
from google.protobuf import descriptor_pool as _descriptor_pool
|
|
8
|
+
from google.protobuf import symbol_database as _symbol_database
|
|
9
|
+
from google.protobuf.internal import builder as _builder
|
|
10
|
+
# @@protoc_insertion_point(imports)
|
|
11
|
+
|
|
12
|
+
_sym_db = _symbol_database.Default()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15\x43ompressedImage.proto\x12\x08\x66oxglove\"/\n\x0f\x43ompressedImage\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\x12\x0e\n\x06\x66ormat\x18\x03 \x01(\tb\x06proto3')
|
|
18
|
+
|
|
19
|
+
_globals = globals()
|
|
20
|
+
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
|
21
|
+
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'CompressedImage_pb2', _globals)
|
|
22
|
+
if _descriptor._USE_C_DESCRIPTORS == False:
|
|
23
|
+
DESCRIPTOR._options = None
|
|
24
|
+
_globals['_COMPRESSEDIMAGE']._serialized_start=35
|
|
25
|
+
_globals['_COMPRESSEDIMAGE']._serialized_end=82
|
|
26
|
+
# @@protoc_insertion_point(module_scope)
|
mcap2img/pb/Pose_pb2.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
|
3
|
+
# source: Pose.proto
|
|
4
|
+
# Protobuf Python Version: 4.25.0
|
|
5
|
+
"""Generated protocol buffer code."""
|
|
6
|
+
from google.protobuf import descriptor as _descriptor
|
|
7
|
+
from google.protobuf import descriptor_pool as _descriptor_pool
|
|
8
|
+
from google.protobuf import symbol_database as _symbol_database
|
|
9
|
+
from google.protobuf.internal import builder as _builder
|
|
10
|
+
# @@protoc_insertion_point(imports)
|
|
11
|
+
|
|
12
|
+
_sym_db = _symbol_database.Default()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
from mcap2img.pb import Quaternion_pb2 as Quaternion__pb2
|
|
16
|
+
from mcap2img.pb import Vector3_pb2 as Vector3__pb2
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\nPose.proto\x12\x08\x66oxglove\x1a\x10Quaternion.proto\x1a\rVector3.proto\"V\n\x04Pose\x12#\n\x08position\x18\x01 \x01(\x0b\x32\x11.foxglove.Vector3\x12)\n\x0borientation\x18\x02 \x01(\x0b\x32\x14.foxglove.Quaternionb\x06proto3')
|
|
20
|
+
|
|
21
|
+
_globals = globals()
|
|
22
|
+
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
|
23
|
+
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'Pose_pb2', _globals)
|
|
24
|
+
if _descriptor._USE_C_DESCRIPTORS == False:
|
|
25
|
+
DESCRIPTOR._options = None
|
|
26
|
+
_globals['_POSE']._serialized_start=57
|
|
27
|
+
_globals['_POSE']._serialized_end=143
|
|
28
|
+
# @@protoc_insertion_point(module_scope)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
|
3
|
+
# source: Quaternion.proto
|
|
4
|
+
# Protobuf Python Version: 4.25.0
|
|
5
|
+
"""Generated protocol buffer code."""
|
|
6
|
+
from google.protobuf import descriptor as _descriptor
|
|
7
|
+
from google.protobuf import descriptor_pool as _descriptor_pool
|
|
8
|
+
from google.protobuf import symbol_database as _symbol_database
|
|
9
|
+
from google.protobuf.internal import builder as _builder
|
|
10
|
+
# @@protoc_insertion_point(imports)
|
|
11
|
+
|
|
12
|
+
_sym_db = _symbol_database.Default()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x10Quaternion.proto\x12\x08\x66oxglove\"8\n\nQuaternion\x12\t\n\x01x\x18\x01 \x01(\x01\x12\t\n\x01y\x18\x02 \x01(\x01\x12\t\n\x01z\x18\x03 \x01(\x01\x12\t\n\x01w\x18\x04 \x01(\x01\x62\x06proto3')
|
|
18
|
+
|
|
19
|
+
_globals = globals()
|
|
20
|
+
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
|
21
|
+
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'Quaternion_pb2', _globals)
|
|
22
|
+
if _descriptor._USE_C_DESCRIPTORS == False:
|
|
23
|
+
DESCRIPTOR._options = None
|
|
24
|
+
_globals['_QUATERNION']._serialized_start=30
|
|
25
|
+
_globals['_QUATERNION']._serialized_end=86
|
|
26
|
+
# @@protoc_insertion_point(module_scope)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
|
3
|
+
# source: RobotInfo.proto
|
|
4
|
+
# Protobuf Python Version: 4.25.0
|
|
5
|
+
"""Generated protocol buffer code."""
|
|
6
|
+
from google.protobuf import descriptor as _descriptor
|
|
7
|
+
from google.protobuf import descriptor_pool as _descriptor_pool
|
|
8
|
+
from google.protobuf import symbol_database as _symbol_database
|
|
9
|
+
from google.protobuf.internal import builder as _builder
|
|
10
|
+
# @@protoc_insertion_point(imports)
|
|
11
|
+
|
|
12
|
+
_sym_db = _symbol_database.Default()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
from mcap2img.pb import Pose_pb2 as Pose__pb2
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0fRobotInfo.proto\x12\x08\x66oxglove\x1a\nPose.proto\"@\n\rRobotLinkInfo\x12\x0c\n\x04name\x18\x01 \x01(\t\x12!\n\ttransform\x18\x02 \x01(\x0b\x32\x0e.foxglove.Pose\"f\n\tRobotInfo\x12*\n\tbase_link\x18\x08 \x01(\x0b\x32\x17.foxglove.RobotLinkInfo\x12-\n\rbaselink_type\x18\t \x01(\x0e\x32\x16.foxglove.BaselinkType*\\\n\x0c\x42\x61selinkType\x12\x1d\n\x19\x42\x41SELINK_TYPE_UNSPECIFIED\x10\x00\x12\x18\n\x14USE_BASELINK_AS_POSE\x10\x01\x12\x13\n\x0fIGNORE_BASELINK\x10\x02\x62\x06proto3')
|
|
19
|
+
|
|
20
|
+
_globals = globals()
|
|
21
|
+
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
|
22
|
+
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'RobotInfo_pb2', _globals)
|
|
23
|
+
if _descriptor._USE_C_DESCRIPTORS == False:
|
|
24
|
+
DESCRIPTOR._options = None
|
|
25
|
+
_globals['_BASELINKTYPE']._serialized_start=211
|
|
26
|
+
_globals['_BASELINKTYPE']._serialized_end=303
|
|
27
|
+
_globals['_ROBOTLINKINFO']._serialized_start=41
|
|
28
|
+
_globals['_ROBOTLINKINFO']._serialized_end=105
|
|
29
|
+
_globals['_ROBOTINFO']._serialized_start=107
|
|
30
|
+
_globals['_ROBOTINFO']._serialized_end=209
|
|
31
|
+
# @@protoc_insertion_point(module_scope)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
|
3
|
+
# source: Vector3.proto
|
|
4
|
+
# Protobuf Python Version: 4.25.0
|
|
5
|
+
"""Generated protocol buffer code."""
|
|
6
|
+
from google.protobuf import descriptor as _descriptor
|
|
7
|
+
from google.protobuf import descriptor_pool as _descriptor_pool
|
|
8
|
+
from google.protobuf import symbol_database as _symbol_database
|
|
9
|
+
from google.protobuf.internal import builder as _builder
|
|
10
|
+
# @@protoc_insertion_point(imports)
|
|
11
|
+
|
|
12
|
+
_sym_db = _symbol_database.Default()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\rVector3.proto\x12\x08\x66oxglove\"*\n\x07Vector3\x12\t\n\x01x\x18\x01 \x01(\x01\x12\t\n\x01y\x18\x02 \x01(\x01\x12\t\n\x01z\x18\x03 \x01(\x01\x62\x06proto3')
|
|
18
|
+
|
|
19
|
+
_globals = globals()
|
|
20
|
+
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
|
21
|
+
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'Vector3_pb2', _globals)
|
|
22
|
+
if _descriptor._USE_C_DESCRIPTORS == False:
|
|
23
|
+
DESCRIPTOR._options = None
|
|
24
|
+
_globals['_VECTOR3']._serialized_start=27
|
|
25
|
+
_globals['_VECTOR3']._serialized_end=69
|
|
26
|
+
# @@protoc_insertion_point(module_scope)
|
mcap2img/pb/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Vendored protobuf message stubs for MCAP decoding."""
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
syntax = "proto3";
|
|
2
|
+
package foxglove;
|
|
3
|
+
|
|
4
|
+
import "Pose.proto";
|
|
5
|
+
|
|
6
|
+
message RobotLinkInfo {
|
|
7
|
+
string name = 1;
|
|
8
|
+
Pose transform = 2;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
enum BaselinkType {
|
|
12
|
+
BASELINK_TYPE_UNSPECIFIED = 0;
|
|
13
|
+
USE_BASELINK_AS_POSE = 1;
|
|
14
|
+
IGNORE_BASELINK = 2;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
message RobotInfo {
|
|
18
|
+
RobotLinkInfo base_link = 8;
|
|
19
|
+
BaselinkType baselink_type = 9;
|
|
20
|
+
}
|
mcap2img/progress.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Throttled progress logging for long-running batch jobs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ProgressReporter:
|
|
11
|
+
"""Log progress at most once per integer percent (or on completion)."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, label: str, total: int | None = None) -> None:
|
|
14
|
+
self.label = label
|
|
15
|
+
self.total = int(total) if total is not None else None
|
|
16
|
+
self.done = 0
|
|
17
|
+
self._last_pct = -1
|
|
18
|
+
|
|
19
|
+
def advance(self, step: int = 1, *, force: bool = False) -> None:
|
|
20
|
+
self.done += step
|
|
21
|
+
if self.total and self.total > 0:
|
|
22
|
+
pct = min(100, int(self.done * 100 / self.total))
|
|
23
|
+
if force or pct != self._last_pct or self.done >= self.total:
|
|
24
|
+
self._last_pct = pct
|
|
25
|
+
logger.info(
|
|
26
|
+
"%s: %d/%d (%d%%)",
|
|
27
|
+
self.label,
|
|
28
|
+
self.done,
|
|
29
|
+
self.total,
|
|
30
|
+
pct,
|
|
31
|
+
)
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
if force or self.done == 1 or self.done % 500 == 0:
|
|
35
|
+
logger.info("%s: %d ...", self.label, self.done)
|
|
36
|
+
|
|
37
|
+
def finish(self) -> None:
|
|
38
|
+
if self.total and self.total > 0:
|
|
39
|
+
self.advance(max(0, self.total - self.done), force=True)
|
|
40
|
+
else:
|
|
41
|
+
self.advance(0, force=True)
|