cascade_av.viz renders any subset of a clip — a single instant, a
time range, or a MatchSet — as a decoded camera frame paired with
the clip's annotation timeline. The timeline carries one bar per
agent action, ego action, environment, condition, and traffic-light
state, plus three documented families of causal arrow (because_of,
containment, action_target). It also paints a yellow
highlight band over any match interval. Every timeline-backed view uses
the same top-to-bottom group order: Ego, Agents, Traffic Lights,
Objects, Environments.
This doc is the authoritative reference; the README's Visualization section is a short teaser pointing here.
The viz API requires the optional [viz] extra:
uv sync --extra vizIt runs in JupyterLab, VS Code / Cursor notebooks, and similar Jupyter-protocol environments. Colab support is best-effort.
The common workflows hang off objects you already use.
ds.get_sequence(clip_id) returns a Sequence — the per-clip handle
that bundles the parsed annotation with camera / sensor accessors and
the .visualize() method:
seq = ds.get_sequence(clip_id)
# Whole-clip scrubbable widget. Pass t=2.5 to open at a single instant,
# or t=(2, 5) for an explicit window.
seq.visualize()
# Single match in context — pads the match interval by `pad` seconds
# and paints `m.interval` as the yellow highlight band.
m = ds.find("agent.type = ped while ego.action = decel").matches[0]
seq.visualize(match=m, pad=1.0)
# Fan out a whole MatchSet into a carousel of mini-players (one per
# match, capped at `limit`).
ds.find("ego.action = decel because_of agent.type = ped").visualize()
# Static publication figure with selected video frames above the tracks.
seq.visualize(mode="paper_figure", timestamps=[1.0, 2.5, 4.0])Filter support differs slightly by entry point. "All" below means
arrows, entity_kinds, agent_ids, track_groups, families, and
track_visibility:
| Entry point | Timeline filters |
|---|---|
seq.visualize(...) |
All, whenever the result includes a timeline; the static=True single-frame path has no tracks to filter |
viz.render_timeline(seq, ...) |
All |
viz.render_paper_figure(seq, ...) |
All |
ClipPlayer(seq, ...) |
All |
matches.visualize(...) |
arrows, families |
The segment filters AND together; the arrows toggle is independent
(it gates arrow families, not the segment filter).
For reports, doc figures, or pipelines without a Jupyter kernel, the headless functions return plain values you can pickle, save, or post-process:
from cascade_av import viz
frame = viz.render_frame(seq, t=3.0) # -> PIL.Image (RGB)
fig = viz.render_timeline(seq, # -> plotly.graph_objects.Figure
highlight=(2, 5))
paper = viz.render_paper_figure( # -> plotly.graph_objects.Figure
seq,
timestamps=[1.0, 3.0, 5.0],
track_visibility={"agent": {"agent_4": False}},
)
frame.save("/tmp/clip_t3.png")
fig.write_image("/tmp/timeline.png") # needs the `kaleido` extra
paper.write_image("/tmp/paper.png") # needs the `kaleido` extraseq.visualize(t=2.5, static=True) is the shorthand for
viz.render_frame(seq, 2.5) — handy when you start interactive and
want a single still without switching modules.
viz.render_paper_figure() composes zero to three explicitly selected
video frames side-by-side above the annotation tracks. The high-level
equivalent is seq.visualize(mode="paper_figure", timestamps=[...]).
Both return a plain Plotly figure rather than an interactive widget.
paper = viz.render_paper_figure(
seq,
timestamps=[4.8, 1.2, 3.0],
track_visibility={
"agent": {"agent_4": False, "agent_5": False},
"env": False,
},
)Timestamp rules are deliberately strict for reproducible figures:
- Pass zero to three finite, non-negative numeric timestamps in seconds. An empty list creates a timeline-only figure and does not access the video.
- Frames are sorted chronologically from left to right, so the unsorted example above displays 1.2 s, then 3.0 s, then 4.8 s. Duplicate timestamps are retained as separate frames.
- Every value must lie within the video's actual timestamp coverage.
Out-of-coverage values raise
ValueError; they are never clamped. mode="paper_figure"usestimestamps, so it cannot be combined witht,match,context, orstatic=True.
When frames are requested, the sequence must have an accessible video.
They are embedded as quality-90 JPEGs capped at 1920 pixels on the longer
edge, keeping saved HTML and executed notebooks compact without changing
the source video. The timeline filters described below, plus highlight,
height, width, show_inline_labels, and show_containments are available
in paper mode.
Paper figures preserve the complete name on every visible timeline box:
labels are never shortened with an ellipsis or omitted just because a box is
narrow. Boxes at either end of the time axis anchor their labels toward the
plot interior. Very dense figures can therefore have overlapping text; use
the visibility/family filters, a wider width, or
show_inline_labels=False when a label-free composition is preferable.
Containment-family rows and their connected arrows are hidden by default in
paper mode. This covers general entity containment, Traffic-Light physical
containment, and signal-head environment containment. Pass
show_containments=True to restore them. Other timeline views retain their
existing containment rows by default.
All timeline-backed entry points use this canonical top-to-bottom order:
- Ego
- Agents
- Traffic Lights
- Objects
- Environments
Filtering a group out collapses the y-axis while preserving the relative order of the groups that remain.
Traffic-Light state boxes use their annotated semantic color: green, yellow,
red, or neutral gray for Other; missing and unknown colors use the existing
dark-red fallback. Arrowheads use the same family color for both their fill
and outline as the corresponding arrow path.
Type: dict[str, bool | dict[str, bool]] · Default: all-on
Use a boolean to switch a whole kind, or a nested mapping to switch individual tracks by their stable, non-empty annotation IDs. Omitted kinds and IDs default to on.
# Hide all Environment tracks and two specific Agent tracks.
seq.visualize(
track_visibility={
"env": False,
"agent": {"agent_4": False, "agent_5": False},
}
)| Kind | Nested selector |
|---|---|
"ego" |
Synthetic singleton ID "ego" |
"agent" |
Agent.id |
"light" |
TrafficLight.id |
"object" |
TrafficObject.id |
"env" |
Environment.id |
Hiding a top-level entity removes its parent track and every descendant
row. Causal arrows with either endpoint hidden are removed too. Unknown
kinds and IDs raise ValueError, which prevents a misspelled selector
from silently leaving the wrong track in a publication figure. Selecting
an ID that occurs more than once also raises: duplicate IDs are ambiguous,
so use a whole-kind switch or fix the annotation IDs first.
Type: dict[str, bool] · Default: all-on
Three documented families. Pass False for any you want to hide;
unmentioned keys default to on.
Paper figures still use these all-on arrow toggles, but their containment rows
default to hidden, so no containment arrow is drawable until
show_containments=True restores those rows.
seq.visualize(arrows={"because_of": False, "containment": False})| Family | What it represents |
|---|---|
because_of |
causal cause arrows — action.because_of field on EgoAction / AgentAction |
containment |
spatial-containment edges (agent ∈ environment, etc.) |
action_target |
action → target edges |
because_of targets resolve to any visible segment-backed stable ID,
including nested actions, light/object states, properties, conditions, and
containment records. Dangling IDs remain non-fatal and do not paint an arrow.
Influence annotations remain available through the schema, query DSL, and
viz.annotation_to_segments(), but timeline-backed DevKit views intentionally
do not render influence rows or influence arrows. For compatibility,
families=["influence"] is still accepted and renders no rows.
Type: bool · Default: False in paper mode
Paper figures omit the containment, physical_containment, and
env_control rows by default. Pass show_containments=True to restore those
rows and make their connected containment arrows eligible to render. This
switch is paper-only; ordinary timelines keep containment content visible by
default.
Type: list[str] · Default: None (all kinds)
viz.render_timeline(seq, entity_kinds=["agent", "ego"])| Value | Selects |
|---|---|
"ego" |
Ego vehicle segments |
"agent" |
Agent segments + their sub-rows |
"light" |
Traffic Light segments (parent + per-signal-head) |
"object" |
Object segments |
"env" |
Environment segments |
Type: list[str] · Default: None (all agents) · Affects only the agent kind
seq.visualize(agent_ids=["agent_3", "agent_7"])Other kinds (env / light / object / ego) are untouched. Combine with
entity_kinds=["agent"] if you want only those specific agents and
nothing else.
Type: list[str] · Default: None (all five categories)
viz.render_timeline(seq, track_groups=["Agents", "Ego"])| Value | Aliases |
|---|---|
"Ego" |
— |
"Agents" |
— |
"Traffic Lights" |
"Lights" (legacy) |
"Objects" |
— |
"Environments" |
"Env" (legacy) |
Rows not in the whitelist drop their tick labels too — the y-axis collapses to the visible categories.
Type: list[str] · Default: None (all families)
matches.visualize(families=["action", "condition"])| Value | Where it lives |
|---|---|
"condition" |
Environments |
"physical_containment" |
Traffic Lights |
"signal_head" |
Traffic Lights (per signal head) |
"env_control" |
Traffic Lights (per signal head) |
"state" |
Traffic Lights (per signal head), Objects |
"containment" |
Objects, Agents, Ego |
"pose" |
Agents |
"action" |
Agents, Ego |
"property" |
Agents, Ego |
Parent retention. Parent entity headers (Env Track 1,
Agent Track 2, ...) are NOT named in this whitelist. They auto-
render for any entity whose sub-rows survive the filter, and drop
completely for entities with zero surviving sub-rows. There are no
orphan section heads.
Example: families=["action"] on a clip with one Env, one Ego, and
two Agents — the Env block disappears (its only sub-row is
condition), and the Ego + Agent blocks keep their parent headers
because each has an action sub-row.
A segment paints only if it survives every supplied selector:
track_groups ∧ entity_kinds ∧ agent_ids ∧ families ∧ track_visibility
∧ paper_containment_switch
The final switch applies only to paper figures. Arrows paint only if BOTH endpoints survived the segment filter.
# Just agent actions for two specific agents, no Ego rows in the way,
# and no `because_of` clutter from the arrow family.
seq.visualize(
track_groups=["Agents"],
families=["action"],
agent_ids=["agent_3", "agent_7"],
arrows={"because_of": False},
)These shape the figure without dropping content.
| Kwarg | Type | Default | What it does |
|---|---|---|---|
highlight |
tuple[float, float] |
None |
Paint a translucent yellow band across (t0, t1) — used to mark a match interval or window of interest. |
show_inline_labels |
bool |
True |
When False, suppress the in-bar text labels. Paper mode otherwise preserves every complete box label; other timeline views use compact labels plus full hover text. |
height |
int |
None (adaptive) |
Pin the total figure height in pixels. Default scales with the deepest sub-lane stack. |
Type: bool · Default: False · Surface: matches.visualize /
build_matchset_carousel
By default the carousel renders one player per match in the
MatchSet. Three matches that all live in the same clip will produce
three players over that clip. Pass unique_clips=True to flip to
one player per distinct clip: matches sharing a clip_id are
unioned and rendered once, with the playback window covering
[min(t0)-pad, max(t1)+pad] and the highlight band spanning the union.
The label switches to clip_id · N matches · [t0, t1]s whenever a clip
has more than one match.
# Three *distinct* clips — even when one of them has many matches.
matches.visualize(limit=3, unique_clips=True)limit counts whatever the carousel is rendering: matches when
unique_clips=False, clips when unique_clips=True. The truncation
notice ("Showing 3 of 12 clips" vs ".. of 12 matches") follows the same
rule.
Use the default when you're hunting individual matches; flip the knob when you're surveying which clips show the behaviour.
| File | Surface |
|---|---|
src/cascade_av/viz/timeline.py |
render_timeline, _paint_timeline_onto (the load-bearing painter; all segment and arrow filters resolve here) |
src/cascade_av/viz/paper.py |
render_paper_figure — validates and decodes selected frames, then composes them above the shared timeline painter |
src/cascade_av/viz/widget.py |
ClipPlayer — forwards every timeline filter to _paint_timeline_onto |
src/cascade_av/viz/carousel.py |
build_matchset_carousel — forwards arrows and families to each per-match ClipPlayer |
src/cascade_av/dataset.py |
Sequence.visualize — high-level dispatcher for frame, player, and paper-figure modes; forwards every timeline filter |
The walkthrough notebook at notebooks/06_visualize.ipynb
demos the interactive entry points and a three-frame, per-entity-filtered
paper figure against a real clip. The runnable
examples/07_visualize.py also writes a
paper figure.