Skip to content

Commit 9abbd45

Browse files
authored
Merge pull request #2445 from borglab/sim3-points
TrajectoryAlignerSim3 - add point3 constraints if available, optionally use GNC
2 parents f7dddec + 4c71396 commit 9abbd45

File tree

5 files changed

+166
-26
lines changed

5 files changed

+166
-26
lines changed

gtsam/sfm/TrajectoryAlignerSim3.cpp

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
* See LICENSE for the license information
55
* -------------------------------------------------------------------------- */
66

7-
#include <gtsam/sfm/TrajectoryAlignerSim3.h>
8-
97
#include <gtsam/inference/Symbol.h>
8+
#include <gtsam/nonlinear/GncOptimizer.h>
9+
#include <gtsam/nonlinear/LevenbergMarquardtOptimizer.h>
10+
#include <gtsam/sfm/TrajectoryAlignerSim3.h>
1011
#include <gtsam/slam/expressions.h>
1112

1213
#include <stdexcept>
@@ -22,7 +23,7 @@ struct MeasurementPair {
2223
};
2324

2425
std::vector<MeasurementPair> overlappingMeasurementPairs(
25-
const std::vector<UnaryMeasurement<Pose3>> &first,
26+
const std::vector<UnaryMeasurement<Pose3>> &first,
2627
const std::vector<UnaryMeasurement<Pose3>> &second) {
2728
std::unordered_map<Key, size_t> indexLookup;
2829
indexLookup.reserve(second.size());
@@ -41,7 +42,8 @@ std::vector<MeasurementPair> overlappingMeasurementPairs(
4142
return pairs;
4243
}
4344

44-
Similarity3 estimateInitialSim3(const std::vector<MeasurementPair> &overlapPairs) {
45+
Similarity3 estimateInitialSim3(
46+
const std::vector<MeasurementPair> &overlapPairs) {
4547
if (overlapPairs.size() < 2) return Similarity3();
4648
Pose3Pairs pairs;
4749
pairs.reserve(overlapPairs.size());
@@ -60,10 +62,15 @@ Similarity3 estimateInitialSim3(const std::vector<MeasurementPair> &overlapPairs
6062
} // namespace
6163

6264
namespace gtsam {
65+
6366
TrajectoryAlignerSim3::TrajectoryAlignerSim3(
64-
const std::vector<UnaryMeasurement<Pose3>> &aTi,
67+
const std::vector<UnaryMeasurement<Pose3>> &aTi,
6568
const std::vector<std::vector<UnaryMeasurement<Pose3>>> &bTi_all,
66-
const std::vector<Similarity3> &bSa_all) {
69+
const std::vector<Similarity3> &bSa_all, const bool use_gnc_optimizer,
70+
const std::vector<std::vector<std::pair<Point3, Point3>>>
71+
&overlapping_points,
72+
const double point3_factor_sigma)
73+
: use_gnc_optimizer_(use_gnc_optimizer) {
6774
const size_t childCount = bTi_all.size();
6875
if (!bSa_all.empty() && bSa_all.size() != childCount) {
6976
throw std::invalid_argument(
@@ -89,7 +96,7 @@ TrajectoryAlignerSim3::TrajectoryAlignerSim3(
8996
const auto overlap = overlappingMeasurementPairs(aTi, bTi);
9097
initial_.insert(simKey, estimateInitialSim3(overlap));
9198
}
92-
99+
93100
const Expression<Similarity3> bSa(simKey);
94101
for (const auto &meas : bTi) {
95102
Key cameraKey = meas.key();
@@ -100,18 +107,42 @@ TrajectoryAlignerSim3::TrajectoryAlignerSim3(
100107
graph_.addExpressionFactor(expected, meas.measured(), meas.noiseModel());
101108
}
102109
}
110+
111+
// Any point3-point3 constraints between overlapping points.
112+
if (!overlapping_points.empty()) {
113+
if (overlapping_points.size() != childCount) {
114+
throw std::invalid_argument(
115+
"TrajectoryAlignerSim3: overlapping_points and bTi_all sizes differ");
116+
}
117+
for (size_t childIdx = 0; childIdx < childCount; ++childIdx) {
118+
const auto &overlap = overlapping_points[childIdx];
119+
const Key simKey = Symbol('S', childIdx);
120+
const Expression<Similarity3> bSa(simKey);
121+
for (const auto &[p1, p2] : overlap) {
122+
const Point3_ p1_(p1);
123+
const Point3_ b_p1_(bSa, &Similarity3::transformFrom, p1_);
124+
graph_.addExpressionFactor(
125+
b_p1_, p2, noiseModel::Isotropic::Sigma(3, point3_factor_sigma));
126+
}
127+
}
128+
}
103129
}
104130

105131
Values TrajectoryAlignerSim3::solve() const {
106-
if (graph_.empty() || initial_.empty()) {
107-
return initial_;
132+
if (graph_.empty() || initial_.empty()) return initial_;
133+
134+
if (use_gnc_optimizer_) {
135+
GncOptimizer<GncParams<LevenbergMarquardtParams>> optimizer(graph_,
136+
initial_);
137+
return optimizer.optimize();
138+
} else {
139+
LevenbergMarquardtOptimizer optimizer(graph_, initial_);
140+
return optimizer.optimize();
108141
}
109-
LevenbergMarquardtOptimizer optimizer(graph_, initial_);
110-
return optimizer.optimize();
111142
}
112143

113144
Marginals TrajectoryAlignerSim3::marginalize(
114-
const Values& solution, const Ordering::OrderingType ordering_type) const {
145+
const Values &solution, const Ordering::OrderingType ordering_type) const {
115146
Ordering ordering = Ordering::Create(ordering_type, graph_);
116147
return Marginals(graph_, solution, ordering);
117148
}

gtsam/sfm/TrajectoryAlignerSim3.h

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
* @file TrajectoryAlignerSim3.h
1414
* @author Akshay Krishnan
1515
* @date January 2026
16-
* @brief Aligning a trajectory of poses to a reference trajectory using a similarity transform.
16+
* @brief Aligning a trajectory of poses to a reference trajectory using a
17+
* similarity transform.
1718
*/
1819

1920
#pragma once
@@ -54,38 +55,50 @@ class GTSAM_EXPORT TrajectoryAlignerSim3 {
5455
// Data members.
5556
ExpressionFactorGraph graph_;
5657
Values initial_;
58+
bool use_gnc_optimizer_;
5759

5860
public:
5961
/**
6062
* @brief Constructs a trajectory aligner with the given measurements.
6163
* @param aTi Parent frame pose measurements (key-value pairs with noise)
62-
* @param bTi_all Vector of child frame pose measurements, one vector per child
63-
* @param bSa_all Initial Sim3 estimates transforming from parent to each child.
64-
* If empty, initial estimates are computed automatically.
64+
* @param bTi_all Vector of child frame pose measurements, one vector per
65+
* child
66+
* @param bSa_all Initial Sim3 estimates transforming from parent to each
67+
* child. If empty, initial estimates are computed automatically.
68+
* @param use_gnc_optimizer Whether to use GncOptimizer for optimization.
69+
* @param overlapping_points Vector of overlapping point3-point3 constraints,
70+
* one vector per child.
71+
* @param point3_factor_sigma Sigma for point3-point3 constraints.
6572
*/
6673
TrajectoryAlignerSim3(
67-
const std::vector<UnaryMeasurement<Pose3>> &aTi,
68-
const std::vector<std::vector<UnaryMeasurement<Pose3>>> &bTi_all,
69-
const std::vector<Similarity3> &bSa_all = {});
74+
const std::vector<UnaryMeasurement<Pose3>> &aTi,
75+
const std::vector<std::vector<UnaryMeasurement<Pose3>>> &bTi_all,
76+
const std::vector<Similarity3> &bSa_all = {},
77+
const bool use_gnc_optimizer = false,
78+
const std::vector<std::vector<std::pair<Point3, Point3>>>
79+
&overlapping_points = {},
80+
const double point3_factor_sigma = 1e-2);
7081

7182
/**
7283
* @brief Optimizes the graph and returns optimized poses and Sim3 transforms.
7384
* @return The optimized poses and transforms. Contains:
74-
* - Parent frame poses (with keys the same as those in input aTi measurements)
85+
* - Parent frame poses (with keys the same as those in input aTi
86+
* measurements)
7587
* - Sim3 transforms (with keys Symbol('S', i), where i is the child index)
7688
*/
7789
Values solve() const;
7890

7991
/**
8092
* @brief Computes the marginals of the solution.
81-
*
82-
* @param solution The solution to marginalize.
83-
* Obtained from solve() or should contain the same keys as variables in graph_.
93+
*
94+
* @param solution The solution to marginalize.
95+
* Obtained from solve() or should contain the same keys as variables in
96+
* graph_.
8497
* @param ordering_type The ordering type to use for the marginalization.
8598
* @return The Marginals object.
8699
*/
87100
Marginals marginalize(
88-
const Values& solution,
89-
const Ordering::OrderingType ordering_type = Ordering::COLAMD) const;
101+
const Values &solution,
102+
const Ordering::OrderingType ordering_type = Ordering::COLAMD) const;
90103
};
91104
} // namespace gtsam

gtsam/sfm/sfm.i

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,24 @@ class TrajectoryAlignerSim3 {
179179
const std::vector<std::vector<gtsam::UnaryMeasurement<gtsam::Pose3>>>& bTi_all,
180180
const std::vector<gtsam::Similarity3>& bSa_all);
181181

182+
TrajectoryAlignerSim3(
183+
const std::vector<gtsam::UnaryMeasurement<gtsam::Pose3>>& aTi,
184+
const std::vector<std::vector<gtsam::UnaryMeasurement<gtsam::Pose3>>>& bTi_all,
185+
const std::vector<gtsam::Similarity3>& bSa_all, const bool use_gnc_optimizer);
186+
187+
TrajectoryAlignerSim3(
188+
const std::vector<gtsam::UnaryMeasurement<gtsam::Pose3>>& aTi,
189+
const std::vector<std::vector<gtsam::UnaryMeasurement<gtsam::Pose3>>>& bTi_all,
190+
const std::vector<gtsam::Similarity3>& bSa_all, const bool use_gnc_optimizer,
191+
const std::vector<std::vector<std::pair<gtsam::Point3, gtsam::Point3>>> &overlapping_points);
192+
193+
TrajectoryAlignerSim3(
194+
const std::vector<gtsam::UnaryMeasurement<gtsam::Pose3>>& aTi,
195+
const std::vector<std::vector<gtsam::UnaryMeasurement<gtsam::Pose3>>>& bTi_all,
196+
const std::vector<gtsam::Similarity3>& bSa_all, const bool use_gnc_optimizer,
197+
const std::vector<std::vector<std::pair<gtsam::Point3, gtsam::Point3>>> &overlapping_points,
198+
const double point3_factor_sigma);
199+
182200
gtsam::Values solve() const;
183201
gtsam::Marginals marginalize(
184202
const gtsam::Values& solution,

gtsam/sfm/tests/testTrajectoryAlignerSim3.cpp

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
#include <gtsam/slam/expressions.h>
2525
#include <gtsam/nonlinear/utilities.h>
2626

27-
#include <random>
2827
#include <vector>
2928

3029
using namespace gtsam;
@@ -91,6 +90,10 @@ bool simClose(const Similarity3& expected, const Similarity3& actual,
9190
return assert_equal<Similarity3>(expected, actual, tol);
9291
}
9392

93+
double simErrorNorm(const Similarity3& expected, const Similarity3& actual) {
94+
return Similarity3::Logmap(expected.inverse() * actual).norm();
95+
}
96+
9497
} // namespace
9598

9699
/* ************************************************************************* */
@@ -236,6 +239,52 @@ TEST(TrajectoryAlignerSim3, ThreeChildrenNoisy) {
236239
}
237240
}
238241

242+
TEST(TrajectoryAlignerSim3, TwoChildrenWithOverlappingPoints) {
243+
const auto parent = makeParentValues();
244+
const auto child1 = transformValues(gtSim1, parent);
245+
const auto child2 = transformValues(gtSim2, parent);
246+
247+
// Add noise to pose measurements only.
248+
PoseMeasurements aTi = makeMeasurements(perturbPoses(parent), 8e-2);
249+
PoseMeasurements b1 = makeMeasurements(perturbPoses(child1), 8e-2);
250+
PoseMeasurements b2 = makeMeasurements(perturbPoses(child2), 8e-2);
251+
ChildrenPoses bTi_all{b1, b2};
252+
253+
std::vector<Similarity3> sims{perturbSim3(gtSim1), perturbSim3(gtSim2)};
254+
255+
// 4 exact overlapping Point3 correspondences per child.
256+
const std::vector<Point3> parentPoints{
257+
Point3(0.25, -0.10, 0.80), Point3(1.00, 0.30, -0.20),
258+
Point3(-0.40, 0.50, 0.15), Point3(0.70, -0.60, 0.25)};
259+
260+
std::vector<std::pair<Point3, Point3>> overlap1, overlap2;
261+
overlap1.reserve(parentPoints.size());
262+
overlap2.reserve(parentPoints.size());
263+
for (const auto& pA : parentPoints) {
264+
overlap1.emplace_back(pA, gtSim1.transformFrom(pA));
265+
overlap2.emplace_back(pA, gtSim2.transformFrom(pA));
266+
}
267+
std::vector<std::vector<std::pair<Point3, Point3>>> overlappingPoints{
268+
overlap1, overlap2};
269+
270+
// Optimize without point correspondences.
271+
TrajectoryAlignerSim3 alignerNoPoints(aTi, bTi_all, sims);
272+
const Values resultNoPoints = alignerNoPoints.solve();
273+
const double simErrorNoPoints =
274+
simErrorNorm(gtSim1, resultNoPoints.at<Similarity3>(Symbol('S', 0))) +
275+
simErrorNorm(gtSim2, resultNoPoints.at<Similarity3>(Symbol('S', 1)));
276+
277+
// Optimize with exact point correspondences.
278+
TrajectoryAlignerSim3 alignerWithPoints(aTi, bTi_all, sims, false, overlappingPoints,
279+
1e-3);
280+
const Values resultWithPoints = alignerWithPoints.solve();
281+
const double simErrorWithPoints =
282+
simErrorNorm(gtSim1, resultWithPoints.at<Similarity3>(Symbol('S', 0))) +
283+
simErrorNorm(gtSim2, resultWithPoints.at<Similarity3>(Symbol('S', 1)));
284+
285+
EXPECT(simErrorWithPoints < simErrorNoPoints);
286+
}
287+
239288
/* ************************************************************************* */
240289
int main() {
241290
TestResult tr;

python/gtsam/tests/test_TrajectoryAlignerSim3.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,35 @@ def test_perfect_alignment_without_initial_sim(self):
5959
recovered = result.atSimilarity3(gtsam.Symbol("S", 0).key())
6060
self.assertTrue(gt_bSa.equals(recovered, 1e-6))
6161

62+
def test_perfect_alignment_without_initial_sim_with_points(self):
63+
parent = make_parent_poses()
64+
gt_bSa = gtsam.Similarity3(
65+
gtsam.Rot3.RzRyRx(0.2, -0.1, 0.05),
66+
gtsam.Point3(0.3, -0.2, 0.1),
67+
1.4,
68+
)
69+
child = transform_poses(gt_bSa, parent)
70+
71+
aTi = make_measurements(parent)
72+
bTi_all = [make_measurements(child)]
73+
74+
parent_points = [
75+
gtsam.Point3(0.25, -0.10, 0.80),
76+
gtsam.Point3(1.00, 0.30, -0.20),
77+
gtsam.Point3(-0.40, 0.50, 0.15),
78+
gtsam.Point3(0.70, -0.60, 0.25),
79+
]
80+
overlap_child = [(p, gt_bSa.transformFrom(p)) for p in parent_points]
81+
overlapping_points = [overlap_child]
82+
83+
aligner = gtsam.TrajectoryAlignerSim3(
84+
aTi, bTi_all, [], False, overlapping_points, 1e-3
85+
)
86+
result = aligner.solve()
87+
88+
recovered = result.atSimilarity3(gtsam.Symbol("S", 0).key())
89+
self.assertTrue(gt_bSa.equals(recovered, 1e-6))
90+
6291
def test_noisy_alignment_with_initial_guess(self):
6392
parent = make_parent_poses()
6493
gt_bSa = gtsam.Similarity3(

0 commit comments

Comments
 (0)