Visualization Design¶
Autoware-ML includes an isolated visualization package intended for prediction preview, calibration debugging, and dataset inspection. The initial backend target is Rerun, but the task-facing API is kept backend-neutral so future backends or offline exporters can reuse the same task adapters.
Design Goals¶
- Keep visualization out of model and transform core logic.
- Make visualization callable from tests, scripts, and CLI commands.
- Reuse one neutral scene-event layer across calibration status, segmentation3d, and detection3d.
- Keep the first integration narrow: sample-at-a-time preview rather than framework-wide logging during training.
Layering¶
flowchart LR
Task[Task code or test] --> Session[VisualizationSession]
Session --> Adapter[Task adapter]
Adapter --> Events[Visualization events]
Events --> Backend[Visualization backend]
Backend --> Rerun[Rerun SDK]
The package is grouped by responsibility:
visualization/session.pyandvisualization/preview.py: the public session and the preview that drives it from a datamodule and a modelvisualization/tasks/: the calibration status, 3D detection and 3D segmentation adaptersvisualization/sensors/: the camera and LiDAR layers every task sharesvisualization/viewers/: the event layer, the layouts, the backends, and the synced comparison pagevisualization/common.pyandvisualization/colors.py: array conversion, labels and palettes
1. Visualization Session¶
autoware_ml.visualization.session.VisualizationSession is the public entrypoint. It
owns a backend and exposes task-oriented helpers:
log_calibration_status(...)log_segmentation3d(...)log_segmentation3d_data(...)log_detection3d(...)log_detection3d_data(...)
This keeps later CLI or test integration simple and avoids leaking Rerun calls into the rest of the framework.
2. Task Adapters¶
Task adapters turn native Autoware-ML data into neutral scene events:
visualization/tasks/calibration_status.pyvisualization/tasks/segmentation3d.pyvisualization/tasks/detection3d.py
These adapters understand task semantics, for example:
- calibration camera intrinsics and lidar-to-camera transforms
- segmentation point labels and confidences
- detection decoded 3D boxes and class scores
- LiDAR point-cloud intensity and semantic point coloring
- pointwise class probabilities for uncertainty visualization
3. Visualization Events¶
The shared event layer in visualization/viewers/events.py defines primitives such as:
AnnotationContextEventImageEventPointCloud3DEventPoints2DEventBoxes3DEventTransform3DEventPinholeEventScalarEventTextEvent
Every event derives from the VisualizationEvent base class, and every array
field is a NumPy array with its shape annotated through jaxtyping. This is the
isolation boundary between task code and any concrete backend.
4. Backend¶
visualization/viewers/rerun_backend.py is the only place that knows the Rerun SDK.
It converts the neutral events into rerun.Image, rerun.Points3D,
rerun.Boxes3D, rerun.Transform3D, and related entities.
There is also a NoOpVisualizationBackend for disabled or test-only paths. The
Rerun backend is imported lazily, so the noop backend and the smoke tests
built on it stay usable without the Rerun SDK installed.
Dependency compatibility¶
rerun-sdk 0.23.1 declares numpy>=1.23, but its generated __array__
implementations forward a copy argument that only NumPy 2.0 and later accept.
Under the pinned numpy==1.26.4 this makes every AnnotationContext serialize
to an empty list, and because Rerun reports the failure as a warning rather than
an exception, class legends disappear from the viewer while the recording still
looks healthy.
rerun_backend.py therefore patches rerun.datatypes.ClassId.__array__ to drop
the keyword when it is None, which restores NumPy 1.x support and leaves
NumPy 2.x behaviour untouched. Backend startup then logs a probe legend and
raises if it still serializes empty, so an incompatible dependency bump fails
loudly instead of quietly dropping every legend. Remove the patch once the
repository moves to NumPy 2.x or a Rerun release that fixes the conversion.
Class name resolution¶
Legends and per-instance labels read as class names rather than integer ids
because the preview takes them from the taxonomies of the tasks the split
serves: taxonomy.segmentation3d.class_names for point labels, with its
ignore_index drawn as unlabelled, and taxonomy.detection3d.class_names for
boxes. Every source of a split reads with the taxonomies of the run, so one
lookup covers the whole split. visualization.segmentation3d_class_names,
visualization.detection3d_class_names and visualization.class_names
override it.
When no names reach an adapter, format_class_label falls back to the
stringified class id.
Task Coverage¶
Calibration Status¶
The initial calibration adapter can log:
- camera intrinsics
- lidar-to-camera extrinsics
- raw camera image
- projected lidar points overlaid on the image plane
- optional fused image preview
- optional 3D lidar points
- predicted and ground-truth calibration status
- prediction confidence and a readable status summary
The adapter takes the lidar-to-camera transform and the intrinsics of the shown image as plain matrices. The calibration pipelines undistort every image before any other transform, so a pinhole projection with those intrinsics places the lidar points exactly where the fused depth channel puts them.
Segmentation 3D¶
The segmentation adapter can log:
- semantic class legend
- point cloud positions
- predicted semantic labels as per-point colors
- optional ground-truth labels
- sample metadata and point counts
- optional mean confidence from
pred_probs - pointwise normalized entropy computed from
pred_probs(-sum(p * log(p)) / log(num_classes)) - selectable point coloring: semantic labels, LiDAR intensity, or a solid color
This matches the segmentation prediction contract, Segmentation3DPredictions,
which carries pred_labels and pred_probs for every scored point.
Combined Detection and Segmentation¶
A combined detection and segmentation sample is logged below one scene
hierarchy. The Rerun blueprint presents ground truth and prediction side by
side, with the matching detection boxes overlaid on each point cloud.
The explicit scene layout contains:
- a Semantic comparison with GT and predicted pointwise classes
- an Intensity comparison using normalized LiDAR return intensity
- a Geometry comparison with a neutral point color
- an Uncertainty comparison between prediction entropy and predicted semantics
- a Cameras tab for the image streams, while every 3D comparison also contains optional calibrated camera frustums and image planes
- compact 3D IoU quality and match-count plots below GT/PD detection comparisons
The three point-cloud tabs make the coloring mode selectable in the viewer.
The session's point_color_mode only chooses which one is initially active.
Every scene view also lists scene/meta/extent, two invisible corner points
spanning the LiDAR points and every GT and predicted box of the frame. Rerun
fits the initial eye of a 3D view to the bounds of what that view shows, so
without the shared extent the GT and prediction views would start from slightly
different eyes whenever only one of them shows a box that sticks out of the
point cloud.
Synced comparison page¶
Rerun keeps one eye per 3D view and offers no way to link two views, so the
prediction and comparison views of the layout above drift apart as soon as one
of them is rotated. --sync-views therefore serves a second page, by default
on port 9091, that keeps both sides on one eye:
- The page embeds two complete Rerun web viewers in two same-origin frames,
served through
visualization/viewers/synced_viewer.py, which proxies the viewer bundle from the Rerun web server and exposes the viewer handle on its page. Both frames connect to the same gRPC stream as the regular page. build_synced_scene_layoutsderives one full-size scene view per side and variant from the same entity paths as the regular layout: the prediction on the left, and GT, intensity, entropy, or probability on the right, each with camera projections off or on. The backend saves every variant as a blueprint file and publishes a manifest; the page loads the selected variant into each frame through the viewer handle.- All variants of a side share one
ViewSpec.identity, which the Rerun backend turns into a stable view id. Rerun keys the viewer state, including the eye, by that id, so switching the comparison or the camera projections keeps the current viewpoint instead of resetting it. - The page mirrors pointer, wheel, and keyboard events from either frame into the other. Because both sides have the same structure, the same statistics row, and the same shared extent, identical input produces identical eyes.
- The page hides the viewer panels and drives the frame cursor and playback of
both viewers from its own toolbar. The viewer's graphics switches, such as
?renderer=webgl, are passed from the page address to both embedded viewers. - A poll follows the viewers so that scrubbing inside one of them moves the
page too. Because
set_time_for_timelineis applied asynchronously, the poll that follows a write still reports the previous frame, so the page keeps a short grace period after every write and additionally requires both viewers to report the same new frame on two consecutive polls before it follows them. Without that, a manual step read its own stale frame back and jumped forward again. Stepping by hand also pauses playback, so a viewer that is still following an incoming recording cannot pull the timeline along. - During playback the viewers advance between polls, so the two-poll check never applies; the toolbar then simply follows the left viewer's frame without writing it back, and Previous and Next step from the frame on screen.
Selection and double-click focus act on whatever entity is under the cursor on each side, so they are the one interaction that can differ between the sides; the toolbar's Reload layout button reapplies the layouts, and the time controls resynchronize the frame cursor. The page depends on the line of the Rerun 0.23.1 viewer page that creates the viewer handle; the proxy fails with HTTP 502 and a log message if a different viewer bundle no longer contains it.
Detection 3D¶
The detection adapter can log:
- semantic class legend
- optional lidar points
- decoded predicted 3D boxes
- optional ground-truth boxes
- class-colored boxes with class/score labels
- sample metadata and prediction/ground-truth counts
- the same point-cloud color modes, including LiDAR intensity from column 4
- same-class, yaw-aware 3D IoU matching at threshold 0.5, with per-frame true-positive, false-positive, false-negative, precision, recall, and mean matched-IoU scalars
- threshold-independent mean-best-GT and frame-maximum IoU scalars, so near misses remain visible even when no prediction reaches the matching threshold
- predicted and ground-truth boxes projected onto every camera image as class-colored wireframes, with edges behind the image plane dropped
Detection and segmentation use sibling scene/prediction and
scene/ground_truth entities. A frame with no detection annotations remains a
segmentation-only preview; no synthetic detection objects are created.
Camera and timing behavior¶
Cameras are read from the untransformed sample of the dataset, whose
image_samples list the image file and the calibration of every camera of the
record at the sample time. Availability is therefore a property of the record
table, not of the task: the segmentation and detection adapters project their
own layers onto every camera in both data and prediction mode, segmentation as
class-colored points and detection as box wireframes, each for ground truth and
prediction.
The calibration refers to the untransformed LiDAR frame, while the points and boxes are logged after the transforms. A training split whose transforms rotate, scale, translate or flip the points records that augmentation as a 4x4 matrix in the sample, so the preview composes its inverse into every camera extrinsic and the overlays stay on the objects in the images.
Each available camera is logged as a transform, pinhole calibration, and image.
The Rerun blueprint places the camera frustums in the same 3D scene as the
LiDAR points and boxes, enabling live 3D camera views and projection inspection.
The backend JPEG-encodes RGB camera frames at quality 95 and broadcasts constant
point radii instead of repeating them per point. These transport optimizations
preserve scene geometry and calibration while keeping 10 Hz recordings usable
over an SSH tunnel.
Prediction box labels are visible by default and contain the class name and
confidence score. They can still be hidden dynamically with Rerun's Show
labels property when a dense set of proposals obscures the point cloud.
When calibrated cameras are present, every 3D comparison contains a prominent
Camera projections OFF / Camera projections ON tab switch. Switching
tabs immediately hides or shows the complete camera subtree, including both
frustums and projected image planes, without restarting inference. Rerun's eye
control beside the cameras subtree remains available for per-view adjustment.
VisualizationSessionConfig.camera_frustums_visible chooses the initially
active switch position. Pure LiDAR previews do not show the switch and keep the
Blueprint panel collapsed.
Every explicit 3D view uses scene as its origin. Automatic views are disabled,
and the Selection and Time panels start collapsed, so a root / text view or
stream is not shown in the working layout. The Time panel can be expanded when
the 10 Hz timeline needs to be scrubbed.
Camera overlays are pointwise LiDAR segmentation only; camera/pixel
segmentation is outside this scope.
Detection statistics are rendered as both line and point series. Point markers keep a single GT keyframe visible and expose its exact value on hover. A cursor-relative ±10-frame query window keeps the plot focused on the surrounding GT anchors. Loading additional GT anchors extends the same plots into normal time-series curves; frames without ground truth do not fabricate statistics. The statistics row receives one third of the comparison height so its axes and markers remain readable without displacing the primary point-cloud views. Match-count axes auto-scale as later GT anchors add larger values, without republishing the blueprint or resetting interactive camera visibility.
Before each timeline step, the dynamic LiDAR, prediction, ground-truth, camera, and metadata entities are cleared recursively, so points from previous frames cannot accumulate. The same holds for the camera, LiDAR and metadata entities of a calibration-status sample, whose overlays are only logged when they hold points. Detection metrics and calibration status scalars retain their timestamped history for the plots.
The point color mode accepts semantic (default), intensity, or solid. For
intensity, the fourth point feature is normalized per frame and rendered with
the shared scalar heatmap.
The detection adapter takes the boxes, scores and labels of one sample as three
arrays, the bboxes_3d, scores_3d and labels_3d fields of
Detection3DSamplePredictions.
Every adapter takes NumPy arrays, annotated with their shapes through
jaxtyping. The preview is the only place that handles torch tensors: it
copies the batch and the predictions to host arrays before logging them, and
describes each camera with a CameraCalibration (image file, intrinsics and
LiDAR-to-camera transform).
Frames between samples¶
With --prediction-frequency-hz, a prediction preview also predicts the lidar
frames recorded between two samples. Every record of the database stores up to
nine lidar frames after its own, so the frames between two 1 Hz annotated
samples are read from the first one:
- Each stored frame after the sample is rounded onto the timeline at the
requested frequency. A frame is kept when it lands before the next sample of
the same scene, one frame per step. A sample owns
round(hz)timeline steps, so with 1 Hz samples at 10 Hz sampleiis logged at step10 * iand its nine stored frames at the steps after it. - The kept frame becomes the current frame of a new sample. Every stored
frame's
lidar_sensor_to_lidar_sweep_matrixmaps the record's own frame to that stored frame, so each one is re-expressed from the new current frame asM_X @ inv(M_F). The sweep loader then picks past and future sweeps around the new current frame exactly as it does for an annotated sample. - The annotations belong to the record's frame, so the new sample has empty boxes and no semantic mask, the path an unsupervised corpus takes through the pipelines. Its frame metadata takes the pose and the time of the new current frame.
- The sample runs through the transforms of its source and the model like any other.
The record lists the cameras of its own frame only, so the frames in between show the point cloud and the predictions without cameras, and without ground truth: GT is neither interpolated nor held from the previous sample. A database that serves every lidar frame as a sample has its next sample one frame later, so no frame is added there.
Because --max-samples counts samples, the option multiplies the recording:
three 1 Hz samples at 10 Hz are 30 frames. The browser-side viewer keeps the
whole recording in a heap of roughly 2.3 GiB, independently of the
server_memory_limit that bounds the sending side. A recording past that limit
logs Reached memory limit of 2.3 GiB, dropping oldest data in the viewer,
drops the head of the stream including the store info, and therefore never
activates a recording, which leaves the synced page waiting on its handle. With
six cameras and dense point clouds the practical ceiling is about twenty
frames.
Current Integration¶
Visualization is wired into a dedicated CLI preview command:
autoware-ml visualize \
--config-name <task>/<model>/<database>/<config> \
--weights <path/to/model.ckpt> \
--mode predictions \
--split test \
--sample-index 0 \
--max-samples 1 \
--point-color-mode semantic
autoware_ml/visualization/preview.py drives it:
- Builds the datamodule from the config, prepares the record tables when they are missing, and sets up the selected split.
- With
--weights, builds the model likeautoware-ml testand loads the checkpoints into it. - For every sample, locates it in the split, collates the transformed
ModelGTSamplewith the dataset's owncollate_fn, and keeps the untransformed sample for its camera files and timestamps. - With a model, moves the batch through
transfer_batch_to_device,on_after_batch_transferandpredict_step, so the predictions come from the model's own preprocessing and decoding. Every 3D layer of a prediction preview reads the preprocessed batch, which is the one the predictions are aligned with. - Logs the tasks the sample carries through
VisualizationSession.
Segmentation predictions are aligned with the points by the head that made
them: a voxel head labels the current-frame points that fall inside the voxel
grid, and a point head labels every point. The preview picks whichever matches
the number of predicted labels and refuses a sample where neither does.
Ground truth is shown only for the tasks the source is supervised on
(det3d_supervised, seg3d_supervised), so an unsupervised corpus never shows
its placeholder labels.
When no weights are provided, the same command previews the transformed data of the split without running prediction.
Recommended Usage Pattern¶
A call site builds a session from its config and logs one frame at a time:
from autoware_ml.visualization.viewers.contracts import VisualizationSessionConfig
from autoware_ml.visualization.session import VisualizationSession
session = VisualizationSession.from_config(
VisualizationSessionConfig(
backend="rerun",
recording_id="calibration-debug",
web_port=9090,
grpc_port=9876,
)
)
session.begin_frame(sample_index)
session.log_calibration_status(lidar_to_camera=..., image_from_camera=..., image=...)
That keeps the dependency on Rerun confined to one backend module. Use
rerun for browser-based Docker workflows, and noop for smoke tests that
only validate the preview pipeline.