Edit

Region Growing

raster4 regiongrowing1 maptiler26

Grow a region from a seed pixel

Click a region on the map. The computed region will be red.

This example uses a ol/source/Raster to generate data based on another source. The raster source accepts any number of input sources (tile or image based) and runs a pipeline of operations on the input data. The return from the final operation is used as the data for the output source.

In this case, a single tiled source of imagery data is used as input. The region is calculated in a single "image" operation using the "seed" pixel provided by the user clicking on the map. The "threshold" value determines whether a given contiguous pixel belongs to the "region" - the difference between a candidate pixel's RGB values and the seed values must be below the threshold.

This example also shows how an additional function can be made available to the operation.

main.js
import Map from 'ol/Map.js';
import RasterSource from 'ol/source/Raster.js';
import View from 'ol/View.js';
import XYZ from 'ol/source/XYZ.js';
import {Image as ImageLayer, Tile as TileLayer} from 'ol/layer.js';
import {fromLonLat} from 'ol/proj.js';

function growRegion(inputs, data) {
  const image = inputs[0];
  let seed = data.pixel;
  const delta = parseInt(data.delta);
  if (!seed) {
    return image;
  }

  seed = seed.map(Math.round);
  const width = image.width;
  const height = image.height;
  const inputData = image.data;
  const outputData = new Uint8ClampedArray(inputData);
  const seedIdx = (seed[1] * width + seed[0]) * 4;
  const seedR = inputData[seedIdx];
  const seedG = inputData[seedIdx + 1];
  const seedB = inputData[seedIdx + 2];
  let edge = [seed];
  while (edge.length) {
    const newedge = [];
    for (let i = 0, ii = edge.length; i < ii; i++) {
      // As noted in the Raster source constructor, this function is provided
      // using the `lib` option. Other functions will NOT be visible unless
      // provided using the `lib` option.
      const next = next4Edges(edge[i]);
      for (let j = 0, jj = next.length; j < jj; j++) {
        const s = next[j][0];
        const t = next[j][1];
        if (s >= 0 && s < width && t >= 0 && t < height) {
          const ci = (t * width + s) * 4;
          const cr = inputData[ci];
          const cg = inputData[ci + 1];
          const cb = inputData[ci + 2];
          const ca = inputData[ci + 3];
          // if alpha is zero, carry on
          if (ca === 0) {
            continue;
          }
          if (
            Math.abs(seedR - cr) < delta &&
            Math.abs(seedG - cg) < delta &&
            Math.abs(seedB - cb) < delta
          ) {
            outputData[ci] = 255;
            outputData[ci + 1] = 0;
            outputData[ci + 2] = 0;
            outputData[ci + 3] = 255;
            newedge.push([s, t]);
          }
          // mark as visited
          inputData[ci + 3] = 0;
        }
      }
    }
    edge = newedge;
  }
  return {data: outputData, width: width, height: height};
}

function next4Edges(edge) {
  const x = edge[0];
  const y = edge[1];
  return [
    [x + 1, y],
    [x - 1, y],
    [x, y + 1],
    [x, y - 1],
  ];
}

const key = 'Get your own API key at https://www.maptiler.com/cloud/';
const attributions =
  '<a href="https://www.maptiler.com/copyright/" target="_blank">&copy; MapTiler</a> ' +
  '<a href="https://www.openstreetmap.org/copyright" target="_blank">&copy; OpenStreetMap contributors</a>';

const imagery = new TileLayer({
  source: new XYZ({
    attributions: attributions,
    url: 'https://api.maptiler.com/tiles/satellite/{z}/{x}/{y}.jpg?key=' + key,
    maxZoom: 20,
    crossOrigin: '',
  }),
});

const raster = new RasterSource({
  sources: [imagery.getSource()],
  operationType: 'image',
  operation: growRegion,
  // Functions in the `lib` object will be available to the operation run in
  // the web worker.
  lib: {
    next4Edges: next4Edges,
  },
});

const rasterImage = new ImageLayer({
  opacity: 0.7,
  source: raster,
});

const map = new Map({
  layers: [imagery, rasterImage],
  target: 'map',
  view: new View({
    center: fromLonLat([-119.07, 47.65]),
    zoom: 11,
  }),
});

let coordinate;

map.on('click', function (event) {
  coordinate = event.coordinate;
  raster.changed();
});

const thresholdControl = document.getElementById('threshold');

raster.on('beforeoperations', function (event) {
  // the event.data object will be passed to operations
  const data = event.data;
  data.delta = thresholdControl.value;
  if (coordinate) {
    data.pixel = map.getPixelFromCoordinate(coordinate);
  }
});

function updateControlValue() {
  document.getElementById('threshold-value').innerText = thresholdControl.value;
}
updateControlValue();

thresholdControl.addEventListener('input', function () {
  updateControlValue();
  raster.changed();
});
index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>Region Growing</title>
    <link rel="stylesheet" href="node_modules/ol/ol.css">
    <style>
      .map {
        width: 100%;
        height: 400px;
      }
      .map {
        cursor: pointer;
      }
      #threshold {
        margin: 0 0.6em;
      }
    </style>
  </head>
  <body>
    <div id="map" class="map"></div>
    <div>
      <label class="input-group">
        Threshold:
        <input id="threshold" type="range" min="1" max="50" value="20">
        <span id="threshold-value"></span>
      </label>
    </div>

    <script type="module" src="main.js"></script>
  </body>
</html>
package.json
{
  "name": "region-growing",
  "dependencies": {
    "ol": "9.1.0"
  },
  "devDependencies": {
    "vite": "^3.2.3"
  },
  "scripts": {
    "start": "vite",
    "build": "vite build"
  }
}