feat(mcap): switch single-source exports to copy layout
Namespace single-source zed_svo_to_mcap outputs with a derived camera label so one-camera exports use the same copy-layout topic shape as current multi-camera copy files. Use the per-camera frame and pose reference identifiers for those single-source outputs, document copy as the current one-camera wire shape, and clarify that legacy /camera/* files are retained only as a legacy contract. Add a small mcap_rgbd_example helper plus mcap_recipes guide for summarizing bundled/copy MCAPs and exporting one RGB/depth sample, and update the validator and viewer wording/behavior to match the bundled-vs-copy semantics.
This commit is contained in:
+12
-4
@@ -1,14 +1,15 @@
|
||||
# MCAP Layout
|
||||
|
||||
`cvmmap-streamer` writes three related MCAP layouts:
|
||||
`cvmmap-streamer` currently deals with three active MCAP layouts plus one legacy single-camera layout:
|
||||
|
||||
- single-camera MCAP export
|
||||
- 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.
|
||||
|
||||
## Single-Camera Layout
|
||||
## Legacy Single-Camera Layout
|
||||
|
||||
Default topics:
|
||||
|
||||
@@ -23,6 +24,8 @@ Default topics:
|
||||
|
||||
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 export namespaces each camera stream and adds one bundle manifest topic:
|
||||
@@ -65,6 +68,11 @@ Because `nearest` emits on the slowest common timeline, faster cameras can legit
|
||||
|
||||
`--copy-range common` trims each camera to the common overlap window without resampling. `--copy-range full` preserves each camera’s full readable timestamp range from the grouped segment.
|
||||
|
||||
Single-source `zed_svo_to_mcap` now writes this same wire shape with one derived label, for example:
|
||||
|
||||
- `/zed4/video`, `/zed4/depth`, `/zed4/calibration` for `*_zed4.svo2`
|
||||
- `/cam1/video`, `/cam1/depth`, `/cam1/calibration` when the filename has no `zedN` suffix
|
||||
|
||||
## 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.
|
||||
@@ -151,4 +159,4 @@ 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
|
||||
|
||||
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.
|
||||
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,172 @@
|
||||
# MCAP Recipes
|
||||
|
||||
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`
|
||||
- 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).
|
||||
|
||||
## Quick Summary
|
||||
|
||||
The repository includes a small example helper:
|
||||
|
||||
```bash
|
||||
uv run python scripts/mcap_rgbd_example.py --help
|
||||
```
|
||||
|
||||
It has two commands:
|
||||
|
||||
- `summary`: print layout, per-camera counts, and timestamp ranges
|
||||
- `export-sample`: write one RGB image plus one depth array/preview
|
||||
|
||||
`summary` works with the base Python dependencies:
|
||||
|
||||
```bash
|
||||
uv sync
|
||||
```
|
||||
|
||||
`export-sample` also needs:
|
||||
|
||||
- `ffmpeg` on `PATH`
|
||||
- the optional depth decoder binding:
|
||||
|
||||
```bash
|
||||
uv sync --extra viewer
|
||||
```
|
||||
|
||||
## The Practical Cases
|
||||
|
||||
For this helper, there are really two supported layouts:
|
||||
|
||||
- `bundled`: multiple namespaced camera topics plus `/bundle`
|
||||
- `copy`: namespaced camera topics 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`.
|
||||
|
||||
Legacy `/camera/*` MCAP files are intentionally out of scope for this helper. Regenerate them into `copy` layout if you still have old files around.
|
||||
|
||||
## Recipe: Summarize One MCAP
|
||||
|
||||
```bash
|
||||
uv run python scripts/mcap_rgbd_example.py summary <MCAP_PATH>
|
||||
```
|
||||
|
||||
What the summary prints:
|
||||
|
||||
- layout and validation status
|
||||
- camera labels
|
||||
- per-camera `video`, `depth`, `pose`, `calibration`, `depth_calibration`, and `body` counts
|
||||
- per-camera video/depth timestamp ranges
|
||||
- for bundled files only:
|
||||
- bundle count
|
||||
- bundle timestamp range
|
||||
- bundle policy counts
|
||||
- per-camera present/corrupted-gap/unknown bundle-member counts
|
||||
|
||||
This is the fastest way to answer:
|
||||
|
||||
- “is this file bundled or copy?”
|
||||
- “which camera labels are inside?”
|
||||
- “do video and depth counts match?”
|
||||
- “what timestamp range does each camera cover?”
|
||||
|
||||
## Recipe: Export One RGB + Depth Sample
|
||||
|
||||
```bash
|
||||
uv run python scripts/mcap_rgbd_example.py export-sample \
|
||||
<MCAP_PATH> \
|
||||
--output-dir /tmp/mcap_sample
|
||||
```
|
||||
|
||||
For multi-camera or one-camera `copy` files, choose the camera explicitly when needed:
|
||||
|
||||
```bash
|
||||
uv run python scripts/mcap_rgbd_example.py export-sample \
|
||||
<MCAP_PATH> \
|
||||
--camera-label zed2 \
|
||||
--sample-index 25 \
|
||||
--output-dir /tmp/mcap_sample_zed2
|
||||
```
|
||||
|
||||
Outputs:
|
||||
|
||||
- `rgb.png`
|
||||
- `depth.npy`
|
||||
- `depth_preview.png`
|
||||
- `sample_metadata.json`
|
||||
|
||||
`sample_index` is always zero-based per-camera RGB+depth sample order.
|
||||
|
||||
That means:
|
||||
|
||||
- `copy`: sample `N` is `/zedN/video[N]` + `/zedN/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
|
||||
|
||||
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:
|
||||
|
||||
- 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.
|
||||
|
||||
## Recipe: Inspect `/bundle` In Python
|
||||
|
||||
The helper script is intentionally small, but sometimes it is easier to inspect `/bundle` directly.
|
||||
This snippet shows how to print bundle membership for one camera:
|
||||
|
||||
```python
|
||||
from pathlib import Path
|
||||
|
||||
import zed_batch_svo_to_mcap as batch
|
||||
|
||||
|
||||
path = Path("<MCAP_PATH>").expanduser().resolve()
|
||||
camera_label = "zed1"
|
||||
reader_module = batch.load_mcap_reader()
|
||||
|
||||
with path.open("rb") as stream:
|
||||
reader = reader_module.make_reader(stream)
|
||||
for schema, channel, message in reader.iter_messages():
|
||||
if channel.topic != "/bundle":
|
||||
continue
|
||||
if schema is None or schema.name != "cvmmap_streamer.BundleManifest":
|
||||
continue
|
||||
|
||||
bundle_class, present_value = batch.load_bundle_manifest_type(schema.data)
|
||||
bundle = bundle_class()
|
||||
bundle.ParseFromString(message.data)
|
||||
|
||||
for member in bundle.members:
|
||||
if str(member.camera_label) != camera_label:
|
||||
continue
|
||||
status_value = int(getattr(member, "status", 0))
|
||||
status_field = member.DESCRIPTOR.fields_by_name.get("status")
|
||||
status_enum = status_field.enum_type if status_field is not None else None
|
||||
status_name = (
|
||||
status_enum.values_by_number.get(status_value).name
|
||||
if status_enum is not None and status_enum.values_by_number.get(status_value) is not None
|
||||
else str(status_value)
|
||||
)
|
||||
print(bundle.bundle_index, status_name)
|
||||
break
|
||||
```
|
||||
|
||||
This is the important mental model:
|
||||
|
||||
- `bundled`: follow `/bundle` for grouping
|
||||
- `copy`: treat each camera as an independent stream
|
||||
Reference in New Issue
Block a user