Merge pull request #1549 from borglab/python/backwards-compatibility
commit
a307fcb4bc
|
@ -4,9 +4,49 @@
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from gtsam.utils import findExampleDataFile
|
||||||
|
|
||||||
from gtsam import gtsam, utils
|
from gtsam import gtsam, utils
|
||||||
from gtsam.gtsam import *
|
from gtsam.gtsam import *
|
||||||
from gtsam.utils import findExampleDataFile
|
|
||||||
|
#### Typedefs to allow for backwards compatibility
|
||||||
|
#TODO(Varun) deprecate in future release
|
||||||
|
# gtsam
|
||||||
|
KeyVector = list
|
||||||
|
# base
|
||||||
|
IndexPairSetMap = dict
|
||||||
|
IndexPairVector = list
|
||||||
|
# geometry
|
||||||
|
Point2Vector = list
|
||||||
|
Pose3Vector = list
|
||||||
|
Rot3Vector = list
|
||||||
|
Point2Pairs = list
|
||||||
|
Point3Pairs = list
|
||||||
|
Pose2Pairs = list
|
||||||
|
Pose3Pairs = list
|
||||||
|
# sfm
|
||||||
|
BinaryMeasurementsPoint3 = list
|
||||||
|
BinaryMeasurementsUnit3 = list
|
||||||
|
BinaryMeasurementsRot3 = list
|
||||||
|
KeyPairDoubleMap = dict
|
||||||
|
SfmTrack2dVector = list
|
||||||
|
SfmTracks = list
|
||||||
|
SfmCameras = list
|
||||||
|
SfmMeasurementVector = list
|
||||||
|
MatchIndicesMap = dict
|
||||||
|
KeypointsVector = list
|
||||||
|
# slam
|
||||||
|
BetweenFactorPose3s = list
|
||||||
|
BetweenFactorPose2s = list
|
||||||
|
|
||||||
|
|
||||||
|
class FixedLagSmootherKeyTimestampMap(dict):
|
||||||
|
"""Class to provide backwards compatibility"""
|
||||||
|
def insert(self, key_value):
|
||||||
|
self[key_value[0]] = key_value[1]
|
||||||
|
|
||||||
|
|
||||||
|
#### End typedefs
|
||||||
|
|
||||||
|
|
||||||
def _init():
|
def _init():
|
||||||
|
|
|
@ -11,11 +11,12 @@ Refactored: Roderick Koehle
|
||||||
"""
|
"""
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import gtsam
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from gtsam.symbol_shorthand import K, L, P
|
from gtsam.symbol_shorthand import K, L, P
|
||||||
from gtsam.utils.test_case import GtsamTestCase
|
from gtsam.utils.test_case import GtsamTestCase
|
||||||
|
|
||||||
|
import gtsam
|
||||||
|
|
||||||
|
|
||||||
def ulp(ftype=np.float64):
|
def ulp(ftype=np.float64):
|
||||||
"""
|
"""
|
||||||
|
@ -26,7 +27,7 @@ def ulp(ftype=np.float64):
|
||||||
|
|
||||||
|
|
||||||
class TestCal3Fisheye(GtsamTestCase):
|
class TestCal3Fisheye(GtsamTestCase):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
"""
|
"""
|
||||||
|
@ -53,7 +54,7 @@ class TestCal3Fisheye(GtsamTestCase):
|
||||||
cls.poses = [pose1, pose2]
|
cls.poses = [pose1, pose2]
|
||||||
cls.cameras = [camera1, camera2]
|
cls.cameras = [camera1, camera2]
|
||||||
cls.measurements = [k.project(cls.origin) for k in cls.cameras]
|
cls.measurements = [k.project(cls.origin) for k in cls.cameras]
|
||||||
|
|
||||||
def test_Cal3Fisheye(self):
|
def test_Cal3Fisheye(self):
|
||||||
K = gtsam.Cal3Fisheye()
|
K = gtsam.Cal3Fisheye()
|
||||||
self.assertEqual(K.fx(), 1.)
|
self.assertEqual(K.fx(), 1.)
|
||||||
|
@ -62,7 +63,7 @@ class TestCal3Fisheye(GtsamTestCase):
|
||||||
def test_distortion(self):
|
def test_distortion(self):
|
||||||
"""Fisheye distortion and rectification"""
|
"""Fisheye distortion and rectification"""
|
||||||
equidistant = gtsam.Cal3Fisheye()
|
equidistant = gtsam.Cal3Fisheye()
|
||||||
perspective_pt = self.obj_point[0:2]/self.obj_point[2]
|
perspective_pt = self.obj_point[0:2] / self.obj_point[2]
|
||||||
distorted_pt = equidistant.uncalibrate(perspective_pt)
|
distorted_pt = equidistant.uncalibrate(perspective_pt)
|
||||||
rectified_pt = equidistant.calibrate(distorted_pt)
|
rectified_pt = equidistant.calibrate(distorted_pt)
|
||||||
self.gtsamAssertEquals(distorted_pt, self.img_point)
|
self.gtsamAssertEquals(distorted_pt, self.img_point)
|
||||||
|
@ -166,7 +167,7 @@ class TestCal3Fisheye(GtsamTestCase):
|
||||||
pose = gtsam.Pose3()
|
pose = gtsam.Pose3()
|
||||||
camera = gtsam.Cal3Fisheye()
|
camera = gtsam.Cal3Fisheye()
|
||||||
state = gtsam.Values()
|
state = gtsam.Values()
|
||||||
camera_key, pose_key, landmark_key = K(0), P(0), L(0)
|
pose_key, landmark_key = P(0), L(0)
|
||||||
state.insert_point3(landmark_key, obj_point)
|
state.insert_point3(landmark_key, obj_point)
|
||||||
state.insert_pose3(pose_key, pose)
|
state.insert_pose3(pose_key, pose)
|
||||||
g = gtsam.NonlinearFactorGraph()
|
g = gtsam.NonlinearFactorGraph()
|
||||||
|
|
|
@ -10,11 +10,12 @@ Author: Frank Dellaert & Duy Nguyen Ta (Python)
|
||||||
"""
|
"""
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import gtsam
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from gtsam.symbol_shorthand import K, L, P
|
from gtsam.symbol_shorthand import K, L, P
|
||||||
from gtsam.utils.test_case import GtsamTestCase
|
from gtsam.utils.test_case import GtsamTestCase
|
||||||
|
|
||||||
|
import gtsam
|
||||||
|
|
||||||
|
|
||||||
class TestCal3Unified(GtsamTestCase):
|
class TestCal3Unified(GtsamTestCase):
|
||||||
|
|
||||||
|
@ -106,7 +107,7 @@ class TestCal3Unified(GtsamTestCase):
|
||||||
state = gtsam.Values()
|
state = gtsam.Values()
|
||||||
measured = self.img_point
|
measured = self.img_point
|
||||||
noise_model = gtsam.noiseModel.Isotropic.Sigma(2, 1)
|
noise_model = gtsam.noiseModel.Isotropic.Sigma(2, 1)
|
||||||
camera_key, pose_key, landmark_key = K(0), P(0), L(0)
|
camera_key, pose_key, landmark_key = K(0), P(0), L(0)
|
||||||
k = self.stereographic
|
k = self.stereographic
|
||||||
state.insert_cal3unified(camera_key, k)
|
state.insert_cal3unified(camera_key, k)
|
||||||
state.insert_pose3(pose_key, gtsam.Pose3())
|
state.insert_pose3(pose_key, gtsam.Pose3())
|
||||||
|
@ -141,7 +142,7 @@ class TestCal3Unified(GtsamTestCase):
|
||||||
Dcal = np.zeros((2, 10), order='F')
|
Dcal = np.zeros((2, 10), order='F')
|
||||||
Dp = np.zeros((2, 2), order='F')
|
Dp = np.zeros((2, 2), order='F')
|
||||||
camera.calibrate(img_point, Dcal, Dp)
|
camera.calibrate(img_point, Dcal, Dp)
|
||||||
|
|
||||||
self.gtsamAssertEquals(Dcal, np.array(
|
self.gtsamAssertEquals(Dcal, np.array(
|
||||||
[[ 0., 0., 0., -1., 0., 0., 0., 0., 0., 0.],
|
[[ 0., 0., 0., -1., 0., 0., 0., 0., 0., 0.],
|
||||||
[ 0., 0., 0., 0., -1., 0., 0., 0., 0., 0.]]))
|
[ 0., 0., 0., 0., -1., 0., 0., 0., 0., 0.]]))
|
||||||
|
|
|
@ -14,7 +14,6 @@ import numpy as np
|
||||||
from gtsam.utils.test_case import GtsamTestCase
|
from gtsam.utils.test_case import GtsamTestCase
|
||||||
|
|
||||||
import gtsam
|
import gtsam
|
||||||
import gtsam_unstable
|
|
||||||
|
|
||||||
|
|
||||||
class TestFixedLagSmootherExample(GtsamTestCase):
|
class TestFixedLagSmootherExample(GtsamTestCase):
|
||||||
|
|
|
@ -14,11 +14,12 @@ from __future__ import print_function
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import gtsam
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from gtsam.symbol_shorthand import X
|
from gtsam.symbol_shorthand import X
|
||||||
from gtsam.utils.test_case import GtsamTestCase
|
from gtsam.utils.test_case import GtsamTestCase
|
||||||
|
|
||||||
|
import gtsam
|
||||||
|
|
||||||
|
|
||||||
def create_graph():
|
def create_graph():
|
||||||
"""Create a basic linear factor graph for testing"""
|
"""Create a basic linear factor graph for testing"""
|
||||||
|
@ -40,6 +41,7 @@ def create_graph():
|
||||||
|
|
||||||
class TestGaussianFactorGraph(GtsamTestCase):
|
class TestGaussianFactorGraph(GtsamTestCase):
|
||||||
"""Tests for Gaussian Factor Graphs."""
|
"""Tests for Gaussian Factor Graphs."""
|
||||||
|
|
||||||
def test_fg(self):
|
def test_fg(self):
|
||||||
"""Test solving a linear factor graph"""
|
"""Test solving a linear factor graph"""
|
||||||
graph, X = create_graph()
|
graph, X = create_graph()
|
||||||
|
@ -98,12 +100,11 @@ class TestGaussianFactorGraph(GtsamTestCase):
|
||||||
bn = gfg.eliminateSequential(ordering)
|
bn = gfg.eliminateSequential(ordering)
|
||||||
self.assertEqual(bn.size(), 3)
|
self.assertEqual(bn.size(), 3)
|
||||||
|
|
||||||
keyVector = []
|
keyVector = [keys[2]]
|
||||||
keyVector.append(keys[2])
|
ordering = gtsam.Ordering.ColamdConstrainedLastGaussianFactorGraph(
|
||||||
#TODO(Varun) Below code causes segfault in Debug config
|
gfg, keyVector)
|
||||||
# ordering = gtsam.Ordering.ColamdConstrainedLastGaussianFactorGraph(gfg, keyVector)
|
bn = gfg.eliminateSequential(ordering)
|
||||||
# bn = gfg.eliminateSequential(ordering)
|
self.assertEqual(bn.size(), 3)
|
||||||
# self.assertEqual(bn.size(), 3)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
|
@ -13,15 +13,15 @@ Author: Frank Dellaert
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import gtsam
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from gtsam import Rot3
|
|
||||||
from gtsam.utils.test_case import GtsamTestCase
|
from gtsam.utils.test_case import GtsamTestCase
|
||||||
|
|
||||||
|
import gtsam
|
||||||
|
from gtsam import Rot3
|
||||||
|
|
||||||
KEY = 0
|
KEY = 0
|
||||||
MODEL = gtsam.noiseModel.Unit.Create(3)
|
MODEL = gtsam.noiseModel.Unit.Create(3)
|
||||||
|
|
||||||
|
|
||||||
# Rot3 version
|
# Rot3 version
|
||||||
R = Rot3.Expmap(np.array([0.1, 0, 0]))
|
R = Rot3.Expmap(np.array([0.1, 0, 0]))
|
||||||
|
|
||||||
|
@ -29,8 +29,10 @@ R = Rot3.Expmap(np.array([0.1, 0, 0]))
|
||||||
class TestKarcherMean(GtsamTestCase):
|
class TestKarcherMean(GtsamTestCase):
|
||||||
|
|
||||||
def test_find(self):
|
def test_find(self):
|
||||||
# Check that optimizing for Karcher mean (which minimizes Between distance)
|
"""
|
||||||
# gets correct result.
|
Check that optimizing for Karcher mean (which minimizes Between distance)
|
||||||
|
gets correct result.
|
||||||
|
"""
|
||||||
rotations = [R, R.inverse()]
|
rotations = [R, R.inverse()]
|
||||||
expected = Rot3()
|
expected = Rot3()
|
||||||
actual = gtsam.FindKarcherMean(rotations)
|
actual = gtsam.FindKarcherMean(rotations)
|
||||||
|
@ -69,8 +71,7 @@ class TestKarcherMean(GtsamTestCase):
|
||||||
result = gtsam.GaussNewtonOptimizer(graph, initial).optimize()
|
result = gtsam.GaussNewtonOptimizer(graph, initial).optimize()
|
||||||
actual = gtsam.FindKarcherMean([result.atRot3(1), result.atRot3(2)])
|
actual = gtsam.FindKarcherMean([result.atRot3(1), result.atRot3(2)])
|
||||||
self.gtsamAssertEquals(expected, actual)
|
self.gtsamAssertEquals(expected, actual)
|
||||||
self.gtsamAssertEquals(
|
self.gtsamAssertEquals(R12, result.atRot3(1).between(result.atRot3(2)))
|
||||||
R12, result.atRot3(1).between(result.atRot3(2)))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
@ -12,12 +12,14 @@ import math
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from gtsam import Point2, Pose2
|
|
||||||
from gtsam.utils.test_case import GtsamTestCase
|
from gtsam.utils.test_case import GtsamTestCase
|
||||||
|
|
||||||
|
from gtsam import Point2, Point2Pairs, Pose2
|
||||||
|
|
||||||
|
|
||||||
class TestPose2(GtsamTestCase):
|
class TestPose2(GtsamTestCase):
|
||||||
"""Test selected Pose2 methods."""
|
"""Test selected Pose2 methods."""
|
||||||
|
|
||||||
def test_adjoint(self) -> None:
|
def test_adjoint(self) -> None:
|
||||||
"""Test adjoint method."""
|
"""Test adjoint method."""
|
||||||
xi = np.array([1, 2, 3])
|
xi = np.array([1, 2, 3])
|
||||||
|
@ -27,7 +29,7 @@ class TestPose2(GtsamTestCase):
|
||||||
|
|
||||||
def test_transformTo(self):
|
def test_transformTo(self):
|
||||||
"""Test transformTo method."""
|
"""Test transformTo method."""
|
||||||
pose = Pose2(2, 4, -math.pi/2)
|
pose = Pose2(2, 4, -math.pi / 2)
|
||||||
actual = pose.transformTo(Point2(3, 2))
|
actual = pose.transformTo(Point2(3, 2))
|
||||||
expected = Point2(2, 1)
|
expected = Point2(2, 1)
|
||||||
self.gtsamAssertEquals(actual, expected, 1e-6)
|
self.gtsamAssertEquals(actual, expected, 1e-6)
|
||||||
|
@ -41,7 +43,7 @@ class TestPose2(GtsamTestCase):
|
||||||
|
|
||||||
def test_transformFrom(self):
|
def test_transformFrom(self):
|
||||||
"""Test transformFrom method."""
|
"""Test transformFrom method."""
|
||||||
pose = Pose2(2, 4, -math.pi/2)
|
pose = Pose2(2, 4, -math.pi / 2)
|
||||||
actual = pose.transformFrom(Point2(2, 1))
|
actual = pose.transformFrom(Point2(2, 1))
|
||||||
expected = Point2(3, 2)
|
expected = Point2(3, 2)
|
||||||
self.gtsamAssertEquals(actual, expected, 1e-6)
|
self.gtsamAssertEquals(actual, expected, 1e-6)
|
||||||
|
|
|
@ -12,11 +12,12 @@ Author: Frank Dellaert, Duy Nguyen Ta
|
||||||
import math
|
import math
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import gtsam
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from gtsam import Point3, Pose3, Rot3
|
|
||||||
from gtsam.utils.test_case import GtsamTestCase
|
from gtsam.utils.test_case import GtsamTestCase
|
||||||
|
|
||||||
|
import gtsam
|
||||||
|
from gtsam import Point3, Pose3, Rot3
|
||||||
|
|
||||||
|
|
||||||
def numerical_derivative_pose(pose, method, delta=1e-5):
|
def numerical_derivative_pose(pose, method, delta=1e-5):
|
||||||
jacobian = np.zeros((6, 6))
|
jacobian = np.zeros((6, 6))
|
||||||
|
|
|
@ -12,12 +12,13 @@ Author: Frank Dellaert
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import gtsam
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
from gtsam.utils.test_case import GtsamTestCase
|
||||||
|
|
||||||
|
import gtsam
|
||||||
from gtsam import (BetweenFactorPose2, LevenbergMarquardtParams, Pose2, Rot2,
|
from gtsam import (BetweenFactorPose2, LevenbergMarquardtParams, Pose2, Rot2,
|
||||||
ShonanAveraging2, ShonanAveraging3,
|
ShonanAveraging2, ShonanAveraging3,
|
||||||
ShonanAveragingParameters2, ShonanAveragingParameters3)
|
ShonanAveragingParameters2, ShonanAveragingParameters3)
|
||||||
from gtsam.utils.test_case import GtsamTestCase
|
|
||||||
|
|
||||||
DEFAULT_PARAMS = ShonanAveragingParameters3(
|
DEFAULT_PARAMS = ShonanAveragingParameters3(
|
||||||
gtsam.LevenbergMarquardtParams.CeresDefaults()
|
gtsam.LevenbergMarquardtParams.CeresDefaults()
|
||||||
|
@ -139,7 +140,6 @@ class TestShonanAveraging(GtsamTestCase):
|
||||||
self.assertAlmostEqual(3.0756, shonan.cost(initial), places=3)
|
self.assertAlmostEqual(3.0756, shonan.cost(initial), places=3)
|
||||||
result, _lambdaMin = shonan.run(initial, 3, 3)
|
result, _lambdaMin = shonan.run(initial, 3, 3)
|
||||||
self.assertAlmostEqual(0.0015, shonan.cost(result), places=3)
|
self.assertAlmostEqual(0.0015, shonan.cost(result), places=3)
|
||||||
|
|
||||||
|
|
||||||
def test_constructorBetweenFactorPose2s(self) -> None:
|
def test_constructorBetweenFactorPose2s(self) -> None:
|
||||||
"""Check if ShonanAveraging2 constructor works when not initialized from g2o file.
|
"""Check if ShonanAveraging2 constructor works when not initialized from g2o file.
|
||||||
|
@ -189,11 +189,11 @@ class TestShonanAveraging(GtsamTestCase):
|
||||||
|
|
||||||
wRi_list = [result_values.atRot2(i) for i in range(num_images)]
|
wRi_list = [result_values.atRot2(i) for i in range(num_images)]
|
||||||
thetas_deg = np.array([wRi.degrees() for wRi in wRi_list])
|
thetas_deg = np.array([wRi.degrees() for wRi in wRi_list])
|
||||||
|
|
||||||
# map all angles to [0,360)
|
# map all angles to [0,360)
|
||||||
thetas_deg = thetas_deg % 360
|
thetas_deg = thetas_deg % 360
|
||||||
thetas_deg -= thetas_deg[0]
|
thetas_deg -= thetas_deg[0]
|
||||||
|
|
||||||
expected_thetas_deg = np.array([0.0, 90.0, 0.0])
|
expected_thetas_deg = np.array([0.0, 90.0, 0.0])
|
||||||
np.testing.assert_allclose(thetas_deg, expected_thetas_deg, atol=0.1)
|
np.testing.assert_allclose(thetas_deg, expected_thetas_deg, atol=0.1)
|
||||||
|
|
||||||
|
|
|
@ -12,9 +12,10 @@ Author: John Lambert
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from gtsam import Pose2, Rot2, Similarity2
|
|
||||||
from gtsam.utils.test_case import GtsamTestCase
|
from gtsam.utils.test_case import GtsamTestCase
|
||||||
|
|
||||||
|
from gtsam import Pose2, Rot2, Similarity2
|
||||||
|
|
||||||
|
|
||||||
class TestSim2(GtsamTestCase):
|
class TestSim2(GtsamTestCase):
|
||||||
"""Test selected Sim2 methods."""
|
"""Test selected Sim2 methods."""
|
||||||
|
@ -55,7 +56,7 @@ class TestSim2(GtsamTestCase):
|
||||||
self.gtsamAssertEquals(wToi, wSe.transformFrom(eToi))
|
self.gtsamAssertEquals(wToi, wSe.transformFrom(eToi))
|
||||||
|
|
||||||
def test_align_poses_along_straight_line_gauge(self):
|
def test_align_poses_along_straight_line_gauge(self):
|
||||||
"""Test if Align Pose3Pairs method can account for gauge ambiguity.
|
"""Test if Pose2 Align method can account for gauge ambiguity.
|
||||||
|
|
||||||
Scenario:
|
Scenario:
|
||||||
3 object poses
|
3 object poses
|
||||||
|
@ -90,7 +91,7 @@ class TestSim2(GtsamTestCase):
|
||||||
self.gtsamAssertEquals(wToi, wSe.transformFrom(eToi))
|
self.gtsamAssertEquals(wToi, wSe.transformFrom(eToi))
|
||||||
|
|
||||||
def test_align_poses_scaled_squares(self):
|
def test_align_poses_scaled_squares(self):
|
||||||
"""Test if Align Pose2Pairs method can account for gauge ambiguity.
|
"""Test if Align method can account for gauge ambiguity.
|
||||||
|
|
||||||
Make sure a big and small square can be aligned.
|
Make sure a big and small square can be aligned.
|
||||||
The u's represent a big square (10x10), and v's represents a small square (4x4).
|
The u's represent a big square (10x10), and v's represents a small square (4x4).
|
||||||
|
|
|
@ -12,17 +12,18 @@ Author: John Lambert
|
||||||
import unittest
|
import unittest
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
import gtsam
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from gtsam import Point3, Pose3, Rot3, Similarity3
|
|
||||||
from gtsam.utils.test_case import GtsamTestCase
|
from gtsam.utils.test_case import GtsamTestCase
|
||||||
|
|
||||||
|
import gtsam
|
||||||
|
from gtsam import Point3, Pose3, Rot3, Similarity3
|
||||||
|
|
||||||
|
|
||||||
class TestSim3(GtsamTestCase):
|
class TestSim3(GtsamTestCase):
|
||||||
"""Test selected Sim3 methods."""
|
"""Test selected Sim3 methods."""
|
||||||
|
|
||||||
def test_align_poses_along_straight_line(self):
|
def test_align_poses_along_straight_line(self):
|
||||||
"""Test Align Pose3Pairs method.
|
"""Test Pose3 Align method.
|
||||||
|
|
||||||
Scenario:
|
Scenario:
|
||||||
3 object poses
|
3 object poses
|
||||||
|
@ -57,7 +58,7 @@ class TestSim3(GtsamTestCase):
|
||||||
self.gtsamAssertEquals(wToi, wSe.transformFrom(eToi))
|
self.gtsamAssertEquals(wToi, wSe.transformFrom(eToi))
|
||||||
|
|
||||||
def test_align_poses_along_straight_line_gauge(self):
|
def test_align_poses_along_straight_line_gauge(self):
|
||||||
"""Test if Align Pose3Pairs method can account for gauge ambiguity.
|
"""Test if Pose3 Align method can account for gauge ambiguity.
|
||||||
|
|
||||||
Scenario:
|
Scenario:
|
||||||
3 object poses
|
3 object poses
|
||||||
|
@ -92,7 +93,7 @@ class TestSim3(GtsamTestCase):
|
||||||
self.gtsamAssertEquals(wToi, wSe.transformFrom(eToi))
|
self.gtsamAssertEquals(wToi, wSe.transformFrom(eToi))
|
||||||
|
|
||||||
def test_align_poses_scaled_squares(self):
|
def test_align_poses_scaled_squares(self):
|
||||||
"""Test if Align Pose3Pairs method can account for gauge ambiguity.
|
"""Test if Pose3 Align method can account for gauge ambiguity.
|
||||||
|
|
||||||
Make sure a big and small square can be aligned.
|
Make sure a big and small square can be aligned.
|
||||||
The u's represent a big square (10x10), and v's represents a small square (4x4).
|
The u's represent a big square (10x10), and v's represents a small square (4x4).
|
||||||
|
|
|
@ -12,13 +12,14 @@ Authors: Frank Dellaert & Fan Jiang (Python) & Sushmita Warrier & John Lambert
|
||||||
import unittest
|
import unittest
|
||||||
from typing import Iterable, List, Optional, Tuple, Union
|
from typing import Iterable, List, Optional, Tuple, Union
|
||||||
|
|
||||||
import gtsam
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
from gtsam.utils.test_case import GtsamTestCase
|
||||||
|
|
||||||
|
import gtsam
|
||||||
from gtsam import (Cal3_S2, Cal3Bundler, CameraSetCal3_S2,
|
from gtsam import (Cal3_S2, Cal3Bundler, CameraSetCal3_S2,
|
||||||
CameraSetCal3Bundler, PinholeCameraCal3_S2,
|
CameraSetCal3Bundler, PinholeCameraCal3_S2,
|
||||||
PinholeCameraCal3Bundler, Point2, Point3, Pose3, Rot3,
|
PinholeCameraCal3Bundler, Point2, Point3, Pose3, Rot3,
|
||||||
TriangulationParameters, TriangulationResult)
|
TriangulationParameters, TriangulationResult)
|
||||||
from gtsam.utils.test_case import GtsamTestCase
|
|
||||||
|
|
||||||
UPRIGHT = Rot3.Ypr(-np.pi / 2, 0.0, -np.pi / 2)
|
UPRIGHT = Rot3.Ypr(-np.pi / 2, 0.0, -np.pi / 2)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,897 @@
|
||||||
|
"""
|
||||||
|
GTSAM Copyright 2010-2019, Georgia Tech Research Corporation,
|
||||||
|
Atlanta, Georgia 30332-0415
|
||||||
|
All Rights Reserved
|
||||||
|
|
||||||
|
See LICENSE for the license information
|
||||||
|
|
||||||
|
Unit tests to ensure backwards compatibility of the Python wrapper.
|
||||||
|
Author: Varun Agrawal
|
||||||
|
"""
|
||||||
|
import unittest
|
||||||
|
from typing import Iterable, List, Optional, Tuple, Union
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from gtsam.gtsfm import Keypoints
|
||||||
|
from gtsam.symbol_shorthand import X
|
||||||
|
from gtsam.utils.test_case import GtsamTestCase
|
||||||
|
|
||||||
|
import gtsam
|
||||||
|
from gtsam import (BetweenFactorPose2, Cal3_S2, Cal3Bundler, CameraSetCal3_S2,
|
||||||
|
CameraSetCal3Bundler, IndexPair, LevenbergMarquardtParams,
|
||||||
|
PinholeCameraCal3_S2, PinholeCameraCal3Bundler, Point2,
|
||||||
|
Point2Pairs, Point3, Pose2, Pose2Pairs, Pose3, Rot2, Rot3,
|
||||||
|
SfmTrack2d, ShonanAveraging2, ShonanAveragingParameters2,
|
||||||
|
Similarity2, Similarity3, TriangulationParameters,
|
||||||
|
TriangulationResult)
|
||||||
|
|
||||||
|
UPRIGHT = Rot3.Ypr(-np.pi / 2, 0.0, -np.pi / 2)
|
||||||
|
|
||||||
|
|
||||||
|
class TestBackwardsCompatibility(GtsamTestCase):
|
||||||
|
"""Tests for the backwards compatibility for the Python wrapper."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Setup test fixtures"""
|
||||||
|
p1 = [-1.0, 0.0, -1.0]
|
||||||
|
p2 = [1.0, 0.0, -1.0]
|
||||||
|
q1 = Rot3(1.0, 0.0, 0.0, 0.0)
|
||||||
|
q2 = Rot3(1.0, 0.0, 0.0, 0.0)
|
||||||
|
pose1 = Pose3(q1, p1)
|
||||||
|
pose2 = Pose3(q2, p2)
|
||||||
|
camera1 = gtsam.PinholeCameraCal3Fisheye(pose1)
|
||||||
|
camera2 = gtsam.PinholeCameraCal3Fisheye(pose2)
|
||||||
|
self.origin = np.array([0.0, 0.0, 0.0])
|
||||||
|
self.poses = gtsam.Pose3Vector([pose1, pose2])
|
||||||
|
|
||||||
|
self.fisheye_cameras = gtsam.CameraSetCal3Fisheye([camera1, camera2])
|
||||||
|
self.fisheye_measurements = gtsam.Point2Vector(
|
||||||
|
[k.project(self.origin) for k in self.fisheye_cameras])
|
||||||
|
|
||||||
|
fx, fy, s, u0, v0 = 2, 2, 0, 0, 0
|
||||||
|
k1, k2, p1, p2 = 0, 0, 0, 0
|
||||||
|
xi = 1
|
||||||
|
self.stereographic = gtsam.Cal3Unified(fx, fy, s, u0, v0, k1, k2, p1,
|
||||||
|
p2, xi)
|
||||||
|
camera1 = gtsam.PinholeCameraCal3Unified(pose1, self.stereographic)
|
||||||
|
camera2 = gtsam.PinholeCameraCal3Unified(pose2, self.stereographic)
|
||||||
|
self.unified_cameras = gtsam.CameraSetCal3Unified([camera1, camera2])
|
||||||
|
self.unified_measurements = gtsam.Point2Vector(
|
||||||
|
[k.project(self.origin) for k in self.unified_cameras])
|
||||||
|
|
||||||
|
## Set up two camera poses
|
||||||
|
# Looking along X-axis, 1 meter above ground plane (x-y)
|
||||||
|
pose1 = Pose3(UPRIGHT, Point3(0, 0, 1))
|
||||||
|
|
||||||
|
# create second camera 1 meter to the right of first camera
|
||||||
|
pose2 = pose1.compose(Pose3(Rot3(), Point3(1, 0, 0)))
|
||||||
|
# twoPoses
|
||||||
|
self.triangulation_poses = gtsam.Pose3Vector()
|
||||||
|
self.triangulation_poses.append(pose1)
|
||||||
|
self.triangulation_poses.append(pose2)
|
||||||
|
|
||||||
|
# landmark ~5 meters infront of camera
|
||||||
|
self.landmark = Point3(5, 0.5, 1.2)
|
||||||
|
|
||||||
|
def test_Cal3Fisheye_triangulation_rectify(self):
|
||||||
|
"""
|
||||||
|
Estimate spatial point from image measurements using
|
||||||
|
rectification from a Cal3Fisheye camera model.
|
||||||
|
"""
|
||||||
|
rectified = gtsam.Point2Vector([
|
||||||
|
k.calibration().calibrate(pt)
|
||||||
|
for k, pt in zip(self.fisheye_cameras, self.fisheye_measurements)
|
||||||
|
])
|
||||||
|
shared_cal = gtsam.Cal3_S2()
|
||||||
|
triangulated = gtsam.triangulatePoint3(self.poses,
|
||||||
|
shared_cal,
|
||||||
|
rectified,
|
||||||
|
rank_tol=1e-9,
|
||||||
|
optimize=False)
|
||||||
|
self.gtsamAssertEquals(triangulated, self.origin)
|
||||||
|
|
||||||
|
def test_Cal3Unified_triangulation_rectify(self):
|
||||||
|
"""
|
||||||
|
Estimate spatial point from image measurements using
|
||||||
|
rectification from a Cal3Unified camera model.
|
||||||
|
"""
|
||||||
|
rectified = gtsam.Point2Vector([
|
||||||
|
k.calibration().calibrate(pt)
|
||||||
|
for k, pt in zip(self.unified_cameras, self.unified_measurements)
|
||||||
|
])
|
||||||
|
shared_cal = gtsam.Cal3_S2()
|
||||||
|
triangulated = gtsam.triangulatePoint3(self.poses,
|
||||||
|
shared_cal,
|
||||||
|
rectified,
|
||||||
|
rank_tol=1e-9,
|
||||||
|
optimize=False)
|
||||||
|
self.gtsamAssertEquals(triangulated, self.origin)
|
||||||
|
|
||||||
|
def test_track_generation(self) -> None:
|
||||||
|
"""Ensures that DSF generates three tracks from measurements
|
||||||
|
in 3 images (H=200,W=400)."""
|
||||||
|
kps_i0 = Keypoints(np.array([[10.0, 20], [30, 40]]))
|
||||||
|
kps_i1 = Keypoints(np.array([[50.0, 60], [70, 80], [90, 100]]))
|
||||||
|
kps_i2 = Keypoints(np.array([[110.0, 120], [130, 140]]))
|
||||||
|
|
||||||
|
keypoints_list = gtsam.KeypointsVector()
|
||||||
|
keypoints_list.append(kps_i0)
|
||||||
|
keypoints_list.append(kps_i1)
|
||||||
|
keypoints_list.append(kps_i2)
|
||||||
|
|
||||||
|
# For each image pair (i1,i2), we provide a (K,2) matrix
|
||||||
|
# of corresponding image indices (k1,k2).
|
||||||
|
matches_dict = gtsam.MatchIndicesMap()
|
||||||
|
matches_dict[IndexPair(0, 1)] = np.array([[0, 0], [1, 1]])
|
||||||
|
matches_dict[IndexPair(1, 2)] = np.array([[2, 0], [1, 1]])
|
||||||
|
|
||||||
|
tracks = gtsam.gtsfm.tracksFromPairwiseMatches(
|
||||||
|
matches_dict,
|
||||||
|
keypoints_list,
|
||||||
|
verbose=False,
|
||||||
|
)
|
||||||
|
assert len(tracks) == 3
|
||||||
|
|
||||||
|
# Verify track 0.
|
||||||
|
track0 = tracks[0]
|
||||||
|
assert track0.numberMeasurements() == 2
|
||||||
|
np.testing.assert_allclose(track0.measurements[0][1], Point2(10, 20))
|
||||||
|
np.testing.assert_allclose(track0.measurements[1][1], Point2(50, 60))
|
||||||
|
assert track0.measurements[0][0] == 0
|
||||||
|
assert track0.measurements[1][0] == 1
|
||||||
|
np.testing.assert_allclose(
|
||||||
|
track0.measurementMatrix(),
|
||||||
|
[
|
||||||
|
[10, 20],
|
||||||
|
[50, 60],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
np.testing.assert_allclose(track0.indexVector(), [0, 1])
|
||||||
|
|
||||||
|
# Verify track 1.
|
||||||
|
track1 = tracks[1]
|
||||||
|
np.testing.assert_allclose(
|
||||||
|
track1.measurementMatrix(),
|
||||||
|
[
|
||||||
|
[30, 40],
|
||||||
|
[70, 80],
|
||||||
|
[130, 140],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
np.testing.assert_allclose(track1.indexVector(), [0, 1, 2])
|
||||||
|
|
||||||
|
# Verify track 2.
|
||||||
|
track2 = tracks[2]
|
||||||
|
np.testing.assert_allclose(
|
||||||
|
track2.measurementMatrix(),
|
||||||
|
[
|
||||||
|
[90, 100],
|
||||||
|
[110, 120],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
np.testing.assert_allclose(track2.indexVector(), [1, 2])
|
||||||
|
|
||||||
|
def test_sfm_track_2d_constructor(self) -> None:
|
||||||
|
"""Test construction of 2D SfM track."""
|
||||||
|
measurements = gtsam.SfmMeasurementVector()
|
||||||
|
measurements.append((0, Point2(10, 20)))
|
||||||
|
track = SfmTrack2d(measurements=measurements)
|
||||||
|
track.measurement(0)
|
||||||
|
assert track.numberMeasurements() == 1
|
||||||
|
|
||||||
|
def test_FixedLagSmootherExample(self):
|
||||||
|
'''
|
||||||
|
Simple test that checks for equality between C++ example
|
||||||
|
file and the Python implementation. See
|
||||||
|
gtsam_unstable/examples/FixedLagSmootherExample.cpp
|
||||||
|
'''
|
||||||
|
# Define a batch fixed lag smoother, which uses
|
||||||
|
# Levenberg-Marquardt to perform the nonlinear optimization
|
||||||
|
lag = 2.0
|
||||||
|
smoother_batch = gtsam.BatchFixedLagSmoother(lag)
|
||||||
|
|
||||||
|
# Create containers to store the factors and linearization points
|
||||||
|
# that will be sent to the smoothers
|
||||||
|
new_factors = gtsam.NonlinearFactorGraph()
|
||||||
|
new_values = gtsam.Values()
|
||||||
|
new_timestamps = gtsam.FixedLagSmootherKeyTimestampMap()
|
||||||
|
|
||||||
|
# Create a prior on the first pose, placing it at the origin
|
||||||
|
prior_mean = Pose2(0, 0, 0)
|
||||||
|
prior_noise = gtsam.noiseModel.Diagonal.Sigmas(
|
||||||
|
np.array([0.3, 0.3, 0.1]))
|
||||||
|
X1 = 0
|
||||||
|
new_factors.push_back(
|
||||||
|
gtsam.PriorFactorPose2(X1, prior_mean, prior_noise))
|
||||||
|
new_values.insert(X1, prior_mean)
|
||||||
|
new_timestamps.insert((X1, 0.0))
|
||||||
|
|
||||||
|
delta_time = 0.25
|
||||||
|
time = 0.25
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
|
||||||
|
ground_truth = [
|
||||||
|
Pose2(0.995821, 0.0231012, 0.0300001),
|
||||||
|
Pose2(1.49284, 0.0457247, 0.045),
|
||||||
|
Pose2(1.98981, 0.0758879, 0.06),
|
||||||
|
Pose2(2.48627, 0.113502, 0.075),
|
||||||
|
Pose2(2.98211, 0.158558, 0.09),
|
||||||
|
Pose2(3.47722, 0.211047, 0.105),
|
||||||
|
Pose2(3.97149, 0.270956, 0.12),
|
||||||
|
Pose2(4.4648, 0.338272, 0.135),
|
||||||
|
Pose2(4.95705, 0.41298, 0.15),
|
||||||
|
Pose2(5.44812, 0.495063, 0.165),
|
||||||
|
Pose2(5.9379, 0.584503, 0.18),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Iterates from 0.25s to 3.0s, adding 0.25s each loop
|
||||||
|
# In each iteration, the agent moves at a constant speed
|
||||||
|
# and its two odometers measure the change. The smoothed
|
||||||
|
# result is then compared to the ground truth
|
||||||
|
while time <= 3.0:
|
||||||
|
previous_key = int(1000 * (time - delta_time))
|
||||||
|
current_key = int(1000 * time)
|
||||||
|
|
||||||
|
# assign current key to the current timestamp
|
||||||
|
new_timestamps.insert((current_key, time))
|
||||||
|
|
||||||
|
# Add a guess for this pose to the new values
|
||||||
|
# Assume that the robot moves at 2 m/s. Position is time[s] *
|
||||||
|
# 2[m/s]
|
||||||
|
current_pose = Pose2(time * 2, 0, 0)
|
||||||
|
new_values.insert(current_key, current_pose)
|
||||||
|
|
||||||
|
# Add odometry factors from two different sources with different
|
||||||
|
# error stats
|
||||||
|
odometry_measurement_1 = Pose2(0.61, -0.08, 0.02)
|
||||||
|
odometry_noise_1 = gtsam.noiseModel.Diagonal.Sigmas(
|
||||||
|
np.array([0.1, 0.1, 0.05]))
|
||||||
|
new_factors.push_back(
|
||||||
|
gtsam.BetweenFactorPose2(previous_key, current_key,
|
||||||
|
odometry_measurement_1,
|
||||||
|
odometry_noise_1))
|
||||||
|
|
||||||
|
odometry_measurement_2 = Pose2(0.47, 0.03, 0.01)
|
||||||
|
odometry_noise_2 = gtsam.noiseModel.Diagonal.Sigmas(
|
||||||
|
np.array([0.05, 0.05, 0.05]))
|
||||||
|
new_factors.push_back(
|
||||||
|
gtsam.BetweenFactorPose2(previous_key, current_key,
|
||||||
|
odometry_measurement_2,
|
||||||
|
odometry_noise_2))
|
||||||
|
|
||||||
|
# Update the smoothers with the new factors. In this case,
|
||||||
|
# one iteration must pass for Levenberg-Marquardt to accurately
|
||||||
|
# estimate
|
||||||
|
if time >= 0.50:
|
||||||
|
smoother_batch.update(new_factors, new_values, new_timestamps)
|
||||||
|
|
||||||
|
estimate = smoother_batch.calculateEstimatePose2(current_key)
|
||||||
|
self.assertTrue(estimate.equals(ground_truth[i], 1e-4))
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
new_timestamps.clear()
|
||||||
|
new_values.clear()
|
||||||
|
new_factors.resize(0)
|
||||||
|
|
||||||
|
time += delta_time
|
||||||
|
|
||||||
|
def test_ordering(self):
|
||||||
|
"""Test ordering"""
|
||||||
|
gfg = gtsam.GaussianFactorGraph()
|
||||||
|
|
||||||
|
x0 = X(0)
|
||||||
|
x1 = X(1)
|
||||||
|
x2 = X(2)
|
||||||
|
|
||||||
|
BETWEEN_NOISE = gtsam.noiseModel.Diagonal.Sigmas(np.ones(1))
|
||||||
|
PRIOR_NOISE = gtsam.noiseModel.Diagonal.Sigmas(np.ones(1))
|
||||||
|
|
||||||
|
gfg.add(x1, np.eye(1), x0, -np.eye(1), np.ones(1), BETWEEN_NOISE)
|
||||||
|
gfg.add(x2, np.eye(1), x1, -np.eye(1), 2 * np.ones(1), BETWEEN_NOISE)
|
||||||
|
gfg.add(x0, np.eye(1), np.zeros(1), PRIOR_NOISE)
|
||||||
|
|
||||||
|
keys = (x0, x1, x2)
|
||||||
|
ordering = gtsam.Ordering()
|
||||||
|
for key in keys[::-1]:
|
||||||
|
ordering.push_back(key)
|
||||||
|
|
||||||
|
bn = gfg.eliminateSequential(ordering)
|
||||||
|
self.assertEqual(bn.size(), 3)
|
||||||
|
|
||||||
|
keyVector = gtsam.KeyVector()
|
||||||
|
keyVector.append(keys[2])
|
||||||
|
ordering = gtsam.Ordering.ColamdConstrainedLastGaussianFactorGraph(
|
||||||
|
gfg, keyVector)
|
||||||
|
bn = gfg.eliminateSequential(ordering)
|
||||||
|
self.assertEqual(bn.size(), 3)
|
||||||
|
|
||||||
|
def test_find(self):
|
||||||
|
"""
|
||||||
|
Check that optimizing for Karcher mean (which minimizes Between distance)
|
||||||
|
gets correct result.
|
||||||
|
"""
|
||||||
|
R = Rot3.Expmap(np.array([0.1, 0, 0]))
|
||||||
|
|
||||||
|
rotations = gtsam.Rot3Vector([R, R.inverse()])
|
||||||
|
expected = Rot3()
|
||||||
|
actual = gtsam.FindKarcherMean(rotations)
|
||||||
|
self.gtsamAssertEquals(expected, actual)
|
||||||
|
|
||||||
|
def test_find_karcher_mean_identity(self):
|
||||||
|
"""Averaging 3 identity rotations should yield the identity."""
|
||||||
|
a1Rb1 = Rot3()
|
||||||
|
a2Rb2 = Rot3()
|
||||||
|
a3Rb3 = Rot3()
|
||||||
|
|
||||||
|
aRb_list = gtsam.Rot3Vector([a1Rb1, a2Rb2, a3Rb3])
|
||||||
|
aRb_expected = Rot3()
|
||||||
|
|
||||||
|
aRb = gtsam.FindKarcherMean(aRb_list)
|
||||||
|
self.gtsamAssertEquals(aRb, aRb_expected)
|
||||||
|
|
||||||
|
def test_factor(self):
|
||||||
|
"""Check that the InnerConstraint factor leaves the mean unchanged."""
|
||||||
|
# Make a graph with two variables, one between, and one InnerConstraint
|
||||||
|
# The optimal result should satisfy the between, while moving the other
|
||||||
|
# variable to make the mean the same as before.
|
||||||
|
# Mean of R and R' is identity. Let's make a BetweenFactor making R21 =
|
||||||
|
# R*R*R, i.e. geodesic length is 3 rather than 2.
|
||||||
|
R = Rot3.Expmap(np.array([0.1, 0, 0]))
|
||||||
|
MODEL = gtsam.noiseModel.Unit.Create(3)
|
||||||
|
|
||||||
|
graph = gtsam.NonlinearFactorGraph()
|
||||||
|
R12 = R.compose(R.compose(R))
|
||||||
|
graph.add(gtsam.BetweenFactorRot3(1, 2, R12, MODEL))
|
||||||
|
keys = gtsam.KeyVector()
|
||||||
|
keys.append(1)
|
||||||
|
keys.append(2)
|
||||||
|
graph.add(gtsam.KarcherMeanFactorRot3(keys))
|
||||||
|
|
||||||
|
initial = gtsam.Values()
|
||||||
|
initial.insert(1, R.inverse())
|
||||||
|
initial.insert(2, R)
|
||||||
|
expected = Rot3()
|
||||||
|
|
||||||
|
result = gtsam.GaussNewtonOptimizer(graph, initial).optimize()
|
||||||
|
actual = gtsam.FindKarcherMean(
|
||||||
|
gtsam.Rot3Vector([result.atRot3(1),
|
||||||
|
result.atRot3(2)]))
|
||||||
|
self.gtsamAssertEquals(expected, actual)
|
||||||
|
self.gtsamAssertEquals(R12, result.atRot3(1).between(result.atRot3(2)))
|
||||||
|
|
||||||
|
def test_align(self) -> None:
|
||||||
|
"""Ensure estimation of the Pose2 element to align two 2d point clouds succeeds.
|
||||||
|
|
||||||
|
Two point clouds represent horseshoe-shapes of the same size, just rotated and translated:
|
||||||
|
|
||||||
|
| X---X
|
||||||
|
| |
|
||||||
|
| X---X
|
||||||
|
------------------
|
||||||
|
|
|
||||||
|
|
|
||||||
|
O | O
|
||||||
|
| | |
|
||||||
|
O---O
|
||||||
|
"""
|
||||||
|
pts_a = [
|
||||||
|
Point2(1, -3),
|
||||||
|
Point2(1, -5),
|
||||||
|
Point2(-1, -5),
|
||||||
|
Point2(-1, -3),
|
||||||
|
]
|
||||||
|
pts_b = [
|
||||||
|
Point2(3, 1),
|
||||||
|
Point2(1, 1),
|
||||||
|
Point2(1, 3),
|
||||||
|
Point2(3, 3),
|
||||||
|
]
|
||||||
|
|
||||||
|
ab_pairs = Point2Pairs(list(zip(pts_a, pts_b)))
|
||||||
|
aTb = Pose2.Align(ab_pairs)
|
||||||
|
self.assertIsNotNone(aTb)
|
||||||
|
|
||||||
|
for pt_a, pt_b in zip(pts_a, pts_b):
|
||||||
|
pt_a_ = aTb.transformFrom(pt_b)
|
||||||
|
np.testing.assert_allclose(pt_a, pt_a_)
|
||||||
|
|
||||||
|
# Matrix version
|
||||||
|
A = np.array(pts_a).T
|
||||||
|
B = np.array(pts_b).T
|
||||||
|
aTb = Pose2.Align(A, B)
|
||||||
|
self.assertIsNotNone(aTb)
|
||||||
|
|
||||||
|
for pt_a, pt_b in zip(pts_a, pts_b):
|
||||||
|
pt_a_ = aTb.transformFrom(pt_b)
|
||||||
|
np.testing.assert_allclose(pt_a, pt_a_)
|
||||||
|
|
||||||
|
def test_align_squares(self):
|
||||||
|
"""Test if Align method can align 2 squares."""
|
||||||
|
square = np.array([[0, 0, 0], [0, 1, 0], [1, 1, 0], [1, 0, 0]],
|
||||||
|
float).T
|
||||||
|
sTt = Pose3(Rot3.Rodrigues(0, 0, -np.pi), gtsam.Point3(2, 4, 0))
|
||||||
|
transformed = sTt.transformTo(square)
|
||||||
|
|
||||||
|
st_pairs = gtsam.Point3Pairs()
|
||||||
|
for j in range(4):
|
||||||
|
st_pairs.append((square[:, j], transformed[:, j]))
|
||||||
|
|
||||||
|
# Recover the transformation sTt
|
||||||
|
estimated_sTt = Pose3.Align(st_pairs)
|
||||||
|
self.gtsamAssertEquals(estimated_sTt, sTt, 1e-10)
|
||||||
|
|
||||||
|
# Matrix version
|
||||||
|
estimated_sTt = Pose3.Align(square, transformed)
|
||||||
|
self.gtsamAssertEquals(estimated_sTt, sTt, 1e-10)
|
||||||
|
|
||||||
|
def test_constructorBetweenFactorPose2s(self) -> None:
|
||||||
|
"""Check if ShonanAveraging2 constructor works when not initialized from g2o file.
|
||||||
|
|
||||||
|
GT pose graph:
|
||||||
|
|
||||||
|
| cam 1 = (0,4)
|
||||||
|
--o
|
||||||
|
| .
|
||||||
|
. .
|
||||||
|
. .
|
||||||
|
| |
|
||||||
|
o-- ... o--
|
||||||
|
cam 0 cam 2 = (4,0)
|
||||||
|
(0,0)
|
||||||
|
"""
|
||||||
|
num_images = 3
|
||||||
|
|
||||||
|
wTi_list = [
|
||||||
|
Pose2(Rot2.fromDegrees(0), np.array([0, 0])),
|
||||||
|
Pose2(Rot2.fromDegrees(90), np.array([0, 4])),
|
||||||
|
Pose2(Rot2.fromDegrees(0), np.array([4, 0])),
|
||||||
|
]
|
||||||
|
|
||||||
|
edges = [(0, 1), (1, 2), (0, 2)]
|
||||||
|
i2Ri1_dict = {(i1, i2):
|
||||||
|
wTi_list[i2].inverse().compose(wTi_list[i1]).rotation()
|
||||||
|
for (i1, i2) in edges}
|
||||||
|
|
||||||
|
lm_params = LevenbergMarquardtParams.CeresDefaults()
|
||||||
|
shonan_params = ShonanAveragingParameters2(lm_params)
|
||||||
|
shonan_params.setUseHuber(False)
|
||||||
|
shonan_params.setCertifyOptimality(True)
|
||||||
|
|
||||||
|
noise_model = gtsam.noiseModel.Unit.Create(3)
|
||||||
|
between_factors = gtsam.BetweenFactorPose2s()
|
||||||
|
for (i1, i2), i2Ri1 in i2Ri1_dict.items():
|
||||||
|
i2Ti1 = Pose2(i2Ri1, np.zeros(2))
|
||||||
|
between_factors.append(
|
||||||
|
BetweenFactorPose2(i2, i1, i2Ti1, noise_model))
|
||||||
|
|
||||||
|
obj = ShonanAveraging2(between_factors, shonan_params)
|
||||||
|
initial = obj.initializeRandomly()
|
||||||
|
result_values, _ = obj.run(initial, min_p=2, max_p=100)
|
||||||
|
|
||||||
|
wRi_list = [result_values.atRot2(i) for i in range(num_images)]
|
||||||
|
thetas_deg = np.array([wRi.degrees() for wRi in wRi_list])
|
||||||
|
|
||||||
|
# map all angles to [0,360)
|
||||||
|
thetas_deg = thetas_deg % 360
|
||||||
|
thetas_deg -= thetas_deg[0]
|
||||||
|
|
||||||
|
expected_thetas_deg = np.array([0.0, 90.0, 0.0])
|
||||||
|
np.testing.assert_allclose(thetas_deg, expected_thetas_deg, atol=0.1)
|
||||||
|
|
||||||
|
def test_align_poses2_along_straight_line(self) -> None:
|
||||||
|
"""Test Align of list of Pose2Pair.
|
||||||
|
|
||||||
|
Scenario:
|
||||||
|
3 object poses
|
||||||
|
same scale (no gauge ambiguity)
|
||||||
|
world frame has poses rotated about 180 degrees.
|
||||||
|
world and egovehicle frame translated by 15 meters w.r.t. each other
|
||||||
|
"""
|
||||||
|
R180 = Rot2.fromDegrees(180)
|
||||||
|
|
||||||
|
# Create source poses (three objects o1, o2, o3 living in the egovehicle "e" frame)
|
||||||
|
# Suppose they are 3d cuboids detected by an onboard sensor in the egovehicle frame
|
||||||
|
eTo0 = Pose2(Rot2(), np.array([5, 0]))
|
||||||
|
eTo1 = Pose2(Rot2(), np.array([10, 0]))
|
||||||
|
eTo2 = Pose2(Rot2(), np.array([15, 0]))
|
||||||
|
|
||||||
|
eToi_list = [eTo0, eTo1, eTo2]
|
||||||
|
|
||||||
|
# Create destination poses
|
||||||
|
# (same three objects, but instead living in the world "w" frame)
|
||||||
|
wTo0 = Pose2(R180, np.array([-10, 0]))
|
||||||
|
wTo1 = Pose2(R180, np.array([-5, 0]))
|
||||||
|
wTo2 = Pose2(R180, np.array([0, 0]))
|
||||||
|
|
||||||
|
wToi_list = [wTo0, wTo1, wTo2]
|
||||||
|
|
||||||
|
we_pairs = Pose2Pairs(list(zip(wToi_list, eToi_list)))
|
||||||
|
|
||||||
|
# Recover the transformation wSe (i.e. world_S_egovehicle)
|
||||||
|
wSe = Similarity2.Align(we_pairs)
|
||||||
|
|
||||||
|
for wToi, eToi in zip(wToi_list, eToi_list):
|
||||||
|
self.gtsamAssertEquals(wToi, wSe.transformFrom(eToi))
|
||||||
|
|
||||||
|
def test_align_poses2_along_straight_line_gauge(self):
|
||||||
|
"""Test if Align Pose2Pairs method can account for gauge ambiguity.
|
||||||
|
|
||||||
|
Scenario:
|
||||||
|
3 object poses
|
||||||
|
with gauge ambiguity (2x scale)
|
||||||
|
world frame has poses rotated by 90 degrees.
|
||||||
|
world and egovehicle frame translated by 11 meters w.r.t. each other
|
||||||
|
"""
|
||||||
|
R90 = Rot2.fromDegrees(90)
|
||||||
|
|
||||||
|
# Create source poses (three objects o1, o2, o3 living in the egovehicle "e" frame)
|
||||||
|
# Suppose they are 3d cuboids detected by an onboard sensor in the egovehicle frame
|
||||||
|
eTo0 = Pose2(Rot2(), np.array([1, 0]))
|
||||||
|
eTo1 = Pose2(Rot2(), np.array([2, 0]))
|
||||||
|
eTo2 = Pose2(Rot2(), np.array([4, 0]))
|
||||||
|
|
||||||
|
eToi_list = [eTo0, eTo1, eTo2]
|
||||||
|
|
||||||
|
# Create destination poses
|
||||||
|
# (same three objects, but instead living in the world/city "w" frame)
|
||||||
|
wTo0 = Pose2(R90, np.array([0, 12]))
|
||||||
|
wTo1 = Pose2(R90, np.array([0, 14]))
|
||||||
|
wTo2 = Pose2(R90, np.array([0, 18]))
|
||||||
|
|
||||||
|
wToi_list = [wTo0, wTo1, wTo2]
|
||||||
|
|
||||||
|
we_pairs = Pose2Pairs(list(zip(wToi_list, eToi_list)))
|
||||||
|
|
||||||
|
# Recover the transformation wSe (i.e. world_S_egovehicle)
|
||||||
|
wSe = Similarity2.Align(we_pairs)
|
||||||
|
|
||||||
|
for wToi, eToi in zip(wToi_list, eToi_list):
|
||||||
|
self.gtsamAssertEquals(wToi, wSe.transformFrom(eToi))
|
||||||
|
|
||||||
|
def test_align_poses2_scaled_squares(self):
|
||||||
|
"""Test if Align Pose2Pairs method can account for gauge ambiguity.
|
||||||
|
|
||||||
|
Make sure a big and small square can be aligned.
|
||||||
|
The u's represent a big square (10x10), and v's represents a small square (4x4).
|
||||||
|
|
||||||
|
Scenario:
|
||||||
|
4 object poses
|
||||||
|
with gauge ambiguity (2.5x scale)
|
||||||
|
"""
|
||||||
|
# 0, 90, 180, and 270 degrees yaw
|
||||||
|
R0 = Rot2.fromDegrees(0)
|
||||||
|
R90 = Rot2.fromDegrees(90)
|
||||||
|
R180 = Rot2.fromDegrees(180)
|
||||||
|
R270 = Rot2.fromDegrees(270)
|
||||||
|
|
||||||
|
aTi0 = Pose2(R0, np.array([2, 3]))
|
||||||
|
aTi1 = Pose2(R90, np.array([12, 3]))
|
||||||
|
aTi2 = Pose2(R180, np.array([12, 13]))
|
||||||
|
aTi3 = Pose2(R270, np.array([2, 13]))
|
||||||
|
|
||||||
|
aTi_list = [aTi0, aTi1, aTi2, aTi3]
|
||||||
|
|
||||||
|
bTi0 = Pose2(R0, np.array([4, 3]))
|
||||||
|
bTi1 = Pose2(R90, np.array([8, 3]))
|
||||||
|
bTi2 = Pose2(R180, np.array([8, 7]))
|
||||||
|
bTi3 = Pose2(R270, np.array([4, 7]))
|
||||||
|
|
||||||
|
bTi_list = [bTi0, bTi1, bTi2, bTi3]
|
||||||
|
|
||||||
|
ab_pairs = Pose2Pairs(list(zip(aTi_list, bTi_list)))
|
||||||
|
|
||||||
|
# Recover the transformation wSe (i.e. world_S_egovehicle)
|
||||||
|
aSb = Similarity2.Align(ab_pairs)
|
||||||
|
|
||||||
|
for aTi, bTi in zip(aTi_list, bTi_list):
|
||||||
|
self.gtsamAssertEquals(aTi, aSb.transformFrom(bTi))
|
||||||
|
|
||||||
|
def test_align_poses3_along_straight_line(self):
|
||||||
|
"""Test Align Pose3Pairs method.
|
||||||
|
|
||||||
|
Scenario:
|
||||||
|
3 object poses
|
||||||
|
same scale (no gauge ambiguity)
|
||||||
|
world frame has poses rotated about x-axis (90 degree roll)
|
||||||
|
world and egovehicle frame translated by 15 meters w.r.t. each other
|
||||||
|
"""
|
||||||
|
Rx90 = Rot3.Rx(np.deg2rad(90))
|
||||||
|
|
||||||
|
# Create source poses (three objects o1, o2, o3 living in the egovehicle "e" frame)
|
||||||
|
# Suppose they are 3d cuboids detected by an onboard sensor in the egovehicle frame
|
||||||
|
eTo0 = Pose3(Rot3(), np.array([5, 0, 0]))
|
||||||
|
eTo1 = Pose3(Rot3(), np.array([10, 0, 0]))
|
||||||
|
eTo2 = Pose3(Rot3(), np.array([15, 0, 0]))
|
||||||
|
|
||||||
|
eToi_list = [eTo0, eTo1, eTo2]
|
||||||
|
|
||||||
|
# Create destination poses
|
||||||
|
# (same three objects, but instead living in the world/city "w" frame)
|
||||||
|
wTo0 = Pose3(Rx90, np.array([-10, 0, 0]))
|
||||||
|
wTo1 = Pose3(Rx90, np.array([-5, 0, 0]))
|
||||||
|
wTo2 = Pose3(Rx90, np.array([0, 0, 0]))
|
||||||
|
|
||||||
|
wToi_list = [wTo0, wTo1, wTo2]
|
||||||
|
|
||||||
|
we_pairs = gtsam.Pose3Pairs(list(zip(wToi_list, eToi_list)))
|
||||||
|
|
||||||
|
# Recover the transformation wSe (i.e. world_S_egovehicle)
|
||||||
|
wSe = Similarity3.Align(we_pairs)
|
||||||
|
|
||||||
|
for wToi, eToi in zip(wToi_list, eToi_list):
|
||||||
|
self.gtsamAssertEquals(wToi, wSe.transformFrom(eToi))
|
||||||
|
|
||||||
|
def test_align_poses3_along_straight_line_gauge(self):
|
||||||
|
"""Test if Align Pose3Pairs method can account for gauge ambiguity.
|
||||||
|
|
||||||
|
Scenario:
|
||||||
|
3 object poses
|
||||||
|
with gauge ambiguity (2x scale)
|
||||||
|
world frame has poses rotated about z-axis (90 degree yaw)
|
||||||
|
world and egovehicle frame translated by 11 meters w.r.t. each other
|
||||||
|
"""
|
||||||
|
Rz90 = Rot3.Rz(np.deg2rad(90))
|
||||||
|
|
||||||
|
# Create source poses (three objects o1, o2, o3 living in the egovehicle "e" frame)
|
||||||
|
# Suppose they are 3d cuboids detected by an onboard sensor in the egovehicle frame
|
||||||
|
eTo0 = Pose3(Rot3(), np.array([1, 0, 0]))
|
||||||
|
eTo1 = Pose3(Rot3(), np.array([2, 0, 0]))
|
||||||
|
eTo2 = Pose3(Rot3(), np.array([4, 0, 0]))
|
||||||
|
|
||||||
|
eToi_list = [eTo0, eTo1, eTo2]
|
||||||
|
|
||||||
|
# Create destination poses
|
||||||
|
# (same three objects, but instead living in the world/city "w" frame)
|
||||||
|
wTo0 = Pose3(Rz90, np.array([0, 12, 0]))
|
||||||
|
wTo1 = Pose3(Rz90, np.array([0, 14, 0]))
|
||||||
|
wTo2 = Pose3(Rz90, np.array([0, 18, 0]))
|
||||||
|
|
||||||
|
wToi_list = [wTo0, wTo1, wTo2]
|
||||||
|
|
||||||
|
we_pairs = gtsam.Pose3Pairs(list(zip(wToi_list, eToi_list)))
|
||||||
|
|
||||||
|
# Recover the transformation wSe (i.e. world_S_egovehicle)
|
||||||
|
wSe = Similarity3.Align(we_pairs)
|
||||||
|
|
||||||
|
for wToi, eToi in zip(wToi_list, eToi_list):
|
||||||
|
self.gtsamAssertEquals(wToi, wSe.transformFrom(eToi))
|
||||||
|
|
||||||
|
def test_align_poses3_scaled_squares(self):
|
||||||
|
"""Test if Align Pose3Pairs method can account for gauge ambiguity.
|
||||||
|
|
||||||
|
Make sure a big and small square can be aligned.
|
||||||
|
The u's represent a big square (10x10), and v's represents a small square (4x4).
|
||||||
|
|
||||||
|
Scenario:
|
||||||
|
4 object poses
|
||||||
|
with gauge ambiguity (2.5x scale)
|
||||||
|
"""
|
||||||
|
# 0, 90, 180, and 270 degrees yaw
|
||||||
|
R0 = Rot3.Rz(np.deg2rad(0))
|
||||||
|
R90 = Rot3.Rz(np.deg2rad(90))
|
||||||
|
R180 = Rot3.Rz(np.deg2rad(180))
|
||||||
|
R270 = Rot3.Rz(np.deg2rad(270))
|
||||||
|
|
||||||
|
aTi0 = Pose3(R0, np.array([2, 3, 0]))
|
||||||
|
aTi1 = Pose3(R90, np.array([12, 3, 0]))
|
||||||
|
aTi2 = Pose3(R180, np.array([12, 13, 0]))
|
||||||
|
aTi3 = Pose3(R270, np.array([2, 13, 0]))
|
||||||
|
|
||||||
|
aTi_list = [aTi0, aTi1, aTi2, aTi3]
|
||||||
|
|
||||||
|
bTi0 = Pose3(R0, np.array([4, 3, 0]))
|
||||||
|
bTi1 = Pose3(R90, np.array([8, 3, 0]))
|
||||||
|
bTi2 = Pose3(R180, np.array([8, 7, 0]))
|
||||||
|
bTi3 = Pose3(R270, np.array([4, 7, 0]))
|
||||||
|
|
||||||
|
bTi_list = [bTi0, bTi1, bTi2, bTi3]
|
||||||
|
|
||||||
|
ab_pairs = gtsam.Pose3Pairs(list(zip(aTi_list, bTi_list)))
|
||||||
|
|
||||||
|
# Recover the transformation wSe (i.e. world_S_egovehicle)
|
||||||
|
aSb = Similarity3.Align(ab_pairs)
|
||||||
|
|
||||||
|
for aTi, bTi in zip(aTi_list, bTi_list):
|
||||||
|
self.gtsamAssertEquals(aTi, aSb.transformFrom(bTi))
|
||||||
|
|
||||||
|
def generate_measurements(
|
||||||
|
self,
|
||||||
|
calibration: Union[Cal3Bundler, Cal3_S2],
|
||||||
|
camera_model: Union[PinholeCameraCal3Bundler, PinholeCameraCal3_S2],
|
||||||
|
cal_params: Iterable[Iterable[Union[int, float]]],
|
||||||
|
camera_set: Optional[Union[CameraSetCal3Bundler,
|
||||||
|
CameraSetCal3_S2]] = None,
|
||||||
|
) -> Tuple[List[Point2], Union[CameraSetCal3Bundler, CameraSetCal3_S2,
|
||||||
|
List[Cal3Bundler], List[Cal3_S2]]]:
|
||||||
|
"""
|
||||||
|
Generate vector of measurements for given calibration and camera model.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
calibration: Camera calibration e.g. Cal3_S2
|
||||||
|
camera_model: Camera model e.g. PinholeCameraCal3_S2
|
||||||
|
cal_params: Iterable of camera parameters for `calibration` e.g. [K1, K2]
|
||||||
|
camera_set: Cameraset object (for individual calibrations)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list of measurements and list/CameraSet object for cameras
|
||||||
|
"""
|
||||||
|
if camera_set is not None:
|
||||||
|
cameras = camera_set()
|
||||||
|
else:
|
||||||
|
cameras = []
|
||||||
|
measurements = gtsam.Point2Vector()
|
||||||
|
|
||||||
|
for k, pose in zip(cal_params, self.triangulation_poses):
|
||||||
|
K = calibration(*k)
|
||||||
|
camera = camera_model(pose, K)
|
||||||
|
cameras.append(camera)
|
||||||
|
z = camera.project(self.landmark)
|
||||||
|
measurements.append(z)
|
||||||
|
|
||||||
|
return measurements, cameras
|
||||||
|
|
||||||
|
def test_TriangulationExample(self) -> None:
|
||||||
|
"""Tests triangulation with shared Cal3_S2 calibration"""
|
||||||
|
# Some common constants
|
||||||
|
sharedCal = (1500, 1200, 0, 640, 480)
|
||||||
|
|
||||||
|
measurements, _ = self.generate_measurements(
|
||||||
|
calibration=Cal3_S2,
|
||||||
|
camera_model=PinholeCameraCal3_S2,
|
||||||
|
cal_params=(sharedCal, sharedCal))
|
||||||
|
|
||||||
|
triangulated_landmark = gtsam.triangulatePoint3(
|
||||||
|
self.triangulation_poses,
|
||||||
|
Cal3_S2(sharedCal),
|
||||||
|
measurements,
|
||||||
|
rank_tol=1e-9,
|
||||||
|
optimize=True)
|
||||||
|
self.gtsamAssertEquals(self.landmark, triangulated_landmark, 1e-9)
|
||||||
|
|
||||||
|
# Add some noise and try again: result should be ~ (4.995, 0.499167, 1.19814)
|
||||||
|
measurements_noisy = gtsam.Point2Vector()
|
||||||
|
measurements_noisy.append(measurements[0] - np.array([0.1, 0.5]))
|
||||||
|
measurements_noisy.append(measurements[1] - np.array([-0.2, 0.3]))
|
||||||
|
|
||||||
|
triangulated_landmark = gtsam.triangulatePoint3(
|
||||||
|
self.triangulation_poses,
|
||||||
|
Cal3_S2(sharedCal),
|
||||||
|
measurements_noisy,
|
||||||
|
rank_tol=1e-9,
|
||||||
|
optimize=True)
|
||||||
|
|
||||||
|
self.gtsamAssertEquals(self.landmark, triangulated_landmark, 1e-2)
|
||||||
|
|
||||||
|
def test_triangulation_robust_three_poses(self) -> None:
|
||||||
|
"""Ensure triangulation with a robust model works."""
|
||||||
|
sharedCal = Cal3_S2(1500, 1200, 0, 640, 480)
|
||||||
|
|
||||||
|
# landmark ~5 meters infront of camera
|
||||||
|
landmark = Point3(5, 0.5, 1.2)
|
||||||
|
|
||||||
|
pose1 = Pose3(UPRIGHT, Point3(0, 0, 1))
|
||||||
|
pose2 = pose1 * Pose3(Rot3(), Point3(1, 0, 0))
|
||||||
|
pose3 = pose1 * Pose3(Rot3.Ypr(0.1, 0.2, 0.1), Point3(0.1, -2, -0.1))
|
||||||
|
|
||||||
|
camera1 = PinholeCameraCal3_S2(pose1, sharedCal)
|
||||||
|
camera2 = PinholeCameraCal3_S2(pose2, sharedCal)
|
||||||
|
camera3 = PinholeCameraCal3_S2(pose3, sharedCal)
|
||||||
|
|
||||||
|
z1: Point2 = camera1.project(landmark)
|
||||||
|
z2: Point2 = camera2.project(landmark)
|
||||||
|
z3: Point2 = camera3.project(landmark)
|
||||||
|
|
||||||
|
poses = gtsam.Pose3Vector([pose1, pose2, pose3])
|
||||||
|
measurements = gtsam.Point2Vector([z1, z2, z3])
|
||||||
|
|
||||||
|
# noise free, so should give exactly the landmark
|
||||||
|
actual = gtsam.triangulatePoint3(poses,
|
||||||
|
sharedCal,
|
||||||
|
measurements,
|
||||||
|
rank_tol=1e-9,
|
||||||
|
optimize=False)
|
||||||
|
self.assertTrue(np.allclose(landmark, actual, atol=1e-2))
|
||||||
|
|
||||||
|
# Add outlier
|
||||||
|
measurements[0] += Point2(100, 120) # very large pixel noise!
|
||||||
|
|
||||||
|
# now estimate does not match landmark
|
||||||
|
actual2 = gtsam.triangulatePoint3(poses,
|
||||||
|
sharedCal,
|
||||||
|
measurements,
|
||||||
|
rank_tol=1e-9,
|
||||||
|
optimize=False)
|
||||||
|
# DLT is surprisingly robust, but still off (actual error is around 0.26m)
|
||||||
|
self.assertTrue(np.linalg.norm(landmark - actual2) >= 0.2)
|
||||||
|
self.assertTrue(np.linalg.norm(landmark - actual2) <= 0.5)
|
||||||
|
|
||||||
|
# Again with nonlinear optimization
|
||||||
|
actual3 = gtsam.triangulatePoint3(poses,
|
||||||
|
sharedCal,
|
||||||
|
measurements,
|
||||||
|
rank_tol=1e-9,
|
||||||
|
optimize=True)
|
||||||
|
# result from nonlinear (but non-robust optimization) is close to DLT and still off
|
||||||
|
self.assertTrue(np.allclose(actual2, actual3, atol=0.1))
|
||||||
|
|
||||||
|
# Again with nonlinear optimization, this time with robust loss
|
||||||
|
model = gtsam.noiseModel.Robust.Create(
|
||||||
|
gtsam.noiseModel.mEstimator.Huber.Create(1.345),
|
||||||
|
gtsam.noiseModel.Unit.Create(2))
|
||||||
|
actual4 = gtsam.triangulatePoint3(poses,
|
||||||
|
sharedCal,
|
||||||
|
measurements,
|
||||||
|
rank_tol=1e-9,
|
||||||
|
optimize=True,
|
||||||
|
model=model)
|
||||||
|
# using the Huber loss we now have a quite small error!! nice!
|
||||||
|
self.assertTrue(np.allclose(landmark, actual4, atol=0.05))
|
||||||
|
|
||||||
|
def test_outliers_and_far_landmarks(self) -> None:
|
||||||
|
"""Check safe triangulation function."""
|
||||||
|
pose1, pose2 = self.poses
|
||||||
|
|
||||||
|
K1 = Cal3_S2(1500, 1200, 0, 640, 480)
|
||||||
|
# create first camera. Looking along X-axis, 1 meter above ground plane (x-y)
|
||||||
|
camera1 = PinholeCameraCal3_S2(pose1, K1)
|
||||||
|
|
||||||
|
# create second camera 1 meter to the right of first camera
|
||||||
|
K2 = Cal3_S2(1600, 1300, 0, 650, 440)
|
||||||
|
camera2 = PinholeCameraCal3_S2(pose2, K2)
|
||||||
|
|
||||||
|
# 1. Project two landmarks into two cameras and triangulate
|
||||||
|
z1 = camera1.project(self.landmark)
|
||||||
|
z2 = camera2.project(self.landmark)
|
||||||
|
|
||||||
|
cameras = CameraSetCal3_S2()
|
||||||
|
cameras.append(camera1)
|
||||||
|
cameras.append(camera2)
|
||||||
|
|
||||||
|
measurements = gtsam.Point2Vector()
|
||||||
|
measurements.append(z1)
|
||||||
|
measurements.append(z2)
|
||||||
|
|
||||||
|
landmarkDistanceThreshold = 10 # landmark is closer than that
|
||||||
|
# all default except landmarkDistanceThreshold:
|
||||||
|
params = TriangulationParameters(1.0, False, landmarkDistanceThreshold)
|
||||||
|
actual: TriangulationResult = gtsam.triangulateSafe(
|
||||||
|
cameras, measurements, params)
|
||||||
|
self.gtsamAssertEquals(actual.get(), self.landmark, 1e-2)
|
||||||
|
self.assertTrue(actual.valid())
|
||||||
|
|
||||||
|
landmarkDistanceThreshold = 4 # landmark is farther than that
|
||||||
|
params2 = TriangulationParameters(1.0, False,
|
||||||
|
landmarkDistanceThreshold)
|
||||||
|
actual = gtsam.triangulateSafe(cameras, measurements, params2)
|
||||||
|
self.assertTrue(actual.farPoint())
|
||||||
|
|
||||||
|
# 3. Add a slightly rotated third camera above with a wrong measurement
|
||||||
|
# (OUTLIER)
|
||||||
|
pose3 = pose1 * Pose3(Rot3.Ypr(0.1, 0.2, 0.1), Point3(0.1, -2, -.1))
|
||||||
|
K3 = Cal3_S2(700, 500, 0, 640, 480)
|
||||||
|
camera3 = PinholeCameraCal3_S2(pose3, K3)
|
||||||
|
z3 = camera3.project(self.landmark)
|
||||||
|
|
||||||
|
cameras.append(camera3)
|
||||||
|
measurements.append(z3 + Point2(10, -10))
|
||||||
|
|
||||||
|
landmarkDistanceThreshold = 10 # landmark is closer than that
|
||||||
|
outlierThreshold = 100 # loose, the outlier is going to pass
|
||||||
|
params3 = TriangulationParameters(1.0, False,
|
||||||
|
landmarkDistanceThreshold,
|
||||||
|
outlierThreshold)
|
||||||
|
actual = gtsam.triangulateSafe(cameras, measurements, params3)
|
||||||
|
self.assertTrue(actual.valid())
|
||||||
|
|
||||||
|
# now set stricter threshold for outlier rejection
|
||||||
|
outlierThreshold = 5 # tighter, the outlier is not going to pass
|
||||||
|
params4 = TriangulationParameters(1.0, False,
|
||||||
|
landmarkDistanceThreshold,
|
||||||
|
outlierThreshold)
|
||||||
|
actual = gtsam.triangulateSafe(cameras, measurements, params4)
|
||||||
|
self.assertTrue(actual.outlier())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
Loading…
Reference in New Issue