Skip to content

Loop closure / map Reconstruction first pass#2242

Merged
leshy merged 51 commits into
mainfrom
feat/ivan/pgo_rewrite
May 29, 2026
Merged

Loop closure / map Reconstruction first pass#2242
leshy merged 51 commits into
mainfrom
feat/ivan/pgo_rewrite

Conversation

@leshy

@leshy leshy commented May 24, 2026

Copy link
Copy Markdown
Member
  • pgo rewrite, map tool vis aditions
  • full global map reconstruction takes a few seconds, we use memory2 to fetch 20% of total lidar frames based on time and space (latest only, not all)
2026-05-24_22-00
  • marker detector as a mem2 transform dimos map hk_village5 --pgo --markers
2026-05-25_15-33
  • marker detection debugger
cd dimos/utils/cli
python markers_rrd.py hk_village3 --out test3.rrd && rerun test3.rrd

# or for full map

dimos map hk_village3 --pgo --markers
2026-05-25_16-32

leshy added 12 commits May 24, 2026 18:53
Replaces the monolithic pgo_then_voxels with four primitives over
mem2 Streams and Transforms:

  pgo_keyframes(lidar)            -> Stream[Keyframe]
  keyframes_to_corrections(kfs)   -> Stream[Transform]   (world_corrected <- world_raw)
  make_interpolator(corrections)  -> (ts) -> Transform   (SLERP + linear, endpoint-clipped)
  apply_corrections(stream, corr) -> Stream[T]           (shuffles obs.pose)

Drift correction is now a first-class, reusable Stream[Transform] that
any pose-stamped consumer can apply — same math as before (rigid
T_corr = T_global @ T_local^-1 per keyframe, SLERP + linear interpolation
between bracketing keyframes), just composable.

dimos map --pgo migrates to the new primitives; pgo_then_voxels is
deleted. The internal _SimplePGO / PGOConfig / _KeyPose machinery stays
in pgo.py and is imported by pgo2.
Renames:
  pgo.py  -> pgo_internals.py   (gtsam/ICP machinery)
  pgo2.py -> pgo.py             (public Stream-shaped API)

`dimos.mapping.relocalization.pgo` is now the canonical import path
(`pgo_keyframes`, `keyframes_to_corrections`, `make_interpolator`,
`apply_corrections`, `correction_at`, `Keyframe`). The internal
_SimplePGO / PGOConfig / _KeyPose / _icp / _voxel_downsample helpers
live in pgo_internals.py and are imported lazily inside pgo.py to keep
gtsam off the public-API import path.
Adds test_pgo.py covering pose normalization on Observation, the Transform↔Pose3
conversion helpers, the interpolator edge cases, apply_corrections behavior, and
a real-recording smoke test (skipped when data/go2_short.db is absent).
Annotates the two map.py helpers so they pass mypy. Removes leftover section
divider comments to satisfy the no-section-markers project rule.
Comment thread dimos/memory2/type/observation.py Outdated
Comment thread dimos/mapping/loop_closure/pgo.py Outdated
Comment thread dimos/mapping/loop_closure/pgo.py Outdated
@codecov

codecov Bot commented May 24, 2026

Copy link
Copy Markdown

