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
+43 -3
View File
@@ -1,9 +1,10 @@
# MCAP Layout
`cvmmap-streamer` writes two related MCAP layouts:
`cvmmap-streamer` writes three related MCAP layouts:
- single-camera MCAP export
- bundled multi-camera MCAP export
- bundled multi-camera timeline export
- multi-camera copy export
This document covers the topic layout, schema types, and timestamp semantics for both.
@@ -40,6 +41,30 @@ Bundled export namespaces each camera stream and adds one bundle manifest topic:
`strict` is still available. In strict mode, bundle membership is thresholded by timestamp skew and `--sync-tolerance-ms` applies. In `nearest` mode, `--sync-tolerance-ms` is not used.
Because `nearest` emits on the slowest common timeline, faster cameras can legitimately end up with the same message count as slower cameras in bundled output.
## Multi-Camera Copy Layout
`copy` export also namespaces each camera stream under `/zedN/...`, but it does not emit `/bundle`.
| Topic | Schema / Encoding | Notes |
|------|------|------|
| `/zedN/video` | `foxglove.CompressedVideo` | Per-camera encoded video |
| `/zedN/depth` | `cvmmap_streamer.DepthMap` | Per-camera depth |
| `/zedN/calibration` | `foxglove.CameraCalibration` | Per-camera video intrinsics |
| `/zedN/depth_calibration` | `foxglove.CameraCalibration` | Written only when exported depth resolution differs from video |
| `/zedN/pose` | `foxglove.PoseInFrame` | Optional per-camera pose |
| `/zedN/body` | raw `cvmmap.body_tracking.v1` | Optional raw body packets; see [mcap_body_tracking.md](./mcap_body_tracking.md) |
`copy` preserves each camera topic exactly as an independent stream inside one MCAP:
- no `/bundle` topic
- no shared bundle index
- no resampling to a common timeline
- original per-camera timestamps and cadence are preserved
`--copy-range common` trims each camera to the common overlap window without resampling. `--copy-range full` preserves each cameras full readable timestamp range from the grouped segment.
## Bundle Manifest Contract
`/bundle` is the authoritative grouping contract for bundled MCAP files. Consumers should not infer grouping from identical MCAP `logTime` values or from matching per-camera timestamps.
@@ -76,6 +101,8 @@ The bundled MCAP contract intentionally separates bundle time from per-camera sa
This means a single bundle can legitimately contain different per-camera timestamps, especially in `nearest` mode.
`copy` has no separate bundle time. Its `/zedN/video`, `/zedN/depth`, and `/zedN/pose` messages all use the original per-camera sample timestamp directly.
## Corruption And Partial Bundles
Bundled `nearest` export is resilient to ZED `CORRUPTED FRAME` runs:
@@ -91,6 +118,13 @@ Bundled `strict` export stays strict:
- corruption is skipped internally until recovery
- only fully present, threshold-qualified groups are emitted
`copy` is also resilient to ZED `CORRUPTED FRAME` runs:
- unreadable tail frames are treated as end-of-stream
- mid-stream corruption is skipped until a readable frame is found
- there is no placeholder or manifest entry because `copy` has no grouping contract
- the affected camera topic simply resumes at the recovered readable frame
## Validation Expectations
For single-camera MCAP files, the current validation contract is:
@@ -111,4 +145,10 @@ For bundled MCAP files, the current validation contract is:
That is why a partially written MCAP with topic presence but mismatched counts is treated as invalid.
The repository-level Python helper [scripts/mcap_bundle_validator.py](../scripts/mcap_bundle_validator.py) now understands both layouts and reports which one it found before applying the corresponding validation rules.
For multi-camera `copy` MCAP files, the current validation contract is:
- `/bundle` must not exist
- each camera must have `/zedN/video`, `/zedN/depth`, and `/zedN/calibration`
- for each camera, `/zedN/video` and `/zedN/depth` message counts must match
The repository-level Python helper [scripts/mcap_bundle_validator.py](../scripts/mcap_bundle_validator.py) now understands all three layouts and reports which one it found before applying the corresponding validation rules.