Skip to content

Commit c2c54bc

Browse files
authored
Merge pull request #1148 from borglab/feature/transformAll
2 parents a9a4075 + 628ffb1 commit c2c54bc

File tree

7 files changed

+164
-8
lines changed

7 files changed

+164
-8
lines changed

gtsam/geometry/Pose2.cpp

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,14 @@ Point2 Pose2::transformTo(const Point2& point,
213213
return q;
214214
}
215215

216+
Matrix Pose2::transformTo(const Matrix& points) const {
217+
if (points.rows() != 2) {
218+
throw std::invalid_argument("Pose2:transformTo expects 2*N matrix.");
219+
}
220+
const Matrix2 Rt = rotation().transpose();
221+
return Rt * (points.colwise() - t_); // Eigen broadcasting!
222+
}
223+
216224
/* ************************************************************************* */
217225
// see doc/math.lyx, SE(2) section
218226
Point2 Pose2::transformFrom(const Point2& point,
@@ -224,6 +232,15 @@ Point2 Pose2::transformFrom(const Point2& point,
224232
return q + t_;
225233
}
226234

235+
236+
Matrix Pose2::transformFrom(const Matrix& points) const {
237+
if (points.rows() != 2) {
238+
throw std::invalid_argument("Pose2:transformFrom expects 2*N matrix.");
239+
}
240+
const Matrix2 R = rotation().matrix();
241+
return (R * points).colwise() + t_; // Eigen broadcasting!
242+
}
243+
227244
/* ************************************************************************* */
228245
Rot2 Pose2::bearing(const Point2& point,
229246
OptionalJacobian<1, 3> Hpose, OptionalJacobian<1, 2> Hpoint) const {

gtsam/geometry/Pose2.h

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,13 +199,29 @@ class Pose2: public LieGroup<Pose2, 3> {
199199
OptionalJacobian<2, 3> Dpose = boost::none,
200200
OptionalJacobian<2, 2> Dpoint = boost::none) const;
201201

202+
/**
203+
* @brief transform many points in world coordinates and transform to Pose.
204+
* @param points 2*N matrix in world coordinates
205+
* @return points in Pose coordinates, as 2*N Matrix
206+
*/
207+
Matrix transformTo(const Matrix& points) const;
208+
202209
/** Return point coordinates in global frame */
203210
GTSAM_EXPORT Point2 transformFrom(const Point2& point,
204211
OptionalJacobian<2, 3> Dpose = boost::none,
205212
OptionalJacobian<2, 2> Dpoint = boost::none) const;
206213

214+
/**
215+
* @brief transform many points in Pose coordinates and transform to world.
216+
* @param points 2*N matrix in Pose coordinates
217+
* @return points in world coordinates, as 2*N Matrix
218+
*/
219+
Matrix transformFrom(const Matrix& points) const;
220+
207221
/** syntactic sugar for transformFrom */
208-
inline Point2 operator*(const Point2& point) const { return transformFrom(point);}
222+
inline Point2 operator*(const Point2& point) const {
223+
return transformFrom(point);
224+
}
209225

210226
/// @}
211227
/// @name Standard Interface

gtsam/geometry/Pose3.cpp

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,14 @@ Point3 Pose3::transformFrom(const Point3& point, OptionalJacobian<3, 6> Hself,
354354
return R_ * point + t_;
355355
}
356356

357+
Matrix Pose3::transformFrom(const Matrix& points) const {
358+
if (points.rows() != 3) {
359+
throw std::invalid_argument("Pose3:transformFrom expects 3*N matrix.");
360+
}
361+
const Matrix3 R = R_.matrix();
362+
return (R * points).colwise() + t_; // Eigen broadcasting!
363+
}
364+
357365
/* ************************************************************************* */
358366
Point3 Pose3::transformTo(const Point3& point, OptionalJacobian<3, 6> Hself,
359367
OptionalJacobian<3, 3> Hpoint) const {
@@ -374,6 +382,14 @@ Point3 Pose3::transformTo(const Point3& point, OptionalJacobian<3, 6> Hself,
374382
return q;
375383
}
376384

385+
Matrix Pose3::transformTo(const Matrix& points) const {
386+
if (points.rows() != 3) {
387+
throw std::invalid_argument("Pose3:transformTo expects 3*N matrix.");
388+
}
389+
const Matrix3 Rt = R_.transpose();
390+
return Rt * (points.colwise() - t_); // Eigen broadcasting!
391+
}
392+
377393
/* ************************************************************************* */
378394
double Pose3::range(const Point3& point, OptionalJacobian<1, 6> Hself,
379395
OptionalJacobian<1, 3> Hpoint) const {
@@ -431,7 +447,7 @@ Unit3 Pose3::bearing(const Pose3& pose, OptionalJacobian<2, 6> Hself,
431447
boost::optional<Pose3> Pose3::Align(const Point3Pairs &abPointPairs) {
432448
const size_t n = abPointPairs.size();
433449
if (n < 3) {
434-
return boost::none; // we need at least three pairs
450+
return boost::none; // we need at least three pairs
435451
}
436452

437453
// calculate centroids
@@ -451,6 +467,18 @@ boost::optional<Pose3> Pose3::Align(const Point3Pairs &abPointPairs) {
451467
return Pose3(aRb, aTb);
452468
}
453469

470+
boost::optional<Pose3> Pose3::Align(const Matrix& a, const Matrix& b) {
471+
if (a.rows() != 3 || b.rows() != 3 || a.cols() != b.cols()) {
472+
throw std::invalid_argument(
473+
"Pose3:Align expects 3*N matrices of equal shape.");
474+
}
475+
Point3Pairs abPointPairs;
476+
for (size_t j=0; j < a.cols(); j++) {
477+
abPointPairs.emplace_back(a.col(j), b.col(j));
478+
}
479+
return Pose3::Align(abPointPairs);
480+
}
481+
454482
boost::optional<Pose3> align(const Point3Pairs &baPointPairs) {
455483
Point3Pairs abPointPairs;
456484
for (const Point3Pair &baPair : baPointPairs) {

gtsam/geometry/Pose3.h

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ class GTSAM_EXPORT Pose3: public LieGroup<Pose3, 6> {
8585
*/
8686
static boost::optional<Pose3> Align(const std::vector<Point3Pair>& abPointPairs);
8787

88+
// Version of Pose3::Align that takes 2 matrices.
89+
static boost::optional<Pose3> Align(const Matrix& a, const Matrix& b);
90+
8891
/// @}
8992
/// @name Testable
9093
/// @{
@@ -249,6 +252,13 @@ class GTSAM_EXPORT Pose3: public LieGroup<Pose3, 6> {
249252
Point3 transformFrom(const Point3& point, OptionalJacobian<3, 6> Hself =
250253
boost::none, OptionalJacobian<3, 3> Hpoint = boost::none) const;
251254

255+
/**
256+
* @brief transform many points in Pose coordinates and transform to world.
257+
* @param points 3*N matrix in Pose coordinates
258+
* @return points in world coordinates, as 3*N Matrix
259+
*/
260+
Matrix transformFrom(const Matrix& points) const;
261+
252262
/** syntactic sugar for transformFrom */
253263
inline Point3 operator*(const Point3& point) const {
254264
return transformFrom(point);
@@ -264,6 +274,13 @@ class GTSAM_EXPORT Pose3: public LieGroup<Pose3, 6> {
264274
Point3 transformTo(const Point3& point, OptionalJacobian<3, 6> Hself =
265275
boost::none, OptionalJacobian<3, 3> Hpoint = boost::none) const;
266276

277+
/**
278+
* @brief transform many points in world coordinates and transform to Pose.
279+
* @param points 3*N matrix in world coordinates
280+
* @return points in Pose coordinates, as 3*N Matrix
281+
*/
282+
Matrix transformTo(const Matrix& points) const;
283+
267284
/// @}
268285
/// @name Standard Interface
269286
/// @{

gtsam/geometry/geometry.i

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,10 @@ class Pose2 {
406406
gtsam::Point2 transformFrom(const gtsam::Point2& p) const;
407407
gtsam::Point2 transformTo(const gtsam::Point2& p) const;
408408

409+
// Matrix versions
410+
Matrix transformFrom(const Matrix& points) const;
411+
Matrix transformTo(const Matrix& points) const;
412+
409413
// Standard Interface
410414
double x() const;
411415
double y() const;
@@ -431,6 +435,9 @@ class Pose3 {
431435
Pose3(const gtsam::Pose2& pose2);
432436
Pose3(Matrix mat);
433437

438+
static boost::optional<gtsam::Pose3> Align(const gtsam::Point3Pairs& abPointPairs);
439+
static boost::optional<gtsam::Pose3> Align(const gtsam::Matrix& a, const gtsam::Matrix& b);
440+
434441
// Testable
435442
void print(string s = "") const;
436443
bool equals(const gtsam::Pose3& pose, double tol) const;
@@ -470,6 +477,10 @@ class Pose3 {
470477
gtsam::Point3 transformFrom(const gtsam::Point3& point) const;
471478
gtsam::Point3 transformTo(const gtsam::Point3& point) const;
472479

480+
// Matrix versions
481+
Matrix transformFrom(const Matrix& points) const;
482+
Matrix transformTo(const Matrix& points) const;
483+
473484
// Standard Interface
474485
gtsam::Rot3 rotation() const;
475486
gtsam::Point3 translation() const;

python/gtsam/tests/test_Pose2.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@
88
Pose2 unit tests.
99
Author: Frank Dellaert & Duy Nguyen Ta & John Lambert
1010
"""
11+
import math
1112
import unittest
1213

13-
import numpy as np
14-
1514
import gtsam
15+
import numpy as np
1616
from gtsam import Point2, Point2Pairs, Pose2
1717
from gtsam.utils.test_case import GtsamTestCase
1818

@@ -26,6 +26,34 @@ def test_adjoint(self) -> None:
2626
actual = Pose2.adjoint_(xi, xi)
2727
np.testing.assert_array_equal(actual, expected)
2828

29+
def test_transformTo(self):
30+
"""Test transformTo method."""
31+
pose = Pose2(2, 4, -math.pi/2)
32+
actual = pose.transformTo(Point2(3, 2))
33+
expected = Point2(2, 1)
34+
self.gtsamAssertEquals(actual, expected, 1e-6)
35+
36+
# multi-point version
37+
points = np.stack([Point2(3, 2), Point2(3, 2)]).T
38+
actual_array = pose.transformTo(points)
39+
self.assertEqual(actual_array.shape, (2, 2))
40+
expected_array = np.stack([expected, expected]).T
41+
np.testing.assert_allclose(actual_array, expected_array, atol=1e-6)
42+
43+
def test_transformFrom(self):
44+
"""Test transformFrom method."""
45+
pose = Pose2(2, 4, -math.pi/2)
46+
actual = pose.transformFrom(Point2(2, 1))
47+
expected = Point2(3, 2)
48+
self.gtsamAssertEquals(actual, expected, 1e-6)
49+
50+
# multi-point version
51+
points = np.stack([Point2(2, 1), Point2(2, 1)]).T
52+
actual_array = pose.transformFrom(points)
53+
self.assertEqual(actual_array.shape, (2, 2))
54+
expected_array = np.stack([expected, expected]).T
55+
np.testing.assert_allclose(actual_array, expected_array, atol=1e-6)
56+
2957
def test_align(self) -> None:
3058
"""Ensure estimation of the Pose2 element to align two 2d point clouds succeeds.
3159

python/gtsam/tests/test_Pose3.py

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import numpy as np
1616

1717
import gtsam
18-
from gtsam import Point3, Pose3, Rot3
18+
from gtsam import Point3, Pose3, Rot3, Point3Pairs
1919
from gtsam.utils.test_case import GtsamTestCase
2020

2121

@@ -30,13 +30,34 @@ def test_between(self):
3030
actual = T2.between(T3)
3131
self.gtsamAssertEquals(actual, expected, 1e-6)
3232

33-
def test_transform_to(self):
33+
def test_transformTo(self):
3434
"""Test transformTo method."""
35-
transform = Pose3(Rot3.Rodrigues(0, 0, -1.570796), Point3(2, 4, 0))
36-
actual = transform.transformTo(Point3(3, 2, 10))
35+
pose = Pose3(Rot3.Rodrigues(0, 0, -math.pi/2), Point3(2, 4, 0))
36+
actual = pose.transformTo(Point3(3, 2, 10))
3737
expected = Point3(2, 1, 10)
3838
self.gtsamAssertEquals(actual, expected, 1e-6)
3939

40+
# multi-point version
41+
points = np.stack([Point3(3, 2, 10), Point3(3, 2, 10)]).T
42+
actual_array = pose.transformTo(points)
43+
self.assertEqual(actual_array.shape, (3, 2))
44+
expected_array = np.stack([expected, expected]).T
45+
np.testing.assert_allclose(actual_array, expected_array, atol=1e-6)
46+
47+
def test_transformFrom(self):
48+
"""Test transformFrom method."""
49+
pose = Pose3(Rot3.Rodrigues(0, 0, -math.pi/2), Point3(2, 4, 0))
50+
actual = pose.transformFrom(Point3(2, 1, 10))
51+
expected = Point3(3, 2, 10)
52+
self.gtsamAssertEquals(actual, expected, 1e-6)
53+
54+
# multi-point version
55+
points = np.stack([Point3(2, 1, 10), Point3(2, 1, 10)]).T
56+
actual_array = pose.transformFrom(points)
57+
self.assertEqual(actual_array.shape, (3, 2))
58+
expected_array = np.stack([expected, expected]).T
59+
np.testing.assert_allclose(actual_array, expected_array, atol=1e-6)
60+
4061
def test_range(self):
4162
"""Test range method."""
4263
l1 = Point3(1, 0, 0)
@@ -81,6 +102,24 @@ def test_serialization(self):
81102
actual.deserialize(serialized)
82103
self.gtsamAssertEquals(expected, actual, 1e-10)
83104

105+
def test_align_squares(self):
106+
"""Test if Align method can align 2 squares."""
107+
square = np.array([[0,0,0],[0,1,0],[1,1,0],[1,0,0]], float).T
108+
sTt = Pose3(Rot3.Rodrigues(0, 0, -math.pi), Point3(2, 4, 0))
109+
transformed = sTt.transformTo(square)
110+
111+
st_pairs = Point3Pairs()
112+
for j in range(4):
113+
st_pairs.append((square[:,j], transformed[:,j]))
114+
115+
# Recover the transformation sTt
116+
estimated_sTt = Pose3.Align(st_pairs)
117+
self.gtsamAssertEquals(estimated_sTt, sTt, 1e-10)
118+
119+
# Matrix version
120+
estimated_sTt = Pose3.Align(square, transformed)
121+
self.gtsamAssertEquals(estimated_sTt, sTt, 1e-10)
122+
84123

85124
if __name__ == "__main__":
86125
unittest.main()

0 commit comments

Comments
 (0)