Image Filters

Apply a filter to imagery

Layer rendering can be manipulated in precompose and postcompose event listeners. These listeners get an event with a reference to the Canvas rendering context. In this example, the postcompose listener applies a filter to the image data.

<!DOCTYPE html>
<html>
  <head>
    <title>Image Filters</title>
    <link rel="stylesheet" href="https://openlayers.org/en/v4.6.4/css/ol.css" type="text/css">
    <!-- The line below is only needed for old environments like Internet Explorer and Android 4.x -->
    <script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=requestAnimationFrame,Element.prototype.classList,URL"></script>
    <script src="https://openlayers.org/en/v4.6.4/build/ol.js"></script>
  </head>
  <body>
    <div id="map" class="map"></div>
    <select id="kernel" name="kernel">
      <option>none</option>
      <option selected>sharpen</option>
      <option value="sharpenless">sharpen less</option>
      <option>blur</option>
      <option>shadow</option>
      <option>emboss</option>
      <option value="edge">edge detect</option>
    </select>
    <script>
      var key = 'Your Bing Maps Key from http://www.bingmapsportal.com/ here';

      var imagery = new ol.layer.Tile({
        source: new ol.source.BingMaps({key: key, imagerySet: 'Aerial'})
      });

      var map = new ol.Map({
        layers: [imagery],
        target: 'map',
        view: new ol.View({
          center: ol.proj.fromLonLat([-120, 50]),
          zoom: 6
        })
      });

      var kernels = {
        none: [
          0, 0, 0,
          0, 1, 0,
          0, 0, 0
        ],
        sharpen: [
          0, -1, 0,
          -1, 5, -1,
          0, -1, 0
        ],
        sharpenless: [
          0, -1, 0,
          -1, 10, -1,
          0, -1, 0
        ],
        blur: [
          1, 1, 1,
          1, 1, 1,
          1, 1, 1
        ],
        shadow: [
          1, 2, 1,
          0, 1, 0,
          -1, -2, -1
        ],
        emboss: [
          -2, 1, 0,
          -1, 1, 1,
          0, 1, 2
        ],
        edge: [
          0, 1, 0,
          1, -4, 1,
          0, 1, 0
        ]
      };

      function normalize(kernel) {
        var len = kernel.length;
        var normal = new Array(len);
        var i, sum = 0;
        for (i = 0; i < len; ++i) {
          sum += kernel[i];
        }
        if (sum <= 0) {
          normal.normalized = false;
          sum = 1;
        } else {
          normal.normalized = true;
        }
        for (i = 0; i < len; ++i) {
          normal[i] = kernel[i] / sum;
        }
        return normal;
      }

      var select = document.getElementById('kernel');
      var selectedKernel = normalize(kernels[select.value]);


      /**
       * Update the kernel and re-render on change.
       */
      select.onchange = function() {
        selectedKernel = normalize(kernels[select.value]);
        map.render();
      };


      /**
       * Apply a filter on "postcompose" events.
       */
      imagery.on('postcompose', function(event) {
        convolve(event.context, selectedKernel);
      });


      /**
       * Apply a convolution kernel to canvas.  This works for any size kernel, but
       * performance starts degrading above 3 x 3.
       * @param {CanvasRenderingContext2D} context Canvas 2d context.
       * @param {Array.<number>} kernel Kernel.
       */
      function convolve(context, kernel) {
        var canvas = context.canvas;
        var width = canvas.width;
        var height = canvas.height;

        var size = Math.sqrt(kernel.length);
        var half = Math.floor(size / 2);

        var inputData = context.getImageData(0, 0, width, height).data;

        var output = context.createImageData(width, height);
        var outputData = output.data;

        for (var pixelY = 0; pixelY < height; ++pixelY) {
          var pixelsAbove = pixelY * width;
          for (var pixelX = 0; pixelX < width; ++pixelX) {
            var r = 0, g = 0, b = 0, a = 0;
            for (var kernelY = 0; kernelY < size; ++kernelY) {
              for (var kernelX = 0; kernelX < size; ++kernelX) {
                var weight = kernel[kernelY * size + kernelX];
                var neighborY = Math.min(
                    height - 1, Math.max(0, pixelY + kernelY - half));
                var neighborX = Math.min(
                    width - 1, Math.max(0, pixelX + kernelX - half));
                var inputIndex = (neighborY * width + neighborX) * 4;
                r += inputData[inputIndex] * weight;
                g += inputData[inputIndex + 1] * weight;
                b += inputData[inputIndex + 2] * weight;
                a += inputData[inputIndex + 3] * weight;
              }
            }
            var outputIndex = (pixelsAbove + pixelX) * 4;
            outputData[outputIndex] = r;
            outputData[outputIndex + 1] = g;
            outputData[outputIndex + 2] = b;
            outputData[outputIndex + 3] = kernel.normalized ? a : 255;
          }
        }
        context.putImageData(output, 0, 0);
      }
    </script>
  </body>
</html>