rastr 0.2.0__py3-none-any.whl → 0.4.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.
- rastr/_version.py +16 -3
- rastr/arr/fill.py +9 -2
- rastr/create.py +155 -35
- rastr/gis/fishnet.py +13 -4
- rastr/gis/smooth.py +7 -4
- rastr/io.py +68 -3
- rastr/meta.py +47 -10
- rastr/raster.py +297 -60
- rastr-0.4.0.dist-info/METADATA +138 -0
- rastr-0.4.0.dist-info/RECORD +15 -0
- rastr-0.2.0.dist-info/METADATA +0 -44
- rastr-0.2.0.dist-info/RECORD +0 -15
- {rastr-0.2.0.dist-info → rastr-0.4.0.dist-info}/WHEEL +0 -0
- {rastr-0.2.0.dist-info → rastr-0.4.0.dist-info}/licenses/LICENSE +0 -0
rastr/_version.py
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
# file generated by setuptools-scm
|
|
2
2
|
# don't change, don't track in version control
|
|
3
3
|
|
|
4
|
-
__all__ = [
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
5
12
|
|
|
6
13
|
TYPE_CHECKING = False
|
|
7
14
|
if TYPE_CHECKING:
|
|
@@ -9,13 +16,19 @@ if TYPE_CHECKING:
|
|
|
9
16
|
from typing import Union
|
|
10
17
|
|
|
11
18
|
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
12
20
|
else:
|
|
13
21
|
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
14
23
|
|
|
15
24
|
version: str
|
|
16
25
|
__version__: str
|
|
17
26
|
__version_tuple__: VERSION_TUPLE
|
|
18
27
|
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
19
30
|
|
|
20
|
-
__version__ = version = '0.
|
|
21
|
-
__version_tuple__ = version_tuple = (0,
|
|
31
|
+
__version__ = version = '0.4.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 4, 0)
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = None
|
rastr/arr/fill.py
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
1
5
|
import numpy as np
|
|
2
|
-
|
|
3
|
-
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from numpy.typing import NDArray
|
|
4
9
|
|
|
5
10
|
|
|
6
11
|
def fillna_nearest_neighbours(arr: NDArray[np.float64]) -> NDArray[np.float64]:
|
|
@@ -11,6 +16,8 @@ def fillna_nearest_neighbours(arr: NDArray[np.float64]) -> NDArray[np.float64]:
|
|
|
11
16
|
In the case of tiebreaks, the value from the neighbour with the lowest index is
|
|
12
17
|
imputed.
|
|
13
18
|
"""
|
|
19
|
+
from scipy.interpolate import NearestNDInterpolator
|
|
20
|
+
|
|
14
21
|
nonnan_mask = np.nonzero(~np.isnan(arr))
|
|
15
22
|
nonnan_idxs = np.array(nonnan_mask).transpose()
|
|
16
23
|
|
rastr/create.py
CHANGED
|
@@ -1,18 +1,32 @@
|
|
|
1
|
-
from
|
|
2
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
import warnings
|
|
5
|
+
from typing import TYPE_CHECKING, TypeVar
|
|
3
6
|
|
|
4
|
-
import geopandas as gpd
|
|
5
7
|
import numpy as np
|
|
6
|
-
import pandas as pd
|
|
7
8
|
import rasterio.features
|
|
9
|
+
import rasterio.transform
|
|
8
10
|
from affine import Affine
|
|
9
|
-
from
|
|
10
|
-
from
|
|
11
|
+
from pyproj import CRS
|
|
12
|
+
from shapely.geometry import Point
|
|
11
13
|
|
|
12
14
|
from rastr.gis.fishnet import create_point_grid, get_point_grid_shape
|
|
13
15
|
from rastr.meta import RasterMeta
|
|
14
16
|
from rastr.raster import RasterModel
|
|
15
17
|
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from collections.abc import Iterable
|
|
20
|
+
|
|
21
|
+
import geopandas as gpd
|
|
22
|
+
from numpy.typing import ArrayLike
|
|
23
|
+
from shapely.geometry import Polygon
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
TQDM_INSTALLED = importlib.util.find_spec("tqdm") is not None
|
|
27
|
+
|
|
28
|
+
_T = TypeVar("_T")
|
|
29
|
+
|
|
16
30
|
|
|
17
31
|
class MissingColumnsError(ValueError):
|
|
18
32
|
"""Raised when target columns are missing from the GeoDataFrame."""
|
|
@@ -61,17 +75,10 @@ def raster_distance_from_polygon(
|
|
|
61
75
|
Raises:
|
|
62
76
|
ValueError: If the provided CRS is geographic (lat/lon).
|
|
63
77
|
"""
|
|
64
|
-
if
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
err_msg = "Only one of 'extent_polygon' or 'snap_raster' can be provided. "
|
|
69
|
-
raise ValueError(err_msg)
|
|
70
|
-
|
|
71
|
-
if not show_pbar:
|
|
72
|
-
|
|
73
|
-
def _pbar(x: Iterable) -> None:
|
|
74
|
-
return x # No-op if no progress bar is needed
|
|
78
|
+
if show_pbar and not TQDM_INSTALLED:
|
|
79
|
+
msg = "The 'tqdm' package is not installed. Progress bars will not be shown."
|
|
80
|
+
warnings.warn(msg, UserWarning, stacklevel=2)
|
|
81
|
+
show_pbar = False
|
|
75
82
|
|
|
76
83
|
# Check if the provided CRS is projected (cartesian)
|
|
77
84
|
if raster_meta.crs.is_geographic:
|
|
@@ -80,36 +87,44 @@ def raster_distance_from_polygon(
|
|
|
80
87
|
)
|
|
81
88
|
raise ValueError(err_msg)
|
|
82
89
|
|
|
83
|
-
|
|
84
|
-
|
|
90
|
+
if extent_polygon is None and snap_raster is None:
|
|
91
|
+
err_msg = "Either 'extent_polygon' or 'snap_raster' must be provided. "
|
|
92
|
+
raise ValueError(err_msg)
|
|
93
|
+
elif extent_polygon is not None and snap_raster is not None:
|
|
94
|
+
err_msg = "Only one of 'extent_polygon' or 'snap_raster' can be provided. "
|
|
95
|
+
raise ValueError(err_msg)
|
|
96
|
+
elif extent_polygon is None and snap_raster is not None:
|
|
97
|
+
# Calculate the coordinates
|
|
85
98
|
x, y = snap_raster.get_xy()
|
|
86
|
-
|
|
99
|
+
|
|
100
|
+
# Create a mask to identify points for which distance should be calculated
|
|
101
|
+
distance_extent = snap_raster.bbox.difference(polygon)
|
|
102
|
+
elif extent_polygon is not None and snap_raster is None:
|
|
87
103
|
x, y = create_point_grid(
|
|
88
104
|
bounds=extent_polygon.bounds, cell_size=raster_meta.cell_size
|
|
89
105
|
)
|
|
90
|
-
|
|
91
|
-
points = [Point(x, y) for x, y in zip(x.flatten(), y.flatten(), strict=True)]
|
|
92
|
-
|
|
93
|
-
# Create a mask to identify points for which distance should be calculated
|
|
94
|
-
if extent_polygon is not None:
|
|
95
106
|
distance_extent = extent_polygon.difference(polygon)
|
|
96
107
|
else:
|
|
97
|
-
|
|
108
|
+
raise AssertionError
|
|
98
109
|
|
|
99
|
-
|
|
100
|
-
_pbar = partial(tqdm, desc="Finding points within extent")
|
|
101
|
-
mask = [distance_extent.intersects(point) for point in _pbar(points)]
|
|
110
|
+
pts = [Point(x, y) for x, y in zip(x.flatten(), y.flatten(), strict=True)]
|
|
102
111
|
|
|
103
|
-
if show_pbar
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
)
|
|
112
|
+
_pts = _pbar(pts, desc="Finding points within extent") if show_pbar else pts
|
|
113
|
+
mask = [distance_extent.intersects(pt) for pt in _pts]
|
|
114
|
+
|
|
115
|
+
_pts = _pbar(pts, desc="Calculating distances") if show_pbar else pts
|
|
116
|
+
distances = np.where(mask, np.array([polygon.distance(pt) for pt in _pts]), np.nan)
|
|
108
117
|
distance_raster = distances.reshape(x.shape)
|
|
109
118
|
|
|
110
119
|
return RasterModel(arr=distance_raster, raster_meta=raster_meta)
|
|
111
120
|
|
|
112
121
|
|
|
122
|
+
def _pbar(iterable: Iterable[_T], *, desc: str | None = None) -> Iterable[_T]:
|
|
123
|
+
from tqdm import tqdm
|
|
124
|
+
|
|
125
|
+
return tqdm(iterable, desc=desc)
|
|
126
|
+
|
|
127
|
+
|
|
113
128
|
def full_raster(
|
|
114
129
|
raster_meta: RasterMeta,
|
|
115
130
|
*,
|
|
@@ -185,7 +200,8 @@ def rasterize_gdf(
|
|
|
185
200
|
shapes,
|
|
186
201
|
out_shape=shape,
|
|
187
202
|
transform=transform,
|
|
188
|
-
|
|
203
|
+
# Fill gaps with NaN
|
|
204
|
+
fill=np.nan, # type: ignore[reportArgumentType] docstring contradicts inferred annotation
|
|
189
205
|
dtype=np.float32,
|
|
190
206
|
)
|
|
191
207
|
|
|
@@ -222,6 +238,8 @@ def _validate_columns_numeric(gdf: gpd.GeoDataFrame, target_cols: list[str]) ->
|
|
|
222
238
|
Raises:
|
|
223
239
|
NonNumericColumnsError: If any columns contain non-numeric data.
|
|
224
240
|
"""
|
|
241
|
+
import pandas as pd
|
|
242
|
+
|
|
225
243
|
non_numeric_cols = []
|
|
226
244
|
for col in target_cols:
|
|
227
245
|
if not pd.api.types.is_numeric_dtype(gdf[col]):
|
|
@@ -259,3 +277,105 @@ def _validate_no_overlapping_geometries(gdf: gpd.GeoDataFrame) -> None:
|
|
|
259
277
|
"Overlapping geometries can lead to data loss during rasterization."
|
|
260
278
|
)
|
|
261
279
|
raise OverlappingGeometriesError(msg)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def raster_from_point_cloud(
|
|
283
|
+
x: ArrayLike,
|
|
284
|
+
y: ArrayLike,
|
|
285
|
+
z: ArrayLike,
|
|
286
|
+
*,
|
|
287
|
+
crs: CRS | str,
|
|
288
|
+
cell_size: float | None = None,
|
|
289
|
+
) -> RasterModel:
|
|
290
|
+
"""Create a raster from a point cloud via interpolation.
|
|
291
|
+
|
|
292
|
+
Interpolation is only possible within the convex hull of the points. Outside of
|
|
293
|
+
this, cells will be NaN-valued.
|
|
294
|
+
|
|
295
|
+
All (x,y) points must be unique.
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
x: X coordinates of points.
|
|
299
|
+
y: Y coordinates of points.
|
|
300
|
+
z: Values at each (x, y) point to assign the raster.
|
|
301
|
+
crs: Coordinate reference system for the (x, y) coordinates.
|
|
302
|
+
cell_size: Desired cell size for the raster. If None, a heuristic is used based
|
|
303
|
+
on the spacing between (x, y) points.
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
Raster containing the interpolated values.
|
|
307
|
+
|
|
308
|
+
Raises:
|
|
309
|
+
ValueError: If any (x, y) points are duplicated, or if they are all collinear.
|
|
310
|
+
"""
|
|
311
|
+
from scipy.interpolate import LinearNDInterpolator
|
|
312
|
+
from scipy.spatial import KDTree, QhullError
|
|
313
|
+
|
|
314
|
+
x = np.asarray(x).ravel()
|
|
315
|
+
y = np.asarray(y).ravel()
|
|
316
|
+
z = np.asarray(z).ravel()
|
|
317
|
+
crs = CRS.from_user_input(crs)
|
|
318
|
+
|
|
319
|
+
# Validate input arrays
|
|
320
|
+
if len(x) != len(y) or len(x) != len(z):
|
|
321
|
+
msg = "Length of x, y, and z must be equal."
|
|
322
|
+
raise ValueError(msg)
|
|
323
|
+
if len(x) < 3:
|
|
324
|
+
msg = "At least three (x, y, z) points are required to triangulate a surface."
|
|
325
|
+
raise ValueError(msg)
|
|
326
|
+
# Check for duplicate (x, y) points
|
|
327
|
+
xy_points = np.column_stack((x, y))
|
|
328
|
+
if len(xy_points) != len(np.unique(xy_points, axis=0)):
|
|
329
|
+
msg = "Duplicate (x, y) points found. Each (x, y) point must be unique."
|
|
330
|
+
raise ValueError(msg)
|
|
331
|
+
|
|
332
|
+
# Heuristic for cell size if not provided
|
|
333
|
+
if cell_size is None:
|
|
334
|
+
# Half the 5th percentile of nearest neighbor distances between the (x,y) points
|
|
335
|
+
tree = KDTree(np.column_stack((x, y)))
|
|
336
|
+
distances, _ = tree.query(np.column_stack((x, y)), k=2)
|
|
337
|
+
distances: np.ndarray
|
|
338
|
+
cell_size = float(np.percentile(distances[distances > 0], 5)) / 2
|
|
339
|
+
|
|
340
|
+
# Compute bounds from data
|
|
341
|
+
minx, miny, maxx, maxy = np.min(x), np.min(y), np.max(x), np.max(y)
|
|
342
|
+
|
|
343
|
+
# Compute grid shape
|
|
344
|
+
width = int(np.ceil((maxx - minx) / cell_size))
|
|
345
|
+
height = int(np.ceil((maxy - miny) / cell_size))
|
|
346
|
+
shape = (height, width)
|
|
347
|
+
|
|
348
|
+
# Compute transform: upper left corner is (minx, maxy)
|
|
349
|
+
transform = Affine.translation(minx, maxy) * Affine.scale(cell_size, -cell_size)
|
|
350
|
+
|
|
351
|
+
# Create grid coordinates for raster cells
|
|
352
|
+
rows, cols = np.indices(shape)
|
|
353
|
+
xs, ys = rasterio.transform.xy(
|
|
354
|
+
transform=transform, rows=rows, cols=cols, offset="center"
|
|
355
|
+
)
|
|
356
|
+
grid_x = np.array(xs).ravel()
|
|
357
|
+
grid_y = np.array(ys).ravel()
|
|
358
|
+
|
|
359
|
+
# Perform interpolation
|
|
360
|
+
try:
|
|
361
|
+
interpolator = LinearNDInterpolator(
|
|
362
|
+
points=xy_points, values=z, fill_value=np.nan
|
|
363
|
+
)
|
|
364
|
+
except QhullError as err:
|
|
365
|
+
msg = (
|
|
366
|
+
"Failed to interpolate. This may be due to insufficient or "
|
|
367
|
+
"degenerate input points. Ensure that the (x, y) points are not all "
|
|
368
|
+
"collinear (i.e. that the convex hull is non-degenerate)."
|
|
369
|
+
)
|
|
370
|
+
raise ValueError(msg) from err
|
|
371
|
+
|
|
372
|
+
grid_values = np.array(interpolator(np.column_stack((grid_x, grid_y))))
|
|
373
|
+
|
|
374
|
+
arr = grid_values.reshape(shape).astype(np.float32)
|
|
375
|
+
|
|
376
|
+
raster_meta = RasterMeta(
|
|
377
|
+
cell_size=cell_size,
|
|
378
|
+
crs=crs,
|
|
379
|
+
transform=transform,
|
|
380
|
+
)
|
|
381
|
+
return RasterModel(arr=arr, raster_meta=raster_meta)
|
rastr/gis/fishnet.py
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
2
5
|
import numpy as np
|
|
3
|
-
from geopandas.array import GeometryArray
|
|
4
6
|
from shapely import BufferCapStyle, BufferJoinStyle
|
|
5
7
|
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from geopandas.array import GeometryArray
|
|
10
|
+
from numpy.typing import NDArray
|
|
11
|
+
|
|
6
12
|
|
|
7
13
|
def create_point_grid(
|
|
8
14
|
*, bounds: tuple[float, float, float, float], cell_size: float
|
|
9
|
-
) -> tuple[
|
|
15
|
+
) -> tuple[NDArray, NDArray]:
|
|
10
16
|
"""Create a regular grid of point coordinates for raster centers.
|
|
11
17
|
|
|
12
18
|
This function replicates the original grid generation logic that uses
|
|
@@ -25,7 +31,8 @@ def create_point_grid(
|
|
|
25
31
|
x_coords = np.arange(xmin + cell_size / 2, xmax + cell_size / 2, cell_size)
|
|
26
32
|
y_coords = np.arange(ymax - cell_size / 2, ymin - cell_size / 2, -cell_size)
|
|
27
33
|
|
|
28
|
-
|
|
34
|
+
x_points, y_points = np.meshgrid(x_coords, y_coords) # type: ignore[reportAssignmentType]
|
|
35
|
+
return x_points, y_points
|
|
29
36
|
|
|
30
37
|
|
|
31
38
|
def get_point_grid_shape(
|
|
@@ -58,6 +65,8 @@ def create_fishnet(
|
|
|
58
65
|
Returns:
|
|
59
66
|
Shapely Polygons.
|
|
60
67
|
"""
|
|
68
|
+
import geopandas as gpd
|
|
69
|
+
|
|
61
70
|
# Use the shared helper function to create the point grid
|
|
62
71
|
xx, yy = create_point_grid(bounds=bounds, cell_size=res)
|
|
63
72
|
|
rastr/gis/smooth.py
CHANGED
|
@@ -5,12 +5,15 @@ Fork + Port of <https://github.com/philipschall/shapelysmooth> (Public domain)
|
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
-
from typing import TypeAlias
|
|
8
|
+
from typing import TYPE_CHECKING, TypeAlias
|
|
9
9
|
|
|
10
10
|
import numpy as np
|
|
11
11
|
from shapely.geometry import LineString, Polygon
|
|
12
12
|
from typing_extensions import assert_never
|
|
13
13
|
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from numpy.typing import NDArray
|
|
16
|
+
|
|
14
17
|
T: TypeAlias = LineString | Polygon
|
|
15
18
|
|
|
16
19
|
|
|
@@ -46,7 +49,7 @@ def catmull_rom_smooth(geometry: T, alpha: float = 0.5, subdivs: int = 10) -> T:
|
|
|
46
49
|
|
|
47
50
|
|
|
48
51
|
def _catmull_rom(
|
|
49
|
-
coords:
|
|
52
|
+
coords: NDArray,
|
|
50
53
|
*,
|
|
51
54
|
alpha: float = 0.5,
|
|
52
55
|
subdivs: int = 8,
|
|
@@ -89,7 +92,7 @@ def _catmull_rom(
|
|
|
89
92
|
|
|
90
93
|
|
|
91
94
|
def _recursive_eval(
|
|
92
|
-
slice4:
|
|
95
|
+
slice4: NDArray, tangents: list[float], ts: NDArray
|
|
93
96
|
) -> list[tuple[float, float]]:
|
|
94
97
|
"""De Boor/De Casteljau-style recursive linear interpolation over 4 control points.
|
|
95
98
|
|
|
@@ -128,7 +131,7 @@ def _recursive_eval(
|
|
|
128
131
|
|
|
129
132
|
def _get_coords(
|
|
130
133
|
geometry: LineString | Polygon,
|
|
131
|
-
) -> tuple[
|
|
134
|
+
) -> tuple[NDArray, list[NDArray]]:
|
|
132
135
|
if isinstance(geometry, LineString):
|
|
133
136
|
return np.array(geometry.coords), []
|
|
134
137
|
elif isinstance(geometry, Polygon):
|
rastr/io.py
CHANGED
|
@@ -1,16 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from pathlib import Path
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
2
5
|
|
|
3
6
|
import numpy as np
|
|
4
7
|
import rasterio
|
|
5
|
-
|
|
6
|
-
from pyproj.crs import CRS
|
|
8
|
+
import rasterio.merge
|
|
9
|
+
from pyproj.crs.crs import CRS
|
|
7
10
|
|
|
8
11
|
from rastr.meta import RasterMeta
|
|
9
12
|
from rastr.raster import RasterModel
|
|
10
13
|
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from numpy.typing import NDArray
|
|
16
|
+
|
|
11
17
|
|
|
12
|
-
def read_raster_inmem(
|
|
18
|
+
def read_raster_inmem(
|
|
19
|
+
raster_path: Path | str, *, crs: CRS | str | None = None
|
|
20
|
+
) -> RasterModel:
|
|
13
21
|
"""Read raster data from a file and return an in-memory Raster object."""
|
|
22
|
+
crs = CRS.from_user_input(crs) if crs is not None else None
|
|
23
|
+
|
|
14
24
|
with rasterio.open(raster_path, mode="r") as dst:
|
|
15
25
|
# Read the entire array
|
|
16
26
|
arr: NDArray[np.float64] = dst.read()
|
|
@@ -27,3 +37,58 @@ def read_raster_inmem(raster_path: Path | str, crs: CRS | None = None) -> Raster
|
|
|
27
37
|
raster_meta = RasterMeta(cell_size=cell_size, crs=crs, transform=transform)
|
|
28
38
|
raster_obj = RasterModel(arr=arr, raster_meta=raster_meta)
|
|
29
39
|
return raster_obj
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def read_raster_mosaic_inmem(
|
|
43
|
+
mosaic_dir: Path | str, *, glob: str = "*.tif", crs: CRS | None = None
|
|
44
|
+
) -> RasterModel:
|
|
45
|
+
"""Read a raster mosaic from a directory and return an in-memory Raster object.
|
|
46
|
+
|
|
47
|
+
This assumes that all rasters have the same metadata, e.g. coordinate system,
|
|
48
|
+
cell size, etc.
|
|
49
|
+
"""
|
|
50
|
+
mosaic_dir = Path(mosaic_dir)
|
|
51
|
+
raster_paths = list(mosaic_dir.glob(glob))
|
|
52
|
+
if not raster_paths:
|
|
53
|
+
msg = f"No raster files found in {mosaic_dir} matching {glob}"
|
|
54
|
+
raise FileNotFoundError(msg)
|
|
55
|
+
|
|
56
|
+
# Sort raster_paths in alphabetical order by stem
|
|
57
|
+
raster_paths.sort(key=lambda p: p.stem)
|
|
58
|
+
|
|
59
|
+
# Open all TIFF datasets using context managers to ensure proper closure
|
|
60
|
+
sources = []
|
|
61
|
+
try:
|
|
62
|
+
for raster_path in raster_paths:
|
|
63
|
+
src = rasterio.open(raster_path)
|
|
64
|
+
sources.append(src)
|
|
65
|
+
|
|
66
|
+
# Merge into a single mosaic array & transform
|
|
67
|
+
arr, transform = rasterio.merge.merge(sources)
|
|
68
|
+
|
|
69
|
+
# Copy metadata from the first dataset
|
|
70
|
+
out_meta = sources[0].meta.copy()
|
|
71
|
+
out_meta.update(
|
|
72
|
+
{
|
|
73
|
+
"driver": "GTiff",
|
|
74
|
+
"height": arr.shape[1],
|
|
75
|
+
"width": arr.shape[2],
|
|
76
|
+
"transform": transform,
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
cell_size = sources[0].res[0]
|
|
80
|
+
if crs is None:
|
|
81
|
+
crs = CRS.from_user_input(sources[0].crs)
|
|
82
|
+
|
|
83
|
+
nodata = sources[0].nodata
|
|
84
|
+
if nodata is not None:
|
|
85
|
+
arr[arr == nodata] = np.nan
|
|
86
|
+
|
|
87
|
+
arr = arr.squeeze().astype(np.float64)
|
|
88
|
+
|
|
89
|
+
raster_meta = RasterMeta(cell_size=cell_size, crs=crs, transform=transform)
|
|
90
|
+
raster_obj = RasterModel(arr=arr, raster_meta=raster_meta)
|
|
91
|
+
return raster_obj
|
|
92
|
+
finally:
|
|
93
|
+
for src in sources:
|
|
94
|
+
src.close()
|
rastr/meta.py
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
1
5
|
import numpy as np
|
|
2
6
|
from affine import Affine
|
|
3
7
|
from pydantic import BaseModel, InstanceOf
|
|
4
8
|
from pyproj import CRS
|
|
5
|
-
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from numpy.typing import NDArray
|
|
12
|
+
from typing_extensions import Self
|
|
6
13
|
|
|
7
14
|
|
|
8
15
|
class RasterMeta(BaseModel, extra="forbid"):
|
|
@@ -28,7 +35,7 @@ class RasterMeta(BaseModel, extra="forbid"):
|
|
|
28
35
|
transform=Affine.scale(2.0, 2.0),
|
|
29
36
|
)
|
|
30
37
|
|
|
31
|
-
def get_cell_centre_coords(self, shape: tuple[int, int]) ->
|
|
38
|
+
def get_cell_centre_coords(self, shape: tuple[int, int]) -> NDArray:
|
|
32
39
|
"""Return an array of (x, y) coordinates for the center of each cell.
|
|
33
40
|
|
|
34
41
|
The coordinates will be in the coordinate system defined by the
|
|
@@ -38,13 +45,43 @@ class RasterMeta(BaseModel, extra="forbid"):
|
|
|
38
45
|
shape: (rows, cols) of the raster array.
|
|
39
46
|
|
|
40
47
|
Returns:
|
|
41
|
-
|
|
42
|
-
cell center.
|
|
48
|
+
(x, y) coordinates for each cell center, with shape (rows, cols, 2)
|
|
43
49
|
"""
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
xv, yv = np.meshgrid(x_idx, y_idx)
|
|
48
|
-
x_coords, y_coords = self.transform * (xv + 0.5, yv + 0.5)
|
|
49
|
-
coords = np.stack([x_coords, y_coords], axis=-1)
|
|
50
|
+
x_coords = self.get_cell_x_coords(shape[1]) # cols for x-coordinates
|
|
51
|
+
y_coords = self.get_cell_y_coords(shape[0]) # rows for y-coordinates
|
|
52
|
+
coords = np.stack(np.meshgrid(x_coords, y_coords), axis=-1)
|
|
50
53
|
return coords
|
|
54
|
+
|
|
55
|
+
def get_cell_x_coords(self, n_columns: int) -> NDArray:
|
|
56
|
+
"""Return an array of x coordinates for the center of each cell.
|
|
57
|
+
|
|
58
|
+
The coordinates will be in the coordinate system defined by the
|
|
59
|
+
raster's transform.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
n_columns: Number of columns in the raster array.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
x_coordinates at cell centers, with shape (n_columns,)
|
|
66
|
+
"""
|
|
67
|
+
x_idx = np.arange(n_columns) + 0.5
|
|
68
|
+
y_idx = np.zeros_like(x_idx) # Use y=0 for a single row
|
|
69
|
+
x_coords, _ = self.transform * (x_idx, y_idx) # type: ignore[reportAssignmentType] overloaded tuple size in affine
|
|
70
|
+
return x_coords
|
|
71
|
+
|
|
72
|
+
def get_cell_y_coords(self, n_rows: int) -> NDArray:
|
|
73
|
+
"""Return an array of y coordinates for the center of each cell.
|
|
74
|
+
|
|
75
|
+
The coordinates will be in the coordinate system defined by the
|
|
76
|
+
raster's transform.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
n_rows: Number of rows in the raster array.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
y_coordinates at cell centers, with shape (n_rows,)
|
|
83
|
+
"""
|
|
84
|
+
x_idx = np.zeros(n_rows) # Use x=0 for a single column
|
|
85
|
+
y_idx = np.arange(n_rows) + 0.5
|
|
86
|
+
_, y_coords = self.transform * (x_idx, y_idx) # type: ignore[reportAssignmentType] overloaded tuple size in affine
|
|
87
|
+
return y_coords
|
rastr/raster.py
CHANGED
|
@@ -1,30 +1,24 @@
|
|
|
1
1
|
"""Raster data structure."""
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.util
|
|
6
|
+
import warnings
|
|
4
7
|
from contextlib import contextmanager
|
|
5
8
|
from pathlib import Path
|
|
6
|
-
from typing import TYPE_CHECKING, Literal
|
|
9
|
+
from typing import TYPE_CHECKING, Any, Literal, overload
|
|
7
10
|
|
|
8
|
-
import geopandas as gpd
|
|
9
|
-
import matplotlib as mpl
|
|
10
11
|
import numpy as np
|
|
11
|
-
import
|
|
12
|
+
import numpy.ma
|
|
12
13
|
import rasterio.plot
|
|
13
14
|
import rasterio.sample
|
|
14
15
|
import rasterio.transform
|
|
15
16
|
import skimage.measure
|
|
16
|
-
import xyzservices.providers as xyz
|
|
17
|
-
from matplotlib import pyplot as plt
|
|
18
|
-
from matplotlib.axes import Axes
|
|
19
|
-
from mpl_toolkits.axes_grid1 import make_axes_locatable
|
|
20
|
-
from numpy.typing import NDArray
|
|
21
17
|
from pydantic import BaseModel, InstanceOf, field_validator
|
|
22
18
|
from pyproj.crs.crs import CRS
|
|
23
19
|
from rasterio.enums import Resampling
|
|
24
|
-
from rasterio.io import
|
|
25
|
-
from
|
|
26
|
-
from shapely.geometry import LineString, Polygon
|
|
27
|
-
from typing_extensions import Self
|
|
20
|
+
from rasterio.io import MemoryFile
|
|
21
|
+
from shapely.geometry import LineString, Point, Polygon
|
|
28
22
|
|
|
29
23
|
from rastr.arr.fill import fillna_nearest_neighbours
|
|
30
24
|
from rastr.gis.fishnet import create_fishnet
|
|
@@ -32,16 +26,14 @@ from rastr.gis.smooth import catmull_rom_smooth
|
|
|
32
26
|
from rastr.meta import RasterMeta
|
|
33
27
|
|
|
34
28
|
if TYPE_CHECKING:
|
|
35
|
-
from
|
|
29
|
+
from collections.abc import Callable, Generator
|
|
36
30
|
|
|
37
|
-
|
|
38
|
-
import folium
|
|
39
|
-
import folium.raster_layers
|
|
31
|
+
import geopandas as gpd
|
|
40
32
|
from folium import Map
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
33
|
+
from matplotlib.axes import Axes
|
|
34
|
+
from numpy.typing import ArrayLike, NDArray
|
|
35
|
+
from rasterio.io import BufferedDatasetWriter, DatasetReader, DatasetWriter
|
|
36
|
+
from typing_extensions import Self
|
|
45
37
|
|
|
46
38
|
try:
|
|
47
39
|
from rasterio._err import CPLE_BaseError
|
|
@@ -49,7 +41,9 @@ except ImportError:
|
|
|
49
41
|
CPLE_BaseError = Exception # Fallback if private module import fails
|
|
50
42
|
|
|
51
43
|
|
|
52
|
-
|
|
44
|
+
FOLIUM_INSTALLED = importlib.util.find_spec("folium") is not None
|
|
45
|
+
BRANCA_INSTALLED = importlib.util.find_spec("branca") is not None
|
|
46
|
+
MATPLOTLIB_INSTALLED = importlib.util.find_spec("matplotlib") is not None
|
|
53
47
|
|
|
54
48
|
|
|
55
49
|
class RasterCellArrayShapeError(ValueError):
|
|
@@ -62,6 +56,57 @@ class RasterModel(BaseModel):
|
|
|
62
56
|
arr: InstanceOf[np.ndarray]
|
|
63
57
|
raster_meta: RasterMeta
|
|
64
58
|
|
|
59
|
+
@property
|
|
60
|
+
def meta(self) -> RasterMeta:
|
|
61
|
+
"""Alias for raster_meta."""
|
|
62
|
+
return self.raster_meta
|
|
63
|
+
|
|
64
|
+
@meta.setter
|
|
65
|
+
def meta(self, value: RasterMeta) -> None:
|
|
66
|
+
self.raster_meta = value
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def shape(self) -> tuple[int, ...]:
|
|
70
|
+
"""Shape of the raster array."""
|
|
71
|
+
return self.arr.shape
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def crs(self) -> CRS:
|
|
75
|
+
"""Convenience property to access the CRS via meta."""
|
|
76
|
+
return self.meta.crs
|
|
77
|
+
|
|
78
|
+
@crs.setter
|
|
79
|
+
def crs(self, value: CRS) -> None:
|
|
80
|
+
"""Set the CRS via meta."""
|
|
81
|
+
self.meta.crs = value
|
|
82
|
+
|
|
83
|
+
def __init__(
|
|
84
|
+
self,
|
|
85
|
+
*,
|
|
86
|
+
arr: ArrayLike,
|
|
87
|
+
meta: RasterMeta | None = None,
|
|
88
|
+
raster_meta: RasterMeta | None = None,
|
|
89
|
+
) -> None:
|
|
90
|
+
arr = np.asarray(arr)
|
|
91
|
+
|
|
92
|
+
# Set the meta
|
|
93
|
+
if meta is not None and raster_meta is not None:
|
|
94
|
+
msg = (
|
|
95
|
+
"Only one of 'meta' or 'raster_meta' should be provided, they are "
|
|
96
|
+
"aliases."
|
|
97
|
+
)
|
|
98
|
+
raise ValueError(msg)
|
|
99
|
+
elif meta is not None and raster_meta is None:
|
|
100
|
+
raster_meta = meta
|
|
101
|
+
elif meta is None and raster_meta is not None:
|
|
102
|
+
pass
|
|
103
|
+
else:
|
|
104
|
+
# Don't need to mention `'meta'` to simplify the messaging.
|
|
105
|
+
msg = "The attribute 'raster_meta' is required."
|
|
106
|
+
raise ValueError(msg)
|
|
107
|
+
|
|
108
|
+
super().__init__(arr=arr, raster_meta=raster_meta)
|
|
109
|
+
|
|
65
110
|
def __eq__(self, other: object) -> bool:
|
|
66
111
|
"""Check equality of two RasterModel objects."""
|
|
67
112
|
if not isinstance(other, RasterModel):
|
|
@@ -160,6 +205,16 @@ class RasterModel(BaseModel):
|
|
|
160
205
|
"""Get the coordinates of the cell centres in the raster."""
|
|
161
206
|
return self.raster_meta.get_cell_centre_coords(self.arr.shape)
|
|
162
207
|
|
|
208
|
+
@property
|
|
209
|
+
def cell_x_coords(self) -> NDArray[np.float64]:
|
|
210
|
+
"""Get the x coordinates of the cell centres in the raster."""
|
|
211
|
+
return self.raster_meta.get_cell_x_coords(self.arr.shape[1])
|
|
212
|
+
|
|
213
|
+
@property
|
|
214
|
+
def cell_y_coords(self) -> NDArray[np.float64]:
|
|
215
|
+
"""Get the y coordinates of the cell centres in the raster."""
|
|
216
|
+
return self.raster_meta.get_cell_y_coords(self.arr.shape[0])
|
|
217
|
+
|
|
163
218
|
@contextmanager
|
|
164
219
|
def to_rasterio_dataset(
|
|
165
220
|
self,
|
|
@@ -195,14 +250,15 @@ class RasterModel(BaseModel):
|
|
|
195
250
|
|
|
196
251
|
def sample(
|
|
197
252
|
self,
|
|
198
|
-
xy: list[tuple[float, float]],
|
|
253
|
+
xy: list[tuple[float, float]] | list[Point] | ArrayLike,
|
|
199
254
|
*,
|
|
200
255
|
na_action: Literal["raise", "ignore"] = "raise",
|
|
201
256
|
) -> NDArray[np.float64]:
|
|
202
257
|
"""Sample raster values at GeoSeries locations and return sampled values.
|
|
203
258
|
|
|
204
259
|
Args:
|
|
205
|
-
xy: A list of (x, y) coordinates to sample the
|
|
260
|
+
xy: A list of (x, y) coordinates or shapely Point objects to sample the
|
|
261
|
+
raster at.
|
|
206
262
|
na_action: Action to take when a NaN value is encountered in the input xy.
|
|
207
263
|
Options are "raise" (raise an error) or "ignore" (replace with
|
|
208
264
|
NaN).
|
|
@@ -213,6 +269,12 @@ class RasterModel(BaseModel):
|
|
|
213
269
|
# If this function is too slow, consider the optimizations detailed here:
|
|
214
270
|
# https://rdrn.me/optimising-sampling/
|
|
215
271
|
|
|
272
|
+
# Convert shapely Points to coordinate tuples if needed
|
|
273
|
+
if isinstance(xy, (list, tuple)):
|
|
274
|
+
xy = [_get_xy_tuple(point) for point in xy]
|
|
275
|
+
|
|
276
|
+
xy = np.asarray(xy, dtype=float)
|
|
277
|
+
|
|
216
278
|
# Short-circuit
|
|
217
279
|
if len(xy) == 0:
|
|
218
280
|
return np.array([], dtype=float)
|
|
@@ -249,7 +311,7 @@ class RasterModel(BaseModel):
|
|
|
249
311
|
|
|
250
312
|
# Convert the sampled values to a NumPy array and set masked values to NaN
|
|
251
313
|
raster_values = np.array(
|
|
252
|
-
[s.data[0] if not s
|
|
314
|
+
[s.data[0] if not numpy.ma.getmask(s) else np.nan for s in samples]
|
|
253
315
|
).astype(float)
|
|
254
316
|
|
|
255
317
|
if len(xy_nan_idxs) > 0:
|
|
@@ -298,19 +360,25 @@ class RasterModel(BaseModel):
|
|
|
298
360
|
m: Map | None = None,
|
|
299
361
|
opacity: float = 1.0,
|
|
300
362
|
colormap: str = "viridis",
|
|
363
|
+
cbar_label: str | None = None,
|
|
301
364
|
) -> Map:
|
|
302
365
|
"""Display the raster on a folium map."""
|
|
303
|
-
if not FOLIUM_INSTALLED:
|
|
304
|
-
msg = "The 'folium'
|
|
366
|
+
if not FOLIUM_INSTALLED or not MATPLOTLIB_INSTALLED:
|
|
367
|
+
msg = "The 'folium' and 'matplotlib' packages are required for 'explore()'."
|
|
305
368
|
raise ImportError(msg)
|
|
306
369
|
|
|
370
|
+
import folium.raster_layers
|
|
371
|
+
import geopandas as gpd
|
|
372
|
+
import matplotlib as mpl
|
|
373
|
+
|
|
307
374
|
if m is None:
|
|
308
375
|
m = folium.Map()
|
|
309
376
|
|
|
310
|
-
|
|
377
|
+
rgba_map: Callable[[float], tuple[float, float, float, float]] = mpl.colormaps[
|
|
311
378
|
colormap
|
|
312
379
|
]
|
|
313
380
|
|
|
381
|
+
# Cast to GDF to facilitate converting bounds to WGS84
|
|
314
382
|
wgs84_crs = CRS.from_epsg(4326)
|
|
315
383
|
gdf = gpd.GeoDataFrame(geometry=[self.bbox], crs=self.raster_meta.crs).to_crs(
|
|
316
384
|
wgs84_crs
|
|
@@ -320,8 +388,15 @@ class RasterModel(BaseModel):
|
|
|
320
388
|
arr = np.array(self.arr)
|
|
321
389
|
|
|
322
390
|
# Normalize the data to the range [0, 1] as this is the cmap range
|
|
323
|
-
|
|
324
|
-
|
|
391
|
+
with warnings.catch_warnings():
|
|
392
|
+
warnings.filterwarnings(
|
|
393
|
+
"ignore",
|
|
394
|
+
message="All-NaN slice encountered",
|
|
395
|
+
category=RuntimeWarning,
|
|
396
|
+
)
|
|
397
|
+
min_val = np.nanmin(arr)
|
|
398
|
+
max_val = np.nanmax(arr)
|
|
399
|
+
|
|
325
400
|
if max_val > min_val: # Prevent division by zero
|
|
326
401
|
arr = (arr - min_val) / (max_val - min_val)
|
|
327
402
|
else:
|
|
@@ -332,26 +407,46 @@ class RasterModel(BaseModel):
|
|
|
332
407
|
flip_x = self.raster_meta.transform.a < 0
|
|
333
408
|
flip_y = self.raster_meta.transform.e > 0
|
|
334
409
|
if flip_x:
|
|
335
|
-
arr = np.flip(
|
|
410
|
+
arr = np.flip(arr, axis=1)
|
|
336
411
|
if flip_y:
|
|
337
|
-
arr = np.flip(
|
|
412
|
+
arr = np.flip(arr, axis=0)
|
|
338
413
|
|
|
339
414
|
img = folium.raster_layers.ImageOverlay(
|
|
340
415
|
image=arr,
|
|
341
416
|
bounds=[[ymin, xmin], [ymax, xmax]],
|
|
342
417
|
opacity=opacity,
|
|
343
|
-
colormap=
|
|
418
|
+
colormap=rgba_map,
|
|
344
419
|
mercator_project=True,
|
|
345
420
|
)
|
|
346
421
|
|
|
347
422
|
img.add_to(m)
|
|
348
423
|
|
|
424
|
+
# Add a colorbar legend
|
|
425
|
+
if BRANCA_INSTALLED:
|
|
426
|
+
from branca.colormap import LinearColormap as BrancaLinearColormap
|
|
427
|
+
from matplotlib.colors import to_hex
|
|
428
|
+
|
|
429
|
+
# Determine legend data range in original units
|
|
430
|
+
vmin = float(min_val) if np.isfinite(min_val) else 0.0
|
|
431
|
+
vmax = float(max_val) if np.isfinite(max_val) else 1.0
|
|
432
|
+
if vmax <= vmin:
|
|
433
|
+
vmax = vmin + 1.0
|
|
434
|
+
|
|
435
|
+
sample_points = np.linspace(0, 1, rgba_map.N)
|
|
436
|
+
colors = [to_hex(rgba_map(x)) for x in sample_points]
|
|
437
|
+
legend = BrancaLinearColormap(colors=colors, vmin=vmin, vmax=vmax)
|
|
438
|
+
if cbar_label:
|
|
439
|
+
legend.caption = cbar_label
|
|
440
|
+
legend.add_to(m)
|
|
441
|
+
|
|
349
442
|
m.fit_bounds([[ymin, xmin], [ymax, xmax]])
|
|
350
443
|
|
|
351
444
|
return m
|
|
352
445
|
|
|
353
446
|
def to_clipboard(self) -> None:
|
|
354
447
|
"""Copy the raster cell array to the clipboard."""
|
|
448
|
+
import pandas as pd
|
|
449
|
+
|
|
355
450
|
pd.DataFrame(self.arr).to_clipboard(index=False, header=False)
|
|
356
451
|
|
|
357
452
|
def plot(
|
|
@@ -363,9 +458,17 @@ class RasterModel(BaseModel):
|
|
|
363
458
|
cmap: str = "viridis",
|
|
364
459
|
) -> Axes:
|
|
365
460
|
"""Plot the raster on a matplotlib axis."""
|
|
461
|
+
if not MATPLOTLIB_INSTALLED:
|
|
462
|
+
msg = "The 'matplotlib' package is required for 'plot()'."
|
|
463
|
+
raise ImportError(msg)
|
|
464
|
+
|
|
465
|
+
from matplotlib import pyplot as plt
|
|
466
|
+
from mpl_toolkits.axes_grid1 import make_axes_locatable
|
|
467
|
+
|
|
366
468
|
if ax is None:
|
|
367
|
-
_,
|
|
368
|
-
|
|
469
|
+
_, _ax = plt.subplots()
|
|
470
|
+
_ax: Axes
|
|
471
|
+
ax = _ax
|
|
369
472
|
|
|
370
473
|
if basemap:
|
|
371
474
|
msg = "Basemap plotting is not yet implemented."
|
|
@@ -387,8 +490,8 @@ class RasterModel(BaseModel):
|
|
|
387
490
|
max_y_nonzero = np.max(y_nonzero)
|
|
388
491
|
|
|
389
492
|
# Transform to raster CRS
|
|
390
|
-
x1, y1 = self.raster_meta.transform * (min_x_nonzero, min_y_nonzero)
|
|
391
|
-
x2, y2 = self.raster_meta.transform * (max_x_nonzero, max_y_nonzero)
|
|
493
|
+
x1, y1 = self.raster_meta.transform * (min_x_nonzero, min_y_nonzero) # type: ignore[reportAssignmentType] overloaded tuple size in affine
|
|
494
|
+
x2, y2 = self.raster_meta.transform * (max_x_nonzero, max_y_nonzero) # type: ignore[reportAssignmentType]
|
|
392
495
|
xmin, xmax = sorted([x1, x2])
|
|
393
496
|
ymin, ymax = sorted([y1, y2])
|
|
394
497
|
|
|
@@ -409,11 +512,14 @@ class RasterModel(BaseModel):
|
|
|
409
512
|
divider = make_axes_locatable(ax)
|
|
410
513
|
cax = divider.append_axes("right", size="5%", pad=0.05)
|
|
411
514
|
fig = ax.get_figure()
|
|
412
|
-
fig
|
|
515
|
+
if fig is not None:
|
|
516
|
+
fig.colorbar(img, label=cbar_label, cax=cax)
|
|
413
517
|
return ax
|
|
414
518
|
|
|
415
519
|
def as_geodataframe(self, name: str = "value") -> gpd.GeoDataFrame:
|
|
416
520
|
"""Create a GeoDataFrame representation of the raster."""
|
|
521
|
+
import geopandas as gpd
|
|
522
|
+
|
|
417
523
|
polygons = create_fishnet(bounds=self.bounds, res=self.raster_meta.cell_size)
|
|
418
524
|
point_tuples = [polygon.centroid.coords[0] for polygon in polygons]
|
|
419
525
|
raster_gdf = gpd.GeoDataFrame(
|
|
@@ -482,6 +588,43 @@ class RasterModel(BaseModel):
|
|
|
482
588
|
raster_meta = RasterMeta.example()
|
|
483
589
|
return cls(arr=arr, raster_meta=raster_meta)
|
|
484
590
|
|
|
591
|
+
@overload
|
|
592
|
+
def apply(
|
|
593
|
+
self,
|
|
594
|
+
func: Callable[[np.ndarray], np.ndarray],
|
|
595
|
+
*,
|
|
596
|
+
raw: Literal[True],
|
|
597
|
+
) -> Self: ...
|
|
598
|
+
@overload
|
|
599
|
+
def apply(
|
|
600
|
+
self,
|
|
601
|
+
func: Callable[[float], float] | Callable[[np.ndarray], np.ndarray],
|
|
602
|
+
*,
|
|
603
|
+
raw: Literal[False] = False,
|
|
604
|
+
) -> Self: ...
|
|
605
|
+
def apply(self, func, *, raw=False) -> Self:
|
|
606
|
+
"""Apply a function element-wise to the raster array.
|
|
607
|
+
|
|
608
|
+
Creates a new raster instance with the same metadata (CRS, transform, etc.)
|
|
609
|
+
but with the data array transformed by the provided function. The original
|
|
610
|
+
raster is not modified.
|
|
611
|
+
|
|
612
|
+
Args:
|
|
613
|
+
func: The function to apply to the raster array. If `raw` is True, this
|
|
614
|
+
function should accept and return a NumPy array. If `raw` is False,
|
|
615
|
+
this function should accept and return a single float value.
|
|
616
|
+
raw: If True, the function is applied directly to the entire array at
|
|
617
|
+
once. If False, the function is applied element-wise to each cell
|
|
618
|
+
in the array using `np.vectorize()`. Default is False.
|
|
619
|
+
"""
|
|
620
|
+
new_raster = self.model_copy()
|
|
621
|
+
if raw:
|
|
622
|
+
new_arr = func(self.arr)
|
|
623
|
+
else:
|
|
624
|
+
new_arr = np.vectorize(func)(self.arr)
|
|
625
|
+
new_raster.arr = np.asarray(new_arr)
|
|
626
|
+
return new_raster
|
|
627
|
+
|
|
485
628
|
def fillna(self, value: float) -> Self:
|
|
486
629
|
"""Fill NaN values in the raster with a specified value.
|
|
487
630
|
|
|
@@ -493,41 +636,42 @@ class RasterModel(BaseModel):
|
|
|
493
636
|
return new_raster
|
|
494
637
|
|
|
495
638
|
def get_xy(self) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
|
|
496
|
-
"""Get the x and y coordinates of the raster in meshgrid format.
|
|
497
|
-
col_idx, row_idx = np.meshgrid(
|
|
498
|
-
np.arange(self.arr.shape[1]),
|
|
499
|
-
np.arange(self.arr.shape[0]),
|
|
500
|
-
)
|
|
639
|
+
"""Get the x and y coordinates of the raster cell centres in meshgrid format.
|
|
501
640
|
|
|
502
|
-
|
|
503
|
-
|
|
641
|
+
Returns the coordinates of the cell centres as two separate 2D arrays in
|
|
642
|
+
meshgrid format, where each array has the same shape as the raster data array.
|
|
504
643
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
644
|
+
Returns:
|
|
645
|
+
A tuple of (x, y) coordinate arrays where:
|
|
646
|
+
- x: 2D array of x-coordinates of cell centres
|
|
647
|
+
- y: 2D array of y-coordinates of cell centres
|
|
648
|
+
Both arrays have the same shape as the raster data array.
|
|
649
|
+
"""
|
|
650
|
+
coords = self.raster_meta.get_cell_centre_coords(self.arr.shape)
|
|
651
|
+
return coords[:, :, 0], coords[:, :, 1]
|
|
511
652
|
|
|
512
653
|
def contour(
|
|
513
|
-
self,
|
|
654
|
+
self, levels: list[float] | NDArray, *, smoothing: bool = True
|
|
514
655
|
) -> gpd.GeoDataFrame:
|
|
515
656
|
"""Create contour lines from the raster data, optionally with smoothing.
|
|
516
657
|
|
|
517
|
-
The contour lines are returned as a GeoDataFrame with the contours
|
|
518
|
-
|
|
658
|
+
The contour lines are returned as a GeoDataFrame with the contours dissolved
|
|
659
|
+
by level, resulting in one row per contour level. Each row contains a
|
|
660
|
+
(Multi)LineString geometry representing all contour lines for that level,
|
|
661
|
+
and the contour level value in a column named 'level'.
|
|
519
662
|
|
|
520
663
|
Consider calling `blur()` before this method to smooth the raster data before
|
|
521
664
|
contouring, to denoise the contours.
|
|
522
665
|
|
|
523
666
|
Args:
|
|
524
|
-
levels: A list of contour levels to generate. The contour lines
|
|
525
|
-
generated for each level in this
|
|
667
|
+
levels: A list or array of contour levels to generate. The contour lines
|
|
668
|
+
will be generated for each level in this sequence.
|
|
526
669
|
smoothing: Defaults to true, which corresponds to applying a smoothing
|
|
527
670
|
algorithm to the contour lines. At the moment, this is the
|
|
528
671
|
Catmull-Rom spline algorithm. If set to False, the raw
|
|
529
672
|
contours will be returned without any smoothing.
|
|
530
673
|
"""
|
|
674
|
+
import geopandas as gpd
|
|
531
675
|
|
|
532
676
|
all_levels = []
|
|
533
677
|
all_geoms = []
|
|
@@ -537,7 +681,7 @@ class RasterModel(BaseModel):
|
|
|
537
681
|
level=level,
|
|
538
682
|
)
|
|
539
683
|
|
|
540
|
-
#
|
|
684
|
+
# Construct shapely LineString objects
|
|
541
685
|
# Convert to CRS from array index coordinates to raster CRS
|
|
542
686
|
geoms = [
|
|
543
687
|
LineString(
|
|
@@ -566,7 +710,8 @@ class RasterModel(BaseModel):
|
|
|
566
710
|
crs=self.raster_meta.crs,
|
|
567
711
|
)
|
|
568
712
|
|
|
569
|
-
|
|
713
|
+
# Dissolve contours by level to merge all contour lines of the same level
|
|
714
|
+
return contour_gdf.dissolve(by="level", as_index=False)
|
|
570
715
|
|
|
571
716
|
def blur(self, sigma: float) -> Self:
|
|
572
717
|
"""Apply a Gaussian blur to the raster data.
|
|
@@ -576,6 +721,7 @@ class RasterModel(BaseModel):
|
|
|
576
721
|
coordinate distance (e.g. meters). A larger sigma results in a more
|
|
577
722
|
blurred image.
|
|
578
723
|
"""
|
|
724
|
+
from scipy.ndimage import gaussian_filter
|
|
579
725
|
|
|
580
726
|
cell_sigma = sigma / self.raster_meta.cell_size
|
|
581
727
|
|
|
@@ -605,6 +751,82 @@ class RasterModel(BaseModel):
|
|
|
605
751
|
|
|
606
752
|
return raster
|
|
607
753
|
|
|
754
|
+
def crop(
|
|
755
|
+
self,
|
|
756
|
+
bounds: tuple[float, float, float, float],
|
|
757
|
+
strategy: Literal["underflow", "overflow"] = "underflow",
|
|
758
|
+
) -> Self:
|
|
759
|
+
"""Crop the raster to the specified bounds as (minx, miny, maxx, maxy).
|
|
760
|
+
|
|
761
|
+
Args:
|
|
762
|
+
bounds: A tuple of (minx, miny, maxx, maxy) defining the bounds to crop to.
|
|
763
|
+
strategy: The cropping strategy to use. 'underflow' will crop the raster
|
|
764
|
+
to be fully within the bounds, ignoring any cells that are
|
|
765
|
+
partially outside the bounds. 'overflow' will instead include
|
|
766
|
+
cells that intersect the bounds, ensuring the bounds area
|
|
767
|
+
remains covered with cells.
|
|
768
|
+
|
|
769
|
+
Returns:
|
|
770
|
+
A new RasterModel instance cropped to the specified bounds.
|
|
771
|
+
"""
|
|
772
|
+
|
|
773
|
+
minx, miny, maxx, maxy = bounds
|
|
774
|
+
arr = self.arr
|
|
775
|
+
|
|
776
|
+
# Get the half cell size for cropping
|
|
777
|
+
cell_size = self.raster_meta.cell_size
|
|
778
|
+
half_cell_size = cell_size / 2
|
|
779
|
+
|
|
780
|
+
# Get the cell centre coordinates as 1D arrays
|
|
781
|
+
x_coords = self.cell_x_coords
|
|
782
|
+
y_coords = self.cell_y_coords
|
|
783
|
+
|
|
784
|
+
# Get the indices to crop the array
|
|
785
|
+
if strategy == "underflow":
|
|
786
|
+
x_idx = (x_coords >= minx + half_cell_size) & (
|
|
787
|
+
x_coords <= maxx - half_cell_size
|
|
788
|
+
)
|
|
789
|
+
y_idx = (y_coords >= miny + half_cell_size) & (
|
|
790
|
+
y_coords <= maxy - half_cell_size
|
|
791
|
+
)
|
|
792
|
+
elif strategy == "overflow":
|
|
793
|
+
x_idx = (x_coords > minx - half_cell_size) & (
|
|
794
|
+
x_coords < maxx + half_cell_size
|
|
795
|
+
)
|
|
796
|
+
y_idx = (y_coords > miny - half_cell_size) & (
|
|
797
|
+
y_coords < maxy + half_cell_size
|
|
798
|
+
)
|
|
799
|
+
else:
|
|
800
|
+
msg = f"Unsupported cropping strategy: {strategy}"
|
|
801
|
+
raise NotImplementedError(msg)
|
|
802
|
+
|
|
803
|
+
# Crop the array
|
|
804
|
+
cropped_arr = arr[np.ix_(y_idx, x_idx)]
|
|
805
|
+
|
|
806
|
+
# Check the shape of the cropped array
|
|
807
|
+
if cropped_arr.size == 0:
|
|
808
|
+
msg = "Cropped array is empty; no cells within the specified bounds."
|
|
809
|
+
raise ValueError(msg)
|
|
810
|
+
|
|
811
|
+
# Recalculate the transform for the cropped raster
|
|
812
|
+
x_coords = x_coords[x_idx]
|
|
813
|
+
y_coords = y_coords[y_idx]
|
|
814
|
+
transform = rasterio.transform.from_bounds(
|
|
815
|
+
west=x_coords.min() - half_cell_size,
|
|
816
|
+
south=y_coords.min() - half_cell_size,
|
|
817
|
+
east=x_coords.max() + half_cell_size,
|
|
818
|
+
north=y_coords.max() + half_cell_size,
|
|
819
|
+
width=cropped_arr.shape[1],
|
|
820
|
+
height=cropped_arr.shape[0],
|
|
821
|
+
)
|
|
822
|
+
|
|
823
|
+
# Update the raster
|
|
824
|
+
cls = self.__class__
|
|
825
|
+
new_meta = RasterMeta(
|
|
826
|
+
cell_size=cell_size, crs=self.raster_meta.crs, transform=transform
|
|
827
|
+
)
|
|
828
|
+
return cls(arr=cropped_arr, raster_meta=new_meta)
|
|
829
|
+
|
|
608
830
|
def resample(
|
|
609
831
|
self, new_cell_size: float, *, method: Literal["bilinear"] = "bilinear"
|
|
610
832
|
) -> Self:
|
|
@@ -654,9 +876,24 @@ class RasterModel(BaseModel):
|
|
|
654
876
|
|
|
655
877
|
@field_validator("arr")
|
|
656
878
|
@classmethod
|
|
657
|
-
def check_2d_array(cls, v:
|
|
879
|
+
def check_2d_array(cls, v: NDArray) -> NDArray:
|
|
658
880
|
"""Validator to ensure the cell array is 2D."""
|
|
659
881
|
if v.ndim != 2:
|
|
660
882
|
msg = "Cell array must be 2D"
|
|
661
883
|
raise RasterCellArrayShapeError(msg)
|
|
662
884
|
return v
|
|
885
|
+
|
|
886
|
+
|
|
887
|
+
def _get_xy_tuple(xy: Any) -> tuple[float, float]:
|
|
888
|
+
"""Convert Point or coordinate tuple to coordinate tuple.
|
|
889
|
+
|
|
890
|
+
Args:
|
|
891
|
+
xy: Either a coordinate tuple or a shapely Point object.
|
|
892
|
+
|
|
893
|
+
Returns:
|
|
894
|
+
A coordinate tuple (x, y).
|
|
895
|
+
"""
|
|
896
|
+
if isinstance(xy, Point):
|
|
897
|
+
return (xy.x, xy.y)
|
|
898
|
+
x, y = xy
|
|
899
|
+
return (float(x), float(y))
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rastr
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Geospatial Raster datatype library for Python.
|
|
5
|
+
Project-URL: Source Code, https://github.com/tonkintaylor/rastr
|
|
6
|
+
Project-URL: Bug Tracker, https://github.com/tonkintaylor/rastr/issues
|
|
7
|
+
Project-URL: Releases, https://github.com/tonkintaylor/rastr/releases
|
|
8
|
+
Project-URL: Source Archive, https://github.com/tonkintaylor/rastr/archive/336916af169603534dd0728c10401667f263d98a.zip
|
|
9
|
+
Author-email: Tonkin & Taylor Limited <Sub-DisciplineData+AnalyticsStaff@tonkintaylor.co.nz>, Nathan McDougall <nmcdougall@tonkintaylor.co.nz>, Ben Karl <bkarl@tonkintaylor.co.nz>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Requires-Dist: affine>=2.4.0
|
|
19
|
+
Requires-Dist: branca>=0.8.1
|
|
20
|
+
Requires-Dist: folium>=0.20.0
|
|
21
|
+
Requires-Dist: geopandas>=1.1.1
|
|
22
|
+
Requires-Dist: matplotlib>=3.10.5
|
|
23
|
+
Requires-Dist: numpy>=2.2.6
|
|
24
|
+
Requires-Dist: pandas>=2.3.1
|
|
25
|
+
Requires-Dist: pydantic>=2.11.7
|
|
26
|
+
Requires-Dist: pyproj>=3.7.1
|
|
27
|
+
Requires-Dist: rasterio>=1.4.3
|
|
28
|
+
Requires-Dist: scikit-image>=0.25.2
|
|
29
|
+
Requires-Dist: scipy>=1.15.3
|
|
30
|
+
Requires-Dist: shapely>=2.1.1
|
|
31
|
+
Requires-Dist: tqdm>=4.67.1
|
|
32
|
+
Requires-Dist: typing-extensions>=4.14.1
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
<h1 align="center">
|
|
36
|
+
<img src="https://raw.githubusercontent.com/tonkintaylor/rastr/refs/heads/develop/docs/logo.svg"><br>
|
|
37
|
+
</h1>
|
|
38
|
+
|
|
39
|
+
# rastr
|
|
40
|
+
|
|
41
|
+
[](<https://pypi.python.org/pypi/rastr>)
|
|
42
|
+
[](https://github.com/astral-sh/uv)
|
|
43
|
+
[](https://github.com/astral-sh/ruff)
|
|
44
|
+
[](https://github.com/usethis-python/usethis-python)
|
|
45
|
+
|
|
46
|
+
A lightweight geospatial raster datatype library for Python focused on simplicity.
|
|
47
|
+
|
|
48
|
+
## Overview
|
|
49
|
+
|
|
50
|
+
`rastr` provides an intuitive interface for creating, reading, manipulating, and exporting geospatial raster data in Python.
|
|
51
|
+
|
|
52
|
+
### Features
|
|
53
|
+
|
|
54
|
+
- 🧮 **Complete raster arithmetic**: Full support for mathematical operations (`+`, `-`, `*`, `/`) between rasters and scalars.
|
|
55
|
+
- 📊 **Flexible visualization**: Built-in plotting with matplotlib and interactive mapping with folium.
|
|
56
|
+
- 🗺️ **Geospatial analysis tools**: Contour generation, Gaussian blurring, and spatial sampling.
|
|
57
|
+
- 🛠️ **Data manipulation**: Fill NaN values, extrapolate missing data, and resample to different resolutions.
|
|
58
|
+
- 🔗 **Seamless integration**: Works with GeoPandas, rasterio, and the broader Python geospatial ecosystem.
|
|
59
|
+
- ↔️ **Vector-to-raster workflows**: Convert GeoDataFrame polygons, points, and lines to raster format.
|
|
60
|
+
|
|
61
|
+
## Installation
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# With uv
|
|
65
|
+
uv add rastr
|
|
66
|
+
|
|
67
|
+
# With pip
|
|
68
|
+
pip install rastr
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Quick Start
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from pyproj.crs.crs import CRS
|
|
75
|
+
from rasterio.transform import from_origin
|
|
76
|
+
from rastr.create import full_raster
|
|
77
|
+
from rastr.meta import RasterMeta
|
|
78
|
+
from rastr.raster import RasterModel
|
|
79
|
+
|
|
80
|
+
# Create an example raster
|
|
81
|
+
raster = RasterModel.example()
|
|
82
|
+
|
|
83
|
+
# Basic arithmetic operations
|
|
84
|
+
doubled = raster * 2
|
|
85
|
+
summed = raster + 10
|
|
86
|
+
combined = raster + doubled
|
|
87
|
+
|
|
88
|
+
# Create full rasters with specified values
|
|
89
|
+
cell_size = 1.0
|
|
90
|
+
empty_raster = full_raster(
|
|
91
|
+
RasterMeta(
|
|
92
|
+
cell_size=cell_size,
|
|
93
|
+
crs=CRS.from_epsg(2193),
|
|
94
|
+
transform=from_origin(0, 100, cell_size, cell_size),
|
|
95
|
+
),
|
|
96
|
+
bounds=(0, 0, 100, 100),
|
|
97
|
+
fill_value=0.0,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Visualize the data
|
|
101
|
+
ax = raster.plot(cbar_label="Values")
|
|
102
|
+
|
|
103
|
+
# Interactive web mapping (requires folium)
|
|
104
|
+
m = raster.explore(opacity=0.8, colormap="plasma")
|
|
105
|
+
|
|
106
|
+
# Sample values at specific coordinates
|
|
107
|
+
xy_points = [(100.0, 200.0), (150.0, 250.0)]
|
|
108
|
+
values = raster.sample(xy_points)
|
|
109
|
+
|
|
110
|
+
# Generate contour lines
|
|
111
|
+
contours = raster.contour(levels=[0.1, 0.5, 0.9], smoothing=True)
|
|
112
|
+
|
|
113
|
+
# Apply spatial operations
|
|
114
|
+
blurred = raster.blur(sigma=2.0) # Gaussian blur
|
|
115
|
+
filled = raster.extrapolate(method="nearest") # Fill NaN values via nearest-neighbours
|
|
116
|
+
resampled = raster.resample(new_cell_size=0.5) # Change resolution
|
|
117
|
+
|
|
118
|
+
# Export to file
|
|
119
|
+
raster.to_file("output.tif")
|
|
120
|
+
|
|
121
|
+
# Convert to GeoDataFrame for vector analysis
|
|
122
|
+
gdf = raster.as_geodataframe(name="elevation")
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Limitations
|
|
126
|
+
|
|
127
|
+
Current version limitations:
|
|
128
|
+
|
|
129
|
+
- Only Single-band rasters are supported.
|
|
130
|
+
- In-memory processing only (streaming support planned).
|
|
131
|
+
- Square cells only (rectangular cell support planned).
|
|
132
|
+
- Only float dtypes (integer support planned).
|
|
133
|
+
|
|
134
|
+
### Contributing
|
|
135
|
+
|
|
136
|
+
See the
|
|
137
|
+
[CONTRIBUTING.md](https://github.com/usethis-python/usethis-python/blob/main/CONTRIBUTING.md)
|
|
138
|
+
file.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
rastr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
rastr/_version.py,sha256=2_0GUP7yBCXRus-qiJKxQD62z172WSs1sQ6DVpPsbmM,704
|
|
3
|
+
rastr/create.py,sha256=6aHpRFRXmpXzuzTt-SxY_BfVc7dXKCBLCArb7DjUrsM,13494
|
|
4
|
+
rastr/io.py,sha256=RgkiV_emOPjTFeI2a1aCBtfWwrSLH0XmP8rx9tu6PAI,2952
|
|
5
|
+
rastr/meta.py,sha256=5iDvGkYe8iMMkPV6gSL04jNcLRhuRNFqe9AppUpp55E,2928
|
|
6
|
+
rastr/raster.py,sha256=I9CQB2NYUOFvvpdzNsQcDBdgLXDCjB7ONFxDAq_6_SU,31995
|
|
7
|
+
rastr/arr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
rastr/arr/fill.py,sha256=80ucb36el9s042fDSwK1SZJhp_GNJNMM0fpQTWmJvgE,1001
|
|
9
|
+
rastr/gis/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
rastr/gis/fishnet.py,sha256=Ic-0HV61ST8OxwhyoMyV_ybihs2xuhgAY-3n4CknAt8,2670
|
|
11
|
+
rastr/gis/smooth.py,sha256=3HQDQHQM5_LeNk21R8Eb8VpF727JcXq21HO9JMvcpW4,4810
|
|
12
|
+
rastr-0.4.0.dist-info/METADATA,sha256=GjZF16Tmxjz3WUbEFhAB1jW19AecpNkJpk__YNSamQQ,4953
|
|
13
|
+
rastr-0.4.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
14
|
+
rastr-0.4.0.dist-info/licenses/LICENSE,sha256=7qUsx93G2ATTRLZiSYuQofwAX_uWvrqnAiMK8PzxvNc,1080
|
|
15
|
+
rastr-0.4.0.dist-info/RECORD,,
|
rastr-0.2.0.dist-info/METADATA
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: rastr
|
|
3
|
-
Version: 0.2.0
|
|
4
|
-
Summary: Geospatial Raster datatype library for Python.
|
|
5
|
-
Project-URL: Source Code, https://github.com/tonkintaylor/rastr
|
|
6
|
-
Project-URL: Bug Tracker, https://github.com/tonkintaylor/rastr/issues
|
|
7
|
-
Project-URL: Releases, https://github.com/tonkintaylor/rastr/releases
|
|
8
|
-
Project-URL: Source Archive, https://github.com/tonkintaylor/rastr/archive/551d36604b84f947fffbe328e9e877732c9e35fb.zip
|
|
9
|
-
Author-email: Tonkin & Taylor Limited <Sub-DisciplineData+AnalyticsStaff@tonkintaylor.co.nz>, Nathan McDougall <nmcdougall@tonkintaylor.co.nz>, Ben Karl <bkarl@tonkintaylor.co.nz>
|
|
10
|
-
License-Expression: MIT
|
|
11
|
-
License-File: LICENSE
|
|
12
|
-
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
-
Requires-Python: >=3.10
|
|
18
|
-
Requires-Dist: affine>=2.4.0
|
|
19
|
-
Requires-Dist: folium>=0.20.0
|
|
20
|
-
Requires-Dist: geopandas>=1.1.1
|
|
21
|
-
Requires-Dist: matplotlib>=3.10.5
|
|
22
|
-
Requires-Dist: numpy>=2.2.6
|
|
23
|
-
Requires-Dist: pandas>=2.3.1
|
|
24
|
-
Requires-Dist: pydantic>=2.11.7
|
|
25
|
-
Requires-Dist: pyproj>=3.7.1
|
|
26
|
-
Requires-Dist: rasterio>=1.4.3
|
|
27
|
-
Requires-Dist: scikit-image>=0.25.2
|
|
28
|
-
Requires-Dist: scipy>=1.15.3
|
|
29
|
-
Requires-Dist: shapely>=2.1.1
|
|
30
|
-
Requires-Dist: tqdm>=4.67.1
|
|
31
|
-
Requires-Dist: typing-extensions>=4.14.1
|
|
32
|
-
Requires-Dist: xyzservices>=2025.4.0
|
|
33
|
-
Description-Content-Type: text/markdown
|
|
34
|
-
|
|
35
|
-
# Rastr
|
|
36
|
-
|
|
37
|
-
[](<https://pypi.python.org/pypi/rastr>)
|
|
38
|
-
[](https://github.com/astral-sh/uv)
|
|
39
|
-
[](https://github.com/astral-sh/ruff)
|
|
40
|
-
[](https://github.com/usethis-python/usethis-python)
|
|
41
|
-
|
|
42
|
-
Geospatial Raster datatype library for Python.
|
|
43
|
-
|
|
44
|
-
Currently, only single-banded, in-memory rasters with square cells are supported.
|
rastr-0.2.0.dist-info/RECORD
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
rastr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
rastr/_version.py,sha256=iB5DfB5V6YB5Wo4JmvS-txT42QtmGaWcWp3udRT7zCI,511
|
|
3
|
-
rastr/create.py,sha256=tHLVnGarMt04p1z8CVknMWMjQLKrb0WrcP_Wgdw8xr4,9346
|
|
4
|
-
rastr/io.py,sha256=dKOY5JYQDULg3Si24cysVTZuWoPjftAE8pbxgSoN1tw,967
|
|
5
|
-
rastr/meta.py,sha256=jmDDx3w61ZZ3dQSrMFgaBx0LJxYp42xTm_7uS_s96lg,1547
|
|
6
|
-
rastr/raster.py,sha256=jGk9buG1uKQ3tb3ciI4yhw9JFu7oyXWXMnUBnPjOD54,23142
|
|
7
|
-
rastr/arr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
-
rastr/arr/fill.py,sha256=7N3ECMli7ssNJJk5qgDoj_3xExgu03nohGPgUKWxcCk,903
|
|
9
|
-
rastr/gis/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
-
rastr/gis/fishnet.py,sha256=LZqtI9cgYPacuWNfIfdbTkRMRSnJCQdYlaT2eVPmorM,2459
|
|
11
|
-
rastr/gis/smooth.py,sha256=LbWvAG1O-O5H6P5LrbwD03mbnXVYjkH1re-_iZ4arIU,4754
|
|
12
|
-
rastr-0.2.0.dist-info/METADATA,sha256=7uUi8aLMaEbxoPfk6go_szv3H_TnDGYGE4RSC_sbUqo,2191
|
|
13
|
-
rastr-0.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
14
|
-
rastr-0.2.0.dist-info/licenses/LICENSE,sha256=7qUsx93G2ATTRLZiSYuQofwAX_uWvrqnAiMK8PzxvNc,1080
|
|
15
|
-
rastr-0.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|