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.
import 'ol/ol.css';
import Map from 'ol/Map';
import RasterSource from 'ol/source/Raster';
import View from 'ol/View';
import XYZ from 'ol/source/XYZ';
import {Image as ImageLayer, Tile as TileLayer} from 'ol/layer';
import {fromLonLat} from 'ol/proj';
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">© MapTiler</a> ' +
'<a href="https://www.openstreetmap.org/copyright" target="_blank">© 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();
});
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Region Growing</title>
<!-- Pointer events polyfill for old browsers, see https://caniuse.com/#feat=pointer -->
<script src="https://unpkg.com/elm-pep"></script>
<!-- The line below is only needed for old environments like Internet Explorer and Android 4.x -->
<script src="https://cdn.polyfill.io/v3/polyfill.min.js?features=fetch,requestAnimationFrame,Element.prototype.classList,URL,TextDecoder,Number.isInteger"></script>
<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 src="main.js"></script>
</body>
</html>
{
"name": "region-growing",
"dependencies": {
"ol": "6.6.1"
},
"devDependencies": {
"parcel": "^2.0.0-beta.1"
},
"scripts": {
"start": "parcel index.html",
"build": "parcel build --public-url . index.html"
}
}