Skip to content

Commit bac7108

Browse files
committed
added in_geotransform, adding support for warping skewed/rotated images
1 parent b20e729 commit bac7108

File tree

5 files changed

+223
-100
lines changed

5 files changed

+223
-100
lines changed

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ const result = geowarp({
4646
// could be 4326 or "EPSG:4326"
4747
in_srs: 4326,
4848

49+
// only necessary when in_data is skewed or rotated
50+
// 6-parameter geotransform using the order from https://gdal.org/tutorials/geotransforms_tut.html
51+
in_geotransform: [337934.48363, -0.142999, -0.576775, 7840518.4648, -0.57677, 0.14299],
52+
4953
// how many pixels wide the input data is
5054
in_width: 1032,
5155

@@ -84,8 +88,8 @@ const result = geowarp({
8488
// could be 4326 or "EPSG:4326"
8589
out_srs: 3857,
8690

91+
// optional
8792
// number of bands in the output
88-
// defaults to the number of input bands
8993
out_pixel_depth: 3,
9094

9195
// height of the output image in pixels

geowarp.js

Lines changed: 74 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const { booleanIntersects, intersect, polygon } = require("bbox-fns");
22
const dufour_peyton_intersection = require("dufour-peyton-intersection");
33
const fastMax = require("fast-max");
44
const fastMin = require("fast-min");
5+
const Geotransform = require("geoaffine/Geotransform.js");
56
const getDepth = require("get-depth");
67
const getTheoreticalMax = require("typed-array-ranges/get-max");
78
const getTheoreticalMin = require("typed-array-ranges/get-min");
@@ -112,6 +113,7 @@ const geowarp = function geowarp({
112113
debug_level = 0,
113114
in_data,
114115
in_bbox = undefined,
116+
in_geotransform = undefined, // 6-parameter geotransform, only necessary when in_data is skewed or rotated
115117
in_layout = "[band][row,column]",
116118
in_srs,
117119
in_height,
@@ -285,6 +287,13 @@ const geowarp = function geowarp({
285287
if (debug_level >= 1) console.log("[geowarp] pixel height of source data:", in_pixel_height);
286288
if (debug_level >= 1) console.log("[geowarp] pixel width of source data:", in_pixel_width);
287289

290+
in_geotransform ??= [in_xmin, in_pixel_width, 0, in_ymax, 0, -1 * in_pixel_height];
291+
292+
const { forward: in_img_pt_to_srs_pt, inverse: in_srs_pt_to_in_img_pt } = Geotransform(in_geotransform);
293+
294+
// convert point in output srs to image pixel coordinate in input image
295+
const out_srs_pt_to_in_img_pt = same_srs ? in_srs_pt_to_in_img_pt : pt => in_srs_pt_to_in_img_pt(inv(pt));
296+
288297
const [out_xmin, out_ymin, out_xmax, out_ymax] = out_bbox;
289298
if (debug_level >= 1) console.log("[geowarp] out_xmin:", out_xmin);
290299
if (debug_level >= 1) console.log("[geowarp] out_ymin:", out_ymin);
@@ -304,6 +313,12 @@ const geowarp = function geowarp({
304313
const half_out_sample_height = out_sample_height / 2;
305314
const half_out_sample_width = out_sample_width / 2;
306315

316+
// const out_geotransform = [out_xmin, out_pixel_width, 0, out_ymax, 0, -1 * out_pixel_height];
317+
// const { forward: out_img_pt_to_srs_pt, inverse: out_srs_pt_to_img_pt } = Geotransform(out_geotransform);
318+
319+
const in_img_pt_to_out_srs_pt = same_srs ? in_img_pt_to_srs_pt : pt => fwd(in_img_pt_to_srs_pt(pt));
320+
// const in_img_pt_to_out_img_pt = same_srs ? pt => out_srs_pt_to_img_pt(in_img_pt_to_srs_pts(pt)) : pt => out_srs_pt_to_img_pt(fwd(in_img_pt_to_srs_pt(pt)));
321+
307322
if (theoretical_min === undefined || theoretical_max === undefined) {
308323
try {
309324
const data_constructor = in_data[0].constructor.name;
@@ -590,61 +605,56 @@ const geowarp = function geowarp({
590605
[left, bottom, right, top] = cutline && cutline_strategy !== "inside" ? intersect(out_bbox_in_srs, cutline_bbox_in_srs) : out_bbox_in_srs;
591606

592607
if ((left < in_xmax && bottom < in_ymax && right > in_xmin) || top < in_ymin) {
593-
const in_row_start = Math.floor((in_ymax - top) / in_pixel_height);
594-
const in_row_start_clamped = clamp(in_row_start, 0, in_height - 1);
595-
const in_row_end = Math.min(Math.floor((in_ymax - bottom) / in_pixel_height), in_height - 1);
596-
const in_row_end_clamped = clamp(in_row_end, 0, in_height - 1);
597-
598-
const in_column_start = Math.floor((left - in_xmin) / in_pixel_width);
599-
const in_column_start_clamped = clamp(in_column_start, 0, in_width - 1);
600-
const in_column_end = Math.min(Math.floor((right - in_xmin) / in_pixel_width), in_width - 1);
601-
const in_column_end_clamped = clamp(in_column_end, 0, in_width - 1);
602-
603-
let pixel_ymin = in_ymax - in_row_start_clamped * in_pixel_height;
604-
for (let r = in_row_start_clamped; r <= in_row_end_clamped; r++) {
605-
// if (clear_process_cache) clear_process_cache();
606-
const pixel_ymax = pixel_ymin;
607-
pixel_ymin = pixel_ymax - in_pixel_height;
608-
// clear_forward_cache(); // don't want cache to get too large, so just cache each row
609-
for (let c = in_column_start_clamped; c <= in_column_end_clamped; c++) {
610-
const raw_values = read_bands.map(band => select({ point: { band, row: r, column: c } }).value);
611-
612-
if (should_skip(raw_values)) continue;
613-
614-
const pixel_xmin = in_xmin + c * in_pixel_width;
615-
const pixel_xmax = pixel_xmin + in_pixel_width;
616-
617-
const pixel_bbox = [pixel_xmin, pixel_ymin, pixel_xmax, pixel_ymax];
618-
619-
// convert pixel to a rectangle polygon in srs of input data
620-
const rect = polygon(pixel_bbox);
621-
622-
// reproject pixel rectangle from input to output srs
623-
const pixel_geometry_in_out_srs = same_srs ? rect : reprojectGeoJSON(rect, { reproject: fwd });
624-
625-
const intersect_options = {
626-
debug: false,
627-
raster_bbox: out_bbox,
628-
raster_height: out_height_in_samples,
629-
raster_width: out_width_in_samples,
630-
geometry: pixel_geometry_in_out_srs
631-
};
632-
633-
// apply band math expression, no-data mapping, and rounding when applicable
634-
const pixel = process({ pixel: raw_values });
635-
636-
if (cutline) {
637-
intersect_options.per_pixel = ({ row, column }) => {
638-
if (segments_by_row[row].some(([start, end]) => column >= start && column <= end)) {
639-
insert_sample({ raw: raw_values, pixel, row, column });
640-
}
641-
};
642-
} else {
643-
intersect_options.per_pixel = ({ row, column }) => {
644-
insert_sample({ raw: raw_values, pixel, row, column });
608+
const out_bbox_in_input_image_coords = reprojectBoundingBox(out_bbox_in_srs, in_srs_pt_to_in_img_pt);
609+
610+
// need to double check intersection in image space in case of rotation/skew
611+
if (booleanIntersects(out_bbox_in_input_image_coords, [0, 0, in_width, in_height])) {
612+
// snap to pixel array inidices
613+
const [in_column_start, in_row_start, in_column_end, in_row_end] = out_bbox_in_input_image_coords.map(n => Math.floor(n));
614+
const in_row_start_clamped = clamp(in_row_start, 0, in_height - 1);
615+
const in_row_end_clamped = clamp(in_row_end, 0, in_height - 1);
616+
const in_column_start_clamped = clamp(in_column_start, 0, in_width - 1);
617+
const in_column_end_clamped = clamp(in_column_end, 0, in_width - 1);
618+
619+
for (let r = in_row_start_clamped; r <= in_row_end_clamped; r++) {
620+
// if (clear_process_cache) clear_process_cache();
621+
// clear_forward_cache(); // don't want cache to get too large, so just cache each row
622+
for (let c = in_column_start_clamped; c <= in_column_end_clamped; c++) {
623+
const raw_values = read_bands.map(band => select({ point: { band, row: r, column: c } }).value);
624+
625+
if (should_skip(raw_values)) continue;
626+
627+
const rect = polygon([c, r, c + 1, r + 1]);
628+
629+
// to-do: reproject to [I, J] (output image point) because
630+
// intersection algorithm assumes an unskewed space
631+
// we'll only have to do this if we want to support rotated/skewed output
632+
const pixel_geometry_in_out_srs = reprojectGeoJSON(rect, { reproject: in_img_pt_to_out_srs_pt });
633+
634+
const intersect_options = {
635+
debug: false,
636+
raster_bbox: out_bbox,
637+
raster_height: out_height_in_samples,
638+
raster_width: out_width_in_samples,
639+
geometry: pixel_geometry_in_out_srs
645640
};
641+
642+
// apply band math expression, no-data mapping, and rounding when applicable
643+
const pixel = process({ pixel: raw_values });
644+
645+
if (cutline) {
646+
intersect_options.per_pixel = ({ row, column }) => {
647+
if (segments_by_row[row].some(([start, end]) => column >= start && column <= end)) {
648+
insert_sample({ raw: raw_values, pixel, row, column });
649+
}
650+
};
651+
} else {
652+
intersect_options.per_pixel = ({ row, column }) => {
653+
insert_sample({ raw: raw_values, pixel, row, column });
654+
};
655+
}
656+
dufour_peyton_intersection.calculate(intersect_options);
646657
}
647-
dufour_peyton_intersection.calculate(intersect_options);
648658
}
649659
}
650660
}
@@ -662,9 +672,7 @@ const geowarp = function geowarp({
662672
const x = out_xmin + c * out_sample_width + half_out_sample_width;
663673
const pt_out_srs = [x, y];
664674
const pt_in_srs = same_srs ? pt_out_srs : inv(pt_out_srs);
665-
const [x_in_srs, y_in_srs] = pt_in_srs;
666-
const x_in_raster_pixels = Math.floor((x_in_srs - in_xmin) / in_pixel_width);
667-
const y_in_raster_pixels = Math.floor((in_ymax - y_in_srs) / in_pixel_height);
675+
const [x_in_raster_pixels, y_in_raster_pixels] = in_srs_pt_to_in_img_pt(pt_in_srs).map(n => Math.floor(n));
668676

669677
let raw_values = [];
670678

@@ -708,10 +716,8 @@ const geowarp = function geowarp({
708716
for (let c = cstart; c <= cend; c++) {
709717
const x = out_xmin + c * out_sample_width + half_out_sample_width;
710718
const pt_out_srs = [x, y];
711-
const [x_in_srs, y_in_srs] = same_srs ? pt_out_srs : inv(pt_out_srs);
712-
713-
const xInRasterPixels = (x_in_srs - in_xmin) / in_pixel_width;
714-
const yInRasterPixels = (in_ymax - y_in_srs) / in_pixel_height;
719+
const pt_in_srs = same_srs ? pt_out_srs : inv(pt_out_srs);
720+
const [xInRasterPixels, yInRasterPixels] = in_srs_pt_to_in_img_pt(pt_in_srs);
715721

716722
const left = Math.floor(xInRasterPixels);
717723
const right = Math.ceil(xInRasterPixels);
@@ -835,19 +841,18 @@ const geowarp = function geowarp({
835841
right = left + out_sample_width;
836842
// top, left, bottom, right is the sample area in the coordinate system of the output
837843

838-
// convert to bbox of input coordinate system
839-
const bbox_in_srs = same_srs ? [left, bottom, right, top] : reprojectBoundingBox([left, bottom, right, top], inv, { density: 0 });
840-
if (debug_level >= 3) console.log("[geowarp] bbox_in_srs:", bbox_in_srs);
841-
const [xmin_in_srs, ymin_in_srs, xmax_in_srs, ymax_in_srs] = bbox_in_srs;
844+
// convert bbox in output srs to image px of input
845+
// combing srs reprojection and srs-to-image mapping, ensures that bounding box corners
846+
// are reprojected fully before calculating containing bbox
847+
// (prevents drift in increasing bbox twice if image is warped)
848+
const [leftInRasterPixels, topInRasterPixels, rightInRasterPixels, bottomInRasterPixels] = reprojectBoundingBox(
849+
[left, bottom, right, top],
850+
out_srs_pt_to_in_img_pt
851+
);
842852

843-
// convert bbox in input srs to raster pixels
844-
const leftInRasterPixels = (xmin_in_srs - in_xmin) / in_pixel_width;
845853
if (debug_level >= 4) console.log("[geowarp] leftInRasterPixels:", leftInRasterPixels);
846-
const rightInRasterPixels = (xmax_in_srs - in_xmin) / in_pixel_width;
847854
if (debug_level >= 4) console.log("[geowarp] rightInRasterPixels:", rightInRasterPixels);
848-
const topInRasterPixels = (in_ymax - ymax_in_srs) / in_pixel_height;
849855
if (debug_level >= 4) console.log("[geowarp] topInRasterPixels:", topInRasterPixels);
850-
const bottomInRasterPixels = (in_ymax - ymin_in_srs) / in_pixel_height;
851856
if (debug_level >= 4) console.log("[geowarp] bottomInRasterPixels:", bottomInRasterPixels);
852857

853858
let leftSample = Math.round(leftInRasterPixels);

package.json

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@
1212
"clean": "rm -fr ./test-data/*.png && rm -fr ./test-output/*.png",
1313
"format": "npm run lint -- --fix && npx prettier --arrow-parens=avoid --print-width=160 --trailing-comma=none --write *.js *.ts",
1414
"lint": "npx eslint *.js",
15-
"perf": "TEST_NAME=*perf* LOG_SKIP=false TIME=true node test.js",
15+
"perf": "TEST_NAME=*perf* LOG_SKIP=false TEST_TIMED=true node test.js",
1616
"prepublish": "npm run lint",
1717
"setup": "cd ./test-data && ./setup.sh",
1818
"test": "npm run clean && npm run test:js && npm run test:ts",
19-
"test:js": "LOG_SKIP=false TIME=true node test.js",
20-
"test:ts": "LOG_SKIP=false TIME=true npx ts-node ./test.ts",
19+
"test:js": "LOG_SKIP=false TEST_TIMED=true node test.js",
20+
"test:ts": "LOG_SKIP=false TEST_TIMED=true npx ts-node ./test.ts",
2121
"test:tsc": "npx tsc --moduleResolution node --noEmit --noImplicitAny --skipLibCheck --target es2020 ./test.ts"
2222
},
2323
"repository": {
@@ -46,30 +46,32 @@
4646
"homepage": "https://github.com/DanielJDufour/geowarp#readme",
4747
"devDependencies": {
4848
"@mapbox/tilebelt": "^1.0.2",
49-
"@types/node": "^18.14.2",
49+
"@types/node": "^18.16.3",
5050
"fast-counter": "^0.1.0",
5151
"find-and-read": "^1.2.0",
52-
"flug": "^2.5.0",
52+
"flug": "^2.6.0",
5353
"geotiff": "1.0.9",
5454
"geotiff-palette": "^0.1.0",
55-
"geotiff-read-bbox": "^1.2.0",
55+
"geotiff-precise-bbox": "^0.2.0",
56+
"geotiff-read-bbox": "^1.3.0",
5657
"pngjs": "^7.0.0",
5758
"proj4-fully-loaded": "^0.2.0",
5859
"quick-resolve": "^0.0.1",
59-
"typescript": "^4.9.5",
60+
"typescript": "^5.0.4",
6061
"write-image": "^0.2.0"
6162
},
6263
"dependencies": {
63-
"bbox-fns": "^0.4.0",
64+
"bbox-fns": "^0.8.0",
6465
"calc-image-stats": "^0.7.0",
6566
"calc-stats": "^2.1.0",
6667
"dufour-peyton-intersection": "^0.1.2",
6768
"fast-max": "^0.4.0",
6869
"fast-min": "^0.3.0",
6970
"faster-median": "^1.0.0",
71+
"geoaffine": "^0.2.0",
7072
"get-depth": "^0.0.3",
7173
"proj-turbo": "^0.0.1",
72-
"reproject-bbox": "^0.7.0",
74+
"reproject-bbox": "^0.10.0",
7375
"reproject-geojson": "^0.3.0",
7476
"segflip": "^0.0.2",
7577
"typed-array-ranges": "^0.0.0",

setup.js

Lines changed: 0 additions & 16 deletions
This file was deleted.

0 commit comments

Comments
 (0)