rastr 0.4.0__py3-none-any.whl → 0.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of rastr might be problematic. Click here for more details.
- rastr/_version.py +2 -2
- rastr/arr/fill.py +3 -2
- rastr/create.py +32 -18
- rastr/gis/fishnet.py +14 -2
- rastr/gis/smooth.py +4 -4
- rastr/io.py +40 -14
- rastr/raster.py +627 -101
- {rastr-0.4.0.dist-info → rastr-0.6.0.dist-info}/METADATA +11 -5
- rastr-0.6.0.dist-info/RECORD +15 -0
- rastr-0.4.0.dist-info/RECORD +0 -15
- {rastr-0.4.0.dist-info → rastr-0.6.0.dist-info}/WHEEL +0 -0
- {rastr-0.4.0.dist-info → rastr-0.6.0.dist-info}/licenses/LICENSE +0 -0
rastr/_version.py
CHANGED
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.
|
|
32
|
-
__version_tuple__ = version_tuple = (0,
|
|
31
|
+
__version__ = version = '0.6.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 6, 0)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
rastr/arr/fill.py
CHANGED
|
@@ -8,7 +8,7 @@ if TYPE_CHECKING:
|
|
|
8
8
|
from numpy.typing import NDArray
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
def fillna_nearest_neighbours(arr: NDArray
|
|
11
|
+
def fillna_nearest_neighbours(arr: NDArray) -> NDArray:
|
|
12
12
|
"""Fill NaN values in an N-dimensional array with their nearest neighbours' values.
|
|
13
13
|
|
|
14
14
|
The nearest neighbour is determined using the Euclidean distance between array
|
|
@@ -28,4 +28,5 @@ def fillna_nearest_neighbours(arr: NDArray[np.float64]) -> NDArray[np.float64]:
|
|
|
28
28
|
# Interpolate at the array indices
|
|
29
29
|
interp = NearestNDInterpolator(nonnan_idxs, arr[nonnan_mask])
|
|
30
30
|
filled_arr = interp(*np.indices(arr.shape))
|
|
31
|
-
|
|
31
|
+
# Preserve the original dtype
|
|
32
|
+
return filled_arr.astype(arr.dtype)
|
rastr/create.py
CHANGED
|
@@ -13,10 +13,10 @@ from shapely.geometry import Point
|
|
|
13
13
|
|
|
14
14
|
from rastr.gis.fishnet import create_point_grid, get_point_grid_shape
|
|
15
15
|
from rastr.meta import RasterMeta
|
|
16
|
-
from rastr.raster import
|
|
16
|
+
from rastr.raster import Raster
|
|
17
17
|
|
|
18
18
|
if TYPE_CHECKING:
|
|
19
|
-
from collections.abc import Iterable
|
|
19
|
+
from collections.abc import Collection, Iterable
|
|
20
20
|
|
|
21
21
|
import geopandas as gpd
|
|
22
22
|
from numpy.typing import ArrayLike
|
|
@@ -49,9 +49,9 @@ def raster_distance_from_polygon(
|
|
|
49
49
|
*,
|
|
50
50
|
raster_meta: RasterMeta,
|
|
51
51
|
extent_polygon: Polygon | None = None,
|
|
52
|
-
snap_raster:
|
|
52
|
+
snap_raster: Raster | None = None,
|
|
53
53
|
show_pbar: bool = False,
|
|
54
|
-
) ->
|
|
54
|
+
) -> Raster:
|
|
55
55
|
"""Make a raster where each cell's value is its centre's distance to a polygon.
|
|
56
56
|
|
|
57
57
|
The raster should use a projected coordinate system.
|
|
@@ -116,7 +116,7 @@ def raster_distance_from_polygon(
|
|
|
116
116
|
distances = np.where(mask, np.array([polygon.distance(pt) for pt in _pts]), np.nan)
|
|
117
117
|
distance_raster = distances.reshape(x.shape)
|
|
118
118
|
|
|
119
|
-
return
|
|
119
|
+
return Raster(arr=distance_raster, raster_meta=raster_meta)
|
|
120
120
|
|
|
121
121
|
|
|
122
122
|
def _pbar(iterable: Iterable[_T], *, desc: str | None = None) -> Iterable[_T]:
|
|
@@ -130,19 +130,19 @@ def full_raster(
|
|
|
130
130
|
*,
|
|
131
131
|
bounds: tuple[float, float, float, float],
|
|
132
132
|
fill_value: float = np.nan,
|
|
133
|
-
) ->
|
|
133
|
+
) -> Raster:
|
|
134
134
|
"""Create a raster with a specified fill value for all cells."""
|
|
135
135
|
shape = get_point_grid_shape(bounds=bounds, cell_size=raster_meta.cell_size)
|
|
136
136
|
arr = np.full(shape, fill_value, dtype=np.float32)
|
|
137
|
-
return
|
|
137
|
+
return Raster(arr=arr, raster_meta=raster_meta)
|
|
138
138
|
|
|
139
139
|
|
|
140
140
|
def rasterize_gdf(
|
|
141
141
|
gdf: gpd.GeoDataFrame,
|
|
142
142
|
*,
|
|
143
143
|
raster_meta: RasterMeta,
|
|
144
|
-
target_cols:
|
|
145
|
-
) -> list[
|
|
144
|
+
target_cols: Collection[str],
|
|
145
|
+
) -> list[Raster]:
|
|
146
146
|
"""Rasterize geometries from a GeoDataFrame.
|
|
147
147
|
|
|
148
148
|
Supports polygons, points, linestrings, and other geometry types.
|
|
@@ -205,14 +205,16 @@ def rasterize_gdf(
|
|
|
205
205
|
dtype=np.float32,
|
|
206
206
|
)
|
|
207
207
|
|
|
208
|
-
# Create
|
|
209
|
-
raster =
|
|
208
|
+
# Create Raster
|
|
209
|
+
raster = Raster(arr=raster_array, raster_meta=raster_meta)
|
|
210
210
|
rasters.append(raster)
|
|
211
211
|
|
|
212
212
|
return rasters
|
|
213
213
|
|
|
214
214
|
|
|
215
|
-
def _validate_columns_exist(
|
|
215
|
+
def _validate_columns_exist(
|
|
216
|
+
gdf: gpd.GeoDataFrame, target_cols: Collection[str]
|
|
217
|
+
) -> None:
|
|
216
218
|
"""Validate that all target columns exist in the GeoDataFrame.
|
|
217
219
|
|
|
218
220
|
Args:
|
|
@@ -228,7 +230,9 @@ def _validate_columns_exist(gdf: gpd.GeoDataFrame, target_cols: list[str]) -> No
|
|
|
228
230
|
raise MissingColumnsError(msg)
|
|
229
231
|
|
|
230
232
|
|
|
231
|
-
def _validate_columns_numeric(
|
|
233
|
+
def _validate_columns_numeric(
|
|
234
|
+
gdf: gpd.GeoDataFrame, target_cols: Collection[str]
|
|
235
|
+
) -> None:
|
|
232
236
|
"""Validate that all target columns contain numeric data.
|
|
233
237
|
|
|
234
238
|
Args:
|
|
@@ -286,7 +290,7 @@ def raster_from_point_cloud(
|
|
|
286
290
|
*,
|
|
287
291
|
crs: CRS | str,
|
|
288
292
|
cell_size: float | None = None,
|
|
289
|
-
) ->
|
|
293
|
+
) -> Raster:
|
|
290
294
|
"""Create a raster from a point cloud via interpolation.
|
|
291
295
|
|
|
292
296
|
Interpolation is only possible within the convex hull of the points. Outside of
|
|
@@ -320,8 +324,18 @@ def raster_from_point_cloud(
|
|
|
320
324
|
if len(x) != len(y) or len(x) != len(z):
|
|
321
325
|
msg = "Length of x, y, and z must be equal."
|
|
322
326
|
raise ValueError(msg)
|
|
327
|
+
xy_finite_mask = np.isfinite(x) & np.isfinite(y)
|
|
328
|
+
if np.any(~xy_finite_mask):
|
|
329
|
+
msg = "Some (x,y) points are NaN-valued or non-finite. These will be ignored."
|
|
330
|
+
warnings.warn(msg, stacklevel=2)
|
|
331
|
+
x = x[xy_finite_mask]
|
|
332
|
+
y = y[xy_finite_mask]
|
|
333
|
+
z = z[xy_finite_mask]
|
|
323
334
|
if len(x) < 3:
|
|
324
|
-
msg =
|
|
335
|
+
msg = (
|
|
336
|
+
"At least three valid (x, y, z) points are required to triangulate a "
|
|
337
|
+
"surface."
|
|
338
|
+
)
|
|
325
339
|
raise ValueError(msg)
|
|
326
340
|
# Check for duplicate (x, y) points
|
|
327
341
|
xy_points = np.column_stack((x, y))
|
|
@@ -332,8 +346,8 @@ def raster_from_point_cloud(
|
|
|
332
346
|
# Heuristic for cell size if not provided
|
|
333
347
|
if cell_size is None:
|
|
334
348
|
# Half the 5th percentile of nearest neighbor distances between the (x,y) points
|
|
335
|
-
tree = KDTree(
|
|
336
|
-
distances, _ = tree.query(
|
|
349
|
+
tree = KDTree(xy_points)
|
|
350
|
+
distances, _ = tree.query(xy_points, k=2)
|
|
337
351
|
distances: np.ndarray
|
|
338
352
|
cell_size = float(np.percentile(distances[distances > 0], 5)) / 2
|
|
339
353
|
|
|
@@ -378,4 +392,4 @@ def raster_from_point_cloud(
|
|
|
378
392
|
crs=crs,
|
|
379
393
|
transform=transform,
|
|
380
394
|
)
|
|
381
|
-
return
|
|
395
|
+
return Raster(arr=arr, raster_meta=raster_meta)
|
rastr/gis/fishnet.py
CHANGED
|
@@ -41,8 +41,20 @@ def get_point_grid_shape(
|
|
|
41
41
|
"""Calculate the shape of the point grid based on bounds and cell size."""
|
|
42
42
|
|
|
43
43
|
xmin, ymin, xmax, ymax = bounds
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
ncols_exact = (xmax - xmin) / cell_size
|
|
45
|
+
nrows_exact = (ymax - ymin) / cell_size
|
|
46
|
+
|
|
47
|
+
# Use round for values very close to integers to avoid floating-point
|
|
48
|
+
# sensitivity while maintaining ceil behavior for truly fractional values
|
|
49
|
+
if np.isclose(ncols_exact, np.round(ncols_exact)):
|
|
50
|
+
ncols = int(np.round(ncols_exact))
|
|
51
|
+
else:
|
|
52
|
+
ncols = int(np.ceil(ncols_exact))
|
|
53
|
+
|
|
54
|
+
if np.isclose(nrows_exact, np.round(nrows_exact)):
|
|
55
|
+
nrows = int(np.round(nrows_exact))
|
|
56
|
+
else:
|
|
57
|
+
nrows = int(np.ceil(nrows_exact))
|
|
46
58
|
|
|
47
59
|
return nrows, ncols
|
|
48
60
|
|
rastr/gis/smooth.py
CHANGED
|
@@ -5,7 +5,7 @@ Fork + Port of <https://github.com/philipschall/shapelysmooth> (Public domain)
|
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
-
from typing import TYPE_CHECKING,
|
|
8
|
+
from typing import TYPE_CHECKING, TypeVar
|
|
9
9
|
|
|
10
10
|
import numpy as np
|
|
11
11
|
from shapely.geometry import LineString, Polygon
|
|
@@ -14,7 +14,7 @@ from typing_extensions import assert_never
|
|
|
14
14
|
if TYPE_CHECKING:
|
|
15
15
|
from numpy.typing import NDArray
|
|
16
16
|
|
|
17
|
-
T
|
|
17
|
+
T = TypeVar("T", bound=LineString | Polygon)
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
class InputeTypeError(TypeError):
|
|
@@ -38,12 +38,12 @@ def catmull_rom_smooth(geometry: T, alpha: float = 0.5, subdivs: int = 10) -> T:
|
|
|
38
38
|
coords, interior_coords = _get_coords(geometry)
|
|
39
39
|
coords_smoothed = _catmull_rom(coords, alpha=alpha, subdivs=subdivs)
|
|
40
40
|
if isinstance(geometry, LineString):
|
|
41
|
-
return
|
|
41
|
+
return geometry.__class__(coords_smoothed)
|
|
42
42
|
elif isinstance(geometry, Polygon):
|
|
43
43
|
interior_coords_smoothed = [
|
|
44
44
|
_catmull_rom(c, alpha=alpha, subdivs=subdivs) for c in interior_coords
|
|
45
45
|
]
|
|
46
|
-
return
|
|
46
|
+
return geometry.__class__(coords_smoothed, holes=interior_coords_smoothed)
|
|
47
47
|
else:
|
|
48
48
|
assert_never(geometry)
|
|
49
49
|
|
rastr/io.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from typing import TYPE_CHECKING
|
|
4
|
+
from typing import TYPE_CHECKING, TypeVar
|
|
5
5
|
|
|
6
6
|
import numpy as np
|
|
7
7
|
import rasterio
|
|
@@ -9,39 +9,59 @@ import rasterio.merge
|
|
|
9
9
|
from pyproj.crs.crs import CRS
|
|
10
10
|
|
|
11
11
|
from rastr.meta import RasterMeta
|
|
12
|
-
from rastr.raster import
|
|
12
|
+
from rastr.raster import Raster
|
|
13
13
|
|
|
14
14
|
if TYPE_CHECKING:
|
|
15
15
|
from numpy.typing import NDArray
|
|
16
16
|
|
|
17
|
+
R = TypeVar("R", bound=Raster)
|
|
18
|
+
|
|
17
19
|
|
|
18
20
|
def read_raster_inmem(
|
|
19
|
-
raster_path: Path | str,
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
raster_path: Path | str,
|
|
22
|
+
*,
|
|
23
|
+
crs: CRS | str | None = None,
|
|
24
|
+
cls: type[R] = Raster,
|
|
25
|
+
) -> R:
|
|
26
|
+
"""Read raster data from a file and return an in-memory Raster object.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
raster_path: Path to the raster file.
|
|
30
|
+
crs: Optional CRS to override the raster's native CRS.
|
|
31
|
+
cls: The Raster subclass to instantiate. This is mostly for internal use,
|
|
32
|
+
but can be useful if you have a custom `Raster` subclass.
|
|
33
|
+
"""
|
|
22
34
|
crs = CRS.from_user_input(crs) if crs is not None else None
|
|
23
35
|
|
|
24
36
|
with rasterio.open(raster_path, mode="r") as dst:
|
|
25
37
|
# Read the entire array
|
|
26
|
-
|
|
27
|
-
|
|
38
|
+
raw_arr: NDArray = dst.read()
|
|
39
|
+
raw_arr = raw_arr.squeeze()
|
|
40
|
+
|
|
28
41
|
# Extract metadata
|
|
29
42
|
cell_size = dst.res[0]
|
|
30
43
|
if crs is None:
|
|
31
44
|
crs = CRS.from_user_input(dst.crs)
|
|
32
45
|
transform = dst.transform
|
|
33
46
|
nodata = dst.nodata
|
|
47
|
+
|
|
48
|
+
# Cast integers to float16 to handle NaN values
|
|
49
|
+
if np.issubdtype(raw_arr.dtype, np.integer):
|
|
50
|
+
arr = raw_arr.astype(np.float16)
|
|
51
|
+
else:
|
|
52
|
+
arr = raw_arr
|
|
53
|
+
|
|
34
54
|
if nodata is not None:
|
|
35
|
-
arr[
|
|
55
|
+
arr[raw_arr == nodata] = np.nan
|
|
36
56
|
|
|
37
57
|
raster_meta = RasterMeta(cell_size=cell_size, crs=crs, transform=transform)
|
|
38
|
-
raster_obj =
|
|
58
|
+
raster_obj = cls(arr=arr, raster_meta=raster_meta)
|
|
39
59
|
return raster_obj
|
|
40
60
|
|
|
41
61
|
|
|
42
62
|
def read_raster_mosaic_inmem(
|
|
43
63
|
mosaic_dir: Path | str, *, glob: str = "*.tif", crs: CRS | None = None
|
|
44
|
-
) ->
|
|
64
|
+
) -> Raster:
|
|
45
65
|
"""Read a raster mosaic from a directory and return an in-memory Raster object.
|
|
46
66
|
|
|
47
67
|
This assumes that all rasters have the same metadata, e.g. coordinate system,
|
|
@@ -81,13 +101,19 @@ def read_raster_mosaic_inmem(
|
|
|
81
101
|
crs = CRS.from_user_input(sources[0].crs)
|
|
82
102
|
|
|
83
103
|
nodata = sources[0].nodata
|
|
84
|
-
|
|
85
|
-
arr[arr == nodata] = np.nan
|
|
104
|
+
raw_arr = arr.squeeze()
|
|
86
105
|
|
|
87
|
-
|
|
106
|
+
# Cast integers to float16 to handle NaN values
|
|
107
|
+
if np.issubdtype(raw_arr.dtype, np.integer):
|
|
108
|
+
arr = raw_arr.astype(np.float16)
|
|
109
|
+
else:
|
|
110
|
+
arr = raw_arr
|
|
111
|
+
|
|
112
|
+
if nodata is not None:
|
|
113
|
+
arr[raw_arr == nodata] = np.nan
|
|
88
114
|
|
|
89
115
|
raster_meta = RasterMeta(cell_size=cell_size, crs=crs, transform=transform)
|
|
90
|
-
raster_obj =
|
|
116
|
+
raster_obj = Raster(arr=arr, raster_meta=raster_meta)
|
|
91
117
|
return raster_obj
|
|
92
118
|
finally:
|
|
93
119
|
for src in sources:
|