ssb-sgis 1.0.4__py3-none-any.whl → 1.0.6__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 +5 -5
- sgis/debug_config.py +1 -0
- sgis/geopandas_tools/buffer_dissolve_explode.py +3 -40
- sgis/geopandas_tools/conversion.py +37 -9
- sgis/geopandas_tools/general.py +330 -106
- sgis/geopandas_tools/geometry_types.py +38 -33
- sgis/geopandas_tools/overlay.py +5 -1
- sgis/io/dapla_functions.py +33 -17
- sgis/maps/explore.py +16 -5
- sgis/maps/map.py +3 -0
- sgis/maps/maps.py +0 -1
- sgis/networkanalysis/closing_network_holes.py +100 -22
- sgis/networkanalysis/cutting_lines.py +4 -147
- sgis/networkanalysis/finding_isolated_networks.py +6 -0
- sgis/networkanalysis/nodes.py +4 -110
- sgis/parallel/parallel.py +267 -182
- sgis/raster/image_collection.py +789 -836
- sgis/raster/indices.py +0 -90
- sgis/raster/regex.py +146 -0
- sgis/raster/sentinel_config.py +9 -0
- {ssb_sgis-1.0.4.dist-info → ssb_sgis-1.0.6.dist-info}/METADATA +1 -1
- {ssb_sgis-1.0.4.dist-info → ssb_sgis-1.0.6.dist-info}/RECORD +24 -26
- sgis/raster/cube.py +0 -1274
- sgis/raster/cubebase.py +0 -25
- sgis/raster/raster.py +0 -1475
- {ssb_sgis-1.0.4.dist-info → ssb_sgis-1.0.6.dist-info}/LICENSE +0 -0
- {ssb_sgis-1.0.4.dist-info → ssb_sgis-1.0.6.dist-info}/WHEEL +0 -0
sgis/raster/raster.py
DELETED
|
@@ -1,1475 +0,0 @@
|
|
|
1
|
-
import functools
|
|
2
|
-
import numbers
|
|
3
|
-
import os
|
|
4
|
-
import re
|
|
5
|
-
import warnings
|
|
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
|
|
11
|
-
from json import loads
|
|
12
|
-
from pathlib import Path
|
|
13
|
-
from typing import Any
|
|
14
|
-
from typing import ClassVar
|
|
15
|
-
|
|
16
|
-
import geopandas as gpd
|
|
17
|
-
import matplotlib.pyplot as plt
|
|
18
|
-
import numpy as np
|
|
19
|
-
import pandas as pd
|
|
20
|
-
import pyproj
|
|
21
|
-
import rasterio
|
|
22
|
-
import rasterio.windows
|
|
23
|
-
import shapely
|
|
24
|
-
from typing_extensions import Self # TODO: imperter fra typing når python 3.11
|
|
25
|
-
|
|
26
|
-
try:
|
|
27
|
-
import xarray as xr
|
|
28
|
-
from xarray import DataArray
|
|
29
|
-
except ImportError:
|
|
30
|
-
|
|
31
|
-
class DataArray:
|
|
32
|
-
"""Placeholder."""
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
try:
|
|
36
|
-
from dapla.gcs import GCSFileSystem
|
|
37
|
-
except ImportError:
|
|
38
|
-
|
|
39
|
-
class GCSFileSystem:
|
|
40
|
-
"""Placeholder."""
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
try:
|
|
44
|
-
from rioxarray.rioxarray import _generate_spatial_coords
|
|
45
|
-
except ImportError:
|
|
46
|
-
pass
|
|
47
|
-
from affine import Affine
|
|
48
|
-
from geopandas import GeoDataFrame
|
|
49
|
-
from geopandas import GeoSeries
|
|
50
|
-
from pandas.api.types import is_list_like
|
|
51
|
-
from rasterio import features
|
|
52
|
-
from rasterio.enums import MergeAlg
|
|
53
|
-
from rasterio.io import DatasetReader
|
|
54
|
-
from rasterio.vrt import WarpedVRT
|
|
55
|
-
from rasterio.warp import reproject
|
|
56
|
-
from shapely import Geometry
|
|
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
|
|
66
|
-
from ..helpers import is_property
|
|
67
|
-
from ..io.opener import opener
|
|
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
|
|
77
|
-
|
|
78
|
-
numpy_func_message = (
|
|
79
|
-
"aggfunc must be functions or strings of numpy functions or methods."
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
class Raster:
|
|
84
|
-
"""For reading, writing and working with rasters.
|
|
85
|
-
|
|
86
|
-
Raster instances should be created with the methods 'from_path', 'from_array' or
|
|
87
|
-
'from_gdf'.
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
Examples:
|
|
91
|
-
---------
|
|
92
|
-
Read tif file.
|
|
93
|
-
|
|
94
|
-
>>> import sgis as sg
|
|
95
|
-
>>> path = 'https://media.githubusercontent.com/media/statisticsnorway/ssb-sgis/main/tests/testdata/raster/dtm_10.tif'
|
|
96
|
-
>>> raster = sg.Raster.from_path(path)
|
|
97
|
-
>>> raster
|
|
98
|
-
Raster(shape=(1, 201, 201), res=10, crs=ETRS89 / UTM zone 33N (N-E), path=https://media.githubusercontent.com/media/statisticsnorway/ssb-sgis/main/tests/testdata/raster/dtm_10.tif)
|
|
99
|
-
|
|
100
|
-
Load the entire image as an numpy ndarray.
|
|
101
|
-
Operations are done in place to save memory.
|
|
102
|
-
The array is stored in the array attribute.
|
|
103
|
-
|
|
104
|
-
>>> raster.load()
|
|
105
|
-
>>> raster.values[raster.values < 0] = 0
|
|
106
|
-
>>> raster.values
|
|
107
|
-
[[[ 0. 0. 0. ... 158.4 155.6 152.6]
|
|
108
|
-
[ 0. 0. 0. ... 158. 154.8 151.9]
|
|
109
|
-
[ 0. 0. 0. ... 158.5 155.1 152.3]
|
|
110
|
-
...
|
|
111
|
-
[ 0. 150.2 150.6 ... 0. 0. 0. ]
|
|
112
|
-
[ 0. 149.9 150.1 ... 0. 0. 0. ]
|
|
113
|
-
[ 0. 149.2 149.5 ... 0. 0. 0. ]]]
|
|
114
|
-
|
|
115
|
-
Save as tif file.
|
|
116
|
-
|
|
117
|
-
>>> raster.write("path/to/file.tif")
|
|
118
|
-
|
|
119
|
-
Convert to GeoDataFrame.
|
|
120
|
-
|
|
121
|
-
>>> gdf = raster.to_gdf(column="elevation")
|
|
122
|
-
>>> gdf
|
|
123
|
-
elevation geometry indexes
|
|
124
|
-
0 1.9 POLYGON ((-25665.000 6676005.000, -25665.000 6... 1
|
|
125
|
-
1 11.0 POLYGON ((-25655.000 6676005.000, -25655.000 6... 1
|
|
126
|
-
2 18.1 POLYGON ((-25645.000 6676005.000, -25645.000 6... 1
|
|
127
|
-
3 15.8 POLYGON ((-25635.000 6676005.000, -25635.000 6... 1
|
|
128
|
-
4 11.6 POLYGON ((-25625.000 6676005.000, -25625.000 6... 1
|
|
129
|
-
... ... ... ...
|
|
130
|
-
25096 13.4 POLYGON ((-24935.000 6674005.000, -24935.000 6... 1
|
|
131
|
-
25097 9.4 POLYGON ((-24925.000 6674005.000, -24925.000 6... 1
|
|
132
|
-
25098 5.3 POLYGON ((-24915.000 6674005.000, -24915.000 6... 1
|
|
133
|
-
25099 2.3 POLYGON ((-24905.000 6674005.000, -24905.000 6... 1
|
|
134
|
-
25100 0.1 POLYGON ((-24895.000 6674005.000, -24895.000 6... 1
|
|
135
|
-
|
|
136
|
-
The image can also be clipped by a mask while loading.
|
|
137
|
-
|
|
138
|
-
>>> small_circle = raster_as_polygons.union_all().centroid.buffer(50)
|
|
139
|
-
>>> raster = sg.Raster.from_path(path).clip(small_circle)
|
|
140
|
-
Raster(shape=(1, 10, 10), res=10, crs=ETRS89 / UTM zone 33N (N-E), path=https://media.githubusercontent.com/media/statisticsnorway/ssb-sgis/main/tests/testdata/raster/dtm_10.tif)
|
|
141
|
-
|
|
142
|
-
Construct raster from GeoDataFrame.
|
|
143
|
-
The arrays are put on top of each other in a 3 dimensional array.
|
|
144
|
-
|
|
145
|
-
>>> raster_as_polygons["elevation_x2"] = raster_as_polygons["elevation"] * 2
|
|
146
|
-
>>> raster_from_polygons = sg.Raster.from_gdf(raster_as_polygons, columns=["elevation", "elevation_x2"], res=20)
|
|
147
|
-
>>> raster_from_polygons
|
|
148
|
-
Raster(shape=(2, 100, 100), res=20, raster_id=-260056673995, crs=ETRS89 / UTM zone 33N (N-E), path=None)
|
|
149
|
-
|
|
150
|
-
Calculate zonal statistics for each polygon in 'gdf'.
|
|
151
|
-
|
|
152
|
-
>>> zonal = raster.zonal(raster_as_polygons, aggfunc=["sum", np.mean])
|
|
153
|
-
>>> zonal
|
|
154
|
-
sum mean geometry
|
|
155
|
-
0 1.9 1.9 POLYGON ((-25665.000 6676005.000, -25665.000 6...
|
|
156
|
-
1 11.0 11.0 POLYGON ((-25655.000 6676005.000, -25655.000 6...
|
|
157
|
-
2 18.1 18.1 POLYGON ((-25645.000 6676005.000, -25645.000 6...
|
|
158
|
-
3 15.8 15.8 POLYGON ((-25635.000 6676005.000, -25635.000 6...
|
|
159
|
-
4 11.6 11.6 POLYGON ((-25625.000 6676005.000, -25625.000 6...
|
|
160
|
-
... ... ... ...
|
|
161
|
-
25096 13.4 13.4 POLYGON ((-24935.000 6674005.000, -24935.000 6...
|
|
162
|
-
25097 9.4 9.4 POLYGON ((-24925.000 6674005.000, -24925.000 6...
|
|
163
|
-
25098 5.3 5.3 POLYGON ((-24915.000 6674005.000, -24915.000 6...
|
|
164
|
-
25099 2.3 2.3 POLYGON ((-24905.000 6674005.000, -24905.000 6...
|
|
165
|
-
25100 0.1 0.1 POLYGON ((-24895.000 6674005.000, -24895.000 6...
|
|
166
|
-
|
|
167
|
-
"""
|
|
168
|
-
|
|
169
|
-
# attributes concerning rasterio metadata
|
|
170
|
-
_profile: ClassVar[dict[str, str | None]] = {
|
|
171
|
-
"driver": "GTiff",
|
|
172
|
-
"compress": "LZW",
|
|
173
|
-
"nodata": None,
|
|
174
|
-
"dtype": None,
|
|
175
|
-
"crs": None,
|
|
176
|
-
"tiled": None,
|
|
177
|
-
"indexes": None,
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
def __init__(
|
|
181
|
-
self,
|
|
182
|
-
data: Self | str | np.ndarray | None = None,
|
|
183
|
-
*,
|
|
184
|
-
file_system: GCSFileSystem | None = None,
|
|
185
|
-
filename_regex: str | None = None,
|
|
186
|
-
**kwargs,
|
|
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)
|
|
207
|
-
return
|
|
208
|
-
|
|
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
|
-
):
|
|
223
|
-
raise TypeError(
|
|
224
|
-
"Must specify either bounds or transform when constructing raster from array."
|
|
225
|
-
)
|
|
226
|
-
|
|
227
|
-
# add class profile first, then override with args and kwargs
|
|
228
|
-
self.update(**self._profile)
|
|
229
|
-
|
|
230
|
-
self._crs = kwargs.pop("crs", self._crs if hasattr(self, "_crs") else None)
|
|
231
|
-
self._bounds = None
|
|
232
|
-
self.file_system = file_system
|
|
233
|
-
self._indexes = self._get_indexes(kwargs.pop("indexes", self.indexes))
|
|
234
|
-
|
|
235
|
-
# override the above with kwargs
|
|
236
|
-
self.update(**kwargs)
|
|
237
|
-
|
|
238
|
-
attributes = set(self.__dict__.keys()).difference(set(self.properties))
|
|
239
|
-
|
|
240
|
-
if self.path is not None and not self._has_nessecary_attrs(attributes):
|
|
241
|
-
self._add_meta()
|
|
242
|
-
self._meta_added = True
|
|
243
|
-
|
|
244
|
-
self._prev_crs = self._crs
|
|
245
|
-
|
|
246
|
-
@classmethod
|
|
247
|
-
def from_path(
|
|
248
|
-
cls,
|
|
249
|
-
path: str,
|
|
250
|
-
res: int | None = None,
|
|
251
|
-
file_system: GCSFileSystem | None = None,
|
|
252
|
-
filename_regex: str | None = None,
|
|
253
|
-
**kwargs,
|
|
254
|
-
) -> Self:
|
|
255
|
-
"""Construct Raster from file path.
|
|
256
|
-
|
|
257
|
-
Args:
|
|
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.
|
|
264
|
-
|
|
265
|
-
Returns:
|
|
266
|
-
A Raster instance.
|
|
267
|
-
"""
|
|
268
|
-
return cls(
|
|
269
|
-
str(path),
|
|
270
|
-
file_system=file_system,
|
|
271
|
-
res=res,
|
|
272
|
-
filename_regex=filename_regex,
|
|
273
|
-
**kwargs,
|
|
274
|
-
)
|
|
275
|
-
|
|
276
|
-
@classmethod
|
|
277
|
-
def from_array(
|
|
278
|
-
cls,
|
|
279
|
-
array: np.ndarray,
|
|
280
|
-
crs: Any,
|
|
281
|
-
*,
|
|
282
|
-
transform: Affine | None = None,
|
|
283
|
-
bounds: tuple | Geometry | None = None,
|
|
284
|
-
copy: bool = True,
|
|
285
|
-
**kwargs,
|
|
286
|
-
) -> Self:
|
|
287
|
-
"""Construct Raster from numpy array.
|
|
288
|
-
|
|
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.
|
|
292
|
-
|
|
293
|
-
Args:
|
|
294
|
-
array: 2d or 3d numpy ndarray.
|
|
295
|
-
crs: Coordinate reference system.
|
|
296
|
-
transform: Affine transform object. Can be specified instead
|
|
297
|
-
of bounds.
|
|
298
|
-
bounds: Minimum and maximum x and y coordinates. Can be specified instead
|
|
299
|
-
of transform.
|
|
300
|
-
copy: Whether to copy the array.
|
|
301
|
-
**kwargs: Arguments concerning file metadata or
|
|
302
|
-
spatial properties of the image.
|
|
303
|
-
|
|
304
|
-
Returns:
|
|
305
|
-
A Raster instance.
|
|
306
|
-
"""
|
|
307
|
-
if array is None:
|
|
308
|
-
raise TypeError("Must specify array.")
|
|
309
|
-
|
|
310
|
-
if not any([transform, bounds]):
|
|
311
|
-
raise TypeError(
|
|
312
|
-
"Must specify either bounds or transform when constructing raster from array."
|
|
313
|
-
)
|
|
314
|
-
|
|
315
|
-
array = array.copy() if copy else array
|
|
316
|
-
|
|
317
|
-
if len(array.shape) == 2:
|
|
318
|
-
height, width = array.shape
|
|
319
|
-
elif len(array.shape) == 3:
|
|
320
|
-
height, width = array.shape[1:]
|
|
321
|
-
else:
|
|
322
|
-
raise ValueError("array must be 2 or 3 dimensional.")
|
|
323
|
-
|
|
324
|
-
transform = Affine(*transform) if transform is not None else None
|
|
325
|
-
|
|
326
|
-
if bounds is not None:
|
|
327
|
-
bounds = to_bbox(bounds)
|
|
328
|
-
|
|
329
|
-
if transform and not bounds:
|
|
330
|
-
bounds = rasterio.transform.array_bounds(height, width, transform)
|
|
331
|
-
|
|
332
|
-
crs = pyproj.CRS(crs) if crs else None
|
|
333
|
-
|
|
334
|
-
return cls(array, crs=crs, transform=transform, bounds=bounds, **kwargs)
|
|
335
|
-
|
|
336
|
-
@classmethod
|
|
337
|
-
def from_gdf(
|
|
338
|
-
cls,
|
|
339
|
-
gdf: GeoDataFrame,
|
|
340
|
-
columns: str | Iterable[str],
|
|
341
|
-
res: int,
|
|
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,
|
|
347
|
-
**kwargs,
|
|
348
|
-
) -> Self:
|
|
349
|
-
"""Construct Raster from a GeoDataFrame.
|
|
350
|
-
|
|
351
|
-
Args:
|
|
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.
|
|
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.
|
|
374
|
-
"""
|
|
375
|
-
if not isinstance(gdf, GeoDataFrame):
|
|
376
|
-
gdf = to_gdf(gdf)
|
|
377
|
-
|
|
378
|
-
if "transform" in kwargs:
|
|
379
|
-
raise TypeError("Unexpected argument 'transform'")
|
|
380
|
-
|
|
381
|
-
kwargs["crs"] = gdf.crs or kwargs.get("crs")
|
|
382
|
-
|
|
383
|
-
if kwargs["crs"] is None:
|
|
384
|
-
raise TypeError("Must specify crs if the object doesn't have crs.")
|
|
385
|
-
|
|
386
|
-
shape = get_shape_from_bounds(gdf.total_bounds, res=res)
|
|
387
|
-
transform = get_transform_from_bounds(gdf.total_bounds, shape)
|
|
388
|
-
kwargs["transform"] = transform
|
|
389
|
-
|
|
390
|
-
def _rasterize(gdf, col):
|
|
391
|
-
return features.rasterize(
|
|
392
|
-
cls._gdf_to_geojson_with_col(gdf, col),
|
|
393
|
-
out_shape=shape,
|
|
394
|
-
transform=transform,
|
|
395
|
-
fill=fill,
|
|
396
|
-
all_touched=all_touched,
|
|
397
|
-
merge_alg=merge_alg,
|
|
398
|
-
default_value=default_value,
|
|
399
|
-
dtype=dtype,
|
|
400
|
-
)
|
|
401
|
-
|
|
402
|
-
# make 2d array
|
|
403
|
-
if isinstance(columns, str):
|
|
404
|
-
array = _rasterize(gdf, columns)
|
|
405
|
-
assert len(array.shape) == 2
|
|
406
|
-
name = kwargs.get("name", columns)
|
|
407
|
-
|
|
408
|
-
# 3d array even if single column in list/tuple
|
|
409
|
-
elif hasattr(columns, "__iter__"):
|
|
410
|
-
array = []
|
|
411
|
-
for col in columns:
|
|
412
|
-
arr = _rasterize(gdf, col)
|
|
413
|
-
array.append(arr)
|
|
414
|
-
array = np.array(array)
|
|
415
|
-
assert len(array.shape) == 3
|
|
416
|
-
name = kwargs.get("name", None)
|
|
417
|
-
|
|
418
|
-
return cls.from_array(array, name=name, **kwargs)
|
|
419
|
-
|
|
420
|
-
@classmethod
|
|
421
|
-
def from_dict(cls, dictionary: dict) -> Self:
|
|
422
|
-
"""Construct Raster from metadata dict to fastpass the initializer.
|
|
423
|
-
|
|
424
|
-
This is the fastest way to create a Raster since a metadata lookup is not
|
|
425
|
-
needed.
|
|
426
|
-
|
|
427
|
-
The dictionary must have all the keys ...
|
|
428
|
-
and at least one of the keys 'transform' and 'bounds'.
|
|
429
|
-
|
|
430
|
-
Args:
|
|
431
|
-
dictionary: Dictionary with the nessecary and optional information
|
|
432
|
-
about the raster. This can be fetched from an existing raster with
|
|
433
|
-
the to_dict method.
|
|
434
|
-
|
|
435
|
-
Returns:
|
|
436
|
-
A Raster instance.
|
|
437
|
-
"""
|
|
438
|
-
cls._validate_dict(dictionary)
|
|
439
|
-
|
|
440
|
-
return cls(**dictionary)
|
|
441
|
-
|
|
442
|
-
def update(self, **kwargs) -> Self:
|
|
443
|
-
"""Update attributes of the Raster."""
|
|
444
|
-
for key, value in kwargs.items():
|
|
445
|
-
self._validate_key(key)
|
|
446
|
-
if is_property(self, key):
|
|
447
|
-
key = "_" + key
|
|
448
|
-
setattr(self, key, value)
|
|
449
|
-
return self
|
|
450
|
-
|
|
451
|
-
def write(
|
|
452
|
-
self, path: str, window: rasterio.windows.Window | None = None, **kwargs
|
|
453
|
-
) -> None:
|
|
454
|
-
"""Write the raster as a single file.
|
|
455
|
-
|
|
456
|
-
Multiband arrays will result in a multiband image file.
|
|
457
|
-
|
|
458
|
-
Args:
|
|
459
|
-
path: File path to write to.
|
|
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.
|
|
464
|
-
"""
|
|
465
|
-
if self.values is None:
|
|
466
|
-
raise AttributeError("The image hasn't been loaded.")
|
|
467
|
-
|
|
468
|
-
profile = self.profile | kwargs
|
|
469
|
-
|
|
470
|
-
with opener(path, "wb", file_system=self.file_system) as file:
|
|
471
|
-
with rasterio.open(file, "w", **profile) as dst:
|
|
472
|
-
self._write(dst, window)
|
|
473
|
-
|
|
474
|
-
self.path = str(path)
|
|
475
|
-
|
|
476
|
-
def load(self, reload: bool = False, **kwargs) -> Self:
|
|
477
|
-
"""Load the entire image as an np.array.
|
|
478
|
-
|
|
479
|
-
The array is stored in the 'array' attribute
|
|
480
|
-
of the Raster.
|
|
481
|
-
|
|
482
|
-
Args:
|
|
483
|
-
reload: Whether to reload the array if already loaded.
|
|
484
|
-
**kwargs: Keyword arguments passed to the rasterio read
|
|
485
|
-
method.
|
|
486
|
-
"""
|
|
487
|
-
if "mask" in kwargs:
|
|
488
|
-
raise ValueError("Got an unexpected keyword argument 'mask'")
|
|
489
|
-
if "window" in kwargs:
|
|
490
|
-
raise ValueError("Got an unexpected keyword argument 'window'")
|
|
491
|
-
|
|
492
|
-
if reload or self.values is None:
|
|
493
|
-
self._read_tif(**kwargs)
|
|
494
|
-
|
|
495
|
-
return self
|
|
496
|
-
|
|
497
|
-
def clip(
|
|
498
|
-
self,
|
|
499
|
-
mask: Any,
|
|
500
|
-
masked: bool = False,
|
|
501
|
-
boundless: bool = True,
|
|
502
|
-
**kwargs,
|
|
503
|
-
) -> Self:
|
|
504
|
-
"""Load the part of the image inside the mask.
|
|
505
|
-
|
|
506
|
-
The returned array is stored in the 'array' attribute
|
|
507
|
-
of the Raster.
|
|
508
|
-
|
|
509
|
-
Args:
|
|
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.
|
|
518
|
-
**kwargs: Keyword arguments passed to the mask function
|
|
519
|
-
from the rasterio.mask module.
|
|
520
|
-
|
|
521
|
-
Returns:
|
|
522
|
-
Self, but with the array loaded.
|
|
523
|
-
"""
|
|
524
|
-
if not isinstance(mask, GeoDataFrame):
|
|
525
|
-
mask = self._return_gdf(mask)
|
|
526
|
-
|
|
527
|
-
try:
|
|
528
|
-
mask = mask.to_crs(self.crs)
|
|
529
|
-
except ValueError:
|
|
530
|
-
mask = mask.set_crs(self.crs)
|
|
531
|
-
|
|
532
|
-
self._read_with_mask(mask=mask, masked=masked, boundless=boundless, **kwargs)
|
|
533
|
-
|
|
534
|
-
return self
|
|
535
|
-
|
|
536
|
-
def intersects(self, other: Any) -> bool:
|
|
537
|
-
"""Returns True if the image bounds intersect with 'other'."""
|
|
538
|
-
return self.union_all().intersects(to_shapely(other))
|
|
539
|
-
|
|
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."""
|
|
544
|
-
if mask is not None:
|
|
545
|
-
points = GeoSeries(self.union_all()).clip(mask).sample_points(n)
|
|
546
|
-
else:
|
|
547
|
-
points = GeoSeries(self.union_all()).sample_points(n)
|
|
548
|
-
buffered = points.buffer(size / self.res)
|
|
549
|
-
boxes = to_gdf(
|
|
550
|
-
[shapely.box(*arr) for arr in buffered.bounds.values], crs=self.crs
|
|
551
|
-
)
|
|
552
|
-
if copy:
|
|
553
|
-
copy = self.copy()
|
|
554
|
-
return copy.clip(boxes, **kwargs)
|
|
555
|
-
return self.clip(boxes, **kwargs)
|
|
556
|
-
|
|
557
|
-
def zonal(
|
|
558
|
-
self,
|
|
559
|
-
polygons: GeoDataFrame,
|
|
560
|
-
aggfunc: str | Callable | list[Callable | str],
|
|
561
|
-
array_func: Callable | None = None,
|
|
562
|
-
dropna: bool = True,
|
|
563
|
-
) -> GeoDataFrame:
|
|
564
|
-
"""Calculate zonal statistics in polygons.
|
|
565
|
-
|
|
566
|
-
Args:
|
|
567
|
-
polygons: A GeoDataFrame of polygon geometries.
|
|
568
|
-
aggfunc: Function(s) of which to aggregate the values
|
|
569
|
-
within each polygon.
|
|
570
|
-
array_func: Optional calculation of the raster
|
|
571
|
-
array before calculating the zonal statistics.
|
|
572
|
-
dropna: If True (default), polygons with all missing
|
|
573
|
-
values will be removed.
|
|
574
|
-
|
|
575
|
-
Returns:
|
|
576
|
-
A GeoDataFrame with aggregated values per polygon.
|
|
577
|
-
"""
|
|
578
|
-
idx_mapper, idx_name = get_index_mapper(polygons)
|
|
579
|
-
polygons, aggfunc, func_names = _prepare_zonal(polygons, aggfunc)
|
|
580
|
-
poly_iter = _make_geometry_iterrows(polygons)
|
|
581
|
-
|
|
582
|
-
aggregated = []
|
|
583
|
-
for i, poly in poly_iter:
|
|
584
|
-
clipped = self.clip(poly)
|
|
585
|
-
if not np.size(clipped.values):
|
|
586
|
-
aggregated.append(_no_overlap_df(func_names, i, date=self.date))
|
|
587
|
-
aggregated.append(
|
|
588
|
-
_aggregate(
|
|
589
|
-
clipped.values, array_func, aggfunc, func_names, self.date, i
|
|
590
|
-
)
|
|
591
|
-
)
|
|
592
|
-
|
|
593
|
-
return _zonal_post(
|
|
594
|
-
aggregated,
|
|
595
|
-
polygons=polygons,
|
|
596
|
-
idx_mapper=idx_mapper,
|
|
597
|
-
idx_name=idx_name,
|
|
598
|
-
dropna=dropna,
|
|
599
|
-
)
|
|
600
|
-
|
|
601
|
-
def to_xarray(self) -> DataArray:
|
|
602
|
-
"""Convert the raster to an xarray.DataArray."""
|
|
603
|
-
self._check_for_array()
|
|
604
|
-
self.name = self.name or self.__class__.__name__.lower()
|
|
605
|
-
coords = _generate_spatial_coords(self.transform, self.width, self.height)
|
|
606
|
-
if len(self.values.shape) == 2:
|
|
607
|
-
dims = ["y", "x"]
|
|
608
|
-
# dims = ["band", "y", "x"]
|
|
609
|
-
# array = np.array([self.values])
|
|
610
|
-
# assert len(array.shape) == 3
|
|
611
|
-
elif len(self.values.shape) == 3:
|
|
612
|
-
dims = ["band", "y", "x"]
|
|
613
|
-
# array = self.values
|
|
614
|
-
else:
|
|
615
|
-
raise ValueError("Array must be 2 or 3 dimensional.")
|
|
616
|
-
return xr.DataArray(
|
|
617
|
-
self.values,
|
|
618
|
-
coords=coords,
|
|
619
|
-
dims=dims,
|
|
620
|
-
name=self.name,
|
|
621
|
-
attrs={"crs": self.crs},
|
|
622
|
-
) # .transpose("y", "x")
|
|
623
|
-
|
|
624
|
-
def to_dict(self) -> dict:
|
|
625
|
-
"""Get a dictionary of Raster attributes."""
|
|
626
|
-
out = {}
|
|
627
|
-
for col in self.ALL_ATTRS:
|
|
628
|
-
try:
|
|
629
|
-
out[col] = self[col]
|
|
630
|
-
except AttributeError:
|
|
631
|
-
pass
|
|
632
|
-
return out
|
|
633
|
-
|
|
634
|
-
def to_gdf(self, column: str | list[str] | None = None) -> GeoDataFrame:
|
|
635
|
-
"""Create a GeoDataFrame from the raster.
|
|
636
|
-
|
|
637
|
-
For multiband rasters, the bands are in separate rows with a "band" column
|
|
638
|
-
value corresponding to the band indexes of the raster.
|
|
639
|
-
|
|
640
|
-
Args:
|
|
641
|
-
column: Name of resulting column(s) that holds the raster values.
|
|
642
|
-
Can be a single string or an iterable with the same length as
|
|
643
|
-
the number of raster bands.
|
|
644
|
-
|
|
645
|
-
Returns:
|
|
646
|
-
A GeoDataFrame with a geometry column, a 'band' column and a
|
|
647
|
-
one or more value columns.
|
|
648
|
-
"""
|
|
649
|
-
self._check_for_array()
|
|
650
|
-
|
|
651
|
-
array_list = self.array_list()
|
|
652
|
-
|
|
653
|
-
if is_list_like(column) and len(column) != len(array_list):
|
|
654
|
-
raise ValueError(
|
|
655
|
-
"columns should be a string or a list of same length as "
|
|
656
|
-
f"layers in the array ({len(array_list)})."
|
|
657
|
-
)
|
|
658
|
-
|
|
659
|
-
if column is None:
|
|
660
|
-
column = ["value"] * len(array_list)
|
|
661
|
-
|
|
662
|
-
if isinstance(column, str):
|
|
663
|
-
column = [column] * len(array_list)
|
|
664
|
-
|
|
665
|
-
gdfs = []
|
|
666
|
-
for i, (col, array) in enumerate(zip(column, array_list, strict=True)):
|
|
667
|
-
gdf = gpd.GeoDataFrame(
|
|
668
|
-
pd.DataFrame(
|
|
669
|
-
self._array_to_geojson(array, self.transform),
|
|
670
|
-
columns=[col, "geometry"],
|
|
671
|
-
),
|
|
672
|
-
geometry="geometry",
|
|
673
|
-
crs=self.crs,
|
|
674
|
-
)
|
|
675
|
-
gdf["indexes"] = i + 1
|
|
676
|
-
gdfs.append(gdf)
|
|
677
|
-
|
|
678
|
-
return pd.concat(gdfs, ignore_index=True)
|
|
679
|
-
|
|
680
|
-
def set_crs(
|
|
681
|
-
self,
|
|
682
|
-
crs: pyproj.CRS | Any,
|
|
683
|
-
allow_override: bool = False,
|
|
684
|
-
) -> Self:
|
|
685
|
-
"""Set coordinate reference system."""
|
|
686
|
-
if not allow_override and self.crs is not None:
|
|
687
|
-
raise ValueError("Cannot overwrite crs when allow_override is False.")
|
|
688
|
-
|
|
689
|
-
if self.values is None:
|
|
690
|
-
raise ValueError("array must be loaded/clipped before set_crs")
|
|
691
|
-
|
|
692
|
-
self._crs = pyproj.CRS(crs)
|
|
693
|
-
return self
|
|
694
|
-
|
|
695
|
-
def to_crs(self, crs: pyproj.CRS | Any, **kwargs) -> Self:
|
|
696
|
-
"""Reproject the raster.
|
|
697
|
-
|
|
698
|
-
Args:
|
|
699
|
-
crs: The new coordinate reference system.
|
|
700
|
-
**kwargs: Keyword arguments passed to the reproject function
|
|
701
|
-
from the rasterio.warp module.
|
|
702
|
-
"""
|
|
703
|
-
if self.crs is None:
|
|
704
|
-
raise ValueError("Raster has no crs. Use set_crs.")
|
|
705
|
-
|
|
706
|
-
# if pyproj.CRS(crs).equals(pyproj.CRS(self._crs)) and pyproj.CRS(crs).equals(
|
|
707
|
-
# pyproj.CRS(self._prev_crs)
|
|
708
|
-
# ):
|
|
709
|
-
# return self
|
|
710
|
-
|
|
711
|
-
if self.values is None:
|
|
712
|
-
project = pyproj.Transformer.from_crs(
|
|
713
|
-
pyproj.CRS(self._prev_crs), pyproj.CRS(crs), always_xy=True
|
|
714
|
-
).transform
|
|
715
|
-
|
|
716
|
-
old_box = shapely.box(*self.bounds)
|
|
717
|
-
new_box = shapely.ops.transform(project, old_box)
|
|
718
|
-
self._bounds = to_bbox(new_box)
|
|
719
|
-
|
|
720
|
-
# TODO: fix area changing... if possible
|
|
721
|
-
# print("old/new:", shapely.area(old_box) / shapely.area(new_box))
|
|
722
|
-
|
|
723
|
-
if pyproj.CRS(crs).equals(pyproj.CRS(self._crs)):
|
|
724
|
-
self._warped_crs = self._crs
|
|
725
|
-
return self
|
|
726
|
-
|
|
727
|
-
# self._bounds = rasterio.warp.transform_bounds(
|
|
728
|
-
# pyproj.CRS(self._prev_crs), pyproj.CRS(crs), *to_bbox(self._bounds)
|
|
729
|
-
# )
|
|
730
|
-
# transformer = pyproj.Transformer.from_crs(
|
|
731
|
-
# pyproj.CRS(self._prev_crs), pyproj.CRS(crs), always_xy=True
|
|
732
|
-
# )
|
|
733
|
-
# minx, miny, maxx, maxy = self.bounds
|
|
734
|
-
# xs, ys = transformer.transform(xx=[minx, maxx], yy=[miny, maxy])
|
|
735
|
-
|
|
736
|
-
# minx, maxx = xs
|
|
737
|
-
# miny, maxy = ys
|
|
738
|
-
# self._bounds = minx, miny, maxx, maxy
|
|
739
|
-
|
|
740
|
-
# self._bounds = shapely.transform(old_box, project)
|
|
741
|
-
else:
|
|
742
|
-
was_2d = len(self.shape) == 2
|
|
743
|
-
self.values, transform = reproject(
|
|
744
|
-
source=self.values,
|
|
745
|
-
src_crs=self._prev_crs,
|
|
746
|
-
src_transform=self.transform,
|
|
747
|
-
dst_crs=pyproj.CRS(crs),
|
|
748
|
-
**kwargs,
|
|
749
|
-
)
|
|
750
|
-
if was_2d and len(self.values.shape) == 3:
|
|
751
|
-
assert self.values.shape[0] == 1
|
|
752
|
-
self.values = self.values[0]
|
|
753
|
-
|
|
754
|
-
self._bounds = rasterio.transform.array_bounds(
|
|
755
|
-
self.height, self.width, transform
|
|
756
|
-
)
|
|
757
|
-
|
|
758
|
-
self._warped_crs = pyproj.CRS(crs)
|
|
759
|
-
self._prev_crs = pyproj.CRS(crs)
|
|
760
|
-
|
|
761
|
-
return self
|
|
762
|
-
|
|
763
|
-
def plot(self, mask: Any | None = None) -> None:
|
|
764
|
-
"""Plot the images. One image per band."""
|
|
765
|
-
self._check_for_array()
|
|
766
|
-
if mask is not None:
|
|
767
|
-
raster = self.copy().clip(mask)
|
|
768
|
-
else:
|
|
769
|
-
raster = self
|
|
770
|
-
|
|
771
|
-
if len(raster.shape) == 2:
|
|
772
|
-
array = np.array([raster.values])
|
|
773
|
-
else:
|
|
774
|
-
array = raster.values
|
|
775
|
-
|
|
776
|
-
for arr in array:
|
|
777
|
-
ax = plt.axes()
|
|
778
|
-
ax.imshow(arr)
|
|
779
|
-
ax.axis("off")
|
|
780
|
-
plt.show()
|
|
781
|
-
plt.close()
|
|
782
|
-
|
|
783
|
-
def astype(self, dtype: type) -> Self:
|
|
784
|
-
"""Convert the datatype of the array."""
|
|
785
|
-
if self.values is None:
|
|
786
|
-
raise ValueError("Array is not loaded.")
|
|
787
|
-
if not rasterio.dtypes.can_cast_dtype(self.values, dtype):
|
|
788
|
-
min_dtype = rasterio.dtypes.get_minimum_dtype(self.values)
|
|
789
|
-
raise ValueError(f"Cannot cast to dtype. Minimum dtype is {min_dtype}")
|
|
790
|
-
self.values = self.values.astype(dtype)
|
|
791
|
-
self._dtype = dtype
|
|
792
|
-
return self
|
|
793
|
-
|
|
794
|
-
def as_minimum_dtype(self) -> Self:
|
|
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)
|
|
798
|
-
return self
|
|
799
|
-
|
|
800
|
-
def min(self) -> int | None:
|
|
801
|
-
"""Minimum value in the array."""
|
|
802
|
-
if np.size(self.values):
|
|
803
|
-
return np.min(self.values)
|
|
804
|
-
return None
|
|
805
|
-
|
|
806
|
-
def max(self) -> int | None:
|
|
807
|
-
"""Maximum value in the array."""
|
|
808
|
-
if np.size(self.values):
|
|
809
|
-
return np.max(self.values)
|
|
810
|
-
return None
|
|
811
|
-
|
|
812
|
-
def _add_meta(self) -> Self:
|
|
813
|
-
mess = "Cannot add metadata after image has been "
|
|
814
|
-
if hasattr(self, "_clipped"):
|
|
815
|
-
raise ValueError(mess + "clipped.")
|
|
816
|
-
if hasattr(self, "_warped_crs"):
|
|
817
|
-
raise ValueError(mess + "reprojected.")
|
|
818
|
-
|
|
819
|
-
with opener(self.path, file_system=self.file_system) as file:
|
|
820
|
-
with rasterio.open(file) as src:
|
|
821
|
-
self._add_meta_from_src(src)
|
|
822
|
-
|
|
823
|
-
return self
|
|
824
|
-
|
|
825
|
-
def array_list(self) -> list[np.ndarray]:
|
|
826
|
-
"""Get a list of 2D arrays."""
|
|
827
|
-
self._check_for_array()
|
|
828
|
-
if len(self.values.shape) == 2:
|
|
829
|
-
return [self.values]
|
|
830
|
-
elif len(self.values.shape) == 3:
|
|
831
|
-
return list(self.values)
|
|
832
|
-
else:
|
|
833
|
-
raise ValueError
|
|
834
|
-
|
|
835
|
-
@property
|
|
836
|
-
def indexes(self) -> int | tuple[int] | None:
|
|
837
|
-
"""Band indexes of the image."""
|
|
838
|
-
return self._indexes
|
|
839
|
-
|
|
840
|
-
@property
|
|
841
|
-
def name(self) -> str | None:
|
|
842
|
-
"""Name of the file in the file path, if any."""
|
|
843
|
-
try:
|
|
844
|
-
return self._name
|
|
845
|
-
except AttributeError:
|
|
846
|
-
try:
|
|
847
|
-
return Path(self.path).name
|
|
848
|
-
except TypeError:
|
|
849
|
-
return None
|
|
850
|
-
|
|
851
|
-
@name.setter
|
|
852
|
-
def name(self, value) -> None:
|
|
853
|
-
self._name = value
|
|
854
|
-
|
|
855
|
-
@property
|
|
856
|
-
def date(self) -> str | None:
|
|
857
|
-
"""Date in the image file name, if filename_regex is present."""
|
|
858
|
-
try:
|
|
859
|
-
return re.match(self.filename_pattern, Path(self.path).name).group("date")
|
|
860
|
-
except (AttributeError, TypeError):
|
|
861
|
-
return None
|
|
862
|
-
|
|
863
|
-
@property
|
|
864
|
-
def band(self) -> str | None:
|
|
865
|
-
"""Band name of the image file name, if filename_regex is present."""
|
|
866
|
-
try:
|
|
867
|
-
return re.match(self.filename_pattern, Path(self.path).name).group("band")
|
|
868
|
-
except (AttributeError, TypeError):
|
|
869
|
-
return None
|
|
870
|
-
|
|
871
|
-
@property
|
|
872
|
-
def dtype(self) -> Any:
|
|
873
|
-
"""Data type of the array."""
|
|
874
|
-
try:
|
|
875
|
-
return self.values.dtype
|
|
876
|
-
except AttributeError:
|
|
877
|
-
try:
|
|
878
|
-
return self._dtype
|
|
879
|
-
except AttributeError:
|
|
880
|
-
return None
|
|
881
|
-
|
|
882
|
-
@dtype.setter
|
|
883
|
-
def dtype(self, new_dtype: Any) -> None:
|
|
884
|
-
self.values = self.values.astype(new_dtype)
|
|
885
|
-
|
|
886
|
-
@property
|
|
887
|
-
def nodata(self) -> int | None:
|
|
888
|
-
"""No data value."""
|
|
889
|
-
try:
|
|
890
|
-
return self._nodata
|
|
891
|
-
except AttributeError:
|
|
892
|
-
return None
|
|
893
|
-
|
|
894
|
-
@property
|
|
895
|
-
def tile(self) -> str | None:
|
|
896
|
-
"""Tile name from regex."""
|
|
897
|
-
try:
|
|
898
|
-
return re.match(self.filename_pattern, Path(self.path).name).group("tile")
|
|
899
|
-
except (AttributeError, TypeError):
|
|
900
|
-
return None
|
|
901
|
-
|
|
902
|
-
@property
|
|
903
|
-
def meta(self) -> dict:
|
|
904
|
-
"""Metadata dict."""
|
|
905
|
-
return {
|
|
906
|
-
"path": self.path,
|
|
907
|
-
"type": self.__class__.__name__,
|
|
908
|
-
"bounds": self.bounds,
|
|
909
|
-
"indexes": self.indexes,
|
|
910
|
-
"crs": self.crs,
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
@property
|
|
914
|
-
def profile(self) -> dict:
|
|
915
|
-
"""Profile of the image file."""
|
|
916
|
-
# TODO: .crs blir feil hvis warpa. Eller?
|
|
917
|
-
return {
|
|
918
|
-
"driver": self.driver,
|
|
919
|
-
"compress": self.compress,
|
|
920
|
-
"dtype": self.dtype,
|
|
921
|
-
"crs": self.crs,
|
|
922
|
-
"transform": self.transform,
|
|
923
|
-
"nodata": self.nodata,
|
|
924
|
-
"count": self.count,
|
|
925
|
-
"height": self.height,
|
|
926
|
-
"width": self.width,
|
|
927
|
-
"indexes": self.indexes,
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
@property
|
|
931
|
-
def read_kwargs(self) -> dict:
|
|
932
|
-
"""Keywords passed to the read method of rasterio.io.DatasetReader."""
|
|
933
|
-
return {
|
|
934
|
-
"indexes": self.indexes,
|
|
935
|
-
"fill_value": self.nodata,
|
|
936
|
-
"masked": False,
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
@property
|
|
940
|
-
def res(self) -> float | None:
|
|
941
|
-
"""Get the spatial resolution of the image."""
|
|
942
|
-
if hasattr(self, "_res") and self._res is not None:
|
|
943
|
-
return self._res
|
|
944
|
-
if self.width is None:
|
|
945
|
-
return None
|
|
946
|
-
diffx = self.bounds[2] - self.bounds[0]
|
|
947
|
-
return diffx / self.width
|
|
948
|
-
|
|
949
|
-
@property
|
|
950
|
-
def height(self) -> int | None:
|
|
951
|
-
"""Get the height of the image as number of pixels."""
|
|
952
|
-
if self.values is None:
|
|
953
|
-
try:
|
|
954
|
-
return self._height
|
|
955
|
-
except AttributeError:
|
|
956
|
-
return None
|
|
957
|
-
i = 1 if len(self.values.shape) == 3 else 0
|
|
958
|
-
return self.values.shape[i]
|
|
959
|
-
|
|
960
|
-
@property
|
|
961
|
-
def width(self) -> int | None:
|
|
962
|
-
"""Get the width of the image as number of pixels."""
|
|
963
|
-
if self.values is None:
|
|
964
|
-
try:
|
|
965
|
-
return self._width
|
|
966
|
-
except AttributeError:
|
|
967
|
-
try:
|
|
968
|
-
heigth, width = get_shape_from_bounds(self, self.res) # .res[0])
|
|
969
|
-
self._width = width
|
|
970
|
-
self._heigth = heigth
|
|
971
|
-
return self._width
|
|
972
|
-
except Exception:
|
|
973
|
-
return None
|
|
974
|
-
i = 2 if len(self.values.shape) == 3 else 1
|
|
975
|
-
return self.values.shape[i]
|
|
976
|
-
|
|
977
|
-
@property
|
|
978
|
-
def count(self) -> int:
|
|
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:
|
|
984
|
-
return 1
|
|
985
|
-
if not hasattr(self._indexes, "__iter__"):
|
|
986
|
-
return 1
|
|
987
|
-
return len(self._indexes)
|
|
988
|
-
|
|
989
|
-
@property
|
|
990
|
-
def shape(self) -> tuple[int]:
|
|
991
|
-
"""Shape that is consistent with the array, whether it is loaded or not."""
|
|
992
|
-
if self.values is not None:
|
|
993
|
-
return self.values.shape
|
|
994
|
-
if hasattr(self._indexes, "__iter__"):
|
|
995
|
-
return self.count, self.width, self.height
|
|
996
|
-
return self.width, self.height
|
|
997
|
-
|
|
998
|
-
@property
|
|
999
|
-
def transform(self) -> Affine | None:
|
|
1000
|
-
"""Get the Affine transform of the image."""
|
|
1001
|
-
try:
|
|
1002
|
-
return rasterio.transform.from_bounds(*self.bounds, self.width, self.height)
|
|
1003
|
-
except (ZeroDivisionError, TypeError):
|
|
1004
|
-
if not self.width or not self.height:
|
|
1005
|
-
return None
|
|
1006
|
-
|
|
1007
|
-
@property
|
|
1008
|
-
def bounds(self) -> tuple[float, float, float, float] | None:
|
|
1009
|
-
"""Get the bounds of the image."""
|
|
1010
|
-
try:
|
|
1011
|
-
return to_bbox(self._bounds)
|
|
1012
|
-
except (AttributeError, TypeError):
|
|
1013
|
-
return None
|
|
1014
|
-
|
|
1015
|
-
@property
|
|
1016
|
-
def crs(self) -> pyproj.CRS | None:
|
|
1017
|
-
"""Get the coordinate reference system of the image."""
|
|
1018
|
-
try:
|
|
1019
|
-
return self._warped_crs
|
|
1020
|
-
except AttributeError:
|
|
1021
|
-
try:
|
|
1022
|
-
return self._crs
|
|
1023
|
-
except AttributeError:
|
|
1024
|
-
return None
|
|
1025
|
-
|
|
1026
|
-
@property
|
|
1027
|
-
def area(self) -> float:
|
|
1028
|
-
"""Get the area of the image."""
|
|
1029
|
-
return shapely.area(self.union_all())
|
|
1030
|
-
|
|
1031
|
-
@property
|
|
1032
|
-
def length(self) -> float:
|
|
1033
|
-
"""Get the circumfence of the image."""
|
|
1034
|
-
return shapely.length(self.union_all())
|
|
1035
|
-
|
|
1036
|
-
@property
|
|
1037
|
-
def unary_union(self) -> Polygon:
|
|
1038
|
-
"""Get the image bounds as a Polygon."""
|
|
1039
|
-
return shapely.box(*self.bounds)
|
|
1040
|
-
|
|
1041
|
-
@property
|
|
1042
|
-
def centroid(self) -> Point:
|
|
1043
|
-
"""Get the centerpoint of the image."""
|
|
1044
|
-
x = (self.bounds[0] + self.bounds[2]) / 2
|
|
1045
|
-
y = (self.bounds[1] + self.bounds[3]) / 2
|
|
1046
|
-
return Point(x, y)
|
|
1047
|
-
|
|
1048
|
-
@property
|
|
1049
|
-
def properties(self) -> list[str]:
|
|
1050
|
-
"""List of all properties of the class."""
|
|
1051
|
-
out = []
|
|
1052
|
-
for attr in dir(self):
|
|
1053
|
-
try:
|
|
1054
|
-
if is_property(self, attr):
|
|
1055
|
-
out.append(attr)
|
|
1056
|
-
except AttributeError:
|
|
1057
|
-
pass
|
|
1058
|
-
return out
|
|
1059
|
-
|
|
1060
|
-
def indexes_as_tuple(self) -> tuple[int, ...]:
|
|
1061
|
-
"""Get the band index(es) as a tuple of integers."""
|
|
1062
|
-
if len(self.shape) == 2:
|
|
1063
|
-
return (1,)
|
|
1064
|
-
return tuple(i + 1 for i in range(self.shape[0]))
|
|
1065
|
-
|
|
1066
|
-
def copy(self, deep: bool = True) -> "Raster":
|
|
1067
|
-
"""Returns a (deep) copy of the class instance.
|
|
1068
|
-
|
|
1069
|
-
Args:
|
|
1070
|
-
deep: Whether to return a deep or shallow copy. Defaults to True.
|
|
1071
|
-
"""
|
|
1072
|
-
if deep:
|
|
1073
|
-
return deepcopy(self)
|
|
1074
|
-
else:
|
|
1075
|
-
return copy(self)
|
|
1076
|
-
|
|
1077
|
-
def equals(self, other: Any) -> bool:
|
|
1078
|
-
"""Check if the Raster is equal to another Raster."""
|
|
1079
|
-
if not isinstance(other, Raster):
|
|
1080
|
-
raise NotImplementedError("other must be of type Raster")
|
|
1081
|
-
if type(other) is not type(self):
|
|
1082
|
-
return False
|
|
1083
|
-
if self.values is None and other.values is not None:
|
|
1084
|
-
return False
|
|
1085
|
-
if self.values is not None and other.values is None:
|
|
1086
|
-
return False
|
|
1087
|
-
|
|
1088
|
-
for method in dir(self):
|
|
1089
|
-
if not is_property(self, method):
|
|
1090
|
-
continue
|
|
1091
|
-
if getattr(self, method) != getattr(other, method):
|
|
1092
|
-
return False
|
|
1093
|
-
|
|
1094
|
-
return np.array_equal(self.values, other.values)
|
|
1095
|
-
|
|
1096
|
-
def __repr__(self) -> str:
|
|
1097
|
-
"""The print representation."""
|
|
1098
|
-
shape = self.shape
|
|
1099
|
-
shp = ", ".join([str(x) for x in shape])
|
|
1100
|
-
try:
|
|
1101
|
-
res = int(self.res)
|
|
1102
|
-
except TypeError:
|
|
1103
|
-
res = None
|
|
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
|
-
)
|
|
1115
|
-
|
|
1116
|
-
def __mul__(self, scalar: int | float) -> "Raster":
|
|
1117
|
-
"""Multiply the array values with *."""
|
|
1118
|
-
self._check_for_array()
|
|
1119
|
-
self.values = self.values * scalar
|
|
1120
|
-
return self
|
|
1121
|
-
|
|
1122
|
-
def __add__(self, scalar: int | float) -> "Raster":
|
|
1123
|
-
"""Add to the array values with +."""
|
|
1124
|
-
self._check_for_array()
|
|
1125
|
-
self.values = self.values + scalar
|
|
1126
|
-
return self
|
|
1127
|
-
|
|
1128
|
-
def __sub__(self, scalar: int | float) -> "Raster":
|
|
1129
|
-
"""Subtract the array values with -."""
|
|
1130
|
-
self._check_for_array()
|
|
1131
|
-
self.values = self.values - scalar
|
|
1132
|
-
return self
|
|
1133
|
-
|
|
1134
|
-
def __truediv__(self, scalar: int | float) -> "Raster":
|
|
1135
|
-
"""Divide the array values with /."""
|
|
1136
|
-
self._check_for_array()
|
|
1137
|
-
self.values = self.values / scalar
|
|
1138
|
-
return self
|
|
1139
|
-
|
|
1140
|
-
def __floordiv__(self, scalar: int | float) -> "Raster":
|
|
1141
|
-
"""Floor divide the array values with //."""
|
|
1142
|
-
self._check_for_array()
|
|
1143
|
-
self.values = self.values // scalar
|
|
1144
|
-
return self
|
|
1145
|
-
|
|
1146
|
-
def __pow__(self, exponent: int | float) -> "Raster":
|
|
1147
|
-
"""Exponentiate the array values with **."""
|
|
1148
|
-
self._check_for_array()
|
|
1149
|
-
self.values = self.values**exponent
|
|
1150
|
-
return self
|
|
1151
|
-
|
|
1152
|
-
def _has_nessecary_attrs(self, dict_like: dict) -> bool:
|
|
1153
|
-
"""Check if Raster init got enough kwargs to not need to read src."""
|
|
1154
|
-
try:
|
|
1155
|
-
self._validate_dict(dict_like)
|
|
1156
|
-
return all(
|
|
1157
|
-
x is not None for x in [self.indexes, self.res, self.crs, self.bounds]
|
|
1158
|
-
)
|
|
1159
|
-
except AttributeError:
|
|
1160
|
-
return False
|
|
1161
|
-
|
|
1162
|
-
def _return_self_or_copy(self, array: np.ndarray, copy: bool) -> "Raster":
|
|
1163
|
-
if not copy:
|
|
1164
|
-
self.values = array
|
|
1165
|
-
return self
|
|
1166
|
-
else:
|
|
1167
|
-
copy = self.copy()
|
|
1168
|
-
copy.values = array
|
|
1169
|
-
return copy
|
|
1170
|
-
|
|
1171
|
-
@classmethod
|
|
1172
|
-
def _validate_dict(cls, dict_like: dict) -> None:
|
|
1173
|
-
missing = []
|
|
1174
|
-
for attr in NESSECARY_META:
|
|
1175
|
-
if any(
|
|
1176
|
-
[
|
|
1177
|
-
attr in dict_like,
|
|
1178
|
-
f"_{attr}" in dict_like,
|
|
1179
|
-
attr.lstrip("_") in dict_like,
|
|
1180
|
-
]
|
|
1181
|
-
):
|
|
1182
|
-
continue
|
|
1183
|
-
missing.append(attr)
|
|
1184
|
-
if missing:
|
|
1185
|
-
raise AttributeError(f"Missing nessecary key(s) {', '.join(missing)}")
|
|
1186
|
-
|
|
1187
|
-
@classmethod
|
|
1188
|
-
def _validate_key(cls, key: str) -> None:
|
|
1189
|
-
if key not in ALLOWED_KEYS:
|
|
1190
|
-
raise ValueError(
|
|
1191
|
-
f"Got an unexpected key {key!r}. Allowed keys are ",
|
|
1192
|
-
", ".join(ALLOWED_KEYS),
|
|
1193
|
-
)
|
|
1194
|
-
|
|
1195
|
-
def _get_shape_from_res(self, res: int) -> tuple[int] | None:
|
|
1196
|
-
if res is None:
|
|
1197
|
-
return None
|
|
1198
|
-
if hasattr(res, "__iter__") and len(res) == 2:
|
|
1199
|
-
res = res[0]
|
|
1200
|
-
diffx = self.bounds[2] - self.bounds[0]
|
|
1201
|
-
diffy = self.bounds[3] - self.bounds[1]
|
|
1202
|
-
width = int(diffx / res)
|
|
1203
|
-
height = int(diffy / res)
|
|
1204
|
-
if hasattr(self.indexes, "__iter__"):
|
|
1205
|
-
return len(self.indexes), width, height
|
|
1206
|
-
return width, height
|
|
1207
|
-
|
|
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:
|
|
1213
|
-
return dst.write(
|
|
1214
|
-
self.values.filled(self.nodata), indexes=1, window=window
|
|
1215
|
-
)
|
|
1216
|
-
|
|
1217
|
-
for i in range(len(self.indexes_as_tuple())):
|
|
1218
|
-
dst.write(
|
|
1219
|
-
self.values[i].filled(self.nodata),
|
|
1220
|
-
indexes=i + 1,
|
|
1221
|
-
window=window,
|
|
1222
|
-
)
|
|
1223
|
-
|
|
1224
|
-
else:
|
|
1225
|
-
if len(self.values.shape) == 2:
|
|
1226
|
-
return dst.write(self.values, indexes=1, window=window)
|
|
1227
|
-
|
|
1228
|
-
for i, idx in enumerate(self.indexes_as_tuple()):
|
|
1229
|
-
dst.write(self.values[i], indexes=idx, window=window)
|
|
1230
|
-
|
|
1231
|
-
def _get_indexes(self, indexes: int | tuple[int] | None) -> int | tuple[int] | None:
|
|
1232
|
-
if isinstance(indexes, numbers.Number):
|
|
1233
|
-
return int(indexes)
|
|
1234
|
-
if indexes is None:
|
|
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:
|
|
1238
|
-
return 1
|
|
1239
|
-
elif self.values is not None:
|
|
1240
|
-
raise ValueError("Array must be 2 or 3 dimensional.")
|
|
1241
|
-
else:
|
|
1242
|
-
return None
|
|
1243
|
-
try:
|
|
1244
|
-
return tuple(int(x) for x in indexes)
|
|
1245
|
-
except Exception as e:
|
|
1246
|
-
raise TypeError(
|
|
1247
|
-
"indexes should be an integer or an iterable of integers."
|
|
1248
|
-
f"Got {type(indexes)}: {indexes}"
|
|
1249
|
-
) from e
|
|
1250
|
-
|
|
1251
|
-
def _return_gdf(self, obj: Any) -> GeoDataFrame:
|
|
1252
|
-
if isinstance(obj, str) and not is_wkt(obj):
|
|
1253
|
-
return self._read_tif(obj)
|
|
1254
|
-
elif isinstance(obj, Raster):
|
|
1255
|
-
return obj.to_gdf()
|
|
1256
|
-
elif is_bbox_like(obj):
|
|
1257
|
-
return to_gdf(shapely.box(*to_bbox(obj)), crs=self.crs)
|
|
1258
|
-
else:
|
|
1259
|
-
return to_gdf(obj, crs=self.crs)
|
|
1260
|
-
|
|
1261
|
-
@staticmethod
|
|
1262
|
-
def _gdf_to_geojson(gdf: GeoDataFrame) -> list[dict]:
|
|
1263
|
-
with warnings.catch_warnings():
|
|
1264
|
-
warnings.filterwarnings("ignore", category=UserWarning)
|
|
1265
|
-
return [x["geometry"] for x in loads(gdf.to_json())["features"]]
|
|
1266
|
-
|
|
1267
|
-
@staticmethod
|
|
1268
|
-
def _gdf_to_geojson_with_col(gdf: GeoDataFrame, column: str) -> list[dict]:
|
|
1269
|
-
with warnings.catch_warnings():
|
|
1270
|
-
warnings.filterwarnings("ignore", category=UserWarning)
|
|
1271
|
-
return [
|
|
1272
|
-
(feature["geometry"], val)
|
|
1273
|
-
for val, feature in zip(
|
|
1274
|
-
gdf[column], loads(gdf.to_json())["features"], strict=False
|
|
1275
|
-
)
|
|
1276
|
-
]
|
|
1277
|
-
|
|
1278
|
-
@staticmethod
|
|
1279
|
-
def _array_to_geojson(array: np.ndarray, transform: Affine) -> list[tuple]:
|
|
1280
|
-
if np.ma.is_masked(array):
|
|
1281
|
-
array = array.data
|
|
1282
|
-
try:
|
|
1283
|
-
return [
|
|
1284
|
-
(value, shape(geom))
|
|
1285
|
-
for geom, value in features.shapes(
|
|
1286
|
-
array, transform=transform, mask=None
|
|
1287
|
-
)
|
|
1288
|
-
]
|
|
1289
|
-
except ValueError:
|
|
1290
|
-
array = array.astype(np.float32)
|
|
1291
|
-
return [
|
|
1292
|
-
(value, shape(geom))
|
|
1293
|
-
for geom, value in features.shapes(
|
|
1294
|
-
array, transform=transform, mask=None
|
|
1295
|
-
)
|
|
1296
|
-
]
|
|
1297
|
-
|
|
1298
|
-
def _add_indexes_from_array(self, indexes: int | tuple[int]) -> int | tuple[int]:
|
|
1299
|
-
if indexes is not None:
|
|
1300
|
-
return indexes
|
|
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:
|
|
1304
|
-
return 1
|
|
1305
|
-
else:
|
|
1306
|
-
raise ValueError
|
|
1307
|
-
|
|
1308
|
-
def _add_meta_from_src(self, src: rasterio.io.DatasetReader) -> None:
|
|
1309
|
-
if not hasattr(self, "_bounds") or self._bounds is None:
|
|
1310
|
-
self._bounds = tuple(src.bounds)
|
|
1311
|
-
|
|
1312
|
-
try:
|
|
1313
|
-
self._crs = pyproj.CRS(src.crs)
|
|
1314
|
-
except pyproj.exceptions.CRSError:
|
|
1315
|
-
self._crs = None
|
|
1316
|
-
|
|
1317
|
-
self._width = src.width
|
|
1318
|
-
self._height = src.height
|
|
1319
|
-
|
|
1320
|
-
# for attr in dir(self):
|
|
1321
|
-
# try:
|
|
1322
|
-
# if is_property(self, attr):
|
|
1323
|
-
# continue
|
|
1324
|
-
# if attr is None:
|
|
1325
|
-
# new_value = getattr(src, attr)
|
|
1326
|
-
# setattr(self, attr, new_value)
|
|
1327
|
-
# except AttributeError:
|
|
1328
|
-
# pass
|
|
1329
|
-
|
|
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
|
|
1339
|
-
|
|
1340
|
-
# if not hasattr(self, "_indexes") or self._indexes is None:
|
|
1341
|
-
# self._indexes = src.indexes
|
|
1342
|
-
|
|
1343
|
-
# if not hasattr(self, "_nodata") or self._nodata is None:
|
|
1344
|
-
# self._nodata = src.nodata
|
|
1345
|
-
|
|
1346
|
-
def _load_warp_file(self) -> DatasetReader:
|
|
1347
|
-
"""(from Torchgeo). Load and warp a file to the correct CRS and resolution.
|
|
1348
|
-
|
|
1349
|
-
Args:
|
|
1350
|
-
filepath: file to load and warp
|
|
1351
|
-
|
|
1352
|
-
Returns:
|
|
1353
|
-
file handle of warped VRT
|
|
1354
|
-
"""
|
|
1355
|
-
with opener(self.path, file_system=self.file_system) as file:
|
|
1356
|
-
src = rasterio.open(file)
|
|
1357
|
-
|
|
1358
|
-
# Only warp if necessary
|
|
1359
|
-
if src.crs != self.crs:
|
|
1360
|
-
vrt = WarpedVRT(src, crs=self.crs)
|
|
1361
|
-
src.close()
|
|
1362
|
-
return vrt
|
|
1363
|
-
return src
|
|
1364
|
-
|
|
1365
|
-
def _read_tif(self, **kwargs) -> None:
|
|
1366
|
-
return self._read(self.path, **kwargs)
|
|
1367
|
-
|
|
1368
|
-
@functools.lru_cache(maxsize=128)
|
|
1369
|
-
def _read(self, path: str | Path, **kwargs) -> None:
|
|
1370
|
-
with opener(path, file_system=self.file_system) as file:
|
|
1371
|
-
with rasterio.open(file) as src:
|
|
1372
|
-
self._add_meta_from_src(src)
|
|
1373
|
-
out_shape = self._get_shape_from_res(self.res)
|
|
1374
|
-
|
|
1375
|
-
if hasattr(self, "_warped_crs"):
|
|
1376
|
-
src = WarpedVRT(src, crs=self.crs)
|
|
1377
|
-
|
|
1378
|
-
self.values = src.read(
|
|
1379
|
-
out_shape=out_shape,
|
|
1380
|
-
**(self.read_kwargs | kwargs),
|
|
1381
|
-
)
|
|
1382
|
-
if self._dtype:
|
|
1383
|
-
self = self.astype(self.dtype)
|
|
1384
|
-
else:
|
|
1385
|
-
self = self.as_minimum_dtype()
|
|
1386
|
-
|
|
1387
|
-
def _read_with_mask(
|
|
1388
|
-
self, mask: Any, masked: bool, boundless: bool, **kwargs
|
|
1389
|
-
) -> None:
|
|
1390
|
-
kwargs["mask"] = mask
|
|
1391
|
-
|
|
1392
|
-
def _read(self, src, mask, **kwargs):
|
|
1393
|
-
self._add_meta_from_src(src)
|
|
1394
|
-
if self.bounds is None:
|
|
1395
|
-
self._bounds = to_bbox(mask)
|
|
1396
|
-
|
|
1397
|
-
window = rasterio.windows.from_bounds(
|
|
1398
|
-
*to_bbox(mask), transform=self.transform
|
|
1399
|
-
)
|
|
1400
|
-
|
|
1401
|
-
out_shape = get_shape_from_bounds(mask, self.res)
|
|
1402
|
-
|
|
1403
|
-
kwargs = (
|
|
1404
|
-
{"window": window, "boundless": boundless} | self.read_kwargs | kwargs
|
|
1405
|
-
)
|
|
1406
|
-
|
|
1407
|
-
if hasattr(self, "_warped_crs"):
|
|
1408
|
-
src = WarpedVRT(src, crs=self.crs)
|
|
1409
|
-
|
|
1410
|
-
self.values = src.read(out_shape=out_shape, **kwargs)
|
|
1411
|
-
|
|
1412
|
-
if not masked:
|
|
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
|
|
1421
|
-
|
|
1422
|
-
if boundless:
|
|
1423
|
-
self._bounds = src.window_bounds(window=window)
|
|
1424
|
-
else:
|
|
1425
|
-
intersected = to_shapely(self.bounds).intersection(to_shapely(mask))
|
|
1426
|
-
if intersected.is_empty:
|
|
1427
|
-
self._bounds = None
|
|
1428
|
-
else:
|
|
1429
|
-
self._bounds = intersected.bounds
|
|
1430
|
-
|
|
1431
|
-
if not np.size(self.values):
|
|
1432
|
-
return
|
|
1433
|
-
|
|
1434
|
-
if self._dtype:
|
|
1435
|
-
self = self.astype(self._dtype)
|
|
1436
|
-
else:
|
|
1437
|
-
self = self.as_minimum_dtype()
|
|
1438
|
-
|
|
1439
|
-
if self.values is not None:
|
|
1440
|
-
with memfile_from_array(self.values, **self.profile) as src:
|
|
1441
|
-
_read(self, src, **kwargs)
|
|
1442
|
-
else:
|
|
1443
|
-
with opener(self.path, file_system=self.file_system) as file:
|
|
1444
|
-
with rasterio.open(file, **self.profile) as src:
|
|
1445
|
-
_read(self, src, **kwargs)
|
|
1446
|
-
|
|
1447
|
-
def _check_for_array(self, text=""):
|
|
1448
|
-
if self.values is None:
|
|
1449
|
-
raise ValueError("Arrays are not loaded. " + text)
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
def get_transform_from_bounds(
|
|
1453
|
-
obj: GeoDataFrame | GeoSeries | Geometry | tuple, shape: tuple[float, ...]
|
|
1454
|
-
) -> Affine:
|
|
1455
|
-
minx, miny, maxx, maxy = to_bbox(obj)
|
|
1456
|
-
if len(shape) == 2:
|
|
1457
|
-
width, height = shape
|
|
1458
|
-
elif len(shape) == 3:
|
|
1459
|
-
_, width, height = shape
|
|
1460
|
-
else:
|
|
1461
|
-
raise ValueError
|
|
1462
|
-
return rasterio.transform.from_bounds(minx, miny, maxx, maxy, width, height)
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
def get_shape_from_bounds(
|
|
1466
|
-
obj: GeoDataFrame | GeoSeries | Geometry | tuple, res: int
|
|
1467
|
-
) -> tuple[int, int]:
|
|
1468
|
-
resx, resy = (res, res) if isinstance(res, numbers.Number) else res
|
|
1469
|
-
|
|
1470
|
-
minx, miny, maxx, maxy = to_bbox(obj)
|
|
1471
|
-
diffx = maxx - minx
|
|
1472
|
-
diffy = maxy - miny
|
|
1473
|
-
width = int(diffx / resx)
|
|
1474
|
-
heigth = int(diffy / resy)
|
|
1475
|
-
return heigth, width
|