ReUseX  0.0.5
3D Point Cloud Processing for Building Reuse
Loading...
Searching...
No Matches
perturb_poses.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 #338: seeded, deterministic drift synthesis on a seed trajectory.
6//
7// WHY THIS EXISTS. Issue #221 Tier 2 asks for a capture that BOTH drifts and
8// has absolute ground truth, because only such a capture can adjudicate a pose
9// refinement stage. The benchmark suite has neither half in one scan:
10//
11// - the office / NewOffice captures drift heavily (~80% of the trajectory's
12// extent, docs/research/registration-improvements.md §9.5) but have no
13// ground-truth geometry, only the GT-FREE surface-consistency metrics; and
14// - the three ARKitScenes scans carry an absolute `3dod_mesh` GT but their
15// seeds are already accurate to 15.7-18.6 mm median with 2-9% edge
16// disagreement — they do not drift.
17//
18// On a scan that does not drift, the correct behaviour of every pose stage is
19// to do nothing, so every solver-side experiment since #298 (#310, #311, #316,
20// #337) has measured the benchmark rather than the solver. This module closes
21// that gap the cheap way: take the ARKitScenes seeds, inject a realistic and
22// exactly reproducible amount of drift, and keep the GT mesh — which lives in
23// the world frame and is untouched — as the adjudicator. A pose stage that
24// removes the synthetic drift now has somewhere to show it.
25//
26// WHAT "REALISTIC" MEANS HERE. Drift from a SLAM front-end is not a random
27// displacement of each pose; it is the accumulation of small errors in the
28// RELATIVE (odometry) chain, which is exactly why §8.3 found no outlier edge
29// for a robust kernel to reject. So the model perturbs each consecutive
30// relative pose and integrates:
31//
32// P'[0] = P[0] (gauge, see below)
33// P'[i+1] = P'[i] * (P[i]^-1 * P[i+1]) * Exp(xi_i)
34//
35// with the per-step twist xi_i built from two components, both scaled by the
36// step length so that drift grows with distance travelled rather than with
37// frame count:
38//
39// - a BIAS term, an Ornstein-Uhlenbeck process with a metric correlation
40// length. This is the systematic part — gyro bias, a scale error, a
41// miscalibrated depth unit — and it is what makes accumulated drift smooth
42// and curved rather than a jitter. Its rotational channel is the dominant
43// one in practice: a small constant yaw error integrates into a position
44// error that grows super-linearly with path length via the lever arm.
45// - a RANDOM-WALK term scaled by sqrt(step) so the relative errors compose
46// as Brownian motion, i.e. the classic "odometry error grows with the
47// square root of distance" behaviour.
48//
49// GAUGE. Frame 0 is left exactly on the seed. This is not cosmetic: the pose
50// graph's only absolute anchor is its tight prior on frame 0 (§8.1), so a
51// rigid displacement of the whole trajectory would be drift that no pose stage
52// could possibly recover, and the benchmark would measure an impossibility
53// rather than a capability. Anchoring frame 0 makes the injected error exactly
54// the DEFORMATION the solver is asked to undo.
55//
56// DETERMINISM (STANDARDS §6). Everything is a pure function of (poses, seed,
57// options). The noise sequence is drawn ONCE from a seeded std::mt19937 and
58// then rescaled by the calibration search, so the amplitude solve cannot
59// perturb the realisation it is calibrating.
60
61#pragma once
62
63#include <Eigen/Core>
64
65#include <vector>
66
67namespace reusex {
68class ProjectDB;
69}
70
71namespace reusex::geometry {
72
83 unsigned seed = 42;
84
87 double drift_scale = 1.0;
88
109 double target_drift_ratio = 0.20;
110
116
117 // --- Character of the drift (base gains, rescaled by the calibration) ----
121 double rot_bias_gain = 0.020;
123 double rot_walk_gain = 0.010;
125 double trans_bias_gain = 0.020;
127 double trans_walk_gain = 0.010;
131
132 // --- Calibration search --------------------------------------------------
138};
139
142 int frames = 0;
143 double trajectory_extent = 0.0;
144 double path_length = 0.0;
145 double amplitude = 0.0;
146 bool calibrated = false;
147
151 double drift_ratio = 0.0;
153
154 // Camera-centre displacement between the seed and drifted trajectories.
156 double max_position_error = 0.0;
157 double final_position_error = 0.0;
158 double max_rotation_error = 0.0;
159};
160
168PoseDriftResult perturb_trajectory(std::vector<Eigen::Matrix4d> &poses,
169 const PoseDriftOptions &opt);
170
178double median_pair_disagreement(const std::vector<Eigen::Matrix4d> &reference,
179 const std::vector<Eigen::Matrix4d> &other,
180 int min_frame_gap);
181
197 bool dry_run = false);
198
199} // namespace reusex::geometry
PoseDriftResult perturb_trajectory(std::vector< Eigen::Matrix4d > &poses, const PoseDriftOptions &opt)
Apply synthetic drift to a trajectory in place (the pure, database-free core — this is what the unit ...
PoseDriftResult perturb_sensor_poses(ProjectDB &db, const PoseDriftOptions &opt, bool dry_run=false)
Apply synthetic drift to every sensor-frame pose stored in db (#338).
double median_pair_disagreement(const std::vector< Eigen::Matrix4d > &reference, const std::vector< Eigen::Matrix4d > &other, int min_frame_gap)
Measure the median relative-pose disagreement between two trajectories over temporally distant frame ...
Parameters for synthetic trajectory drift (#338).
double target_drift_ratio
Target median relative-pose disagreement between the drifted and the seed trajectory over temporally ...
double bias_correlation_length
Correlation length of the bias process (m).
double trans_bias_gain
Systematic translational (scale) error, as a fraction of each step.
double trans_walk_gain
Translational random walk (m per sqrt(m)).
unsigned seed
RNG seed (STANDARDS §6).
double rot_bias_gain
Systematic rotational error per metre travelled (rad/m).
double rot_walk_gain
Rotational random walk (rad per sqrt(m)).
int calibration_iterations
Bisection steps used to solve for the amplitude.
double calibration_tolerance
Relative tolerance on the realised drift ratio.
double drift_scale
Multiplier on target_drift_ratio.
int min_frame_gap
Frame gap defining "temporally distant" for the metric above, mirroring LoopClosureOptions::min_frame...
What a drift-synthesis run did, in the units the benchmark reports.
double amplitude
solved multiplier on the base gains
double path_length
m, cumulative seed travel
double drift_ratio
Realised median relative-pose disagreement over temporally distant pairs, as a fraction of trajectory...
double median_pair_disagreement
m, numerator of the above
bool calibrated
the search reached the requested ratio
double trajectory_extent
m, bbox diagonal of the SEED centres
double final_position_error
m, last frame