ReUseX  0.0.5
3D Point Cloud Processing for Building Reuse
Loading...
Searching...
No Matches
PanoramaLoopEdges.hpp
Go to the documentation of this file.
1// SPDX-FileCopyrightText: 2026 Povl Filip Sonne-Frederiksen
2//
3// SPDX-License-Identifier: GPL-3.0-or-later
4//
5// Issue #236: wide-baseline pose-graph loop edges derived from 360 panoramas.
6//
7// WHY A PANORAMA IS THE RIGHT SENSOR FOR THIS
8// -------------------------------------------
9// The plane-landmark back-end (PlaneGraphOptimizer) makes frames that
10// co-observe the same wall mutually consistent, but two temporally distant
11// views of the same place that share no plane-landmark chain are left
12// unconstrained — the "basin problem" of
13// docs/research/registration-improvements.md. The ORB front-end in
14// LoopClosure.hpp attacks that by matching frame PAIRS, which requires the two
15// narrow-FOV frames to overlap each other.
16//
17// A 360 panorama sees every direction at once, so a SINGLE panorama routinely
18// matches frames that do not overlap each other at all — including frames from
19// opposite ends of the capture. Each such panorama is therefore a hub that ties
20// a whole group of temporally distant frames together.
21//
22// WHAT MAKES THE EDGE INFORMATIVE (the one thing to get right)
23// ------------------------------------------------------------
24// `PanoramaAlignment` resects ONE panorama pose in WORLD coordinates, from
25// frame keypoints already transformed to world by their (drifted) seed poses.
26// An edge built from that pose would be circular: `T_pano_A^-1 · T_pano_B`
27// would reproduce `seed(A)^-1 · seed(B)` and carry exactly ZERO
28// drift-correction information — it would restate the drift rather than measure
29// it.
30//
31// This module therefore resects the panorama INDEPENDENTLY against each matched
32// frame, using that frame's keypoints in the frame's OWN optical coordinates:
33//
34// T_pano_A : pano-from-A, from A's correspondences alone
35// T_pano_B : pano-from-B, from B's correspondences alone
36// T_AB = T_pano_A^-1 · T_pano_B (== pose(A)^-1 · pose(B))
37//
38// Neither resection reads `sensor_frame_pose`, so `T_AB` is a genuine metric
39// measurement of where B sits relative to A. The panorama is a shared, rigid
40// intermediate coordinate frame — it never has to be correctly placed in world.
41// (A useful consequence: `rux align 360` is NOT a prerequisite. Panorama poses
42// are neither read nor written here.)
43//
44// GUARDRAILS: nothing new is invented. The emitted LoopEdges are the SAME type
45// the ORB front-end emits, are gated by the SAME LoopClosureOptions knobs
46// (`min_frame_gap`, `min_seed_disagreement`, `max_seed_disagreement`), are
47// PCM-filtered as part of the UNION with the other edge sources, and enter the
48// SAME GncOptimizer graph — so a wrong panorama edge is down-weighted rather
49// than corrupting the solution.
50//
51// This header carries no GTSAM and no OpenCV include (docs/STANDARDS.md §2).
52
53#pragma once
54
55#include "reusex/slam/LoopClosure.hpp"
56
57#include <Eigen/Core>
58
59#include <vector>
60
61namespace reusex {
62class ProjectDB;
63}
64
65namespace reusex::geometry {
66
74 bool enable = false;
75
76 // --- candidate frames ----------------------------------------------------
84 int max_frames = 240;
88
89 // --- panorama slicing (geometry/EquirectProjection) ----------------------
90 int n_yaw = 8;
91 double fov_deg = 90.0;
92 int slice = 1008;
93
94 // --- ORB front-end -------------------------------------------------------
95 int max_features = 3000;
96 float ratio_test = 0.85f;
97 float min_depth = 0.3f;
98 float max_depth = 6.0f;
99
100 // --- per-frame resection -------------------------------------------------
113 float ransac_reproj_px = 5.0f;
122 double max_pano_distance = 8.0;
123
124 // --- edge emission -------------------------------------------------------
135 float base_sigma_trans = 0.15f;
136 float base_sigma_rot = 0.06f;
137 float min_sigma_trans = 0.05f;
138 float min_sigma_rot = 0.025f;
139
140 unsigned seed = 42;
141};
142
156
180std::vector<LoopEdge>
181detect_panorama_loop_edges(ProjectDB &db, const std::vector<int> &node_ids,
182 const std::vector<Eigen::Matrix4d> &seed_poses,
183 const PanoramaLoopOptions &options,
184 const LoopClosureOptions &gates,
185 PanoramaLoopResult *out_result = nullptr);
186
190namespace detail {
191
194 int frame = -1;
197 Eigen::Matrix4d T_pano_frame = Eigen::Matrix4d::Identity();
198 int inliers = 0;
199};
200
212std::vector<LoopEdge>
213edges_from_resections(std::vector<PanoResection> resections,
214 const std::vector<Eigen::Matrix4d> &seed_poses,
215 const PanoramaLoopOptions &opt,
216 const LoopClosureOptions &gates,
217 PanoramaLoopResult *stats = nullptr);
218
219} // namespace detail
220
221} // namespace reusex::geometry
Internals of detect_loop_edges, exposed ONLY so the geometry stages of the pipeline can be regression...
std::vector< LoopEdge > edges_from_resections(std::vector< PanoResection > resections, const std::vector< Eigen::Matrix4d > &seed_poses, const PanoramaLoopOptions &opt, const LoopClosureOptions &gates, PanoramaLoopResult *stats=nullptr)
Turn one panorama's per-frame resections into gated loop edges.
std::vector< LoopEdge > detect_panorama_loop_edges(ProjectDB &db, const std::vector< int > &node_ids, const std::vector< Eigen::Matrix4d > &seed_poses, const PanoramaLoopOptions &options, const LoopClosureOptions &gates, PanoramaLoopResult *out_result=nullptr)
Detect panorama-derived wide-baseline loop edges.
Parameters for wide-baseline loop-edge detection.
Parameters for panorama-derived loop-edge detection.
double fov_deg
per-slice horizontal FOV (overlapping)
double max_pano_distance
Reject a resection whose panorama centre sits further than this from the frame (m).
int refine_iterations
bearing-space Gauss-Newton steps
int max_edges_per_panorama
Cap the edges contributed per panorama (highest joint inlier support first).
int max_features
ORB features per image.
float min_sigma_trans
floor on the translational std (m)
int max_frames
Panorama alignment matches a temporal window around the timestamp-seed frame (PanoramaAlignmentOption...
float ransac_reproj_px
solvePnPRansac reprojection threshold
float min_sigma_rot
floor on the rotational std (rad)
float base_sigma_trans
Base sigmas at min_frame_inliers support, shrinking as sqrt(min_frame_inliers / inliers) down to the ...
int n_yaw
perspective slices around the equator
int min_slice_correspondences
A single (slice, frame) pair needs this many correspondences to seed the resection with solvePnPRansa...
bool enable
Off by default; opt in with rux optimize --loop-closure --use-panoramas.
float min_depth
ignore frame keypoints outside [min,max] (m)
int min_frame_inliers
Accept a frame's independent resection above this many gated inliers.
int min_frame_correspondences
A frame needs this many slice correspondences before resection is attempted.
unsigned seed
RANSAC determinism (docs/STANDARDS.md §6).
int min_candidate_frames
Skip frames whose colour/depth cannot be read rather than failing; a scan with fewer than this many u...
Statistics from panorama loop-edge detection.
int proposed
candidate frame pairs before gating
int dropped_seed_gate
rejected: min/max seed-disagreement
int frames_resected
accepted independent per-frame resections
int dropped_gap
rejected: closer than min_frame_gap
int total_inliers
summed supporting inliers
int dropped_cap
rejected: max_edges_per_panorama
int panoramas_matched
panoramas that resected >= 2 frames
int panoramas
panoramas in the project
One panorama's independent resection against one frame.
Eigen::Matrix4d T_pano_frame
pano-from-frame: maps a point in the frame's optical coordinates into the panorama's coordinates.
int frame
index into the optimizer's frame vector
int inliers
gated bearing inliers supporting this resection