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.
This commit is contained in:
@@ -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.
|
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.
|
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
|
### MCAP RGBD Viewer
|
||||||
|
|
||||||
|
|||||||
+6
-28
@@ -1,30 +1,15 @@
|
|||||||
# MCAP Layout
|
# 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
|
- bundled multi-camera timeline export
|
||||||
- multi-camera copy export
|
- multi-camera copy export
|
||||||
- single-source `zed_svo_to_mcap` output, which now uses the one-camera `copy` shape
|
- 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
|
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.
|
||||||
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.
|
|
||||||
|
|
||||||
## Bundled Multi-Camera Layout
|
## Bundled Multi-Camera Layout
|
||||||
|
|
||||||
@@ -135,15 +120,6 @@ Bundled `strict` export stays strict:
|
|||||||
|
|
||||||
## Validation Expectations
|
## 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:
|
For bundled MCAP files, the current validation contract is:
|
||||||
|
|
||||||
- `/bundle` must exist
|
- `/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`
|
- each camera must have `/zedN/video`, `/zedN/depth`, and `/zedN/calibration`
|
||||||
- for each camera, `/zedN/video` and `/zedN/depth` message counts must match
|
- 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.
|
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.
|
||||||
|
|||||||
@@ -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`
|
||||||
+20
-13
@@ -4,12 +4,12 @@ This guide is the simple, non-GUI path for inspecting RGB+depth MCAP files.
|
|||||||
|
|
||||||
Use it when you want to:
|
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
|
- inspect camera labels, message counts, and timestamp ranges
|
||||||
- export one RGB frame and one decoded depth sample as a concrete example
|
- export one RGB frame and one decoded depth sample as a concrete example
|
||||||
- understand how `/bundle` changes the meaning of timestamps and sample grouping
|
- 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
|
## Quick Summary
|
||||||
|
|
||||||
@@ -41,14 +41,19 @@ uv sync --extra viewer
|
|||||||
|
|
||||||
## The Practical Cases
|
## 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`
|
- `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
|
## Recipe: Summarize One MCAP
|
||||||
|
|
||||||
@@ -70,7 +75,7 @@ What the summary prints:
|
|||||||
|
|
||||||
This is the fastest way to answer:
|
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?”
|
- “which camera labels are inside?”
|
||||||
- “do video and depth counts match?”
|
- “do video and depth counts match?”
|
||||||
- “what timestamp range does each camera cover?”
|
- “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
|
--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
|
```bash
|
||||||
uv run python scripts/mcap_rgbd_example.py export-sample \
|
uv run python scripts/mcap_rgbd_example.py export-sample \
|
||||||
@@ -104,25 +109,26 @@ Outputs:
|
|||||||
|
|
||||||
That means:
|
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`
|
- `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.
|
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:
|
Bundled files intentionally separate bundle time from camera sample time:
|
||||||
|
|
||||||
- `/bundle.timestamp` is the nominal common-timeline bundle timestamp
|
- `/bundle.timestamp` is the nominal common-timeline bundle timestamp
|
||||||
- `/zedN/video` and `/zedN/depth` keep the original per-camera sample timestamps
|
- `/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`
|
- there is no `/bundle`
|
||||||
- each camera topic keeps its own original cadence and timestamps
|
- each camera topic keeps its own original cadence and timestamps
|
||||||
|
|
||||||
If you care about grouping, use `/bundle` in bundled files.
|
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
|
## Recipe: Inspect `/bundle` In Python
|
||||||
|
|
||||||
@@ -169,4 +175,5 @@ with path.open("rb") as stream:
|
|||||||
This is the important mental model:
|
This is the important mental model:
|
||||||
|
|
||||||
- `bundled`: follow `/bundle` for grouping
|
- `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`
|
||||||
|
|||||||
@@ -149,6 +149,8 @@ def is_present_status(status_name: str) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
def topic_for(layout: str, camera_label: str, kind: str) -> str:
|
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"}:
|
if layout not in {"copy", "bundled"}:
|
||||||
raise click.ClickException(f"unsupported layout '{layout}'")
|
raise click.ClickException(f"unsupported layout '{layout}'")
|
||||||
return f"/{camera_label}/{kind}"
|
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:
|
def ensure_supported_layout(base_summary: bundle_validator.McapSummary) -> None:
|
||||||
if base_summary.layout == "single-camera":
|
if base_summary.layout not in {"single-camera", "copy", "bundled"}:
|
||||||
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"}:
|
|
||||||
reason = base_summary.validation_reason or "unsupported MCAP layout"
|
reason = base_summary.validation_reason or "unsupported MCAP layout"
|
||||||
raise click.ClickException(reason)
|
raise click.ClickException(reason)
|
||||||
|
|
||||||
@@ -544,7 +542,7 @@ def write_sample_outputs(
|
|||||||
|
|
||||||
@click.group()
|
@click.group()
|
||||||
def main() -> None:
|
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")
|
@main.command("summary")
|
||||||
@@ -558,7 +556,7 @@ def summary_command(mcap_path: Path) -> None:
|
|||||||
|
|
||||||
@main.command("export-sample")
|
@main.command("export-sample")
|
||||||
@click.argument("mcap_path", type=click.Path(path_type=Path, exists=True))
|
@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("--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("--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.")
|
@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_max_m: float,
|
||||||
depth_palette: str,
|
depth_palette: str,
|
||||||
) -> None:
|
) -> 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())
|
summary = summarize_mcap(mcap_path.resolve())
|
||||||
ensure_supported_layout(summary.base)
|
ensure_supported_layout(summary.base)
|
||||||
if summary.base.validation_status != "valid":
|
if summary.base.validation_status != "valid":
|
||||||
|
|||||||
Reference in New Issue
Block a user