Skip to article frontmatterSkip to article content

Range SLAM with iSAM

A 2D Range SLAM example, with iSAM and smart range factors

Author: Frank Dellaert

Open In Colab

Data is second UWB ranging dataset, B2 or “plaza 2”, from

“Navigating with Ranging Radios: Five Data Sets with Ground Truth”, by Joseph Djugash, Bradley Hamner, and Stephan Roth, available at https://www.ri.cmu.edu/pub_files/2009/9/Final_5datasetsRangingRadios.pdf

# pylint: disable=invalid-name, E1101

from gtsam import Point2, Pose2
import plotly.express as px
import numpy as np
import gtsam
import math

import matplotlib.pyplot as plt
from gtsam.utils import plot
from numpy.random import default_rng

rng = default_rng()

NM = gtsam.noiseModel
# load the odometry
# DR: Odometry Input (delta distance traveled and delta heading change)
#    Time (sec)  Delta Distance Traveled (m) Delta Heading (rad)
odometry = {}
data_file = gtsam.findExampleDataFile("Plaza2_DR.txt")
for row in np.loadtxt(data_file):
    t, distance_traveled, delta_heading = row
    odometry[t] = Pose2(distance_traveled, 0, delta_heading)
M = len(odometry)
print(f"Read {M} odometry entries.")
Read 4090 odometry entries.
# load the ranges from TD
#    Time (sec)  Sender / Antenna ID Receiver Node ID  Range (m)
triples = []
data_file = gtsam.findExampleDataFile("Plaza2_TD.txt")
for row in np.loadtxt(data_file):
    t, sender, receiver, _range = row
    triples.append((t, int(receiver), _range))
K = len(triples)
print(f"Read {K} range triples.")
Read 1816 range triples.
# parameters
minK = 150  # minimum number of range measurements to process initially
incK = 25  # minimum number of range measurements to process after
robust = True
# Set Noise parameters
priorSigmas = gtsam.Point3(1, 1, math.pi)
odoSigmas = gtsam.Point3(0.05, 0.01, 0.1)
sigmaR = 100        # range standard deviation

priorNoise = NM.Diagonal.Sigmas(priorSigmas)  # prior
looseNoise = NM.Isotropic.Sigma(2, 1000)     # loose LM prior
odoNoise = NM.Diagonal.Sigmas(odoSigmas)     # odometry
gaussian = NM.Isotropic.Sigma(1, sigmaR)     # non-robust
tukey = NM.Robust.Create(NM.mEstimator.Tukey.Create(15), gaussian)  # robust
rangeNoise = tukey if robust else gaussian
# Initialize iSAM
isam = gtsam.ISAM2()
print(isam)
: cliques: 0, variables: 0

# Add prior on first pose
pose0 = Pose2(-34.2086489999201, 45.3007639991120, math.pi - 2.021089)
newFactors = gtsam.NonlinearFactorGraph()
newFactors.addPriorPose2(0, pose0, priorNoise)
initial = gtsam.Values()
initial.insert(0, pose0)
print(newFactors, initial)
NonlinearFactorGraph: size: 1

Factor 0: PriorFactor on 0
  prior mean:  (-34.208649, 45.300764, 1.12050365)
  noise model: diagonal sigmas[1; 1; 3.14159265];

 Values with 1 values:
Value 0: (gtsam::Pose2)
(-34.208649, 45.300764, 1.12050365)


# set some loop variables
i = 1  # step counter
k = 0  # range measurement counter
initialized = False
lastPose = pose0
countK = 0

initializedLandmarks = set()

# Loop over odometry
for t, relative_pose in odometry.items():
    # add odometry factor
    newFactors.add(gtsam.BetweenFactorPose2(i - 1, i, relative_pose,
                                            odoNoise))

    # predict pose and add as initial estimate
    predictedPose = lastPose.compose(relative_pose)
    lastPose = predictedPose
    initial.insert(i, predictedPose)

    # Check if there are range factors to be added
    while (k < K) and (triples[k][0] <= t):
        j = triples[k][1]
        landmark_key = gtsam.symbol('L', j)
        _range = triples[k][2]
        newFactors.add(gtsam.RangeFactor2D(
            i, landmark_key, _range, rangeNoise))
        if landmark_key not in initializedLandmarks:
            p = rng.normal(loc=0, scale=100, size=(2,))
            initial.insert(landmark_key, p)
            print(f"Adding landmark L{j}")
            initializedLandmarks.add(landmark_key)
            # We also add a very loose prior on the landmark in case there is only
            # one sighting, which cannot fully determine the landmark.
            newFactors.add(gtsam.PriorFactorPoint2(
                landmark_key, Point2(0, 0), looseNoise))
        k = k + 1
        countK = countK + 1

    # Check whether to update iSAM 2
    if (k > minK) and (countK > incK):
        if not initialized:  # Do a full optimize for first minK ranges
            print(f"Initializing at time {k}")
            batchOptimizer = gtsam.LevenbergMarquardtOptimizer(
                newFactors, initial)
            initial = batchOptimizer.optimize()
            initialized = True

        isam.update(newFactors, initial)
        result = isam.calculateEstimate()
        lastPose = result.atPose2(i)
        newFactors = gtsam.NonlinearFactorGraph()
        initial = gtsam.Values()
        countK = 0

    i += 1

finalResult = isam.calculateEstimate()
Adding landmark L1
Adding landmark L6
Adding landmark L0
Adding landmark L5
Initializing at time 151
# Print optimized landmarks:
for j in [0,1,5,6]:
    landmark_key = gtsam.symbol('L', j)
    p = finalResult.atPoint2(landmark_key)
    print(f"{landmark_key}: {p}")
5476377146882523136: [-35.97329685  26.31658086]
5476377146882523137: [-75.1003452   21.01144091]
5476377146882523141: [ -1.03876425 -12.13811931]
5476377146882523142: [-36.08926944  72.3500464 ]
# plot positions
poses = gtsam.utilities.allPose2s(finalResult)
landmarks = gtsam.utilities.extractPoint2(finalResult)
positions = np.array([poses.atPose2(key).translation()
                     for key in poses.keys()])
print(positions.shape)
(4090, 2)
fig = px.scatter(x=positions[:,0],y=positions[:,1])
fig.add_scatter(x=landmarks[:,0], y=landmarks[:,1], mode="markers", showlegend= False)
fig.update_layout(margin=dict(l=0, r=0, t=0, b=0))
fig.update_yaxes(scaleanchor = "x", scaleratio = 1)
fig.show()
Loading...
landmarks
array([[-35.97329685, 26.31658086], [-75.1003452 , 21.01144091], [ -1.03876425, -12.13811931], [-36.08926944, 72.3500464 ]])