ssb-sgis 1.0.1__py3-none-any.whl → 1.0.3__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.
- sgis/__init__.py +107 -121
- sgis/exceptions.py +5 -3
- sgis/geopandas_tools/__init__.py +1 -0
- sgis/geopandas_tools/bounds.py +86 -47
- sgis/geopandas_tools/buffer_dissolve_explode.py +62 -39
- sgis/geopandas_tools/centerlines.py +53 -44
- sgis/geopandas_tools/cleaning.py +87 -104
- sgis/geopandas_tools/conversion.py +164 -107
- sgis/geopandas_tools/duplicates.py +33 -19
- sgis/geopandas_tools/general.py +84 -52
- sgis/geopandas_tools/geometry_types.py +24 -10
- sgis/geopandas_tools/neighbors.py +23 -11
- sgis/geopandas_tools/overlay.py +136 -53
- sgis/geopandas_tools/point_operations.py +11 -10
- sgis/geopandas_tools/polygon_operations.py +53 -61
- sgis/geopandas_tools/polygons_as_rings.py +121 -78
- sgis/geopandas_tools/sfilter.py +17 -17
- sgis/helpers.py +116 -58
- sgis/io/dapla_functions.py +32 -23
- sgis/io/opener.py +13 -6
- sgis/io/read_parquet.py +2 -2
- sgis/maps/examine.py +55 -28
- sgis/maps/explore.py +471 -112
- sgis/maps/httpserver.py +12 -12
- sgis/maps/legend.py +285 -134
- sgis/maps/map.py +248 -129
- sgis/maps/maps.py +123 -119
- sgis/maps/thematicmap.py +260 -94
- sgis/maps/tilesources.py +3 -8
- sgis/networkanalysis/_get_route.py +5 -4
- sgis/networkanalysis/_od_cost_matrix.py +44 -1
- sgis/networkanalysis/_points.py +10 -4
- sgis/networkanalysis/_service_area.py +5 -2
- sgis/networkanalysis/closing_network_holes.py +22 -64
- sgis/networkanalysis/cutting_lines.py +58 -46
- sgis/networkanalysis/directednetwork.py +16 -8
- sgis/networkanalysis/finding_isolated_networks.py +6 -5
- sgis/networkanalysis/network.py +15 -13
- sgis/networkanalysis/networkanalysis.py +79 -61
- sgis/networkanalysis/networkanalysisrules.py +21 -17
- sgis/networkanalysis/nodes.py +2 -3
- sgis/networkanalysis/traveling_salesman.py +6 -3
- sgis/parallel/parallel.py +372 -142
- sgis/raster/base.py +9 -3
- sgis/raster/cube.py +331 -213
- sgis/raster/cubebase.py +15 -29
- sgis/raster/image_collection.py +2560 -0
- sgis/raster/indices.py +17 -12
- sgis/raster/raster.py +356 -275
- sgis/raster/sentinel_config.py +104 -0
- sgis/raster/zonal.py +38 -14
- {ssb_sgis-1.0.1.dist-info → ssb_sgis-1.0.3.dist-info}/LICENSE +1 -1
- {ssb_sgis-1.0.1.dist-info → ssb_sgis-1.0.3.dist-info}/METADATA +87 -16
- ssb_sgis-1.0.3.dist-info/RECORD +61 -0
- {ssb_sgis-1.0.1.dist-info → ssb_sgis-1.0.3.dist-info}/WHEEL +1 -1
- sgis/raster/bands.py +0 -48
- sgis/raster/gradient.py +0 -78
- sgis/raster/methods_as_functions.py +0 -124
- sgis/raster/torchgeo.py +0 -150
- ssb_sgis-1.0.1.dist-info/RECORD +0 -63
sgis/raster/raster.py
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import functools
|
|
2
2
|
import numbers
|
|
3
|
+
import os
|
|
3
4
|
import re
|
|
4
5
|
import warnings
|
|
5
|
-
from collections.abc import Callable
|
|
6
|
-
from
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from collections.abc import Iterable
|
|
8
|
+
from collections.abc import Iterator
|
|
9
|
+
from copy import copy
|
|
10
|
+
from copy import deepcopy
|
|
7
11
|
from json import loads
|
|
8
12
|
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
from typing import ClassVar
|
|
9
15
|
|
|
10
16
|
import geopandas as gpd
|
|
11
17
|
import matplotlib.pyplot as plt
|
|
@@ -13,17 +19,25 @@ import numpy as np
|
|
|
13
19
|
import pandas as pd
|
|
14
20
|
import pyproj
|
|
15
21
|
import rasterio
|
|
22
|
+
import rasterio.windows
|
|
16
23
|
import shapely
|
|
17
24
|
from typing_extensions import Self # TODO: imperter fra typing når python 3.11
|
|
18
25
|
|
|
19
|
-
|
|
20
26
|
try:
|
|
21
27
|
import xarray as xr
|
|
22
28
|
from xarray import DataArray
|
|
23
29
|
except ImportError:
|
|
24
30
|
|
|
25
31
|
class DataArray:
|
|
26
|
-
|
|
32
|
+
"""Placeholder."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
from dapla.gcs import GCSFileSystem
|
|
37
|
+
except ImportError:
|
|
38
|
+
|
|
39
|
+
class GCSFileSystem:
|
|
40
|
+
"""Placeholder."""
|
|
27
41
|
|
|
28
42
|
|
|
29
43
|
try:
|
|
@@ -31,7 +45,8 @@ try:
|
|
|
31
45
|
except ImportError:
|
|
32
46
|
pass
|
|
33
47
|
from affine import Affine
|
|
34
|
-
from geopandas import GeoDataFrame
|
|
48
|
+
from geopandas import GeoDataFrame
|
|
49
|
+
from geopandas import GeoSeries
|
|
35
50
|
from pandas.api.types import is_list_like
|
|
36
51
|
from rasterio import features
|
|
37
52
|
from rasterio.enums import MergeAlg
|
|
@@ -39,22 +54,26 @@ from rasterio.io import DatasetReader
|
|
|
39
54
|
from rasterio.vrt import WarpedVRT
|
|
40
55
|
from rasterio.warp import reproject
|
|
41
56
|
from shapely import Geometry
|
|
42
|
-
from shapely.geometry import Point
|
|
43
|
-
|
|
44
|
-
from
|
|
45
|
-
|
|
57
|
+
from shapely.geometry import Point
|
|
58
|
+
from shapely.geometry import Polygon
|
|
59
|
+
from shapely.geometry import shape
|
|
60
|
+
|
|
61
|
+
from ..geopandas_tools.conversion import to_bbox
|
|
62
|
+
from ..geopandas_tools.conversion import to_gdf
|
|
63
|
+
from ..geopandas_tools.conversion import to_shapely
|
|
64
|
+
from ..geopandas_tools.general import is_bbox_like
|
|
65
|
+
from ..geopandas_tools.general import is_wkt
|
|
46
66
|
from ..helpers import is_property
|
|
47
67
|
from ..io.opener import opener
|
|
48
|
-
from .base import ALLOWED_KEYS
|
|
49
|
-
from .
|
|
50
|
-
from .
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
68
|
+
from .base import ALLOWED_KEYS
|
|
69
|
+
from .base import NESSECARY_META
|
|
70
|
+
from .base import get_index_mapper
|
|
71
|
+
from .base import memfile_from_array
|
|
72
|
+
from .zonal import _aggregate
|
|
73
|
+
from .zonal import _make_geometry_iterrows
|
|
74
|
+
from .zonal import _no_overlap_df
|
|
75
|
+
from .zonal import _prepare_zonal
|
|
76
|
+
from .zonal import _zonal_post
|
|
58
77
|
|
|
59
78
|
numpy_func_message = (
|
|
60
79
|
"aggfunc must be functions or strings of numpy functions or methods."
|
|
@@ -68,11 +87,11 @@ class Raster:
|
|
|
68
87
|
'from_gdf'.
|
|
69
88
|
|
|
70
89
|
|
|
71
|
-
Examples
|
|
72
|
-
|
|
73
|
-
|
|
90
|
+
Examples:
|
|
91
|
+
---------
|
|
74
92
|
Read tif file.
|
|
75
93
|
|
|
94
|
+
>>> import sgis as sg
|
|
76
95
|
>>> path = 'https://media.githubusercontent.com/media/statisticsnorway/ssb-sgis/main/tests/testdata/raster/dtm_10.tif'
|
|
77
96
|
>>> raster = sg.Raster.from_path(path)
|
|
78
97
|
>>> raster
|
|
@@ -83,8 +102,8 @@ class Raster:
|
|
|
83
102
|
The array is stored in the array attribute.
|
|
84
103
|
|
|
85
104
|
>>> raster.load()
|
|
86
|
-
>>> raster.
|
|
87
|
-
>>> raster.
|
|
105
|
+
>>> raster.values[raster.values < 0] = 0
|
|
106
|
+
>>> raster.values
|
|
88
107
|
[[[ 0. 0. 0. ... 158.4 155.6 152.6]
|
|
89
108
|
[ 0. 0. 0. ... 158. 154.8 151.9]
|
|
90
109
|
[ 0. 0. 0. ... 158.5 155.1 152.3]
|
|
@@ -147,14 +166,8 @@ class Raster:
|
|
|
147
166
|
|
|
148
167
|
"""
|
|
149
168
|
|
|
150
|
-
# attributes
|
|
151
|
-
|
|
152
|
-
date_format: str | None = None
|
|
153
|
-
contains: str | None = None
|
|
154
|
-
endswith: str = ".tif"
|
|
155
|
-
|
|
156
|
-
# attributes conserning rasterio metadata
|
|
157
|
-
_profile = {
|
|
169
|
+
# attributes concerning rasterio metadata
|
|
170
|
+
_profile: ClassVar[dict[str, str | None]] = {
|
|
158
171
|
"driver": "GTiff",
|
|
159
172
|
"compress": "LZW",
|
|
160
173
|
"nodata": None,
|
|
@@ -164,43 +177,58 @@ class Raster:
|
|
|
164
177
|
"indexes": None,
|
|
165
178
|
}
|
|
166
179
|
|
|
167
|
-
# driver: str = "GTiff"
|
|
168
|
-
# compress: str = "LZW"
|
|
169
|
-
# _nodata: int | float | None = None
|
|
170
|
-
# _dtype: type | None = None
|
|
171
|
-
|
|
172
180
|
def __init__(
|
|
173
181
|
self,
|
|
174
|
-
|
|
182
|
+
data: Self | str | np.ndarray | None = None,
|
|
175
183
|
*,
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
array: np.ndarray | None = None,
|
|
179
|
-
file_system=None,
|
|
184
|
+
file_system: GCSFileSystem | None = None,
|
|
185
|
+
filename_regex: str | None = None,
|
|
180
186
|
**kwargs,
|
|
181
|
-
):
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
187
|
+
) -> None:
|
|
188
|
+
"""Note: use the classmethods from_path, from_array, from_gdf etc. instead of the initialiser.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
data: A file path, an array or a Raster object.
|
|
192
|
+
file_system: Optional GCSFileSystem.
|
|
193
|
+
filename_regex: Regular expression to match file name attributes (date, band, tile, resolution).
|
|
194
|
+
**kwargs: Arguments concerning file metadata or
|
|
195
|
+
spatial properties of the image.
|
|
196
|
+
"""
|
|
197
|
+
warnings.warn("This class is deprecated in favor of Band", stacklevel=1)
|
|
198
|
+
self.filename_regex = filename_regex
|
|
199
|
+
if filename_regex:
|
|
200
|
+
self.filename_pattern = re.compile(self.filename_regex, re.VERBOSE)
|
|
201
|
+
else:
|
|
202
|
+
self.filename_pattern = None
|
|
203
|
+
|
|
204
|
+
if isinstance(data, Raster):
|
|
205
|
+
for key, value in data.__dict__.items():
|
|
206
|
+
setattr(data, key, value)
|
|
189
207
|
return
|
|
190
208
|
|
|
191
|
-
if
|
|
209
|
+
if isinstance(data, (str | Path | os.PathLike)):
|
|
210
|
+
self.path = data
|
|
211
|
+
|
|
212
|
+
else:
|
|
213
|
+
self.path = None
|
|
214
|
+
|
|
215
|
+
if isinstance(data, (np.ndarray)):
|
|
216
|
+
self.values = data
|
|
217
|
+
else:
|
|
218
|
+
self.values = None
|
|
219
|
+
|
|
220
|
+
if self.path is None and not any(
|
|
221
|
+
[kwargs.get("transform"), kwargs.get("bounds")]
|
|
222
|
+
):
|
|
192
223
|
raise TypeError(
|
|
193
224
|
"Must specify either bounds or transform when constructing raster from array."
|
|
194
225
|
)
|
|
195
226
|
|
|
196
|
-
# add class profile first
|
|
227
|
+
# add class profile first, then override with args and kwargs
|
|
197
228
|
self.update(**self._profile)
|
|
198
229
|
|
|
199
230
|
self._crs = kwargs.pop("crs", self._crs if hasattr(self, "_crs") else None)
|
|
200
231
|
self._bounds = None
|
|
201
|
-
|
|
202
|
-
self.path = path
|
|
203
|
-
self.array = array
|
|
204
232
|
self.file_system = file_system
|
|
205
233
|
self._indexes = self._get_indexes(kwargs.pop("indexes", self.indexes))
|
|
206
234
|
|
|
@@ -220,21 +248,28 @@ class Raster:
|
|
|
220
248
|
cls,
|
|
221
249
|
path: str,
|
|
222
250
|
res: int | None = None,
|
|
223
|
-
file_system=None,
|
|
251
|
+
file_system: GCSFileSystem | None = None,
|
|
252
|
+
filename_regex: str | None = None,
|
|
224
253
|
**kwargs,
|
|
225
|
-
):
|
|
254
|
+
) -> Self:
|
|
226
255
|
"""Construct Raster from file path.
|
|
227
256
|
|
|
228
257
|
Args:
|
|
229
258
|
path: Path to a raster image file.
|
|
259
|
+
res: Spatial resolution when reading the image.
|
|
260
|
+
file_system: Optional file system.
|
|
261
|
+
filename_regex: Regular expression with optional match groups.
|
|
262
|
+
**kwargs: Arguments concerning file metadata or
|
|
263
|
+
spatial properties of the image.
|
|
230
264
|
|
|
231
265
|
Returns:
|
|
232
266
|
A Raster instance.
|
|
233
267
|
"""
|
|
234
268
|
return cls(
|
|
235
|
-
|
|
269
|
+
str(path),
|
|
236
270
|
file_system=file_system,
|
|
237
271
|
res=res,
|
|
272
|
+
filename_regex=filename_regex,
|
|
238
273
|
**kwargs,
|
|
239
274
|
)
|
|
240
275
|
|
|
@@ -242,19 +277,18 @@ class Raster:
|
|
|
242
277
|
def from_array(
|
|
243
278
|
cls,
|
|
244
279
|
array: np.ndarray,
|
|
245
|
-
crs,
|
|
280
|
+
crs: Any,
|
|
246
281
|
*,
|
|
247
282
|
transform: Affine | None = None,
|
|
248
283
|
bounds: tuple | Geometry | None = None,
|
|
249
284
|
copy: bool = True,
|
|
250
285
|
**kwargs,
|
|
251
|
-
):
|
|
286
|
+
) -> Self:
|
|
252
287
|
"""Construct Raster from numpy array.
|
|
253
288
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
which transform will then be created from.
|
|
289
|
+
Must also specify nessecary spatial properties
|
|
290
|
+
The necessary metadata is 'crs' and either 'transform' (Affine object)
|
|
291
|
+
or 'bounds', which transform will then be created from.
|
|
258
292
|
|
|
259
293
|
Args:
|
|
260
294
|
array: 2d or 3d numpy ndarray.
|
|
@@ -263,7 +297,9 @@ class Raster:
|
|
|
263
297
|
of bounds.
|
|
264
298
|
bounds: Minimum and maximum x and y coordinates. Can be specified instead
|
|
265
299
|
of transform.
|
|
266
|
-
|
|
300
|
+
copy: Whether to copy the array.
|
|
301
|
+
**kwargs: Arguments concerning file metadata or
|
|
302
|
+
spatial properties of the image.
|
|
267
303
|
|
|
268
304
|
Returns:
|
|
269
305
|
A Raster instance.
|
|
@@ -295,7 +331,7 @@ class Raster:
|
|
|
295
331
|
|
|
296
332
|
crs = pyproj.CRS(crs) if crs else None
|
|
297
333
|
|
|
298
|
-
return cls(array
|
|
334
|
+
return cls(array, crs=crs, transform=transform, bounds=bounds, **kwargs)
|
|
299
335
|
|
|
300
336
|
@classmethod
|
|
301
337
|
def from_gdf(
|
|
@@ -303,21 +339,38 @@ class Raster:
|
|
|
303
339
|
gdf: GeoDataFrame,
|
|
304
340
|
columns: str | Iterable[str],
|
|
305
341
|
res: int,
|
|
306
|
-
fill=0,
|
|
307
|
-
all_touched=False,
|
|
308
|
-
merge_alg=MergeAlg.replace,
|
|
309
|
-
default_value=1,
|
|
310
|
-
dtype=None,
|
|
342
|
+
fill: int = 0,
|
|
343
|
+
all_touched: bool = False,
|
|
344
|
+
merge_alg: Callable = MergeAlg.replace,
|
|
345
|
+
default_value: int = 1,
|
|
346
|
+
dtype: Any | None = None,
|
|
311
347
|
**kwargs,
|
|
312
|
-
):
|
|
348
|
+
) -> Self:
|
|
313
349
|
"""Construct Raster from a GeoDataFrame.
|
|
314
350
|
|
|
315
351
|
Args:
|
|
316
|
-
gdf: The GeoDataFrame.
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
352
|
+
gdf: The GeoDataFrame to rasterize.
|
|
353
|
+
columns: Column(s) in the GeoDataFrame whose values are used to populate the raster.
|
|
354
|
+
This can be a single column name or a list of column names.
|
|
355
|
+
res: Resolution of the raster in units of the GeoDataFrame's coordinate reference system.
|
|
356
|
+
fill: Fill value for areas outside of input geometries (default is 0).
|
|
357
|
+
all_touched: Whether to consider all pixels touched by geometries,
|
|
358
|
+
not just those whose center is within the polygon (default is False).
|
|
359
|
+
merge_alg: Merge algorithm to use when combining geometries
|
|
360
|
+
(default is 'MergeAlg.replace').
|
|
361
|
+
default_value: Default value to use for the rasterized pixels
|
|
362
|
+
(default is 1).
|
|
363
|
+
dtype: Data type of the output array. If None, it will be
|
|
364
|
+
determined automatically.
|
|
365
|
+
**kwargs: Additional keyword arguments passed to the raster
|
|
366
|
+
creation process, e.g., custom CRS or transform settings.
|
|
320
367
|
|
|
368
|
+
Returns:
|
|
369
|
+
A Raster instance based on the specified GeoDataFrame and parameters.
|
|
370
|
+
|
|
371
|
+
Raises:
|
|
372
|
+
TypeError: If 'transform' is provided in kwargs, as this is
|
|
373
|
+
computed based on the GeoDataFrame bounds and resolution.
|
|
321
374
|
"""
|
|
322
375
|
if not isinstance(gdf, GeoDataFrame):
|
|
323
376
|
gdf = to_gdf(gdf)
|
|
@@ -362,10 +415,10 @@ class Raster:
|
|
|
362
415
|
assert len(array.shape) == 3
|
|
363
416
|
name = kwargs.get("name", None)
|
|
364
417
|
|
|
365
|
-
return cls.from_array(array
|
|
418
|
+
return cls.from_array(array, name=name, **kwargs)
|
|
366
419
|
|
|
367
420
|
@classmethod
|
|
368
|
-
def from_dict(cls, dictionary: dict):
|
|
421
|
+
def from_dict(cls, dictionary: dict) -> Self:
|
|
369
422
|
"""Construct Raster from metadata dict to fastpass the initializer.
|
|
370
423
|
|
|
371
424
|
This is the fastest way to create a Raster since a metadata lookup is not
|
|
@@ -387,6 +440,7 @@ class Raster:
|
|
|
387
440
|
return cls(**dictionary)
|
|
388
441
|
|
|
389
442
|
def update(self, **kwargs) -> Self:
|
|
443
|
+
"""Update attributes of the Raster."""
|
|
390
444
|
for key, value in kwargs.items():
|
|
391
445
|
self._validate_key(key)
|
|
392
446
|
if is_property(self, key):
|
|
@@ -394,7 +448,9 @@ class Raster:
|
|
|
394
448
|
setattr(self, key, value)
|
|
395
449
|
return self
|
|
396
450
|
|
|
397
|
-
def write(
|
|
451
|
+
def write(
|
|
452
|
+
self, path: str, window: rasterio.windows.Window | None = None, **kwargs
|
|
453
|
+
) -> None:
|
|
398
454
|
"""Write the raster as a single file.
|
|
399
455
|
|
|
400
456
|
Multiband arrays will result in a multiband image file.
|
|
@@ -402,26 +458,29 @@ class Raster:
|
|
|
402
458
|
Args:
|
|
403
459
|
path: File path to write to.
|
|
404
460
|
window: Optional window to clip the image to.
|
|
461
|
+
**kwargs: Keyword arguments passed to rasterio.open.
|
|
462
|
+
Thise will override the items in the Raster's profile,
|
|
463
|
+
if overlapping.
|
|
405
464
|
"""
|
|
406
|
-
|
|
407
|
-
if self.array is None:
|
|
465
|
+
if self.values is None:
|
|
408
466
|
raise AttributeError("The image hasn't been loaded.")
|
|
409
467
|
|
|
410
468
|
profile = self.profile | kwargs
|
|
411
469
|
|
|
412
|
-
with opener(path, file_system=self.file_system) as file:
|
|
470
|
+
with opener(path, "wb", file_system=self.file_system) as file:
|
|
413
471
|
with rasterio.open(file, "w", **profile) as dst:
|
|
414
472
|
self._write(dst, window)
|
|
415
473
|
|
|
416
474
|
self.path = str(path)
|
|
417
475
|
|
|
418
|
-
def load(self, **kwargs) -> Self:
|
|
476
|
+
def load(self, reload: bool = False, **kwargs) -> Self:
|
|
419
477
|
"""Load the entire image as an np.array.
|
|
420
478
|
|
|
421
479
|
The array is stored in the 'array' attribute
|
|
422
480
|
of the Raster.
|
|
423
481
|
|
|
424
482
|
Args:
|
|
483
|
+
reload: Whether to reload the array if already loaded.
|
|
425
484
|
**kwargs: Keyword arguments passed to the rasterio read
|
|
426
485
|
method.
|
|
427
486
|
"""
|
|
@@ -430,13 +489,14 @@ class Raster:
|
|
|
430
489
|
if "window" in kwargs:
|
|
431
490
|
raise ValueError("Got an unexpected keyword argument 'window'")
|
|
432
491
|
|
|
433
|
-
self.
|
|
492
|
+
if reload or self.values is None:
|
|
493
|
+
self._read_tif(**kwargs)
|
|
434
494
|
|
|
435
495
|
return self
|
|
436
496
|
|
|
437
497
|
def clip(
|
|
438
498
|
self,
|
|
439
|
-
mask,
|
|
499
|
+
mask: Any,
|
|
440
500
|
masked: bool = False,
|
|
441
501
|
boundless: bool = True,
|
|
442
502
|
**kwargs,
|
|
@@ -448,6 +508,13 @@ class Raster:
|
|
|
448
508
|
|
|
449
509
|
Args:
|
|
450
510
|
mask: Geometry-like object or bounding box.
|
|
511
|
+
masked: If 'masked' is True the return value will be a masked
|
|
512
|
+
array. Otherwise (default) the return value will be a
|
|
513
|
+
regular array. Masks will be exactly the inverse of the
|
|
514
|
+
GDAL RFC 15 conforming arrays returned by read_masks().
|
|
515
|
+
boundless: If True, windows that extend beyond the dataset's extent
|
|
516
|
+
are permitted and partially or completely filled arrays will
|
|
517
|
+
be returned as appropriate.
|
|
451
518
|
**kwargs: Keyword arguments passed to the mask function
|
|
452
519
|
from the rasterio.mask module.
|
|
453
520
|
|
|
@@ -462,17 +529,18 @@ class Raster:
|
|
|
462
529
|
except ValueError:
|
|
463
530
|
mask = mask.set_crs(self.crs)
|
|
464
531
|
|
|
465
|
-
# if not self.crs.equals(pyproj.CRS(mask.crs)):
|
|
466
|
-
# raise ValueError("crs mismatch.")
|
|
467
|
-
|
|
468
532
|
self._read_with_mask(mask=mask, masked=masked, boundless=boundless, **kwargs)
|
|
469
533
|
|
|
470
534
|
return self
|
|
471
535
|
|
|
472
|
-
def intersects(self, other) -> bool:
|
|
536
|
+
def intersects(self, other: Any) -> bool:
|
|
537
|
+
"""Returns True if the image bounds intersect with 'other'."""
|
|
473
538
|
return self.unary_union.intersects(to_shapely(other))
|
|
474
539
|
|
|
475
|
-
def sample(
|
|
540
|
+
def sample(
|
|
541
|
+
self, n: int = 1, size: int = 20, mask: Any = None, copy: bool = True, **kwargs
|
|
542
|
+
) -> Self:
|
|
543
|
+
"""Take a random spatial sample of the image."""
|
|
476
544
|
if mask is not None:
|
|
477
545
|
points = GeoSeries(self.unary_union).clip(mask).sample_points(n)
|
|
478
546
|
else:
|
|
@@ -508,19 +576,21 @@ class Raster:
|
|
|
508
576
|
A GeoDataFrame with aggregated values per polygon.
|
|
509
577
|
"""
|
|
510
578
|
idx_mapper, idx_name = get_index_mapper(polygons)
|
|
511
|
-
polygons, aggfunc, func_names =
|
|
512
|
-
poly_iter =
|
|
579
|
+
polygons, aggfunc, func_names = _prepare_zonal(polygons, aggfunc)
|
|
580
|
+
poly_iter = _make_geometry_iterrows(polygons)
|
|
513
581
|
|
|
514
582
|
aggregated = []
|
|
515
583
|
for i, poly in poly_iter:
|
|
516
584
|
clipped = self.clip(poly)
|
|
517
|
-
if not np.size(clipped.
|
|
585
|
+
if not np.size(clipped.values):
|
|
518
586
|
aggregated.append(_no_overlap_df(func_names, i, date=self.date))
|
|
519
587
|
aggregated.append(
|
|
520
|
-
_aggregate(
|
|
588
|
+
_aggregate(
|
|
589
|
+
clipped.values, array_func, aggfunc, func_names, self.date, i
|
|
590
|
+
)
|
|
521
591
|
)
|
|
522
592
|
|
|
523
|
-
return
|
|
593
|
+
return _zonal_post(
|
|
524
594
|
aggregated,
|
|
525
595
|
polygons=polygons,
|
|
526
596
|
idx_mapper=idx_mapper,
|
|
@@ -528,73 +598,23 @@ class Raster:
|
|
|
528
598
|
dropna=dropna,
|
|
529
599
|
)
|
|
530
600
|
|
|
531
|
-
def gradient(self, degrees: bool = False, copy: bool = False) -> Self:
|
|
532
|
-
"""Get the slope of an elevation raster.
|
|
533
|
-
|
|
534
|
-
Calculates the absolute slope between the grid cells
|
|
535
|
-
based on the image resolution.
|
|
536
|
-
|
|
537
|
-
For multiband images, the calculation is done for each band.
|
|
538
|
-
|
|
539
|
-
Args:
|
|
540
|
-
degrees: If False (default), the returned values will be in ratios,
|
|
541
|
-
where a value of 1 means 1 meter up per 1 meter forward. If True,
|
|
542
|
-
the values will be in degrees from 0 to 90.
|
|
543
|
-
copy: Whether to copy or overwrite the original Raster.
|
|
544
|
-
Defaults to False to save memory.
|
|
545
|
-
|
|
546
|
-
Returns:
|
|
547
|
-
The class instance with new array values, or a copy if copy is True.
|
|
548
|
-
|
|
549
|
-
Examples
|
|
550
|
-
--------
|
|
551
|
-
Making an array where the gradient to the center is always 10.
|
|
552
|
-
|
|
553
|
-
>>> import sgis as sg
|
|
554
|
-
>>> import numpy as np
|
|
555
|
-
>>> arr = np.array(
|
|
556
|
-
... [
|
|
557
|
-
... [100, 100, 100, 100, 100],
|
|
558
|
-
... [100, 110, 110, 110, 100],
|
|
559
|
-
... [100, 110, 120, 110, 100],
|
|
560
|
-
... [100, 110, 110, 110, 100],
|
|
561
|
-
... [100, 100, 100, 100, 100],
|
|
562
|
-
... ]
|
|
563
|
-
... )
|
|
564
|
-
|
|
565
|
-
Now let's create a Raster from this array with a resolution of 10.
|
|
566
|
-
|
|
567
|
-
>>> r = sg.Raster.from_array(arr, crs=None, bounds=(0, 0, 50, 50))
|
|
568
|
-
|
|
569
|
-
The gradient will be 1 (1 meter up for every meter forward).
|
|
570
|
-
The calculation is by default done in place to save memory.
|
|
571
|
-
|
|
572
|
-
>>> r.gradient()
|
|
573
|
-
>>> r.array
|
|
574
|
-
array([[0., 1., 1., 1., 0.],
|
|
575
|
-
[1., 1., 1., 1., 1.],
|
|
576
|
-
[1., 1., 0., 1., 1.],
|
|
577
|
-
[1., 1., 1., 1., 1.],
|
|
578
|
-
[0., 1., 1., 1., 0.]])
|
|
579
|
-
"""
|
|
580
|
-
return get_gradient(self, degrees=degrees, copy=copy)
|
|
581
|
-
|
|
582
601
|
def to_xarray(self) -> DataArray:
|
|
602
|
+
"""Convert the raster to an xarray.DataArray."""
|
|
583
603
|
self._check_for_array()
|
|
584
604
|
self.name = self.name or self.__class__.__name__.lower()
|
|
585
605
|
coords = _generate_spatial_coords(self.transform, self.width, self.height)
|
|
586
|
-
if len(self.
|
|
606
|
+
if len(self.values.shape) == 2:
|
|
587
607
|
dims = ["y", "x"]
|
|
588
608
|
# dims = ["band", "y", "x"]
|
|
589
|
-
# array = np.array([self.
|
|
609
|
+
# array = np.array([self.values])
|
|
590
610
|
# assert len(array.shape) == 3
|
|
591
|
-
elif len(self.
|
|
611
|
+
elif len(self.values.shape) == 3:
|
|
592
612
|
dims = ["band", "y", "x"]
|
|
593
|
-
# array = self.
|
|
613
|
+
# array = self.values
|
|
594
614
|
else:
|
|
595
615
|
raise ValueError("Array must be 2 or 3 dimensional.")
|
|
596
616
|
return xr.DataArray(
|
|
597
|
-
self.
|
|
617
|
+
self.values,
|
|
598
618
|
coords=coords,
|
|
599
619
|
dims=dims,
|
|
600
620
|
name=self.name,
|
|
@@ -602,6 +622,7 @@ class Raster:
|
|
|
602
622
|
) # .transpose("y", "x")
|
|
603
623
|
|
|
604
624
|
def to_dict(self) -> dict:
|
|
625
|
+
"""Get a dictionary of Raster attributes."""
|
|
605
626
|
out = {}
|
|
606
627
|
for col in self.ALL_ATTRS:
|
|
607
628
|
try:
|
|
@@ -642,11 +663,11 @@ class Raster:
|
|
|
642
663
|
column = [column] * len(array_list)
|
|
643
664
|
|
|
644
665
|
gdfs = []
|
|
645
|
-
for i, (
|
|
666
|
+
for i, (col, array) in enumerate(zip(column, array_list, strict=True)):
|
|
646
667
|
gdf = gpd.GeoDataFrame(
|
|
647
668
|
pd.DataFrame(
|
|
648
669
|
self._array_to_geojson(array, self.transform),
|
|
649
|
-
columns=[
|
|
670
|
+
columns=[col, "geometry"],
|
|
650
671
|
),
|
|
651
672
|
geometry="geometry",
|
|
652
673
|
crs=self.crs,
|
|
@@ -658,20 +679,20 @@ class Raster:
|
|
|
658
679
|
|
|
659
680
|
def set_crs(
|
|
660
681
|
self,
|
|
661
|
-
crs,
|
|
682
|
+
crs: pyproj.CRS | Any,
|
|
662
683
|
allow_override: bool = False,
|
|
663
684
|
) -> Self:
|
|
664
685
|
"""Set coordinate reference system."""
|
|
665
686
|
if not allow_override and self.crs is not None:
|
|
666
687
|
raise ValueError("Cannot overwrite crs when allow_override is False.")
|
|
667
688
|
|
|
668
|
-
if self.
|
|
689
|
+
if self.values is None:
|
|
669
690
|
raise ValueError("array must be loaded/clipped before set_crs")
|
|
670
691
|
|
|
671
692
|
self._crs = pyproj.CRS(crs)
|
|
672
693
|
return self
|
|
673
694
|
|
|
674
|
-
def to_crs(self, crs, **kwargs) -> Self:
|
|
695
|
+
def to_crs(self, crs: pyproj.CRS | Any, **kwargs) -> Self:
|
|
675
696
|
"""Reproject the raster.
|
|
676
697
|
|
|
677
698
|
Args:
|
|
@@ -687,7 +708,7 @@ class Raster:
|
|
|
687
708
|
# ):
|
|
688
709
|
# return self
|
|
689
710
|
|
|
690
|
-
if self.
|
|
711
|
+
if self.values is None:
|
|
691
712
|
project = pyproj.Transformer.from_crs(
|
|
692
713
|
pyproj.CRS(self._prev_crs), pyproj.CRS(crs), always_xy=True
|
|
693
714
|
).transform
|
|
@@ -719,16 +740,16 @@ class Raster:
|
|
|
719
740
|
# self._bounds = shapely.transform(old_box, project)
|
|
720
741
|
else:
|
|
721
742
|
was_2d = len(self.shape) == 2
|
|
722
|
-
self.
|
|
723
|
-
source=self.
|
|
743
|
+
self.values, transform = reproject(
|
|
744
|
+
source=self.values,
|
|
724
745
|
src_crs=self._prev_crs,
|
|
725
746
|
src_transform=self.transform,
|
|
726
747
|
dst_crs=pyproj.CRS(crs),
|
|
727
748
|
**kwargs,
|
|
728
749
|
)
|
|
729
|
-
if was_2d and len(self.
|
|
730
|
-
assert self.
|
|
731
|
-
self.
|
|
750
|
+
if was_2d and len(self.values.shape) == 3:
|
|
751
|
+
assert self.values.shape[0] == 1
|
|
752
|
+
self.values = self.values[0]
|
|
732
753
|
|
|
733
754
|
self._bounds = rasterio.transform.array_bounds(
|
|
734
755
|
self.height, self.width, transform
|
|
@@ -739,18 +760,18 @@ class Raster:
|
|
|
739
760
|
|
|
740
761
|
return self
|
|
741
762
|
|
|
742
|
-
def plot(self, mask=None) -> None:
|
|
743
|
-
self._check_for_array()
|
|
763
|
+
def plot(self, mask: Any | None = None) -> None:
|
|
744
764
|
"""Plot the images. One image per band."""
|
|
765
|
+
self._check_for_array()
|
|
745
766
|
if mask is not None:
|
|
746
767
|
raster = self.copy().clip(mask)
|
|
747
768
|
else:
|
|
748
769
|
raster = self
|
|
749
770
|
|
|
750
771
|
if len(raster.shape) == 2:
|
|
751
|
-
array = np.array([raster.
|
|
772
|
+
array = np.array([raster.values])
|
|
752
773
|
else:
|
|
753
|
-
array = raster.
|
|
774
|
+
array = raster.values
|
|
754
775
|
|
|
755
776
|
for arr in array:
|
|
756
777
|
ax = plt.axes()
|
|
@@ -760,28 +781,32 @@ class Raster:
|
|
|
760
781
|
plt.close()
|
|
761
782
|
|
|
762
783
|
def astype(self, dtype: type) -> Self:
|
|
763
|
-
|
|
784
|
+
"""Convert the datatype of the array."""
|
|
785
|
+
if self.values is None:
|
|
764
786
|
raise ValueError("Array is not loaded.")
|
|
765
|
-
if not rasterio.dtypes.can_cast_dtype(self.
|
|
766
|
-
min_dtype = rasterio.dtypes.get_minimum_dtype(self.
|
|
787
|
+
if not rasterio.dtypes.can_cast_dtype(self.values, dtype):
|
|
788
|
+
min_dtype = rasterio.dtypes.get_minimum_dtype(self.values)
|
|
767
789
|
raise ValueError(f"Cannot cast to dtype. Minimum dtype is {min_dtype}")
|
|
768
|
-
self.
|
|
790
|
+
self.values = self.values.astype(dtype)
|
|
769
791
|
self._dtype = dtype
|
|
770
792
|
return self
|
|
771
793
|
|
|
772
794
|
def as_minimum_dtype(self) -> Self:
|
|
773
|
-
|
|
774
|
-
|
|
795
|
+
"""Convert the array to the minimum dtype without overflow."""
|
|
796
|
+
min_dtype = rasterio.dtypes.get_minimum_dtype(self.values)
|
|
797
|
+
self.values = self.values.astype(min_dtype)
|
|
775
798
|
return self
|
|
776
799
|
|
|
777
800
|
def min(self) -> int | None:
|
|
778
|
-
|
|
779
|
-
|
|
801
|
+
"""Minimum value in the array."""
|
|
802
|
+
if np.size(self.values):
|
|
803
|
+
return np.min(self.values)
|
|
780
804
|
return None
|
|
781
805
|
|
|
782
806
|
def max(self) -> int | None:
|
|
783
|
-
|
|
784
|
-
|
|
807
|
+
"""Maximum value in the array."""
|
|
808
|
+
if np.size(self.values):
|
|
809
|
+
return np.max(self.values)
|
|
785
810
|
return None
|
|
786
811
|
|
|
787
812
|
def _add_meta(self) -> Self:
|
|
@@ -798,20 +823,23 @@ class Raster:
|
|
|
798
823
|
return self
|
|
799
824
|
|
|
800
825
|
def array_list(self) -> list[np.ndarray]:
|
|
826
|
+
"""Get a list of 2D arrays."""
|
|
801
827
|
self._check_for_array()
|
|
802
|
-
if len(self.
|
|
803
|
-
return [self.
|
|
804
|
-
elif len(self.
|
|
805
|
-
return list(self.
|
|
828
|
+
if len(self.values.shape) == 2:
|
|
829
|
+
return [self.values]
|
|
830
|
+
elif len(self.values.shape) == 3:
|
|
831
|
+
return list(self.values)
|
|
806
832
|
else:
|
|
807
833
|
raise ValueError
|
|
808
834
|
|
|
809
835
|
@property
|
|
810
836
|
def indexes(self) -> int | tuple[int] | None:
|
|
837
|
+
"""Band indexes of the image."""
|
|
811
838
|
return self._indexes
|
|
812
839
|
|
|
813
840
|
@property
|
|
814
841
|
def name(self) -> str | None:
|
|
842
|
+
"""Name of the file in the file path, if any."""
|
|
815
843
|
try:
|
|
816
844
|
return self._name
|
|
817
845
|
except AttributeError:
|
|
@@ -821,35 +849,30 @@ class Raster:
|
|
|
821
849
|
return None
|
|
822
850
|
|
|
823
851
|
@name.setter
|
|
824
|
-
def name(self, value):
|
|
852
|
+
def name(self, value) -> None:
|
|
825
853
|
self._name = value
|
|
826
|
-
return self._name
|
|
827
854
|
|
|
828
855
|
@property
|
|
829
|
-
def date(self):
|
|
856
|
+
def date(self) -> str | None:
|
|
857
|
+
"""Date in the image file name, if filename_regex is present."""
|
|
830
858
|
try:
|
|
831
|
-
|
|
832
|
-
return re.match(pattern, Path(self.path).name).group("date")
|
|
859
|
+
return re.match(self.filename_pattern, Path(self.path).name).group("date")
|
|
833
860
|
except (AttributeError, TypeError):
|
|
834
861
|
return None
|
|
835
862
|
|
|
836
863
|
@property
|
|
837
864
|
def band(self) -> str | None:
|
|
865
|
+
"""Band name of the image file name, if filename_regex is present."""
|
|
838
866
|
try:
|
|
839
|
-
|
|
840
|
-
return re.match(pattern, Path(self.path).name).group("band")
|
|
867
|
+
return re.match(self.filename_pattern, Path(self.path).name).group("band")
|
|
841
868
|
except (AttributeError, TypeError):
|
|
842
869
|
return None
|
|
843
870
|
|
|
844
|
-
# @property
|
|
845
|
-
# def band_color(self):
|
|
846
|
-
# """To be implemented in subclasses."""
|
|
847
|
-
# pass
|
|
848
|
-
|
|
849
871
|
@property
|
|
850
|
-
def dtype(self):
|
|
872
|
+
def dtype(self) -> Any:
|
|
873
|
+
"""Data type of the array."""
|
|
851
874
|
try:
|
|
852
|
-
return self.
|
|
875
|
+
return self.values.dtype
|
|
853
876
|
except AttributeError:
|
|
854
877
|
try:
|
|
855
878
|
return self._dtype
|
|
@@ -857,12 +880,12 @@ class Raster:
|
|
|
857
880
|
return None
|
|
858
881
|
|
|
859
882
|
@dtype.setter
|
|
860
|
-
def dtype(self, new_dtype):
|
|
861
|
-
self.
|
|
862
|
-
return self.array.dtype
|
|
883
|
+
def dtype(self, new_dtype: Any) -> None:
|
|
884
|
+
self.values = self.values.astype(new_dtype)
|
|
863
885
|
|
|
864
886
|
@property
|
|
865
887
|
def nodata(self) -> int | None:
|
|
888
|
+
"""No data value."""
|
|
866
889
|
try:
|
|
867
890
|
return self._nodata
|
|
868
891
|
except AttributeError:
|
|
@@ -870,12 +893,15 @@ class Raster:
|
|
|
870
893
|
|
|
871
894
|
@property
|
|
872
895
|
def tile(self) -> str | None:
|
|
873
|
-
|
|
896
|
+
"""Tile name from regex."""
|
|
897
|
+
try:
|
|
898
|
+
return re.match(self.filename_pattern, Path(self.path).name).group("tile")
|
|
899
|
+
except (AttributeError, TypeError):
|
|
874
900
|
return None
|
|
875
|
-
return f"{int(self.bounds[0])}_{int(self.bounds[1])}"
|
|
876
901
|
|
|
877
902
|
@property
|
|
878
903
|
def meta(self) -> dict:
|
|
904
|
+
"""Metadata dict."""
|
|
879
905
|
return {
|
|
880
906
|
"path": self.path,
|
|
881
907
|
"type": self.__class__.__name__,
|
|
@@ -886,6 +912,7 @@ class Raster:
|
|
|
886
912
|
|
|
887
913
|
@property
|
|
888
914
|
def profile(self) -> dict:
|
|
915
|
+
"""Profile of the image file."""
|
|
889
916
|
# TODO: .crs blir feil hvis warpa. Eller?
|
|
890
917
|
return {
|
|
891
918
|
"driver": self.driver,
|
|
@@ -902,14 +929,16 @@ class Raster:
|
|
|
902
929
|
|
|
903
930
|
@property
|
|
904
931
|
def read_kwargs(self) -> dict:
|
|
932
|
+
"""Keywords passed to the read method of rasterio.io.DatasetReader."""
|
|
905
933
|
return {
|
|
906
934
|
"indexes": self.indexes,
|
|
907
935
|
"fill_value": self.nodata,
|
|
908
|
-
"masked":
|
|
936
|
+
"masked": False,
|
|
909
937
|
}
|
|
910
938
|
|
|
911
939
|
@property
|
|
912
940
|
def res(self) -> float | None:
|
|
941
|
+
"""Get the spatial resolution of the image."""
|
|
913
942
|
if hasattr(self, "_res") and self._res is not None:
|
|
914
943
|
return self._res
|
|
915
944
|
if self.width is None:
|
|
@@ -919,17 +948,19 @@ class Raster:
|
|
|
919
948
|
|
|
920
949
|
@property
|
|
921
950
|
def height(self) -> int | None:
|
|
922
|
-
|
|
951
|
+
"""Get the height of the image as number of pixels."""
|
|
952
|
+
if self.values is None:
|
|
923
953
|
try:
|
|
924
954
|
return self._height
|
|
925
955
|
except AttributeError:
|
|
926
956
|
return None
|
|
927
|
-
i = 1 if len(self.
|
|
928
|
-
return self.
|
|
957
|
+
i = 1 if len(self.values.shape) == 3 else 0
|
|
958
|
+
return self.values.shape[i]
|
|
929
959
|
|
|
930
960
|
@property
|
|
931
961
|
def width(self) -> int | None:
|
|
932
|
-
|
|
962
|
+
"""Get the width of the image as number of pixels."""
|
|
963
|
+
if self.values is None:
|
|
933
964
|
try:
|
|
934
965
|
return self._width
|
|
935
966
|
except AttributeError:
|
|
@@ -940,15 +971,16 @@ class Raster:
|
|
|
940
971
|
return self._width
|
|
941
972
|
except Exception:
|
|
942
973
|
return None
|
|
943
|
-
i = 2 if len(self.
|
|
944
|
-
return self.
|
|
974
|
+
i = 2 if len(self.values.shape) == 3 else 1
|
|
975
|
+
return self.values.shape[i]
|
|
945
976
|
|
|
946
977
|
@property
|
|
947
978
|
def count(self) -> int:
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
979
|
+
"""Get the number of bands in the image."""
|
|
980
|
+
if self.values is not None:
|
|
981
|
+
if len(self.values.shape) == 3:
|
|
982
|
+
return self.values.shape[0]
|
|
983
|
+
if len(self.values.shape) == 2:
|
|
952
984
|
return 1
|
|
953
985
|
if not hasattr(self._indexes, "__iter__"):
|
|
954
986
|
return 1
|
|
@@ -957,14 +989,15 @@ class Raster:
|
|
|
957
989
|
@property
|
|
958
990
|
def shape(self) -> tuple[int]:
|
|
959
991
|
"""Shape that is consistent with the array, whether it is loaded or not."""
|
|
960
|
-
if self.
|
|
961
|
-
return self.
|
|
992
|
+
if self.values is not None:
|
|
993
|
+
return self.values.shape
|
|
962
994
|
if hasattr(self._indexes, "__iter__"):
|
|
963
995
|
return self.count, self.width, self.height
|
|
964
996
|
return self.width, self.height
|
|
965
997
|
|
|
966
998
|
@property
|
|
967
999
|
def transform(self) -> Affine | None:
|
|
1000
|
+
"""Get the Affine transform of the image."""
|
|
968
1001
|
try:
|
|
969
1002
|
return rasterio.transform.from_bounds(*self.bounds, self.width, self.height)
|
|
970
1003
|
except (ZeroDivisionError, TypeError):
|
|
@@ -973,13 +1006,15 @@ class Raster:
|
|
|
973
1006
|
|
|
974
1007
|
@property
|
|
975
1008
|
def bounds(self) -> tuple[float, float, float, float] | None:
|
|
1009
|
+
"""Get the bounds of the image."""
|
|
976
1010
|
try:
|
|
977
1011
|
return to_bbox(self._bounds)
|
|
978
1012
|
except (AttributeError, TypeError):
|
|
979
1013
|
return None
|
|
980
1014
|
|
|
981
1015
|
@property
|
|
982
|
-
def crs(self):
|
|
1016
|
+
def crs(self) -> pyproj.CRS | None:
|
|
1017
|
+
"""Get the coordinate reference system of the image."""
|
|
983
1018
|
try:
|
|
984
1019
|
return self._warped_crs
|
|
985
1020
|
except AttributeError:
|
|
@@ -990,24 +1025,29 @@ class Raster:
|
|
|
990
1025
|
|
|
991
1026
|
@property
|
|
992
1027
|
def area(self) -> float:
|
|
1028
|
+
"""Get the area of the image."""
|
|
993
1029
|
return shapely.area(self.unary_union)
|
|
994
1030
|
|
|
995
1031
|
@property
|
|
996
1032
|
def length(self) -> float:
|
|
1033
|
+
"""Get the circumfence of the image."""
|
|
997
1034
|
return shapely.length(self.unary_union)
|
|
998
1035
|
|
|
999
1036
|
@property
|
|
1000
1037
|
def unary_union(self) -> Polygon:
|
|
1038
|
+
"""Get the image bounds as a Polygon."""
|
|
1001
1039
|
return shapely.box(*self.bounds)
|
|
1002
1040
|
|
|
1003
1041
|
@property
|
|
1004
1042
|
def centroid(self) -> Point:
|
|
1043
|
+
"""Get the centerpoint of the image."""
|
|
1005
1044
|
x = (self.bounds[0] + self.bounds[2]) / 2
|
|
1006
1045
|
y = (self.bounds[1] + self.bounds[3]) / 2
|
|
1007
1046
|
return Point(x, y)
|
|
1008
1047
|
|
|
1009
1048
|
@property
|
|
1010
1049
|
def properties(self) -> list[str]:
|
|
1050
|
+
"""List of all properties of the class."""
|
|
1011
1051
|
out = []
|
|
1012
1052
|
for attr in dir(self):
|
|
1013
1053
|
try:
|
|
@@ -1018,11 +1058,12 @@ class Raster:
|
|
|
1018
1058
|
return out
|
|
1019
1059
|
|
|
1020
1060
|
def indexes_as_tuple(self) -> tuple[int, ...]:
|
|
1061
|
+
"""Get the band index(es) as a tuple of integers."""
|
|
1021
1062
|
if len(self.shape) == 2:
|
|
1022
1063
|
return (1,)
|
|
1023
1064
|
return tuple(i + 1 for i in range(self.shape[0]))
|
|
1024
1065
|
|
|
1025
|
-
def copy(self, deep=True):
|
|
1066
|
+
def copy(self, deep: bool = True) -> "Raster":
|
|
1026
1067
|
"""Returns a (deep) copy of the class instance.
|
|
1027
1068
|
|
|
1028
1069
|
Args:
|
|
@@ -1033,14 +1074,15 @@ class Raster:
|
|
|
1033
1074
|
else:
|
|
1034
1075
|
return copy(self)
|
|
1035
1076
|
|
|
1036
|
-
def equals(self, other) -> bool:
|
|
1077
|
+
def equals(self, other: Any) -> bool:
|
|
1078
|
+
"""Check if the Raster is equal to another Raster."""
|
|
1037
1079
|
if not isinstance(other, Raster):
|
|
1038
1080
|
raise NotImplementedError("other must be of type Raster")
|
|
1039
1081
|
if type(other) != type(self):
|
|
1040
1082
|
return False
|
|
1041
|
-
if self.
|
|
1083
|
+
if self.values is None and other.values is not None:
|
|
1042
1084
|
return False
|
|
1043
|
-
if self.
|
|
1085
|
+
if self.values is not None and other.values is None:
|
|
1044
1086
|
return False
|
|
1045
1087
|
|
|
1046
1088
|
for method in dir(self):
|
|
@@ -1049,7 +1091,7 @@ class Raster:
|
|
|
1049
1091
|
if getattr(self, method) != getattr(other, method):
|
|
1050
1092
|
return False
|
|
1051
1093
|
|
|
1052
|
-
return np.array_equal(self.
|
|
1094
|
+
return np.array_equal(self.values, other.values)
|
|
1053
1095
|
|
|
1054
1096
|
def __repr__(self) -> str:
|
|
1055
1097
|
"""The print representation."""
|
|
@@ -1059,39 +1101,55 @@ class Raster:
|
|
|
1059
1101
|
res = int(self.res)
|
|
1060
1102
|
except TypeError:
|
|
1061
1103
|
res = None
|
|
1062
|
-
return f"{self.__class__.__name__}(shape=({shp}), res={res},
|
|
1104
|
+
return f"{self.__class__.__name__}(shape=({shp}), res={res}, band={self.band})"
|
|
1105
|
+
|
|
1106
|
+
def __iter__(self) -> Iterator[np.ndarray]:
|
|
1107
|
+
"""Iterate over the arrays."""
|
|
1108
|
+
if len(self.values.shape) == 2:
|
|
1109
|
+
return iter([self.values])
|
|
1110
|
+
if len(self.values.shape) == 3:
|
|
1111
|
+
return iter(self.values)
|
|
1112
|
+
raise ValueError(
|
|
1113
|
+
f"Array should have shape length 2 or 3. Got {len(self.values.shape)}"
|
|
1114
|
+
)
|
|
1063
1115
|
|
|
1064
|
-
def __mul__(self, scalar):
|
|
1116
|
+
def __mul__(self, scalar: int | float) -> "Raster":
|
|
1117
|
+
"""Multiply the array values with *."""
|
|
1065
1118
|
self._check_for_array()
|
|
1066
|
-
self.
|
|
1119
|
+
self.values = self.values * scalar
|
|
1067
1120
|
return self
|
|
1068
1121
|
|
|
1069
|
-
def __add__(self, scalar):
|
|
1122
|
+
def __add__(self, scalar: int | float) -> "Raster":
|
|
1123
|
+
"""Add to the array values with +."""
|
|
1070
1124
|
self._check_for_array()
|
|
1071
|
-
self.
|
|
1125
|
+
self.values = self.values + scalar
|
|
1072
1126
|
return self
|
|
1073
1127
|
|
|
1074
|
-
def __sub__(self, scalar):
|
|
1128
|
+
def __sub__(self, scalar: int | float) -> "Raster":
|
|
1129
|
+
"""Subtract the array values with -."""
|
|
1075
1130
|
self._check_for_array()
|
|
1076
|
-
self.
|
|
1131
|
+
self.values = self.values - scalar
|
|
1077
1132
|
return self
|
|
1078
1133
|
|
|
1079
|
-
def __truediv__(self, scalar):
|
|
1134
|
+
def __truediv__(self, scalar: int | float) -> "Raster":
|
|
1135
|
+
"""Divide the array values with /."""
|
|
1080
1136
|
self._check_for_array()
|
|
1081
|
-
self.
|
|
1137
|
+
self.values = self.values / scalar
|
|
1082
1138
|
return self
|
|
1083
1139
|
|
|
1084
|
-
def __floordiv__(self, scalar):
|
|
1140
|
+
def __floordiv__(self, scalar: int | float) -> "Raster":
|
|
1141
|
+
"""Floor divide the array values with //."""
|
|
1085
1142
|
self._check_for_array()
|
|
1086
|
-
self.
|
|
1143
|
+
self.values = self.values // scalar
|
|
1087
1144
|
return self
|
|
1088
1145
|
|
|
1089
|
-
def __pow__(self, exponent):
|
|
1146
|
+
def __pow__(self, exponent: int | float) -> "Raster":
|
|
1147
|
+
"""Exponentiate the array values with **."""
|
|
1090
1148
|
self._check_for_array()
|
|
1091
|
-
self.
|
|
1149
|
+
self.values = self.values**exponent
|
|
1092
1150
|
return self
|
|
1093
1151
|
|
|
1094
|
-
def _has_nessecary_attrs(self, dict_like) -> bool:
|
|
1152
|
+
def _has_nessecary_attrs(self, dict_like: dict) -> bool:
|
|
1095
1153
|
"""Check if Raster init got enough kwargs to not need to read src."""
|
|
1096
1154
|
try:
|
|
1097
1155
|
self._validate_dict(dict_like)
|
|
@@ -1101,17 +1159,17 @@ class Raster:
|
|
|
1101
1159
|
except AttributeError:
|
|
1102
1160
|
return False
|
|
1103
1161
|
|
|
1104
|
-
def _return_self_or_copy(self, array, copy: bool):
|
|
1162
|
+
def _return_self_or_copy(self, array: np.ndarray, copy: bool) -> "Raster":
|
|
1105
1163
|
if not copy:
|
|
1106
|
-
self.
|
|
1164
|
+
self.values = array
|
|
1107
1165
|
return self
|
|
1108
1166
|
else:
|
|
1109
1167
|
copy = self.copy()
|
|
1110
|
-
copy.
|
|
1168
|
+
copy.values = array
|
|
1111
1169
|
return copy
|
|
1112
1170
|
|
|
1113
1171
|
@classmethod
|
|
1114
|
-
def _validate_dict(cls, dict_like) -> None:
|
|
1172
|
+
def _validate_dict(cls, dict_like: dict) -> None:
|
|
1115
1173
|
missing = []
|
|
1116
1174
|
for attr in NESSECARY_META:
|
|
1117
1175
|
if any(
|
|
@@ -1127,14 +1185,14 @@ class Raster:
|
|
|
1127
1185
|
raise AttributeError(f"Missing nessecary key(s) {', '.join(missing)}")
|
|
1128
1186
|
|
|
1129
1187
|
@classmethod
|
|
1130
|
-
def _validate_key(cls, key) -> None:
|
|
1188
|
+
def _validate_key(cls, key: str) -> None:
|
|
1131
1189
|
if key not in ALLOWED_KEYS:
|
|
1132
1190
|
raise ValueError(
|
|
1133
1191
|
f"Got an unexpected key {key!r}. Allowed keys are ",
|
|
1134
1192
|
", ".join(ALLOWED_KEYS),
|
|
1135
1193
|
)
|
|
1136
1194
|
|
|
1137
|
-
def _get_shape_from_res(self, res) -> tuple[int] | None:
|
|
1195
|
+
def _get_shape_from_res(self, res: int) -> tuple[int] | None:
|
|
1138
1196
|
if res is None:
|
|
1139
1197
|
return None
|
|
1140
1198
|
if hasattr(res, "__iter__") and len(res) == 2:
|
|
@@ -1147,36 +1205,38 @@ class Raster:
|
|
|
1147
1205
|
return len(self.indexes), width, height
|
|
1148
1206
|
return width, height
|
|
1149
1207
|
|
|
1150
|
-
def _write(
|
|
1151
|
-
|
|
1152
|
-
|
|
1208
|
+
def _write(
|
|
1209
|
+
self, dst: rasterio.io.DatasetReader, window: rasterio.windows.Window
|
|
1210
|
+
) -> None:
|
|
1211
|
+
if np.ma.is_masked(self.values):
|
|
1212
|
+
if len(self.values.shape) == 2:
|
|
1153
1213
|
return dst.write(
|
|
1154
|
-
self.
|
|
1214
|
+
self.values.filled(self.nodata), indexes=1, window=window
|
|
1155
1215
|
)
|
|
1156
1216
|
|
|
1157
1217
|
for i in range(len(self.indexes_as_tuple())):
|
|
1158
1218
|
dst.write(
|
|
1159
|
-
self.
|
|
1219
|
+
self.values[i].filled(self.nodata),
|
|
1160
1220
|
indexes=i + 1,
|
|
1161
1221
|
window=window,
|
|
1162
1222
|
)
|
|
1163
1223
|
|
|
1164
1224
|
else:
|
|
1165
|
-
if len(self.
|
|
1166
|
-
return dst.write(self.
|
|
1225
|
+
if len(self.values.shape) == 2:
|
|
1226
|
+
return dst.write(self.values, indexes=1, window=window)
|
|
1167
1227
|
|
|
1168
1228
|
for i, idx in enumerate(self.indexes_as_tuple()):
|
|
1169
|
-
dst.write(self.
|
|
1229
|
+
dst.write(self.values[i], indexes=idx, window=window)
|
|
1170
1230
|
|
|
1171
|
-
def _get_indexes(self, indexes):
|
|
1231
|
+
def _get_indexes(self, indexes: int | tuple[int] | None) -> int | tuple[int] | None:
|
|
1172
1232
|
if isinstance(indexes, numbers.Number):
|
|
1173
1233
|
return int(indexes)
|
|
1174
1234
|
if indexes is None:
|
|
1175
|
-
if self.
|
|
1176
|
-
return tuple(i + 1 for i in range(self.
|
|
1177
|
-
elif self.
|
|
1235
|
+
if self.values is not None and len(self.values.shape) == 3:
|
|
1236
|
+
return tuple(i + 1 for i in range(self.values.shape[0]))
|
|
1237
|
+
elif self.values is not None and len(self.values.shape) == 2:
|
|
1178
1238
|
return 1
|
|
1179
|
-
elif self.
|
|
1239
|
+
elif self.values is not None:
|
|
1180
1240
|
raise ValueError("Array must be 2 or 3 dimensional.")
|
|
1181
1241
|
else:
|
|
1182
1242
|
return None
|
|
@@ -1188,7 +1248,7 @@ class Raster:
|
|
|
1188
1248
|
f"Got {type(indexes)}: {indexes}"
|
|
1189
1249
|
) from e
|
|
1190
1250
|
|
|
1191
|
-
def _return_gdf(self, obj) -> GeoDataFrame:
|
|
1251
|
+
def _return_gdf(self, obj: Any) -> GeoDataFrame:
|
|
1192
1252
|
if isinstance(obj, str) and not is_wkt(obj):
|
|
1193
1253
|
return self._read_tif(obj)
|
|
1194
1254
|
elif isinstance(obj, Raster):
|
|
@@ -1210,34 +1270,42 @@ class Raster:
|
|
|
1210
1270
|
warnings.filterwarnings("ignore", category=UserWarning)
|
|
1211
1271
|
return [
|
|
1212
1272
|
(feature["geometry"], val)
|
|
1213
|
-
for val, feature in zip(
|
|
1273
|
+
for val, feature in zip(
|
|
1274
|
+
gdf[column], loads(gdf.to_json())["features"], strict=False
|
|
1275
|
+
)
|
|
1214
1276
|
]
|
|
1215
1277
|
|
|
1216
1278
|
@staticmethod
|
|
1217
|
-
def _array_to_geojson(array: np.ndarray, transform: Affine):
|
|
1279
|
+
def _array_to_geojson(array: np.ndarray, transform: Affine) -> list[tuple]:
|
|
1280
|
+
if np.ma.is_masked(array):
|
|
1281
|
+
array = array.data
|
|
1218
1282
|
try:
|
|
1219
1283
|
return [
|
|
1220
1284
|
(value, shape(geom))
|
|
1221
|
-
for geom, value in features.shapes(
|
|
1285
|
+
for geom, value in features.shapes(
|
|
1286
|
+
array, transform=transform, mask=None
|
|
1287
|
+
)
|
|
1222
1288
|
]
|
|
1223
1289
|
except ValueError:
|
|
1224
1290
|
array = array.astype(np.float32)
|
|
1225
1291
|
return [
|
|
1226
1292
|
(value, shape(geom))
|
|
1227
|
-
for geom, value in features.shapes(
|
|
1293
|
+
for geom, value in features.shapes(
|
|
1294
|
+
array, transform=transform, mask=None
|
|
1295
|
+
)
|
|
1228
1296
|
]
|
|
1229
1297
|
|
|
1230
|
-
def _add_indexes_from_array(self, indexes):
|
|
1298
|
+
def _add_indexes_from_array(self, indexes: int | tuple[int]) -> int | tuple[int]:
|
|
1231
1299
|
if indexes is not None:
|
|
1232
1300
|
return indexes
|
|
1233
|
-
elif len(self.
|
|
1234
|
-
return tuple(x + 1 for x in range(len(self.
|
|
1235
|
-
elif len(self.
|
|
1301
|
+
elif len(self.values.shape) == 3:
|
|
1302
|
+
return tuple(x + 1 for x in range(len(self.values)))
|
|
1303
|
+
elif len(self.values.shape) == 2:
|
|
1236
1304
|
return 1
|
|
1237
1305
|
else:
|
|
1238
1306
|
raise ValueError
|
|
1239
1307
|
|
|
1240
|
-
def _add_meta_from_src(self, src):
|
|
1308
|
+
def _add_meta_from_src(self, src: rasterio.io.DatasetReader) -> None:
|
|
1241
1309
|
if not hasattr(self, "_bounds") or self._bounds is None:
|
|
1242
1310
|
self._bounds = tuple(src.bounds)
|
|
1243
1311
|
|
|
@@ -1259,10 +1327,15 @@ class Raster:
|
|
|
1259
1327
|
# except AttributeError:
|
|
1260
1328
|
# pass
|
|
1261
1329
|
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1330
|
+
if not hasattr(self, "_indexes") or self._indexes is None:
|
|
1331
|
+
new_value = src.indexes
|
|
1332
|
+
if new_value == 1 or new_value == (1,):
|
|
1333
|
+
new_value = 1
|
|
1334
|
+
self._indexes = new_value
|
|
1335
|
+
|
|
1336
|
+
if not hasattr(self, "_nodata") or self._nodata is None:
|
|
1337
|
+
new_value = src.nodata
|
|
1338
|
+
self._nodata = new_value
|
|
1266
1339
|
|
|
1267
1340
|
# if not hasattr(self, "_indexes") or self._indexes is None:
|
|
1268
1341
|
# self._indexes = src.indexes
|
|
@@ -1293,7 +1366,7 @@ class Raster:
|
|
|
1293
1366
|
return self._read(self.path, **kwargs)
|
|
1294
1367
|
|
|
1295
1368
|
@functools.lru_cache(maxsize=128)
|
|
1296
|
-
def _read(self, path, **kwargs):
|
|
1369
|
+
def _read(self, path: str | Path, **kwargs) -> None:
|
|
1297
1370
|
with opener(path, file_system=self.file_system) as file:
|
|
1298
1371
|
with rasterio.open(file) as src:
|
|
1299
1372
|
self._add_meta_from_src(src)
|
|
@@ -1302,7 +1375,7 @@ class Raster:
|
|
|
1302
1375
|
if hasattr(self, "_warped_crs"):
|
|
1303
1376
|
src = WarpedVRT(src, crs=self.crs)
|
|
1304
1377
|
|
|
1305
|
-
self.
|
|
1378
|
+
self.values = src.read(
|
|
1306
1379
|
out_shape=out_shape,
|
|
1307
1380
|
**(self.read_kwargs | kwargs),
|
|
1308
1381
|
)
|
|
@@ -1311,7 +1384,9 @@ class Raster:
|
|
|
1311
1384
|
else:
|
|
1312
1385
|
self = self.as_minimum_dtype()
|
|
1313
1386
|
|
|
1314
|
-
def _read_with_mask(
|
|
1387
|
+
def _read_with_mask(
|
|
1388
|
+
self, mask: Any, masked: bool, boundless: bool, **kwargs
|
|
1389
|
+
) -> None:
|
|
1315
1390
|
kwargs["mask"] = mask
|
|
1316
1391
|
|
|
1317
1392
|
def _read(self, src, mask, **kwargs):
|
|
@@ -1332,11 +1407,17 @@ class Raster:
|
|
|
1332
1407
|
if hasattr(self, "_warped_crs"):
|
|
1333
1408
|
src = WarpedVRT(src, crs=self.crs)
|
|
1334
1409
|
|
|
1335
|
-
self.
|
|
1410
|
+
self.values = src.read(out_shape=out_shape, **kwargs)
|
|
1336
1411
|
|
|
1337
1412
|
if not masked:
|
|
1338
|
-
|
|
1339
|
-
|
|
1413
|
+
try:
|
|
1414
|
+
self.values[self.values.mask] = self.nodata
|
|
1415
|
+
self.values = self.values.data
|
|
1416
|
+
except AttributeError:
|
|
1417
|
+
pass
|
|
1418
|
+
# self.values = np.ma.masked_array(self.values, mask=mask)
|
|
1419
|
+
# self.values[self.values.mask] = self.nodata
|
|
1420
|
+
# self.values = self.values.data
|
|
1340
1421
|
|
|
1341
1422
|
if boundless:
|
|
1342
1423
|
self._bounds = src.window_bounds(window=window)
|
|
@@ -1347,7 +1428,7 @@ class Raster:
|
|
|
1347
1428
|
else:
|
|
1348
1429
|
self._bounds = intersected.bounds
|
|
1349
1430
|
|
|
1350
|
-
if not np.size(self.
|
|
1431
|
+
if not np.size(self.values):
|
|
1351
1432
|
return
|
|
1352
1433
|
|
|
1353
1434
|
if self._dtype:
|
|
@@ -1355,8 +1436,8 @@ class Raster:
|
|
|
1355
1436
|
else:
|
|
1356
1437
|
self = self.as_minimum_dtype()
|
|
1357
1438
|
|
|
1358
|
-
if self.
|
|
1359
|
-
with memfile_from_array(self.
|
|
1439
|
+
if self.values is not None:
|
|
1440
|
+
with memfile_from_array(self.values, **self.profile) as src:
|
|
1360
1441
|
_read(self, src, **kwargs)
|
|
1361
1442
|
else:
|
|
1362
1443
|
with opener(self.path, file_system=self.file_system) as file:
|
|
@@ -1364,7 +1445,7 @@ class Raster:
|
|
|
1364
1445
|
_read(self, src, **kwargs)
|
|
1365
1446
|
|
|
1366
1447
|
def _check_for_array(self, text=""):
|
|
1367
|
-
if self.
|
|
1448
|
+
if self.values is None:
|
|
1368
1449
|
raise ValueError("Arrays are not loaded. " + text)
|
|
1369
1450
|
|
|
1370
1451
|
|