Repository navigation
feat(relocalization): fiducial relocalization prior + pre-map marker aggregation - #3162
AaryanAgrawal wants to merge 41 commits into
Conversation
…ationModule A surveyed AprilTag becomes an absolute relocalization fix that competes in the same wall-fitness judge as RANSAC. Adds the prior pool + per-prior triggers, the Huber/Markley tag aggregation (live + --markers survey), and --eval per-source logging. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…oup+fuse map.py had its own group-by-marker_id + robust_cluster_pose loop, overlapping the aggregation module. Moved to apriltag_aggregation.aggregate_by_marker_id; the marker-map writer now sits beside load_marker_map in priors. map.py just builds PGO-corrected observations and calls them. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- unitree_go2: three reloc variants collapsed to the upstream unitree_go2_relocalization name (both priors declared; --disable / -o toggles instead of separate blueprints) - drop our aggregated_detections LCMTransport override: autoconnect wires the Python consumer; an explicit transport is only for cpp/rust or external modules - remove _MOVED_TO_PRIORS + its validator (extra=forbid already raises) - Pose7 defined once in apriltag_aggregation, replacing 6 spellings (fixes mypy strict) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…onfig default Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… comments Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e anchor Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…itations Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
AaryanAgrawal
left a comment
There was a problem hiding this comment.
Three notes on the decision points: the port wiring, the ambiguity gate, and the upright-pool refusal.
| ) | ||
| error, rvec, tvec = scored[0] | ||
| # error > 1e-12 px guards the ratio div-by-zero when the best pose fits the corners exactly | ||
| if len(scored) > 1 and error > 1e-12 and scored[1][0] / error < ambiguity_ratio_min: |
There was a problem hiding this comment.
This gate is what makes the fused pose usable. On a flip-contaminated set the full detect/gate/fuse/compose path recovers a known pose to 0.19 deg and 3 mm; with the gate off a flip majority corrupts it to 178 deg.
| # Gravity filter. An all-tilted pool is REFUSED, not resurrected -- a tilted winner is a rotationally-symmetric-floor mis-solve, not a valid pose. | ||
| upright = [T for T in candidates if _gravity_tilt_deg(T) <= gravity_tilt_max_deg] | ||
| if not upright: | ||
| raise NoUprightCandidateError( |
There was a problem hiding this comment.
Upstream fell back to the full pool when every candidate was tilted. Refusing instead: the best of an all-tilted pool is a rotationally-symmetric-floor mis-solve, and publishing it anchors the map to a wrong pose.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| # Prior objects are built ONCE, not per frame: the fiducial holds pending-fix state across bursts that a fresh instance would reset. The RANSAC prior is a pure source; the module owns its poll timer (below). | ||
| ransac_entry = next( |
There was a problem hiding this comment.
this can be done in a cleaner way. dict lookup or equivalent
| (p for p in self.config.priors.values() if isinstance(p, RansacPriorConfig)), | ||
| RansacPriorConfig(), | ||
| ) | ||
| self._ransac_prior = RansacPrior() |
There was a problem hiding this comment.
priors shouldn't be instantiated if they are not enabled
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| for d in dets: | ||
| c, o = d.data.center, d.data.orientation | ||
| # quality off the RAW pair: PGO moves camera and marker by the same per-frame correction, so camera-relative geometry is unchanged | ||
| distance_m, view_angle_deg = camera_relative_quality( |
There was a problem hiding this comment.
Smoothed surveys still bypass gates
When survey export uses the default positive --marker-smoothing, DetectMarkers clears each smoothed detection's camera transform, so camera_relative_quality returns missing distance and view-angle values and gate_reason skips both checks. Far or oblique sightings then enter the persisted marker aggregate, biasing the marker anchor used for relocalization.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| distance_m, view_angle_deg = camera_relative_quality( | ||
| d.data, (c.x, c.y, c.z, o.x, o.y, o.z, o.w) | ||
| ) |
There was a problem hiding this comment.
Smoothed surveys still bypass gates
When survey export uses the default positive marker-smoothing window, DetectMarkers clears each smoothed detection's camera transform, so camera_relative_quality returns missing distance and view-angle values and gate_reason skips both checks. Far or oblique sightings can then enter the persisted marker aggregate, causing fiducial relocalization to publish an incorrect world-to-map transform.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| aggregation = MarkerDetectionStreamModuleConfig.model_fields["aggregation"].get_default( | ||
| call_default_factory=True | ||
| ) | ||
| in_gate, rejected = _gate_observations(_marker_observations(all_dets, graph), aggregation) |
There was a problem hiding this comment.
Smoothed surveys bypass geometry gates
When survey export uses the default positive marker-smoothing window, DetectMarkers clears each smoothed detection's camera transform, so camera_relative_quality returns missing distance and view-angle values and gate_reason skips both checks. Far or oblique sightings then enter the persisted marker aggregate, biasing the marker anchor used to publish a world-to-map relocalization transform.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| distance_m, view_angle_deg = camera_relative_quality( | ||
| d.data, (c.x, c.y, c.z, o.x, o.y, o.z, o.w) |
There was a problem hiding this comment.
Smoothed surveys still bypass gates
When survey export uses the default positive marker-smoothing window, DetectMarkers clears each smoothed detection's camera transform, so camera_relative_quality returns missing distance and view-angle values and gate_reason skips both checks. Far or oblique sightings then enter the persisted marker aggregate, biasing the marker anchor used for relocalization.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| distance_m, view_angle_deg = camera_relative_quality( | ||
| d.data, (c.x, c.y, c.z, o.x, o.y, o.z, o.w) | ||
| ) |
There was a problem hiding this comment.
Smoothed surveys still bypass gates
When survey export uses the default positive marker-smoothing window, DetectMarkers clears each smoothed detection's camera transform, so camera_relative_quality returns missing distance and view-angle values and gate_reason skips both checks. Far or oblique sightings then enter the persisted marker aggregate, biasing the marker anchor used for relocalization.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| distance_m, view_angle_deg = camera_relative_quality( | ||
| d.data, (c.x, c.y, c.z, o.x, o.y, o.z, o.w) | ||
| ) |
There was a problem hiding this comment.
Smoothed surveys bypass geometry gates
When survey export uses the default positive --marker-smoothing window, DetectMarkers clears each smoothed detection's camera transform, so camera_relative_quality returns missing distance and view-angle values and both gates are skipped. Distant or oblique sightings then enter the persisted marker aggregate, causing fiducial relocalization to publish an incorrect world-to-map transform.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| distance_m, view_angle_deg = camera_relative_quality( | ||
| d.data, (c.x, c.y, c.z, o.x, o.y, o.z, o.w) | ||
| ) |
There was a problem hiding this comment.
Smoothed surveys still bypass gates
When survey export uses the default positive --marker-smoothing value, smoothing removes each detection's camera transform, so camera_relative_quality returns missing distance and view-angle values and gate_reason skips both checks. Distant or oblique sightings then enter the persisted marker aggregate, causing fiducial relocalization to publish an incorrect world-to-map transform.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| aggregation = MarkerDetectionStreamModuleConfig.model_fields["aggregation"].get_default( | ||
| call_default_factory=True | ||
| ) | ||
| in_gate, rejected = _gate_observations(_marker_observations(all_dets, graph), aggregation) |
There was a problem hiding this comment.
Smoothed surveys still bypass gates
When survey export uses the default positive --marker-smoothing window, DetectMarkers clears each emitted detection's camera transform, so _marker_observations receives missing camera-relative measurements and skips the distance and view-angle gates. Distant or oblique sightings then enter the persisted marker aggregate, causing fiducial relocalization to publish an incorrect world-to-map transform.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
This pull request has been automatically marked as stale because it has not had recent activity. It will be closed in 7 days if no further activity occurs. |
|
This pull request has been automatically closed because it has been stale for 30 days with no activity. Feel free to reopen it if you plan to continue working on it. |
Contribution path
Problem
What exists
world -> mapon/tf.dimos map global --markersdetects tags and overlays them in rerun. It does not aggregate them, and writes no marker map.Result
The fiducial fix landed at 0.867 in 0.1 s; RANSAC took 7.3 s to reach 0.755. Higher fitness, ~70x faster, and it fired 40 seconds before RANSAC could, while the submap was still under the point floor.
Solution
map_T_markerper id alongside the cloud.map_T_world;/tfcarries its inverse,world_T_map.source=.--evalraises skipped and rejected to warning and prints a per-prior table at stop.Breaking Changes
None
How to Test
Hardware (Go2). Run on the robot; see Result.
Take any premap recording and run it through once. One pass writes both the premap and the marker map:
--exportimplies--pgo.--marker-sizeis the printed tag edge in meters, default0.10. It sets metric scale at PnP, so a wrong value makes every marker pose wrong by a constant factor with no error.Then drive, three legs, same premap:
Read the first line before moving the robot:
fiducialunderinertmeans the marker map did not load.relocalize:line withsource=fiducial.Replay, no robot:
-o relocalizationmodule.priors.<key>.<field>:enabled,fitness_threshold,interval_s,min_local_points,marker_map_file.No test file is added. The one existing test edit is the detector's port assertion, which gains
aggregated_detections.Improvements
Next features
AI assistance
Claude Code with Claude Opus 4.8.
Checklist
uv run pytest, pre-commit) for the files I changed.