evo: Python Trajectory Evaluation for SLAM and Odometry
┌─────────────────────────────────────────────────────┐
│ Analysis Summary │
├─────────────────────────────────────────────────────┤
│ Type: Project │
│ Purpose: Python Trajectory Evaluation for SLAM and Odometry│
│ Primary Language: python + markdown + json │
│ LOC: 15K │
│ Test Files: 10 │
│ Architecture: python │
│ Confidence: High │
└─────────────────────────────────────────────────────┘
Analyzed: 43d02fc6 from 2025-10-04
evo: Python Trajectory Evaluation for SLAM and Odometry
evo provides a comprehensive toolkit for evaluating and comparing trajectory outputs from SLAM and odometry algorithms. The 14,515-line Python codebase supports multiple trajectory formats including TUM, KITTI, EuRoC MAV, and ROS bag files, offering both command-line tools and a programmatic API for trajectory analysis.
The project implements standard trajectory evaluation metrics including absolute pose error (APE) and relative pose error (RPE), with flexible options for trajectory association, alignment, and visualization. Built on NumPy, Matplotlib, and SciPy, it processes trajectories as 3D pose sequences with support for various coordinate representations and reference frame transformations.
Quick Start
pip install evo
Basic trajectory comparison:
cd test/data
evo_traj kitti KITTI_00_ORB.txt KITTI_00_SPTAM.txt --ref=KITTI_00_gt.txt -p --plot_mode=xz
Time to first result: ~2 minutes
Alternative Approaches
| Solution | Setup Time | Learning Curve | Format Support | Visualization |
|---|---|---|---|---|
| evo | Low | Medium | TUM/KITTI/ROS/EuRoC | Excellent |
| rpg_trajectory_evaluation | Medium | High | TUM/EuRoC | Basic |
| TUM RGB-D tools | Low | Low | TUM only | Basic |
| KITTI devkit | Low | Low | KITTI only | Basic |
| Custom scripts | High | Variable | Custom | Variable |
Architecture and Implementation
The codebase centers around trajectory synchronization and error computation. The core synchronization logic in evo/core/sync.py:42-56 implements timestamp matching:
def matching_time_indices(stamps_1: np.ndarray, stamps_2: np.ndarray,
max_diff: float = 0.01,
offset_2: float = 0.0) -> MatchingIndices:
"""
Searches for the best matching timestamps of two lists of timestamps
and returns the list indices of the best matches.
:param stamps_1: first vector of timestamps (numpy array)
:param stamps_2: second vector of timestamps (numpy array)
:param max_diff: max. allowed absolute time difference
:param offset_2: optional time offset to be applied to stamps_2
:return: 2 lists of the matching timestamp indices (stamps_1, stamps_2)
"""
matching_indices_1 = []
matching_indices_2 = []
stamps_2 = copy.deepcopy(stamps_2)
The trajectory association function in evo/core/sync.py:67-81 builds on this foundation:
def associate_trajectories(
traj_1: PoseTrajectory3D, traj_2: PoseTrajectory3D,
max_diff: float = 0.01, offset_2: float = 0.0,
first_name: str = "first trajectory",
snd_name: str = "second trajectory") -> TrajectoryPair:
"""
Synchronizes two trajectories by matching their timestamps.
:param traj_1: trajectory.PoseTrajectory3D object of first trajectory
:param traj_2: trajectory.PoseTrajectory3D object of second trajectory
:param max_diff: max. allowed absolute time difference for associating
:param offset_2: optional time offset of second trajectory
:param first_name: name of first trajectory for verbose logging
:param snd_name: name of second trajectory for verbose/debug logging
:return: traj_1, traj_2 (synchronized)
"""
Results are encapsulated in a structured container from evo/core/result.py:37-51:
class Result(object):
def __init__(self):
self.info = {}
self.stats = {}
self.np_arrays = {}
self.trajectories = {}
def __str__(self) -> str:
return self.pretty_str(stats=True)
def __eq__(self, other: object) -> bool:
if not isinstance(other, Result):
return False
equal = (self.info == other.info)
equal &= (self.stats == other.stats)
The equality implementation in evo/core/result.py:47-61 handles NumPy array comparisons:
def __eq__(self, other: object) -> bool:
if not isinstance(other, Result):
return False
equal = (self.info == other.info)
equal &= (self.stats == other.stats)
equal &= (self.trajectories == other.trajectories)
for k in self.np_arrays:
if k not in other.np_arrays:
equal &= False
break
if not equal:
break
equal &= all(
[np.array_equal(self.np_arrays[k], other.np_arrays[k])])
return equal
Performance Characteristics
Bundle Impact:
- Core library: ~13KB Python code
- Dependencies: 14 packages including NumPy, Matplotlib, SciPy
- Total installation: ~50MB with dependencies
Other Considerations:
- Runtime dependencies: 14 core packages
- Test coverage: 10 test files across 128 total files
- Build tooling: Standard Python packaging with pip
Best for: Research environments requiring flexible trajectory analysis with publication-quality visualizations and statistical comparisons.
When to Use evo
The evidence suggests this project fits well for:
- Multi-format trajectory evaluation where datasets use different formats (TUM, KITTI, ROS bags) as shown by the format support in the README
- Research workflows requiring statistical analysis with the
evo_restool demonstrating comparative statistics and visualization capabilities - Publication-quality visualization needs evidenced by the extensive matplotlib integration and plot customization options
Consider alternatives when:
- Single-format, simple comparisons are sufficient, as lighter-weight tools may be more appropriate
- Real-time trajectory evaluation is required, since the analysis focuses on post-processing rather than streaming
- Custom metrics beyond APE/RPE are needed, as the core focuses on these standard evaluation approaches
The test suite in test/test_trajectory.py:35-54 demonstrates robust trajectory validation with proper error handling for incorrect initialization parameters, while test/test_trajectory.py:78-97 shows sophisticated quaternion equivalence checking, indicating mature handling of 3D rotation representations.