feat(mcap): add copy mode for multi-camera exports

Add an opt-in multi-camera copy layout for zed_svo_to_mcap.

copy mode preserves each camera's original timestamps and cadence,
writes namespaced /zedN topics without /bundle, and supports
common-overlap or full-range export windows.

Update the batch wrapper, Python validator, RGBD viewer, and
documentation so copy-layout MCAP files are treated as a first-class
format rather than invalid bundled output.

Validation:
- cmake --build build --target zed_svo_to_mcap -j4
- uv run python -m py_compile scripts/zed_batch_svo_to_mcap.py scripts/mcap_bundle_validator.py scripts/mcap_rgbd_viewer.py
- build/bin/zed_svo_to_mcap --segment-dir /workspaces/data/kindergarten/bar/2026-03-18T11-59-41 --output /tmp/bar_11-59-41_copy_common.mcap --encoder-device nvidia --depth-mode neural_plus --depth-size optimal --bundle-policy copy --copy-range common
- uv run python scripts/mcap_bundle_validator.py /tmp/bar_11-59-41_copy_common.mcap
- uv run --extra viewer python scripts/mcap_rgbd_viewer.py /tmp/bar_11-59-41_copy_common.mcap --summary-only
This commit is contained in:
2026-03-24 10:13:09 +00:00
parent 80d90887ed
commit 8597976678
8 changed files with 1831 additions and 200 deletions
+9 -3
View File
@@ -112,7 +112,7 @@ def record_single_camera_topic(
def probe_single_camera_output(path: Path) -> batch.OutputProbeResult:
base_probe = batch.probe_output(path, ("camera",), bundle_topic=None)
base_probe = batch.probe_output(path, ("camera",), layout="single-camera", bundle_topic=None)
if base_probe.status != "valid":
return base_probe
@@ -210,6 +210,7 @@ def summarize_mcap(path: Path) -> McapSummary:
camera_labels: set[str] = set()
saw_single_camera_topic = False
saw_namespaced_camera_topic = False
saw_bundle_manifest = False
with path.open("rb") as stream:
reader = reader_module.make_reader(stream)
@@ -218,6 +219,7 @@ def summarize_mcap(path: Path) -> McapSummary:
schema_name = schema.name if schema is not None else None
if topic == BUNDLE_TOPIC:
summary.layout = "bundled"
saw_bundle_manifest = True
if schema is None or schema.name != "cvmmap_streamer.BundleManifest":
summary.validation_status = "invalid"
summary.validation_reason = f"bundle topic '{BUNDLE_TOPIC}' is missing the BundleManifest schema"
@@ -257,7 +259,7 @@ def summarize_mcap(path: Path) -> McapSummary:
continue
saw_namespaced_camera_topic = True
if summary.layout == "unknown":
summary.layout = "bundled"
summary.layout = "copy"
camera_labels.add(label)
stats = summary.camera_stats.setdefault(label, CameraSummary())
if stream_kind == "video":
@@ -276,9 +278,12 @@ def summarize_mcap(path: Path) -> McapSummary:
if saw_single_camera_topic and saw_namespaced_camera_topic:
summary.layout = "mixed"
summary.validation_status = "invalid"
summary.validation_reason = "MCAP mixes single-camera and bundled topic layouts"
summary.validation_reason = "MCAP mixes single-camera and multi-camera topic layouts"
return summary
if saw_namespaced_camera_topic and not saw_bundle_manifest and summary.layout == "bundled":
summary.layout = "copy"
if summary.layout == "single-camera":
summary.camera_labels = ("camera",)
probe = probe_single_camera_output(path)
@@ -294,6 +299,7 @@ def summarize_mcap(path: Path) -> McapSummary:
probe = batch.probe_output(
path,
summary.camera_labels,
layout=summary.layout,
bundle_topic=BUNDLE_TOPIC if summary.layout == "bundled" else None,
)
summary.validation_status = probe.status