Skip to content

Commit

Permalink
Add converters to evo to perform trajectory alignment
Browse files Browse the repository at this point in the history
  • Loading branch information
mitchellcohen3 committed Mar 4, 2025
1 parent 095456f commit bc01d0d
Show file tree
Hide file tree
Showing 3 changed files with 250 additions and 0 deletions.
163 changes: 163 additions & 0 deletions navlie/utils/alignment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
"""
Utilities for interfacing with the evo package for trajectory alignment and
evaluation.
"""

import numpy as np
import typing
from pymlg import SE3, SO3
from navlie.lib.states import SE3State

from evo.core import trajectory
from evo.core import sync


def state_list_to_evo_traj(
state_list: typing.List[SE3State],
) -> trajectory.PoseTrajectory3D:
"""Converts a list of SE3States to an evo trajectory.
Parameters
----------
state_list : typing.List[SE3State]
The list of SE3States to convert
Returns
-------
trajectory.PoseTrajectory3D
The converted evo trajectory
"""
positions = np.array([state.position for state in state_list])
quats = np.array(
[SO3.to_quat(x.attitude, order="wxyz").ravel() for x in state_list]
)
stamps = np.array([x.stamp for x in state_list])
traj = trajectory.PoseTrajectory3D(positions, quats, stamps)
return traj


def evo_traj_to_state_list(
traj: trajectory.PoseTrajectory3D,
) -> typing.List[SE3State]:
"""Converts an evo trajectory to a list of SE3State.
Parameters
----------
traj : trajectory.PoseTrajectory3D
The evo pose trajectory to convert
Returns
-------
typing.List[SE3State]
The converted list of SE3State
"""
state_list = []
for i in range(len(traj.timestamps)):
pose_mat = SE3.from_components(
SO3.from_quat(traj.orientations_quat_wxyz[i, :], order="wxyz"),
traj._positions_xyz[i, :],
)
state = SE3State(
value=pose_mat,
stamp=traj.timestamps[i],
)
state.stamp = traj.timestamps[i]
state_list.append(state)
return state_list


def associate_and_align_trajectories(
traj_ref_list: typing.List[SE3State],
traj_est_list: typing.List[SE3State],
max_diff: float = 0.02,
offset: float = 0.0,
align: bool = True,
correct_scale: bool = False,
n_to_align: int = -1,
verbose: bool = False,
) -> typing.Tuple[
typing.List[SE3State], typing.List[SE3State], typing.Dict[str, np.ndarray]
]:
"""Associates the stamps of two trajectories and aligns the
estimated trajectory to the reference trajectory.
Parameters
----------
traj_ref : typing.List[SE3State]
Reference trajectory, a list of SE3State
traj_est : typing.List[SE3State]
Estimated trajectory to be aligned to the reference, a list of SE3State
max_diff : float, optional
The maximum allowable difference in timestamps between the estimate
and the reference trajectory, by default 0.02
offset : float, optional
optional time offset of the second trajectory, by default 0.0
align: bool, optional
Whether to align the trajectories, by default True
correct_scale: bool, optional
Whether to correct the scale, by default False. If correct_scale is
false, the alignment transformation is an element of SE(3), otherwise,
it is an element of Sim(3).
n_to_align: int, optional
The number of poses to use for alignment, by default -1. If n_to_align
is -1, all poses are used.
verbose : bool, optional
Verbosity flag, by default False
Returns
-------
typing.List[SE3State]
The reference trajectory, with timestamps at the same times as
the estimated trajectory.
typing.List[SE3State]
The aligned estimated trajectory.
typing.Dict[str, np.ndarray]
A dictionary containing the transformation information. The keys are:
- "rotation": The DCM of the alignment transformation
- "position": The transformation component of the alignment transformation
- "scale": The scale component alignment transformation.
"""
# Convert trajectories to evo, and associate the timestamps
traj_ref = state_list_to_evo_traj(traj_ref_list)
traj_est = state_list_to_evo_traj(traj_est_list)
traj_ref_sync, traj_est_sync = sync.associate_trajectories(
traj_ref,
traj_est,
max_diff=max_diff,
offset_2=offset,
)

if verbose:
print(f"Number of matching states: {len(traj_ref_sync.timestamps)}")
print(f"Length of reference trajectory: {len(traj_ref_list)}")
print(f"Length of estimated trajectory: {len(traj_est_list)}")

