Skip to content

Commit 5d2a856

Browse files
authored
Define consistent nodata propagation for interp_points and allow to return interpolator (#560)
1 parent f188583 commit 5d2a856

File tree

8 files changed

+1377
-641
lines changed

8 files changed

+1377
-641
lines changed

geoutils/raster/georeferencing.py

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
from __future__ import annotations
2+
3+
from typing import Iterable, Literal
4+
5+
import numpy as np
6+
import rasterio as rio
7+
8+
from geoutils._config import config
9+
from geoutils._typing import ArrayLike, NDArrayNum
10+
11+
12+
def _ij2xy(
13+
i: ArrayLike,
14+
j: ArrayLike,
15+
transform: rio.transform.Affine,
16+
area_or_point: Literal["Area", "Point"] | None,
17+
shift_area_or_point: bool | None = None,
18+
force_offset: str | None = None,
19+
) -> tuple[NDArrayNum, NDArrayNum]:
20+
"""See description of Raster.ij2xy."""
21+
22+
# If undefined, default to the global system config
23+
if shift_area_or_point is None:
24+
shift_area_or_point = config["shift_area_or_point"]
25+
26+
# Shift by half a pixel back for "Point" interpretation
27+
if shift_area_or_point and force_offset is None:
28+
if area_or_point is not None and area_or_point == "Point":
29+
i = np.asarray(i) - 0.5
30+
j = np.asarray(j) - 0.5
31+
32+
# Default offset is upper-left for raster coordinates
33+
if force_offset is None:
34+
force_offset = "ul"
35+
36+
x, y = rio.transform.xy(transform, i, j, offset=force_offset)
37+
38+
return x, y
39+
40+
41+
def _xy2ij(
42+
x: ArrayLike,
43+
y: ArrayLike,
44+
transform: rio.transform.Affine,
45+
area_or_point: Literal["Area", "Point"] | None,
46+
op: type = np.float32,
47+
precision: float | None = None,
48+
shift_area_or_point: bool | None = None,
49+
) -> tuple[NDArrayNum, NDArrayNum]:
50+
"""See description of Raster.xy2ij."""
51+
52+
# If undefined, default to the global system config
53+
if shift_area_or_point is None:
54+
shift_area_or_point = config["shift_area_or_point"]
55+
56+
# Input checks
57+
if op not in [np.float32, np.float64, float]:
58+
raise UserWarning(
59+
"Operator is not of type float: rio.Dataset.index might "
60+
"return unreliable indexes due to rounding issues."
61+
)
62+
63+
i, j = rio.transform.rowcol(transform, x, y, op=op, precision=precision)
64+
65+
# Necessary because rio.Dataset.index does not return abc.Iterable for a single point
66+
if not isinstance(i, Iterable):
67+
i, j = (
68+
np.asarray(
69+
[
70+
i,
71+
]
72+
),
73+
np.asarray(
74+
[
75+
j,
76+
]
77+
),
78+
)
79+
else:
80+
i, j = (np.asarray(i), np.asarray(j))
81+
82+
# AREA_OR_POINT GDAL attribute, i.e. does the value refer to the upper left corner "Area" or
83+
# the center of pixel "Point". This normally has no influence on georeferencing, it's only
84+
# about the interpretation of the raster values, and thus can affect sub-pixel interpolation,
85+
# for more details see: https://gdal.org/user/raster_data_model.html#metadata
86+
87+
# If the user wants to shift according to the interpretation
88+
if shift_area_or_point:
89+
90+
# Shift by half a pixel if the AREA_OR_POINT attribute is "Point", otherwise leave as is
91+
if area_or_point is not None and area_or_point == "Point":
92+
if not isinstance(i.flat[0], (np.floating, float)):
93+
raise ValueError("Operator must return np.floating values to perform pixel interpretation shifting.")
94+
95+
i += 0.5
96+
j += 0.5
97+
98+
# Convert output indexes to integer if they are all whole numbers
99+
if np.all(np.mod(i, 1) == 0) and np.all(np.mod(j, 1) == 0):
100+
i = i.astype(int)
101+
j = j.astype(int)
102+
103+
return i, j
104+
105+
106+
def _coords(
107+
transform: rio.transform.Affine,
108+
shape: tuple[int, int],
109+
area_or_point: Literal["Area", "Point"] | None,
110+
grid: bool = True,
111+
shift_area_or_point: bool | None = None,
112+
force_offset: str | None = None,
113+
) -> tuple[NDArrayNum, NDArrayNum]:
114+
"""See description of Raster.coords."""
115+
116+
# The coordinates are extracted from indexes 0 to shape
117+
_, yy = _ij2xy(
118+
i=np.arange(shape[0] - 1, -1, -1),
119+
j=0,
120+
transform=transform,
121+
area_or_point=area_or_point,
122+
shift_area_or_point=shift_area_or_point,
123+
force_offset=force_offset,
124+
)
125+
xx, _ = _ij2xy(
126+
i=0,
127+
j=np.arange(shape[1]),
128+
transform=transform,
129+
area_or_point=area_or_point,
130+
shift_area_or_point=shift_area_or_point,
131+
force_offset=force_offset,
132+
)
133+
134+
# If grid is True, return coordinate grids
135+
if grid:
136+
meshgrid = tuple(np.meshgrid(xx, np.flip(yy)))
137+
return meshgrid # type: ignore
138+
else:
139+
return np.asarray(xx), np.asarray(yy)
140+
141+
142+
def _outside_image(
143+
xi: ArrayLike,
144+
yj: ArrayLike,
145+
transform: rio.transform.Affine,
146+
shape: tuple[int, int],
147+
area_or_point: Literal["Area", "Point"] | None,
148+
index: bool = True,
149+
) -> bool:
150+
"""See description of Raster.outside_image."""
151+
152+
if not index:
153+
xi, xj = _xy2ij(xi, yj, transform=transform, area_or_point=area_or_point)
154+
155+
if np.any(np.array((xi, yj)) < 0):
156+
return True
157+
elif np.asanyarray(xi) > shape[1] or np.asanyarray(yj) > shape[0]:
158+
return True
159+
else:
160+
return False
161+
162+
163+
def _res(transform: rio.transform.Affine) -> tuple[float, float]:
164+
"""See description of Raster.res"""
165+
166+
return transform[0], abs(transform[4])
167+
168+
169+
def _bounds(transform: rio.transform.Affine, shape: tuple[int, int]) -> rio.coords.BoundingBox:
170+
"""See description of Raster.bounds."""
171+
172+
return rio.coords.BoundingBox(*rio.transform.array_bounds(height=shape[0], width=shape[1], transform=transform))

0 commit comments

Comments
 (0)