geotiff-stats
Version:
Get Statistics from a Large GeoTIFF while Using a Low Amount of Memory
547 lines (482 loc) • 16.5 kB
JavaScript
(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
const calcStats = require("calc-stats");
const iterPixels = require("./iter-pixels");
module.exports = function calcBandStats({ image, bandIndex, calcStatsOptions = {} }) {
const noDataValue = image.getGDALNoData();
const pixels = iterPixels({ image, sample: bandIndex });
// clone to preserve immutabilty
calcStatsOptions = { ...calcStatsOptions };
calcStatsOptions.async = true;
if (noDataValue !== null) calcStatsOptions.noData = noDataValue;
return calcStats(pixels, calcStatsOptions);
};
},{"./iter-pixels":4,"calc-stats":6}],2:[function(require,module,exports){
module.exports = function getGDALStats(image, bandIndex) {
if (typeof bandIndex !== "number") throw new Error("you must specify bandIndex");
const bandStats = {};
const gdalMetadata = image.getGDALMetadata(bandIndex);
if (gdalMetadata) {
if (["string", "number"].includes(typeof gdalMetadata.STATISTICS_MAXIMUM)) {
const max = Number(gdalMetadata.STATISTICS_MAXIMUM);
if (!isNaN(max)) bandStats.max = max;
}
if (["string", "number"].includes(typeof gdalMetadata.STATISTICS_MINIMUM)) {
const min = Number(gdalMetadata.STATISTICS_MINIMUM);
if (!isNaN(min)) bandStats.min = min;
}
if (["string", "number"].includes(typeof gdalMetadata.STATISTICS_MEAN)) {
const mean = Number(gdalMetadata.STATISTICS_MEAN);
if (!isNaN(mean)) bandStats.mean = mean;
}
if (["string", "number"].includes(typeof gdalMetadata.STATISTICS_MEAN)) {
const median = Number(gdalMetadata.STATISTICS_MEDIAN);
if (!isNaN(median)) bandStats.median = median;
}
if (["string", "number"].includes(typeof gdalMetadata.STATISTICS_STDDEV)) {
const stddev = Number(gdalMetadata.STATISTICS_STDDEV);
if (!isNaN(stddev)) bandStats.stddev = stddev;
}
}
return bandStats;
};
},{}],3:[function(require,module,exports){
const getGDALStats = require("./get-gdal-stats");
const calcBandStats = require("./calc-band-stats");
/**
* @name getStats
* @param {Object} image - the image object from tiff.getImage() using the geotiff.js library
* @param {Object} options
* @param {Object} options.debug - set to true to log more information
* @param {Array<String>} options.enough - if these stats are in the GDAL Metadata, then we will avoid running calcStats on all the images' pixels. default is ["min", "max"]. valid values are "min", "max", "median", "mean", "sum", "mode", "modes"
* @param {Object} options.calcStatsOptions - options to pass along to [calc-stats](https://github.com/danieljdufour/calc-stats#advanced-usage)
* @returns {Array<Object>} - Array of Statistics Objects
*/
async function getStats(image, options = {}) {
// support old getStats(image, debug)
if (options === true) options = { calcStatsOptions: undefined, debug: true };
const { debug, enough = ["min", "max"] } = options || {};
if (debug) console.log("[geotiff-stats] debug:", debug);
const calcStatsOptions = typeof options.calcStatsOptions === "object" ? { ...options.calcStatsOptions } : {};
const noDataValue = image.getGDALNoData();
if (debug) console.log("[geotiff-stats] noDataValue:", noDataValue);
const samplesPerPixel = image.getSamplesPerPixel();
if (debug) console.log("[geotiff-stats] samplesPerPixel:", samplesPerPixel);
const stats = {
bands: []
};
for (let bandIndex = 0; bandIndex < samplesPerPixel; bandIndex++) {
if (debug) console.log("[geotiff-stats] bandIndex:", bandIndex);
let bandStats = getGDALStats(image, bandIndex);
const isFloat = image.getSampleFormat(bandIndex) === 3;
const sufficient = enough.every(key => typeof bandStats[key] === "number");
if (debug) console.log("[geotiff-stats] sufficient:", sufficient);
if (!sufficient) {
if (debug)
console.log(
"[geotiff-stats] we weren't able to parse enough stats from the image's metadata, so running calcStats"
);
// let rangeFilter;
// if (typeof bandStats.max === "number" && typeof bandStats.min === "number") {
// rangeFilter = ({ value }) => value >= bandStats.min && value <= bandStats.max;
// } else if (typeof bandStats.max === "number") {
// rangeFilter = ({ value }) => value <= bandStats.max;
// } else if (typeof bandStats.min === "number") {
// rangeFilter = ({ value }) => value >= bandStats.min;
// }
let signFilter;
if (isFloat && typeof bandStats.min === "number" && bandStats.min >= 0) {
// sometimes there's an inconsistency in the GDAL NoData value for 32-bit floating point rasters
// and I'm not sure why, but we can fix it
// if all the values are positive (according to the GDAL Metadata), except for the No Data Value,
// then if we see a negative number, it's probably going to supposed to be no data
signFilter = ({ value }) => value >= 0;
}
if (typeof signFilter === "function") {
if (typeof calcStatsOptions.filter === "function") {
const userProvidedFilter = calcStatsOptions.filter;
calcStatsOptions.filter = data => {
return signFilter(data) && userProvidedFilter(data);
};
} else {
calcStatsOptions.filter = signFilter;
}
}
const calculated = await calcBandStats({ image, bandIndex, calcStatsOptions });
Object.assign(bandStats, calculated);
}
stats.bands.push(bandStats);
}
if (debug) console.log("[geotiff-stats] returning: " + JSON.stringify(stats));
return stats;
}
if (typeof module === "object") module.exports = { getStats };
if (typeof window === "object") window.getStats = getStats;
},{"./calc-band-stats":1,"./get-gdal-stats":2}],4:[function(require,module,exports){
const { wrapNextFunction } = require("iter-fun");
const getGDALStats = require("./get-gdal-stats");
const iterTiles = require("./iter-tiles");
module.exports = function iterPixels({ image, sample }) {
if (typeof sample !== "number") throw new Error("you must specify a bandIndex");
const height = image.getHeight();
const width = image.getWidth();
const tileWidth = image.getTileWidth();
const tileHeight = image.getTileHeight();
const numTilesPerRow = Math.ceil(width / tileWidth);
const numTilesPerCol = Math.ceil(height / tileHeight);
const numTiles = numTilesPerRow * numTilesPerCol;
let nums;
let i = -1;
let iend = 0;
let tileRequests = 0;
let tiles = iterTiles(image, [sample]);
return wrapNextFunction(() => {
i++;
if (i === iend) {
if (tileRequests === numTiles) {
return { done: true };
} else {
return {
done: false,
value: tiles.next().value.then(tile => {
tileRequests++;
nums = tile[0];
i = 0;
iend = nums.length;
return nums[i];
})
};
}
} else {
return {
done: false,
value: nums[i]
};
}
});
};
},{"./get-gdal-stats":2,"./iter-tiles":5,"iter-fun":8}],5:[function(require,module,exports){
const { wrapNextFunction } = require("iter-fun");
module.exports = function iterTiles(image, samples) {
const height = image.getHeight();
const width = image.getWidth();
const tileWidth = image.getTileWidth();
const tileHeight = image.getTileHeight();
const numTilesPerRow = Math.ceil(width / tileWidth);
const numTilesPerCol = Math.ceil(height / tileHeight);
const numTiles = numTilesPerRow * numTilesPerCol;
let tileIndex = -1;
const iterTiles = wrapNextFunction(function next() {
try {
tileIndex++;
if (tileIndex >= numTiles) {
return { value: false, done: true };
} else {
const row = Math.floor(tileIndex / numTilesPerRow);
const column = tileIndex % numTilesPerRow;
const xmin = column * tileWidth;
const ymin = row * tileHeight;
const xmax = xmin + tileWidth;
const ymax = ymin + tileHeight;
const imageWindow = [xmin, ymin, Math.min(width, xmax), Math.min(height, ymax)];
const rasters = image.readRasters({
samples,
window: imageWindow
});
return { value: rasters, done: false };
}
} catch (error) {
console.error(error);
}
});
return iterTiles;
};
},{"iter-fun":8}],6:[function(require,module,exports){
const { getOrCreateIterator } = require("iter-fun");
const fasterMedian = require("faster-median");
function calcStats(
data,
{
async = false,
noData = undefined,
filter = undefined,
calcHistogram = true,
calcMax = true,
calcMean = true,
calcMedian = true,
calcMin = true,
calcMode = true,
calcModes = true,
calcSum = true
} = { debugLevel: 0 }
) {
const iter = getOrCreateIterator(data);
let needCount = calcMean || calcMedian || typeof filter === "function";
let needHistogram = calcHistogram || calcMedian || calcMode || calcModes;
let needSum = calcSum || calcMean;
let count = 0;
let index = 0;
let min;
let max;
let sum = 0;
const histogram = {};
// after it processes filtering
const process = value => {
if (needCount) count++;
if (calcMin && (min === undefined || value < min)) min = value;
if (calcMax && (max === undefined || value > max)) max = value;
if (needSum) sum += value;
if (needHistogram) {
if (value in histogram) histogram[value].ct++;
else histogram[value] = { n: value, ct: 1 };
}
};
let step;
if (typeof noData === "number" && typeof filter === "function") {
step = value => {
index++;
if (value !== noData && filter({ count, index, value }) === true) {
process(value);
}
};
} else if (typeof noData === "number") {
step = value => value !== noData && process(value);
} else if (typeof filter === "function") {
step = value => {
index++;
if (filter({ count, index, value }) === true) {
process(value);
}
};
} else {
step = process;
}
const finish = () => {
const results = {};
if (calcMedian)
results.median = fasterMedian({ counts: histogram, total: count });
if (calcMin) results.min = min;
if (calcMax) results.max = max;
if (calcSum) results.sum = sum;
if (calcMean) results.mean = sum / count;
if (calcHistogram) results.histogram = histogram;
if (calcMode || calcModes) {
let highest_count = 0;
let modes = [];
for (let key in histogram) {
const { n, ct } = histogram[key];
if (ct === highest_count) {
modes.push(n);
} else if (ct > highest_count) {
highest_count = ct;
modes = [n];
}
}
if (calcModes) results.modes = modes;
// compute mean value of all the most popular numbers
if (calcMode)
results.mode = modes.reduce((acc, n) => acc + n, 0) / modes.length;
}
return results;
};
if (async) {
return (async () => {
for await (let value of iter) step(value);
return finish();
})();
} else {
for (let value of iter) step(value);
return finish();
}
}
module.exports = calcStats;
},{"faster-median":7,"iter-fun":8}],7:[function(require,module,exports){
const countWithTotal = ({ nums, no_data }) => {
let len = nums.length;
const counts = {};
let total = 0;
if (no_data !== undefined) {
for (let i = 0; i < len; i++) {
const n = nums[i];
if (n !== no_data) {
total++;
if (n in counts) counts[n].ct++;
else counts[n] = { n, ct: 1 };
}
}
} else {
for (let i = 0; i < len; i++) {
const n = nums[i];
total++;
if (n in counts) counts[n].ct++;
else counts[n] = { n, ct: 1 };
}
}
return { counts, total };
};
const median_of_a_few = ({ nums, no_data }) => {
nums = nums.filter(n => n !== no_data).sort((a, b) => a - b);
switch (nums.length) {
case 0:
return undefined;
case 1:
return nums[0];
default:
const mid = nums.length / 2;
if (nums.length % 2 === 0) {
return (nums[mid - 1] + nums[mid]) / 2;
} else {
return nums[Math.floor(mid)];
}
}
};
const median_of_a_lot = ({ counts, nums, no_data, total }) => {
if (counts === undefined || total === undefined) {
({ counts, total } = countWithTotal({ nums, no_data }));
}
// sort counts by value
const countArray = Object.values(counts).sort((a, b) => a.n - b.n);
const half = total / 2;
const number_of_unique_values = countArray.length;
if (number_of_unique_values === 0) {
return undefined;
} else if (number_of_unique_values === 1) {
return countArray[0].n;
} else {
let x = 0;
if (total % 2 === 0) {
for (let i = 0; i < number_of_unique_values; i++) {
const { n, ct } = countArray[i];
x += ct;
if (x > half) {
// handle if odd or even
// just barely pass cut off
if (x - ct === half) {
return (countArray[i - 1].n + n) / 2;
} else {
return n;
}
}
}
} else {
for (let i = 0; i < number_of_unique_values; i++) {
const { n, ct } = countArray[i];
x += ct;
if (x > half) return n;
}
}
}
};
const fasterMedian = ({ nums, no_data, threshold = 50, counts, total }) => {
if (counts !== undefined || total !== undefined || nums.length > threshold)
return median_of_a_lot({ counts, total, nums, no_data });
else return median_of_a_few({ nums, no_data });
};
if (typeof module === "object") module.exports = fasterMedian;
if (typeof window === "object") window.fasterMedian = fasterMedian;
if (typeof self === "object") self.fasterMedian = fasterMedian;
},{}],8:[function(require,module,exports){
function addSymbolIterator(obj) {
try {
obj[Symbol.iterator] = function () {
return this;
};
} catch (error) {
// pass
}
}
function addSymbolIteratorFallback(obj) {
obj["@@iterator"] = function () {
return this;
};
}
function wrapNextFunction(next) {
const iter = { next };
addSymbolIterator(iter);
addSymbolIteratorFallback(iter);
return iter;
}
function isArray(data) {
try {
return data.constructor.name.endsWith("Array");
} catch {
return false;
}
}
function hasNext(data) {
try {
return typeof data.next === "function";
} catch {
return false;
}
}
function hasIterator(data) {
try {
return "@@iterator" in data;
} catch {
return false;
}
}
function hasSymbolIterator(data) {
try {
return Symbol.iterator in data.constructor.prototype;
} catch {
return false;
}
}
function isIterator(data) {
try {
return (
Symbol.iterator in data &&
typeof data.next === "function" &&
data.propertyIsEnumerable("next") === false
);
} catch {
return false;
}
}
function getIterator(data) {
const iter = data["@@iterator"];
if (hasNext(iter)) {
return iter;
} else if (typeof iter === "function") {
return iter();
}
}
function createIterator(data) {
let i = 0;
let len = data.length;
const next = () =>
i++ < len ? { value: data[i], done: false } : { done: true };
return wrapNextFunction(next);
}
function getOrCreateIterator(data) {
if (isIterator(data)) {
return data;
} else if (hasSymbolIterator(data)) {
return data[Symbol.iterator]();
} else if (hasNext(data)) {
return wrapNextFunction(data.next);
} else if (hasIterator(data)) {
return getIterator(data);
} else if (typeof data === "string" || isArray(data)) {
return createIterator(data);
} else {
throw "[iter-fun] unable to determine iterator";
}
}
if (typeof module === "object") {
module.exports = {
addSymbolIterator,
addSymbolIteratorFallback,
isIterator,
isArray,
hasNext,
hasSymbolIterator,
hasIterator,
getIterator,
createIterator,
getOrCreateIterator,
wrapNextFunction
};
}
},{}]},{},[3]);