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/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