From d0f3dc5cf1e488f3bc3936fea48a4773de12c136 Mon Sep 17 00:00:00 2001 From: crosstyan Date: Tue, 24 Mar 2026 10:55:01 +0000 Subject: [PATCH] docs(mcap): split legacy single-camera layout reference Move the legacy /camera/* contract into its own reference page so the main MCAP layout doc can stay focused on current bundled and copy-layout behavior. Document the compatibility model explicitly: legacy single-camera is operationally equivalent to one-camera copy when the effective camera label is treated as the literal `camera`. Update the mcap_rgbd_example helper and recipe docs to accept legacy /camera/* inputs under that compatibility rule instead of rejecting them. --- README.md | 4 +- docs/mcap_layout.md | 34 +++----------- docs/mcap_legacy_single_camera_layout.md | 60 ++++++++++++++++++++++++ docs/mcap_recipes.md | 33 ++++++++----- scripts/mcap_rgbd_example.py | 14 +++--- 5 files changed, 94 insertions(+), 51 deletions(-) create mode 100644 docs/mcap_legacy_single_camera_layout.md diff --git a/README.md b/README.md index 42e9947..3d76057 100644 --- a/README.md +++ b/README.md @@ -271,9 +271,9 @@ When stderr is attached to a TTY, `zed_batch_svo_to_mcap.py` uses a `progress-ta Bundled MCAP export now defaults to `--bundle-policy nearest`. That mode emits one `/bundle` manifest message per bundle timestamp on the common timeline and keeps the original per-camera timestamps on `/zedN/video`, `/zedN/depth`, and optional `/zedN/pose`. Faster cameras are sampled onto the slowest common timeline there, so they can end up with the same message count as slower cameras. Consumers that care about grouping should follow `/bundle` instead of inferring bundle membership from identical message timestamps. Use `--bundle-policy strict` when you want thresholded grouping; `--sync-tolerance-ms` only applies in that strict mode. Use `--bundle-policy copy` when you want one MCAP containing all camera namespaces with their original per-camera cadence and no `/bundle` manifest. `copy` disables `--start-frame`, `--end-frame`, and `--sync-tolerance-ms`; `--copy-range common|full` controls whether it trims to the overlap window or preserves each camera’s full timestamp range. -Single-source `zed_svo_to_mcap` now writes the one-camera `copy` shape by default, so `foo_zed4.svo2` exports namespaced topics like `/zed4/video` and `/zed4/depth` with no `/bundle`. See [docs/mcap_layout.md](./docs/mcap_layout.md) for the full bundled, copy, and legacy `/camera/*` MCAP topic contract. +Single-source `zed_svo_to_mcap` now writes the one-camera `copy` shape by default, so `foo_zed4.svo2` exports namespaced topics like `/zed4/video` and `/zed4/depth` with no `/bundle`. See [docs/mcap_layout.md](./docs/mcap_layout.md) for the current bundled/copy contract and [docs/mcap_legacy_single_camera_layout.md](./docs/mcap_legacy_single_camera_layout.md) for the separate legacy `/camera/*` reference. -For the simple non-GUI path, use `scripts/mcap_rgbd_example.py` and [docs/mcap_recipes.md](./docs/mcap_recipes.md). That helper is for current `bundled` and `copy` MCAPs only; old legacy `/camera/*` files should be regenerated into `copy` layout instead of being supported there. +For the simple non-GUI path, use `scripts/mcap_rgbd_example.py` and [docs/mcap_recipes.md](./docs/mcap_recipes.md). That helper supports current `bundled` and `copy` MCAPs, and it also accepts the legacy `/camera/*` shape by treating it as a single-camera stream with the literal label `camera`. ### MCAP RGBD Viewer diff --git a/docs/mcap_layout.md b/docs/mcap_layout.md index 0639787..6636d59 100644 --- a/docs/mcap_layout.md +++ b/docs/mcap_layout.md @@ -1,30 +1,15 @@ # MCAP Layout -`cvmmap-streamer` currently deals with three active MCAP layouts plus one legacy single-camera layout: +`cvmmap-streamer` currently deals with three active MCAP layout shapes: -- legacy single-camera `/camera/*` MCAP export - bundled multi-camera timeline export - multi-camera copy export - single-source `zed_svo_to_mcap` output, which now uses the one-camera `copy` shape -This document covers the topic layout, schema types, and timestamp semantics for both. +This document covers the current bundled and copy contracts. -## Legacy Single-Camera Layout - -Default topics: - -| Topic | Schema / Encoding | Notes | -|------|------|------| -| `/camera/video` | `foxglove.CompressedVideo` | H.264 or H.265 Annex B access units | -| `/camera/depth` | `cvmmap_streamer.DepthMap` | RVL-compressed depth payload | -| `/camera/calibration` | `foxglove.CameraCalibration` | Video intrinsics | -| `/camera/depth_calibration` | `foxglove.CameraCalibration` | Written only when depth resolution differs from video | -| `/camera/pose` | `foxglove.PoseInFrame` | Optional; only when pose export is enabled and tracking is valid | -| `/camera/body` | raw `cvmmap.body_tracking.v1` | Optional raw body-tracking packets; see [mcap_body_tracking.md](./mcap_body_tracking.md) | - -Single-camera export preserves the original per-frame source timestamp on video, depth, and pose messages. - -This layout is still used by the generic sink/testers in the repository, but it is no longer the default shape written by `zed_svo_to_mcap` for a single `.svo` or `.svo2` input. +Legacy `/camera/*` is documented separately in [mcap_legacy_single_camera_layout.md](./mcap_legacy_single_camera_layout.md). +Conceptually, it is compatible with one-camera `copy` if you treat the camera label as the literal `camera` instead of `zedN` or another derived label. ## Bundled Multi-Camera Layout @@ -135,15 +120,6 @@ Bundled `strict` export stays strict: ## Validation Expectations -For single-camera MCAP files, the current validation contract is: - -- `/camera/video` must exist and contain at least one message -- `/camera/depth` must exist and contain at least one message -- `/camera/calibration` must exist exactly once -- `/camera/video` and `/camera/depth` message counts must match -- `/camera/depth_calibration` may appear zero or one time -- `/camera/pose` is optional, but it may not outnumber `/camera/video` - For bundled MCAP files, the current validation contract is: - `/bundle` must exist @@ -159,4 +135,6 @@ For multi-camera `copy` MCAP files, the current validation contract is: - each camera must have `/zedN/video`, `/zedN/depth`, and `/zedN/calibration` - for each camera, `/zedN/video` and `/zedN/depth` message counts must match +Legacy `/camera/*` validation expectations are documented in [mcap_legacy_single_camera_layout.md](./mcap_legacy_single_camera_layout.md). + The repository-level Python helper [scripts/mcap_bundle_validator.py](../scripts/mcap_bundle_validator.py) understands bundled, copy, and legacy `/camera/*` layouts and reports which one it found before applying the corresponding validation rules. diff --git a/docs/mcap_legacy_single_camera_layout.md b/docs/mcap_legacy_single_camera_layout.md new file mode 100644 index 0000000..c5cb436 --- /dev/null +++ b/docs/mcap_legacy_single_camera_layout.md @@ -0,0 +1,60 @@ +# Legacy Single-Camera MCAP Layout + +This page is the reference for the older `/camera/*` MCAP wire shape. + +It is still used by some generic sink/tester paths in the repository, but it is no longer the default shape written by `zed_svo_to_mcap` for a single `.svo` or `.svo2` input. + +## Compatibility With Copy Layout + +In practice, this layout is very close to the one-camera `copy` shape: + +- legacy single-camera uses `/camera/video`, `/camera/depth`, and related `/camera/*` topics +- one-camera `copy` uses `/{label}/video`, `/{label}/depth`, and related `/{label}/*` topics + +So the compatibility mental model is: + +- treat the camera label as the literal `camera` +- replace `/{label}/...` with `/camera/...` +- there is still no `/bundle` +- timestamps remain per-camera sample timestamps + +That is why legacy single-camera is conceptually compatible with `copy`, even though the topic paths are not namespaced the same way. + +## Topics + +| Topic | Schema / Encoding | Notes | +|------|------|------| +| `/camera/video` | `foxglove.CompressedVideo` | H.264 or H.265 Annex B access units | +| `/camera/depth` | `cvmmap_streamer.DepthMap` | RVL-compressed depth payload | +| `/camera/calibration` | `foxglove.CameraCalibration` | Video intrinsics | +| `/camera/depth_calibration` | `foxglove.CameraCalibration` | Written only when depth resolution differs from video | +| `/camera/pose` | `foxglove.PoseInFrame` | Optional; only when pose export is enabled and tracking is valid | +| `/camera/body` | raw `cvmmap.body_tracking.v1` | Optional raw body-tracking packets; see [mcap_body_tracking.md](./mcap_body_tracking.md) | + +## Timestamp Semantics + +Legacy single-camera export preserves the original per-frame source timestamp on video, depth, and pose messages. + +Like `copy`, there is no separate bundle time: + +- `/camera/video`, `/camera/depth`, and `/camera/pose` use the original camera sample timestamp +- calibration and depth-calibration messages use the timestamp of the first emitted sample + +## Corruption Behavior + +Like `copy`, this layout has no manifest or placeholder contract: + +- unreadable tail frames are treated as end-of-stream +- mid-stream corruption is skipped until a readable frame is found +- the topic stream simply resumes at the recovered readable frame + +## Validation Expectations + +The current validation contract is: + +- `/camera/video` must exist and contain at least one message +- `/camera/depth` must exist and contain at least one message +- `/camera/calibration` must exist exactly once +- `/camera/video` and `/camera/depth` message counts must match +- `/camera/depth_calibration` may appear zero or one time +- `/camera/pose` is optional, but it may not outnumber `/camera/video` diff --git a/docs/mcap_recipes.md b/docs/mcap_recipes.md index f4b0abd..0eb4cff 100644 --- a/docs/mcap_recipes.md +++ b/docs/mcap_recipes.md @@ -4,12 +4,12 @@ This guide is the simple, non-GUI path for inspecting RGB+depth MCAP files. Use it when you want to: -- confirm whether an MCAP is bundled or `copy` +- confirm whether an MCAP is bundled, `copy`, or legacy `/camera/*` - inspect camera labels, message counts, and timestamp ranges - export one RGB frame and one decoded depth sample as a concrete example - understand how `/bundle` changes the meaning of timestamps and sample grouping -For the full layout contract, see [mcap_layout.md](./mcap_layout.md). +For the current bundled/copy layout contract, see [mcap_layout.md](./mcap_layout.md). The older `/camera/*` wire shape is documented separately in [mcap_legacy_single_camera_layout.md](./mcap_legacy_single_camera_layout.md). ## Quick Summary @@ -41,14 +41,19 @@ uv sync --extra viewer ## The Practical Cases -For this helper, there are really two supported layouts: +For this helper, there are really two operational cases: - `bundled`: multiple namespaced camera topics plus `/bundle` -- `copy`: namespaced camera topics with no `/bundle` +- single-camera stream with no `/bundle` -Current single-source `zed_svo_to_mcap` output uses the one-camera `copy` shape by default, so even a one-camera file still looks like namespaced `/{label}/*` topics with no `/bundle`. +That second case can appear in two wire shapes: -Legacy `/camera/*` MCAP files are intentionally out of scope for this helper. Regenerate them into `copy` layout if you still have old files around. +- `copy`: namespaced topics such as `/zed4/video` +- legacy single-camera: `/camera/video` + +Current single-source `zed_svo_to_mcap` output uses the one-camera `copy` shape by default, so even a one-camera file usually looks like namespaced `/{label}/*` topics with no `/bundle`. + +The helper treats legacy `/camera/*` as compatible with `copy` by using the implicit camera label `camera`. ## Recipe: Summarize One MCAP @@ -70,7 +75,7 @@ What the summary prints: This is the fastest way to answer: -- “is this file bundled or copy?” +- “is this file bundled, copy, or legacy single-camera?” - “which camera labels are inside?” - “do video and depth counts match?” - “what timestamp range does each camera cover?” @@ -83,7 +88,7 @@ uv run python scripts/mcap_rgbd_example.py export-sample \ --output-dir /tmp/mcap_sample ``` -For multi-camera or one-camera `copy` files, choose the camera explicitly when needed: +For multi-camera or namespaced one-camera files, choose the camera explicitly when needed: ```bash uv run python scripts/mcap_rgbd_example.py export-sample \ @@ -104,25 +109,26 @@ Outputs: That means: -- `copy`: sample `N` is `/zedN/video[N]` + `/zedN/depth[N]` +- legacy `/camera/*`: sample `N` is `/camera/video[N]` + `/camera/depth[N]` +- `copy`: sample `N` is `/{label}/video[N]` + `/{label}/depth[N]` - `bundled`: sample `N` is the `N`th present sample for that camera, not bundle index `N` In bundled files, `sample_metadata.json` also records the matched `/bundle` member metadata for the selected camera sample. -## Recipe: Understand Bundled vs Copy Timing +## Recipe: Understand Bundled vs Non-Bundled Timing Bundled files intentionally separate bundle time from camera sample time: - `/bundle.timestamp` is the nominal common-timeline bundle timestamp - `/zedN/video` and `/zedN/depth` keep the original per-camera sample timestamps -Copy files do not have bundle time at all: +Copy and legacy single-camera files do not have bundle time at all: - there is no `/bundle` - each camera topic keeps its own original cadence and timestamps If you care about grouping, use `/bundle` in bundled files. -For `copy`, treat each camera stream independently. +For `copy` and legacy single-camera files, treat each camera stream independently. ## Recipe: Inspect `/bundle` In Python @@ -169,4 +175,5 @@ with path.open("rb") as stream: This is the important mental model: - `bundled`: follow `/bundle` for grouping -- `copy`: treat each camera as an independent stream +- `copy`: treat each namespaced camera as an independent stream +- legacy `/camera/*`: same model as one-camera `copy`, with the implicit label `camera` diff --git a/scripts/mcap_rgbd_example.py b/scripts/mcap_rgbd_example.py index fc6fc7b..a02f342 100644 --- a/scripts/mcap_rgbd_example.py +++ b/scripts/mcap_rgbd_example.py @@ -149,6 +149,8 @@ def is_present_status(status_name: str) -> bool: def topic_for(layout: str, camera_label: str, kind: str) -> str: + if layout == "single-camera": + return f"/camera/{kind}" if layout not in {"copy", "bundled"}: raise click.ClickException(f"unsupported layout '{layout}'") return f"/{camera_label}/{kind}" @@ -164,11 +166,7 @@ def selected_camera_label(base_summary: bundle_validator.McapSummary, camera_lab def ensure_supported_layout(base_summary: bundle_validator.McapSummary) -> None: - if base_summary.layout == "single-camera": - raise click.ClickException( - "legacy /camera/* MCAP files are not supported by this helper; regenerate them into copy-layout MCAPs" - ) - if base_summary.layout not in {"copy", "bundled"}: + if base_summary.layout not in {"single-camera", "copy", "bundled"}: reason = base_summary.validation_reason or "unsupported MCAP layout" raise click.ClickException(reason) @@ -544,7 +542,7 @@ def write_sample_outputs( @click.group() def main() -> None: - """Small MCAP RGBD example helper for bundled and copy-layout MCAP files.""" + """Small MCAP RGBD example helper for bundled, copy, and legacy single-camera MCAP files.""" @main.command("summary") @@ -558,7 +556,7 @@ def summary_command(mcap_path: Path) -> None: @main.command("export-sample") @click.argument("mcap_path", type=click.Path(path_type=Path, exists=True)) -@click.option("--camera-label", help="Camera label to export. Defaults to the first sorted namespaced label.") +@click.option("--camera-label", help="Camera label to export. Defaults to `camera` for legacy files or the first sorted namespaced label.") @click.option("--sample-index", default=0, show_default=True, type=click.IntRange(min=0), help="Zero-based per-camera RGB+depth sample index.") @click.option("--output-dir", required=True, type=click.Path(path_type=Path), help="Directory to write rgb.png, depth.npy, depth_preview.png, and sample_metadata.json.") @click.option("--ffmpeg-bin", default="ffmpeg", show_default=True, help="ffmpeg binary used to decode the selected RGB frame.") @@ -581,7 +579,7 @@ def export_sample_command( depth_max_m: float, depth_palette: str, ) -> None: - """Export one per-camera RGB/depth sample from a bundled or copy-layout MCAP file.""" + """Export one per-camera RGB/depth sample from a bundled, copy, or legacy single-camera MCAP file.""" summary = summarize_mcap(mcap_path.resolve()) ensure_supported_layout(summary.base) if summary.base.validation_status != "valid":