Download Isolines / Read the documentation / Isolines source on Github

Isolines is a Processing library that implements the marching squares algorithm for finding regions of continous color within a grayscale image, similar to the “magic wand” tool in Photoshop. It is based on code by Murphy Stein.

What is marching squares?

An easy way to understand marching squares is to imagine water filling up a landscape. Picture a closed-box with a series of peaks and valleys. As you poured, the water would stay level and move up the height of the landscape, creating a series of closed shapes around the peaks it had yet to completely cover. Marching squares works in much the same way. You set a threshold — the fill-level you want the water to reach — and the algorithm finds a series of paths around the parts of the image that are brighter than that threshold.

The library is called “Isolines” because marching squares finds all of the areas in the image that are the same color value (“iso” is a Latin prefix meaning “same”).

See the Makematics interview with Murphy Stein for a complete explanation of the algorithm.

Installation and Usage

To install Isolines, download the library and unzip it into your Processing libraries folder. Restart Processing and Isolines should show up under the “File > Examples” menu under “Contributed Libraries”.

Transit Map data processed with Isoliness

The following example code demonstrates the usage of the Isolines library to process an image with Transit Score data displayed in color overlaid on a map of San Francisco (see above). This example uses the Channels library to extract the hue value from the heat map image. Isolines needs grayscale pixel values to operate on and the hue channel provides those in a way that most represents the data we’re trying to process here.

import isolines.*;
import channels.*;

Isolines finder;
PImage img;
int threshold = 200;

void setup() {
  // load the image and scale
  // the sketch to the image size
  img = loadImage("walkscore.png");
  size(img.width, img.height);
  // initialize an isolines finder based on img dimensions
  finder = new Isolines(this, img.width, img.height);
}

void draw() {
  image(img, 0,0);
  // update the threshold
  finder.setThreshold(threshold);
  // Use the Channels library to extract
  // the hue channel as an int array
  int[] pix = Channels.hue(img.pixels);
  // find the isolines in the hue pixels
  finder.find(pix);

  // draw the contours
  stroke(255);
  for (int k = 0; k < finder.getNumContours(); k++) {
    finder.drawContour(k);
  }
  
  text("threshold: " + threshold, width-150, 20);
}

void keyPressed() {
  if (key == '-') {
    threshold-=5;
    if (threshold < 0) {
      threshold = 0;
    }
  }
  if (key == '=') {
    threshold+=5;
  }
}

Exporting contours as vectors

Isolines also contains a function for accessing each contour as an array of PVectors. The library includes an example that uses this function to export a series of PDFs containing the vector contours at a series of different thresholds. This is useful for fabrication of a three dimensional model made up of a series of slices representing each contour (for example via laser cutting).

Here is the commented source code of that example (also included with the library as ExportVectorsFromIsolines):

import isolines.*;
import channels.*;
// the Processing PDF exporter
import processing.pdf.*;

Isolines finder;
PImage img;
int threshold = 200;
boolean exporting = false;
// number of steps of threshold to skip
// per layer, lower this for more layers
int layerResolution = 5;

void setup() {
  // load the image and scale
  // the sketch to the image size
  img = loadImage("walkscore.png");
  size(img.width, img.height);
  // initialize an isolines finder based on img dimensions
  finder = new Isolines(this, img.width, img.height);
}

void draw() {
  image(img, 0, 0);
  // update the threshold
  finder.setThreshold(threshold);
  // Use the Channels library to extract
  // the hue channel as an int array
  int[] pix = Channels.hue(img.pixels);
  // find the isolines in the hue pixels
  finder.find(pix);

  // draw the contours
  stroke(0);
  // if we're exporting
  if (exporting) {
    // start a new pdf file named after
    // the current threshold
    beginRecord(PDF, "layer_"+threshold+".pdf");
    println("exporting layer at: " + threshold);
  }
  
  noFill();
  for (int k = 0; k < finder.getNumContours(); k++) {
    
    // get each contour as an array of PVectors
    // so we can work with the individual points
    PVector[] points = finder.getContourPoints(k);
    
    // draw a shape for each contour
    beginShape();
    for (int i = 0; i < points.length; i++) {
      PVector p = points[i];
      // add a vertex to the shape corresponding to
      // each PVector in the the contour
      vertex(p.x, p.y);
    }
    // close the shape
    endShape(CLOSE);
  }

  if(exporting){
    // stop drawing to the PDF file
    endRecord();
    // if we're under  255, we still
    // have more layers to go,
    // so increase the threshold and go again
    // otherwise stop exporting
    if(threshold < 255){
      threshold += layerResolution;
    } else {
      exporting = false;
      println("exporting complete");
    }
  }

  text("threshold: " + threshold, width-150, 20);
}

// when they hit the spacebar
// start exporting at threshold of 0
void keyPressed() {
  if(key == ' '){
    println("exporting layers");
    threshold = 0;
    exporting = true;
  }
}