Skip to content

[feat][ROS2] Create new keypoint extractor using a vertex map

Jeanne Faure requested to merge feat/AddVertexMap into feat/ROS2


This MR will implement an idea from !104 (closed) : using a vertex map to extract keypoints.

So far, we were only extracting keypoints looking at their neighborhood on their scanline.

For dense rotative lidar, the scanlines are close enough to each other to build a meaningful image and create 2D neighborhoods.

In this MR, we reproduce SSKE behaviour and improve it along the way. We also fix a few things related to keypoint extractors.

The next goal is to be able to use new criteria related to upper and lower lines. This will belong to another MR.



KeypointExtractor aims to define common methods and parameters to keypoint extractors. This abstract class is called in Slam, because the choice of the extractor is now a parameter handled in lidar_slam wrapper : it allows to use the output of an extractor without knowing the process used inside.

plane_angle_threshold & edge_angle_threshold are new common parameters for extractors, used in different ways (for PlaneSinAngleThreshold & EdgeSinAngleThreshold in SSKE, for PlaneCosAngleThreshold & EdgeCosAngleThreshold in DSSKE). See below for details.



In ComputeCurvature(), we changed the way we extract neighborhood for computation time concerns, and to add the central point to its own neighborhoods. Indeed, it was useful for the new vision of FitLineAndCheckConsistency().


We decided to change the theory of FitLineAndCheckConsistency() to something less precise but way more efficient. Instead of doing a RANSAC, we now do two things. First, we check the relative distance between each point. The ratio between max and min distance between two consecutive points have to be inferior to a certain ratio called SquaredRatio. Secondly, we fit a line between the first (central) point and the last point of the neighborhood, define its position as the centroid of the whole neighborhood, and then check if any point is too far from the fitted line (if it is outside the widthThreshold we allow).

We removed MaxLineWidth parameter in LineFitting. Indeed, it made sense when the length of the neighborhood was depending on a MinNeighNb. Since we use MinNeighRadius to extend the neighborhood until it has a convenient line length, a MaxLineWidth makes less sense than a LengthWidthRatio.

Small fix

  • We added the possibility to have random laser_id in GetDebugArray(), as it has already been added to the rest of SpinningSensorKeypointExtractor.
  • We changed the way we cleared the Keypoints map, because it is better to clean it all for LidarView live usage when one can choose to change enabled keypoints between 2 frames.


A new keypoint extractor class is created, derived from KeypointExtractor.

Vertex Map

The vertex map is the projection of the 3D pointcloud onto a 2D image. The type of data stored is a smart pointer to a new structure PtFeat in an array of structure like container.

PtFeat stores the index of the point in the ScanCloud, and all of its features (depth, spacegap...).

Extraction of features

ComputeCurvature() will extract a 2D neighborhood of a point (using a Kernel, for now diamond or square, see below for illustration).

It will then extract features as SSKE does, in the scanline of the current point.

Some differences have to be pointed out :

  • For space gap : we now count the number of direct neighbors in the vertex map which have PtFeat set as nullptr : they are points fired by a laser but never returned to the sensor. This is faster than the technic used in SSKE

  • For depth gap : we look at the direct neighbor of the central point in the vertex map, and the direct neighbor of this direct neighbor (to avoid extracting points on a bended wall as keypoints). Supposing that sometimes for an edge a point can be missed, we decided to add the possibility to have the second direct neighbor and the third for depth gap calculus is the direct neighbor has not been returned.

  • For angle : to be more precise in angle selection, we decided to use the cosine and use thresholds in a different way (in SSKE, we use the absolute value of sine).


Keypoints extraction

Create2DGrid / Create3DGrid

Create the grids to downsample the keypoints. Useful in the case of a MaxPoints (= max number of keypoints to extract for each type) lower than the number of keypoints candidates. We will use the grids to extract the keypoints uniformly in the 3D space.

  • 2D Grid

    Downsample using 2D patches in the vertex map

  • 3D Grid

    Downsample using a 3D voxel grid, analogous to the Rolling Grid used in SLAM. It is less efficient than 2D mode, but will certainly extract keypoints more uniformly as it uses another dimension.

Note that, for efficiency purposes, the only points added to the grids have to respect the thresholds specific to a keypoint type, hence the isPtFeatValid function as argument.


Use the selected grid to extract keypoints progressively

Config files

  • Extraction mode : One can choose between dense or sparse extractor setting the boolean dense_extractor to respectively true or false

  • Plane and Edge angle threshold : One can set characteristic angles for planes and edges. They will then be used to define absolute sine thresholds in SSKE and cosine thresholds in DSSKE, following the theory illustrated above.

  • Downsampling section : We moved max_points and voxel_grid_resolution (useful for SSKE & DSSKE) in the downsampling section to make their purpose clear.

  • Sampling mode :

    One can choose between 2 types of division in grid for keypoints extraction.

    • Patch : division of the Vertex Map, which is a 2D projection of the pointcloud. This mode is efficient but can have trouble extracting keypoints far away
    • Voxel : division of the Scan Cloud, which is the 3D pointcloud. This mode is costlier but gives better keypoint outputs (better distributed).
  • Patch Size : useful for Patch sampling mode, size of a square patch, the patch will contain at most (patch_size * patch_size) points.


    Points & features

    One can output points and their features as pgm images using OutputFeatures().

    The absolute black corresponds to points which never returned to the sensor. The other shades of grey until the absolute white represent the intensity of the feature in comparison with the feature maximum on the map.


    One can output keypoints as 2 different formats using OutputKeypoints().

    • As pgm image to see the keypoint repartition in 2 dimensions, useful if one's using Patch mode.
    • As csv file, to be used to observe the resulting 3D pointcloud of keypoints in an adapted software.



