feat(mcap): add Python layout validator

Document the bundled and single-camera MCAP topic contract in docs/mcap_layout.md and link it from the README.

Add scripts/mcap_bundle_validator.py to summarize and validate both bundled /bundle-based MCAPs and single-camera /camera/* MCAPs from Python.

Validate bundled files against bundle-member presence counts and single-camera files against topic/schema expectations plus video/depth/calibration count rules.
This commit is contained in:
2026-03-24 07:54:12 +00:00
parent 807a73b480
commit 6d50b29eff
3 changed files with 471 additions and 0 deletions
+114
View File
@@ -0,0 +1,114 @@
# MCAP Layout
`cvmmap-streamer` writes two related MCAP layouts:
- single-camera MCAP export
- bundled multi-camera MCAP export
This document covers the topic layout, schema types, and timestamp semantics for both.
## 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.
## Bundled Multi-Camera Layout
Bundled export namespaces each camera stream and adds one bundle manifest topic:
| Topic | Schema / Encoding | Notes |
|------|------|------|
| `/bundle` | `cvmmap_streamer.BundleManifest` | One message per emitted bundle |
| `/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) |
`nearest` is the default bundled policy. It emits one bundle on a common timeline, but it keeps the original per-camera timestamps on `/zedN/video`, `/zedN/depth`, and optional `/zedN/pose`.
`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.
## 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.
`cvmmap_streamer.BundleManifest` contains:
- `timestamp`: the nominal bundle timestamp on the common timeline
- `bundle_index`: zero-based emitted bundle index
- `policy`: `BUNDLE_POLICY_NEAREST` or `BUNDLE_POLICY_STRICT`
- `members`: one entry per camera label in the bundle
Each bundle member contains:
- `camera_label`
- `timestamp`: the original camera sample timestamp when a payload is present
- `delta_ns`: `member.timestamp - bundle.timestamp` when a payload is present
- `status`
- `corrupted_frames_skipped`
Current member statuses:
- `BUNDLE_MEMBER_STATUS_PRESENT`
- `BUNDLE_MEMBER_STATUS_CORRUPTED_GAP`
`CORRUPTED_GAP` means the exporter skipped one or more unreadable ZED frames and intentionally emitted no video, depth, or pose payload for that camera for this bundle. `corrupted_frames_skipped` reports the size of the skipped run that led to the next recovered readable frame.
## Timestamp Semantics
The bundled MCAP contract intentionally separates bundle time from per-camera sample time:
- `/bundle.logTime` and `/bundle.publishTime` use the nominal bundle timestamp
- `/zedN/video`, `/zedN/depth`, and `/zedN/pose` use the original camera sample timestamp
- calibration and depth-calibration messages use the timestamp of the first emitted sample on that camera
This means a single bundle can legitimately contain different per-camera timestamps, especially in `nearest` mode.
## Corruption And Partial Bundles
Bundled `nearest` export is 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
- bundles inside the unreadable gap still exist
- the affected camera is marked `CORRUPTED_GAP` in `/bundle`
- no `/zedN/video`, `/zedN/depth`, or `/zedN/pose` message is written for that camera for those bundles
Bundled `strict` export stays strict:
- corruption is skipped internally until recovery
- only fully present, threshold-qualified groups are emitted
## 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
- every bundle must contain one member per camera label
- for each camera, the count of `BUNDLE_MEMBER_STATUS_PRESENT` members must match the number of `/zedN/video` messages
- for each camera, the count of `BUNDLE_MEMBER_STATUS_PRESENT` members must match the number of `/zedN/depth` messages
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.