mirror of
https://github.com/introlab/rtabmap_ros.git
synced 2026-10-12 04:29:49 +08:00
* rtabmap_demos tests and docs * added bag testing * added rtabmap_examples launch tests * updating demo bag download paths * added netherdrone demo * Fixed rgb-only callback with lidar rejected. Updated lidar params * Added back OrbitOriented rviz view to ros2, with optional octomap wll clipping * fixing ci * lidar demo added intermediate_nodes option * added netherdrone as demo test * running rtabmap_demos tests on ci * added rtabmap_launch tests, fixed ground_truth_base_frame_id usage * ficing rolling * updating demo test harnest * densify golden trajectories to avoid tf missing * fixing tf steps * updated min icp ratio for netherdrone demo * fixing image_transport arg->params * export pose opt=0 * lets process all frames * updated netherdrone golden * fixing publishers queue size just for tests * added playdback demo doc * Adding more logs to debug ci * Fixing QOS for CI to reliable, added find-object demo test * name threads * fixing camera info expected transient on lyrical/rolling. Fixing find_object not appearing idle * 30 Hz polling backward comp * fixing test tf sim lock * fixing lyrical qos bag parsing * faster replay * lockstep * fixing clock deadlock * Added test on shutdown * updated netherdrone golden poses * Extended stereo outdoor test * updated shutdown test * updated test * multi-thread flaky test * adding backtrace when test fails * increased closure slack for netherdrone * g2o gauss newton on stereo * adjusted maximum optimizer iterations * updated default iterations * Added netherdrone in list of demos
101 lines
4.5 KiB
Python
101 lines
4.5 KiB
Python
"""
|
|
A SLAM graph as the playback tests keep and compare it, exported from rtabmap's
|
|
database by rtabmap-export (export_graph):
|
|
<prefix>.g2o the graph (--poses_format 4). OptimizerG2O::saveGraph() writes each
|
|
edge's link type as a column past the information matrix; that is
|
|
what tells the loop closures apart.
|
|
<prefix>.tum the optimized poses with their stamps (--poses_format 10: stamp x y z
|
|
qx qy qz qw). The test replays a golden one as ground truth, from which
|
|
rtabmap computes the trajectory error itself (Gt/* statistics).
|
|
"""
|
|
|
|
import shutil
|
|
import subprocess
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
from typing import Dict, List, Set, Tuple
|
|
|
|
import numpy as np
|
|
|
|
# rtabmap::Link::Type
|
|
NEIGHBOR, GLOBAL_CLOSURE, LOCAL_SPACE_CLOSURE, LOCAL_TIME_CLOSURE = 0, 1, 2, 3
|
|
LANDMARK = 8
|
|
|
|
|
|
@dataclass
|
|
class Graph:
|
|
poses: Dict[int, Tuple[float, ...]] = field(default_factory=dict) # x y z qx qy qz qw
|
|
link_types: List[int] = field(default_factory=list)
|
|
# Vertices that are landmarks, not nodes: rtabmap-export writes them after the nodes,
|
|
# with positive ids, the ends of the landmark links.
|
|
landmarks: Set[int] = field(default_factory=set)
|
|
|
|
@classmethod
|
|
def load(cls, g2o_path: str) -> 'Graph':
|
|
"""Read the vertices and the edges' types; a 2D graph (Reg/Force3DoF) is SE2."""
|
|
graph = cls()
|
|
# Fields each edge tag defines; the link type is the column after them.
|
|
edge_fields = {'EDGE_SE2': 12, 'EDGE_SE3:QUAT': 31}
|
|
with open(g2o_path) as f:
|
|
for line in f:
|
|
v = line.split()
|
|
if not v:
|
|
continue
|
|
if v[0] == 'VERTEX_SE3:QUAT':
|
|
graph.poses[int(v[1])] = tuple(float(x) for x in v[2:9])
|
|
elif v[0] == 'VERTEX_SE2':
|
|
yaw = float(v[4])
|
|
graph.poses[int(v[1])] = (float(v[2]), float(v[3]), 0.0,
|
|
0.0, 0.0, np.sin(yaw / 2), np.cos(yaw / 2))
|
|
elif v[0] in edge_fields:
|
|
n = edge_fields[v[0]]
|
|
link_type = int(v[n]) if len(v) > n else NEIGHBOR
|
|
graph.link_types.append(link_type)
|
|
if link_type == LANDMARK:
|
|
graph.landmarks.add(int(v[2]))
|
|
return graph
|
|
|
|
def summary(self) -> dict:
|
|
ids = sorted(set(self.poses) - self.landmarks)
|
|
xyz = np.array([self.poses[i][:3] for i in ids]).reshape(-1, 3)
|
|
return {
|
|
'nodes': len(ids),
|
|
'landmarks': len(self.landmarks),
|
|
'global_closures': sum(t == GLOBAL_CLOSURE for t in self.link_types),
|
|
'local_closures': sum(t in (LOCAL_SPACE_CLOSURE, LOCAL_TIME_CLOSURE)
|
|
for t in self.link_types),
|
|
'path_length': round(float(np.linalg.norm(np.diff(xyz, axis=0), axis=1).sum()), 2),
|
|
}
|
|
|
|
|
|
def export_graph(database: Path, prefix: Path):
|
|
"""Write <prefix>.g2o and <prefix>.tum from an rtabmap database.
|
|
|
|
The graph of every node in the database, optimized (--opt 0). Not the optimized poses
|
|
rtabmap saved when it closed (--opt 2): they are those of its local map only, without
|
|
the intermediate nodes, and are saved or not depending on how it closed, while
|
|
rtabmap-export falls back to optimizing every node when there are none.
|
|
"""
|
|
tool = shutil.which('rtabmap-export')
|
|
if tool is None:
|
|
raise FileNotFoundError('rtabmap-export not found on PATH (RTAB-Map built without '
|
|
'its tools?)')
|
|
out_dir = database.parent
|
|
for poses_format, extension, suffix in ((4, 'g2o', '.g2o'), (10, 'txt', '.tum')):
|
|
subprocess.run([tool, '--poses', '--poses_format', str(poses_format), '--opt', '0',
|
|
'--output_dir', str(out_dir), str(database)],
|
|
check=True, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True)
|
|
exported = out_dir / f'{database.stem}_poses.{extension}'
|
|
exported.replace(str(prefix) + suffix)
|
|
|
|
|
|
def load_tum(path: str) -> List[Tuple[float, Tuple[float, ...]]]:
|
|
"""(stamp, (x y z qx qy qz qw)) of each line of a TUM trajectory file."""
|
|
trajectory = []
|
|
with open(path) as f:
|
|
for line in f:
|
|
v = line.split()
|
|
if len(v) == 8 and not line.startswith('#'):
|
|
trajectory.append((float(v[0]), tuple(float(x) for x in v[1:])))
|
|
return trajectory
|