Skip to content

Commit b6fc7fb

Browse files
committed
Add artifact-first relocalization MVP pipeline
1 parent e6458d8 commit b6fc7fb

26 files changed

Lines changed: 7878 additions & 9 deletions

CMakeLists.txt

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,15 @@ target_link_libraries(lidar_localization_node
8383
${rclcpp_lifecycle_LIBRARIES}
8484
${std_msgs_LIBRARIES}
8585
)
86+
87+
add_executable(relocalization_ndt_score_jobs src/relocalization_ndt_score_jobs.cpp)
88+
ament_target_dependencies(relocalization_ndt_score_jobs
89+
ndt_omp_ros2
90+
)
91+
target_link_libraries(relocalization_ndt_score_jobs
92+
${ndt_omp_ros2_LIBRARIES}
93+
${PCL_LIBRARIES}
94+
)
8695
link_directories(
8796
${PCL_LIBRARY_DIRS}
8897
)
@@ -92,6 +101,7 @@ ament_export_libraries(lidar_localization_component)
92101

93102
install(TARGETS
94103
lidar_localization_node
104+
relocalization_ndt_score_jobs
95105
DESTINATION lib/${PROJECT_NAME})
96106

97107
install(TARGETS
@@ -115,6 +125,10 @@ install(PROGRAMS
115125
scripts/benchmark_eval_trajectory
116126
scripts/benchmark_eval_evo_ape
117127
scripts/augment_pointcloud_intensity.py
128+
scripts/make_disabled_relocalization_attempts.py
129+
scripts/observe_relocalization_reset_execution.py
130+
scripts/make_route_grid_relocalization_attempts.py
131+
scripts/make_registration_relocalization_jobs.py
118132
scripts/scaffold_mapless_public_dataset_bundle.py
119133
scripts/fetch_official_autoware_istanbul_dataset.sh
120134
scripts/fetch_koide_hard_pointcloud_localization_dataset.sh
@@ -129,6 +143,7 @@ install(PROGRAMS
129143
scripts/publish_odom_from_localization.py
130144
scripts/publish_odom_from_twist.py
131145
scripts/publish_pose_from_odom.py
146+
scripts/publish_relocalization_reset_commands.py
132147
scripts/republish_initialpose_on_reinit.py
133148
scripts/relay_localization_inputs_with_current_stamp.py
134149
scripts/record_hdl_localization_outputs.py
@@ -144,6 +159,17 @@ install(PROGRAMS
144159
scripts/run_nav2_demo_smoke
145160
scripts/run_nav2_replay_smoke
146161
scripts/send_nav2_goal.py
162+
scripts/summarize_localization_health.py
163+
scripts/summarize_relocalization_attempts.py
164+
scripts/summarize_registration_relocalization_scores.py
165+
scripts/compare_registration_relocalization_score_summaries.py
166+
scripts/run_registration_ordering_comparison.py
167+
scripts/make_relocalization_reset_commands.py
168+
scripts/select_relocalization_reset_candidates.py
169+
scripts/validate_relocalization_reset_commands.py
170+
scripts/validate_relocalization_reset_candidate_plan.py
171+
scripts/score_relocalization_candidates_with_reference.py
172+
scripts/resolve_registration_relocalization_scans.py
147173
scripts/run_autoware_istanbul_60s_predictor_compare.sh
148174
scripts/run_hdl_localization_docker_benchmark.sh
149175
DESTINATION lib/${PROJECT_NAME})

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Then choose the path that matches what you want to do:
2222
| Run a self-contained Nav2 smoke path | [Recommended entry points](docs/v1_status.md#recommended-entry-points) |
2323
| Run public replay/regression checks | [Benchmarking](#benchmarking) |
2424
| Evaluate a rosbag against reference poses | [Benchmarking guide](docs/benchmarking.md) |
25+
| Plan the next relocalization/recovery work | [v1.1 relocalization plan](docs/v1_1_relocalization.md) |
2526
| Develop or compare recovery behavior | [Experiment-First Development](#experiment-first-development) |
2627
| Check what `v1.0.0` does and does not claim | [v1 status](docs/v1_status.md) |
2728

@@ -98,6 +99,15 @@ This aggregates:
9899

99100
and writes a combined summary under `artifacts/public/release_regression_suite/`.
100101

102+
The v1.1 relocalization work is documented in
103+
[docs/v1_1_relocalization.md](docs/v1_1_relocalization.md). Its public endpoint is a validated
104+
dry-run `/initialpose` command artifact, not automatic reset publication. The guarded publisher and
105+
post-reset observation helpers are experimental controlled-test utilities and are outside the default
106+
public benchmark path.
107+
108+
Detailed health, relocalization, registration-scoring, and dry-run reset artifact commands live in
109+
[docs/benchmarking.md](docs/benchmarking.md#run-a-manifest-with-health-summary).
110+
101111
## IO
102112
- input
103113
/cloud (sensor_msgs/PointCloud2)

docs/benchmarking.md

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,79 @@ This repository now includes a small benchmark harness so that localization chan
1818
- Records `diagnostic_msgs/msg/DiagnosticArray` into CSV
1919
- `benchmark_eval_trajectory`
2020
- Compares an estimated trajectory CSV against a reference trajectory CSV
21+
- `summarize_localization_health.py`
22+
- Summarizes `alignment_status.csv` into recovery/reinitialization health metrics
23+
- Writes `health_summary.json` and `health_summary.md`
24+
- Tracks ok rate, failure-like rows, request windows, recovered/unrecovered windows, max accepted gap, and reject streaks
25+
- `benchmark_from_manifest` writes this automatically for single-run manifests unless `benchmark.write_health_summary: false`
26+
- `summarize_relocalization_attempts.py`
27+
- Summarizes experimental `relocalization_attempts.csv` artifacts and matches them to request windows from `alignment_status.csv`
28+
- Writes `relocalization_attempt_summary.json` and `relocalization_attempt_summary.md`
29+
- Records request-window coverage, accepted/rejected attempt counts, candidate counts, runtime stats, and false-recovery observability
30+
- Treats a missing attempts CSV as zero attempts when run through `benchmark_from_manifest`, which makes "no global relocalization yet" explicit in artifacts
31+
- `make_disabled_relocalization_attempts.py`
32+
- Converts each reinitialization request window in `alignment_status.csv` into one rejected baseline attempt
33+
- Writes `candidate_count=0`, `accepted=false`, and `rejection_reason=candidate_generator_disabled`
34+
- Useful for v1.1 public artifacts before a real candidate generator is enabled
35+
- `make_route_grid_relocalization_attempts.py`
36+
- Converts each request window into route-corridor candidate poses from a reference trajectory
37+
- Writes `relocalization_attempts.csv` plus per-candidate `relocalization_candidates.csv`
38+
- Does not score candidates, call registration, or reset pose; attempts remain rejected with `candidate_scoring_not_implemented`
39+
- Useful as the first non-zero-candidate artifact for the v1.1 relocalization pipeline
40+
- `score_relocalization_candidates_with_reference.py`
41+
- Scores candidate poses against a reference trajectory as an offline oracle
42+
- Writes `relocalization_candidate_scores.csv` and fills attempt `best_score`, `second_score`, and `candidate_margin`
43+
- Does not call registration or reset pose; attempts remain rejected with `offline_oracle_scored_no_reset`
44+
- Useful for separating "candidate set contains a plausible pose" from "runtime relocalization works"
45+
- `make_registration_relocalization_jobs.py`
46+
- Converts candidate artifacts into a fixed registration-scoring job CSV
47+
- Writes bag path, cloud topic, map path, candidate pose, backend label, local-map radius, voxel size, and timeout per job
48+
- Does not run registration or reset pose; it freezes the input contract for the next scorer implementation
49+
- `resolve_registration_relocalization_scans.py`
50+
- Resolves the nearest PointCloud2 scan for each registration job
51+
- Writes `relocalization_registration_scores.csv` with scan timestamps, time deltas, point counts, and scan bounds
52+
- Does not run registration or reset pose; rows are marked `scan_resolved_no_registration` when scan lookup succeeds
53+
- `relocalization_ndt_score_jobs`
54+
- Runs NDT_OMP for rows that already have resolved scan PCDs
55+
- Fills registration score, convergence, refinement delta, final pose, source point count, and target crop point count
56+
- Fills `registration_gate_passed` and `registration_gate_reason` from score/refinement thresholds
57+
- Does not accept or reset pose; scored rows are marked `registration_scored_no_reset`
58+
- `summarize_registration_relocalization_scores.py`
59+
- Summarizes `relocalization_registration_scores*.csv`
60+
- Writes scored/converged/gate-passed coverage plus score, refinement, runtime, source-point, and target-point stats
61+
- Keeps reset acceptance explicitly out of scope
62+
- `compare_registration_relocalization_score_summaries.py`
63+
- Compares two or more registration score summary JSON files
64+
- Useful for comparing candidate ordering strategies such as `candidate_index` versus `oracle_rank`
65+
- Treats `oracle_rank` comparisons as offline upper-bound diagnostics only
66+
- `run_registration_ordering_comparison.py`
67+
- Runs the bounded candidate-ordering diagnostic pipeline for `candidate_index` and `oracle_rank`
68+
- Generates jobs, resolves scans, runs NDT_OMP scoring, summarizes each order, and writes one comparison artifact
69+
- Still never accepts or resets pose; it is for top-K ordering diagnostics before any reset policy exists
70+
- `select_relocalization_reset_candidates.py`
71+
- Selects one gate-passing registration-scored candidate per attempt for a dry-run reset plan
72+
- Uses registration score/gate fields, not `oracle_rank`, in the default policy
73+
- Writes `accepted=false`; it is still a reset-plan artifact, not runtime reset execution
74+
- `validate_relocalization_reset_candidate_plan.py`
75+
- Validates reset-plan artifacts before they are used by any executor
76+
- Fails by default if any row has `accepted=true` or uses `oracle_rank` selection
77+
- Can cross-check selected job/candidate rows against the registration score CSV
78+
- `make_relocalization_reset_commands.py`
79+
- Converts selected reset-plan rows into dry-run `/initialpose` command artifacts
80+
- Uses `selected_final_pose_*` as the command pose; `selected_initial_pose_*` remains candidate-seed provenance
81+
- Does not publish or execute resets
82+
- `validate_relocalization_reset_commands.py`
83+
- Validates dry-run reset command artifacts
84+
- Requires `dry_run=true`, finite pose fields, near-unit quaternion, and zero published count by default
85+
- Rejects accepted source plans and `oracle_rank` provenance by default
86+
- `publish_relocalization_reset_commands.py`
87+
- Experimental guarded utility for `geometry_msgs/PoseWithCovarianceStamped` reset commands
88+
- Requires `--execute` for actual publishing
89+
- Without `--execute`, writes only an execution report with `published_count=0`
90+
- `observe_relocalization_reset_execution.py`
91+
- Observes post-reset recovery from reset execution CSV and `alignment_status.csv`
92+
- Counts post-reset `ok` rows, stable ok streak, reject streak, and first-ok latency
93+
- Sets `accepted=true` only when a published command is followed by enough stable `ok` rows
2194
- `benchmark_eval_evo_ape`
2295
- Converts the benchmark CSVs to TUM format and evaluates them with `evo_ape`
2396
- Useful when you need a paper-style `ATE` workflow instead of only the built-in RMSE JSON
@@ -98,6 +171,163 @@ Interpretation:
98171
- Istanbul has no accelerometer stream, so it is only a default-on no-IMU safety check, not an IMU benefit benchmark
99172
- HDL has no strong public ground truth and single-run pose-row ratios are noisy, so the suite uses broad smoke bounds there rather than a strict acceptance benchmark
100173

174+
### Run a manifest with health summary
175+
176+
Single-run manifests use `benchmark_from_manifest`. After replay and trajectory evaluation, the
177+
wrapper writes recovery diagnostics by default:
178+
179+
- `health_summary.json`
180+
- `health_summary.md`
181+
182+
Relevant manifest knobs:
183+
184+
```yaml
185+
benchmark:
186+
write_health_summary: true
187+
health_stable_ok_rows: 5
188+
health_recovery_window_sec: 10.0
189+
health_false_recovery_rmse_threshold_m: 5.0
190+
write_route_grid_relocalization_attempts: true
191+
route_grid_time_radius_sec: 15.0
192+
route_grid_min_spacing_m: 10.0
193+
route_grid_yaw_offsets_deg: [-15, 0, 15]
194+
route_grid_lateral_offsets_m: [-2.0, 0.0, 2.0]
195+
route_grid_longitudinal_offsets_m: [0.0]
196+
write_reference_oracle_relocalization_scores: true
197+
reference_oracle_translation_threshold_m: 2.0
198+
reference_oracle_yaw_threshold_deg: 15.0
199+
reference_oracle_yaw_weight_m_per_rad: 1.0
200+
write_registration_relocalization_jobs: true
201+
registration_jobs_method: NDT_OMP
202+
registration_jobs_max_candidates_per_attempt: 32
203+
registration_jobs_selection_source: candidate_index
204+
registration_jobs_voxel_leaf_size: 1.0
205+
registration_jobs_local_map_radius: 150.0
206+
registration_jobs_timeout_sec: 1.0
207+
write_registration_relocalization_scan_scores: true
208+
registration_scores_max_scan_time_diff: 0.25
209+
registration_scores_min_range: 1.0
210+
registration_scores_max_range: 120.0
211+
registration_scores_max_jobs: 32
212+
registration_scores_export_scan_pcd_dir: manifest://registration_scans
213+
write_ndt_relocalization_scores: false
214+
ndt_relocalization_max_jobs: 1
215+
ndt_relocalization_score_gate_threshold: 6.0
216+
ndt_relocalization_refinement_delta_gate_m: 2.0
217+
ndt_relocalization_refinement_yaw_gate_rad: 0.7853981633974483
218+
write_registration_relocalization_score_summary: true
219+
write_registration_ordering_comparison: false
220+
registration_ordering_top_k: 5
221+
registration_ordering_output_dir: manifest://registration_ordering_comparison
222+
write_relocalization_reset_candidate_plan: false
223+
relocalization_reset_candidate_plan_csv: manifest://relocalization_reset_candidate_plan.csv
224+
relocalization_reset_candidate_plan_min_score_margin: 0.0
225+
validate_relocalization_reset_candidate_plan: false
226+
relocalization_reset_candidate_plan_min_selected_count: 0
227+
write_relocalization_reset_command_dry_run: false
228+
relocalization_reset_commands_csv: manifest://relocalization_reset_commands.csv
229+
relocalization_reset_publish_topic: /initialpose
230+
relocalization_reset_frame_id: map
231+
validate_relocalization_reset_commands: false
232+
relocalization_reset_commands_min_generated_count: 0
233+
write_disabled_relocalization_attempts: false
234+
write_relocalization_attempt_summary: true
235+
relocalization_request_match_window_sec: 10.0
236+
relocalization_post_reset_stable_ok_rows: 5
237+
```
238+
239+
Disable it with `write_health_summary: false` only when the run intentionally has no
240+
`/alignment_status` recording.
241+
242+
Relocalization helpers write `relocalization_attempts.csv` into the output directory, and
243+
`benchmark_from_manifest` includes it in `relocalization_attempt_summary.json/md`.
244+
245+
The v1.1 relocalization endpoint is a validated dry-run `/initialpose` command artifact. Actual
246+
publication is guarded, opt-in, and outside the default public benchmark path. The minimal CSV schema
247+
is:
248+
249+
```csv
250+
attempt_id,trigger_stamp_sec,source,candidate_count,accepted,rejection_reason,runtime_sec
251+
```
252+
253+
Recommended optional fields are:
254+
255+
```csv
256+
start_stamp_sec,end_stamp_sec,mode,roi_type,accepted_candidate_rank,best_score,second_score,candidate_margin,overlap,converged,refinement_delta_m,refinement_delta_yaw_rad,post_reset_ok_rows,post_reset_window_sec,false_recovery
257+
```
258+
259+
`write_disabled_relocalization_attempts: true` generates a baseline attempts CSV only when one does
260+
not already exist. This preserves real candidate-generator output once the experimental relocalizer
261+
starts writing the same file.
262+
263+
`write_route_grid_relocalization_attempts: true` generates non-zero route-corridor candidates from
264+
`dataset.reference_csv` or `benchmark.route_grid_reference_csv`. The generated attempts still set
265+
`accepted=false`; the artifact only proves candidate coverage and keeps scoring/refinement/reset
266+
work separate from the public regression contract.
267+
268+
`write_reference_oracle_relocalization_scores: true` scores those candidates against the same
269+
reference trajectory and writes `relocalization_candidate_scores.csv`. This is an offline upper-bound
270+
diagnostic, not a runtime relocalization claim; it leaves attempts rejected and only fills score and
271+
oracle coverage fields.
272+
273+
`write_registration_relocalization_jobs: true` writes `relocalization_registration_jobs.csv` for the
274+
registration scorer. The default `registration_jobs_selection_source: candidate_index` avoids using
275+
oracle ordering in runtime-like jobs. Use `oracle_rank` only for offline upper-bound diagnostics.
276+
277+
`write_registration_relocalization_scan_scores: true` reads those jobs and resolves the nearest scan
278+
from the rosbag2 input. The resulting `relocalization_registration_scores.csv` is still a pre-scorer
279+
artifact: it verifies scan availability and timing, but keeps `converged=false` and score fields
280+
empty until a real registration backend is wired in.
281+
282+
The NDT scorer is intentionally explicit for now:
283+
284+
```bash
285+
ros2 run lidar_localization_ros2 relocalization_ndt_score_jobs \
286+
--input-csv /tmp/run/relocalization_registration_scores.csv \
287+
--output-csv /tmp/run/relocalization_registration_scores_ndt.csv \
288+
--max-jobs 1
289+
```
290+
291+
Use small `--max-jobs` values first. The scorer loads map crops and scan PCDs and can be much heavier
292+
than the CSV-only artifact steps.
293+
294+
In manifests, keep `write_ndt_relocalization_scores: false` for normal public replay artifacts and
295+
enable it only for bounded scorer smoke runs. Even when the gate passes, the row remains
296+
`registration_scored_no_reset`; accepted reset execution is outside the default public path.
297+
298+
When NDT scoring is enabled, `benchmark_from_manifest` also writes
299+
`relocalization_registration_score_summary.json/md` by default.
300+
301+
Set `write_registration_ordering_comparison: true` only for bounded diagnostic runs. It invokes
302+
`run_registration_ordering_comparison.py` after candidate artifacts exist and writes a separate
303+
`registration_ordering_comparison/` directory by default. This compares `candidate_index` and
304+
`oracle_rank` top-K ordering with NDT_OMP scoring, but still does not accept or reset pose.
305+
306+
After a scored CSV exists, `select_relocalization_reset_candidates.py` can produce the next artifact
307+
boundary: `relocalization_reset_candidate_plan.csv/json/md`. The default policy selects the
308+
lowest-score candidate that converged and passed the registration gate for each attempt. The plan
309+
keeps `accepted=false`; it is the handoff contract for the dry-run reset command artifact.
310+
311+
Set `write_relocalization_reset_candidate_plan: true` to emit that handoff artifact from a manifest.
312+
When ordering comparison is also enabled, the default input is the `candidate_index_topK` scored CSV,
313+
not the `oracle_rank` CSV. Otherwise the default input is `relocalization_registration_scores_ndt.csv`.
314+
315+
Set `validate_relocalization_reset_candidate_plan: true` to write
316+
`relocalization_reset_candidate_plan_validation.json/md` and fail the manifest run if the plan breaks
317+
the artifact-only safety contract.
318+
319+
Set `write_relocalization_reset_command_dry_run: true` to write
320+
`relocalization_reset_commands.csv/json/md`. These rows are publish-intent artifacts only:
321+
`published_count` remains zero, and the command pose is taken from `selected_final_pose_*`.
322+
323+
Set `validate_relocalization_reset_commands: true` to write
324+
`relocalization_reset_commands_validation.json/md` and fail if a command artifact is not dry-run safe.
325+
326+
The v1.1 example manifests are starter manifests. They stop before NDT scoring and reset-command
327+
generation by default so a normal public replay remains lightweight and cannot publish. To exercise
328+
the full v1.1 endpoint, run a bounded scorer smoke first, then explicitly enable reset-plan,
329+
dry-run command, and command-validation artifacts.
330+
101331
### Fetch a real benchmark dataset from hdl_localization
102332

103333
Use the helper script when you want a reproducible real LiDAR bag instead of the graph-derived synthetic smoke test.

0 commit comments

Comments
 (0)