28 January 2013

Math - The split-and-merge algorithm

The split-and-merge or (Ramer-)Douglas-Peucker algorithm simplifies a list of points, removing those deemed redundant with respect to a given tolerance. You can read more about it here. I used it to reduce the input of the mouse cursor to a manageable set of points in a drawing application. Next is an implementation in C# in the form of an extension that acts on a List<Point>:

The entry method:

internal static List<Point> SplitAndMerge(this List<Point> points, double tolerance) {
  if (points == null || points.Count < 3) {
    return points;
  }
  else {
    List<int> indices = new List<int>();
    indices.Add(0);
    indices.Add(points.Count - 1);

    points.SplitAndMerge(0, points.Count - 1, tolerance, ref indices);

    List<point> result = new List<Point>();
    indices.Sort();
    foreach (int i in indices)
      result.Add(points[i]);
    return result;
  }
}

The private submethod that handles the recursive calls:

private static void SplitAndMerge(this List<Point> points, int indexFirst, int indexLast, double tolerance, ref List<int> indices) {
  double maxDist = 0d;
  int indexMaxDist = 0;
  double dist = 0d;

  for (int i = indexFirst; i < indexLast; i++) {
    dist = points[i].PerpendicularDistanceFrom(points[indexFirst], points[indexLast]);
    if (dist > maxDist) {
      maxDist = dist;
      indexMaxDist = i;
    }
  }

  if (maxDist > tolerance && indexMaxDist > 0) {
    indices.Add(indexMaxDist);
    points.SplitAndMerge(indexFirst, indexMaxDist, tolerance, ref indices);
    points.SplitAndMerge(indexMaxDist, indexLast, tolerance, ref indices);
  }
}

A helper method that calculates the distance between a point p to the line between a and b:

internal static double PerpendicularDistanceFrom(this Point p, Point a, Point b) {
  double area = Math.Abs(0.5d * (a.X * b.Y + b.X * p.Y + p.X * a.Y - b.X * a.Y - p.X * b.Y - a.X * p.Y));
  double bottom = Math.Sqrt((a.X - b.X) * (a.X - b.X) + (a.Y - b.Y) * (a.Y - b.Y));
  return area / bottom * 2d;
}