Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions data/.lfs/go2_dds_china_office.mcap.tar.gz
Git LFS file not shown
3 changes: 3 additions & 0 deletions data/.lfs/go2_dds_stairs.mcap.tar.gz
Git LFS file not shown
2 changes: 1 addition & 1 deletion dimos/mapping/utils/cli/dataset_validation.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Dataset Validation

```sh
dimos map summary recording_go2_mid360_2026-05-29_4-45pm-PST.db
dimos mem summary recording_go2_mid360_2026-05-29_4-45pm-PST.db

Stream("color_image"): 11141 items, 2026-05-29 23:32:57 — 2026-05-29 23:45:57 (780.1s)
Stream("fastlio_lidar"): 7240 items, 2026-05-29 23:32:56 — 2026-05-29 23:45:57 (781.7s)
Expand Down
7 changes: 3 additions & 4 deletions dimos/mapping/utils/cli/map.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,24 +346,23 @@ def main(
) -> None:
"""Rebuild a voxel map from a recorded SQLite dataset, write a .rrd, and open it in rerun."""
from dimos.mapping.loop_closure.pgo import PGO
from dimos.memory2.store.sqlite import SqliteStore
from dimos.memory2.cli.dataset import open_store, resolve_dataset
from dimos.memory2.transform import QualityWindow, SpeedLimit
from dimos.memory2.utils.progress import progress
from dimos.msgs.sensor_msgs.CameraInfo import CameraInfo
from dimos.msgs.sensor_msgs.Image import Image
from dimos.msgs.sensor_msgs.PointCloud2 import PointCloud2
from dimos.perception.fiducial.marker_transformer import DetectMarkers
from dimos.robot.unitree.go2.connection import BASE_TO_OPTICAL, _camera_info_static
from dimos.utils.data import resolve_named_path
from dimos.visualization.rerun.init import rerun_init

db_path = resolve_named_path(dataset, ".db")
db_path = resolve_dataset(dataset)
store = open_store(db_path)
if out is None:
out = Path.cwd() / f"{db_path.stem}.rrd"
if export or full_pgo:
pgo = True

store = SqliteStore(path=db_path)
lidar = store.stream(lidar_stream, PointCloud2).from_time(seek or None).to_time(duration)

print(lidar.summary())
Expand Down
62 changes: 30 additions & 32 deletions dimos/mapping/utils/cli/replay.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,50 +206,48 @@ def main(
),
) -> None:
"""Dump a recording to .rrd (lidar clouds + camera frames) and open it in rerun."""
from dimos.mapping.utils.cli.summary import _stream_payload_types
from dimos.mapping.voxels import VoxelMapTransformer
from dimos.memory2.store.sqlite import SqliteStore
from dimos.memory2.cli.dataset import open_store, resolve_dataset, stream_payload_types
from dimos.memory2.transform import throttle
from dimos.msgs.geometry_msgs.PoseStamped import PoseStamped
from dimos.msgs.nav_msgs.Odometry import Odometry
from dimos.msgs.sensor_msgs.Image import Image
from dimos.msgs.sensor_msgs.PointCloud2 import PointCloud2, register_colormap_annotation
from dimos.robot.unitree.go2.connection import _camera_info_static
from dimos.utils.data import resolve_named_path

db_path = resolve_named_path(dataset, ".db")
src_path = resolve_dataset(dataset)
store = open_store(src_path)
if out is None:
out = Path.cwd() / f"{db_path.stem}.rrd"
out = Path.cwd() / f"{src_path.stem}.rrd"
cam_info = _camera_info_static()

# Resolve which streams to voxelize: all PointCloud2 streams, or the
# explicit --map-source subset. Validate up front so typos fail fast.
pc_streams = [n for n, t in _stream_payload_types(db_path).items() if t is PointCloud2]
map_sources = list(map_source) or pc_streams
if (map or map_final) and (bad := [s for s in map_sources if s not in pc_streams]):
raise typer.BadParameter(f"--map-source: not PointCloud2 stream(s): {', '.join(bad)}")

rr.init("dimos map_rrd", recording_id=db_path.stem)
rr.save(str(out))
register_colormap_annotation("turbo")

# Static pinhole on the camera entity; per-frame Transform3D goes on the
# same entity. Image is the child so it projects through the pinhole.
pinhole = cam_info.to_rerun()
assert not isinstance(pinhole, list)
rr.log("world/camera", pinhole, static=True)

# Static axis triads as children of each moving Transform3D, so the
# transforms are actually visible in the 3D view.
axes = rr.Arrows3D(
vectors=[[0.3, 0, 0], [0, 0.3, 0], [0, 0, 0.3]],
colors=[[255, 0, 0], [0, 255, 0], [0, 0, 255]],
)
rr.log("world/fastlio/axes", axes, static=True)
rr.log("world/odom/axes", axes, static=True)

store = SqliteStore(path=str(db_path))
with store:
# Resolve which streams to voxelize: all PointCloud2 streams, or the
# explicit --map-source subset. Validate up front so typos fail fast.
pc_streams = [n for n, t in stream_payload_types(store).items() if t is PointCloud2]
map_sources = list(map_source) or pc_streams
if (map or map_final) and (bad := [s for s in map_sources if s not in pc_streams]):
raise typer.BadParameter(f"--map-source: not PointCloud2 stream(s): {', '.join(bad)}")

rr.init("dimos map_rrd", recording_id=src_path.stem)
rr.save(str(out))
register_colormap_annotation("turbo")

# Static pinhole on the camera entity; per-frame Transform3D goes on the
# same entity. Image is the child so it projects through the pinhole.
pinhole = cam_info.to_rerun()
assert not isinstance(pinhole, list)
rr.log("world/camera", pinhole, static=True)

# Static axis triads as children of each moving Transform3D, so the
# transforms are actually visible in the 3D view.
axes = rr.Arrows3D(
vectors=[[0.3, 0, 0], [0, 0.3, 0], [0, 0, 0.3]],
colors=[[255, 0, 0], [0, 255, 0], [0, 0, 255]],
)
rr.log("world/fastlio/axes", axes, static=True)
rr.log("world/odom/axes", axes, static=True)

print(store.summary())

def clipped(name: str, ptype: type[Any]) -> Stream[Any]:
Expand Down
7 changes: 3 additions & 4 deletions dimos/mapping/utils/cli/replay_marker.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,16 +66,16 @@ def main(
),
) -> None:
"""Dump an AprilTag detection replay to .rrd and open it in rerun."""
from dimos.memory2.store.sqlite import SqliteStore
from dimos.memory2.cli.dataset import open_store, resolve_dataset
from dimos.memory2.transform import QualityWindow, SpeedLimit
from dimos.memory2.vis.color import Color
from dimos.msgs.sensor_msgs.Image import Image
from dimos.msgs.sensor_msgs.PointCloud2 import PointCloud2
from dimos.perception.fiducial.marker_transformer import DetectMarkers
from dimos.robot.unitree.go2.connection import _camera_info_static
from dimos.utils.data import resolve_named_path

db_path = resolve_named_path(dataset, ".db")
db_path = resolve_dataset(dataset)
store = open_store(db_path)
if out is None:
out = Path.cwd() / f"{db_path.stem}.rrd"
cam_info = _camera_info_static()
Expand All @@ -89,7 +89,6 @@ def main(
assert not isinstance(pinhole, list)
rr.log("world/camera", pinhole, static=True)

store = SqliteStore(path=str(db_path))
with store:
color_image = store.stream("color_image", Image).from_time(seek or None).to_time(duration)
lidar = store.stream("lidar", PointCloud2).from_time(seek or None).to_time(duration)
Expand Down
59 changes: 0 additions & 59 deletions dimos/mapping/utils/cli/summary.py

This file was deleted.

11 changes: 7 additions & 4 deletions dimos/mapping/utils/cli/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,13 @@ def dataset() -> str:


def test_summary(dataset: str) -> None:
res = _run("summary", dataset)
assert res.returncode == 0, res.stderr
assert "lidar" in res.stdout
assert "odom" in res.stdout
# `summary` is generic and lives under `dimos mem` (not `dimos map`).
from dimos.robot.cli.dimos import main as cli_app

res = _runner.invoke(cli_app, ["mem", "summary", dataset])
assert res.exit_code == 0, res.output
assert "lidar" in res.output
assert "odom" in res.output


@requires_turbojpeg
Expand Down
32 changes: 31 additions & 1 deletion dimos/mapping/voxels.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

from __future__ import annotations

from collections import deque
import time
from typing import TYPE_CHECKING, Any

Expand Down Expand Up @@ -43,6 +44,11 @@ class VoxelGrid:

No Module/framework dependency. Can be used standalone or wrapped
by VoxelGridMapper (Module) or VoxelMapTransformer (memory2 Transformer).

``time_window`` controls how long voxels live (by frame timestamp):
``-1`` (default) keeps every frame forever (a growing global map); ``0``
keeps only the current frame; ``N`` keeps the last ``N`` seconds (a rolling
window that tracks the local surface). Windowing overrides ``carve_columns``.
"""

def __init__(
Expand All @@ -53,10 +59,13 @@ def __init__(
carve_columns: bool = True,
frame_id: str = "world",
show_startup_log: bool = True,
time_window: float = -1.0,
) -> None:
self._voxel_size = voxel_size
self._carve_columns = carve_columns
self._frame_id = frame_id
self._time_window = time_window
self._frames: deque[tuple[float, o3c.Tensor]] = deque() # (ts, voxel keys) for windowing

dev = (
o3c.Device(device)
Expand Down Expand Up @@ -101,7 +110,9 @@ def add_frame(self, frame: PointCloud2) -> None:
vox = (pts / self._voxel_size).floor().to(self._key_dtype)
keys_Nx3 = vox.contiguous()

if self._carve_columns:
if self._time_window >= 0:
self._add_windowed(keys_Nx3)
elif self._carve_columns:
self._carve_and_insert(keys_Nx3)
else:
self._voxel_hashmap.activate(keys_Nx3)
Expand All @@ -117,6 +128,24 @@ def add_frame(self, frame: PointCloud2) -> None:
if str(self._dev).startswith("CUDA"):
o3c.cuda.release_cache()

def _add_windowed(self, new_keys: o3c.Tensor) -> None:
"""Keep only voxels from frames within ``time_window`` s of the latest frame.

Buffers each frame's keys, drops frames that have aged out, then rebuilds
the active set as the union of the survivors (``activate`` dedups). Always
keeps at least the current frame, so ``time_window == 0`` is single-frame.
"""
ts = self._latest_frame_ts
self._frames.append((ts, new_keys))
while len(self._frames) > 1 and ts - self._frames[0][0] > self._time_window:
self._frames.popleft()

active = self._voxel_hashmap.active_buf_indices()
if active.shape[0] > 0:
self._voxel_hashmap.erase(self._voxel_hashmap.key_tensor()[active].contiguous())
for _, keys in self._frames:
self._voxel_hashmap.activate(keys)

def _carve_and_insert(self, new_keys: o3c.Tensor) -> None:
"""Column carving: remove all existing voxels sharing (X,Y) with new_keys, then insert."""
if new_keys.shape[0] == 0:
Expand Down Expand Up @@ -251,6 +280,7 @@ class VoxelGridMapperConfig(ModuleConfig):
carve_columns: bool = True
frame_id: str = "world"
emit_every: int = 1
time_window: float = -1.0


class VoxelGridMapper(StreamModule[PointCloud2, PointCloud2]):
Expand Down
28 changes: 26 additions & 2 deletions dimos/memory2/cli/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,32 @@ def rerun(
out: str = typer.Option(None, "--out", help="Output .rrd (default: alongside the source)"),
seconds: float = typer.Option(None, "--seconds", help="Only the first N seconds"),
no_gui: bool = typer.Option(False, "--no-gui", help="Write the .rrd but don't open the viewer"),
root: str = typer.Option(
None, "--root", help="Nest every stream under this entity path (<root>/<name>)"
),
) -> None:
"""Render a memory2 store into rerun (writes a .rrd, then opens the viewer)."""
from dimos.memory2.cli.render import open_store, render_store
from dimos.memory2.cli.dataset import open_dataset
from dimos.memory2.cli.render import render_store

render_store(open_store(path), out=out, seconds=seconds, no_gui=no_gui)
render_store(open_dataset(path), out=out, seconds=seconds, no_gui=no_gui, root=root)


@mem_app.command()
def summary(
dataset: str = typer.Argument(..., help="Dataset .db/.mcap: bare name (cwd or data/) or path"),
) -> None:
"""Print per-stream counts and time ranges for a recorded dataset."""
from dimos.memory2.cli.dataset import open_dataset

store = open_dataset(dataset)
with store:
for name in store.list_streams():
store.streams[name] # register so summary() includes it

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This kinda indicates a deficiency in the API if you have to include empty statements to get .summary() to work.

Shouldn't .summary() loop over the streams instead?

print(store.summary())

# mcap files may carry channels we have no codec for (e.g. h264 video);
# list them so the inventory is complete rather than silently filtered.
uncodec = getattr(store, "uncodec_channels", None)

@paul-nechifor paul-nechifor Jun 18, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like this approach of getting random methods by name.

If you specifically need McapStore.uncodec_channels, then check if store is a McapStore. If multiple stores are expected to have uncodec_channels, then declare uncodec_channels on Store and set it to return [] by default and let McapStore.uncodec_channels implement a different functionality.

for topic, (count, schema) in sorted(uncodec().items()) if uncodec else []:
print(f" (no codec) {topic}: {count} msgs [{schema or '?'}]")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better to use typer.echo(...).

Loading
Loading