1000 Edges extraction with SSKE, voxel size set to 1f.


1000 Edges extraction with 3D sampling, voxel size set to 1f.


1000 Edges extraction with 2D sampling, patch size set to 32.


100 000 000 Edges extraction with SSKE.

100 000 000 Edges extraction with DSSKE.


100 000 000 Planes extraction with SSKE.


100 000 000 Planes extraction with DSSKE.



I have used data of an Ouster Lidar with 128 lasers in an enclosed warehouse with what seems to be an office space.

I also tested a bag from a Velodyne 16 in outdoor mode, and used the "save maps" option to save keypoints map that you can find here.


colcon build --base-paths slam/ros2_wrapping --cmake-args -DCMAKE_BUILD_TYPE=Release


  • Change the parameters :
    • To select the keypoint extractor :

      slam: ke: extractor_mode: [0, 1] to set at 0 for sparse (SSKE), 1 for dense (DSSKE).

    • To set the thresholds for angle feature in both extractors:

      slam: ke: edge_angle_threshold: [°], maximum value for an angle between two lines to form an edge.

      slam: ke: plane_angle_threshold: [°], minimum value for an angle between two lines to form a plane (also used with its opposite cosine value in DSSKE to filter too sharp edges).

    • Parameters specific to dense extractor (DenseSpinningSensorKeypointExtractor) :

      slam: ke: downsampling: sampling_mode: [0, 1] to set at 0 for patch (2D grid), 1 for voxel (3D grid).

      slam: ke: downsampling: patch_size: the dimension of a patch in term of number of points in one dimension (if set to 32, the patch will contain at most 32x32 points).

      note : for voxel downsampling, use voxel_grid_resolution used in SSKE as well.


ros2 launch lidar_slam outdoor:=false replay:=true use_sim_time:=true

ros2 bag play --clock path/to/this_bag.bag --clock

You can add the -p --start-offset 10.5 to the bag play to control the arriving frame.


This table gives the duration of the Keypoint extraction step in different configurations, in release mode. The patch size was set to 32, and the voxel size to 1.f. We were extracting planes, edges and intensity edges.

Using 1 thread :

Number of keypoints DSSKE - 2D for all DSSKE - 3D for all SSKE - new SSKE - old
1000 75 ms 78 ms 49 ms 98 ms
huge (+100 000 000) 79 ms 48 ms 96 ms

Using 5 threads :

Number of keypoints DSSKE - 2D for all DSSKE - 3D for all SSKE - new SSKE - old
1000 44 ms 47 ms 34 ms 51 ms
huge (+100 000 000) 46 ms 30 ms 50 ms


  • Camel case everywhere except for ROS variables/parameters
  • Lower case for local variables and lambda functions
  • Upper case for pour members, methods and tool functions (in Utils)
  • Precise namespace when calling a function (or this->X or classe.X)
  • Align code (for multiline if and while, "&&" or "||" go in upper line to ensure alignement)
  • Space between if, while, for and parenthesis
  • Space between operators and variables: e.g. a + b
  • Space after ","
  • Mind your commit titles/desc (plurals, he/she + "s", correct tags, title should begin by a verb...)
  • Function names should start with a verb, variable names should start with a name
  • Macros should be between {}
  • Do not use negative boolean (i.e. noJoe)
  • Check minimal size of the types (double -> float -> int -> uint)
  • Check const and ref in functions arguments
  • References should be written "type& name", not "type &name"
  • Update documentation if needed
  • Add MR labels [ROS]/[ROS2]/[PV]
  • If ros/ros2, update task table here
  • Add a comment over each non trivial function in header files
  • Add a header to each new file

What remains to solve

  • Create a function to create the vertex map from the pointcloud

  • Create a struct like : ptFeat = {index, feat1, feat2, feat3} corresponding to each point and sort them in the vertex map (of type vec<vec<ptFeat*>>)

  • Create a function to test the vertex map (output as BMP format)

  • Compute criterions and create output functions related

  • Extract a limited number of kpts by cutting the image into patches / voxels

  • Create a downsample function for the vertex map

  • For future MR :

    • Seek to robustify them using kernels
    • Create new criteria in the map
    • Observe the criterions, keep the most relevant
    • Apply morphological filters if relevant
    • Kernel mode : One can choose between 2 types of neighborhood for keypoint extraction:
    • Cross :


    • Square :


Edited by Jeanne Faure

Merge request reports