Episode export
Bundle a session into rgb.mp4 + mesh.ply + thumbnail + annotation.hdf5 + calibrations + visualization.rrd in one call.
The call
session.export(out_dir, visualizer=None, **kwargs) -> dictWrites a complete episode directory and returns a {"saved": [...], "skipped": [...]} manifest. The same manifest is logged at INFO level so you can see what landed even when ignoring the return value.
session.export("episodes/run_01", visualizer=viz)What it writes
See Episode layout for the file-by-file breakdown and HDF5 schema for the annotation.hdf5 reference.
How frames + annotations get into the export
The export pulls from two buffers populated during your loop:
for frame in session.frames():
blurred = blur.blur(frame)
hands = tracker.detect_hands(frame)
session.add_rgb_frame(frame.index, blurred) # → rgb.mp4
session.add_hand_pose(frame.index, hands) # → annotation.hdf5:/hand-poseadd_rgb_frame, lazily opens an internal H.264 writer; the resulting mp4 is finalised and moved into the episode directory at export time. Use this when you want a post-processed (e.g. blurred) video instead of the raw RGB stream from the mcap.add_hand_pose, accumulates per-frameHandPoselists keyed by frame index. Skip this if you don't run hand detection.
Neither is mandatory. If you don't call add_rgb_frame, the export does its own pass over session.frames() to write the raw rgb.mp4. If you don't call add_hand_pose, /hand-pose is simply absent from the HDF5 (logged as skipped).
Stage logs
The export emits one INFO log per stage, plus a tqdm progress bar on the frame pass:
INFO Exporting episode to episodes/run_01 (frames=13647, duration=911.3s, fps=14.97)
INFO Opening rgb.mp4 writer (1280x720 @ 14.97 fps)
INFO Opening annotation.hdf5
INFO Streaming frames to rgb.mp4 / depth / thumbnail (n=13647)
Export frames: 100%|█████████| 13647/13647 [01:14<00:00, 184.21fr/s]
INFO Finalizing rgb.mp4 (waiting for ffmpeg)
INFO Writing thumbnail.jpg
INFO Writing mesh.ply
INFO Writing calibrations/
INFO Writing /cam-pose
INFO Writing /imu
INFO Writing /metadata
INFO Episode written to episodes/run_01
INFO saved (8): rgb.mp4, annotation.hdf5:/depth, thumbnail.jpg, ...
INFO skipped (1): annotation.hdf5:/hand-pose (no session.add_hand_pose calls)Enable logs once at the top of your script:
import stera
stera.setup_logging()Kwargs
session.export(
out_dir,
visualizer=None,
skip_rgb_mp4=False,
skip_thumbnail=False,
thumbnail_rgb=None,
)| Kwarg | Default | Notes |
|---|---|---|
visualizer | None | An already-populated Visualizer. Its .rrd is promoted to <out_dir>/visualization.rrd. |
skip_rgb_mp4 | False | Set if you wrote rgb.mp4 yourself somewhere else. |
skip_thumbnail | False | Skip the JPEG thumbnail. |
thumbnail_rgb | None | Pre-computed thumbnail (RGB ndarray); used when skip_thumbnail=False. Overrides the mid-frame default. |
When you streamed frames via session.add_rgb_frame, the export auto-detects this and skips its own rgb.mp4 pass. You don't need skip_rgb_mp4=True in that case.
Patterns
Minimal, no hands, no viz
session = MCAPReader("recording.mcap")
session.export("episodes/run_01")Produces rgb.mp4, mesh.ply (if available), thumbnail.jpg, annotation.hdf5 (with /depth /cam-pose /imu /metadata), and calibrations/.
Full pipeline
import stera
from stera.data import MCAPReader
from stera.models import HandTracker, FaceBlurrer, UpperBodyEstimator
from stera.viz import Visualizer
stera.setup_logging()
session = MCAPReader("recording.mcap")
tracker = HandTracker(model="wilor", model_path="/opt/WiLoR")
blur = FaceBlurrer(model="egoblur", model_path="/opt/EgoBlur")
skeleton = UpperBodyEstimator(session=session)
visualizer = Visualizer(session, map_3d="auto")
for frame in session.frames():
blurred = blur.blur(frame)
hands = tracker.detect_hands(frame)
body = skeleton.estimate(frame, hands=hands)
session.add_rgb_frame(frame.index, blurred)
session.add_hand_pose(frame.index, hands)
visualizer.log_frame(frame, hands=hands, skeleton=body)
session.export("episodes/run_01", visualizer=visualizer)Use a custom thumbnail
import cv2
custom = cv2.cvtColor(cv2.imread("title-card.png"), cv2.COLOR_BGR2RGB)
session.export("episodes/run_01", thumbnail_rgb=custom)Append your own data after export
The HDF5 file is closed cleanly by session.export. Re-open it and add your own groups:
import h5py
session.export("episodes/run_01")
with h5py.File("episodes/run_01/annotation.hdf5", "a") as f:
grp = f.create_group("my_annotations")
grp.create_dataset("foo", data=...)session.export never throws for missing upstream data. It writes what it
can and skips the rest, with reasons in the manifest. Treat the return
dict as the source of truth for what made it into the directory.
Pair with Evaluate
Evaluate reads from the same session and is the natural companion to session.export — one writes the training-ready bundle, the other writes the QC report you'd attach to it. The most common end-of-loop in practice:
session.export("episodes/run_01", visualizer=viz)
Evaluate(session).show() # opens an HTML report in the browser
# or: Evaluate(session).export("episodes/run_01/report.html")Evaluate picks up hand poses from session.add_hand_pose automatically and produces a 0–100 health score plus charts for trajectory / IMU / depth / sync / hands. See the Evaluate landing page for the report layout and EvaluateConfig for tuning thresholds.
See also
- Episode layout, every output file.
- HDF5 schema,
annotation.hdf5reference. MCAPReader.export, full signature.- Evaluate, per-stream metrics + health score for the same session.