There are so many laser range finders available on the market it can be difficult to know what to choose from. Just browsing Google shopping, there’s a huge list, which include:

• Parallax 15-122cm Laser Rangefinder (£86)

• Hokuyo URG-04LX-UG01 Scanning Laser Rangefinder (£880)

• SF10-C Laser Rangefinder (£430)

• LIDAR-Lite 2 Laser Rangefinder (£90)

• RoboPeak RPLIDAR 360° Laser Scanner (£250)

I really like the RPLIDAR 360° laser scanner – it sits at the lower end of the price range and is a 360 degree 2D scanner (LIDAR) solution, with more than 6 meters of effective range detection. The 2D point cloud data it creates can easily be consumed by a simultaneous localisation and mapping (SLAM) algorithm.

Incidentally, you’ll find a very similar laser range finder in a Neato XV robot vacuum cleaner – so if you’re a hacker at heart, you could tear one down, get the sensor, plus harvest a host of other useful parts.

Due to environmental conditions, a point cloud generated from any laser scanning device will contain noise - so how can we clean the data and establish clean landmarks? Landmarks are features which can easily be distinguished from the environment and repeatedly be reacquired, allowing the robot to localise itself.

I decided to implement a derivative of the RANSAC algorithm. Random sample consensus (RANSAC) is an iterative method to estimate parameters of a mathematical model from a set of observed data which contains outliers. For the point cloud data I’ve been looking at, I decided to drop the random element of the algorithm, which gives me more consistent results.

Given a dataset whose data elements contain both inliers and outliers, RANSAC uses a voting scheme to find the optimal fitting result. Data elements in the dataset are used to vote for one or more models. The implementation of this voting scheme is based on two assumptions: that the noisy features will not vote consistently for any single model and there are enough features to agree on a good model.

The code below firstly creates a noisy data set, simulating a laser scan point cloud for a square room. Then it applies my hacked implementation of the RANSAC algorithm to correctly identify walls from the noisy data.

Code Snippet

- using System;
- using System.Drawing;
- using System.Windows.Forms;
- public class Ransac : Form
- {
- const int MAX_LINES = 4;
- const int SAMPLE_SIZE = 10;
- const int RANSAC_CONSENSUS = 50;
- const double RANSAC_TOLERANCE = 7.5;
- const double D2R = Math.PI / 180.0;
- double[] LinesA = new double[MAX_LINES];
- double[] LinesB = new double[MAX_LINES];
- double[] PointCloud = new double[360];
- bool Draw = false;
- int LineCount = 0;
- [STAThread]
- static void Main()
- {
- Application.Run(new Ransac());
- }
- public Ransac()
- {
- Width = 500;
- Height = 525;
- Text = "RANSAC Demo";
- CreateNoisyPointCloud();
- ExtractLines();
- Draw = true;
- }
- private void CreateNoisyPointCloud()
- {
- Random rnd = new Random();
- for (int i = 0; i < 90; i++)
- {
- double length = 150 / Math.Cos((i - 45) * D2R);
- PointCloud[i] = length + rnd.NextDouble() * 16 - 8;
- PointCloud[i + 90] = length + rnd.NextDouble() * 16 - 8;
- PointCloud[i + 180] = length + rnd.NextDouble() * 16 - 8;
- PointCloud[i + 270] = length + rnd.NextDouble() * 16 - 8;
- }
- }
- private void ExtractLines()
- {
- int[] pointStatus = new int[360];
- for (int seed = 0; seed < 360; seed++)
- {
- int[] sample = new int[SAMPLE_SIZE];
- for (int i = 0; i < SAMPLE_SIZE; i++) sample[i] = (seed + i) % 360;
- int[] fitPoints = new int[360];
- int fitCount = 0;
- double a = 0;
- double b = 0;
- LeastSquaresFit(PointCloud, sample, SAMPLE_SIZE, ref a, ref b);
- for (int i = 0; i < 360; i++)
- {
- if (pointStatus[i] == 0)
- {
- // Convert scan vectors to cartesian coordinates.
- double x = Math.Cos(i * D2R) * PointCloud[i];
- double y = Math.Sin(i * D2R) * PointCloud[i];
- // Claim points close to sample line.
- if (DistanceToLine(x, y, a, b) < RANSAC_TOLERANCE)
- fitPoints[fitCount++] = i;
- }
- }
- if (fitCount > RANSAC_CONSENSUS)
- {
- // Refresh line and add to collection.
- LeastSquaresFit(PointCloud, fitPoints, fitCount, ref a, ref b);
- LinesA[LineCount] = a;
- LinesB[LineCount] = b;
- LineCount++;
- // Update point cloud status.
- for (int i = 0; i < fitCount; i++) pointStatus[fitPoints[i]] = LineCount;
- }
- }
- }
- private void LeastSquaresFit(double[] pointCloud, int[] selection, int count, ref double a, ref double b)
- {
- double sumX = 0;
- double sumY = 0;
- double sumXX = 0;
- double sumYY = 0;
- double sumXY = 0;
- for (int i = 0; i < count; i++)
- {
- double x = Math.Cos(selection[i] * D2R) * pointCloud[selection[i]];
- double y = Math.Sin(selection[i] * D2R) * pointCloud[selection[i]];
- sumX += x;
- sumXX += Math.Pow(x, 2);
- sumY += y;
- sumYY += Math.Pow(y, 2);
- sumXY += x * y;
- }
- a = (count * sumXY - sumX * sumY) / (count * sumXX - Math.Pow(sumX, 2));
- b = (sumY * sumXX - sumX * sumXY) / (count * sumXX - Math.Pow(sumX, 2));
- }
- private double DistanceToLine(double x, double y, double a, double b)
- {
- double ao = -1.0 / a;
- double bo = y - ao * x;
- double px = (b - bo) / (ao - a);
- double py = ((ao * (b - bo)) / (ao - a)) + bo;
- return Math.Sqrt(Math.Pow(x - px, 2) + Math.Pow(y - py, 2));
- }
- protected override void OnPaint(PaintEventArgs e)
- {
- if (Draw)
- {
- Draw = false;
- Graphics g = e.Graphics;
- Pen pen = new Pen(Color.Gray, 4);
- for (int i = 0; i < LineCount; i++)
- {
- int x1 = 0;
- int y1 = 250 + (int)(LinesA[i] * -250 + LinesB[i]);
- int x2 = 500;
- int y2 = 250 + (int)(LinesA[i] * 250 + LinesB[i]);
- g.DrawLine(pen, x1, y1, x2, y2);
- }
- pen = new Pen(Color.Red, 4);
- for (int i = 0; i < 360; i++)
- {
- int x = 250 + (int)(Math.Cos(i * D2R) * PointCloud[i]);
- int y = 250 + (int)(Math.Sin(i * D2R) * PointCloud[i]);
- g.DrawEllipse(pen, x, y, 1, 1);
- }
- }
- }
- }