# If we are not aligning, return the synced trajectories
if not align:
traj_ref_sync = evo_traj_to_state_list(traj_ref_sync)
traj_est_sync = evo_traj_to_state_list(traj_est_sync)
transformation_dict = {
"rotation": np.identity(3),
"position": np.zeros(3),
"scale": 1.0,
}
return traj_ref_sync, traj_est_sync, transformation_dict

# Otherwise, align the trajectories
traj_est_sync: trajectory.PoseTrajectory3D
R, t, s = traj_est_sync.align(
traj_ref_sync,
correct_scale=correct_scale,
n=n_to_align,
)

# Conver back to navlie types
traj_est_aligned = evo_traj_to_state_list(traj_est_sync)
traj_ref_aligned = evo_traj_to_state_list(traj_ref_sync)
transformation_dict = {
"rotation": R,
"position": t,
"scale": s,
}

return traj_ref_aligned, traj_est_aligned, transformation_dict
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"pymlg @ git+https://github.com/decargroup/pymlg@main",
"tqdm>=4.64.1",
"seaborn>=0.11.2",
"evo>=1.31.0"
],
url="https://github.com/decargroup/navlie",
classifiers=[
Expand Down
86 changes: 86 additions & 0 deletions tests/unit/test_alignment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import typing
import numpy as np
from navlie.lib.datasets import SimulatedPoseRangingDataset
from navlie.lib.states import SE3State
from navlie.utils import plot_poses

from pymlg import SO3, SE3
import matplotlib.pyplot as plt

from navlie.utils.alignment import associate_and_align_trajectories, state_list_to_evo_traj, evo_traj_to_state_list

def test_conversion_to_evo():
"""Converts a list of SE3State to an evo trajectory and back."""
dataset = SimulatedPoseRangingDataset()
poses_1 = dataset.get_ground_truth()
evo_traj = state_list_to_evo_traj(poses_1)
poses_2 = evo_traj_to_state_list(evo_traj)

for pose_1, pose_2 in zip(poses_1, poses_2):
assert np.allclose(pose_1.position, pose_2.position, atol=1e-5)
assert np.allclose(pose_1.attitude, pose_2.attitude, atol=1e-5)
assert np.allclose(pose_1.stamp, pose_2.stamp, atol=1e-5)

def test_associate_and_align_trajectories():
"""Test utilizing evo to associate and align two trajectories."""
# Load in the sample EuRoC dataset
# Create an initial trajectory
dataset = SimulatedPoseRangingDataset()
poses_1 = dataset.get_ground_truth()

# Create a second trajectory that is a similarity transformation of the first
scale = 0.9
rot = SO3.Exp(np.array([0.01, 0.01, 1.2]))
pos = np.array([1.0, 2.0, 0.5])

poses_2: typing.List[SE3State] = []
for pose in poses_1:
new_position = scale * rot @ pose.position + pos
new_att = rot @ pose.attitude
new_pose = SE3State(
value=SE3.from_components(new_att, new_position),
stamp=pose.stamp,
direction=pose.direction,
)
poses_2.append(new_pose)

fig, ax = plot_poses(
poses_1, line_color="tab:blue", step=None, label="Original Trajectory"
)
fig, ax = plot_poses(
poses_2, ax=ax, line_color="tab:red", step=None, label="Transformed Trajectory"
)
ax.set_xlabel("x (m)")
ax.set_ylabel("y (m)")
ax.set_zlabel("z (m)")

# Try aligning the first trajectory to the second
traj_ref, aligned_traj_sim3, transformation_info = associate_and_align_trajectories(
poses_2,
poses_1,
correct_scale=True,
align=True,
verbose=True
)

# Check that the computed transformation matches the true one
assert np.allclose(transformation_info["scale"], scale, atol=1e-5)
assert np.allclose(transformation_info["rotation"], rot, atol=1e-5)
assert np.allclose(transformation_info["position"], pos, atol=1e-5)

# Verify visually
fig, ax = plot_poses(
aligned_traj_sim3,
ax=ax,
line_color="tab:green",
step=None,
label="Aligned Trajectory (Sim3)",
)

ax.legend()
fig.tight_layout()
plt.show()

if __name__ == "__main__":
test_conversion_to_evo()
test_associate_and_align_trajectories()

0 comments on commit bc01d0d

Please sign in to comment.