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 ADDED
@@ -0,0 +1,3 @@
1
+ """Convert MCAP camera frames to PNG with robot coordinate axis overlays."""
2
+
3
+ __version__ = "0.1.0"
mcap2img/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from mcap2img.cli import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
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)
@@ -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)
@@ -0,0 +1 @@
1
+ """Vendored protobuf message stubs for MCAP decoding."""
@@ -0,0 +1,12 @@
1
+ syntax = "proto3";
2
+ package foxglove;
3
+
4
+ message CameraCalibration {
5
+ fixed32 width = 2;
6
+ fixed32 height = 3;
7
+ string distortion_model = 4;
8
+ repeated double D = 5;
9
+ repeated double K = 6;
10
+ repeated double P = 8;
11
+ repeated double T_b_c = 10;
12
+ }
@@ -0,0 +1,7 @@
1
+ syntax = "proto3";
2
+ package foxglove;
3
+
4
+ message CompressedImage {
5
+ bytes data = 2;
6
+ string format = 3;
7
+ }
@@ -0,0 +1,10 @@
1
+ syntax = "proto3";
2
+ package foxglove;
3
+
4
+ import "Quaternion.proto";
5
+ import "Vector3.proto";
6
+
7
+ message Pose {
8
+ Vector3 position = 1;
9
+ Quaternion orientation = 2;
10
+ }
@@ -0,0 +1,9 @@
1
+ syntax = "proto3";
2
+ package foxglove;
3
+
4
+ message Quaternion {
5
+ double x = 1;
6
+ double y = 2;
7
+ double z = 3;
8
+ double w = 4;
9
+ }
@@ -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
+ }
@@ -0,0 +1,8 @@
1
+ syntax = "proto3";
2
+ package foxglove;
3
+
4
+ message Vector3 {
5
+ double x = 1;
6
+ double y = 2;
7
+ double z = 3;
8
+ }
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)