Added Graph tests

This commit is contained in:
matlabbe
2026-05-17 13:57:59 -07:00
parent c498a71bc1
commit ffe2482cfb
5 changed files with 1331 additions and 120 deletions
+416 -95
View File
@@ -40,39 +40,106 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
namespace rtabmap { namespace rtabmap {
class Memory; class Memory;
/**
* @namespace graph
* @brief Pose-graph I/O, trajectory metrics, link utilities, and path planning.
*
* Functions operate on maps of signature ids to @ref Transform poses and
* @ref Link constraints (typically stored as `std::multimap<int, Link>` keyed by
* the source node id).
*
* Main groups:
* - **I/O:** @ref exportPoses(), @ref importPoses(), @ref exportGPS()
* - **Evaluation:** @ref calcKittiSequenceErrors(), @ref calcRelativeErrors(),
* @ref calcRMSE(), @ref computeMaxGraphErrors()
* - **Links:** @ref findLink(), @ref findLinks(), @ref filterLinks(),
* @ref filterDuplicateLinks()
* - **Spatial queries:** @ref findNearestNode(), @ref findNearestNodes(),
* @ref frustumPosesFiltering(), @ref radiusPosesFiltering()
* - **Planning:** @ref computePath(), @ref computePathLength(), @ref getPaths()
*/
namespace graph { namespace graph {
//////////////////////////////////////////// /**
// Graph utilities * @brief Writes poses (and optional constraints) to disk.
//////////////////////////////////////////// * @param filePath Output path; extension may be appended from @p format.
* @param format Export format:
* - `0` Raw text (`.txt`): r11 r12 r13 tx r21 r22 r23 ty r31 r32 r33 tz
* - `1` RGBD-SLAM format, in motion capture frame like the ground truth of RGB-D SLAM Dataset (requires @p stamps) : stamp x y z qx qy qz qw
* - `10` Like `1` without coordinate-frame change (i.e., in base frame) : stamp x y z qx qy qz qw
* - `11` Like `10` with landmark ids after positive ids : stamp x y z qx qy qz qw id
* - `2` KITTI odometry format : r11 r12 r13 tx r21 r22 r23 ty r31 r32 r33 tz
* - `3` TORO graph (requires @p constraints; uses @p parameters)
* - `4` g2o (requires @p constraints; uses @p parameters)
* @param poses Node id → pose.
* @param constraints Required for formats `3` and `4`.
* @param stamps Required for formats `1`, `10`, and `11` (same size as @p poses).
* @param parameters Optional optimizer parameters for formats `3` and `4`.
* @return False on I/O or validation error.
*/
bool RTABMAP_CORE_EXPORT exportPoses( bool RTABMAP_CORE_EXPORT exportPoses(
const std::string & filePath, const std::string & filePath,
int format, // 0=Raw (*.txt), 1=RGBD-SLAM motion capture (*.txt) (10=without change of coordinate frame, 11=10+ID), 2=KITTI (*.txt), 3=TORO (*.graph), 4=g2o (*.g2o) int format,
const std::map<int, Transform> & poses, const std::map<int, Transform> & poses,
const std::multimap<int, Link> & constraints = std::multimap<int, Link>(), // required for formats 3 and 4 const std::multimap<int, Link> & constraints = std::multimap<int, Link>(),
const std::map<int, double> & stamps = std::map<int, double>(), // required for format 1 const std::map<int, double> & stamps = std::map<int, double>(),
const ParametersMap & parameters = ParametersMap()); // optional for formats 3 and 4 const ParametersMap & parameters = ParametersMap());
/**
* @brief Loads poses (and optional constraints) from disk.
* @param filePath Input path.
* @param format Import format:
* - `0` Raw text: 3×4 matrix per line (`Transform::fromString()`)
* - `1` RGBD-SLAM motion capture: stamp x y z qw qx qy qz (applies optical-frame conversion)
* - `2` KITTI odometry: 3×4 matrix per line (applies optical-frame conversion)
* - `3` TORO graph (fills @p constraints)
* - `4` g2o (not supported yet)
* - `5` NewCollege: stamp x y (2D; first pose is origin)
* - `6` Malaga Urban GPS: 25-field `*_GPS.txt` line (local X/Y/Z)
* - `7` St Lucia INS: 12-field log (GPS → local ENU + roll/pitch/yaw)
* - `8` Karlsruhe: timestamp lat lon alt x y z roll pitch yaw (first pose is origin)
* - `9` EuRoC MAV: stamp x y z qw qx qy qz vx vy vz vr vp vy ax ay az (17 CSV fields)
* - `10` RGBD-SLAM like `1` without coordinate-frame change
* - `11` RGBD-SLAM like `10` with node id as 9th field: stamp x y z qw qx qy qz id
* - `12` RGBD Bonn dynamic dataset format (stamp + pose; Bonn-specific frame conversion)
* @param poses Output node id → pose.
* @param constraints Optional output links (format `3` only).
* @param stamps Optional output timestamps (formats `1`, `5`–`9`, `10`–`12` when present in file).
* @return False on I/O or parse error.
*/
bool RTABMAP_CORE_EXPORT importPoses( bool RTABMAP_CORE_EXPORT importPoses(
const std::string & filePath, const std::string & filePath,
int format, // 0=Raw, 1=RGBD-SLAM motion capture (10=without change of coordinate frame, 11=10+ID), 2=KITTI, 3=TORO, 4=g2o, 5=NewCollege(t,x,y), 6=Malaga Urban GPS, 7=St Lucia INS, 8=Karlsruhe, 9=EuRoC MAV, 12=rgbd_bonn int format,
std::map<int, Transform> & poses, std::map<int, Transform> & poses,
std::multimap<int, Link> * constraints = 0, // optional for formats 3 and 4 std::multimap<int, Link> * constraints = 0,
std::map<int, double> * stamps = 0); // optional for format 1 and 9 std::map<int, double> * stamps = 0);
/**
* @brief Exports GPS samples to a PLY point cloud.
* @param filePath Output `.ply` path.
* @param gpsValues Node id → @ref GPS fix.
* @param rgba Point color (default opaque white).
*/
bool RTABMAP_CORE_EXPORT exportGPS( bool RTABMAP_CORE_EXPORT exportGPS(
const std::string & filePath, const std::string & filePath,
const std::map<int, GPS> & gpsValues, const std::map<int, GPS> & gpsValues,
unsigned int rgba = 0xFFFFFFFF); unsigned int rgba = 0xFFFFFFFF);
/** /**
* Compute translation and rotation errors for KITTI datasets. * @brief KITTI odometry benchmark error over fixed trajectory segments.
* See http://www.cvlibs.net/datasets/kitti/eval_odometry.php. *
* @param poses_gt, Ground Truth poses * For each start pose (every 10 frames) and segment length in
* @param poses_result, Estimated poses * {100, 200, …, 800} m along @p poses_gt, compares the relative transform
* @param t_err, Output translation error (%) * GT vs estimate and accumulates normalized errors. The returned values are
* @param r_err, Output rotation error (deg/m) * the mean over all valid segments.
*
* @param poses_gt Ground-truth poses in temporal order (one per frame).
* @param poses_result Estimated poses (same length and ordering as @p poses_gt).
* @param t_err Output mean translation error (%): segment translation error (m)
* divided by segment length, averaged, then × 100.
* @param r_err Output mean rotation error (deg/m): segment rotation error (rad)
* divided by segment length, averaged, then converted to deg/m.
* @see http://www.cvlibs.net/datasets/kitti/eval_odometry.php
*/ */
void RTABMAP_CORE_EXPORT calcKittiSequenceErrors( void RTABMAP_CORE_EXPORT calcKittiSequenceErrors(
const std::vector<Transform> &poses_gt, const std::vector<Transform> &poses_gt,
@@ -81,11 +148,21 @@ void RTABMAP_CORE_EXPORT calcKittiSequenceErrors(
float & r_err); float & r_err);
/** /**
* Compute average of translation and rotation errors between each poses. * @brief Mean frame-to-frame relative pose error (RPE-style, one step).
* @param poses_gt, Ground Truth poses *
* @param poses_result, Estimated poses * For each consecutive pair `(i, i+1)`, builds the relative motion in ground
* @param t_err, Output translation error (m) * truth and in the estimate, then measures how much they differ:
* @param r_err, Output rotation error (deg) * - translation: Euclidean distance between the two relative transforms (m)
* - rotation: angle between the two relative transforms (rad → deg)
*
* Returns the arithmetic mean over all `N-1` pairs (`N` = trajectory length).
* Unlike @ref calcKittiSequenceErrors(), there is no fixed segment length and
* no path-length normalization.
*
* @param poses_gt Ground-truth poses in temporal order (one per frame).
* @param poses_result Estimated poses (same length and ordering as @p poses_gt).
* @param t_err Output mean translation error over consecutive pairs (m).
* @param r_err Output mean rotation error over consecutive pairs (deg).
*/ */
void RTABMAP_CORE_EXPORT calcRelativeErrors ( void RTABMAP_CORE_EXPORT calcRelativeErrors (
const std::vector<Transform> &poses_gt, const std::vector<Transform> &poses_gt,
@@ -94,12 +171,39 @@ void RTABMAP_CORE_EXPORT calcRelativeErrors (
float & r_err); float & r_err);
/** /**
* Compute root-mean-square error (RMSE) like the TUM RGBD * @brief Absolute trajectory error (ATE) with Sim(3)-style alignment (TUM RGB-D tool).
* dataset's evaluation tool (absolute trajectory error). *
* See https://vision.in.tum.de/data/datasets/rgbd-dataset * Only poses whose id exists in both @p groundTruth and @p poses are compared.
* @param groundTruth, Ground Truth poses * An alignment transform @c t is estimated so that per-pose error is measured after
* @param poses, Estimated poses * bringing the estimate into the reference frame:
* @return Gt to Map transform * - If more than five poses match: @c t from SVD on position correspondences
* (estimate positions → ground-truth positions; z ignored when @p align2D is true).
* - Otherwise: @c t = groundTruth[firstId] * poses[firstId]⁻¹ using the first matched id.
*
* For each matched pose, after `aligned = t * poses[id]`:
* - **Translational error:** Euclidean distance between `aligned` and `groundTruth[id]` (m).
* - **Rotational error:** Angle between the poses' +X axes (deg).
*
* The eight `@p translational_*` and `@p rotational_*` outputs are statistics over those
* per-pose errors (all matched poses). They are set to `0` when no id matches.
*
* @param groundTruth Reference trajectory (node id → pose).
* @param poses Estimated trajectory; ids not in @p groundTruth are skipped.
* @param translational_rmse Root mean square of translational errors (m).
* @param translational_mean Arithmetic mean of translational errors (m).
* @param translational_median Middle sample in matched-pose iteration order (m).
* @param translational_std Standard deviation of translational errors (m).
* @param translational_min Minimum translational error (m).
* @param translational_max Maximum translational error (m).
* @param rotational_rmse Root mean square of rotational errors (deg).
* @param rotational_mean Arithmetic mean of rotational errors (deg).
* @param rotational_median Middle sample in matched-pose iteration order (deg).
* @param rotational_std Standard deviation of rotational errors (deg).
* @param rotational_min Minimum rotational error (deg).
* @param rotational_max Maximum rotational error (deg).
* @param align2D If true, alignment uses x/y only (z set to 0 for correspondence); 3D if false.
* @return Alignment transform @c t applied as `t * poses[id]` before error computation.
* @see https://vision.in.tum.de/data/datasets/rgbd-dataset
*/ */
Transform RTABMAP_CORE_EXPORT calcRMSE( Transform RTABMAP_CORE_EXPORT calcRMSE(
const std::map<int, Transform> &groundTruth, const std::map<int, Transform> &groundTruth,
@@ -118,94 +222,203 @@ Transform RTABMAP_CORE_EXPORT calcRMSE(
float & rotational_max, float & rotational_max,
bool align2D = false); bool align2D = false);
/**
* @brief Largest pose-graph constraint violations after optimization.
*
* For each non-self-referenced link (`from != to`), compares the relative pose implied by @p poses to the
* link measurement and tracks the worst linear/angular error and error/std ratios.
*/
struct MaxGraphErrors struct MaxGraphErrors
{ {
float linear=-1.0f; // absolute error (m) of the link with maximum linear error float linear=-1.0f; ///< Absolute linear error (m) of the worst link.
float angular=-1.0f; // absolute error (rad) of the link with maximum angular error float angular=-1.0f; ///< Absolute angular error (rad) of the worst link.
float linearRatio=-1.0f; // Ratio = absolute error (m) / linear std (m), of the link with maximum linear error float linearRatio=-1.0f; ///< linear / sqrt(trans variance) of the worst link.
float angularRatio=-1.0f; // Ratio = absolute error (rad) / angular std (rad), of the link with maximum angular error float angularRatio=-1.0f; ///< angular / sqrt(rot variance) of the worst link.
Link linearLink; // link with maximum linear error Link linearLink; ///< Link with largest @ref linearRatio.
Link angularLink; // link with maximum angular error Link angularLink; ///< Link with largest @ref angularRatio.
}; };
/**
* @brief Finds the worst pose-graph constraint residuals after optimization.
*
* Iterates over @p links and, for each non-self-referenced edge (`from != to`):
* 1. Looks up `T_from` and `T_to` in @p poses (returns default @ref MaxGraphErrors if
* any endpoint pose is missing, null, or not invertible).
* 2. Builds the relative pose implied by the optimized poses:
* - Normal link: `t = T_from⁻¹ · T_to`
* - Landmark (`from < 0`): `t = T_to⁻¹ · T_from`, link measurement inverted
* 3. Compares `t` to the link transform:
* - **Linear error:** max |Δx|, |Δy|, and |Δz| (z ignored when @p for3DoF is true).
* - **Angular error:** full 3D angle between `t` and the link, or yaw-only if @p for3DoF;
* skipped for @ref Link::kLandmark when the information matrix does not constrain yaw.
* 4. Normalizes by link uncertainty: `error / sqrt(variance)` using the link information
* matrix (largest diagonal variance for translation/rotation).
*
* The returned @ref MaxGraphErrors holds the link with the highest linear and angular
* *ratios* (not necessarily the largest absolute error).
*
* @param poses Optimized node poses (must contain every `from` and `to` id used).
* @param links Graph constraints (typically `std::multimap<int, Link>` keyed by `from`).
* @param for3DoF If true, linear error uses x/y only and angular error compares yaw only.
* @return @ref MaxGraphErrors; fields stay `-1` when no valid link was checked or on early abort.
*/
MaxGraphErrors RTABMAP_CORE_EXPORT computeMaxGraphErrors( MaxGraphErrors RTABMAP_CORE_EXPORT computeMaxGraphErrors(
const std::map<int, Transform> & poses, const std::map<int, Transform> & poses,
const std::multimap<int, Link> & links, const std::multimap<int, Link> & links,
bool for3DoF = false); bool for3DoF = false);
/**
* @brief Maximum information-matrix diagonal over odometry neighbor links.
*
* Scans @p links of type @ref Link::kNeighbor or @ref Link::kNeighborMerged and,
* for each dof (x, y, z, roll, pitch, yaw), keeps the largest diagonal entry of
* the 6×6 information matrix.
*
* @param links Graph constraints (multimap keyed by source id).
* @return Six maximum information values, or an empty vector if no neighbor links exist.
*/
std::vector<double> RTABMAP_CORE_EXPORT getMaxOdomInf(const std::multimap<int, Link> & links); std::vector<double> RTABMAP_CORE_EXPORT getMaxOdomInf(const std::multimap<int, Link> & links);
/**
* @brief Finds the first link from @p from to @p to in a multimap keyed by source id.
*
* Iterates all entries with key @p from and matches the destination (and optionally
* @p type). When @p checkBothWays is true, also searches key @p to for a link back
* to @p from.
*
* @param links Link multimap (`key` = source node id).
* @param from Source node id.
* @param to Destination node id.
* @param checkBothWays If true, also match `to → from`.
* @param type Required link type, or @ref Link::kUndef to accept any type.
* @return Iterator to the link, or `links.end()` if not found.
*/
std::multimap<int, Link>::iterator RTABMAP_CORE_EXPORT findLink( std::multimap<int, Link>::iterator RTABMAP_CORE_EXPORT findLink(
std::multimap<int, Link> & links, std::multimap<int, Link> & links,
int from, int from,
int to, int to,
bool checkBothWays = true, bool checkBothWays = true,
Link::Type type = Link::kUndef); Link::Type type = Link::kUndef);
/** @overload `std::multimap<int, std::pair<int, Link::Type>>`. */
std::multimap<int, std::pair<int, Link::Type> >::iterator RTABMAP_CORE_EXPORT findLink( std::multimap<int, std::pair<int, Link::Type> >::iterator RTABMAP_CORE_EXPORT findLink(
std::multimap<int, std::pair<int, Link::Type> > & links, std::multimap<int, std::pair<int, Link::Type> > & links,
int from, int from,
int to, int to,
bool checkBothWays = true, bool checkBothWays = true,
Link::Type type = Link::kUndef); Link::Type type = Link::kUndef);
/** @overload `std::multimap<int, int>`. */
std::multimap<int, int>::iterator RTABMAP_CORE_EXPORT findLink( std::multimap<int, int>::iterator RTABMAP_CORE_EXPORT findLink(
std::multimap<int, int> & links, std::multimap<int, int> & links,
int from, int from,
int to, int to,
bool checkBothWays = true); bool checkBothWays = true);
/** @overload const `std::multimap<int, Link>`. */
std::multimap<int, Link>::const_iterator RTABMAP_CORE_EXPORT findLink( std::multimap<int, Link>::const_iterator RTABMAP_CORE_EXPORT findLink(
const std::multimap<int, Link> & links, const std::multimap<int, Link> & links,
int from, int from,
int to, int to,
bool checkBothWays = true, bool checkBothWays = true,
Link::Type type = Link::kUndef); Link::Type type = Link::kUndef);
/** @overload const `std::multimap<int, std::pair<int, Link::Type>>`. */
std::multimap<int, std::pair<int, Link::Type> >::const_iterator RTABMAP_CORE_EXPORT findLink( std::multimap<int, std::pair<int, Link::Type> >::const_iterator RTABMAP_CORE_EXPORT findLink(
const std::multimap<int, std::pair<int, Link::Type> > & links, const std::multimap<int, std::pair<int, Link::Type> > & links,
int from, int from,
int to, int to,
bool checkBothWays = true, bool checkBothWays = true,
Link::Type type = Link::kUndef); Link::Type type = Link::kUndef);
/** @overload const `std::multimap<int, int>`. */
std::multimap<int, int>::const_iterator RTABMAP_CORE_EXPORT findLink( std::multimap<int, int>::const_iterator RTABMAP_CORE_EXPORT findLink(
const std::multimap<int, int> & links, const std::multimap<int, int> & links,
int from, int from,
int to, int to,
bool checkBothWays = true); bool checkBothWays = true);
/**
* @brief Lists all links incident on node @p from.
*
* Outgoing links (`link.from() == from`) are returned as stored; for incoming links
* (`link.to() == from`), the inverse link is returned so the pose of @p from is always
* the source frame.
*
* @param links Graph constraints.
* @param from Node id to query.
* @return Incident links (may be empty).
*/
std::list<Link> RTABMAP_CORE_EXPORT findLinks( std::list<Link> RTABMAP_CORE_EXPORT findLinks(
const std::multimap<int, Link> & links, const std::multimap<int, Link> & links,
int from); int from);
/**
* @brief Removes duplicate undirected links.
*
* Keeps the first occurrence of each `(from, to)` or `(to, from)` pair with the same
* @ref Link::Type (see @ref findLink() with @p checkBothWays).
*
* @param links Input link multimap.
* @return Copy without duplicates.
*/
std::multimap<int, Link> RTABMAP_CORE_EXPORT filterDuplicateLinks( std::multimap<int, Link> RTABMAP_CORE_EXPORT filterDuplicateLinks(
const std::multimap<int, Link> & links); const std::multimap<int, Link> & links);
/** /**
* Return links not of type "filteredType". If inverted=true, return links of type "filteredType". * @brief Filters links by type or self-reference.
*
* - If @p filteredType is @ref Link::kSelfRefLink: exclude self-references (`from == to`),
* or include only them when @p inverted is true.
* - Otherwise: exclude links of @p filteredType, or keep only that type when @p inverted is true.
*
* @param links Input links.
* @param filteredType Type to filter, or @ref Link::kSelfRefLink for self-reference filtering.
* @param inverted If true, keep the filtered set instead of removing it.
* @return Filtered link container (same structure as input).
*/ */
std::multimap<int, Link> RTABMAP_CORE_EXPORT filterLinks( std::multimap<int, Link> RTABMAP_CORE_EXPORT filterLinks(
const std::multimap<int, Link> & links, const std::multimap<int, Link> & links,
Link::Type filteredType, Link::Type filteredType,
bool inverted = false); bool inverted = false);
/** /** @overload for `std::map<int, Link>`. */
* Return links not of type "filteredType". If inverted=true, return links of type "filteredType".
*/
std::map<int, Link> RTABMAP_CORE_EXPORT filterLinks( std::map<int, Link> RTABMAP_CORE_EXPORT filterLinks(
const std::map<int, Link> & links, const std::map<int, Link> & links,
Link::Type filteredType, Link::Type filteredType,
bool inverted = false); bool inverted = false);
//Note: This assumes a coordinate system where X is forward, * Y is up, and Z is right. /**
* @brief Keeps poses inside (or outside) a camera frustum.
*
* Transforms each pose position into the frustum defined by @p cameraPose using
* @ref util3d::frustumFiltering() (this assumes the cameraPose includes the optical rotation of the camera (X right, Y down, Z forward).
*
* @param poses Input poses (null poses are skipped) in base frame (X forward, Y left, Z up),
* @param cameraPose Frustum origin and orientation including the optical rotation of the camera (X right, Y down, Z forward).
* @param horizontalFOV Horizontal field of view (deg); see @ref CameraModel::horizontalFOV().
* @param verticalFOV Vertical field of view (deg); see @ref CameraModel::verticalFOV().
* @param nearClipPlaneDistance Near clipping distance (m).
* @param farClipPlaneDistance Far clipping distance (m).
* @param negative If false, keep poses inside the frustum; if true, keep poses outside.
* @return Subset of @p poses passing the filter.
*/
std::map<int, Transform> RTABMAP_CORE_EXPORT frustumPosesFiltering( std::map<int, Transform> RTABMAP_CORE_EXPORT frustumPosesFiltering(
const std::map<int, Transform> & poses, const std::map<int, Transform> & poses,
const Transform & cameraPose, const Transform & cameraPose,
float horizontalFOV = 45.0f, // in degrees, xfov = atan((image_width/2)/fx)*2 float horizontalFOV = 45.0f,
float verticalFOV = 45.0f, // in degrees, yfov = atan((image_height/2)/fy)*2 float verticalFOV = 45.0f,
float nearClipPlaneDistance = 0.1f, float nearClipPlaneDistance = 0.1f,
float farClipPlaneDistance = 100.0f, float farClipPlaneDistance = 100.0f,
bool negative = false); bool negative = false);
/** /**
* Get only the the most recent or older poses in the defined radius. * @brief Subsamples poses that are spatially (and optionally angularly) redundant.
* @param poses The poses *
* @param radius Radius (m) of the search for near neighbors * For each pose not yet processed, finds all poses within @p radius (KD-tree). When
* @param angle Maximum angle (rad, [0,PI]) of accepted neighbor nodes in the radius (0 means ignore angle) * @p angle &gt; 0, only poses whose +X axis differs by at most @p angle (rad) are grouped.
* @param keepLatest keep the latest node if true, otherwise the oldest node is kept * From each group, keeps one pose: the latest in map order if @p keepLatest, otherwise
* @return A map containing only most recent or older poses in the the defined radius * the earliest. The first and last poses of the input map are always kept.
*
* @param poses Input trajectory (map iteration order defines “latest/oldest”).
* @param radius Clustering radius (m); if `≤ 0` or fewer than three poses, returns @p poses unchanged.
* @param angle Max heading difference within a cluster (rad); `0` ignores orientation.
* @param keepLatest If true, keep the latest pose per cluster; otherwise the earliest.
* @return Subsampled poses.
*/ */
std::map<int, Transform> RTABMAP_CORE_EXPORT radiusPosesFiltering( std::map<int, Transform> RTABMAP_CORE_EXPORT radiusPosesFiltering(
const std::map<int, Transform> & poses, const std::map<int, Transform> & poses,
@@ -214,31 +427,55 @@ std::map<int, Transform> RTABMAP_CORE_EXPORT radiusPosesFiltering(
bool keepLatest = true); bool keepLatest = true);
/** /**
* Get all neighbor nodes in a fixed radius around each pose. * @brief Radius-neighbor clustering of poses.
* @param poses The poses *
* @param radius Radius (m) of the search for near neighbors * For each pose, inserts `(queryId, neighborId)` into the output for every other pose
* @param angle Maximum angle (rad, [0,PI]) of accepted neighbor nodes in the radius (0 means ignore angle) * within @p radius (and within @p angle of the query heading when @p angle &gt; 0).
* @return A map between each pose id and its neighbors found in the radius *
* @param poses Input poses.
* @param radius Search radius (m); no pairs if `≤ 0` or fewer than two poses.
* @param angle Max heading difference (rad); `0` ignores orientation.
* @return Multimap of pose id → neighbor id (both ids from @p poses).
*/ */
std::multimap<int, int> RTABMAP_CORE_EXPORT radiusPosesClustering( std::multimap<int, int> RTABMAP_CORE_EXPORT radiusPosesClustering(
const std::map<int, Transform> & poses, const std::map<int, Transform> & poses,
float radius, float radius,
float angle); float angle);
/**
* @brief Reduces a pose graph into hyper-nodes and hyper-links.
*
* **Hyper-nodes:** clusters poses connected by non-neighbor loop-closure links.
* Clustering starts from the largest id downward; each cluster is keyed by its parent
* (hyper-node) id.
*
* **Hyper-links:** for each @ref Link::kNeighbor or @ref Link::kNeighborMerged link between
* different clusters, builds one merged @ref Link along the shortest path through
* intra-cluster closure links (Dijkstra with unit cost).
*
* @param poses Input optimized poses.
* @param links Input constraints (should be unique per directed edge for closure links).
* @param hyperNodes Output `hyperNodeId → childPoseId` membership.
* @param hyperLinks Output links between hyper-nodes (one per hyper-edge, most recent kept).
*/
void reduceGraph( void reduceGraph(
const std::map<int, Transform> & poses, const std::map<int, Transform> & poses,
const std::multimap<int, Link> & links, const std::multimap<int, Link> & links,
std::multimap<int, int> & hyperNodes, //<parent ID, child ID> std::multimap<int, int> & hyperNodes,
std::multimap<int, Link> & hyperLinks); std::multimap<int, Link> & hyperLinks);
/** /**
* Perform A* path planning in the graph. * @brief A* shortest path on a pose graph with Euclidean edge costs.
* @param poses The graph's poses *
* @param links The graph's links (from node id -> to node id) * Edge cost between adjacent nodes is the Euclidean distance between their poses in
* @param from initial node * @p poses. Uses `costSoFar + distToEnd` where `distToEnd` is the distance to the goal pose.
* @param to final node *
* @param updateNewCosts Keep up-to-date costs while traversing the graph. * @param poses Node id → pose (must contain every node reached by @p links).
* @return the path ids from id "from" to id "to" including initial and final nodes. * @param links Directed edges (`from` → `to`) keyed by source id.
* @param from Start node id.
* @param to Goal node id.
* @param updateNewCosts If true, use a multimap queue that can decrease keys when a shorter path is found.
* @return Ordered path from @p from to @p to (inclusive) with poses; empty if unreachable.
*/ */
std::list<std::pair<int, Transform> > RTABMAP_CORE_EXPORT computePath( std::list<std::pair<int, Transform> > RTABMAP_CORE_EXPORT computePath(
const std::map<int, rtabmap::Transform> & poses, const std::map<int, rtabmap::Transform> & poses,
@@ -248,14 +485,17 @@ std::list<std::pair<int, Transform> > RTABMAP_CORE_EXPORT computePath(
bool updateNewCosts = false); bool updateNewCosts = false);
/** /**
* Perform Dijkstra path planning in the graph. * @brief Dijkstra shortest path on link constraints.
* @param poses The graph's poses *
* @param links The graph's links (from node id -> to node id) * Explores outgoing links keyed by `link.from()`. Edge cost is `1` when
* @param from initial node * @p useSameCostForAllLinks is true, otherwise the translation norm of the link transform.
* @param to final node *
* @param updateNewCosts Keep up-to-date costs while traversing the graph. * @param links Constraints keyed by source node id.
* @param useSameCostForAllLinks Ignore distance between nodes * @param from Start node id.
* @return the path ids from id "from" to id "to" including initial and final nodes. * @param to Goal node id.
* @param updateNewCosts If true, allow cost improvements on open nodes.
* @param useSameCostForAllLinks If true, unit edge cost; else use `link.transform().getNorm()`.
* @return Node ids from @p from to @p to (inclusive); empty if unreachable.
*/ */
std::list<int> RTABMAP_CORE_EXPORT computePath( std::list<int> RTABMAP_CORE_EXPORT computePath(
const std::multimap<int, Link> & links, const std::multimap<int, Link> & links,
@@ -265,13 +505,36 @@ std::list<int> RTABMAP_CORE_EXPORT computePath(
bool useSameCostForAllLinks = false); bool useSameCostForAllLinks = false);
/** /**
* Perform Dijkstra path planning in the graph. * @brief Dijkstra path through the live @ref Memory pose graph.
* @param fromId initial node *
* @param toId final node * Loads links from @ref Memory (optionally from the database), chains transforms along
* @param memory The graph's memory * the chosen path, and returns the accumulated poses. Self-referenced links are skipped.
* @param lookInDatabase check links in database *
* @param updateNewCosts Keep up-to-date costs while traversing the graph. * By default (`linearVelocity` and `angularVelocity` ≤ 0), edge cost is translation
* @return the path ids from id "fromId" to id "toId" including initial and final nodes (Identity pose for the first node). * distance (m) only. When set &gt; 0, costs are expressed in seconds of motion:
* - @p linearVelocity adds `linkTranslation / linearVelocity` (time to drive the edge at
* that speed). Used alone it scales every edge by the same factor, so the **shortest path
* is unchanged**; set it to your robot’s typical forward speed (e.g. `0.5` m/s) when you
* also use @p angularVelocity so translation and rotation costs are comparable.
* - @p angularVelocity adds `headingMismatch / angularVelocity`, where heading mismatch is
* the angle between the displacement to the next node and that node’s forward (+X) axis.
* This is what changes which path is chosen: a chain followed **mostly forward** (small
* mismatch) can beat a shorter route through loop closures that require large reorientations
* (e.g. `angularVelocity = 1.0` rad/s with `linearVelocity = 0.5` m/s).
* With @p angularVelocity &gt; 0 and @p linearVelocity ≤ 0, translation is ignored and the
* path minimizes heading mismatch only (forward-following paths, regardless of distance).
* This can help loop-closure detection when the map was built with a forward-facing camera:
* the path stays aligned with how places were observed while driving forward.
*
* @param fromId Start signature id (`≥ 0`).
* @param toId Goal signature id (`≠ 0`).
* @param memory Graph memory (must not be null).
* @param lookInDatabase If true, load links from the database when not already in RAM.
* @param updateNewCosts If true, allow cost improvements on open nodes.
* @param linearVelocity If &gt; 0, add `translationNorm / linearVelocity` to edge cost (m/s).
* @param angularVelocity If &gt; 0, add rotation time from motion direction change (rad/s).
* @param ignoreDirectLinks If true, skip the direct edge between @p fromId and @p toId.
* @return Path as `(nodeId, pose)` pairs; first pose is identity at @p fromId. Empty if unreachable.
*/ */
std::list<std::pair<int, Transform> > RTABMAP_CORE_EXPORT computePath( std::list<std::pair<int, Transform> > RTABMAP_CORE_EXPORT computePath(
int fromId, int fromId,
@@ -279,16 +542,19 @@ std::list<std::pair<int, Transform> > RTABMAP_CORE_EXPORT computePath(
const Memory * memory, const Memory * memory,
bool lookInDatabase = true, bool lookInDatabase = true,
bool updateNewCosts = false, bool updateNewCosts = false,
float linearVelocity = 0.0f, // m/sec float linearVelocity = 0.0f,
float angularVelocity = 0.0f, // rad/sec float angularVelocity = 0.0f,
bool ignoreDirectLinks = false); bool ignoreDirectLinks = false);
/** /**
* Find the nearest node of the target pose * @brief Id of the nearest pose to @p targetPose.
* @param nodes the nodes to search for *
* @param targetPose the target pose to search around * Wrapper around @ref findNearestNodes() with `radius=0`, `k=1` (1-NN in 3D).
* @param distance squared distance of the nearest node found (optional) *
* @return the node id. * @param poses Nodes to search.
* @param targetPose Query position (x, y, z only; orientation is not used).
* @param distance If not null, set to the squared Euclidean distance of the match.
* @return Closest node id, or `0` if @p poses is empty.
*/ */
int RTABMAP_CORE_EXPORT findNearestNode( int RTABMAP_CORE_EXPORT findNearestNode(
const std::map<int, rtabmap::Transform> & poses, const std::map<int, rtabmap::Transform> & poses,
@@ -296,12 +562,18 @@ int RTABMAP_CORE_EXPORT findNearestNode(
float * distance = 0); float * distance = 0);
/** /**
* Find the nearest nodes of the query pose or node * @brief Spatial neighbors of a node (KD-tree radius or k-NN search).
* @param nodeId the query id *
* @param nodes the nodes to search for * @p nodeId is removed from the search set. Requires `radius &gt; 0` or `k &gt; 0`.
* @param radius radius to search for (m), if 0, k should be > 0. * When `radius &gt; 0`, returns all poses within @p radius (up to @p k if `k &gt; 0`).
* @param k max nearest neighbors (0=all inside the radius) * When `radius == 0`, returns the @p k nearest neighbors.
* @return the nodes with squared distance to query node. *
* @param nodeId Query node (must exist in @p poses); excluded from results.
* @param poses Candidate poses.
* @param radius Search radius (m).
* @param angle Max +X axis angle difference (rad); `0` ignores heading.
* @param k Max neighbors (`0` = all within radius).
* @return Neighbor id → squared Euclidean distance.
*/ */
std::map<int, float> RTABMAP_CORE_EXPORT findNearestNodes( std::map<int, float> RTABMAP_CORE_EXPORT findNearestNodes(
int nodeId, int nodeId,
@@ -309,18 +581,32 @@ std::map<int, float> RTABMAP_CORE_EXPORT findNearestNodes(
float radius, float radius,
float angle = 0.0f, float angle = 0.0f,
int k=0); int k=0);
/**
* @brief Spatial neighbors of a pose (KD-tree radius or k-NN search).
* @param targetPose Query pose (position used; orientation used when @p angle &gt; 0).
* @param poses Candidate poses (not modified).
* @param radius Search radius (m).
* @param angle Max +X axis angle difference (rad); `0` ignores heading.
* @param k Max neighbors (`0` = all within radius).
* @return Neighbor id → squared Euclidean distance.
*/
std::map<int, float> RTABMAP_CORE_EXPORT findNearestNodes( std::map<int, float> RTABMAP_CORE_EXPORT findNearestNodes(
const Transform & targetPose, const Transform & targetPose,
const std::map<int, Transform> & poses, const std::map<int, Transform> & poses,
float radius, float radius,
float angle = 0.0f, float angle = 0.0f,
int k=0); int k=0);
/**
* @brief Like @ref findNearestNodes(int,const std::map<int,Transform>&,float,float,int)
* but returns full @ref Transform values.
*/
std::map<int, Transform> RTABMAP_CORE_EXPORT findNearestPoses( std::map<int, Transform> RTABMAP_CORE_EXPORT findNearestPoses(
int nodeId, int nodeId,
const std::map<int, Transform> & poses, const std::map<int, Transform> & poses,
float radius, float radius,
float angle = 0.0f, float angle = 0.0f,
int k=0); int k=0);
/** @overload query by @ref Transform instead of node id. */
std::map<int, Transform> RTABMAP_CORE_EXPORT findNearestPoses( std::map<int, Transform> RTABMAP_CORE_EXPORT findNearestPoses(
const Transform & targetPose, const Transform & targetPose,
const std::map<int, Transform> & poses, const std::map<int, Transform> & poses,
@@ -328,28 +614,63 @@ std::map<int, Transform> RTABMAP_CORE_EXPORT findNearestPoses(
float angle = 0.0f, float angle = 0.0f,
int k=0); int k=0);
// Use new findNearestNodes() interface with radius=0, angle=0. /** @deprecated Use @ref findNearestNodes(const Transform&,const std::map<int,Transform>&,float,float,int) with `radius=0`, `k` set. */
RTABMAP_DEPRECATED std::map<int, float> RTABMAP_CORE_EXPORT findNearestNodes(const std::map<int, rtabmap::Transform> & nodes, const rtabmap::Transform & targetPose, int k); RTABMAP_DEPRECATED std::map<int, float> RTABMAP_CORE_EXPORT findNearestNodes(const std::map<int, rtabmap::Transform> & nodes, const rtabmap::Transform & targetPose, int k);
// Renamed to findNearestNodes() /** @deprecated Use @ref findNearestNodes(int,const std::map<int,Transform>&,float,float,int). */
RTABMAP_DEPRECATED std::map<int, float> RTABMAP_CORE_EXPORT getNodesInRadius(int nodeId, const std::map<int, Transform> & nodes, float radius); RTABMAP_DEPRECATED std::map<int, float> RTABMAP_CORE_EXPORT getNodesInRadius(int nodeId, const std::map<int, Transform> & nodes, float radius);
// Renamed to findNearestNodes() /** @deprecated Use @ref findNearestNodes(const Transform&,const std::map<int,Transform>&,float,float,int). */
RTABMAP_DEPRECATED std::map<int, float> RTABMAP_CORE_EXPORT getNodesInRadius(const Transform & targetPose, const std::map<int, Transform> & nodes, float radius); RTABMAP_DEPRECATED std::map<int, float> RTABMAP_CORE_EXPORT getNodesInRadius(const Transform & targetPose, const std::map<int, Transform> & nodes, float radius);
// Renamed to findNearestNodes() /** @deprecated Use @ref findNearestPoses(int,const std::map<int,Transform>&,float,float,int). */
RTABMAP_DEPRECATED std::map<int, Transform> RTABMAP_CORE_EXPORT getPosesInRadius(int nodeId, const std::map<int, Transform> & nodes, float radius, float angle = 0.0f); RTABMAP_DEPRECATED std::map<int, Transform> RTABMAP_CORE_EXPORT getPosesInRadius(int nodeId, const std::map<int, Transform> & nodes, float radius, float angle = 0.0f);
// Renamed to findNearestNodes() /** @deprecated Use @ref findNearestPoses(const Transform&,const std::map<int,Transform>&,float,float,int). */
RTABMAP_DEPRECATED std::map<int, Transform> RTABMAP_CORE_EXPORT getPosesInRadius(const Transform & targetPose, const std::map<int, Transform> & nodes, float radius, float angle = 0.0f); RTABMAP_DEPRECATED std::map<int, Transform> RTABMAP_CORE_EXPORT getPosesInRadius(const Transform & targetPose, const std::map<int, Transform> & nodes, float radius, float angle = 0.0f);
/**
* @brief Path length along an ordered list of poses.
*
* Sums `path[i].second.getDistance(path[i+1].second)` for consecutive entries.
*
* @param path Ordered `(nodeId, pose)` pairs.
* @return Total length (m), or `0` if fewer than two poses.
*/
float RTABMAP_CORE_EXPORT computePathLength( float RTABMAP_CORE_EXPORT computePathLength(
const std::vector<std::pair<int, Transform> > & path); const std::vector<std::pair<int, Transform> > & path);
// assuming they are all linked in map order /**
* @brief Path length in map iteration order.
*
* Sums distances between consecutive poses in ascending map key order (does not verify
* that entries form a connected path in the graph).
*
* @param path Poses keyed by node id (sorted by key).
* @return Total length (m), or `0` if fewer than two poses.
*/
float RTABMAP_CORE_EXPORT computePathLength( float RTABMAP_CORE_EXPORT computePathLength(
const std::map<int, Transform> & path); const std::map<int, Transform> & path);
/**
* @brief Splits poses into chains connected only by neighbor links.
*
* Repeatedly builds a path starting from the lowest remaining id: adds the next pose
* in map order only if a @ref Link::kNeighbor or @ref Link::kNeighborMerged link exists
* from the previous pose to it. Stops at the first gap, pushes the chain, and continues
* until @p poses is empty.
*
* @param poses Input poses (cleared as segments are extracted).
* @param links Graph constraints keyed by source id.
* @return List of pose maps, each a contiguous neighbor chain.
*/
std::list<std::map<int, Transform> > RTABMAP_CORE_EXPORT getPaths( std::list<std::map<int, Transform> > RTABMAP_CORE_EXPORT getPaths(
std::map<int, Transform> poses, std::map<int, Transform> poses,
const std::multimap<int, Link> & links); const std::multimap<int, Link> & links);
/**
* @brief Axis-aligned bounding box of pose positions.
*
* @param poses Input poses (no effect if empty).
* @param min Output minimum (x, y, z) in meters.
* @param max Output maximum (x, y, z) in meters.
*/
void RTABMAP_CORE_EXPORT computeMinMax(const std::map<int, Transform> & poses, void RTABMAP_CORE_EXPORT computeMinMax(const std::map<int, Transform> & poses,
cv::Vec3f & min, cv::Vec3f & min,
cv::Vec3f & max); cv::Vec3f & max);
+2 -2
View File
@@ -1,5 +1,5 @@
/* /*
Copyright (c) 2010-2018, Mathieu Labbe - IntRoLab - Universite de Sherbrooke Copyright (c) 2010-2026, Mathieu Labbe - IntRoLab - Universite de Sherbrooke
All rights reserved. All rights reserved.
Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without
@@ -130,7 +130,7 @@ public:
* Applies @ref localTransform() rotation to vectors and covariances, then sets * Applies @ref localTransform() rotation to vectors and covariances, then sets
* rotational part of @ref localTransform() to identity (translation unchanged). * rotational part of @ref localTransform() to identity (translation unchanged).
* No-op if @ref localTransform() is null or rotation is identity. * No-op if @ref localTransform() is null or rotation is identity.
* Orientation is updated only when quaternion x, y, z are not all zero. * Orientation is updated only when quaternion qx, qy, qz, qw are not all zero.
*/ */
void convertToBaseFrame(); void convertToBaseFrame();
+48 -23
View File
@@ -666,18 +666,11 @@ int32_t lastFrameFromSegmentLength(std::vector<float> &dist,int32_t first_frame,
} }
inline float rotationError(const Transform &pose_error) { inline float rotationError(const Transform &pose_error) {
float a = pose_error(0,0); return pose_error.getAngle(Transform::getIdentity());
float b = pose_error(1,1);
float c = pose_error(2,2);
float d = 0.5*(a+b+c-1.0);
return std::acos(std::max(std::min(d,1.0f),-1.0f));
} }
inline float translationError(const Transform &pose_error) { inline float translationError(const Transform &pose_error) {
float dx = pose_error.x(); return pose_error.getNorm();
float dy = pose_error.y();
float dz = pose_error.z();
return sqrt(dx*dx+dy*dy+dz*dz);
} }
void calcKittiSequenceErrors ( void calcKittiSequenceErrors (
@@ -688,6 +681,14 @@ void calcKittiSequenceErrors (
UASSERT(poses_gt.size() == poses_result.size()); UASSERT(poses_gt.size() == poses_result.size());
t_err = 0.0f;
r_err = 0.0f;
if(poses_gt.size() < 2)
{
return;
}
// error vector // error vector
std::vector<errors> err; std::vector<errors> err;
@@ -713,24 +714,35 @@ void calcKittiSequenceErrors (
if (last_frame==-1) if (last_frame==-1)
continue; continue;
const Transform & gtFirst = poses_gt[first_frame];
const Transform & gtLast = poses_gt[last_frame];
const Transform & estFirst = poses_result[first_frame];
const Transform & estLast = poses_result[last_frame];
UASSERT_MSG(gtFirst.isInvertible() && gtLast.isInvertible() &&
estFirst.isInvertible() && estLast.isInvertible(),
uFormat("Non-invertible poses at frames %d and %d (segment length %f m)",
first_frame, last_frame, len).c_str());
// compute rotational and translational errors // compute rotational and translational errors
Transform pose_delta_gt = poses_gt[first_frame].inverse()*poses_gt[last_frame]; Transform pose_delta_gt = gtFirst.inverse()*gtLast;
Transform pose_delta_result = poses_result[first_frame].inverse()*poses_result[last_frame]; Transform pose_delta_result = estFirst.inverse()*estLast;
Transform pose_error = pose_delta_result.inverse()*pose_delta_gt; Transform pose_error = pose_delta_result.inverse()*pose_delta_gt;
float r_err = rotationError(pose_error); const float rotErr = rotationError(pose_error);
float t_err = translationError(pose_error); const float transErr = translationError(pose_error);
// compute speed // compute speed
float num_frames = (float)(last_frame-first_frame+1); float num_frames = (float)(last_frame-first_frame+1);
float speed = len/(0.1*num_frames); float speed = len/(0.1f*num_frames);
// write to file // write to file
err.push_back(errors(first_frame,r_err/len,t_err/len,len,speed)); err.push_back(errors(first_frame, rotErr/len, transErr/len, len, speed));
} }
} }
t_err = 0; if(err.empty())
r_err = 0; {
return;
}
// for all errors do => compute sum of t_err, r_err // for all errors do => compute sum of t_err, r_err
for (std::vector<errors>::iterator it=err.begin(); it!=err.end(); it++) for (std::vector<errors>::iterator it=err.begin(); it!=err.end(); it++)
@@ -740,11 +752,11 @@ void calcKittiSequenceErrors (
} }
// save errors // save errors
float num = err.size(); const float num = float(err.size());
t_err /= num; t_err /= num;
r_err /= num; r_err /= num;
t_err *= 100.0f; // Translation error (%) t_err *= 100.0f; // Translation error (%)
r_err *= 180/CV_PI; // Rotation error (deg/m) r_err *= 180.0f/CV_PI; // Rotation error (deg/m)
} }
// KITTI evaluation end // KITTI evaluation end
@@ -1872,6 +1884,7 @@ std::list<std::pair<int, Transform> > computePath(
if(mapIter->second == nodeIter->first) if(mapIter->second == nodeIter->first)
{ {
pqmap.erase(mapIter); pqmap.erase(mapIter);
nodeIter->second.setFromId(currentNode->id());
nodeIter->second.setCostSoFar(newCostSoFar); nodeIter->second.setCostSoFar(newCostSoFar);
pqmap.insert(std::make_pair(nodeIter->second.totalCost(), nodeIter->first)); pqmap.insert(std::make_pair(nodeIter->second.totalCost(), nodeIter->first));
break; break;
@@ -1962,7 +1975,7 @@ std::list<int> computePath(
pq.push(Pair(n.id(), n.totalCost())); pq.push(Pair(n.id(), n.totalCost()));
} }
} }
else if(!useSameCostForAllLinks && updateNewCosts && nodeIter->second.isOpened()) else if(updateNewCosts && nodeIter->second.isOpened())
{ {
float newCostSoFar = currentNode->costSoFar() + cost; float newCostSoFar = currentNode->costSoFar() + cost;
if(nodeIter->second.costSoFar() > newCostSoFar) if(nodeIter->second.costSoFar() > newCostSoFar)
@@ -1973,6 +1986,7 @@ std::list<int> computePath(
if(mapIter->second == nodeIter->first) if(mapIter->second == nodeIter->first)
{ {
pqmap.erase(mapIter); pqmap.erase(mapIter);
nodeIter->second.setFromId(currentNode->id());
nodeIter->second.setCostSoFar(newCostSoFar); nodeIter->second.setCostSoFar(newCostSoFar);
pqmap.insert(std::make_pair(nodeIter->second.totalCost(), nodeIter->first)); pqmap.insert(std::make_pair(nodeIter->second.totalCost(), nodeIter->first));
break; break;
@@ -2142,6 +2156,7 @@ std::list<std::pair<int, Transform> > computePath(
if(mapIter->second == nodeIter->first) if(mapIter->second == nodeIter->first)
{ {
pqmap.erase(mapIter); pqmap.erase(mapIter);
nodeIter->second.setFromId(currentNode->id());
nodeIter->second.setCostSoFar(newCostSoFar); nodeIter->second.setCostSoFar(newCostSoFar);
pqmap.insert(std::make_pair(nodeIter->second.totalCost(), nodeIter->first)); pqmap.insert(std::make_pair(nodeIter->second.totalCost(), nodeIter->first));
break; break;
@@ -2420,8 +2435,18 @@ std::list<std::map<int, Transform> > getPaths(
std::map<int, Transform> path; std::map<int, Transform> path;
for(std::map<int, Transform>::iterator iter=poses.begin(); iter!=poses.end();) for(std::map<int, Transform>::iterator iter=poses.begin(); iter!=poses.end();)
{ {
std::multimap<int, Link>::const_iterator jter = findLink(links, path.rbegin()->first, iter->first); bool addPose = false;
if(path.size() == 0 || (jter != links.end() && (jter->second.type() == Link::kNeighbor || jter->second.type() == Link::kNeighborMerged))) if(path.empty())
{
addPose = true;
}
else
{
std::multimap<int, Link>::const_iterator jter = findLink(links, path.rbegin()->first, iter->first);
addPose = jter != links.end() &&
(jter->second.type() == Link::kNeighbor || jter->second.type() == Link::kNeighborMerged);
}
if(addPose)
{ {
path.insert(*iter); path.insert(*iter);
poses.erase(iter++); poses.erase(iter++);
+5
View File
@@ -111,6 +111,11 @@ add_executable(test_imu test_imu.cpp)
target_link_libraries(test_imu gtest_main rtabmap_core) target_link_libraries(test_imu gtest_main rtabmap_core)
add_test(NAME test_imu COMMAND test_imu) add_test(NAME test_imu COMMAND test_imu)
#Graph.h
add_executable(test_graph test_graph.cpp)
target_link_libraries(test_graph gtest_main rtabmap_core)
add_test(NAME test_graph COMMAND test_graph)
#GeodeticCoords.h #GeodeticCoords.h
add_executable(test_geodeticcoords test_geodeticcoords.cpp) add_executable(test_geodeticcoords test_geodeticcoords.cpp)
target_link_libraries(test_geodeticcoords gtest_main rtabmap_core) target_link_libraries(test_geodeticcoords gtest_main rtabmap_core)
+860
View File
@@ -0,0 +1,860 @@
#include <gtest/gtest.h>
#include <rtabmap/core/Graph.h>
#include <rtabmap/core/Link.h>
#include <cmath>
#include <string>
#include <vector>
using namespace rtabmap;
namespace {
static cv::Mat infMatrixDiagonal(double x, double y, double z, double roll, double pitch, double yaw)
{
cv::Mat inf = cv::Mat::zeros(6, 6, CV_64FC1);
inf.at<double>(0, 0) = x;
inf.at<double>(1, 1) = y;
inf.at<double>(2, 2) = z;
inf.at<double>(3, 3) = roll;
inf.at<double>(4, 4) = pitch;
inf.at<double>(5, 5) = yaw;
return inf;
}
static Link neighborLink(int from, int to, float dx = 1.0f)
{
return Link(from, to, Link::kNeighbor, Transform(dx, 0, 0, 0, 0, 0));
}
static void insertLink(std::multimap<int, Link> & links, const Link & link)
{
links.insert(std::make_pair(link.from(), link));
}
static std::map<int, Transform> linePoses(unsigned int count, float step = 1.0f)
{
std::map<int, Transform> poses;
for(unsigned int i = 0; i < count; ++i)
{
poses.insert(std::make_pair(static_cast<int>(i + 1), Transform(step * i, 0, 0, 0, 0, 0)));
}
return poses;
}
// KITTI metrics use 100–800 m segments; need ~800 m of trajectory at 1 m/frame.
static std::vector<Transform> lineTrajectory(unsigned int count, float step = 1.0f)
{
std::vector<Transform> traj;
traj.reserve(count);
for(unsigned int i = 0; i < count; ++i)
{
traj.push_back(Transform(step * i, 0, 0, 0, 0, 0));
}
return traj;
}
static std::map<int, Transform> transformPoses(
const std::map<int, Transform> & poses,
const Transform & t)
{
std::map<int, Transform> out;
for(std::map<int, Transform>::const_iterator iter = poses.begin(); iter != poses.end(); ++iter)
{
out.insert(std::make_pair(iter->first, t * iter->second));
}
return out;
}
static float calcTranslationalRmse(
const std::map<int, Transform> & groundTruth,
const std::map<int, Transform> & poses,
bool align2D = true)
{
float tRmse = 0.0f;
float tMean = 0.0f;
float tMedian = 0.0f;
float tStd = 0.0f;
float tMin = 0.0f;
float tMax = 0.0f;
float rRmse = 0.0f;
float rMean = 0.0f;
float rMedian = 0.0f;
float rStd = 0.0f;
float rMin = 0.0f;
float rMax = 0.0f;
graph::calcRMSE(
groundTruth,
poses,
tRmse,
tMean,
tMedian,
tStd,
tMin,
tMax,
rRmse,
rMean,
rMedian,
rStd,
rMin,
rMax,
align2D);
return tRmse;
}
} // namespace
TEST(GraphTest, FindLinkForwardAndReverse)
{
std::multimap<int, Link> links;
insertLink(links, neighborLink(1, 2));
EXPECT_NE(graph::findLink(links, 1, 2), links.end());
EXPECT_EQ(graph::findLink(links, 1, 2)->second.to(), 2);
EXPECT_EQ(graph::findLink(links, 2, 1, false), links.end());
EXPECT_NE(graph::findLink(links, 2, 1, true), links.end());
EXPECT_EQ(graph::findLink(links, 1, 2, true, Link::kGlobalClosure), links.end());
EXPECT_NE(graph::findLink(links, 1, 2, true, Link::kNeighbor), links.end());
}
TEST(GraphTest, FindLinkIntMultimap)
{
std::multimap<int, int> links;
links.insert(std::make_pair(1, 2));
links.insert(std::make_pair(2, 3));
EXPECT_NE(graph::findLink(links, 1, 2), links.end());
EXPECT_NE(graph::findLink(links, 3, 2, true), links.end());
EXPECT_EQ(graph::findLink(links, 1, 3), links.end());
}
TEST(GraphTest, FindLinksIncludesIncomingAsInverse)
{
std::multimap<int, Link> links;
insertLink(links, neighborLink(1, 2));
const std::list<Link> from1 = graph::findLinks(links, 1);
ASSERT_EQ(from1.size(), 1u);
EXPECT_EQ(from1.front().from(), 1);
EXPECT_EQ(from1.front().to(), 2);
const std::list<Link> from2 = graph::findLinks(links, 2);
ASSERT_EQ(from2.size(), 1u);
EXPECT_EQ(from2.front().from(), 2);
EXPECT_EQ(from2.front().to(), 1);
}
TEST(GraphTest, FilterDuplicateLinks)
{
std::multimap<int, Link> links;
insertLink(links, neighborLink(1, 2));
insertLink(links, neighborLink(1, 2));
insertLink(links, neighborLink(2, 1));
const std::multimap<int, Link> filtered = graph::filterDuplicateLinks(links);
EXPECT_EQ(filtered.size(), 1u);
}
TEST(GraphTest, FilterLinksByType)
{
std::multimap<int, Link> links;
insertLink(links, neighborLink(1, 2));
insertLink(links, Link(2, 3, Link::kGlobalClosure, Transform::getIdentity()));
const std::multimap<int, Link> noClosure = graph::filterLinks(links, Link::kGlobalClosure, false);
EXPECT_EQ(noClosure.size(), 1u);
EXPECT_EQ(noClosure.begin()->second.type(), Link::kNeighbor);
const std::multimap<int, Link> onlyClosure = graph::filterLinks(links, Link::kGlobalClosure, true);
ASSERT_EQ(onlyClosure.size(), 1u);
EXPECT_EQ(onlyClosure.begin()->second.type(), Link::kGlobalClosure);
}
TEST(GraphTest, FilterSelfReferenceLinks)
{
std::multimap<int, Link> links;
insertLink(links, neighborLink(1, 2));
insertLink(links, Link(3, 3, Link::kPosePrior, Transform::getIdentity()));
const std::multimap<int, Link> nonSelf = graph::filterLinks(links, Link::kSelfRefLink, false);
EXPECT_EQ(nonSelf.size(), 1u);
EXPECT_NE(nonSelf.begin()->second.from(), nonSelf.begin()->second.to());
const std::multimap<int, Link> selfOnly = graph::filterLinks(links, Link::kSelfRefLink, true);
ASSERT_EQ(selfOnly.size(), 1u);
EXPECT_EQ(selfOnly.begin()->second.from(), selfOnly.begin()->second.to());
}
static std::list<int> computeDijkstraPath(bool updateNewCosts, bool useSameCostForAllLinks)
{
std::multimap<int, Link> links;
insertLink(links, neighborLink(1, 2));
insertLink(links, neighborLink(2, 3));
insertLink(links, neighborLink(1, 3, 5.0f));
return graph::computePath(links, 1, 3, updateNewCosts, useSameCostForAllLinks);
}
static std::string dijkstraPathToString(const std::list<int> & path)
{
std::string s;
for(std::list<int>::const_iterator it = path.begin(); it != path.end(); ++it)
{
if(!s.empty())
{
s += "->";
}
s += std::to_string(*it);
}
return s;
}
static void expectDijkstraPath(
const std::list<int> & path,
const std::initializer_list<int> expected)
{
ASSERT_EQ(path.size(), expected.size());
auto it = path.begin();
for(int id : expected)
{
ASSERT_NE(it, path.end());
EXPECT_EQ(*it, id);
++it;
}
}
TEST(GraphTest, ComputePathDijkstraWeighted)
{
// Same graph as computeDijkstraPath(); edge cost = link length (m).
//
// 1 -------- 5 m -------- 3 cost 5 -> {1, 3} when updateNewCosts=false
// | ^
// +-- 1 m -- 2 -- 1 m ----+ cost 2 -> {1, 2, 3} when updateNewCosts=true
//
expectDijkstraPath(computeDijkstraPath(false, false), {1, 3}); // updateNewCosts=false: 3 stays on direct link
expectDijkstraPath(computeDijkstraPath(true, false), {1, 2, 3});
}
TEST(GraphTest, ComputePathDijkstraUnitCostUpdateNewCostsEquivalent)
{
// Same topology; useSameCostForAllLinks=true (1 hop per edge).
//
// 1 --------------------- 3 1 hop -> {1, 3} (both updateNewCosts values)
// |
// +-- 1 hop -- 2 -- 1 hop -- 3 2 hops (never chosen)
//
// Relaxation only applies on a strictly lower hop count, so updateNewCosts cannot
// change the result.
expectDijkstraPath(computeDijkstraPath(false, true), {1, 3});
expectDijkstraPath(computeDijkstraPath(true, true), {1, 3});
EXPECT_EQ(
dijkstraPathToString(computeDijkstraPath(false, true)),
dijkstraPathToString(computeDijkstraPath(true, true)));
}
TEST(GraphTest, ComputePathAStar)
{
std::map<int, Transform> poses = linePoses(3);
std::multimap<int, int> links;
links.insert(std::make_pair(1, 2));
links.insert(std::make_pair(2, 3));
const std::list<std::pair<int, Transform> > path = graph::computePath(poses, links, 1, 3, false);
ASSERT_EQ(path.size(), 3u);
EXPECT_EQ(path.front().first, 1);
EXPECT_EQ(path.back().first, 3);
}
static std::string aStarPathToString(const std::list<std::pair<int, Transform> > & path)
{
std::string s;
for(std::list<std::pair<int, Transform> >::const_iterator it = path.begin(); it != path.end(); ++it)
{
if(it != path.begin())
{
s += "->";
}
s += std::to_string(it->first);
}
return s;
}
static void expectAStarPath(
const std::list<std::pair<int, Transform> > & path,
const std::initializer_list<int> expectedIds)
{
ASSERT_EQ(path.size(), expectedIds.size()) << "path=" << aStarPathToString(path);
auto it = path.begin();
for(int id : expectedIds)
{
ASSERT_NE(it, path.end());
EXPECT_EQ(it->first, id);
++it;
}
}
static std::list<std::pair<int, Transform> > computeAStarPath(
const std::map<int, Transform> & poses,
const std::multimap<int, int> & links,
bool updateNewCosts)
{
return graph::computePath(poses, links, 1, 3, updateNewCosts);
}
TEST(GraphTest, ComputePathAStarUpdateNewCostsChangesPath)
{
// Detour 1→2→5→6→3 vs shortcut 1→2→4→6→3. No 5→3 edge so h(5,3) < cost(5→6→3).
// Node 5 is expanded before 4 (lower f-score). Node 6 is first reached from 5;
// expanding 4 relaxes the parent of 6 when updateNewCosts=true.
//
// 3 goal (10, 0)
// |
// 6 (2, -5)
// / \
// 5 4 (1,-2) (1.5,-4)
// \ /
// 2 (1, 0)
// |
// 1 start (0, 0)
//
// Links: 1—2, 2—5, 2—4, 5—6, 4—6, 6—3 (no 5—3)
const std::map<int, Transform> poses = {
{1, Transform(0, 0, 0, 0, 0, 0)},
{2, Transform(1, 0, 0, 0, 0, 0)},
{3, Transform(10, 0, 0, 0, 0, 0)},
{4, Transform(1.5f, -4, 0, 0, 0, 0)},
{5, Transform(1, -2, 0, 0, 0, 0)},
{6, Transform(2, -5, 0, 0, 0, 0)}};
std::multimap<int, int> links;
links.insert(std::make_pair(1, 2));
links.insert(std::make_pair(2, 5)); // before 2→4
links.insert(std::make_pair(2, 4));
links.insert(std::make_pair(5, 6));
links.insert(std::make_pair(4, 6));
links.insert(std::make_pair(6, 3));
const std::list<std::pair<int, Transform> > pathNoUpdate =
computeAStarPath(poses, links, false);
const std::list<std::pair<int, Transform> > pathUpdate =
computeAStarPath(poses, links, true);
expectAStarPath(pathNoUpdate, {1, 2, 5, 6, 3});
expectAStarPath(pathUpdate, {1, 2, 4, 6, 3});
EXPECT_NE(aStarPathToString(pathNoUpdate), aStarPathToString(pathUpdate));
}
TEST(GraphTest, FindNearestNode)
{
const std::map<int, Transform> poses = linePoses(3, 2.0f);
const Transform query(2.1f, 0.1f, 0, 0, 0, 0);
float sqDist = -1.0f;
const int id = graph::findNearestNode(poses, query, &sqDist);
EXPECT_EQ(id, 2);
EXPECT_NEAR(sqDist, 0.1f * 0.1f + 0.1f * 0.1f, 1e-4f);
}
TEST(GraphTest, FindNearestNodesKnn)
{
const std::map<int, Transform> poses = linePoses(4);
const std::map<int, float> nearest = graph::findNearestNodes(poses.at(2), poses, 0.0f, 0.0f, 2);
ASSERT_EQ(nearest.size(), 2u);
ASSERT_TRUE(nearest.find(2) != nearest.end());
EXPECT_NEAR(nearest.at(2), 0.0f, 1e-6f);
// nodeId overload excludes the query node from results
const std::map<int, float> excludingSelf = graph::findNearestNodes(2, poses, 0.0f, 0.0f, 2);
ASSERT_EQ(excludingSelf.size(), 2u);
EXPECT_TRUE(excludingSelf.find(2) == excludingSelf.end());
EXPECT_NEAR(excludingSelf.at(1), 1.0f, 1e-6f);
EXPECT_NEAR(excludingSelf.at(3), 1.0f, 1e-6f);
}
TEST(GraphTest, FindNearestNodesRadius)
{
const std::map<int, Transform> poses = linePoses(4);
const std::map<int, float> inRadius = graph::findNearestNodes(poses.at(2), poses, 1.5f);
EXPECT_EQ(inRadius.size(), 3u);
EXPECT_TRUE(inRadius.find(2) != inRadius.end());
EXPECT_NEAR(inRadius.at(2), 0.0f, 1e-6f);
EXPECT_TRUE(inRadius.find(4) == inRadius.end());
}
TEST(GraphTest, ComputePathLength)
{
const std::vector<std::pair<int, Transform> > vecPath = {
{1, Transform(0, 0, 0, 0, 0, 0)},
{2, Transform(3, 4, 0, 0, 0, 0)},
{3, Transform(3, 9, 0, 0, 0, 0)}};
EXPECT_NEAR(graph::computePathLength(vecPath), 10.0f, 1e-4f);
const std::map<int, Transform> mapPath = linePoses(3);
EXPECT_NEAR(graph::computePathLength(mapPath), 2.0f, 1e-4f);
}
TEST(GraphTest, ComputeMinMax)
{
const std::map<int, Transform> poses = {
{1, Transform(-1, 2, 3, 0, 0, 0)},
{2, Transform(4, -5, 0, 0, 0, 0)}};
cv::Vec3f min, max;
graph::computeMinMax(poses, min, max);
EXPECT_FLOAT_EQ(min[0], -1.0f);
EXPECT_FLOAT_EQ(min[1], -5.0f);
EXPECT_FLOAT_EQ(min[2], 0.0f);
EXPECT_FLOAT_EQ(max[0], 4.0f);
EXPECT_FLOAT_EQ(max[1], 2.0f);
EXPECT_FLOAT_EQ(max[2], 3.0f);
}
TEST(GraphTest, CalcRelativeErrorsIdenticalTrajectories)
{
const std::vector<Transform> traj = {
Transform(0, 0, 0, 0, 0, 0),
Transform(1, 0, 0, 0, 0, 0),
Transform(2, 0, 0, 0, 0, 0)};
float tErr = -1.0f;
float rErr = -1.0f;
graph::calcRelativeErrors(traj, traj, tErr, rErr);
EXPECT_NEAR(tErr, 0.0f, 1e-5f);
EXPECT_NEAR(rErr, 0.0f, 1e-5f);
}
TEST(GraphTest, CalcRelativeErrorsWithNoise)
{
const std::vector<Transform> gt = {
Transform(0, 0, 0, 0, 0, 0),
Transform(1, 0, 0, 0, 0, 0),
Transform(2, 0, 0, 0, 0, 0),
Transform(3, 0, 0, 0, 0, 0),
Transform(4, 0, 0, 0, 0, 0)};
// Small position and orientation noise on the estimate.
const std::vector<Transform> est = {
Transform(0.01f, -0.02f, 0.005f, 0, 0, 0.01f),
Transform(1.03f, 0.01f, -0.01f, 0, 0, -0.02f),
Transform(2.02f, -0.03f, 0.02f, 0, 0, 0.015f),
Transform(2.98f, 0.02f, 0.01f, 0, 0, -0.01f),
Transform(4.01f, -0.01f, -0.02f, 0, 0, 0.005f)};
float tErr = 0.0f;
float rErr = 0.0f;
graph::calcRelativeErrors(gt, est, tErr, rErr);
EXPECT_GT(tErr, 0.0f);
EXPECT_LT(tErr, 0.1f);
EXPECT_GT(rErr, 0.0f);
EXPECT_LT(rErr, 2.0f);
}
TEST(GraphTest, CalcKittiSequenceErrorsIdenticalTrajectories)
{
const std::vector<Transform> traj = lineTrajectory(901, 1.0f);
float tErr = -1.0f;
float rErr = -1.0f;
graph::calcKittiSequenceErrors(traj, traj, tErr, rErr);
EXPECT_NEAR(tErr, 0.0f, 1e-5f);
EXPECT_NEAR(rErr, 0.0f, 1e-5f);
}
TEST(GraphTest, CalcKittiSequenceErrorsWithNoise)
{
const std::vector<Transform> gt = lineTrajectory(901, 1.0f);
ASSERT_EQ(gt.size(), 901u);
ASSERT_NEAR(gt[0].x(), 0.0f, 1e-5f);
std::vector<Transform> est;
est.reserve(gt.size());
for(unsigned int i = 0; i < gt.size(); ++i)
{
const int ii = static_cast<int>(i);
const float dx = 0.02f * static_cast<float>((ii % 3) - 1);
const float dy = 0.01f * static_cast<float>((ii % 5) - 2);
est.push_back(Transform(
gt.at(i).x() + dx,
gt.at(i).y() + dy,
gt.at(i).z(),
0.0f,
0.0f,
0.0f));
}
ASSERT_EQ(est.size(), 901u);
ASSERT_NEAR(est.at(0).x(), -0.02f, 1e-3f);
ASSERT_NEAR(est.at(800).x(), 800.02f, 1e-1f);
const Transform poseDeltaEst = est.at(0).inverse() * est.at(800);
ASSERT_NEAR(poseDeltaEst.getNorm(), 800.0f, 5.0f);
float tErr = 0.0f;
float rErr = 0.0f;
graph::calcKittiSequenceErrors(gt, est, tErr, rErr);
EXPECT_TRUE(std::isfinite(tErr)) << "tErr=" << tErr;
EXPECT_TRUE(std::isfinite(rErr)) << "rErr=" << rErr;
EXPECT_GT(tErr, 0.0f);
EXPECT_LT(tErr, 2.0f); // translation error (%)
EXPECT_NEAR(rErr, 0.0f, 0.5f); // no orientation noise on the trajectory
}
TEST(GraphTest, CalcRMSEIdenticalMaps)
{
const std::map<int, Transform> gt = linePoses(3);
float tRmse = -1.0f;
float tMean = -1.0f;
float tMedian = -1.0f;
float tStd = -1.0f;
float tMin = -1.0f;
float tMax = -1.0f;
float rRmse = -1.0f;
float rMean = -1.0f;
float rMedian = -1.0f;
float rStd = -1.0f;
float rMin = -1.0f;
float rMax = -1.0f;
const Transform align = graph::calcRMSE(
gt, gt,
tRmse, tMean, tMedian, tStd, tMin, tMax,
rRmse, rMean, rMedian, rStd, rMin, rMax,
true);
EXPECT_TRUE(align.isIdentity());
EXPECT_NEAR(tRmse, 0.0f, 1e-4f);
EXPECT_NEAR(rRmse, 0.0f, 1e-4f);
}
TEST(GraphTest, CalcRMSEWithNoise)
{
const std::map<int, Transform> gt = linePoses(8, 1.0f);
std::map<int, Transform> est = gt;
est[2] = Transform(1.05f, 0.02f, 0, 0, 0, 0.01f);
est[4] = Transform(3.02f, -0.03f, 0.01f, 0, 0, -0.02f);
est[6] = Transform(5.01f, 0.01f, -0.02f, 0, 0, 0.015f);
est[8] = Transform(7.0f, -0.01f, 0.02f, 0, 0, -0.005f);
const float tRmse = calcTranslationalRmse(gt, est, true);
EXPECT_GT(tRmse, 0.0f);
EXPECT_LT(tRmse, 0.1f);
}
TEST(GraphTest, CalcRMSEAlignsRotatedTrajectory)
{
// Eight poses so calcRMSE uses SVD alignment (more than five matched poses).
const std::map<int, Transform> gt = linePoses(8, 1.0f);
std::map<int, Transform> noisy = gt;
noisy[2] = Transform(1.05f, 0.02f, 0, 0, 0, 0.01f);
noisy[5] = Transform(4.02f, -0.02f, 0, 0, 0, -0.01f);
noisy[7] = Transform(6.01f, 0.01f, 0, 0, 0, 0.02f);
const Transform yaw90(0, 0, 0, 0, 0, static_cast<float>(CV_PI / 2.0));
const std::map<int, Transform> rotated = transformPoses(gt, yaw90);
// Without alignment, positions would differ a lot (x vs y).
const Transform p = gt.at(4);
const Transform r = rotated.at(4);
EXPECT_GT(p.getDistance(r), 1.0f);
const float rmseNoisy = calcTranslationalRmse(gt, noisy, true);
float tRmse = 0.0f;
float tMean = 0.0f;
float tMedian = 0.0f;
float tStd = 0.0f;
float tMin = 0.0f;
float tMax = 0.0f;
float rRmse = 0.0f;
float rMean = 0.0f;
float rMedian = 0.0f;
float rStd = 0.0f;
float rMin = 0.0f;
float rMax = 0.0f;
const Transform align = graph::calcRMSE(
gt,
rotated,
tRmse,
tMean,
tMedian,
tStd,
tMin,
tMax,
rRmse,
rMean,
rMedian,
rStd,
rMin,
rMax,
true);
EXPECT_LT(rmseNoisy, 0.1f);
EXPECT_LT(tRmse, 0.1f);
EXPECT_NEAR(tRmse, rmseNoisy, 0.08f);
// est = yaw90 * gt => align * est ≈ gt => align ≈ yaw90⁻¹
const Transform expectedAlign = yaw90.inverse();
EXPECT_NEAR(align.getAngle(expectedAlign), 0.0f, 0.05f);
EXPECT_NEAR(align.x(), expectedAlign.x(), 1e-2f);
EXPECT_NEAR(align.y(), expectedAlign.y(), 1e-2f);
EXPECT_NEAR(align.theta(), expectedAlign.theta(), 1e-2f);
EXPECT_LT((align * rotated.at(4)).getDistance(gt.at(4)), 1e-2f);
}
static graph::MaxGraphErrors maxGraphErrors(
const std::map<int, Transform> & poses,
const std::multimap<int, Link> & links,
bool for3DoF = false)
{
return graph::computeMaxGraphErrors(poses, links, for3DoF);
}
TEST(GraphTest, ComputeMaxGraphErrorsZeroResidual)
{
std::map<int, Transform> poses;
poses.insert(std::make_pair(1, Transform(0, 0, 0, 0, 0, 0)));
poses.insert(std::make_pair(2, Transform(1, 0, 0, 0, 0, 0)));
std::multimap<int, Link> links;
insertLink(links, neighborLink(1, 2, 1.0f));
const graph::MaxGraphErrors errors = maxGraphErrors(poses, links);
EXPECT_NEAR(errors.linear, 0.0f, 1e-4f);
EXPECT_NEAR(errors.angular, 0.0f, 1e-4f);
EXPECT_NEAR(errors.linearRatio, 0.0f, 1e-4f);
EXPECT_NEAR(errors.angularRatio, 0.0f, 1e-4f);
EXPECT_TRUE(errors.linearLink.isValid());
EXPECT_EQ(errors.linearLink.from(), 1);
EXPECT_EQ(errors.linearLink.to(), 2);
}
TEST(GraphTest, ComputeMaxGraphErrorsKnownLinearResidual)
{
std::map<int, Transform> poses;
poses.insert(std::make_pair(1, Transform(0, 0, 0, 0, 0, 0)));
poses.insert(std::make_pair(2, Transform(2, 0, 0, 0, 0, 0))); // 2 m apart
std::multimap<int, Link> links;
insertLink(links, neighborLink(1, 2, 1.0f)); // link says 1 m
const graph::MaxGraphErrors errors = maxGraphErrors(poses, links);
EXPECT_NEAR(errors.linear, 1.0f, 1e-4f);
EXPECT_NEAR(errors.linearRatio, 1.0f, 1e-4f); // default inf: variance 1, stddev 1
EXPECT_TRUE(errors.linearLink.isValid());
EXPECT_EQ(errors.linearLink.from(), 1);
EXPECT_EQ(errors.linearLink.to(), 2);
}
TEST(GraphTest, ComputeMaxGraphErrorsPicksHighestLinearRatio)
{
std::map<int, Transform> poses;
poses.insert(std::make_pair(1, Transform(0, 0, 0, 0, 0, 0)));
poses.insert(std::make_pair(2, Transform(0.5f, 0, 0, 0, 0, 0))); // 0.5 m error vs link
poses.insert(std::make_pair(3, Transform(2.5f, 0, 0, 0, 0, 0))); // 1 m error vs link
std::multimap<int, Link> links;
insertLink(links, Link(
1,
2,
Link::kNeighbor,
Transform(0, 0, 0, 0, 0, 0),
infMatrixDiagonal(100, 100, 100, 1, 1, 1))); // ratio ≈ 0.5 / 0.1 = 5
insertLink(links, neighborLink(2, 3, 1.0f)); // ratio ≈ 1 / 1 = 1
const graph::MaxGraphErrors errors = maxGraphErrors(poses, links);
EXPECT_NEAR(errors.linear, 0.5f, 1e-4f);
EXPECT_NEAR(errors.linearRatio, 5.0f, 1e-3f);
EXPECT_EQ(errors.linearLink.from(), 1);
EXPECT_EQ(errors.linearLink.to(), 2);
}
TEST(GraphTest, ComputeMaxGraphErrorsAngularResidual)
{
std::map<int, Transform> poses;
poses.insert(std::make_pair(1, Transform(0, 0, 0, 0, 0, 0)));
poses.insert(std::make_pair(2, Transform(1, 0, 0, 0, 0, 0.5f)));
std::multimap<int, Link> links;
insertLink(links, neighborLink(1, 2, 1.0f));
const graph::MaxGraphErrors errors = maxGraphErrors(poses, links);
EXPECT_NEAR(errors.linear, 0.0f, 1e-4f);
EXPECT_NEAR(errors.angular, 0.5f, 1e-4f);
EXPECT_NEAR(errors.angularRatio, 0.5f, 1e-4f);
EXPECT_EQ(errors.angularLink.from(), 1);
EXPECT_EQ(errors.angularLink.to(), 2);
}
TEST(GraphTest, ComputeMaxGraphErrorsDifferentWorstLinearAndAngularLinks)
{
std::map<int, Transform> poses;
poses.insert(std::make_pair(1, Transform(0, 0, 0, 0, 0, 0)));
poses.insert(std::make_pair(2, Transform(0.6f, 0, 0, 0, 0, 0)));
poses.insert(std::make_pair(3, Transform(1.6f, 0, 0, 0, 0, 0.5f)));
std::multimap<int, Link> links;
insertLink(links, Link(
1,
2,
Link::kNeighbor,
Transform(1, 0, 0, 0, 0, 0),
infMatrixDiagonal(100, 100, 100, 1, 1, 1)));
insertLink(links, Link(
2,
3,
Link::kNeighbor,
Transform(1, 0, 0, 0, 0, 0),
infMatrixDiagonal(1, 1, 1, 100, 100, 100)));
const graph::MaxGraphErrors errors = maxGraphErrors(poses, links);
EXPECT_EQ(errors.linearLink.from(), 1);
EXPECT_EQ(errors.linearLink.to(), 2);
EXPECT_EQ(errors.angularLink.from(), 2);
EXPECT_EQ(errors.angularLink.to(), 3);
EXPECT_NE(errors.linearLink.from(), errors.angularLink.from());
}
TEST(GraphTest, ComputeMaxGraphErrorsFor3DoF)
{
std::map<int, Transform> poses;
poses.insert(std::make_pair(1, Transform(0, 0, 0, 0, 0, 0)));
poses.insert(std::make_pair(2, Transform(1, 0, 0.3f, 0, 0, 0)));
std::multimap<int, Link> links;
insertLink(links, neighborLink(1, 2, 1.0f));
const graph::MaxGraphErrors errors6 = maxGraphErrors(poses, links, false);
const graph::MaxGraphErrors errors3 = maxGraphErrors(poses, links, true);
EXPECT_NEAR(errors6.linear, 0.3f, 1e-4f);
EXPECT_NEAR(errors3.linear, 0.0f, 1e-4f);
}
TEST(GraphTest, ComputeMaxGraphErrorsSkipsSelfLinks)
{
std::map<int, Transform> poses;
poses.insert(std::make_pair(1, Transform(0, 0, 0, 0, 0, 0)));
std::multimap<int, Link> links;
insertLink(links, Link(1, 1, Link::kPosePrior, Transform(1, 2, 3, 0, 0, 0)));
const graph::MaxGraphErrors errors = maxGraphErrors(poses, links);
EXPECT_FLOAT_EQ(errors.linear, -1.0f);
EXPECT_FLOAT_EQ(errors.angular, -1.0f);
EXPECT_FALSE(errors.linearLink.isValid());
}
TEST(GraphTest, ComputeMaxGraphErrorsAbortsOnMissingPose)
{
std::map<int, Transform> poses;
poses.insert(std::make_pair(1, Transform(0, 0, 0, 0, 0, 0)));
std::multimap<int, Link> links;
insertLink(links, neighborLink(1, 2, 1.0f));
const graph::MaxGraphErrors errors = maxGraphErrors(poses, links);
EXPECT_FLOAT_EQ(errors.linear, -1.0f);
EXPECT_FLOAT_EQ(errors.angular, -1.0f);
EXPECT_FALSE(errors.linearLink.isValid());
}
TEST(GraphTest, ComputeMaxGraphErrorsLandmarkSkipsUnconstrainedYaw)
{
std::map<int, Transform> poses;
poses.insert(std::make_pair(1, Transform(0, 0, 0, 0, 0, 0)));
poses.insert(std::make_pair(2, Transform(1, 0, 0, 0, 0, 0.1f))); // 0.1 rad yaw vs neighbor link
poses.insert(std::make_pair(-10, Transform(2, 1, 0, 0, 0, 0.5f))); // 1 m y and 0.5 rad yaw vs landmark link
std::multimap<int, Link> links;
insertLink(links, neighborLink(1, 2, 1.0f));
// Landmark yaw is not constrained; angular error is skipped even though poses disagree by 0.5 rad.
insertLink(links, Link(
1,
-10,
Link::kLandmark,
Transform(2, 0, 0, 0, 0, 0),
infMatrixDiagonal(1, 1, 1, 1, 1, 0.00001)));
const graph::MaxGraphErrors errors = maxGraphErrors(poses, links);
EXPECT_NEAR(errors.linear, 1.0f, 1e-4f);
EXPECT_EQ(errors.linearLink.from(), 1);
EXPECT_EQ(errors.linearLink.to(), -10);
EXPECT_NEAR(errors.angular, 0.1f, 1e-4f);
EXPECT_EQ(errors.angularLink.from(), 1);
EXPECT_EQ(errors.angularLink.to(), 2);
EXPECT_NE(errors.angularLink.type(), Link::kLandmark);
}
TEST(GraphTest, ComputeMaxGraphErrorsLandmarkTwoPoseObservations)
{
// Same landmark -10 observed from poses 1 and 2 (two links sharing the landmark id).
//
// -10 (1, 1)
// / \
// 1 2
// (0,0) (2,0)
//
std::map<int, Transform> poses;
poses.insert(std::make_pair(1, Transform(0, 0, 0, 0, 0, 0)));
poses.insert(std::make_pair(2, Transform(2, 0, 0, 0, 0, 0)));
poses.insert(std::make_pair(-10, Transform(1, 1, 0, 0, 0, 0)));
std::multimap<int, Link> links;
insertLink(links, Link(1, -10, Link::kLandmark, Transform(1, 1, 0, 0, 0, 0)));
insertLink(links, Link(2, -10, Link::kLandmark, Transform(-1, 1, 0, 0, 0, 0)));
const graph::MaxGraphErrors consistent = maxGraphErrors(poses, links);
EXPECT_NEAR(consistent.linear, 0.0f, 1e-4f);
EXPECT_NEAR(consistent.angular, 0.0f, 1e-4f);
// Poses still agree with observation from 1; link 2→-10 is wrong by 1 m in y.
std::multimap<int, Link> linksOneInconsistent;
insertLink(linksOneInconsistent, Link(1, -10, Link::kLandmark, Transform(1, 1, 0, 0, 0, 0)));
insertLink(linksOneInconsistent, Link(2, -10, Link::kLandmark, Transform(-1, 0, 0, 0, 0, 0)));
const graph::MaxGraphErrors oneInconsistent = maxGraphErrors(poses, linksOneInconsistent);
EXPECT_NEAR(oneInconsistent.linear, 1.0f, 1e-4f);
EXPECT_EQ(oneInconsistent.linearLink.from(), 2);
EXPECT_EQ(oneInconsistent.linearLink.to(), -10);
// Same mismatch with landmark as link.from (from < 0): measurement is inverted.
std::multimap<int, Link> linksOneInconsistentLandmarkFrom;
insertLink(linksOneInconsistentLandmarkFrom, Link(-10, 1, Link::kLandmark, Transform(-1, -1, 0, 0, 0, 0)));
insertLink(linksOneInconsistentLandmarkFrom, Link(-10, 2, Link::kLandmark, Transform(1, 0, 0, 0, 0, 0)));
const graph::MaxGraphErrors oneInconsistentLandmarkFrom =
maxGraphErrors(poses, linksOneInconsistentLandmarkFrom);
EXPECT_NEAR(oneInconsistentLandmarkFrom.linear, oneInconsistent.linear, 1e-4f);
EXPECT_NEAR(oneInconsistentLandmarkFrom.linearRatio, oneInconsistent.linearRatio, 1e-4f);
EXPECT_EQ(oneInconsistentLandmarkFrom.linearLink.from(), -10);
EXPECT_EQ(oneInconsistentLandmarkFrom.linearLink.to(), 2);
// Move the optimized landmark; both observations are now inconsistent by 1 m in y.
poses[-10] = Transform(1, 0, 0, 0, 0, 0);
const graph::MaxGraphErrors inconsistent = maxGraphErrors(poses, links);
EXPECT_NEAR(inconsistent.linear, 1.0f, 1e-4f);
EXPECT_TRUE(inconsistent.linearLink.isValid());
EXPECT_EQ(inconsistent.linearLink.to(), -10);
EXPECT_TRUE(inconsistent.linearLink.from() == 1 || inconsistent.linearLink.from() == 2);
}
TEST(GraphTest, GetMaxOdomInf)
{
std::multimap<int, Link> links;
insertLink(links, Link(1, 2, Link::kNeighbor, Transform::getIdentity(), infMatrixDiagonal(1, 2, 3, 4, 5, 6)));
insertLink(links, Link(2, 3, Link::kNeighbor, Transform::getIdentity(), infMatrixDiagonal(6, 5, 4, 3, 2, 1)));
insertLink(links, Link(3, 4, Link::kGlobalClosure, Transform::getIdentity(), infMatrixDiagonal(99, 99, 99, 99, 99, 99)));
const std::vector<double> maxInf = graph::getMaxOdomInf(links);
ASSERT_EQ(maxInf.size(), 6u);
EXPECT_DOUBLE_EQ(maxInf[0], 6.0);
EXPECT_DOUBLE_EQ(maxInf[5], 6.0);
}
TEST(GraphTest, GetPathsNeighborChain)
{
std::map<int, Transform> poses = linePoses(3);
std::multimap<int, Link> links;
insertLink(links, neighborLink(1, 2));
insertLink(links, neighborLink(2, 3));
const std::list<std::map<int, Transform> > paths = graph::getPaths(poses, links);
ASSERT_EQ(paths.size(), 1u);
EXPECT_EQ(paths.front().size(), 3u);
EXPECT_EQ(poses.size(), 3u); // poses passed by value, caller's map is unchanged
}