@leshy leshy changed the title Feat/ivan/pgo rewrite Loop closure / map Reconstruction first pass May 24, 2026
leshy added 3 commits May 24, 2026 22:46
The colors-copy block in `PointCloud2.transform` calls `self.pointcloud`
to check `has_colors()`, which forces a tensor->legacy conversion on
every invocation. With `pgo --full-pgo` rebuilding from hundreds of
lidar frames, that hidden allocation dominates the per-frame cost. The
colors path was lidar-irrelevant (lidar clouds have no colors anyway)
and the feature it was meant to support never landed; remove it so
transform() stays a clean numpy round-trip.
Comment thread dimos/msgs/sensor_msgs/PointCloud2.py
Comment thread dimos/mapping/loop_closure/utils/markers_rrd.py Outdated
leshy added 2 commits May 28, 2026 16:59
The outer `pose = obs.pose` (Pose | None) was being shadowed by
`pose = estimate_marker_pose(...)` (tuple[rvec, tvec] | None), which
mypy flags as an incompatible reassignment. Renamed the inner one
to `marker_pose`.
NearFilter now holds a typed `Vector3` instead of an `Any` named `pose`.
`Stream.near()` does the one-time normalization at construction (Pose/
PoseStamped → .position, then Vector3(...) which accepts tuple/ndarray/
Vector3). `matches()` uses Vector3 subtraction + length_squared(); the
sqlite filter compiler reads f.position.x/.y/.z directly. The _xyz()
helper is gone.
@dimensionalOS dimensionalOS deleted a comment from greptile-apps Bot May 28, 2026
@greptile-apps

greptile-apps Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR replaces the numpy-based relocalization/pgo.py with a fully redesigned loop_closure/pgo.py that wraps GTSAM ISAM2 as a memory2 Transformer, and refactors Observation so that pose is now a typed Pose property while the raw 7-tuple is stored in pose_tuple. Several bugs caught in previous review rounds have been resolved (placeholder-pose filter, qw or 1.0 corruption, rr.Points3D wrong kwarg, and obs.pose unpack crashes).

  • dimos/mapping/loop_closure/pgo.py — new PGO implementation using GTSAM ISAM2; PoseGraph is itself a Transformer that applies drift corrections to arbitrary streams, replacing the old hand-rolled SLERP+voxel pass.
  • dimos/memory2/type/observation.pyObservation.pose now returns a typed Pose | None; raw storage moves to pose_tuple; _to_tuple centralizes coercion from Transform / PoseStamped / tuple / list.
  • dimos/perception/fiducial/marker_transformer.py + dimos/utils/cli/map.py — ArUco/AprilTag marker detection wired into the mem2 stream pipeline with optional pose smoothing and rerun visualization.

Confidence Score: 4/5

Most of the bugs from previous rounds are now fixed; the remaining unfixed crash (LookupError on empty PGO output in map.py and eval.py) is the main blocker for recordings with all-placeholder poses.

The Observation refactor, GTSAM-based PGO, and marker pipeline are all sound and well-tested. All previously flagged pose-corruption and type-crash bugs are addressed. The one active issue is that lidar.transform(PGO()).last() in map.py and eval.py still raises an unhandled LookupError when a recording yields zero valid keyframes — a condition that can occur with bad data and produces a raw traceback instead of a clean diagnostic.

dimos/utils/cli/map.py (line 285) and dimos/mapping/loop_closure/eval.py (line 95) — both call .last() on the PGO stream without a guard for empty output.

Important Files Changed

Filename Overview
dimos/mapping/loop_closure/pgo.py New GTSAM ISAM2-based PGO implementation as a memory2 Transformer; clean design with PoseGraph dual-role as corrector; placeholder-pose filter now correctly uses is_zero() for orientation.
dimos/memory2/type/observation.py Major refactor: pose stored as pose_tuple, exposed as typed Pose property; _to_tuple centralizes coercion and now handles list/tuple/Transform/Pose/PoseStamped correctly with is-not-None guards.
dimos/memory2/observationstore/sqlite.py _reconstruct_pose replaces qw or 1.0 pattern with explicit asserts; uses pose_tuple for insert/reconstruct; assert-based validation is correct for current schema but can be bypassed with -O.
dimos/utils/cli/map.py New map CLI with spatial dedup, PGO two-pass reconstruction, and marker overlay; uses Pose API correctly; LookupError on line 285 when all frames are placeholder poses remains unhandled (pre-existing flagged issue).
dimos/perception/fiducial/marker_transformer.py New file: ArUco/AprilTag detection as a memory2 Transformer with optional smoothing; correctly uses obs.pose (Pose object) and composes world-frame marker poses from camera-in-world transform.
dimos/memory2/transform.py Adds SpeedLimit transformer for motion-blur gating; speed() updated to Pose API; SpeedLimit correctly drops pose-less frames and first frame (documented).
dimos/msgs/sensor_msgs/PointCloud2.py Fixes to_rerun mode='points' to pass radii= instead of the non-existent sizes= kwarg; removes unused size parameter from signature; color carry-through comment clarified.
dimos/msgs/geometry_msgs/Quaternion.py Adds is_zero(), angle_to(), dot(), and to_rotation_matrix() helpers; is_zero() correctly targets the all-zero uninitialized case without filtering the identity quaternion.

