Matt Sweetman

Latest articles

Threshold filter in Canvas

Tube Threshold screenshot

Tube Threshold is a continuation of my experiments into visualising the London underground. The previous version of this experiment, Tube Proximity, used Google Maps to generate an overlay that displayed proximity to tube stations as a series of overlapping circles. This produced a reasonably good result, but had a couple of drawbacks: firstly the api drawing routine took a long time, and secondly the circles had very angular intersections.

This new version uses a heat map to visualise proximity, and applies a threshold filter to it in order to create the desired effect. Together they solve some of the problems described above. A threshold filter is designed to reduce the number of colours in an image, usually to a very low number. In this case the filter reduces the heat map to just 2 colours: blue and transparent.

Heatmap screenshot

This is a crop of the heat map, created in Photoshop using gradient fills. Each white spot is centered on a tube station. The colours in the gradient map move from white to black, via each primary color, allowing us to add the values up from left-to-right. For example the gradient starts at #ffffff, then goes to #ffff00, #ff0000 and finally to #000000, giving us a total of 765 levels (255+255+255).

The threshold filter is applied to the heat map using the Canvas object. The getImageData and putImageData methods allow us to work with an image as an array of pixel values, creating something like a rudimentary shader. This implementation simply needs to clamp pixels either side of a given value to a specific colour. Here's a stripped down version of how it works:

canvas = document.getElementById('canvas');
context = canvas.getContext('2d');

// These will hold the heat map image data, before and after the filter
var originImageData;
var filterImageData;

// Load the heat map image and extract the pixel data
var img = new Image();
img.src = 'heatmap.png';
img.onload = function() {
  // Draw the image to canvas then get the pixel data
  context.drawImage(img, 0, 0);
  originImageData = context.getImageData(0, 0, canvas.width, canvas.height);
  filterImageData = context.getImageData(0, 0, canvas.width, canvas.height);
  runFilter();
};

function runFilter() {
  // Each pixel takes up 4 entries in the array using the RGBA format
  var originPixels = originImageData.data;
  var filterPixels = filterImageData.data;
  for (var i = 0; i < originPixels.length; i += 4) {
    r = originPixels[i];
    g = originPixels[i+1];
    b = originPixels[i+2];
    // Clear the red/green channels
    filterPixels[i] = filterPixels[i+1] = 0;
    // Add some blue
    filterPixels[i+2] = 255;
    // Add opacity if the total colour value is above the threshold
    filterPixels[i+3] = ((r + g + b) >= amount) ? 96 : 0;
  }
  // Write filtered pixel data to the canvas
  context.putImageData(filterImageData, 0, 0);
}

The result produces very smooth intersections between circles, and re-draw times are fast. You can check out the example here. The only problem I've discovered is a slight 'jump' in the filter, noticeable around the mid-way mark. This is caused by the gradients generated by Photoshop, which are not completely linear. The other problem is this effect is only applied on top of a static image, not real Google Maps. Getting this working with movable/zoomable maps is something I'd like to look into.