Skip to main content
After a run you have a recording - a memory2 .db from Record & replay holding every camera frame, lidar scan, and pose from the session. This page is about asking that file questions:
  • Where did the robot go, and how fast? Debug navigation without re-running the robot.
  • What did the environment look like? Map room lighting, coverage, sensor quality.
  • When and where did the robot see X? Search hours of video with a sentence, then jump straight to those frames and places.
That last one is the headline capability: recorded vision becomes a searchable index. Instead of scrubbing video, you type โ€œplantโ€ and get back timestamps, map locations, and the frames themselves. Everything below is an executable notebook (the code blocks run for real in CI) working on go2_bigoffice, a bundled five-minute Go2 drive around an office. Swap in your own recording and everything works the same. The query API used throughout is documented in The memory2 library.

The session

Open the store and see what was recorded:
Python
Five minutes of driving: 4k camera frames, 2k lidar scans, 5k odometry samples. Every observation carries its timestamp and the robotโ€™s pose, which is what makes the spatial queries below possible.

Draw where the robot went

Space is a top-down spatial canvas: add any stream to it and each observation is drawn at the pose where it was captured, colored by time (turbo colormap, blue early to red late). Adding the camera stream literally draws the robotโ€™s trajectory:
Python
output One glance answers: what was covered, where the robot lingered, whether it doubled back.

Derive new streams

Queries are lazy stream pipelines: .transform() and .map() build new streams from recorded ones without touching the database until you draw or iterate. Two practical examples. How fast was the robot moving, where? Useful for spotting where navigation slowed down or got stuck:
Python
output What is the lighting like around the space? Any per-frame quantity can be painted onto the map the same way - here, image brightness. This kind of environment map matters later: dark frames are useless for vision, and we will filter on exactly this signal before embedding:
Python
output

Make the camera stream searchable

To search video with text, embed the frames with CLIP once and save the result as a new stream in the same store. The pipeline filters dark frames (see the lighting map above), picks the sharpest frame in each half-second window, embeds, and saves:
Python
Pipelines are lazy - execute by iterating, or with .drain():
skip
Our bundled dataset already contains this color_image_embedded stream (267 embedded frames of the 4164 recorded).

Search by text

Now the session is queryable in natural language. Embed a text query and search - matches come back as ordinary observations, so they draw on the map like anything else:
Python
output We can go further: take the top matches, pull only the lidar captured at those moments, and reconstruct just the geometry around them - a focused 3D view of โ€œthe places that look like a shopโ€:
Python
output And view the matching frames themselves:
Python
output Search, locate, look - the whole point of pose-stamped recording in three cells.

Case study: find the plants

A full worked example, end to end: did the robot see plants during the session, where exactly are they in the building, and can we verify with a detector? This is the workflow for any โ€œfind X in my recordingsโ€ task. It also plots over time rather than space, using the Plot API (time-series companion to Space). First, a feel for the session on a timeline - speed, brightness, and elapsed time on separate axes:
session=robotdata output=none
output

Score every frame against โ€œplantโ€

Search the embedded stream and re-sort by time, giving a โ€œplant-ness over the sessionโ€ signal:
session=robotdata
output There are clear peaks at the beginning and end of the run - but the graph is gappy and ugly. Why? Embeddings were only computed above a minimum brightness. Completely dark images are both useless and semantically close to everything. Overlay brightness to confirm the gaps line up:
session=robotdata
output Nothing is embedded below the brightness floor. Clean the signal up: treat unmapped values as zero, connect points within 7.5 s, smooth over a 5 s window, normalize:
session=robotdata
output

Auto-detect the peaks and verify with a VLM

Obvious peaks. Auto-detect them, pull the frames at those moments, and run a detector (Moondream, one of the VLM backends) to verify there really are plants:
skip session=robotdata
output output output

Which peaks are significant?

We got 15 peaks back. Most prominences sit around 0.02-0.03 and only a couple (0.067 at t=37s, 0.047 at t=240s) really stand out. significant() replaces eyeballing that cutoff by thresholding on the distribution of prominences itself (default: MAD, median absolute deviation). Once the surviving peaks go on the timeline, we get two very obvious plants:
skip session=robotdata
output output Rule of thumb: keep a small absolute floor on peaks(prominence=...) to reject shape-noise, then let significant() pick the statistical cutoff.

Zoom into a hotspot

Focus on the strongest peak: load every image captured within 2.5 m of it (filtered for brightness and sharpness), rebuild the local 3D map from the lidar around it, and run the detector on all nearby views:
skip session=robotdata
output output

Project detections into 3D

Finally, lift the 2D detections into 3D boxes on the map using the camera model and the pointcloud - from โ€œthe robot saw a plant at t=37sโ€ to โ€œthere is a plant hereโ€:
skip session=robotdata output=none
output That is the full loop: text query, temporal peaks, spatial hotspot, verified detections, 3D locations - all from one recorded drive, none of it requiring the robot again.

Appendix: plotting API notes

Small things worth knowing about Plot when you build your own analyses. Colors auto-cycle as you add series:
session=plot output=none
output Named colors can also be used explicitly. When you pin a series to one of the named colors, the auto-cycle excludes it for the remaining series, so you never end up with two lines that share a color by accident:
session=plot output=none
output