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.
5.1 KiB
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
/bundlechanges the meaning of timestamps and sample grouping
For the full layout contract, see mcap_layout.md.
Quick Summary
The repository includes a small example helper:
uv run python scripts/mcap_rgbd_example.py --help
It has two commands:
summary: print layout, per-camera counts, and timestamp rangesexport-sample: write one RGB image plus one depth array/preview
summary works with the base Python dependencies:
uv sync
export-sample also needs:
ffmpegonPATH- the optional depth decoder binding:
uv sync --extra viewer
The Practical Cases
For this helper, there are really two supported layouts:
bundled: multiple namespaced camera topics plus/bundlecopy: 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
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, andbodycounts - 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
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:
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.pngdepth.npydepth_preview.pngsample_metadata.json
sample_index is always zero-based per-camera RGB+depth sample order.
That means:
copy: sampleNis/zedN/video[N]+/zedN/depth[N]bundled: sampleNis theNth present sample for that camera, not bundle indexN
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.timestampis the nominal common-timeline bundle timestamp/zedN/videoand/zedN/depthkeep 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:
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/bundlefor groupingcopy: treat each camera as an independent stream