Handle masks in GeoTIFFs

This commit is contained in:
Tim Schaub
2022-08-26 22:34:31 +02:00
parent c7988de1d8
commit c860a362e8
4 changed files with 204 additions and 102 deletions

View File

@@ -19,6 +19,18 @@ import {clamp} from '../math.js';
import {getCenter, getIntersection} from '../extent.js';
import {fromCode as unitsFromCode} from '../proj/Units.js';
/**
* Determine if an image type is a mask.
* See https://www.awaresystems.be/imaging/tiff/tifftags/newsubfiletype.html
* @param {GeoTIFFImage} image The image.
* @return {boolean} The image is a mask.
*/
function isMask(image) {
const fileDirectory = image.fileDirectory;
const type = fileDirectory.NewSubfileType || 0;
return (type & 4) === 4;
}
/**
* @typedef {Object} SourceInfo
* @property {string} [url] URL for the source GeoTIFF.
@@ -363,6 +375,12 @@ class GeoTIFFSource extends DataTile {
*/
this.sourceImagery_ = new Array(numSources);
/**
* @type {Array<Array<GeoTIFFImage>>}
* @private
*/
this.sourceMasks_ = new Array(numSources);
/**
* @type {Array<number>}
* @private
@@ -467,8 +485,22 @@ class GeoTIFFSource extends DataTile {
const sourceCount = sources.length;
for (let sourceIndex = 0; sourceIndex < sourceCount; ++sourceIndex) {
const images = sources[sourceIndex];
const images = [];
const masks = [];
sources[sourceIndex].forEach((item) => {
if (isMask(item)) {
masks.push(item);
} else {
images.push(item);
}
});
const imageCount = images.length;
if (masks.length > 0 && masks.length !== imageCount) {
throw new Error(
`Expected one mask per image found ${masks.length} masks and ${imageCount} images`
);
}
let sourceExtent;
let sourceOrigin;
@@ -574,6 +606,7 @@ class GeoTIFFSource extends DataTile {
}
this.sourceImagery_[sourceIndex] = images.reverse();
this.sourceMasks_[sourceIndex] = masks.reverse();
}
for (let i = 0, ii = this.sourceImagery_.length; i < ii; ++i) {
@@ -606,6 +639,10 @@ class GeoTIFFSource extends DataTile {
this.addAlpha_ = true;
break;
}
if (this.sourceMasks_[sourceIndex].length) {
this.addAlpha_ = true;
break;
}
const values = nodataValues[sourceIndex];
@@ -659,15 +696,20 @@ class GeoTIFFSource extends DataTile {
});
}
/**
* @param {number} z The z tile index.
* @param {number} x The x tile index.
* @param {number} y The y tile index.
* @return {Promise} The composed tile data.
* @private
*/
loadTile_(z, x, y) {
const sourceTileSize = this.getTileSize(z);
const sourceCount = this.sourceImagery_.length;
const requests = new Array(sourceCount);
const addAlpha = this.addAlpha_;
const bandCount = this.bandCount;
const samplesPerPixel = this.samplesPerPixel_;
const requests = new Array(sourceCount * 2);
const nodataValues = this.nodataValues_;
const sourceInfo = this.sourceInfo_;
const pool = getWorkerPool();
for (let sourceIndex = 0; sourceIndex < sourceCount; ++sourceIndex) {
const source = sourceInfo[sourceIndex];
const resolutionFactor = this.resolutionFactors_[sourceIndex];
@@ -705,113 +747,151 @@ class GeoTIFFSource extends DataTile {
height: sourceTileSize[1],
samples: samples,
fillValue: fillValue,
pool: getWorkerPool(),
pool: pool,
interleave: false,
});
// requests after `sourceCount` are for mask data (if any)
const maskIndex = sourceCount + sourceIndex;
const mask = this.sourceMasks_[sourceIndex][z];
if (!mask) {
requests[maskIndex] = Promise.resolve(null);
continue;
}
requests[maskIndex] = mask.readRasters({
window: pixelBounds,
width: sourceTileSize[0],
height: sourceTileSize[1],
samples: [0],
pool: pool,
interleave: false,
});
}
const pixelCount = sourceTileSize[0] * sourceTileSize[1];
const dataLength = pixelCount * bandCount;
const normalize = this.normalize_;
const metadata = this.metadata_;
return Promise.all(requests)
.then(function (sourceSamples) {
/** @type {Uint8Array|Float32Array} */
let data;
if (normalize) {
data = new Uint8Array(dataLength);
} else {
data = new Float32Array(dataLength);
}
let dataIndex = 0;
for (let pixelIndex = 0; pixelIndex < pixelCount; ++pixelIndex) {
let transparent = addAlpha;
for (let sourceIndex = 0; sourceIndex < sourceCount; ++sourceIndex) {
const source = sourceInfo[sourceIndex];
let min = source.min;
let max = source.max;
let gain, bias;
if (normalize) {
const stats = metadata[sourceIndex][0];
if (min === undefined) {
if (stats && STATISTICS_MINIMUM in stats) {
min = parseFloat(stats[STATISTICS_MINIMUM]);
} else {
min = getMinForDataType(sourceSamples[sourceIndex][0]);
}
}
if (max === undefined) {
if (stats && STATISTICS_MAXIMUM in stats) {
max = parseFloat(stats[STATISTICS_MAXIMUM]);
} else {
max = getMaxForDataType(sourceSamples[sourceIndex][0]);
}
}
gain = 255 / (max - min);
bias = -min * gain;
}
for (
let sampleIndex = 0;
sampleIndex < samplesPerPixel[sourceIndex];
++sampleIndex
) {
const sourceValue =
sourceSamples[sourceIndex][sampleIndex][pixelIndex];
let value;
if (normalize) {
value = clamp(gain * sourceValue + bias, 0, 255);
} else {
value = sourceValue;
}
if (!addAlpha) {
data[dataIndex] = value;
} else {
let nodata = source.nodata;
if (nodata === undefined) {
let bandIndex;
if (source.bands) {
bandIndex = source.bands[sampleIndex] - 1;
} else {
bandIndex = sampleIndex;
}
nodata = nodataValues[sourceIndex][bandIndex];
}
const nodataIsNaN = isNaN(nodata);
if (
(!nodataIsNaN && sourceValue !== nodata) ||
(nodataIsNaN && !isNaN(sourceValue))
) {
transparent = false;
data[dataIndex] = value;
}
}
dataIndex++;
}
}
if (addAlpha) {
if (!transparent) {
data[dataIndex] = 255;
}
dataIndex++;
}
}
return data;
})
.then(this.composeTile_.bind(this, sourceTileSize))
.catch(function (error) {
// output then rethrow
console.error(error); // eslint-disable-line no-console
throw error;
});
}
/**
* @param {import("../size.js").Size} sourceTileSize The source tile size.
* @param {Array} sourceSamples The source samples.
* @return {import("../DataTile.js").Data} The composed tile data.
* @private
*/
composeTile_(sourceTileSize, sourceSamples) {
const metadata = this.metadata_;
const sourceInfo = this.sourceInfo_;
const sourceCount = this.sourceImagery_.length;
const bandCount = this.bandCount;
const samplesPerPixel = this.samplesPerPixel_;
const nodataValues = this.nodataValues_;
const normalize = this.normalize_;
const addAlpha = this.addAlpha_;
const pixelCount = sourceTileSize[0] * sourceTileSize[1];
const dataLength = pixelCount * bandCount;
/** @type {Uint8Array|Float32Array} */
let data;
if (normalize) {
data = new Uint8Array(dataLength);
} else {
data = new Float32Array(dataLength);
}
let dataIndex = 0;
for (let pixelIndex = 0; pixelIndex < pixelCount; ++pixelIndex) {
let transparent = addAlpha;
for (let sourceIndex = 0; sourceIndex < sourceCount; ++sourceIndex) {
const source = sourceInfo[sourceIndex];
let min = source.min;
let max = source.max;
let gain, bias;
if (normalize) {
const stats = metadata[sourceIndex][0];
if (min === undefined) {
if (stats && STATISTICS_MINIMUM in stats) {
min = parseFloat(stats[STATISTICS_MINIMUM]);
} else {
min = getMinForDataType(sourceSamples[sourceIndex][0]);
}
}
if (max === undefined) {
if (stats && STATISTICS_MAXIMUM in stats) {
max = parseFloat(stats[STATISTICS_MAXIMUM]);
} else {
max = getMaxForDataType(sourceSamples[sourceIndex][0]);
}
}
gain = 255 / (max - min);
bias = -min * gain;
}
for (
let sampleIndex = 0;
sampleIndex < samplesPerPixel[sourceIndex];
++sampleIndex
) {
const sourceValue =
sourceSamples[sourceIndex][sampleIndex][pixelIndex];
let value;
if (normalize) {
value = clamp(gain * sourceValue + bias, 0, 255);
} else {
value = sourceValue;
}
if (!addAlpha) {
data[dataIndex] = value;
} else {
let nodata = source.nodata;
if (nodata === undefined) {
let bandIndex;
if (source.bands) {
bandIndex = source.bands[sampleIndex] - 1;
} else {
bandIndex = sampleIndex;
}
nodata = nodataValues[sourceIndex][bandIndex];
}
const nodataIsNaN = isNaN(nodata);
if (
(!nodataIsNaN && sourceValue !== nodata) ||
(nodataIsNaN && !isNaN(sourceValue))
) {
transparent = false;
data[dataIndex] = value;
}
}
dataIndex++;
}
if (!transparent) {
const maskIndex = sourceCount + sourceIndex;
const mask = sourceSamples[maskIndex];
if (mask && !mask[0][pixelIndex]) {
transparent = true;
}
}
}
if (addAlpha) {
if (!transparent) {
data[dataIndex] = 255;
}
dataIndex++;
}
}
return data;
}
}
/**

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

View File

@@ -0,0 +1,22 @@
import GeoTIFF from '../../../../src/ol/source/GeoTIFF.js';
import Map from '../../../../src/ol/Map.js';
import TileLayer from '../../../../src/ol/layer/WebGLTile.js';
const source = new GeoTIFF({
convertToRGB: true,
sources: [{url: '/data/raster/masked.tif'}],
});
new Map({
layers: [
new TileLayer({
source: source,
}),
],
target: 'map',
view: source.getView(),
});
render({
message: 'works with geotiffs that include a mask',
});

Binary file not shown.