Sequence Diagram

sequenceDiagram
    participant CLI as dimos map CLI
    participant Lidar as Stream[PointCloud2]
    participant PGO as PGO Transformer
    participant PGOState as _PGOState (ISAM2)
    participant PoseGraph as PoseGraph
    participant Accum as _accumulate

    CLI->>Lidar: store.streams.lidar + spatial dedup
    CLI->>Lidar: lidar.tap(prog).transform(PGO())
    loop per lidar frame
        Lidar->>PGO: Observation[PointCloud2]
        PGO->>PGO: filter placeholder poses (is_zero)
        PGO->>PGOState: process(pose3, ts, cloud)
        PGOState->>PGOState: _add_keyframe (BetweenFactorPose3)
        PGOState->>PGOState: _search_for_loops (KDTree + ICP)
        alt loop accepted
            PGOState->>PGOState: _smooth_and_update (ISAM2)
            PGO-->>CLI: yield Observation[PoseGraph] snapshot
        end
    end
    PGO-->>CLI: final yield Observation[PoseGraph]
    CLI->>CLI: "graph = .last().data"
    CLI->>Accum: "_accumulate(seen.values(), graph=graph)"
    loop per deduped frame
        Accum->>PoseGraph: correction_at(obs.ts)
        PoseGraph->>PoseGraph: slerp/lerp interpolation
        PoseGraph-->>Accum: drift-correction Transform
        Accum->>Accum: cloud.transform(correction) → VoxelGrid
    end
    Accum-->>CLI: PointCloud2 (PGO map)
Loading

Reviews (24): Last reviewed commit: "Merge branch 'main' into feat/ivan/pgo_r..." | Re-trigger Greptile

Comment thread dimos/memory2/observationstore/sqlite.py Outdated
Comment thread dimos/memory2/type/observation.py Outdated
leshy added 8 commits May 28, 2026 21:08
np.percentile sorts the full z array on every frame; min/max is O(n)
and avoids the per-frame cost for visualization color scaling.
map.py imported dimos.mapping/memory2 at module top, which transitively
pulls torch, transformers, open3d, sklearn. Since dimos.py eagerly imports
map to register the subcommand, dimos --help ballooned to ~7s and tripped
test_cli_startup. Move those imports into the function bodies; module top
keeps only the typer signature so --help still renders all options.
Comment thread dimos/memory2/type/observation.py
Comment thread dimos/memory2/stream.py
Comment thread dimos/utils/cli/map.py
@leshy leshy enabled auto-merge (squash) May 29, 2026 04:38
@leshy leshy merged commit 002d2a0 into main May 29, 2026
21 checks passed
@leshy leshy deleted the feat/ivan/pgo_rewrite branch May 29, 2026 04:54
bogwi added a commit that referenced this pull request May 29, 2026
bogwi added a commit that referenced this pull request May 29, 2026
leshy added a commit that referenced this pull request Jun 1, 2026
Integrate origin/main (PR #2242 loop_closure rewrite, #2278 aruco
Detection3D, #2316 docs/coding-agents rename, mem2 time windowing, etc.).
Took origin's marker-free PGO/PoseGraph rewrite for loop_closure (eval,
pgo, test_pgo, markers_rrd), marker_transformer, and the map CLI; kept
branch map_rrd. Stripped remaining # ---- section markers from map_rrd.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants