Skip to content

Commit 6fb71e7

Browse files
committed
added support for multiple no_data values and automatic recognition of NaN as invalid (effectively no_data)
1 parent 988db4e commit 6fb71e7

File tree

4 files changed

+118
-50
lines changed

4 files changed

+118
-50
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ const result = geowarp({
4242
// see: https://github.com/danieljdufour/xdim
4343
in_layout: "[band][row,column]",
4444

45+
// a number or array of numbers
46+
in_no_data: -99,
47+
4548
// a number or string representing the spatial reference system of the input data
4649
// could be 4326 or "EPSG:4326"
4750
in_srs: 4326,

geowarp.js

Lines changed: 61 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const Geotransform = require("geoaffine/Geotransform.js");
66
const getDepth = require("get-depth");
77
const getTheoreticalMax = require("typed-array-ranges/get-max");
88
const getTheoreticalMin = require("typed-array-ranges/get-min");
9-
const fasterMedian = require("faster-median");
9+
const calcMedian = require("mediana").calculate;
1010
const reprojectBoundingBox = require("bbox-fns/reproject.js");
1111
const reprojectGeoJSON = require("reproject-geojson/pluggable");
1212
const { turbocharge } = require("proj-turbo");
@@ -18,6 +18,8 @@ const xdim = require("xdim");
1818

1919
const clamp = (n, min, max) => (n < min ? min : n > max ? max : n);
2020

21+
const isInvalid = n => n === undefined || n === null || n !== n;
22+
2123
const scaleInteger = (n, r) => {
2224
const n2 = Math.round(n * r);
2325
return [n2, n2 / n, n / n2];
@@ -41,7 +43,7 @@ const forEach = (nums, no_data, cb) => {
4143
if (no_data) {
4244
for (let i = 0; i < len; i++) {
4345
const n = nums[i];
44-
if (n !== no_data) cb(n);
46+
if (no_data.indexOf(n) === -1) cb(n);
4547
}
4648
} else {
4749
for (let i = 0; i < len; i++) {
@@ -62,12 +64,11 @@ const mean = (nums, in_no_data) => {
6264

6365
const mode = (nums, no_data) => {
6466
if (nums.length === 0) return undefined;
65-
6667
const counts = {};
6768
if (no_data) {
6869
for (let i = 0; i < nums.length; i++) {
6970
const n = nums[i];
70-
if (n !== no_data) {
71+
if (typeof n === "number" && n === n && no_data.indexOf(n) === -1) {
7172
if (n in counts) counts[n].count++;
7273
else counts[n] = { n, count: 1 };
7374
}
@@ -121,7 +122,7 @@ const geowarp = function geowarp({
121122
in_pixel_height, // optional, automatically calculated from in_bbox
122123
in_pixel_width, // optional, automatically calculated from in_bbox
123124
in_width,
124-
in_no_data,
125+
in_no_data, // optional, supports one number or an array of unique no data values
125126
out_array_types, // array of constructor names passed to internal call to xdim's prepareData function
126127
out_bands, // array of bands to keep and order, default is keeping all the bands in same order
127128
out_data, // single or multi-dimensional array that geowarp will fill in with the output
@@ -240,6 +241,15 @@ const geowarp = function geowarp({
240241
if (round && typeof out_no_data === "number") out_no_data = Math.round(out_no_data);
241242
// if (out_no_data === null && out_no_data_strategy === "keep") out_no_data = in_no_data;
242243

244+
if (Array.isArray(in_no_data) === false) {
245+
if ("in_no_data" in arguments[0]) {
246+
in_no_data = [in_no_data];
247+
} else {
248+
in_no_data = [];
249+
}
250+
}
251+
const primary_in_no_data = in_no_data[0];
252+
243253
// processing step after we have read the raw pixel values
244254
let process;
245255
if (expr) {
@@ -258,14 +268,14 @@ const geowarp = function geowarp({
258268
process = ({ pixel }) =>
259269
out_bands_to_read_bands.map(iband => {
260270
const value = pixel[iband];
261-
return value === undefined || value === in_no_data ? out_no_data : Math.round(value);
271+
return isInvalid(value) || in_no_data.includes(value) ? out_no_data : Math.round(value);
262272
});
263273
} else {
264274
// without rounding
265275
process = ({ pixel }) =>
266276
out_bands_to_read_bands.map(iband => {
267277
const value = pixel[iband];
268-
return value === undefined || value === in_no_data ? out_no_data : value;
278+
return isInvalid(value) || in_no_data.includes(value) ? out_no_data : value;
269279
});
270280
}
271281
}
@@ -594,9 +604,9 @@ const geowarp = function geowarp({
594604

595605
const should_skip =
596606
skip_no_data_strategy === "any"
597-
? px => px.includes(undefined) || px.includes(in_no_data)
607+
? px => px.some(n => isInvalid(n) || in_no_data.includes(n))
598608
: skip_no_data_strategy === "all"
599-
? px => px.every(n => n === in_no_data)
609+
? px => px.every(n => isInvalid(n) || in_no_data.includes(n))
600610
: () => false;
601611

602612
if (method === "vectorize") {
@@ -703,7 +713,7 @@ const geowarp = function geowarp({
703713

704714
if (x_in_raster_pixels < 0 || y_in_raster_pixels < 0 || x_in_raster_pixels >= in_width || y_in_raster_pixels >= in_height) {
705715
// through reprojection, we can sometimes find ourselves just across the edge
706-
raw_values = new Array(read_bands.length).fill(in_no_data);
716+
raw_values = new Array(read_bands.length).fill(primary_in_no_data);
707717
} else {
708718
raw_values = selectPixel({
709719
row: y_in_raster_pixels,
@@ -758,31 +768,33 @@ const geowarp = function geowarp({
758768
const topWeight = top === bottom ? 0.5 : bottom - yInRasterPixels;
759769
const bottomWeight = top === bottom ? 0.5 : yInRasterPixels - top;
760770

761-
const leftInvalid = left < 0 || left >= in_width;
762-
const rightInvalid = right < 0 || right >= in_width;
763-
const topInvalid = top < 0 || top >= in_height;
764-
const bottomInvalid = bottom < 0 || bottom >= in_height;
771+
const leftOutside = left < 0 || left >= in_width;
772+
const rightOutside = right < 0 || right >= in_width;
773+
const topOutside = top < 0 || top >= in_height;
774+
const bottomOutside = bottom < 0 || bottom >= in_height;
765775

766-
const upperLeftInvalid = topInvalid || leftInvalid;
767-
const upperRightInvalid = topInvalid || rightInvalid;
768-
const lowerLeftInvalid = bottomInvalid || leftInvalid;
769-
const lowerRightInvalid = bottomInvalid || rightInvalid;
776+
const upperleftOutside = topOutside || leftOutside;
777+
const upperRightOutside = topOutside || rightOutside;
778+
const lowerleftOutside = bottomOutside || leftOutside;
779+
const lowerRightOutside = bottomOutside || rightOutside;
770780

771781
const raw_values = new Array();
772782
for (let i = 0; i < read_bands.length; i++) {
773783
const read_band = read_bands[i];
774784

775-
const upperLeftValue = upperLeftInvalid ? in_no_data : select({ point: { band: read_band, row: top, column: left } }).value;
776-
const upperRightValue = upperRightInvalid ? in_no_data : select({ point: { band: read_band, row: top, column: right } }).value;
777-
const lowerLeftValue = lowerLeftInvalid ? in_no_data : select({ point: { band: read_band, row: bottom, column: left } }).value;
778-
const lowerRightValue = lowerRightInvalid ? in_no_data : select({ point: { band: read_band, row: bottom, column: right } }).value;
785+
const upperLeftValue = upperleftOutside ? primary_in_no_data : select({ point: { band: read_band, row: top, column: left } }).value;
786+
const upperRightValue = upperRightOutside ? primary_in_no_data : select({ point: { band: read_band, row: top, column: right } }).value;
787+
const lowerLeftValue = lowerleftOutside ? primary_in_no_data : select({ point: { band: read_band, row: bottom, column: left } }).value;
788+
const lowerRightValue = lowerRightOutside ? primary_in_no_data : select({ point: { band: read_band, row: bottom, column: right } }).value;
779789

780790
let topValue;
781-
if ((upperLeftValue === undefined || upperLeftValue === in_no_data) && (upperRightValue === undefined || upperRightValue === in_no_data)) {
791+
const upperLeftInvalid = isInvalid(upperLeftValue) || in_no_data.includes(upperLeftValue);
792+
const upperRightInvalid = isInvalid(upperRightValue) || in_no_data.includes(upperRightValue);
793+
if (upperLeftInvalid && upperRightInvalid) {
782794
// keep topValue undefined
783-
} else if (upperLeftValue === undefined || upperLeftValue === in_no_data) {
795+
} else if (upperLeftInvalid) {
784796
topValue = upperRightValue;
785-
} else if (upperRightValue === undefined || upperRightValue === in_no_data) {
797+
} else if (upperRightInvalid) {
786798
topValue = upperLeftValue;
787799
} else if (upperLeftValue === upperRightValue) {
788800
// because the upper-left and upper-right values are the same, no weighting is necessary
@@ -792,11 +804,13 @@ const geowarp = function geowarp({
792804
}
793805

794806
let bottomValue;
795-
if ((lowerLeftValue === undefined || lowerLeftValue === in_no_data) && (lowerRightValue === undefined || lowerRightValue === in_no_data)) {
807+
const lowerLeftInvalid = isInvalid(lowerLeftValue) || in_no_data.includes(lowerLeftValue);
808+
const lowerRightInvalid = isInvalid(lowerRightValue) || in_no_data.includes(lowerRightValue);
809+
if (lowerLeftInvalid && lowerRightInvalid) {
796810
// keep bottom value undefined
797-
} else if (lowerLeftValue === undefined || lowerLeftValue === in_no_data) {
811+
} else if (lowerLeftInvalid) {
798812
bottomValue = lowerRightValue;
799-
} else if (lowerRightValue === undefined || lowerRightValue === in_no_data) {
813+
} else if (lowerRightInvalid) {
800814
bottomValue = lowerLeftValue;
801815
} else if (lowerLeftValue === lowerRightValue) {
802816
// because the lower-left and lower-right values are the same, no weighting is necessary
@@ -807,7 +821,7 @@ const geowarp = function geowarp({
807821

808822
let value;
809823
if (topValue === undefined && bottomValue === undefined) {
810-
value = in_no_data;
824+
value = primary_in_no_data;
811825
} else if (topValue === undefined) {
812826
value = bottomValue;
813827
} else if (bottomValue === undefined) {
@@ -827,27 +841,29 @@ const geowarp = function geowarp({
827841
}
828842
}
829843
} else {
844+
// Q: why don't we pass no_data to the following statistical methods (e.g. fastMax)?
845+
// A: we are already filtering out invalid and no-data values beforehand
830846
let calc;
831847
if (typeof method === "function") {
832-
calc = values => (values.length === 0 ? in_no_data : method({ values }));
848+
calc = values => method({ values });
833849
} else if (method === "max") {
834-
calc = values => (values.length === 0 ? in_no_data : fastMax(values, { no_data: in_no_data, theoretical_max }));
850+
calc = values => fastMax(values, { theoretical_max });
835851
} else if (method === "mean") {
836-
calc = values => (values.length === 0 ? in_no_data : mean(values, in_no_data));
852+
calc = values => mean(values);
837853
} else if (method === "median") {
838-
calc = values => (values.length === 0 ? in_no_data : fasterMedian({ nums: values, no_data: in_no_data }));
854+
calc = values => calcMedian(values);
839855
} else if (method === "min") {
840-
calc = values => (values.length === 0 ? in_no_data : fastMin(values, { no_data: in_no_data, theoretical_min }));
856+
calc = values => fastMin(values, { theoretical_min });
841857
} else if (method === "mode") {
842-
calc = values => (values.length === 0 ? in_no_data : mode(values)[0]);
858+
calc = values => mode(values)[0];
843859
} else if (method === "mode-max") {
844-
calc = values => (values.length === 0 ? in_no_data : fastMax(mode(values)));
860+
calc = values => fastMax(mode(values));
845861
} else if (method === "mode-mean") {
846-
calc = values => (values.length === 0 ? in_no_data : mean(mode(values)));
862+
calc = values => mean(mode(values));
847863
} else if (method === "mode-median") {
848-
calc = values => (values.length === 0 ? in_no_data : fasterMedian({ nums: mode(values) }));
864+
calc = values => calcMedian(mode(values));
849865
} else if (method === "mode-min") {
850-
calc = values => (values.length === 0 ? in_no_data : fastMin(mode(values)));
866+
calc = values => fastMin(mode(values));
851867
} else {
852868
throw new Error(`[geowarp] unknown method "${method}"`);
853869
}
@@ -923,8 +939,12 @@ const geowarp = function geowarp({
923939
column: [leftSample, Math.max(leftSample, rightSample - 1)]
924940
}
925941
});
926-
const valid_values = values.filter(v => v !== undefined && v !== in_no_data);
927-
raw_values.push(calc(valid_values));
942+
const valid_values = values.filter(v => typeof v === "number" && v === v && in_no_data.indexOf(v) === -1);
943+
if (valid_values.length === 0) {
944+
raw_values.push(primary_in_no_data);
945+
} else {
946+
raw_values.push(calc(valid_values));
947+
}
928948
}
929949
}
930950

package.json

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,34 +46,33 @@
4646
"homepage": "https://github.com/DanielJDufour/geowarp#readme",
4747
"devDependencies": {
4848
"@mapbox/tilebelt": "^1.0.2",
49-
"@types/node": "^20.10.5",
49+
"@types/node": "^20.11.0",
5050
"eslint": "^8.56.0",
5151
"fast-counter": "^0.1.0",
5252
"find-and-read": "^1.2.0",
53-
"flug": "^2.7.1",
53+
"flug": "^2.7.2",
5454
"geotiff": "1.0.9",
5555
"geotiff-palette": "^0.1.0",
5656
"geotiff-precise-bbox": "^0.2.0",
5757
"geotiff-read-bbox": "^2.2.0",
5858
"pngjs": "^7.0.0",
59-
"prettier": "^3.1.1",
59+
"prettier": "^3.2.2",
6060
"proj4-fully-loaded": "^0.2.0",
6161
"typescript": "^5.3.3",
6262
"write-image": "^0.2.0"
6363
},
6464
"dependencies": {
65-
"bbox-fns": "^0.19.0",
65+
"bbox-fns": "^0.20.2",
6666
"calc-image-stats": "^0.9.0",
67-
"calc-stats": "^2.3.0",
6867
"dufour-peyton-intersection": "^0.2.0",
69-
"fast-max": "^0.4.0",
70-
"fast-min": "^0.3.0",
71-
"faster-median": "^1.0.0",
68+
"fast-max": "^0.5.0",
69+
"fast-min": "^0.4.0",
7270
"geoaffine": "^0.2.0",
7371
"get-depth": "^0.0.3",
72+
"mediana": "^2.0.0",
7473
"proj-turbo": "^0.0.1",
7574
"quick-resolve": "^0.0.1",
76-
"reproject-bbox": "^0.12.0",
75+
"reproject-bbox": "^0.13.1",
7776
"reproject-geojson": "^0.5.0",
7877
"segflip": "^0.0.2",
7978
"typed-array-ranges": "^0.0.0",

test.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -811,3 +811,49 @@ test("skew", async ({ eq }) => {
811811
}
812812
}
813813
});
814+
815+
test("o0antarctica", async ({ eq }) => {
816+
const filename = "bremen_sea_ice_conc_2022_9_9.tif";
817+
const filepath = path.resolve(__dirname, "./test-data", filename);
818+
const geotiff = await GeoTIFF.fromFile(filepath);
819+
const image = await geotiff.getImage(0);
820+
const in_data = await image.readRasters();
821+
// console.log("read data", in_data);
822+
const bbox = getBoundingBox(image);
823+
const in_height = image.getHeight();
824+
const in_width = image.getWidth();
825+
// const fd = image.fileDirectory;
826+
const geokeys = image.getGeoKeys();
827+
const in_srs = geokeys.ProjectedCSTypeGeoKey; // 3031
828+
829+
const methods = ["near", "bilinear", "median"];
830+
for (let i = 0; i < methods.length; i++) {
831+
const method = methods[i];
832+
const result = await geowarp({
833+
in_bbox: bbox,
834+
in_data,
835+
in_layout: "[band][row,column]",
836+
in_srs,
837+
in_height,
838+
in_width,
839+
out_bbox: bbox,
840+
out_height: 512,
841+
out_no_data: 127,
842+
out_width: 512,
843+
out_layout: "[band][row][column]",
844+
out_srs: 3031,
845+
method
846+
});
847+
console.log(method + " warped", result.data);
848+
849+
// check that no NaN values in output
850+
eq(
851+
result.data.flat(3).findIndex(it => isNaN(it)),
852+
-1
853+
);
854+
855+
if (process.env.WRITE) {
856+
writePNGSync({ h: 512, w: 512, data: [result.data[0], result.data[0], result.data[0]], filepath: `./test-output/sea-icea-${method}` });
857+
}
858+
}
859+
});

0 commit comments

Comments
 (0)