examples/python/droid_semantic_search/README.md
Find moments in robot demonstrations by describing them in plain language, and jump straight to the matching frame in the Rerun viewer.
<picture> <source media="(max-width: 480px)" srcset="https://static.rerun.io/droid_semantic_search/fd265e110c9d05c9fdd018c68dd83561aa836c52/480w.png"> <source media="(max-width: 768px)" srcset="https://static.rerun.io/droid_semantic_search/fd265e110c9d05c9fdd018c68dd83561aa836c52/768w.png"> <source media="(max-width: 1024px)" srcset="https://static.rerun.io/droid_semantic_search/fd265e110c9d05c9fdd018c68dd83561aa836c52/1024w.png"> <source media="(max-width: 1200px)" srcset="https://static.rerun.io/droid_semantic_search/fd265e110c9d05c9fdd018c68dd83561aa836c52/1200w.png"> </picture>This pulls frame embeddings from a Rerun dataset, indexes them in an external vector store, and resolves text queries back into deep-links that open the relevant frames in the viewer. It uses LanceDB as the concrete store, but the pattern — embeddings out of Rerun, search in your vector database of choice, deep-links back into the viewer — applies to any vector store.
DataSource / Field / VideoFrameDecoder) for streaming and decoding H.264 frames on demand.segment_url deep-links that focus the viewer on a specific frame.Three scripts:
prepare_dataset.py — registers a few DROID episodes to your local catalog as a dataset (using the episodes bundled in the repo, or downloading them from the Hugging Face Hub when those aren't present). This is the data the other two scripts read.
ingest.py — builds the index. For each camera it auto-detects the embedding source:
/camera/{role}/embedding column (DROID registered with --create-embeddings), it reads those vectors directly via a DataFusion query — no model, no video decoding.VideoStream through the dataloader, decodes frames, and embeds them with SigLIP-2.Either way it writes (segment_id, camera, timestamp_ms, vector) rows into a searchable LanceDB table.
search.py — embeds your example image or text prompt with the SigLIP-2 encoder, runs a vector search over the LanceDB table, prints the ranked matches, and opens the best one in the viewer.
This example has its own uv project, separate from the workspace .venv, because it needs the experimental rerun-sdk[dataloader] extras plus heavy ML deps (transformers, lancedb).
Standalone (sparse-checkout of just this directory, no local Rerun build):
uv sync --no-sources --no-dev
Monorepo dev (full repo checkout, editable local rerun-sdk):
cd examples/python/droid_semantic_search
RERUN_ALLOW_MISSING_BIN=1 uv sync
uv pip install ../../../rerun_py/rerun_dev_fixup
The second command installs the .pth shim that points import rerun (and the rerun CLI) at the in-repo editable source tree.
It's a separate uv pip install rather than a dev-group dependency because uv resolves all dependency groups unconditionally, so a path-only package in pyproject.toml would break the standalone --no-sources resolution above.
Then either source .venv/bin/activate or prefix subsequent commands with uv run.
This example reads data from a local open-source Rerun catalog server. In a separate terminal, start one:
rerun server
This serves a catalog at rerun+http://127.0.0.1:51234 — the default the scripts use.
Leave it running; the steps below connect to it.
Registers a few DROID episodes to the catalog as droid:sample:
uv run python prepare_dataset.py
By default it auto-selects the source:
tests/assets/rrd/sample_5 (via git-LFS), so there's nothing to download. If those are un-pulled LFS pointers, run git lfs install && git lfs pull first.rerun/droid_sample on the Hugging Face Hub into ./data.Useful flags:
--source bundled|huggingface to force a source (default auto).--num-episodes N to register more (or fewer) episodes; 0 for all (the full Hub dataset is ~3.3 GB).--dataset-name to register under a different name (pass the same name to ingest.py/search.py).--no-optimize to register episodes as-is (see the video decode yield note below).--catalog-url "" to skip registration.Index a handful of segments (the exterior camera works well for scene-level search):
uv run python ingest.py --num-segments 15 --cameras ext1
These sample episodes ship video only (no pre-computed embeddings), so ingest.py takes the compute path:
the first run downloads the SigLIP-2 model (a few hundred MB) and then decodes and embeds frames — the slow step.
Start with a small --num-segments to keep it quick; raise it once you've seen it work.
Search by text and open the best hit in the viewer:
uv run python search.py "an open drawer full of tools" --top-k 5
Or search by example image instead of text (same vector space):
uv run python search.py --image ./query.jpg --top-k 5
Queries that discriminate well on DROID describe concrete, visible objects/scenes, e.g. "a pink flower", "a cardboard box", "a white plastic bag", "a robot arm over an empty table".
Run ingest.py --help and search.py --help for the full flag list — index multiple cameras, change the sampling rate, widen the time selection around a hit, and more.
--image <path>, by an example frame. SigLIP-2 puts text and image features in one space, so image-to-image search reuses the exact same index and ranking — only the query encoder differs.ingest.py and search.py at your store of choice.padding="max_length", max_length=64. With dynamic padding the text embeddings are malformed and text→image retrieval collapses onto a single "hub" frame that wins every query.VideoStream:is_keyframe markers the decoder needs to seek, so without them only ~25 % of sampled frames decode.
prepare_dataset.py derives the markers up front (via optimize), which brings yield to ~100 %.
Optimized copies land in ./optimized; the originals are untouched.
Skip it with --no-optimize if you'd rather register the raw episodes.prepare_dataset.py — register DROID sample episodes (bundled or downloaded) to the catalog.ingest.py — build the LanceDB index from the dataset.search.py — query the index and open results in the viewer.embeddings.py — SigLIP-2 helpers (adapted from the DROID loader's embedding_util.py).