# Rerun
> Rerun is the Unified Data Layer for Physical AI: a platform for logging, visualizing, querying, transforming, and training on multi-rate, multimodal, time-series data in robotics, spatial computing, and embodied AI.
- Rerun has two parts: the open source **Rerun SDK** (libraries and tools for logging, storing, querying, visualizing, transforming, and training on multi-rate, multimodal data) and **Rerun Hub**, a commercial data catalog and storage engine that extends the SDK to large-scale datasets backed by object storage. Together they form the Unified Data Layer for Physical AI.
- **Rerun SDK is open source.** You can find the project on [GitHub](https://github.com/rerun-io/rerun). It is permissively licensed under MIT, so you can use it for personal and commercial projects.
- The Rerun SDK includes logging APIs for [Python](https://ref.rerun.io/docs/python), [C++](https://ref.rerun.io/docs/cpp), and [Rust](https://docs.rs/rerun/), the Rerun viewer, a CLI, catalog APIs, chunk processing APIs, and a [PyTorch dataloader](https://rerun.io/blog/data-layer-for-robot-learning).
- The Rerun viewer can either be installed and run natively, or in the browser.
- You can [try Rerun in your browser](/viewer).
## Docs
# Reference
The reference docs detail how to use the logging APIs and the viewer.
- [Types](https://rerun.io/docs/reference/types.md) - archetypes, components, and datatypes
- [Viewer](https://rerun.io/docs/reference/viewer/overview.md) - the Rerun Viewer UI
- [CLI manual](https://rerun.io/docs/reference/cli.md) - command-line interface reference
- [Python APIs](https://ref.rerun.io/docs/python) - Python SDK reference
- [Rust APIs](https://docs.rs/rerun/) - Rust SDK reference
- [C++ APIs](https://ref.rerun.io/docs/cpp) - C++ SDK reference
- [Web Viewer API](https://ref.rerun.io/docs/js/) - JavaScript/TypeScript web viewer API
- [Migration](https://rerun.io/docs/reference/migration.md) - guides for upgrading between versions
# Getting Started
Rerun helps robotics and Physical AI teams iterate faster: log from any sensor, visualize in the Viewer, query with dataframes, and train with a dataloader tailored to robotic learning β across one recording or many.
## Installation
`pip install rerun-sdk[dataplatform, dataloader]` bundles the **SDK** (log/query from code) and the **Viewer** (visualizer app). The optional dependencies support queries and training below.
For Rust, C++, see [Install Rerun](https://rerun.io/docs/getting-started/install-rerun.md) and [Set up a project](https://rerun.io/docs/getting-started/project-setup.md).
## Open the Viewer
`rerun` launches the Viewer.
Pass a file to open it directly:
```bash
rerun path/to/recording.rrd
```
Supports `.rrd`, `.mcap`, and [more](https://rerun.io/docs/getting-started/data-in/open-any-file.md).
Also available in-browser at [rerun.io/viewer](https://rerun.io/viewer).
## Scale across many recordings
Rerun's catalog organizes recordings as queryable [**segments**](https://rerun.io/docs/concepts/query-and-transform/catalog-object-model.md).
The workflow: log (or convert) data to an `.rrd`, start a catalog server (or connect to an existing one if using the commercial Rerun Hub), register the `.rrd` as a segment, then visualize and query across recordings.
### Log
Save data to an `.rrd` see [Log and Ingest](https://rerun.io/docs/getting-started/data-in.md) for more details.
If you already have data in another format see our [how-to](https://rerun.io/docs/howto/logging-and-ingestion) for various examples converting to `.rrd`.
```python
import math
import rerun as rr
with rr.RecordingStream(
"rerun_example_getting_started", recording_id="run-1", send_properties=False
) as rec:
rec.save("run-1.rrd")
for t in range(10):
rec.set_time("step", sequence=t)
rec.log("/arm/shoulder", rr.Scalars(math.sin(t * 0.5)))
rec.log("/arm/elbow", rr.Scalars(math.cos(t * 0.5)))
```
### Start a catalog server
`rerun server` starts a local catalog on port `51234` (use Rerun Hub for persistent, multi-user storage), then connect from your code:
```bash
rerun server
```
```python
# `server_url` is the catalog URL β defaults to "rerun+http://127.0.0.1:51234"
# when running `rerun server` locally.
client = rr.catalog.CatalogClient(server_url)
```
### Ingest
Register an `.rrd` with a dataset so it shows up as a queryable segment.
```python
dataset = client.create_dataset("demo", exist_ok=True)
dataset.register([Path("run-1.rrd").absolute().as_uri()]).wait()
```
### Visualize
Point the Viewer at your server to browse every recording in the catalog.
See [Configure the Viewer](https://rerun.io/docs/getting-started/configure-the-viewer.md).
```bash
rerun rerun+http://127.0.0.1:51234
```
### Query
Query the catalog into a [DataFusion](https://datafusion.apache.org/) DataFrame. See [Query and Transform](https://rerun.io/docs/getting-started/data-out.md).
```python
df = dataset.filter_contents(["/arm/**"]).reader(index="step")
print(
df.select(
"rerun_segment_id",
"/arm/shoulder:Scalars:scalars",
"/arm/elbow:Scalars:scalars",
)
)
```
### Train
Connect a Dataloader to the server to generate training batches. See [Train](https://rerun.io/docs/getting-started/train.md).
```python
from torch.utils.data import DataLoader
from rerun.experimental.dataloader import (
DataSource,
Field,
NumericDecoder,
RerunIterableDataset,
)
ds = RerunIterableDataset(
source=DataSource(dataset=dataset),
index="step",
fields={
"shoulder": Field(
"/arm/shoulder:Scalars:scalars", decode=NumericDecoder()
),
"elbow": Field("/arm/elbow:Scalars:scalars", decode=NumericDecoder()),
},
)
for batch in DataLoader(ds, batch_size=4):
print(batch)
```
## If you're stuck
- Check the [troubleshooting guide](https://rerun.io/docs/getting-started/install-rerun/troubleshooting.md).
- [Open an issue](https://github.com/rerun-io/rerun/issues/new/choose).
- [Join the Discord server](https://discord.gg/PXtCgFBSmH).
# Developing Rerun
If you want to contribute and help develop Rerun, that work happens in the [Rerun repository](https://github.com/rerun-io/rerun). Also, make sure to join the [Rerun Discord](https://discord.gg/PXtCgFBSmH).
# Concepts
For a deeper understanding of how your logging data can be organized and visualized, it is useful to learn about the underlying concepts in Rerun.
- [How Does Rerun Work](https://rerun.io/docs/concepts/how-does-rerun-work.md) - understand the high-level architecture
- [Logging and ingestion](https://rerun.io/docs/concepts/logging-and-ingestion.md) - how data is structured and sent to Rerun
- [Visualization](https://rerun.io/docs/concepts/visualization.md) - how data is displayed in the Viewer
- [Query and transform](https://rerun.io/docs/concepts/query-and-transform.md) - how to query and transform data
- [Lenses](https://rerun.io/docs/concepts/query-and-transform/lenses.md) - extract, reshape, and reroute component data
- [Train](https://rerun.io/docs/concepts/train.md) - how to use Rerun data for training
# How-to
Guides for using Rerun in more advanced ways.
- [Logging and ingestion](https://rerun.io/docs/howto/logging-and-ingestion.md) - sending data to Rerun
- [Visualization](https://rerun.io/docs/howto/visualization.md) - displaying data in the Viewer
- [Query and transform](https://rerun.io/docs/howto/query-and-transform.md) - querying and transforming data
- [Train](https://rerun.io/docs/howto/train.md) - using Rerun data for training
- [Integrations](https://rerun.io/docs/howto/integrations.md) - integrating Rerun with other tools
# Log and Ingest
How-to guides for sending data to Rerun.
# Integrations
How-to guides for integrating Rerun with other tools.
# Extend Rerun
There are currently two major ways of extending Rerun. You can use Rerun with [your own custom data](https://rerun.io/docs/howto/logging-and-ingestion/custom-data.md), or [extend the Rerun Viewer](https://rerun.io/docs/howto/visualization/extend-ui.md) (currently Rust only).
The goal is for Rerun to become easy to extend at every level. For example, with plugins for
- data sources
- data stores
- data transforms
- custom rendering
- custom UI interactions
If you're in need of a particular kind of extension mechanism that we don't yet support. Head over to [GitHub](https://github.com/rerun-io/rerun/issues) and either upvote an existing issue or open a new one.
# Query and Transform
How-to guides for querying and transforming data in Rerun.
# Visualize
How-to guides for visualizing data in the Rerun Viewer.
# Building blueprints programmatically
For maximum control and automation, you can define [Blueprints](https://rerun.io/docs/concepts/visualization/blueprints.md) in code using the Python Blueprint API. This is ideal for:
- Creating layouts dynamically based on your data
- Ensuring consistent views for specific debugging scenarios
- Generating complex layouts that would be tedious to build manually
- Sending different blueprints based on runtime conditions
### Getting started example
This walkthrough demonstrates the Blueprint API using stock market data. We'll start simple and progressively build more complex layouts.
#### Setup
First, create a virtual environment and install dependencies:
**Linux/Mac:**
```bash
python -m venv venv
source venv/bin/activate
pip install rerun-sdk humanize yfinance
```
**Windows:**
```bash
python -m venv venv
.\venv\Scripts\activate
pip install rerun-sdk humanize yfinance
```
#### Basic script
Create `stocks.py` with the necessary imports:
```python
#!/usr/bin/env python3
import datetime as dt
import humanize
import pytz
import yfinance as yf
from typing import Any
import rerun as rr
import rerun.blueprint as rrb
```
Add helper functions for styling:
```python
brand_colors = {
"AAPL": 0xA2AAADFF,
"AMZN": 0xFF9900FF,
"GOOGL": 0x34A853FF,
"META": 0x0081FBFF,
"MSFT": 0xF14F21FF,
}
def style_plot(symbol: str) -> rr.SeriesLine:
return rr.SeriesLine(
color=brand_colors[symbol],
name=symbol,
)
def style_peak(symbol: str) -> rr.SeriesPoint:
return rr.SeriesPoint(
color=0xFF0000FF,
name=f"{symbol} (peak)",
marker="Up",
)
def info_card(
shortName: str,
industry: str,
marketCap: int,
totalRevenue: int,
**args: dict[str, Any],
) -> rr.TextDocument:
markdown = f"""
- **Name**: {shortName}
- **Industry**: {industry}
- **Market cap**: ${humanize.intword(marketCap)}
- **Total Revenue**: ${humanize.intword(totalRevenue)}
"""
return rr.TextDocument(markdown, media_type=rr.MediaType.MARKDOWN)
```
Add the main function that logs data:
```python
def main() -> None:
symbols = ["AAPL", "AMZN", "GOOGL", "META", "MSFT"]
# Use eastern time for market hours
et_timezone = pytz.timezone("America/New_York")
start_date = dt.date(2024, 3, 18)
dates = [start_date + dt.timedelta(days=i) for i in range(5)]
# Initialize Rerun and spawn a new viewer
rr.init("rerun_example_blueprint_stocks", spawn=True)
# This is where we will edit the blueprint
blueprint = None
# rr.send_blueprint(blueprint)
# Log the stock data for each symbol and date
for symbol in symbols:
stock = yf.Ticker(symbol)
# Log the stock info document as static
rr.log(f"stocks/{symbol}/info", info_card(**stock.info), static=True)
for day in dates:
# Log the styling data as static
rr.log(f"stocks/{symbol}/{day}", style_plot(symbol), static=True)
rr.log(f"stocks/{symbol}/peaks/{day}", style_peak(symbol), static=True)
# Query the stock data during market hours
open_time = dt.datetime.combine(day, dt.time(9, 30), et_timezone)
close_time = dt.datetime.combine(day, dt.time(16, 00), et_timezone)
hist = stock.history(start=open_time, end=close_time, interval="5m")
# Offset the index to be in seconds since the market open
hist.index = hist.index - open_time
peak = hist.High.idxmax()
# Log the stock state over the course of the day
for row in hist.itertuples():
rr.set_time("time", duration=row.Index)
rr.log(f"stocks/{symbol}/{day}", rr.Scalars(row.High))
if row.Index == peak:
rr.log(f"stocks/{symbol}/peaks/{day}", rr.Scalars(row.High))
if __name__ == "__main__":
main()
```
Run the script:
```bash
python stocks.py
```
Without a blueprint, the heuristic layout may not be ideal:
### Creating a simple view
Replace the blueprint section with:
```python
# Create a single chart for all the AAPL data:
blueprint = rrb.Blueprint(
rrb.TimeSeriesView(name="AAPL", origin="/stocks/AAPL"),
)
rr.send_blueprint(blueprint)
```
The `origin` parameter scopes the view to a specific subtree. Now you'll see just the AAPL data:
### Controlling panel state
You can control which panels are visible:
```python
# Create a single chart and collapse the selection and time panels:
blueprint = rrb.Blueprint(
rrb.TimeSeriesView(name="AAPL", origin="/stocks/AAPL"),
rrb.BlueprintPanel(state="expanded"),
rrb.SelectionPanel(state="collapsed"),
rrb.TimePanel(state="collapsed"),
)
rr.send_blueprint(blueprint)
```
### Combining multiple views
Use containers to combine multiple views. The `Vertical` container stacks views, and `row_shares` controls relative sizing:
```python
# Create a vertical layout of an info document and a time series chart
blueprint = rrb.Blueprint(
rrb.Vertical(
rrb.TextDocumentView(name="Info", origin="/stocks/AAPL/info"),
rrb.TimeSeriesView(name="Chart", origin="/stocks/AAPL"),
row_shares=[1, 4],
),
rrb.BlueprintPanel(state="expanded"),
rrb.SelectionPanel(state="collapsed"),
rrb.TimePanel(state="collapsed"),
)
rr.send_blueprint(blueprint)
```
### Specifying view contents
The `contents` parameter provides fine-grained control over what appears in a view. You can include data from multiple sources:
```python
# Create a view with two stock time series
blueprint = rrb.Blueprint(
rrb.TimeSeriesView(
name="META vs MSFT",
contents=[
"+ /stocks/META/2024-03-19",
"+ /stocks/MSFT/2024-03-19",
],
),
rrb.BlueprintPanel(state="expanded"),
rrb.SelectionPanel(state="collapsed"),
rrb.TimePanel(state="collapsed"),
)
rr.send_blueprint(blueprint)
```
### Filtering with expressions
Content expressions can include or exclude subtrees using wildcards. They can reference `$origin` and use `/**` to match entire subtrees:
```python
# Create a chart for AAPL and filter out the peaks:
blueprint = rrb.Blueprint(
rrb.TimeSeriesView(
name="AAPL",
origin="/stocks/AAPL",
contents=[
"+ $origin/**",
"- $origin/peaks/**",
],
),
rrb.BlueprintPanel(state="expanded"),
rrb.SelectionPanel(state="collapsed"),
rrb.TimePanel(state="collapsed"),
)
rr.send_blueprint(blueprint)
```
See [Entity Queries](https://rerun.io/docs/concepts/visualization/entity-queries.md) for complete expression syntax.
### Programmatic layout generation
Since blueprints are Python code, you can generate them dynamically. This example creates a grid with one row per stock symbol:
```python
# Iterate over all symbols and days to create a comprehensive grid
blueprint = rrb.Blueprint(
rrb.Vertical(
contents=[
rrb.Horizontal(
contents=[
rrb.TextDocumentView(
name=f"{symbol}",
origin=f"/stocks/{symbol}/info",
),
]
+ [
rrb.TimeSeriesView(
name=f"{day}",
origin=f"/stocks/{symbol}/{day}",
)
for day in dates
],
name=symbol,
)
for symbol in symbols
]
),
rrb.BlueprintPanel(state="expanded"),
rrb.SelectionPanel(state="collapsed"),
rrb.TimePanel(state="collapsed"),
)
rr.send_blueprint(blueprint)
```
### Saving blueprints from code
You can save programmatically-created blueprints to `.rbl` files:
```python
"""Craft a blueprint with the python API and save it to file."""
import sys
import rerun.blueprint as rrb
path_to_rbl = sys.argv[1]
blueprint = rrb.Blueprint(
rrb.TimeSeriesView(name="AAPL", origin="/stocks/AAPL"),
)
# Save to a file
blueprint.save("rerun_example_blueprint_stocks", path_to_rbl)
```
#### Loading blueprints from any language
Existing blueprint files (e.g. created with the Python SDK or saved from the viewer) can be programmatically loaded into Rerun
This is particularly useful when using Rust or C++ SDKs, since the blueprint API is not yet available for these languages:
```python
"""
Query and display the first 10 rows of a recording in a dataframe view.
The blueprint is being loaded from an existing blueprint recording file.
"""
# python dataframe_view_query_external.py /tmp/dna.rrd /tmp/dna.rbl
import sys
import rerun as rr
path_to_rrd = sys.argv[1]
path_to_rbl = sys.argv[2]
rr.init("rerun_example_dataframe_view_query_external", spawn=True)
rr.log_file_from_path(path_to_rrd)
rr.log_file_from_path(path_to_rbl)
```
This works using the `log_file_from_path` API, which allows you to log any file that contains data that Rerun understands β in this case, blueprint data.
API reference:
- [π Python `log_file_from_path`](https://ref.rerun.io/docs/python/stable/common/logging_functions/#rerun.log_file_from_path)
- [π¦ Rust `log_file_from_path`](https://docs.rs/rerun/latest/rerun/struct.RecordingStream.html#method.log_file_from_path)
- [π C++ `log_file_from_path`](https://ref.rerun.io/docs/cpp/stable/classrerun_1_1RecordingStream.html#a20798d7ea74cce5c8174e5cacd0a2c47)
See the [Blueprint API Reference](https://ref.rerun.io/docs/python/stable/common/blueprint_apis/) for complete details.
### Advanced customization
Blueprints support deep customization of view properties. For example:
```python
# Configure a 3D view with custom camera settings
rrb.Spatial3DView(
name="Robot view",
origin="/world/robot",
background=[100, 149, 237], # Light blue
eye_controls=rrb.EyeControls3D(
kind=rrb.Eye3DKind.FirstPerson,
speed=20.0,
),
)
# Configure a time series view with custom axis and time ranges
rrb.TimeSeriesView(
name="Sensor Data",
origin="/sensors",
axis_y=rrb.ScalarAxis(range=(-10.0, 10.0), zoom_lock=True),
plot_legend=rrb.PlotLegend(visible=False),
time_ranges=[
rrb.VisibleTimeRange(
"time",
start=rrb.TimeRangeBoundary.cursor_relative(seq=-100),
end=rrb.TimeRangeBoundary.cursor_relative(),
),
],
)
```
See [Visualizers and Overrides](https://rerun.io/docs/concepts/visualization/customize-views.md) for information on overriding component values and controlling visualizers from code.
---
## Youtube overview
While some people might want to read through the documentation on this page, others might prefer to watch a video! If you would like to follow along with the Youtube video, you can find the code used in the video below.
```python
from __future__ import annotations
import math
import numpy as np
import rerun as rr
import rerun.blueprint as rrb
from numpy.random import default_rng
rr.init("rerun_blueprint_example", spawn=True)
rr.set_time("time", sequence=0)
rr.log("log/status", rr.TextLog("Application started.", level=rr.TextLogLevel.INFO))
rr.set_time("time", sequence=5)
rr.log("log/other", rr.TextLog("A warning.", level=rr.TextLogLevel.WARN))
for i in range(10):
rr.set_time("time", sequence=i)
rr.log("log/status", rr.TextLog(f"Processing item {i}.", level=rr.TextLogLevel.INFO))
# Create a text view that displays all logs.
blueprint = rrb.Blueprint(
rrb.TextLogView(origin="/log", name="Text Logs"),
rrb.SelectionPanel(state="expanded"),
collapse_panels=True,
)
rr.send_blueprint(blueprint)
input("Press Enter to continueβ¦")
# Create a spiral of points:
n = 150
angle = np.linspace(0, 10 * np.pi, n)
spiral_radius = np.linspace(0.0, 3.0, n) ** 2
positions = np.column_stack((np.cos(angle) * spiral_radius, np.sin(angle) * spiral_radius))
colors = np.dstack((np.linspace(255, 255, n), np.linspace(255, 0, n), np.linspace(0, 255, n)))[0].astype(int)
radii = np.linspace(0.01, 0.7, n)
rr.log("points", rr.Points2D(positions, colors=colors, radii=radii))
# Create a Spatial2D view to display the points.
blueprint = rrb.Blueprint(
rrb.Spatial2DView(
origin="/",
name="2D Scene",
# Set the background color
background=[105, 20, 105],
# Note that this range is smaller than the range of the points,
# so some points will not be visible.
visual_bounds=rrb.VisualBounds2D(x_range=[-5, 5], y_range=[-5, 5]),
),
collapse_panels=True,
)
rr.send_blueprint(blueprint)
input("Press Enter to continueβ¦")
rr.log(
"points",
rr.GeoPoints(
lat_lon=[[47.6344, 19.1397], [47.6334, 19.1399]],
radii=rr.Radius.ui_points(20.0),
),
)
# Create a map view to display the chart.
blueprint = rrb.Blueprint(
rrb.MapView(
origin="points",
name="MapView",
zoom=16.0,
background=rrb.MapProvider.OpenStreetMap,
),
collapse_panels=True,
)
rr.send_blueprint(blueprint)
input("Press Enter to continueβ¦")
blueprint = rrb.Blueprint(
rrb.Grid(
rrb.MapView(
origin="points",
name="MapView",
zoom=16.0,
background=rrb.MapProvider.OpenStreetMap,
),
rrb.Spatial2DView(
origin="/",
name="2D Scene",
# Set the background color
background=[105, 20, 105],
# Note that this range is smaller than the range of the points,
# so some points will not be visible.
visual_bounds=rrb.VisualBounds2D(x_range=[-5, 5], y_range=[-5, 5]),
),
rrb.TextLogView(origin="/log", name="Text Logs"),
),
rrb.TimePanel(state="expanded"),
rrb.BlueprintPanel(state="expanded"),
collapse_panels=True,
)
rr.send_blueprint(blueprint)
blueprint.save("my_favorite_blueprint", "data/blueprint.rbl")
input("Press Enter to continueβ¦")
rr.log("bar_chart", rr.BarChart([8, 4, 0, 9, 1, 4, 1, 6, 9, 0]))
rng = default_rng(12345)
positions = rng.uniform(-5, 5, size=[50, 3])
colors = rng.uniform(0, 255, size=[50, 3])
radii = rng.uniform(0.1, 0.5, size=[50])
rr.log("3dpoints", rr.Points3D(positions, colors=colors, radii=radii))
tensor = np.random.randint(0, 256, (32, 240, 320, 3), dtype=np.uint8)
rr.log("tensor", rr.Tensor(tensor, dim_names=("batch", "x", "y", "channel")))
rr.log(
"markdown",
rr.TextDocument(
"""
# Hello Markdown!
[Click here to see the raw text](recording://markdown:Text).
"""
),
)
rr.log("trig/sin", rr.SeriesLines(colors=[255, 0, 0], names="sin(0.01t)"), static=True)
for t in range(int(math.pi * 4 * 100.0)):
rr.set_time("time", sequence=t)
rr.set_time("timeline1", duration=t)
rr.log("trig/sin", rr.Scalars(math.sin(float(t) / 100.0)))
blueprint = rrb.Blueprint(
rrb.Grid(
rrb.MapView(
origin="points",
name="MapView",
zoom=16.0,
background=rrb.MapProvider.OpenStreetMap,
),
rrb.Spatial2DView(
origin="/",
name="2D Scene",
# Set the background color
background=[105, 20, 105],
# Note that this range is smaller than the range of the points,
# so some points will not be visible.
visual_bounds=rrb.VisualBounds2D(x_range=[-5, 5], y_range=[-5, 5]),
),
rrb.TextLogView(origin="/log", name="Text Logs"),
rrb.BarChartView(origin="bar_chart", name="Bar Chart"),
rrb.Spatial3DView(
origin="/3dpoints",
name="3D Scene",
# Set the background color to light blue.
background=[100, 149, 237],
# Configure the eye controls.
eye_controls=rrb.EyeControls3D(
kind=rrb.Eye3DKind.FirstPerson,
speed=20.0,
),
),
rrb.TensorView(
origin="tensor",
name="Tensor",
# Explicitly pick which dimensions to show.
slice_selection=rrb.TensorSliceSelection(
# Use the first dimension as width.
width=1,
# Use the second dimension as height and invert it.
height=rr.TensorDimensionSelection(dimension=2, invert=True),
# Set which indices to show for the other dimensions.
indices=[
rr.TensorDimensionIndexSelection(dimension=2, index=4),
rr.TensorDimensionIndexSelection(dimension=3, index=5),
],
# Show a slider for dimension 2 only. If not specified, all dimensions in `indices` will have sliders.
slider=[2],
),
# Set a scalar mapping with a custom colormap, gamma and magnification filter.
scalar_mapping=rrb.TensorScalarMapping(colormap="turbo", gamma=1.5, mag_filter="linear"),
# Fill the view, ignoring aspect ratio.
view_fit="fill",
),
rrb.TextDocumentView(origin="markdown", name="Markdown example"),
rrb.TimeSeriesView(
origin="/trig",
# Set a custom Y axis.
axis_y=rrb.ScalarAxis(range=(-1.0, 1.0), zoom_lock=True),
# Configure the legend.
plot_legend=rrb.PlotLegend(visible=False),
# Set time different time ranges for different timelines.
time_ranges=[
# Sliding window depending on the time cursor for the first timeline.
rrb.VisibleTimeRange(
"time",
start=rrb.TimeRangeBoundary.cursor_relative(seq=-100),
end=rrb.TimeRangeBoundary.cursor_relative(),
),
# Time range from some point to the end of the timeline for the second timeline.
rrb.VisibleTimeRange(
"timeline1",
start=rrb.TimeRangeBoundary.absolute(seconds=300.0),
end=rrb.TimeRangeBoundary.infinite(),
),
],
),
),
collapse_panels=True,
)
rr.send_blueprint(blueprint)
```
# Plot any scalar
Rerun can plot numerical data as a time series, even data that wasn't logged with Rerun semantics.
By remapping where a visualizer reads its inputs from, you can separate how you _model_ your data from how you _visualize_ it.
This is useful for plotting custom messages from MCAPs, or data logged via `AnyValues` and `DynamicArchetype`.
As a bonus, logging multiple scalars to the same entity can drastically reduce `.rrd` file sizes.
Each visualizer takes components as input and determines their values from various sources.
By configuring _component mappings_, you can control exactly where each input comes from.
The supported data types are:
- `Float32` and `Float64`
- `Int8`, `Int16`, `Int32`, and `Int64`
- `UInt8`, `UInt16`, `UInt32`, and `UInt64`
- `Boolean`
- Any of the above nested inside of [Arrow structs](https://arrow.apache.org/docs/format/Intro.html#struct).
For background on how visualizers resolve component values, see [Customize views](https://rerun.io/docs/concepts/visualization/customize-views.md).
## Logging custom data
Use `DynamicArchetype` to send data with custom component names alongside regular Rerun data.
Flat arrays and Arrow `StructArray`s are both supported.
This is what the data looks like for the `/plot` entity:
```python
# Custom scalar batch with a cos using a custom component name.
*rr.DynamicArchetype.columns(
archetype="custom",
components={"my_custom_scalar": np.cos(times / 10.0)},
),
# Nested custom scalar batch with a sigmoid inside a struct.
*rr.DynamicArchetype.columns(
archetype="custom",
components={"my_nested_scalar": make_sigmoid_struct_array(64)},
),
```
## Remapping components
A visualizer can source its inputs from any component with a compatible datatype.
For example, the `SeriesLines` visualizer accepts any numerical data for its `Scalar` input.
This works with data from MCAP files, `AnyValues`, or `DynamicArchetype`.
Optional components like `Names` and `Colors` can be sourced similarly from arbitrary data.
The following remaps the `Scalars:scalars` input to read from `custom:my_custom_scalar` instead:
```python
# Green cosine:
# * source scalars from the custom component
# "custom:my_custom_scalar"
# * set the name via an override
# * everything else uses the automatic component mappings,
# so it will pick up colors from the view default.
rr.SeriesLines(names="cosine (custom)").visualizer(
mappings=[
# Map scalars to the custom component.
VisualizerComponentMapping(
target="Scalars:scalars",
source_kind=ComponentSourceKind.SourceComponent,
# Map from custom component
source_component="custom:my_custom_scalar",
),
]
),
```
### Add data by dragging components
You can set up this mapping interactively instead of via the blueprint API: drag a component from the streams tree onto a time series view. If the component has a compatible (numeric) datatype, a new `SeriesLines` visualizer is added that remaps `Scalars:scalars` from it. Non-numeric components (e.g. a string) are rejected, as is dropping a component that the view already plots.
## Selectors for nested data
When your data lives inside an Arrow `StructArray`, use a _selector_ to extract a specific field.
Selectors use a `jq`-inspired syntax (e.g. `.values` to select the `values` field).
Data types are automatically cast when compatible. For example, `Float32` data will be cast to `Float64` as needed by the visualizer.
Here is how to create a nested `StructArray`:
```python
def make_sigmoid_struct_array(steps: int) -> pa.StructArray:
"""Creates a StructArray with a `values` field containing sigmoid data.
Note: We intentionally use float32 here to demonstrate that the data will
be automatically cast to the correct type (float64) when resolved by the
visualizer.
"""
x = np.arange(steps, dtype=np.float32) / 10.0
sigmoid_values = 1.0 / (1.0 + np.exp(-(x - 3.0)))
return pa.StructArray.from_arrays(
[pa.array(sigmoid_values, type=pa.float32())], names=["values"]
)
```
The following remaps the `Scalars:scalars` input to read from `custom:my_nested_scalar` and selects the `values` field:
```python
# Blue sigmoid:
# * source scalars from a nested struct using a selector to
# extract the "values" field
# * set the name and an explicit blue color via overrides
rr.SeriesLines(
names="sigmoid (nested)", colors=[0, 0, 255]
).visualizer(
mappings=[
VisualizerComponentMapping(
target="Scalars:scalars",
source_kind=ComponentSourceKind.SourceComponent,
source_component="custom:my_nested_scalar",
selector=".values",
),
]
),
```
## Providing default values
You can also force a visualizer to use a specific source kind. Setting the source to `Default` makes the visualizer
ignore any store data and use the view's default instead:
```python
# Red sine:
# * set the name via an override
# * explicitly use the view's default for color
# * everything else uses the automatic component mappings,
# so it will pick up scalars from the store.
rr.SeriesLines(names="sine (store)").visualizer(
mappings=[
VisualizerComponentMapping(
target="SeriesLines:colors",
source_kind=ComponentSourceKind.Default,
),
]
),
```
## Full example
The complete example logs three series to a single entity and configures each with a different component mapping strategy. This leads to the following visualizers for the `/plot` entity:
* π [Python](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/component_mapping.py)
* π¦ [Rust](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/component_mapping.rs)
When the view is selected, the selection panel shows an overview of all configured visualizers:
# Visualize fixed-window plots
As of Rerun 0.16, the [TimeSeriesView](https://rerun.io/docs/reference/types/views/time_series_view.md) now supports direct
manipulation of the visible time range. This allows you to create a plot that only shows a fixed window of data.
## VisibleTimeRange
To specify the visible time range, you must pass one or more `VisibleTimeRange` objects to the `time_ranges` parameter of the `TimeSeriesView` blueprint type. If your app only uses a single timeline, you can directly pass a single `VisibleTimeRange` object instead of wrapping it in a list.
The `VisibleTimeRange` object takes three parameters:
- `timeline`: The timeline that the range will apply to. This must match the timeline used to log your data, or if you are only using the rerun-provided timestamps, you can use the strings `"log_time"`, or `"log_tick"`.
- `start`: The start of the visible time range.
- `end`: The end of the visible time range.
The `start` and `end` parameters are set using a `TimeRangeBoundary`:
- To specify an absolute time, you can use the `TimeRangeBoundary.absolute()` method.
- To specify a cursor-relative time, you can use the `TimeRangeBoundary.cursor_relative()` method.
- You can also specify `TimeRangeBoundary.infinite()` to indicate that the start or end of the time range should be unbounded.
In order to account for the different types of timeline (temporal or sequence-based), both the
`TimeRangeBoundary.absolute()` and `TimeRangeBoundary.cursor_relative()` methods can be specified using one of
the keyword args:
- `seconds`/`nanos`: Use these if you called `rr.set_time()` to update the timeline.
- `seq`: Use this if you called `rr.set_time_sequence()` to update the timeline.
## Example syntax
To create a trailing 5 second window plot, you can specify your `TimeSeriesView` like this:
```python
rrb.TimeSeriesView(
origin="plot_path",
time_ranges=rrb.VisibleTimeRange(
timeline="time",
start=rrb.TimeRangeBoundary.cursor_relative(seconds=-5.0),
end=rrb.TimeRangeBoundary.cursor_relative(),
),
)
```
## Full example
For a complete working example, you can run the following code:
```python
#!/usr/bin/env python3
"""A live plot of a random walk using a scrolling fixed window size."""
from __future__ import annotations
import time
import numpy as np
import rerun as rr # pip install rerun-sdk
import rerun.blueprint as rrb
rr.init("rerun_example_fixed_window_plot", spawn=True)
rr.send_blueprint(
rrb.TimeSeriesView(
origin="random_walk",
time_ranges=rrb.VisibleTimeRange(
"time",
start=rrb.TimeRangeBoundary.cursor_relative(seconds=-5.0),
end=rrb.TimeRangeBoundary.cursor_relative(),
),
),
)
cur_time = time.time()
value = 0.0
while True:
cur_time += 0.01
sleep_for = cur_time - time.time()
if sleep_for > 0:
time.sleep(sleep_for)
value += np.random.normal()
rr.set_time("time", timestamp=cur_time)
rr.log("random_walk", rr.Scalars(value))
```
This should create a plot that only shows the last 5 seconds of data. If you select the view, you should
see that the time range is configured as expected.
Alternatively, you can check out a more full-featured example with multiple plot windows [here](https://github.com/rerun-io/rerun/tree/latest/examples/python/live_scrolling_plot).
## Additional notes
- Any time you log data, it has two timepoints associated with it: "log_time", and "log_tick".
# Component mappings
By default, each visualizer reads its input components from the - for example, the `Points3D` visualizer reads colors from `Points3D:colors`.
**Component mappings** let you override this, redirecting any visualizer input to a different component on the same entity. This makes it possible to store multiple variants of the same data and switch between them per view.
To learn more about how visualizers are set up in general, also have a look at the [concept page on customizing Views](https://rerun.io/docs/concepts/visualization/customize-views).
This guide uses a point cloud with two color sets as a running example, but the same technique works for any component!
## Full example
You can find the full example here:
* π [Python](https://github.com/rerun-io/rerun/blob/latest/docs/snippets/all/howto/dual_color_point_cloud.py)
* π¦ [Rust](https://github.com/rerun-io/rerun/blob/latest/docs/snippets/all/howto/dual_color_point_cloud.rs)
## How it works
### Storing multiple component variants with custom archetypes
A standard archetype like `Points3D` assigns a fixed column name, as well as component type & archetype metainformation, to every component it logs (`Points3D:colors`, `Points3D:positions`, etc.).
To store additional variants of a component on the same entity, log them as **custom archetypes**. The easiest way to do this is to use the `DynamicArchetype` utility.
```python
# --- Log positions once, then each color set as a separate custom archetype ---
rr.log(
"pointcloud",
rr.Points3D(positions, radii=0.06),
rr.DynamicArchetype(
"HeightColors",
components={"colors": rr.components.ColorBatch(height_rgba)},
),
rr.DynamicArchetype(
"SpinColors", components={"colors": rr.components.ColorBatch(spin_rgba)}
),
)
```
After this call, the entity's component list contains four components:
- `Points3D:positions`, `Points3D:radii` β from the standard archetype
- `HeightColors:colors` β the first custom variant
- `SpinColors:colors` β the second custom variant
You can inspect this in the viewer by selecting the entity in the streams panel:
### Configuring component mappings in blueprint
To make a visualizer read from an arbitrary source, we need to explicitly set the **component mapping** for a visualizer.
The mapping specifies a *target* (the component the visualizer expects) and a *source* (the component to actually read from).
Everything that isn't explicitly mapped keeps its default behavior.
```python
# --- Blueprint: two side-by-side 3D views with different color mappings ---
blueprint = rrb.Blueprint(
rrb.Horizontal(
rrb.Spatial3DView(
name="Height Colors",
origin="/",
overrides={
"pointcloud": [
rr.Points3D.from_fields().visualizer(
mappings=[
VisualizerComponentMapping(
target="Points3D:colors",
source_kind=ComponentSourceKind.SourceComponent,
source_component="HeightColors:colors",
),
]
),
],
},
),
rrb.Spatial3DView(
name="Spin Colors",
origin="/",
overrides={
"pointcloud": [
rr.Points3D.from_fields().visualizer(
mappings=[
VisualizerComponentMapping(
target="Points3D:colors",
source_kind=ComponentSourceKind.SourceComponent,
source_component="SpinColors:colors",
),
]
),
],
},
),
),
collapse_panels=True,
)
rr.send_blueprint(blueprint)
```
In this example, each view overrides only the color source for the `Points3D` visualizer - positions, radii, and everything else are still read from their default sources automatically.
In the viewer you can access and this in the visualizer settings, presented when selecting an entity in a view:
### Configuring component mappings in the UI
You can set up the same mappings interactively without writing any blueprint code:
- Add two 3D views (or clone an existing one)
- **Select the entity** in one of the views.
- In the selection panel, find the visualizer section (e.g. **Points3D**).
- Click on the component row you want to remap (e.g. **colors**) to expand the component mapping options.
- Change the source from the default to the desired custom archetype component (e.g. `HeightColors:colors`).
# Visualize geospatial data
Rerun 0.20 introduced a new [map view](https://rerun.io/docs/reference/types/views/map_view.md).
This guide provides a short overview on how to use it to visualize geospatial data.
## Coordinate system
The map view uses the [ESPG:3857](https://epsg.io/3857) [spherical mercator projection](https://en.wikipedia.org/wiki/Web_Mercator_projection) commonly used by web services such as [OpenStreetMap](https://www.openstreetmap.org/).
This enables the use of commonly available web tiles for the background map.
To be compatible with this view, geospatial data must be expressed using [ESPG:4326](https://epsg.io/4326) (aka WGS84) latitudes and longitudes.
This corresponds to what is commonly referred to as "GPS coordinates."
Rerun provides a set of archetypes prefixed with `Geo` designed to encapsulate such data.
For example, [`GeoPoints`](https://rerun.io/docs/reference/types/archetypes/geo_points.md) represent a single geospatial location (or a batch thereof). The location of the Eiffel Tower can be logged as follows:
```python
rr.log("eiffel_tower", rr.GeoPoints(lat_lon=[48.858222, 2.2945]))
```
Both the latitude and longitude must be provided in degrees, with positive values corresponding to the North, resp. East directions.
Note that Rerun always expects latitudes first and longitudes second.
As there is [no accepted ordering standard](https://stackoverflow.com/questions/7309121/preferred-order-of-writing-latitude-longitude-tuples-in-gis-services), our APIs strive to make this ordering choice as explicit as possible.
In this case, the `lat_lon` argument is keyword-only and must thus be explicitly named as a reminder of this order.
## Types of geometries
Rerun currently supports two types of geometries:
- [`GeoPoints`](https://rerun.io/docs/reference/types/archetypes/geo_points.md): batch of individual points, with optional [radius](https://rerun.io/docs/reference/types/components/radius.md) and [color](https://rerun.io/docs/reference/types/components/color.md)
- [`GeoLineStrings`](https://rerun.io/docs/reference/types/archetypes/geo_line_strings.md): batch of line strings, with optional [radius](https://rerun.io/docs/reference/types/components/radius.md) and [color](https://rerun.io/docs/reference/types/components/color.md)
> [!NOTE]
> Polygons are planned but are not supported yet (see [this issue](https://github.com/rerun-io/rerun/issues/8066)).
As in other views, radii may be expressed either as UI points (negative values) or scene units (positive values).
For the latter case, the map view uses meters are scene units.
Apart from the use of latitude and longitude, `GeoPoints` and `GeoLineStrings` are otherwise similar to the [`Points2D`](https://rerun.io/docs/reference/types/archetypes/points2d.md) and [`LineStrip2D`](https://rerun.io/docs/reference/types/archetypes/line_strips2d.md) archetypes used in the [2D view](https://rerun.io/docs/reference/types/views/spatial2d_view.md).
The map view supports several types of background maps, including a few from [Mapbox](https://www.mapbox.com).
A Mapbox access token is required to use them.
It must be provided either using the `RERUN_MAPBOX_ACCESS_TOKEN` environment variable or configured in the settings screen ("Settingsβ¦" item in the Rerun menu).
An access token may be freely obtained by creating a Mapbox account.
## Creating a map view from code
Like other views, the map view can be configured using the [blueprint API](https://rerun.io/docs/getting-started/configure-the-viewer/navigating-the-viewer.md):
```python
import rerun.blueprint as rrb
blueprint = rrb.Blueprint(
rrb.MapView(
origin="/robot/position",
name="map view",
zoom=16.0,
background=rrb.MapProvider.OpenStreetMap,
),
)
```
Check the [map view](https://rerun.io/docs/reference/types/views/map_view.md) reference for details.
# Use multiple native viewers
You can run multiple Native Viewer windows simultaneously, each displaying different data or views of the same data.
## How it works
Every Native Viewer binds to a gRPC port on startup. By default, this is port `9876`. When you run `rerun`, it checks if a viewer is already listening on that port:
- **If yes**: it connects to the existing viewer (or sends data to it)
- **If no**: it starts a new viewer on that port
To open multiple viewer windows, use different ports with the `--port` flag.
## Examples
```sh
# Start a viewer on the default port (9876)
$ rerun &
# This does nothing β a viewer is already running on :9876
$ rerun &
# Start a second viewer on port 6789
$ rerun --port 6789 &
# Log an image to the first viewer (port 9876)
$ rerun image.jpg
# Log an image to the second viewer (port 6789)
$ rerun --port 6789 image.jpg
```
## From the SDK
When using `connect_grpc()` from the SDK, specify the port to target a specific viewer:
```python
import rerun as rr
# Connect to viewer on default port
rr.init("rerun_example_demo")
rr.connect_grpc()
# Or connect to a specific port
rr.connect_grpc("rerun+http://127.0.0.1:6789")
```
## Tips
- Use `spawn()` to automatically start a new viewer if needed β it will reuse an existing viewer on the default port if one is running
- Each viewer maintains its own Chunk Store, so data sent to different viewers is independent
- The Web Viewer doesn't use gRPC ports the same way β it connects via WebSocket when served locally
# Limit the viewer's memory usage
### --memory-limit
The Rerun Viewer can not yet view more data than fits in RAM. The more data you log, the more RAM the Rerun Viewer will use. When it reaches a certain limit, the oldest data will be dropped. The default limit is to use up to 75% of the total system RAM.
You can set the limit with the `--memory-limit` command-line argument, or the `memory_limit` argument of [`rr.spawn`](https://ref.rerun.io/docs/python/stable/common/initialization_functions/#rerun.spawn).
Alternatively, you can adjust the limit for an active session also in the viewer's settings. It will be reset to the default the next time you open the viewer.
# Visualize state changes
The [StateTimelineView](https://rerun.io/docs/reference/types/views/state_timeline_view.md) shows how entities transition between discrete states over time. Each entity becomes a horizontal lane, and each logged state is rendered as a colored band that runs until the next change. This is a good fit for state machines, mode transitions, sensor health, or any other piece of data that's better described as "what state am I in right now?" than as a numerical value.
## Logging state changes
Use [`StateChange`](https://rerun.io/docs/reference/types/archetypes/state_change.md) to log a transition. Each call marks the start of a new state at the current time; the previous state implicitly ends. The state value is a string, so you can use any label that's meaningful for your application.
```python
# Log state transitions for two entities. Each call marks the start of a new
# state; the previous state implicitly ends. The `/door` lane uses the
# `StateConfiguration` above, while `/window` gets default styling (raw value
# as label, hashed color).
rr.set_time("step", sequence=0)
rr.log("door", rr.StateChange(state="open"))
rr.log("window", rr.StateChange(state="closed"))
rr.set_time("step", sequence=1)
rr.log("door", rr.StateChange(state="closed"))
rr.set_time("step", sequence=3)
rr.log("window", rr.StateChange(state="open"))
rr.set_time("step", sequence=4)
rr.log("door", rr.StateChange(state="open"))
```
### Notes
- The view groups state changes by entity path, so logging to `/door` and `/window` produces two separate lanes.
- Logging the same state value twice in a row is a no-op for visualization, only transitions to a different value start a new phase.
- Each phase runs from its `StateChange` time to the next `StateChange` time on the same entity. The final phase extends indefinitely.
## Customizing labels, colors, and visibility
To override the default styling, log a [`StateConfiguration`](https://rerun.io/docs/reference/types/archetypes/state_configuration.md) to the same entity. `values`, `labels`, `colors`, and `visible` are parallel arrays β index `i` of each describes the same state value. Anything you don't provide falls back to the default (raw value as label, hashed color, visible).
It is usually best to log `StateConfiguration` as static, since it describes how to display values rather than a moment in time.
```python
# Customize how each state value is displayed (label, color, visibility).
# Log as static so the configuration applies for the entire recording.
rr.log(
"door",
rr.StateConfiguration(
values=["open", "closed"],
labels=["Open", "Closed"],
colors=[0x4CAF50FF, 0xEF5350FF],
),
static=True,
)
```
## Visualize any component as state
You don't have to log [`StateChange`](https://rerun.io/docs/reference/types/archetypes/state_change.md) to use this view. Any component whose data is string-, boolean-, or number-like can drive a lane by **remapping** the visualizer's `StateChange:state` input to read from it instead. This lets you separate how you _model_ your data from how you _visualize_ it. For example, visualizing a robot mode that you logged as a plain string via `AnyValues` or `DynamicArchetype` (the same idea as [Plot any scalar](https://rerun.io/docs/howto/visualization/plot-any-scalar.md), applied to the state slot).
The supported source data types are:
- `Utf8` and `LargeUtf8` (rendered as string states)
- `Boolean` (rendered as two states)
- `Int8`, `Int16`, `Int32`, `Int64`, `UInt8`, `UInt16`, `UInt32`, `UInt64`, `Float16`, `Float32`, and `Float64` (rendered as numeric states)
For background on how visualizers resolve their inputs, see [Component mappings](https://rerun.io/docs/howto/visualization/component-mappings.md) and [Customize views](https://rerun.io/docs/concepts/visualization/customize-views.md).
For example, log a robot mode as a plain string component:
```python
# Log a robot mode as a plain string component β note that this is *not* a
# `StateChange`, just an arbitrary string logged via `AnyValues`. It shows up on
# the entity as the component `AnyValues:mode`.
modes = ["booting", "idle", "driving", "idle", "charging"]
for step, mode in enumerate(modes):
rr.set_time("step", sequence=step)
rr.log("robot", rr.AnyValues(mode=mode))
```
Then point the state-timeline visualizer at it by remapping `StateChange:state`:
```python
# Remap the state-timeline visualizer's `StateChange:state` input to read from
# the custom `AnyValues:mode` component instead of an actual `StateChange`.
# Any string, boolean, or numeric component can be visualized this way.
blueprint = rrb.Blueprint(
rrb.StateTimelineView(
origin="/",
name="Robot mode",
overrides={
"robot": [
rr.StateChange.from_fields().visualizer(
mappings=[
VisualizerComponentMapping(
target="StateChange:state",
source_kind=ComponentSourceKind.SourceComponent,
source_component="AnyValues:mode",
),
],
),
],
},
),
)
rr.send_blueprint(blueprint)
```
### Add data by dragging components
You can set up the same mapping interactively: drag a component from the streams tree onto a State Timeline view. If the component is a compatible source (string, boolean, or numeric), a new lane is added that remaps `StateChange:state` from it. Incompatible components (e.g. a blob or tensor) are rejected, as is dropping a component that the view already visualizes.
## Setting up the view via blueprint
The State Timeline view is also created automatically when `StateChange` data is present, but you can also configure it explicitly via the blueprint API:
```python
# Place a state timeline view at the root. The viewer will create one
# automatically as soon as it sees `StateChange` data, but the blueprint API
# lets you control the origin, name, and layout explicitly.
blueprint = rrb.Blueprint(
rrb.StateTimelineView(origin="/", name="Doors and windows"),
)
rr.send_blueprint(blueprint)
```
# React to events in the Viewer
We support registering callbacks to Viewer events in these environments:
- Web browsers, through our [JS package](https://rerun.io/docs/howto/integrations/embed-web.md)
- Jupyter notebooks, through our [Notebook API](https://rerun.io/docs/howto/integrations/embed-notebooks.md)
For users extending the Viewer through the Rust [`re_viewer`](https://docs.rs/re_viewer/latest/re_viewer/) crate, there are two options:
- Use [`StartupOptions.on_event`](https://docs.rs/re_viewer/latest/re_viewer/struct.StartupOptions.html#structfield.on_event) to register
the same events available on the web and in Jupyter.
- [Extend the UI](https://github.com/rerun-io/rerun/tree/main/examples/rust/custom_callback) to add your own widgets using `egui`, and
fire completely custom events.
# Implement custom visualizations (Rust only)
There are three ways to extend the Rerun Viewer with custom Rust code, depending on how deep you need to go:
embedding custom UI panels alongside the Viewer, adding a custom visualizer to a built-in view, or implementing an entirely new view class.
> [!WARNING]
> The interfaces for extending the Viewer are not yet stable. Expect code implementing custom extensions to break with every release of Rerun.
## Embedding custom UI in the Viewer

In the above screenshot you see the example [`extend_viewer_ui`](https://github.com/rerun-io/rerun/tree/main/examples/rust/extend_viewer_ui), which contains the Rerun Viewer to the left and a custom panel to the right. In this example the panel contains a hierarchical text view of the loaded data.
### How to build it
The Rerun Viewer is defined by the crate [`re_viewer`](https://github.com/rerun-io/rerun/tree/main/crates/viewer/re_viewer). It uses the popular Rust GUI library [`egui`](https://github.com/emilk/egui) (written by our CTO) and its framework [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe). To extend the UI you need to create your own `eframe` application and embed `re_viewer` inside of it. You can then use `egui` to add custom panels and windows.
The best way to get started is by reading [the source code of the `extend_viewer_ui` example](https://github.com/rerun-io/rerun/tree/main/examples/rust/extend_viewer_ui).
## Custom visualizers for built-in views
You can register custom visualizers with existing built-in views (such as the 3D Spatial View) without having to implement an entire view class from scratch. This is the right approach when your data fits naturally into an existing view but needs custom rendering. A custom visualizer typically consists of a custom archetype, a visualizer system, and optionally a custom GPU renderer.
The [`custom_visualizer`](https://github.com/rerun-io/rerun/tree/main/examples/rust/custom_visualizer) example demonstrates this by adding a GPU-rendered heightfield to the built-in 3D Spatial View. See its README for a detailed walkthrough of the three parts involved.
## Custom view classes
If you need a completely new kind of view (not just a new visualizer within an existing view), you can implement a custom view class.
The [`custom_view`](https://github.com/rerun-io/rerun/tree/main/examples/rust/custom_view) example demonstrates how to add a fully custom View class to Rerun on startup.
Views that are added this way have access to the exact same interfaces as all other Views,
meaning that any of the built-in Views can serve as an additional example on how to implement Views.
The best way to get started is by reading [the source code of the `custom_view` example](https://github.com/rerun-io/rerun/tree/main/examples/rust/custom_view).
## Future work
In the future we'll allow embedding your own GUI widgets inside existing views.
Beyond that we want to open customizability to more languages and support adding ui elements with [callbacks](https://github.com/rerun-io/rerun/issues/2691)
via blueprint definitions.
For more information check https://github.com/rerun-io/rerun/issues/3087
# Dataset Resampling
This snippet demonstrates how to resample a dataset based on the time index of one component
within your data. This is particularly helpful when you have data that is produced at very
different frequencies.
First, load a dataset to use for evaluation:
```python
sample_dataset_path = (
Path(__file__).parents[4] / "tests" / "assets" / "rrd" / "dataset"
)
server = rr.server.Server(datasets={"dataset": sample_dataset_path})
# Using OSS server for demonstration but in practice replace with
# the URL of your cloud instance
CATALOG_URL = server.url()
client = rr.catalog.CatalogClient(CATALOG_URL)
dataset = client.get_dataset(name="dataset")
```
## Investigate time ranges
Before we do the resampling, we can examine the dataset's time ranges using the function
`get_index_ranges()`. This is not strictly necessary for the resampling work to follow, but
it can be helpful during investigation of your data. This will show you the start and
end values for all indexes in your dataset, one per segment.
```python
(
dataset
.get_index_ranges()
.select(
"rerun_segment_id",
"time_1:start",
"time_1:end",
"time_2:start",
"time_2:end",
"time_3:start",
"time_3:end",
)
.sort("rerun_segment_id")
.show()
)
```
## Prior to resampling
The sample data we have loaded is very basic, but it demonstrates having components from
three different entities at different times in the dataset. The code below demonstrates
what the data looks like before resampling. In order to do data analysis on this DataFrame
you would likely need to do some aggregation or window across the time index.
```python
time_index = "time_3"
columns_of_interest = [
"rerun_segment_id",
time_index,
"/obj1:Points3D:positions",
"/obj2:Points3D:positions",
"/obj3:Points3D:positions",
]
(
dataset
.reader(index=time_index)
.select(*columns_of_interest)
.sort("rerun_segment_id", time_index)
.show()
)
# +----------------------------------+--------+--------------------------+--------------------------+--------------------------+
# | rerun_segment_id | time_3 | /obj1:Points3D:positions | /obj2:Points3D:positions | /obj3:Points3D:positions |
# +----------------------------------+--------+--------------------------+--------------------------+--------------------------+
# | 141a866deb2d49f69eb3215e8a404ffc | 1 | [[49.0, 0.0, 0.0]] | [[44.0, 1.0, 0.0]] | [[1.0, 2.0, 0.0]] |
# | 141a866deb2d49f69eb3215e8a404ffc | 2 | [[27.0, 0.0, 0.0]] | [[42.0, 1.0, 0.0]] | |
# | 141a866deb2d49f69eb3215e8a404ffc | 3 | [[25.0, 0.0, 0.0]] | [[30.0, 1.0, 0.0]] | [[3.0, 2.0, 0.0]] |
# | 141a866deb2d49f69eb3215e8a404ffc | 4 | [[38.0, 0.0, 0.0]] | [[19.0, 1.0, 0.0]] | |
# | 141a866deb2d49f69eb3215e8a404ffc | 5 | [[17.0, 0.0, 0.0]] | [[5.0, 1.0, 0.0]] | [[5.0, 2.0, 0.0]] |
# | 141a866deb2d49f69eb3215e8a404ffc | 6 | [[2.0, 0.0, 0.0]] | [[35.0, 1.0, 0.0]] | |
# | 141a866deb2d49f69eb3215e8a404ffc | 7 | [[44.0, 0.0, 0.0]] | [[4.0, 1.0, 0.0]] | [[7.0, 2.0, 0.0]] |
```
## Resampled data
The snippet below demonstrates resampling using two lines. First we create a new DataFrame
which contains the index values we care about per segment. It is *very* important in
doing this that you do not set `fill_latest_at=True`. Otherwise it would negate the effect
we are trying to produce where we only have rows for which we have data in our component
of interest. The required output of this DataFrame is only the segment ID and the index
value.
Once we have a DataFrame with these index values, we can now query the dataset using that
DataFrame. You can see from the output below that we generate one row per time index for
which the component of interest is not null.
```python
resample_column = "/obj3:Points3D:positions"
times_of_interest = (
dataset
.reader(index=time_index)
.filter(col(resample_column).is_not_null())
.select("rerun_segment_id", time_index)
)
(
dataset
.reader(
index=time_index,
using_index_values=times_of_interest,
fill_latest_at=True,
)
.select(*columns_of_interest)
.sort("rerun_segment_id", time_index)
.show()
)
# +----------------------------------+--------+--------------------------+--------------------------+--------------------------+
# | rerun_segment_id | time_3 | /obj1:Points3D:positions | /obj2:Points3D:positions | /obj3:Points3D:positions |
# +----------------------------------+--------+--------------------------+--------------------------+--------------------------+
# | 141a866deb2d49f69eb3215e8a404ffc | 1 | [[49.0, 0.0, 0.0]] | [[44.0, 1.0, 0.0]] | [[1.0, 2.0, 0.0]] |
# | 141a866deb2d49f69eb3215e8a404ffc | 3 | [[25.0, 0.0, 0.0]] | [[30.0, 1.0, 0.0]] | [[3.0, 2.0, 0.0]] |
# | 141a866deb2d49f69eb3215e8a404ffc | 5 | [[17.0, 0.0, 0.0]] | [[5.0, 1.0, 0.0]] | [[5.0, 2.0, 0.0]] |
# | 141a866deb2d49f69eb3215e8a404ffc | 7 | [[44.0, 0.0, 0.0]] | [[4.0, 1.0, 0.0]] | [[7.0, 2.0, 0.0]] |
# | 141a866deb2d49f69eb3215e8a404ffc | 10 | [[12.0, 0.0, 0.0]] | [[6.0, 1.0, 0.0]] | [[10.0, 2.0, 0.0]] |
# | 141a866deb2d49f69eb3215e8a404ffc | 12 | [[13.0, 0.0, 0.0]] | [[17.0, 1.0, 0.0]] | [[12.0, 2.0, 0.0]] |
# | 141a866deb2d49f69eb3215e8a404ffc | 13 | [[20.0, 0.0, 0.0]] | [[32.0, 1.0, 0.0]] | [[13.0, 2.0, 0.0]] |
```
# Creating sub-datasets
When experimenting with new features it's often practical to work with a subset of data without modifying the original.
A sub-dataset references the same underlying RRD files so no data is copied.
The dependencies in this example are contained in `rerun-sdk[all]`.
## Setup
Simplified setup to launch the local server for demonstration.
In practice you'll connect to your cloud instance.
```python
from __future__ import annotations
from pathlib import Path
import pyarrow as pa
import pyarrow.compute as pc
from datafusion import col, lit
from datafusion import functions as F
import rerun as rr
sample_5_path = (
Path(__file__).parents[4] / "tests" / "assets" / "rrd" / "sample_5"
)
server = rr.server.Server(datasets={"sample_dataset": sample_5_path})
CATALOG_URL = server.url()
client = rr.catalog.CatalogClient(CATALOG_URL)
source_dataset = client.get_dataset(name="sample_dataset")
```
## Helper function
Query the source dataset's [manifest](https://rerun.io/docs/concepts/query-and-transform/catalog-object-model.md) for storage URLs per (segment, layer) pair and re-register them into a new dataset.
```python
def create_sub_dataset(
client: rr.catalog.CatalogClient,
source: rr.catalog.DatasetEntry,
name: str,
segment_ids: list[str],
) -> rr.catalog.DatasetEntry:
"""Create a new dataset with a subset of segments from another dataset."""
# Look up the storage URLs of the selected segments.
selected = pa.table(
source
.segment_table()
.filter(
F.in_list(col("rerun_segment_id"), [lit(s) for s in segment_ids])
)
.select("rerun_storage_urls", "rerun_layer_names")
)
sub_dataset = client.create_dataset(name)
# Flatten the per-segment lists into the (url, layer) pairs to register.
uris = pc.list_flatten(selected.column("rerun_storage_urls")).to_pylist()
layers = pc.list_flatten(selected.column("rerun_layer_names")).to_pylist()
if uris:
sub_dataset.register(uris, layer_name=layers).wait()
return sub_dataset
```
## Selecting segments
Select segments by any criteria β a hardcoded list, a slice, or a filtered query based on segment properties or metadata joins.
```python
# View available segments
print("Available segments:")
print(
source_dataset
.segment_table()
.select("rerun_segment_id")
.sort("rerun_segment_id")
)
# Select a subset β here we pick the first 3 segments.
all_segment_ids = source_dataset.segment_ids()
subset_ids = all_segment_ids[:3]
```
## Creating the sub-dataset
```python
sub_dataset = create_sub_dataset(
client, source_dataset, "my_experiment", subset_ids
)
```
## Verifying the result
```python
print("\nSub-dataset segments:")
print(
sub_dataset
.segment_table()
.select("rerun_segment_id", "rerun_layer_names")
.sort("rerun_segment_id")
)
print("\nSub-dataset storage URLs:")
print(
sub_dataset
.segment_table()
.select("rerun_segment_id", "rerun_layer_names", "rerun_storage_urls")
.sort("rerun_segment_id")
)
```
## Cleanup
Delete the sub-dataset when it is no longer needed.
This only removes the dataset entry from the catalog. The underlying RRD storage is not affected.
```python
# When done experimenting, delete the sub-dataset.
# This only removes the dataset entry β the underlying RRD storage is not
# affected.
sub_dataset.delete()
```
# Common Dataframe Operations
Dataframes are core to modern analytics workflows.
Rerun provides a dataframe interface to your data via [DataFusion](https://datafusion.apache.org/python/).
This example performs a series of joins, filters, etc that highlight a variety of common operations in context.
Because datafusion has a lazy execution model it is generally more performant to use datafusion for processing,
however datafusion does allow conversion to dataframes for popular tools (pandas, polars, pyarrow).
The dependencies in this example are contained in `rerun-sdk[all]`.
## Setup
Perform initial import and spawn local server for demonstration.
In practice you'll connect to your cloud instance.
```python
from __future__ import annotations
from pathlib import Path
import datafusion as dfn
import numpy as np
import pyarrow as pa
from datafusion import col, lit
from datafusion import functions as F
import rerun as rr
sample_5_path = (
Path(__file__).parents[4] / "tests" / "assets" / "rrd" / "sample_5"
)
server = rr.server.Server(datasets={"sample_dataset": sample_5_path})
CATALOG_URL = server.url()
client = rr.catalog.CatalogClient(CATALOG_URL)
dataset = client.get_dataset(name="sample_dataset")
observations = dataset.filter_contents(["/observation/**"]).reader(
index="real_time"
)
```
## Group-by / aggregation
Perform an aggregation on the episodes to track the first and last timestamp for the columns of interest.
```python
first_last = observations.aggregate(
col("rerun_segment_id"),
[
F.first_value(col("real_time")).alias("start"),
F.last_value(col("real_time")).alias("end"),
],
)
# Sort for consistency here
first_last = first_last.sort("start")
pa.table(first_last)["start"][0]
```
## Join and query
Some of our columns start much later than others.
Find out how often this delay exceeds some threshold.
> [!WARNING]
> **Performance warning:**
> Even though datafusion pulls data lazily, we don't currently decouple our payload from its timeline.
> E.g. in this example this means that we have to pull the full camera data to inspect their min/max timestamps.
> This works quickly when the data is already local and in memory, but can be a bottleneck on cloud at scale.
```python
joints = dataset.filter_contents(["/observation/joint_positions"])
# Find the earliest joint position in each episode (cast to unix epoch
# nanoseconds for easier math later)
joint_min_t = (
joints
.reader(index="real_time")
.with_column("joint_epoch_ns", col("real_time").cast(pa.int64()))
.select("rerun_segment_id", "joint_epoch_ns")
.aggregate(
col("rerun_segment_id"),
F.min(col("joint_epoch_ns")).alias("joint_min_t"),
)
)
cameras = dataset.filter_contents(["/camera/**"])
# Find the earliest camera frame in each episode (cast to unix epoch
# nanoseconds for easier math later)
camera_min_t = (
cameras
.reader(index="real_time")
.with_column("camera_epoch_ns", col("real_time").cast(pa.int64()))
.select(
"rerun_segment_id",
col("real_time").cast(pa.int64()).alias("camera_epoch_ns"),
)
.aggregate(
col("rerun_segment_id"),
F.min(col("camera_epoch_ns")).alias("camera_min_t"),
)
)
# Join the two dataframes
min_t = camera_min_t.join(
joint_min_t.with_column_renamed("rerun_segment_id", "segment_id"),
left_on="rerun_segment_id",
right_on="segment_id",
how="left",
)
delta_t = min_t.select(
col("rerun_segment_id"),
(col("camera_min_t") - col("joint_min_t")).alias("start_delta_t"),
)
THRESHOLD_S = 1
NANO_S = 1_000_000_000
outliers = delta_t.filter(
dfn.Expr.between(
col("start_delta_t"),
-THRESHOLD_S * NANO_S,
THRESHOLD_S * NANO_S,
negated=True,
),
)
outliers = outliers.with_column(
"start_delta_t_s", col("start_delta_t") / 1_000_000_000.0
)
print(
f"{outliers.count()=}\n",
f"{joint_min_t.count()=}\n",
f"{camera_min_t.count()=}",
sep="",
)
```
## Extract sub-episodes from recording
Oftentimes a recording will capture multiple episodes.
For instance a robotic arm may place multiple items, where each item could be considered an episode.
This example looks for contiguous time ranges where the gripper opens and closes in order to separate these sub-episodes for further downstream processing.
```python
# Grab a dataframe
all_data = (
dataset
.filter_contents(["/action/**", "/observation/**"])
.reader(index="real_time", fill_latest_at=True)
.filter(
col(
"/observation/joint_positions:Scalars:scalars"
).is_not_null() # filter out rows where there is no observation
)
)
# Drop heavy columns for performance
light_slice = all_data.select(
"rerun_segment_id",
"real_time",
"/observation/gripper_position:Scalars:scalars",
)
# Define criteria for sub-episode start/end
THRESHOLD = 0.1
light_slice = light_slice.with_column(
"gripper_open",
col("/observation/gripper_position:Scalars:scalars") > [THRESHOLD],
)
# Find start and end
light_slice = light_slice.with_column(
"prev_gripper_open",
F.lag(
col("gripper_open"),
default_value=False,
partition_by=[col("rerun_segment_id")],
order_by=[col("real_time")],
),
)
light_slice = light_slice.with_column(
"gripper_change",
col("gripper_open").cast(pa.int8())
- col("prev_gripper_open").cast(pa.int8()),
)
slice_times = light_slice.with_column(
"start",
F
.case(col("gripper_change"))
.when(lit(1), col("real_time"))
.otherwise(lit(None)),
).with_column(
"end",
F
.case(col("gripper_change"))
.when(lit(-1), col("real_time"))
.otherwise(lit(None)),
)
# Helper because pyarrow timestamps didn't have a nice min/max utility
max_ts = pa.scalar(np.iinfo(np.int64).max, type=pa.timestamp("ns"))
min_ts = pa.scalar(
np.iinfo(np.int64).min + 1_000_000_000, type=pa.timestamp("ns")
)
# This generates the column for the last observed start time
slice_dense_times = (
slice_times
.select("rerun_segment_id", "real_time", "start", "end")
.with_column(
"dense_start",
F.last_value(col("start")).over(
dfn.expr.Window(
window_frame=dfn.expr.WindowFrame("rows", None, 0),
order_by=col("real_time"),
partition_by=col("rerun_segment_id"),
null_treatment=dfn.common.NullTreatment.IGNORE_NULLS,
)
),
)
.fill_null(value=max_ts, subset=["dense_start"])
)
# This generates the column for the next observed end time (by finding the
# last_value in reversed order)
slice_dense_times = slice_dense_times.with_column(
"dense_end",
F.last_value(col("end")).over(
dfn.expr.Window(
window_frame=dfn.expr.WindowFrame("rows", None, 0),
order_by=col("real_time").sort(ascending=False),
partition_by=col("rerun_segment_id"),
null_treatment=dfn.common.NullTreatment.IGNORE_NULLS,
)
),
).fill_null(value=min_ts, subset=["dense_end"])
slice_dense_times = slice_dense_times.select(
"rerun_segment_id", "real_time", "dense_start", "dense_end"
)
sub_episodes = slice_dense_times.filter(
dfn.Expr.between(col("real_time"), col("dense_start"), col("dense_end")),
)
print(f"{sub_episodes.count()=}")
```
# Query video streams
Video streams provide the best compression ratio for camera feeds, but require special handling when querying data back from a catalog server.
For more details about the different video types we support see our [video reference](https://rerun.io/docs/concepts/logging-and-ingestion/video.md).
This guide focuses on querying [`VideoStream`](https://rerun.io/docs/reference/types/archetypes/video_stream.md) data from a catalog server,
including how to decode individual frames and how to export entire streams to MP4 files.
The dependencies in this example require `rerun-sdk[all]` and `av` for video decoding.
## Setup
Simplified setup to launch the local server for demonstration.
In practice you'll connect to your cloud instance.
```python
from fractions import Fraction
from io import BytesIO
from pathlib import Path
import av
import numpy as np
import pyarrow as pa
from datafusion import col
import rerun as rr
sample_video_path = (
Path(__file__).parents[4] / "tests" / "assets" / "rrd" / "video_sample"
)
server = rr.server.Server(datasets={"video_dataset": sample_video_path})
CATALOG_URL = server.url()
client = rr.catalog.CatalogClient(CATALOG_URL)
dataset = client.get_dataset(name="video_dataset")
df = dataset.filter_contents(["/video_stream/**"]).reader(index="log_time")
times = pa.table(df.select("log_time"))["log_time"].to_numpy()
```
## Understanding video stream data
Video streams are logged using the [`VideoStream`](https://rerun.io/docs/reference/types/archetypes/video_stream.md) archetype,
which stores encoded video samples (frames) along with codec information.
Key columns you'll work with:
- `VideoStream:codec` - The video codec used (e.g., H.264)
- `VideoStream:sample` - The encoded video frame data (in Annex B format for H.264)
## Checking the video codec
Before processing video data, verify the codec matches what you expect:
```python
codec_column = "/video_stream:VideoStream:codec"
num_codec_matches = df.select(
col(codec_column)[0] == rr.VideoCodec.H264.value
).count()
if num_codec_matches != df.select(codec_column).count():
raise ValueError(
f"Expected H.264 codec {rr.VideoCodec.H264.value}, "
f"got {df.select(codec_column).limit(1)}"
)
```
## Decoding a specific frame
Unlike raw images, video frames are encoded using inter-frame compression.
To decode a specific frame, you must decode from the beginning of the stream (or from the most recent keyframe) and iterate forward.
`av` handles keyframe detection internally during decoding.
```python
video_column = "/video_stream:VideoStream:sample"
selected_frame_index = 3 # Pick an arbitrary frame to decode
# Query all samples up to and including the target frame.
# We need to decode from the start (or a keyframe) to reach our target.
selected_time = times[selected_frame_index]
video_df = df.filter(col("log_time") <= selected_time).select(
"log_time", video_column
)
pa_table = pa.table(video_df)
# Concatenate samples into a byte buffer
samples = pa_table[video_column].to_numpy()
sample_times = pa_table["log_time"].to_numpy()
sample_bytes = b""
for sample in samples:
sample_bytes += sample[0].tobytes()
data_buffer = BytesIO(sample_bytes)
# Decode using PyAV
container = av.open(data_buffer, format="h264", mode="r")
video_stream: av.video.stream.VideoStream = container.streams.video[0]
start_time = sample_times[0]
# Decode all frames up to our target, keeping only the last one
frame = None
for packet, time in zip(
container.demux(video_stream), sample_times, strict=False
):
packet.time_base = Fraction(1, 1_000_000_000) # Timestamps in nanoseconds
packet.pts = int(time - start_time)
packet.dts = packet.pts # No B-frames, so dts == pts
for decoded_frame in packet.decode():
frame = decoded_frame
if not isinstance(frame, av.VideoFrame):
raise RuntimeError("Failed to decode frame.")
image = np.asarray(frame.to_image())
print(f"Decoded frame shape: {image.shape}")
```
## Efficient random access with keyframe information
The example above queries all samples from the start of the stream, which can be inefficient for long videos.
For better performance with random access, you can add keyframe information as a layer.
### Adding keyframe information as a layer
You can analyze your video data once to identify keyframes and register them as a separate layer:
```python
# Preprocessing step: Add keyframe information to existing video data as a layer
# This is typically done once to make subsequent queries faster
# Query all video samples from the existing recording
video_samples_df = df.select("log_time", video_column)
video_table = pa.table(video_samples_df)
sample_times = video_table["log_time"].to_numpy()
samples = video_table[video_column].to_numpy()
# Concatenate all samples to analyze keyframes
sample_bytes = b""
for sample in samples:
sample_bytes += sample[0].tobytes()
# Decode the video to detect keyframes
data_buffer = BytesIO(sample_bytes)
container = av.open(data_buffer, format="h264", mode="r")
video_stream = container.streams.video[0]
# Identify which samples are keyframes
keyframe_times = []
for packet, ts in zip(container.demux(video_stream), sample_times):
if packet.is_keyframe:
keyframe_times.append(ts)
container.close()
keyframe_values = [True] * len(keyframe_times)
print(f"Found {len(keyframe_times)} keyframes")
# Save keyframe data as a separate layer
# Get the segment ID to align with the original recording
segment_ids = dataset.segment_ids()
first_segment_id = segment_ids[0]
# Create time column and content using the columnar API
# Make sure the timeline matches the original video stream
timeline = "log_time"
time_column = rr.TimeColumn(timeline=timeline, timestamp=keyframe_times)
content = rr.DynamicArchetype.columns(
archetype="KeyframeData", components={"is_keyframe": keyframe_values}
)
# Write to a new file as a layer
layer_path = TMP_DIR / "keyframe_layer.rrd"
with rr.RecordingStream(
application_id="keyframes",
recording_id=first_segment_id, # Match original recording_id
) as rec:
rec.save(layer_path)
rec.send_columns("/video_stream", indexes=[time_column], columns=[*content])
# Register the layer with the dataset
dataset.register([layer_path.as_uri()], layer_name="keyframes")
print(f"Registered keyframe layer at {layer_path}")
```
This preprocessing approach:
- Decodes the video once to detect which packets are keyframes using `packet.is_keyframe`
- Creates sparse data containing only keyframe timestamps
- Writes the keyframe data to a separate RRD file
- Registers it as a layer on the dataset
Once registered, the layer data appears as additional columns when querying the dataset (see [catalog object model](https://rerun.io/docs/concepts/query-and-transform/catalog-object-model.md) for details on datasets and layers).
### Querying with keyframe information
With the keyframe layer registered, you can query only the samples between the nearest keyframe and your target frame,
significantly reducing the amount of data to fetch and decode:
```python
# Query using keyframe information for efficient random access
# Assume we've already added keyframe information via the preprocessing step
# above
target_frame_index = 42
target_time = times[target_frame_index]
# Create a reader that includes the keyframe layer data
# The column name follows the pattern: /{entity_path}:{component_name}
keyframe_column = "/video_stream:is_keyframe"
full_df = dataset.filter_contents(["/video_stream/**"]).reader(index="log_time")
# Query to find the most recent keyframe at or before the target time.
# Since we only log when is_keyframe=True, any row with this column present
# is a keyframe
keyframe_slice = full_df.filter(
(col("log_time") <= target_time) & col(keyframe_column).is_not_null()
)
closest_keyframe_df = keyframe_slice.aggregate(
[],
[
F.last_value(col("log_time"), order_by=[col("log_time")]).alias(
"latest_keyframe"
)
],
)
keyframe_result = pa.table(closest_keyframe_df)
# Start decoding from the most recent keyframe
start_time = keyframe_result["latest_keyframe"].to_numpy()[0]
start_frame_idx = np.searchsorted(times, start_time)
frames_saved = target_frame_index - start_frame_idx
print(
f"Found keyframe at frame {start_frame_idx}, "
f"saved decoding {frames_saved} frames"
)
# Query only the video samples from keyframe to target (much more efficient!)
efficient_video_df = df.filter(
col("log_time").between(start_time, target_time)
).select("log_time", video_column)
efficient_table = pa.table(efficient_video_df)
frames_to_decode = len(efficient_table)
print(
f"Decoding {frames_to_decode} frames "
f"(vs {target_frame_index + 1} without keyframe info)"
)
# Now decode just this smaller range
samples = efficient_table[video_column].to_numpy()
sample_times = efficient_table["log_time"].to_numpy()
sample_bytes = b""
for sample in samples:
sample_bytes += sample[0].tobytes()
data_buffer = BytesIO(sample_bytes)
container = av.open(data_buffer, format="h264", mode="r")
video_stream = container.streams.video[0]
# Decode to the target frame
frame = None
for packet, time in zip(
container.demux(video_stream), sample_times, strict=False
):
packet.time_base = Fraction(1, 1_000_000_000)
packet.pts = int(time - sample_times[0])
packet.dts = packet.pts
for decoded_frame in packet.decode():
frame = decoded_frame
if isinstance(frame, av.VideoFrame):
image = np.asarray(frame.to_image())
print(
f"Efficiently decoded frame {target_frame_index} "
f"with shape: {image.shape}"
)
```
This approach is especially beneficial for:
- Long video sequences where decoding from the start is expensive
- Random access patterns where you need to jump to arbitrary frames
- High-resolution video where bandwidth and decode time are significant
- Interactive applications that need to seek to specific timestamps
## Exporting to MP4 (remuxing)
You can export video stream data to an MP4 file without re-encoding.
This is called "remuxing", the encoded samples are simply repackaged into a container format.
```python
# Query all video samples
video_df = df.select("log_time", "/video_stream:VideoStream:sample")
pa_table = pa.table(video_df)
all_times = pa_table["log_time"]
all_samples = pa_table["/video_stream:VideoStream:sample"]
# Concatenate samples into a single byte buffer
sample_bytes = np.concatenate([
sample[0] for sample in all_samples.to_numpy()
]).tobytes()
sample_bytes_io = BytesIO(sample_bytes)
# Setup input container (H.264 Annex B stream)
input_container = av.open(sample_bytes_io, mode="r", format="h264")
input_stream = input_container.streams.video[0]
# Setup output container (MP4)
output_path = TMP_DIR / "output.mp4"
output_container = av.open(output_path, mode="w")
output_stream = output_container.add_stream_from_template(input_stream)
# Remux packets with correct timestamps
start_time = all_times.chunk(0)[0]
for packet, time in zip(
input_container.demux(input_stream), all_times, strict=False
):
packet.time_base = Fraction(1, 1_000_000_000)
packet.pts = int(time.value - start_time.value)
packet.dts = packet.pts
packet.stream = output_stream
output_container.mux(packet)
input_container.close()
output_container.close()
print(f"Exported video to {output_path}")
```
## Important considerations
### Keyframe handling
Video streams often use inter-frame compression where most frames only store the difference from previous frames.
`av` handles keyframe detection internally, but for efficient random access to specific frames,
you may want to log keyframe indicators separately at recording time.
### Timestamp handling
Video timestamps in Rerun are typically stored in nanoseconds.
When using PyAV for decoding or muxing, ensure you set the correct `time_base` (typically `Fraction(1, 1_000_000_000)`).
### B-frames
Currently, Rerun's [`VideoStream`](https://rerun.io/docs/reference/types/archetypes/video_stream.md) does not support B-frames,
so `dts` (decode timestamp) equals `pts` (presentation timestamp).
# Overview
Rerun is the Unified Data Layer for Physical AI.
The Rerun SDK connects to a catalog server, which allows you to store, retrieve, and query over large amounts of data, and integrates with the SDK so you can browse and inspect the data visually.
The Rerun SDK includes a simplified open-source catalog server that is API compatible with Rerun Hub, our managed offering.
The open-source server loads everything into memory, which makes it fast and simple to operate for very small datasets, which in turn makes it perfect for quick testing and local experimentation.
See the [how-to guide for the open-source server](https://rerun.io/docs/howto/query-and-transform/get-data-out.md) for more details on launching and connecting to the server.
# Query images
Images are incredibly useful, however there are many ways to store and manipulate them.
This example focuses on querying image frames from a catalog server.
The dependencies in this example require `rerun-sdk[all]`.
## Setup
Simplified setup to launch the local server for demonstration.
In practice you'll connect to your cloud instance.
```python
from __future__ import annotations
from io import BytesIO
from pathlib import Path
import numpy as np
import pyarrow as pa
from datafusion import col
from PIL import Image
import rerun as rr
sample_video_path = (
Path(__file__).parents[4] / "tests" / "assets" / "rrd" / "video_sample"
)
server = rr.server.Server(datasets={"video_dataset": sample_video_path})
CATALOG_URL = server.url()
client = rr.catalog.CatalogClient(CATALOG_URL)
dataset = client.get_dataset(name="video_dataset")
df = dataset.filter_contents([
"/compressed_images/**",
"/raw_images/**",
]).reader(index="log_time")
times = pa.table(df.select("log_time"))["log_time"].to_numpy()
```
## Compressed image
Compressed images are just stored as a string of bytes, so you can query them directly and transform back into a raw image.
```python
column_name = "/compressed_images:EncodedImage:blob"
row = df.filter(col("log_time") == times[0]).select(column_name)
image_byte_array = pa.table(row)[column_name].to_numpy()[0][0]
image = np.asarray(Image.open(BytesIO(image_byte_array.tobytes())))
print(f"{image.shape=}")
```
## Raw image
Raw images are stored in a flattened layout, so we need to reshape them.
These format details are written to the RRD when images are logged.
```python
content_column = "/raw_images:Image:buffer"
format_column = "/raw_images:Image:format"
row = df.filter(col("log_time") == times[0]).select(
content_column, format_column
)
table = pa.table(row)
format_details = table[format_column][0][0]
flattened_image = table[content_column].to_numpy()[0][0]
num_channels = rr.datatypes.color_model.ColorModel.auto(
int(format_details["color_model"].as_py())
).num_channels()
image = flattened_image.reshape(
format_details["height"].as_py(),
format_details["width"].as_py(),
num_channels,
)
print(f"{image.shape=}")
```
# Time-align data
Real-world data is usually not time-aligned.
Rerun provides capabilities to simplify time alignment.
One common use case is to fill forward to run compute at a fixed frequency.
This example demonstrates how Rerun simplifies that process.
The dependencies in this example require `rerun-sdk[all]`, and `pandas` because python datetimes only support microsecond precision.
## Setup
Simplified setup to launch the local server for demonstration.
In practice you'll connect to your cloud instance.
```python
from __future__ import annotations
from pathlib import Path
import numpy as np
from datafusion import col
import rerun as rr
sample_5_path = (
Path(__file__).parents[4] / "tests" / "assets" / "rrd" / "sample_5"
)
server = rr.server.Server(datasets={"sample_dataset": sample_5_path})
CATALOG_URL = server.url()
client = rr.catalog.CatalogClient(CATALOG_URL)
dataset = client.get_dataset(name="sample_dataset")
```
## Extract desired timepoints
Select start and end time of data, downsample to a fixed frequency, and specify those as the desired output timestamps.
```python
view = dataset.filter_segments(
"ILIAD_sbd7d2c6_2023_12_24_16h_20m_37s"
).filter_contents("/observation/joint_positions")
ranges = view.get_index_ranges().to_arrow_table()
min_time = ranges["real_time:start"].to_numpy().flatten()
max_time = ranges["real_time:end"].to_numpy().flatten()
desired_timestamps = np.arange(
min_time[0], max_time[0], np.timedelta64(100, "ms")
) # 10Hz
```
## Time-align data
Select the timeline, columns, and episode of interest.
Extract rows at the specified time points, and fill forward to eliminate sparse entries.
Finally, filter out nulls for initial sensor state that cannot be resolved with forward fill.
```python
# Select columns of interest
# specify desired timestamps
# forward fill to specified time for alignment
fixed_hz = (
dataset
.filter_segments("ILIAD_sbd7d2c6_2023_12_24_16h_20m_37s")
.filter_contents(["/observation/joint_positions", "/camera/ext1/**"])
.reader(
index="real_time",
using_index_values=desired_timestamps,
fill_latest_at=True,
)
)
# Filter out partially sparse rows (since one column may start before the other)
fixed_hz_filtered = fixed_hz.filter(
col("/observation/joint_positions:Scalars:scalars").is_not_null(),
col("/camera/ext1:VideoStream:sample").is_not_null(),
)
```
# Query data out of Rerun
Rerun comes with the ability to get data out of Rerun from code. This page provides an overview of the API, as well as recipes to load the data in popular packages such as [Pandas](https://pandas.pydata.org), [Polars](https://pola.rs), and [DuckDB](https://duckdb.org).
## Starting a server with recordings
The first step to query data is to start a catalog server and load it with a dataset containing your recording.
See the [catalog object model](https://rerun.io/docs/concepts/query-and-transform/catalog-object-model.md) docs for more details on how datasets are organized in Rerun.
```python
import rerun as rr
# Start a server with one or more .rrd files
with rr.server.Server(datasets={"my_dataset": ["recording.rrd"]}) as server:
client = server.client()
dataset = client.get_dataset("my_dataset")
```
The server can host multiple datasets. Each dataset maps to either a list of `.rrd` files or a directory (which will be scanned for `.rrd` files):
```python
with rr.server.Server(
datasets={
# Explicit list of RRD files
"dataset1": ["recording1.rrd", "recording2.rrd"],
# Directory containing RRD files
"dataset2": "/path/to/recordings_dir",
}
) as server:
client = server.client()
# Access each dataset by name
ds1 = client.get_dataset("dataset1")
ds2 = client.get_dataset("dataset2")
```
When multiple recordings are loaded into a dataset, each gets mapped to a separate segment whose ID is the corresponding recording ID.
You can also start a longer running server in a separate process and connect to it by its local address.
In one file or terminal launch the server and print its address,
```python
server = rr.server.Server()
print(server.url())
```
in a separate file or terminal connect to that url
```python
client = rr.catalog.CatalogClient(server.url())
```
## Adding new datasets
New datasets can also be created or appended after the server is launched:
```python
dataset = client.create_dataset(
name="oss_demo",
)
dataset.register(Path("/path/to/recording/recording.rrd").resolve().as_uri()).wait()
```
## Viewing datasets
Either specify the network location with the CLI at launch:
```console
rerun connect localhost:51234
```
or open the command palette in the viewer (`cmd/ctrl + P` or via the menu) and enter/select `Add Redap server`.
Set the scheme to `http` and enter the hostname and port in the dialog.
## Inspecting the schema
The content of a dataset can be inspected using the `schema()` method:
```python
schema = dataset.schema()
schema.index_columns() # list of all index columns (timelines)
schema.component_columns() # list of all component columns
```
## Querying a dataset using `reader`
The primary means of querying data is the `reader()` method. In its simplest form, it is used as follows:
```python
df = dataset.reader(index="frame_nr")
print(df)
```
The returned object is a [`datafusion.DataFrame`](https://datafusion.apache.org/python/autoapi/datafusion/dataframe/index.html#datafusion.dataframe.DataFrame). Rerun's query APIs heavily rely on [DataFusion](https://datafusion.apache.org), which offers a rich set of data filtering, manipulation, and conversion tools.
When calling `reader()`, an index column must be specified. It can be any of the recording's timelines. Each row of the view will correspond to a unique value of the index column. It is also possible to query the dataset using `index=None`. In this case, only the `static=True` data will be returned.
By default, when performing a query on a dataset, data for all its segments is returned. An additional `"rerun_segment_id"` column is added to the dataframe to indicate which segment each row belongs to.
An often used parameter of the `reader()` method is `fill_latest_at=True`. When used, all `null` data will be filled with a latest-at value, similarly to how the viewer works.
## Querying a subset of a dataset
In general, datasets can be arbitrarily large, and it is often useful to query only a subset of it. This is achieved using `DatasetView` objects:
```python
# Filter by entity paths
dataset_view = dataset.filter_contents(["/world/robot/**", "/sensors/**"])
# Filter by segment IDs (recording IDs)
dataset_view = dataset.filter_segments(["recording_001", "recording_002"])
# Chain filters
dataset_view = dataset.filter_contents(["/world/**"]).filter_segments(["recording_001"])
```
`DatasetView` instances have the exact same `reader()` method as the original dataset:
```python
df = dataset_view.reader(index="frame_nr")
print(df)
```
## Filtering with DataFusion
DataFusion offers a rich set of filtering, projection, and joining capabilities. Check the [DataFusion Python documentation](https://datafusion.apache.org/python/) for details.
For illustration, here are a few simple examples:
```python
from datafusion import col
df = dataset.reader(index="frame_nr")
# Filter by index range
df = df.filter(col("frame_nr") >= 0).filter(col("frame_nr") <= 100)
# Filter by column not null
df = df.filter(col("/world/robot:Position3D:positions").is_not_null())
# Select specific columns
df = df.select("frame_nr", "/world/robot:Position3D:positions")
```
## Converting to other formats
Likewise, DataFusion offers a rich set of tools to convert a dataframe to various formats.
### Load data to a PyArrow `Table`
```python
import rerun as rr
with rr.server.Server(datasets={"my_dataset": ["recording.rrd"]}) as server:
dataset = server.client().get_dataset("my_dataset")
table = dataset.reader(index="frame_nr").to_arrow_table()
```
### Load data to a Pandas dataframe
```python
import rerun as rr
with rr.server.Server(datasets={"my_dataset": ["recording.rrd"]}) as server:
dataset = server.client().get_dataset("my_dataset")
df = dataset.reader(index="frame_nr").to_pandas()
```
### Load data to a Polars dataframe
```python
import rerun as rr
import polars as pl
with rr.server.Server(datasets={"my_dataset": ["recording.rrd"]}) as server:
dataset = server.client().get_dataset("my_dataset")
df = pl.from_arrow(dataset.reader(index="frame_nr").to_arrow_table())
```
### Load data to a DuckDB relation
```python
import rerun as rr
import duckdb
with rr.server.Server(datasets={"my_dataset": ["recording.rrd"]}) as server:
dataset = server.client().get_dataset("my_dataset")
table = dataset.reader(index="frame_nr").to_arrow_table()
rel = duckdb.arrow(table)
```
# View Operations
Robotics data has many sensors and many columns.
In order to more narrowly specify relevant content for further dataframe operations you first generate a view.
This view can filter on episode, time, column name etc.
This example shows specific instances highlighting these capabilities.
The dependencies in this example are contained in `rerun-sdk[all]`.
## Setup
Simplified setup.
Perform imports and launch local server for demonstration.
Extract an initial view `observations` that we later refine before generating a dataframe.
```python
from __future__ import annotations
from pathlib import Path
import pyarrow as pa
from datafusion import col
import rerun as rr
sample_5_path = (
Path(__file__).parents[4] / "tests" / "assets" / "rrd" / "sample_5"
)
server = rr.server.Server(datasets={"sample_dataset": sample_5_path})
CATALOG_URL = server.url()
client = rr.catalog.CatalogClient(CATALOG_URL)
dataset = client.get_dataset(name="sample_dataset")
observations = dataset.filter_contents("/observation/**")
```
## Filtering on episode and time
Limit the scope of the view so that only a subset of data will ever be considered client side.
Pick a specific episode by id, and a time range.
```python
episode = "ILIAD_50aee79f_2023_07_12_20h_55m_08s"
start = 1689220508
end = start + 5
filtered_view = dataset.filter_segments(episode).filter_contents(
"/observation/**"
)
filtered_df = filtered_view.reader(index="real_time")
filtered_df = filtered_df.filter(
(col("real_time") >= start) & (col("real_time") < end)
)
```
## Querying static data
So far we've been selecting `real_time` as the index. However, some data might not change in time (e.g. static transformations)
and thus isn't aligned with a time index. We specify this static timeline with `None`.
```python
instructions = dataset.filter_contents("/language_instruction/**").reader(
index=None
)
# Sort to ensure documented output is always correct
instructions = instructions.sort("/language_instruction:TextDocument:text")
instructions_tbl = pa.table(instructions)
instructions_tbl["/language_instruction:TextDocument:text"][0]
```
# Query Performance Tuning
This is a loose collection of considerations when querying Rerun datasets.
Over time baseline performance will improve, rendering some of these approaches unnecessary.
Since Rerun depends on [DataFusion](https://datafusion.apache.org/), some of these approaches are observations from our own usage.
First, generate a DataFrame for comparison:
```python
sample_video_path = (
Path(__file__).parents[4] / "tests" / "assets" / "rrd" / "video_sample"
)
server = rr.server.Server(datasets={"video_dataset": sample_video_path})
# Using OSS server for demonstration but in practice replace with
# the URL of your cloud instance
CATALOG_URL = server.url()
client = rr.catalog.CatalogClient(CATALOG_URL)
dataset = client.get_dataset(name="video_dataset")
df = dataset.filter_contents([
"/compressed_images/**",
"/raw_images/**",
]).reader(index="log_time")
```
## Extract Python types from a DataFrame
DataFusion is a streaming query engine, which allows for processing arbitrarily large amounts of data.
When working with smaller or filtered-down datasets that fit into memory, you can extract data into Python variables for further post processing.
In these examples, we convert DataFrames to [PyArrow](https://arrow.apache.org/docs/python/index.html) tables to materialize them in memory.
Similar patterns using Polars or Pandas also apply.
### Prefer to_numpy
This is technically a [PyArrow](https://arrow.apache.org/docs/python/index.html) and general Python detail.
For example, when extracting data from a PyArrow table, `to_pylist` can be multiple orders of magnitude slower, even when using `to_numpy(zero_copy_only=False)`.
```python
table = pa.table(df)
table["log_time"].to_numpy()
# vs.
table["log_time"].to_pylist()
```
## Fine-tune data collection
Similar to the approach described above to collect a DataFusion `DataFrame` into a PyArrow table, you can instead collect the results in memory and keep them as a `DataFrame`.
Then any operations on this in-memory (cached) `DataFrame` are typically _very_ fast.
```python
df.count() # has to pull some data
df.count() # has to pull same data again
# vs.
cache_df = df.cache() # materializes table in memory
cache_df.count() # basically free
cache_df.count() # basically free
```
## Leverage sparsity to minimize scans
In a write once, read many paradigm adding an additional sparse column can enable cheap access to data of interest via filtering.
The catalog server has the ability to "push down" filters to greatly reduce the amount of data returned, improving query performance.
In this example we take advantage of this fact by filtering based on a sparse marker we have intentionally inserted into the recording.
```python
# Create a new sparse layer identifying interesting events
segment_id = dataset.segment_ids()[0]
second_to_last_timestamp = pa.table(df)["log_time"].to_numpy()[-2]
with rr.RecordingStream("rerun_example_layer", recording_id=segment_id) as rec:
rec.save(RRD_PATH)
rec.set_time("log_time", timestamp=second_to_last_timestamp)
rec.log("/events", rr.AnyValues(flag=True))
dataset.register([Path(RRD_PATH).as_uri()], layer_name="event_layer")
# Read dataframe including new sparse layer
df_with_flag = dataset.filter_contents([
"/compressed_images/**",
"/raw_images/**",
"/events/**",
]).reader(index="log_time")
# This filter only looks at the single row in events
df_with_flag.filter(col("/events:flag").is_not_null())
# vs. using row_number which requires scanning all rows
df_with_row_number = df.with_column(
"row_num",
F.row_number(order_by="log_time"),
)
df_with_row_number.filter(col("row_num") == df_with_row_number.count() - 1)
```
# Generate segment URLs in dataframes
The [`segment_url`](https://ref.rerun.io/docs/python/stable/common/utilities/#rerun.utilities.datafusion.functions.url_generation.segment_url) DataFusion utility can be used to generate Rerun URLs that are clickable within the viewer.
The generated URLs can optionally seek to a timestamp, select a time range, or select an entity path.
## Setup
We start by loading sample data in a local catalog server instance and creating a table with some segment metadata.
```python
from __future__ import annotations
from datetime import datetime, timedelta
from pathlib import Path
import pyarrow as pa
from datafusion import lit
import rerun as rr
from rerun.utilities.datafusion.functions.url_generation import segment_url
sample_5_path = (
Path(__file__).parents[5] / "tests" / "assets" / "rrd" / "sample_5"
)
server = rr.server.Server(datasets={"sample_dataset": sample_5_path})
client = server.client()
dataset = client.get_dataset(name="sample_dataset")
# Pick 3 deterministic segment IDs and create a view filtered to them
segment_ids = sorted(dataset.segment_ids())[:3]
view = dataset.filter_segments(segment_ids)
# Build a synthetic metadata table keyed by rerun_segment_id
base_time = datetime(2023, 11, 14, 22, 13, 20)
event_times = [base_time + timedelta(seconds=i) for i in range(3)]
meta = pa.record_batch(
{
"rerun_segment_id": segment_ids,
"event_time": pa.array(event_times, type=pa.timestamp("ns")),
"range_start": pa.array(event_times, type=pa.timestamp("ns")),
"range_end": pa.array(
[t + timedelta(milliseconds=500) for t in event_times],
type=pa.timestamp("ns"),
),
"entity_path": [
"/camera/rgb",
"/observation/joint_positions",
"/observation/gripper_state",
],
},
)
ctx = client.ctx
meta_df = ctx.from_arrow(meta)
```
## Basic URL
With no extra arguments, `segment_url` produces a URL that opens the segment in the viewer.
```python
basic = view.segment_table().select("rerun_segment_id").sort("rerun_segment_id")
basic = basic.with_column("url", segment_url(dataset))
for url in basic.select("url").to_pydict()["url"]:
print(url)
```
Output:
```
rerun+http://localhost:51234/dataset/?segment_id=
rerun+http://localhost:51234/dataset/?segment_id=
rerun+http://localhost:51234/dataset/?segment_id=
```
## Specify the time cursor position
Pass `timestamp` and `timeline_name` to generate a URL that tells the viewer to activate a specific timeline and set
the time cursor to a specific value.
If `timestamp` is a string, it will be interpreted as a column name.
Alternatively, any DataFusion expression can be provided, including a literal.
```python
ts = view.segment_table(join_meta=meta_df).select(
"rerun_segment_id", "event_time"
)
ts = ts.sort("rerun_segment_id")
ts = ts.with_column(
"url",
segment_url(dataset, timestamp="event_time", timeline_name="real_time"),
)
for url in ts.select("url").to_pydict()["url"]:
print(url)
```
Output:
```
rerun+http://localhost:51234/dataset/?segment_id=#when=real_time@2023-11-14T22:13:20Z
rerun+http://localhost:51234/dataset/?segment_id=#when=real_time@2023-11-14T22:13:21Z
rerun+http://localhost:51234/dataset/?segment_id=#when=real_time@2023-11-14T22:13:22Z
```
## Selecting a time range
Pass `time_range_start` and `time_range_end` together with `timeline_name` to generate a URL that specifies a time range to be selected.
Both can be a column name or a DataFusion expression.
```python
tr = view.segment_table(join_meta=meta_df).select(
"rerun_segment_id", "range_start", "range_end"
)
tr = tr.sort("rerun_segment_id")
tr = tr.with_column(
"url",
segment_url(
dataset,
time_range_start="range_start",
time_range_end="range_end",
timeline_name="real_time",
),
)
for url in tr.select("url").to_pydict()["url"]:
print(url)
```
Output:
```
rerun+http://localhost:51234/dataset/?segment_id=#time_selection=real_time@2023-11-14T22:13:20Z..2023-11-14T22:13:20.5Z
rerun+http://localhost:51234/dataset/?segment_id=#time_selection=real_time@2023-11-14T22:13:21Z..2023-11-14T22:13:21.5Z
rerun+http://localhost:51234/dataset/?segment_id=#time_selection=real_time@2023-11-14T22:13:22Z..2023-11-14T22:13:22.5Z
```
## Selecting an entity
Pass `selection` to generate a URL that specifies which entity path, instance, and/or component to select.
The value must be a string using entity path syntax, optionally followed by an instance index in brackets
and/or a component name after a colon.
For example: `/world/points`, `/world/points[#42]`, `/world/points:Color`, or `/world/points[#42]:Color`.
```python
sel = view.segment_table(join_meta=meta_df).select(
"rerun_segment_id", "entity_path"
)
sel = sel.sort("rerun_segment_id")
sel = sel.with_column("url", segment_url(dataset, selection="entity_path"))
for url in sel.select("url").to_pydict()["url"]:
print(url)
```
Output:
```
rerun+http://localhost:51234/dataset/?segment_id=#selection=/camera/rgb
rerun+http://localhost:51234/dataset/?segment_id=#selection=/observation/joint_positions
rerun+http://localhost:51234/dataset/?segment_id=#selection=/observation/gripper_state
```
## Combining features
All three features can be used together. The generated URL includes every fragment that was specified.
```python
combined = view.segment_table(join_meta=meta_df).select(
"rerun_segment_id", "event_time", "range_start", "range_end", "entity_path"
)
combined = combined.sort("rerun_segment_id")
combined = combined.with_column(
"url",
segment_url(
dataset,
timestamp="event_time",
timeline_name="real_time",
time_range_start="range_start",
time_range_end="range_end",
selection="entity_path",
),
)
for url in combined.select("url").to_pydict()["url"]:
print(url)
```
Output:
```
rerun+http://localhost:51234/dataset/?segment_id=#selection=/camera/rgb&when=real_time@2023-11-14T22:13:20Z&time_selection=real_time@2023-11-14T22:13:20Z..2023-11-14T22:13:20.5Z
rerun+http://localhost:51234/dataset/?segment_id=#selection=/observation/joint_positions&when=real_time@2023-11-14T22:13:21Z&time_selection=real_time@2023-11-14T22:13:21Z..2023-11-14T22:13:21.5Z
rerun+http://localhost:51234/dataset/?segment_id=#selection=/observation/gripper_state&when=real_time@2023-11-14T22:13:22Z&time_selection=real_time@2023-11-14T22:13:22Z..2023-11-14T22:13:22.5Z
```
## Using expressions
Every parameter that accepts a column name string also accepts an arbitrary DataFusion expression.
This is useful when you want to supply a constant value for all rows using `lit()` or build more advanced expressions.
```python
expr = view.segment_table(join_meta=meta_df).select(
"rerun_segment_id", "event_time"
)
expr = expr.sort("rerun_segment_id")
expr = expr.with_column(
"url",
segment_url(
dataset,
timestamp="event_time",
timeline_name="real_time",
selection=lit("/camera/rgb"),
),
)
for url in expr.select("url").to_pydict()["url"]:
print(url)
```
Output:
```
rerun+http://localhost:51234/dataset/?segment_id=#selection=/camera/rgb&when=real_time@2023-11-14T22:13:20Z
rerun+http://localhost:51234/dataset/?segment_id=#selection=/camera/rgb&when=real_time@2023-11-14T22:13:21Z
rerun+http://localhost:51234/dataset/?segment_id=#selection=/camera/rgb&when=real_time@2023-11-14T22:13:22Z
```
# Embed Rerun in notebooks
Starting with version 0.15.1, Rerun has improved support for embedding the Rerun Viewer directly within IPython-style
notebooks. This makes it easy to iterate on API calls as well as to share data with others.
Rerun has been tested with:
- [Jupyter Notebook Classic](https://jupyter.org/)
- [Jupyter Lab](https://jupyter.org/)
- [VSCode](https://code.visualstudio.com/blogs/2021/08/05/notebooks)
- [Google Colab](https://colab.research.google.com/)
To begin, install the `rerun-sdk` package with the `notebook` extra:
```sh
pip install rerun-sdk[notebook]
```
This installs both [rerun-sdk](https://pypi.org/project/rerun-sdk/) and [rerun-notebook](https://pypi.org/project/rerun-notebook/).
## The APIs
When using the Rerun logging APIs, by default, the logged messages are buffered in-memory until
you send them to a sink such as via `rr.connect_grpc()` or `rr.save()`.
When using Rerun in a notebook, rather than using the other sinks, you have the option to use [`rr.notebook_show()`](https://ref.rerun.io/docs/python/stable/common/initialization_functions/#rerun.notebook_show). This method embeds the [web viewer](https://rerun.io/docs/howto/integrations/embed-web.md) using the IPython `display` mechanism in the cell output, and sends the current recording data to it.
Once the viewer is open, any subsequent `rr.log()` calls will send their data directly to the viewer,
without any intermediate buffering.
For example:
```python
import rerun as rr
from numpy.random import default_rng
rr.init("rerun_example_notebook")
rng = default_rng(12345)
positions = rng.uniform(-5, 5, size=[10, 3])
colors = rng.uniform(0, 255, size=[10, 3])
radii = rng.uniform(0, 1, size=[10])
rr.log("random", rr.Points3D(positions, colors=colors, radii=radii))
rr.notebook_show()
```
This is similar to calling `rr.connect_grpc()` or `rr.serve()` in that it configures the Rerun SDK to send data to a viewer instance.
Note that the call to `rr.notebook_show()` drains the recording of its data. This means that any subsequent calls to `rr.notebook_show()`
will not result in the same data being displayed, because it has already been removed from the recording.
Support for this is tracked in [#6612](https://github.com/rerun-io/rerun/issues/6612).
If you wish to start a new recording, you can call `rr.init()` again.
The `notebook_show()` method also takes optional arguments for specifying the width and height of the viewer. For example:
```python
rr.notebook_show(width=400, height=400)
```
## Reacting to events in the Viewer
It is possible to register a callback to be triggered when certain Viewer events happen.
For example, here is how you can track which entities are currently selected in the Viewer:
```python
from rerun.notebook import Viewer, ViewerEvent
selected_entities = []
def on_event(event: ViewerEvent):
global selected_entities
selected_entities = [] # clear the list
if event.type == "selection_change":
for item in event.items:
if item.type == "entity":
selected_entities.append(item.entity_path)
viewer = Viewer()
viewer.on_event(on_event)
display(viewer)
```
Whenever an entity is selected in the Viewer, `selected_entities.value` changes. The payload includes other useful information,
such as the position of the selection within a 2D or 3D view.
For a more complete example, see [callbacks.ipynb](https://github.com/rerun-io/rerun/blob/main/examples/python/notebook_callbacks/notebook_callbacks.ipynb).
## Working with blueprints
[Blueprints](https://rerun.io/docs/getting-started/configure-the-viewer/navigating-the-viewer.md) can also be used with `notebook_show()` by providing a `blueprint`
parameter.
For example
```python
blueprint = rrb.Blueprint(
rrb.Horizontal(rrb.Spatial3DView(origin="/world"), rrb.Spatial2DView(origin="/world/camera"), column_shares=[2, 1]),
)
rr.notebook_show(blueprint=blueprint)
```
Because blueprint types implement `_ipython_display_`, you can also just end any cell with a blueprint
object, and it will call `notebook_show()` behind the scenes.
```python
import numpy as np
import rerun as rr
import rerun.blueprint as rrb
rr.init("rerun_example_image")
rng = np.random.default_rng(12345)
image1 = rng.uniform(0, 255, size=[24, 64, 3])
image2 = rng.uniform(0, 255, size=[24, 64, 1])
rr.log("image1", rr.Image(image1))
rr.log("image2", rr.Image(image2))
rrb.Vertical(rrb.Spatial2DView(origin="/image1"), rrb.Spatial2DView(origin="/image2"))
```
## Streaming data
The notebook integration supports streaming data to the viewer during cell execution.
You can call `rr.notebook_show()` at any point after calling `rr.init()`, and any
`rr.log()` calls will be sent to the viewer in real-time.
```python
import math
from time import sleep
import numpy as np
import rerun as rr
from rerun.utilities import build_color_grid
rr.init("rerun_example_notebook")
rr.notebook_show()
STEPS = 100
twists = math.pi * np.sin(np.linspace(0, math.tau, STEPS)) / 4
for t in range(STEPS):
sleep(0.05) # delay to simulate a long-running computation
rr.set_time("step", sequence=t)
cube = build_color_grid(10, 10, 10, twist=twists[t])
rr.log("cube", rr.Points3D(cube.positions, colors=cube.colors, radii=0.5))
```
## Some working examples
To experiment with notebooks yourself, there are a few options.
### Running locally
The GitHub repo includes a [notebook example](https://github.com/rerun-io/rerun/blob/main/examples/python/notebook/cube.ipynb).
If you have a local checkout of Rerun, you can:
```bash
$ cd examples/python/notebook
$ pip install -r requirements.txt
$ jupyter notebook cube.ipynb
```
This will open a browser window showing the notebook where you can follow along.
### Running in Google Colab
We also host a copy of the notebook in [Google Colab](https://colab.research.google.com/drive/1R9I7s4o6wydQC_zkybqaSRFTtlEaked_)
Note that if you copy and run the notebook yourself, the first Cell installs Rerun into the Colab environment.
After running this cell you will need to restart the Runtime for the Rerun package to show up successfully.
## Limitations
Browsers have limitations in the amount of memory usable by a single tab. If you are working with large datasets,
you may run into browser tab crashes due to out-of-memory errors.
If you encounter the issue, you can try to use the `save()` API to save the data to a file and share it as a standalone asset.
## Future work
We are actively working on improving the notebook experience and welcome any [feedback or suggestions](https://rerun.io/feedback).
The ongoing roadmap is being tracked in [GitHub issue #1815](https://github.com/rerun-io/rerun/issues/1815).
# Use Rerun with ROS 2
Rerun does not yet have native ROS support, but many of the concepts in ROS and Rerun
line up fairly well. In this guide, you will learn how to write a simple ROS 2 Python node
that subscribes to some common ROS topics and logs them to Rerun.
For information on future plans to enable more native ROS support
see [#1537](https://github.com/rerun-io/rerun/issues/1537).
In case you have recorded data, you may also want to read our documentation on [using MCAP](https://rerun.io/docs/howto/logging-and-ingestion/mcap.md).
The following is primarily intended for existing ROS 2 users. It will not spend much time
covering how to use ROS 2 itself. If you are a Rerun user that is curious about ROS,
please consult the [ROS 2 Documentation](https://docs.ros.org) instead.
All of the code for this guide can be found on GitHub in
[rerun/examples/python/ros_node](https://github.com/rerun-io/rerun/blob/main/examples/python/ros_node/).
---
Other relevant tutorials:
- [Log and Ingest Tutorial](https://rerun.io/docs/getting-started/data-in.md)
- [Viewer Walkthrough](https://rerun.io/docs/getting-started/configure-the-viewer/navigating-the-viewer.md)
- [Transforms & Coordinate Frames](https://rerun.io/docs/concepts/logging-and-ingestion/transforms.md)
- [Loading URDF models](https://rerun.io/docs/howto/logging-and-ingestion/urdf.md)
- [Working with MCAP](https://rerun.io/docs/howto/logging-and-ingestion/mcap.md)
If you're new to Rerun or wonder about differences to RViz, we recommend also to read the *"What is Rerun for?"* introduction in our README (see [here](https://github.com/rerun-io/rerun?tab=readme-ov-file#what-is-rerun-for)).
## Install and run the example
All steps that are required to install and run this example are explained in the example's description, which can be found [here](https://rerun.io/examples/robotics/ros_node) or in the [README.md](https://github.com/rerun-io/rerun/tree/main/examples/python/ros_node) of the example's source code.
## Code explanation
It may be helpful to open [rerun/examples/python/ros_node/main.py](https://github.com/rerun-io/rerun/blob/latest/examples/python/ros_node/main.py)
to follow along.
At a very high level, for each ROS message we are interested in, we create a
subscriber with a callback that does some form of data conversion and then logs the data to Rerun.
In most cases, this conversion is either trivial or easy to do with utilities from the ROS ecosystem.
For simplicity, this example uses the rosclpy `MultiThreadedExecutor` and `ReentrantCallbackGroup` for each topic. This
allows each callback thread to do TF lookups without blocking the other incoming messages. More advanced ROS execution
models and using asynchronous TF lookups are outside the scope of this guide.
### Updating time
First of all, we want our messages to show up on the timeline based on their _stamped_ time rather than the
time that they were received by the listener, or relayed to Rerun.
To do this, we will use a Rerun timeline called `ros_time`.
Each callback follows a common pattern of updating `ros_time` based on the stamped time of the message that was
received.
```python
def some_msg_callback(self, msg: Msg):
time = Time.from_msg(msg.header.stamp)
rr.set_time("ros_time", timestamp=np.datetime64(time.nanoseconds, "ns"))
```
This timestamp will apply to all subsequent log calls on in this callback (on this thread) until the time is updated
again.
### TF to rr.Transform3D
Next, we need to map the [ROS TF2](https://docs.ros.org/en/humble/Concepts/About-Tf2.html) transforms to the corresponding Rerun archetype.
Since Rerun 0.28, the [`Transform3D`](https://rerun.io/docs/reference/types/archetypes/transform3d.md) archetype supports parent/child frame relationships, which makes our conversion step straight-forward. We just have to remember that the ROS [TFMessage](https://docs.ros2.org/foxy/api/tf2_msgs/msg/TFMessage.html) is a container for multiple transforms that have individual timestamps each and set the time accordingly.
By specifying the parent and child frames, we can log all transforms to the same entity path, similar to the TF topic in ROS.
To make sense of these transforms in the rest of our logged data, we also have to associate them to their respective frame name using [`CoordinateFrame`](https://rerun.io/docs/reference/types/archetypes/coordinate_frame.md)s.
See the laser scan section below for an example.
More information about the different ways Rerun can handle transforms can be found [here](https://rerun.io/docs/concepts/logging-and-ingestion/transforms.md).
```python
def tf_callback(self, tf_msg: TFMessage) -> None:
for transform in tf_msg.transforms:
time = Time.from_msg(transform.header.stamp)
rr.set_time("ros_time", timestamp=np.datetime64(time.nanoseconds, "ns"))
rr.log(
"transforms",
rr.Transform3D(
translation=[
transform.transform.translation.x,
transform.transform.translation.y,
transform.transform.translation.z,
],
rotation=rr.Quaternion(
xyzw=[
transform.transform.rotation.x,
transform.transform.rotation.y,
transform.transform.rotation.z,
transform.transform.rotation.w,
]
),
parent_frame=transform.header.frame_id,
child_frame=transform.child_frame_id,
),
)
```
### `robot_description` (URDF)
Rerun features a built-in importer for URDF, so we can just forward the string received on the `/robot_description` topic to it.
More information about how to use URDF with Rerun can be found [here](https://rerun.io/docs/howto/logging-and-ingestion/urdf.md).
```python
def urdf_callback(self, urdf_msg: String) -> None:
# NOTE: file_path is not known here, robot.urdf is just a placeholder to let
# Rerun know the file type. Since we run this example in a ROS environment,
# Rerun can use AMENT_PREFIX_PATH etc to resolve asset paths of the URDF.
rr.log_file_from_contents(
file_path="robot.urdf",
file_contents=urdf_msg.data.encode("utf-8"),
entity_path_prefix="urdf",
static=True,
)
```
### LaserScan to rr.LineStrips3D
Rerun does not yet have native support for a `LaserScan` style primitive, so we need
to do a bit of additional transformation logic (see: [#1534](https://github.com/rerun-io/rerun/issues/1534).)
First, we convert the scan into a point-cloud using the `laser_geometry` package.
We could have logged the Points directly using `rr.Points3D`, but for
the sake of this demo, we wanted to instead log a laser scan as a bunch of lines
in a similar fashion to how it is depicted in gazebo.
We generate a second matching set of points for each ray projected out 0.3m from
the origin and then interlace the two sets of points using Numpy hstack and reshape.
This results in a set of alternating points defining rays from the origin to each
laser scan result, which is the format expected by `rr.LineStrips3D`.
By logging also scan's `frame_id` as a [`CoordinateFrame`](https://rerun.io/docs/reference/types/archetypes/coordinate_frame.md), we make sure that Rerun visualizes the lines at the right location in the transform hierarchy.
```python
def __init__(self) -> None:
# β¦
self.laser_proj = laser_geometry.laser_geometry.LaserProjection()
def scan_callback(self, scan: LaserScan) -> None:
time = Time.from_msg(scan.header.stamp)
rr.set_time("ros_time", timestamp=np.datetime64(time.nanoseconds, "ns"))
# Project the laser scan to a collection of points
points = self.laser_proj.projectLaser(scan)
pts = point_cloud2.read_points(points, field_names=["x", "y", "z"], skip_nans=True)
pts = structured_to_unstructured(pts)
# Turn every pt into a line-segment from the origin to the point.
origin = (pts / np.linalg.norm(pts, axis=1).reshape(-1, 1)) * 0.3
segs = np.hstack([origin, pts]).reshape(pts.shape[0] * 2, 3)
rr.log("scan", rr.LineStrips3D(segs, radii=0.0025, colors=[255, 165, 0]))
rr.log("scan", rr.CoordinateFrame(frame=scan.header.frame_id))
```
### OccupancyGrid to rr.GridMap
ROS [`nav_msgs/OccupancyGrid`](https://docs.ros2.org/latest/api/nav_msgs/msg/OccupancyGrid.html) messages map directly to Rerun's [`GridMap`](https://rerun.io/docs/reference/types/archetypes/grid_map.md) archetype.
This example subscribes to the static map and the local & global costmap topics, logging them with Rerun's RViz-compatible `RvizMap` and `RvizCostmap` colormaps and with draw-order values for defined layering.
Most fields are a 1:1 mapping: the occupancy data becomes the `GridMap` image data, `info.resolution` becomes the cell size, and `info.origin` defines the map pose.
The main caveat is row order: ROS occupancy grids start at the map's bottom-left cell, while regular image buffers as used by Rerun's `GridMap` are top-row first.
The example therefore flips the rows before logging the grid data.
### Camera info and images
ROS Images can also be mapped to Rerun very easily, using the `cv_bridge` package.
The output of `cv_bridge.imgmsg_to_cv2` can be fed directly into `rr.Image`.
For the camera info topic, we can use the `image_geometry` package that has a `PinholeCameraModel` that exposes the intrinsic matrix in the same structure as used by Rerun `rr.Pinhole`.
Like for the laser scan, we also have to associate the data with the correct coordinate frame.
In order to have a nice projection of the image in the pinhole frustum in Rerun's 3D view, we have to establish a relationship between the 3D extrinsic camera frame and the 2D image plane.
The first is just the `frame_id` that we get from the ROS message, while the latter is something that isn't a concept in ROS.
To distinguish the two, we just use an `_image_plane` suffix in the image plane frame name and make sure that both the pinhole and image logging use it.
```python
def __init__(self) -> None:
# β¦
self.cv_bridge = cv_bridge.CvBridge()
def cam_info_callback(self, info: CameraInfo) -> None:
"""
Logs CameraInfo as a Rerun Pinhole.
"""
time = Time.from_msg(info.header.stamp)
self.pinhole_model.from_camera_info(info)
rr.set_time("ros_time", timestamp=np.datetime64(time.nanoseconds, "ns"))
rr.log(
"rgbd_camera/camera_info",
rr.Pinhole(
resolution=[info.width, info.height],
image_from_camera=self.pinhole_model.intrinsic_matrix(),
image_plane_distance=1.0,
parent_frame=info.header.frame_id,
# Specifying a `child_frame` for the 2D image plane allows Rerun to
# visualize the pinhole frustum together with the image in 3D views.
# This has to match the coordinate frames used when logging images,
# see `image_callback` below.
child_frame=info.header.frame_id + "_image_plane",
),
)
def image_callback(self, img: Image) -> None:
time = Time.from_msg(img.header.stamp)
rr.set_time("ros_time", timestamp=np.datetime64(time.nanoseconds, "ns"))
rr.log("rgbd_camera/image", rr.Image(self.cv_bridge.imgmsg_to_cv2(img)))
# Make sure the image plane frame matches what we set in `cam_info_callback`.
rr.log("rgbd_camera/image", rr.CoordinateFrame(frame=img.header.frame_id + "_image_plane"))
```
### Others
The example also logs more data, like depth images and parts of the odometry data.
Please refer to the [source code](https://github.com/rerun-io/rerun/blob/main/examples/python/ros_node/) of the example to see the details of those.
## In summary
Although there is a non-trivial amount of code, none of it is overly complicated. Each message callback
operates independently of the others, processing an incoming message, adapting it to Rerun and then
logging it again.
There are several places where Rerun is currently missing support for primitives that will further
simplify this implementation. We will continue to update this guide as new functionality becomes
available.
While this guide has only covered a small fraction of the possible ROS messages that could
be sent to Rerun, hopefully, it has given you some tools to apply to your project.
If you find that specific functionality is lacking for your use case, please provide more
context in the existing issues or [open an new one](https://github.com/rerun-io/rerun/issues/new/choose) on GitHub.
# Integrate Rerun with native loggers
The Rerun SDK implements the native logging interfaces of its supported host languages, allowing you to transparently stream text logs logged with the native APIs into the Rerun Viewer.
The details of how to achieve that vary language by language, see the snippets below.
```python
"""Shows integration of Rerun's `TextLog` with the native logging interface."""
import logging
import rerun as rr
rr.init("rerun_example_text_log_integration", spawn=True)
# Log a text entry directly
rr.log(
"logs",
rr.TextLog("this entry has loglevel TRACE", level=rr.TextLogLevel.TRACE),
)
# Or log via a logging handler
logging.getLogger().addHandler(rr.LoggingHandler("logs/handler"))
logging.getLogger().setLevel(-1)
logging.info("This INFO log got added through the standard logging interface")
```
# Embed Rerun in Web pages
Integrating the Rerun Viewer into your web application can be accomplished either by [utilizing an iframe](#embedding-apprerunio-using-an-iframe) or by using our [JavaScript package](#using-the-javascript-package).
## Embedding `app.rerun.io` using an `