ssb-sgis 1.0.2__py3-none-any.whl → 1.0.4__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 +20 -9
- sgis/debug_config.py +24 -0
- sgis/exceptions.py +2 -2
- sgis/geopandas_tools/bounds.py +33 -36
- sgis/geopandas_tools/buffer_dissolve_explode.py +136 -35
- sgis/geopandas_tools/centerlines.py +4 -91
- sgis/geopandas_tools/cleaning.py +1576 -583
- sgis/geopandas_tools/conversion.py +38 -19
- sgis/geopandas_tools/duplicates.py +29 -8
- sgis/geopandas_tools/general.py +263 -100
- sgis/geopandas_tools/geometry_types.py +4 -4
- sgis/geopandas_tools/neighbors.py +19 -15
- sgis/geopandas_tools/overlay.py +2 -2
- sgis/geopandas_tools/point_operations.py +5 -5
- sgis/geopandas_tools/polygon_operations.py +510 -105
- sgis/geopandas_tools/polygons_as_rings.py +40 -8
- sgis/geopandas_tools/sfilter.py +29 -12
- sgis/helpers.py +3 -3
- sgis/io/dapla_functions.py +238 -19
- sgis/io/read_parquet.py +1 -1
- sgis/maps/examine.py +27 -12
- sgis/maps/explore.py +450 -65
- sgis/maps/legend.py +177 -76
- sgis/maps/map.py +206 -103
- sgis/maps/maps.py +178 -105
- sgis/maps/thematicmap.py +243 -83
- sgis/networkanalysis/_service_area.py +6 -1
- sgis/networkanalysis/closing_network_holes.py +2 -2
- sgis/networkanalysis/cutting_lines.py +15 -8
- sgis/networkanalysis/directednetwork.py +1 -1
- sgis/networkanalysis/finding_isolated_networks.py +15 -8
- sgis/networkanalysis/networkanalysis.py +17 -19
- sgis/networkanalysis/networkanalysisrules.py +1 -1
- sgis/networkanalysis/traveling_salesman.py +1 -1
- sgis/parallel/parallel.py +64 -27
- sgis/raster/__init__.py +0 -6
- sgis/raster/base.py +208 -0
- sgis/raster/cube.py +54 -8
- sgis/raster/image_collection.py +3257 -0
- sgis/raster/indices.py +17 -5
- sgis/raster/raster.py +138 -243
- sgis/raster/sentinel_config.py +120 -0
- sgis/raster/zonal.py +0 -1
- {ssb_sgis-1.0.2.dist-info → ssb_sgis-1.0.4.dist-info}/METADATA +6 -7
- ssb_sgis-1.0.4.dist-info/RECORD +62 -0
- sgis/raster/methods_as_functions.py +0 -0
- sgis/raster/torchgeo.py +0 -171
- ssb_sgis-1.0.2.dist-info/RECORD +0 -61
- {ssb_sgis-1.0.2.dist-info → ssb_sgis-1.0.4.dist-info}/LICENSE +0 -0
- {ssb_sgis-1.0.2.dist-info → ssb_sgis-1.0.4.dist-info}/WHEEL +0 -0
sgis/raster/raster.py
CHANGED
|
@@ -88,7 +88,7 @@ class Raster:
|
|
|
88
88
|
|
|
89
89
|
|
|
90
90
|
Examples:
|
|
91
|
-
|
|
91
|
+
---------
|
|
92
92
|
Read tif file.
|
|
93
93
|
|
|
94
94
|
>>> import sgis as sg
|
|
@@ -102,8 +102,8 @@ class Raster:
|
|
|
102
102
|
The array is stored in the array attribute.
|
|
103
103
|
|
|
104
104
|
>>> raster.load()
|
|
105
|
-
>>> raster.
|
|
106
|
-
>>> raster.
|
|
105
|
+
>>> raster.values[raster.values < 0] = 0
|
|
106
|
+
>>> raster.values
|
|
107
107
|
[[[ 0. 0. 0. ... 158.4 155.6 152.6]
|
|
108
108
|
[ 0. 0. 0. ... 158. 154.8 151.9]
|
|
109
109
|
[ 0. 0. 0. ... 158.5 155.1 152.3]
|
|
@@ -135,7 +135,7 @@ class Raster:
|
|
|
135
135
|
|
|
136
136
|
The image can also be clipped by a mask while loading.
|
|
137
137
|
|
|
138
|
-
>>> small_circle = raster_as_polygons.
|
|
138
|
+
>>> small_circle = raster_as_polygons.union_all().centroid.buffer(50)
|
|
139
139
|
>>> raster = sg.Raster.from_path(path).clip(small_circle)
|
|
140
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
141
|
|
|
@@ -194,7 +194,12 @@ class Raster:
|
|
|
194
194
|
**kwargs: Arguments concerning file metadata or
|
|
195
195
|
spatial properties of the image.
|
|
196
196
|
"""
|
|
197
|
+
warnings.warn("This class is deprecated in favor of Band", stacklevel=1)
|
|
197
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
|
|
198
203
|
|
|
199
204
|
if isinstance(data, Raster):
|
|
200
205
|
for key, value in data.__dict__.items():
|
|
@@ -208,9 +213,9 @@ class Raster:
|
|
|
208
213
|
self.path = None
|
|
209
214
|
|
|
210
215
|
if isinstance(data, (np.ndarray)):
|
|
211
|
-
self.
|
|
216
|
+
self.values = data
|
|
212
217
|
else:
|
|
213
|
-
self.
|
|
218
|
+
self.values = None
|
|
214
219
|
|
|
215
220
|
if self.path is None and not any(
|
|
216
221
|
[kwargs.get("transform"), kwargs.get("bounds")]
|
|
@@ -457,24 +462,25 @@ class Raster:
|
|
|
457
462
|
Thise will override the items in the Raster's profile,
|
|
458
463
|
if overlapping.
|
|
459
464
|
"""
|
|
460
|
-
if self.
|
|
465
|
+
if self.values is None:
|
|
461
466
|
raise AttributeError("The image hasn't been loaded.")
|
|
462
467
|
|
|
463
468
|
profile = self.profile | kwargs
|
|
464
469
|
|
|
465
|
-
with opener(path, file_system=self.file_system) as file:
|
|
470
|
+
with opener(path, "wb", file_system=self.file_system) as file:
|
|
466
471
|
with rasterio.open(file, "w", **profile) as dst:
|
|
467
472
|
self._write(dst, window)
|
|
468
473
|
|
|
469
474
|
self.path = str(path)
|
|
470
475
|
|
|
471
|
-
def load(self, **kwargs) -> Self:
|
|
476
|
+
def load(self, reload: bool = False, **kwargs) -> Self:
|
|
472
477
|
"""Load the entire image as an np.array.
|
|
473
478
|
|
|
474
479
|
The array is stored in the 'array' attribute
|
|
475
480
|
of the Raster.
|
|
476
481
|
|
|
477
482
|
Args:
|
|
483
|
+
reload: Whether to reload the array if already loaded.
|
|
478
484
|
**kwargs: Keyword arguments passed to the rasterio read
|
|
479
485
|
method.
|
|
480
486
|
"""
|
|
@@ -483,7 +489,8 @@ class Raster:
|
|
|
483
489
|
if "window" in kwargs:
|
|
484
490
|
raise ValueError("Got an unexpected keyword argument 'window'")
|
|
485
491
|
|
|
486
|
-
self.
|
|
492
|
+
if reload or self.values is None:
|
|
493
|
+
self._read_tif(**kwargs)
|
|
487
494
|
|
|
488
495
|
return self
|
|
489
496
|
|
|
@@ -528,16 +535,16 @@ class Raster:
|
|
|
528
535
|
|
|
529
536
|
def intersects(self, other: Any) -> bool:
|
|
530
537
|
"""Returns True if the image bounds intersect with 'other'."""
|
|
531
|
-
return self.
|
|
538
|
+
return self.union_all().intersects(to_shapely(other))
|
|
532
539
|
|
|
533
540
|
def sample(
|
|
534
541
|
self, n: int = 1, size: int = 20, mask: Any = None, copy: bool = True, **kwargs
|
|
535
542
|
) -> Self:
|
|
536
543
|
"""Take a random spatial sample of the image."""
|
|
537
544
|
if mask is not None:
|
|
538
|
-
points = GeoSeries(self.
|
|
545
|
+
points = GeoSeries(self.union_all()).clip(mask).sample_points(n)
|
|
539
546
|
else:
|
|
540
|
-
points = GeoSeries(self.
|
|
547
|
+
points = GeoSeries(self.union_all()).sample_points(n)
|
|
541
548
|
buffered = points.buffer(size / self.res)
|
|
542
549
|
boxes = to_gdf(
|
|
543
550
|
[shapely.box(*arr) for arr in buffered.bounds.values], crs=self.crs
|
|
@@ -575,10 +582,12 @@ class Raster:
|
|
|
575
582
|
aggregated = []
|
|
576
583
|
for i, poly in poly_iter:
|
|
577
584
|
clipped = self.clip(poly)
|
|
578
|
-
if not np.size(clipped.
|
|
585
|
+
if not np.size(clipped.values):
|
|
579
586
|
aggregated.append(_no_overlap_df(func_names, i, date=self.date))
|
|
580
587
|
aggregated.append(
|
|
581
|
-
_aggregate(
|
|
588
|
+
_aggregate(
|
|
589
|
+
clipped.values, array_func, aggfunc, func_names, self.date, i
|
|
590
|
+
)
|
|
582
591
|
)
|
|
583
592
|
|
|
584
593
|
return _zonal_post(
|
|
@@ -589,74 +598,23 @@ class Raster:
|
|
|
589
598
|
dropna=dropna,
|
|
590
599
|
)
|
|
591
600
|
|
|
592
|
-
def gradient(self, degrees: bool = False, copy: bool = False) -> Self:
|
|
593
|
-
"""Get the slope of an elevation raster.
|
|
594
|
-
|
|
595
|
-
Calculates the absolute slope between the grid cells
|
|
596
|
-
based on the image resolution.
|
|
597
|
-
|
|
598
|
-
For multiband images, the calculation is done for each band.
|
|
599
|
-
|
|
600
|
-
Args:
|
|
601
|
-
degrees: If False (default), the returned values will be in ratios,
|
|
602
|
-
where a value of 1 means 1 meter up per 1 meter forward. If True,
|
|
603
|
-
the values will be in degrees from 0 to 90.
|
|
604
|
-
copy: Whether to copy or overwrite the original Raster.
|
|
605
|
-
Defaults to False to save memory.
|
|
606
|
-
|
|
607
|
-
Returns:
|
|
608
|
-
The class instance with new array values, or a copy if copy is True.
|
|
609
|
-
|
|
610
|
-
Examples:
|
|
611
|
-
--------
|
|
612
|
-
Making an array where the gradient to the center is always 10.
|
|
613
|
-
|
|
614
|
-
>>> import sgis as sg
|
|
615
|
-
>>> import numpy as np
|
|
616
|
-
>>> arr = np.array(
|
|
617
|
-
... [
|
|
618
|
-
... [100, 100, 100, 100, 100],
|
|
619
|
-
... [100, 110, 110, 110, 100],
|
|
620
|
-
... [100, 110, 120, 110, 100],
|
|
621
|
-
... [100, 110, 110, 110, 100],
|
|
622
|
-
... [100, 100, 100, 100, 100],
|
|
623
|
-
... ]
|
|
624
|
-
... )
|
|
625
|
-
|
|
626
|
-
Now let's create a Raster from this array with a resolution of 10.
|
|
627
|
-
|
|
628
|
-
>>> r = sg.Raster.from_array(arr, crs=None, bounds=(0, 0, 50, 50))
|
|
629
|
-
|
|
630
|
-
The gradient will be 1 (1 meter up for every meter forward).
|
|
631
|
-
The calculation is by default done in place to save memory.
|
|
632
|
-
|
|
633
|
-
>>> r.gradient()
|
|
634
|
-
>>> r.array
|
|
635
|
-
array([[0., 1., 1., 1., 0.],
|
|
636
|
-
[1., 1., 1., 1., 1.],
|
|
637
|
-
[1., 1., 0., 1., 1.],
|
|
638
|
-
[1., 1., 1., 1., 1.],
|
|
639
|
-
[0., 1., 1., 1., 0.]])
|
|
640
|
-
"""
|
|
641
|
-
return get_gradient(self, degrees=degrees, copy=copy)
|
|
642
|
-
|
|
643
601
|
def to_xarray(self) -> DataArray:
|
|
644
602
|
"""Convert the raster to an xarray.DataArray."""
|
|
645
603
|
self._check_for_array()
|
|
646
604
|
self.name = self.name or self.__class__.__name__.lower()
|
|
647
605
|
coords = _generate_spatial_coords(self.transform, self.width, self.height)
|
|
648
|
-
if len(self.
|
|
606
|
+
if len(self.values.shape) == 2:
|
|
649
607
|
dims = ["y", "x"]
|
|
650
608
|
# dims = ["band", "y", "x"]
|
|
651
|
-
# array = np.array([self.
|
|
609
|
+
# array = np.array([self.values])
|
|
652
610
|
# assert len(array.shape) == 3
|
|
653
|
-
elif len(self.
|
|
611
|
+
elif len(self.values.shape) == 3:
|
|
654
612
|
dims = ["band", "y", "x"]
|
|
655
|
-
# array = self.
|
|
613
|
+
# array = self.values
|
|
656
614
|
else:
|
|
657
615
|
raise ValueError("Array must be 2 or 3 dimensional.")
|
|
658
616
|
return xr.DataArray(
|
|
659
|
-
self.
|
|
617
|
+
self.values,
|
|
660
618
|
coords=coords,
|
|
661
619
|
dims=dims,
|
|
662
620
|
name=self.name,
|
|
@@ -728,7 +686,7 @@ class Raster:
|
|
|
728
686
|
if not allow_override and self.crs is not None:
|
|
729
687
|
raise ValueError("Cannot overwrite crs when allow_override is False.")
|
|
730
688
|
|
|
731
|
-
if self.
|
|
689
|
+
if self.values is None:
|
|
732
690
|
raise ValueError("array must be loaded/clipped before set_crs")
|
|
733
691
|
|
|
734
692
|
self._crs = pyproj.CRS(crs)
|
|
@@ -750,7 +708,7 @@ class Raster:
|
|
|
750
708
|
# ):
|
|
751
709
|
# return self
|
|
752
710
|
|
|
753
|
-
if self.
|
|
711
|
+
if self.values is None:
|
|
754
712
|
project = pyproj.Transformer.from_crs(
|
|
755
713
|
pyproj.CRS(self._prev_crs), pyproj.CRS(crs), always_xy=True
|
|
756
714
|
).transform
|
|
@@ -782,16 +740,16 @@ class Raster:
|
|
|
782
740
|
# self._bounds = shapely.transform(old_box, project)
|
|
783
741
|
else:
|
|
784
742
|
was_2d = len(self.shape) == 2
|
|
785
|
-
self.
|
|
786
|
-
source=self.
|
|
743
|
+
self.values, transform = reproject(
|
|
744
|
+
source=self.values,
|
|
787
745
|
src_crs=self._prev_crs,
|
|
788
746
|
src_transform=self.transform,
|
|
789
747
|
dst_crs=pyproj.CRS(crs),
|
|
790
748
|
**kwargs,
|
|
791
749
|
)
|
|
792
|
-
if was_2d and len(self.
|
|
793
|
-
assert self.
|
|
794
|
-
self.
|
|
750
|
+
if was_2d and len(self.values.shape) == 3:
|
|
751
|
+
assert self.values.shape[0] == 1
|
|
752
|
+
self.values = self.values[0]
|
|
795
753
|
|
|
796
754
|
self._bounds = rasterio.transform.array_bounds(
|
|
797
755
|
self.height, self.width, transform
|
|
@@ -811,9 +769,9 @@ class Raster:
|
|
|
811
769
|
raster = self
|
|
812
770
|
|
|
813
771
|
if len(raster.shape) == 2:
|
|
814
|
-
array = np.array([raster.
|
|
772
|
+
array = np.array([raster.values])
|
|
815
773
|
else:
|
|
816
|
-
array = raster.
|
|
774
|
+
array = raster.values
|
|
817
775
|
|
|
818
776
|
for arr in array:
|
|
819
777
|
ax = plt.axes()
|
|
@@ -824,31 +782,31 @@ class Raster:
|
|
|
824
782
|
|
|
825
783
|
def astype(self, dtype: type) -> Self:
|
|
826
784
|
"""Convert the datatype of the array."""
|
|
827
|
-
if self.
|
|
785
|
+
if self.values is None:
|
|
828
786
|
raise ValueError("Array is not loaded.")
|
|
829
|
-
if not rasterio.dtypes.can_cast_dtype(self.
|
|
830
|
-
min_dtype = rasterio.dtypes.get_minimum_dtype(self.
|
|
787
|
+
if not rasterio.dtypes.can_cast_dtype(self.values, dtype):
|
|
788
|
+
min_dtype = rasterio.dtypes.get_minimum_dtype(self.values)
|
|
831
789
|
raise ValueError(f"Cannot cast to dtype. Minimum dtype is {min_dtype}")
|
|
832
|
-
self.
|
|
790
|
+
self.values = self.values.astype(dtype)
|
|
833
791
|
self._dtype = dtype
|
|
834
792
|
return self
|
|
835
793
|
|
|
836
794
|
def as_minimum_dtype(self) -> Self:
|
|
837
795
|
"""Convert the array to the minimum dtype without overflow."""
|
|
838
|
-
min_dtype = rasterio.dtypes.get_minimum_dtype(self.
|
|
839
|
-
self.
|
|
796
|
+
min_dtype = rasterio.dtypes.get_minimum_dtype(self.values)
|
|
797
|
+
self.values = self.values.astype(min_dtype)
|
|
840
798
|
return self
|
|
841
799
|
|
|
842
800
|
def min(self) -> int | None:
|
|
843
801
|
"""Minimum value in the array."""
|
|
844
|
-
if np.size(self.
|
|
845
|
-
return np.min(self.
|
|
802
|
+
if np.size(self.values):
|
|
803
|
+
return np.min(self.values)
|
|
846
804
|
return None
|
|
847
805
|
|
|
848
806
|
def max(self) -> int | None:
|
|
849
807
|
"""Maximum value in the array."""
|
|
850
|
-
if np.size(self.
|
|
851
|
-
return np.max(self.
|
|
808
|
+
if np.size(self.values):
|
|
809
|
+
return np.max(self.values)
|
|
852
810
|
return None
|
|
853
811
|
|
|
854
812
|
def _add_meta(self) -> Self:
|
|
@@ -867,10 +825,10 @@ class Raster:
|
|
|
867
825
|
def array_list(self) -> list[np.ndarray]:
|
|
868
826
|
"""Get a list of 2D arrays."""
|
|
869
827
|
self._check_for_array()
|
|
870
|
-
if len(self.
|
|
871
|
-
return [self.
|
|
872
|
-
elif len(self.
|
|
873
|
-
return list(self.
|
|
828
|
+
if len(self.values.shape) == 2:
|
|
829
|
+
return [self.values]
|
|
830
|
+
elif len(self.values.shape) == 3:
|
|
831
|
+
return list(self.values)
|
|
874
832
|
else:
|
|
875
833
|
raise ValueError
|
|
876
834
|
|
|
@@ -898,8 +856,7 @@ class Raster:
|
|
|
898
856
|
def date(self) -> str | None:
|
|
899
857
|
"""Date in the image file name, if filename_regex is present."""
|
|
900
858
|
try:
|
|
901
|
-
|
|
902
|
-
return re.match(pattern, Path(self.path).name).group("date")
|
|
859
|
+
return re.match(self.filename_pattern, Path(self.path).name).group("date")
|
|
903
860
|
except (AttributeError, TypeError):
|
|
904
861
|
return None
|
|
905
862
|
|
|
@@ -907,8 +864,7 @@ class Raster:
|
|
|
907
864
|
def band(self) -> str | None:
|
|
908
865
|
"""Band name of the image file name, if filename_regex is present."""
|
|
909
866
|
try:
|
|
910
|
-
|
|
911
|
-
return re.match(pattern, Path(self.path).name).group("band")
|
|
867
|
+
return re.match(self.filename_pattern, Path(self.path).name).group("band")
|
|
912
868
|
except (AttributeError, TypeError):
|
|
913
869
|
return None
|
|
914
870
|
|
|
@@ -916,7 +872,7 @@ class Raster:
|
|
|
916
872
|
def dtype(self) -> Any:
|
|
917
873
|
"""Data type of the array."""
|
|
918
874
|
try:
|
|
919
|
-
return self.
|
|
875
|
+
return self.values.dtype
|
|
920
876
|
except AttributeError:
|
|
921
877
|
try:
|
|
922
878
|
return self._dtype
|
|
@@ -925,7 +881,7 @@ class Raster:
|
|
|
925
881
|
|
|
926
882
|
@dtype.setter
|
|
927
883
|
def dtype(self, new_dtype: Any) -> None:
|
|
928
|
-
self.
|
|
884
|
+
self.values = self.values.astype(new_dtype)
|
|
929
885
|
|
|
930
886
|
@property
|
|
931
887
|
def nodata(self) -> int | None:
|
|
@@ -937,10 +893,11 @@ class Raster:
|
|
|
937
893
|
|
|
938
894
|
@property
|
|
939
895
|
def tile(self) -> str | None:
|
|
940
|
-
"""
|
|
941
|
-
|
|
896
|
+
"""Tile name from regex."""
|
|
897
|
+
try:
|
|
898
|
+
return re.match(self.filename_pattern, Path(self.path).name).group("tile")
|
|
899
|
+
except (AttributeError, TypeError):
|
|
942
900
|
return None
|
|
943
|
-
return f"{int(self.bounds[0])}_{int(self.bounds[1])}"
|
|
944
901
|
|
|
945
902
|
@property
|
|
946
903
|
def meta(self) -> dict:
|
|
@@ -976,7 +933,7 @@ class Raster:
|
|
|
976
933
|
return {
|
|
977
934
|
"indexes": self.indexes,
|
|
978
935
|
"fill_value": self.nodata,
|
|
979
|
-
"masked":
|
|
936
|
+
"masked": False,
|
|
980
937
|
}
|
|
981
938
|
|
|
982
939
|
@property
|
|
@@ -992,18 +949,18 @@ class Raster:
|
|
|
992
949
|
@property
|
|
993
950
|
def height(self) -> int | None:
|
|
994
951
|
"""Get the height of the image as number of pixels."""
|
|
995
|
-
if self.
|
|
952
|
+
if self.values is None:
|
|
996
953
|
try:
|
|
997
954
|
return self._height
|
|
998
955
|
except AttributeError:
|
|
999
956
|
return None
|
|
1000
|
-
i = 1 if len(self.
|
|
1001
|
-
return self.
|
|
957
|
+
i = 1 if len(self.values.shape) == 3 else 0
|
|
958
|
+
return self.values.shape[i]
|
|
1002
959
|
|
|
1003
960
|
@property
|
|
1004
961
|
def width(self) -> int | None:
|
|
1005
962
|
"""Get the width of the image as number of pixels."""
|
|
1006
|
-
if self.
|
|
963
|
+
if self.values is None:
|
|
1007
964
|
try:
|
|
1008
965
|
return self._width
|
|
1009
966
|
except AttributeError:
|
|
@@ -1014,16 +971,16 @@ class Raster:
|
|
|
1014
971
|
return self._width
|
|
1015
972
|
except Exception:
|
|
1016
973
|
return None
|
|
1017
|
-
i = 2 if len(self.
|
|
1018
|
-
return self.
|
|
974
|
+
i = 2 if len(self.values.shape) == 3 else 1
|
|
975
|
+
return self.values.shape[i]
|
|
1019
976
|
|
|
1020
977
|
@property
|
|
1021
978
|
def count(self) -> int:
|
|
1022
979
|
"""Get the number of bands in the image."""
|
|
1023
|
-
if self.
|
|
1024
|
-
if len(self.
|
|
1025
|
-
return self.
|
|
1026
|
-
if len(self.
|
|
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:
|
|
1027
984
|
return 1
|
|
1028
985
|
if not hasattr(self._indexes, "__iter__"):
|
|
1029
986
|
return 1
|
|
@@ -1032,8 +989,8 @@ class Raster:
|
|
|
1032
989
|
@property
|
|
1033
990
|
def shape(self) -> tuple[int]:
|
|
1034
991
|
"""Shape that is consistent with the array, whether it is loaded or not."""
|
|
1035
|
-
if self.
|
|
1036
|
-
return self.
|
|
992
|
+
if self.values is not None:
|
|
993
|
+
return self.values.shape
|
|
1037
994
|
if hasattr(self._indexes, "__iter__"):
|
|
1038
995
|
return self.count, self.width, self.height
|
|
1039
996
|
return self.width, self.height
|
|
@@ -1069,12 +1026,12 @@ class Raster:
|
|
|
1069
1026
|
@property
|
|
1070
1027
|
def area(self) -> float:
|
|
1071
1028
|
"""Get the area of the image."""
|
|
1072
|
-
return shapely.area(self.
|
|
1029
|
+
return shapely.area(self.union_all())
|
|
1073
1030
|
|
|
1074
1031
|
@property
|
|
1075
1032
|
def length(self) -> float:
|
|
1076
1033
|
"""Get the circumfence of the image."""
|
|
1077
|
-
return shapely.length(self.
|
|
1034
|
+
return shapely.length(self.union_all())
|
|
1078
1035
|
|
|
1079
1036
|
@property
|
|
1080
1037
|
def unary_union(self) -> Polygon:
|
|
@@ -1121,11 +1078,11 @@ class Raster:
|
|
|
1121
1078
|
"""Check if the Raster is equal to another Raster."""
|
|
1122
1079
|
if not isinstance(other, Raster):
|
|
1123
1080
|
raise NotImplementedError("other must be of type Raster")
|
|
1124
|
-
if type(other)
|
|
1081
|
+
if type(other) is not type(self):
|
|
1125
1082
|
return False
|
|
1126
|
-
if self.
|
|
1083
|
+
if self.values is None and other.values is not None:
|
|
1127
1084
|
return False
|
|
1128
|
-
if self.
|
|
1085
|
+
if self.values is not None and other.values is None:
|
|
1129
1086
|
return False
|
|
1130
1087
|
|
|
1131
1088
|
for method in dir(self):
|
|
@@ -1134,7 +1091,7 @@ class Raster:
|
|
|
1134
1091
|
if getattr(self, method) != getattr(other, method):
|
|
1135
1092
|
return False
|
|
1136
1093
|
|
|
1137
|
-
return np.array_equal(self.
|
|
1094
|
+
return np.array_equal(self.values, other.values)
|
|
1138
1095
|
|
|
1139
1096
|
def __repr__(self) -> str:
|
|
1140
1097
|
"""The print representation."""
|
|
@@ -1144,52 +1101,52 @@ class Raster:
|
|
|
1144
1101
|
res = int(self.res)
|
|
1145
1102
|
except TypeError:
|
|
1146
1103
|
res = None
|
|
1147
|
-
return f"{self.__class__.__name__}(shape=({shp}), res={res},
|
|
1104
|
+
return f"{self.__class__.__name__}(shape=({shp}), res={res}, band={self.band})"
|
|
1148
1105
|
|
|
1149
1106
|
def __iter__(self) -> Iterator[np.ndarray]:
|
|
1150
1107
|
"""Iterate over the arrays."""
|
|
1151
|
-
if len(self.
|
|
1152
|
-
return iter([self.
|
|
1153
|
-
if len(self.
|
|
1154
|
-
return iter(self.
|
|
1108
|
+
if len(self.values.shape) == 2:
|
|
1109
|
+
return iter([self.values])
|
|
1110
|
+
if len(self.values.shape) == 3:
|
|
1111
|
+
return iter(self.values)
|
|
1155
1112
|
raise ValueError(
|
|
1156
|
-
f"Array should have shape length 2 or 3. Got {len(self.
|
|
1113
|
+
f"Array should have shape length 2 or 3. Got {len(self.values.shape)}"
|
|
1157
1114
|
)
|
|
1158
1115
|
|
|
1159
1116
|
def __mul__(self, scalar: int | float) -> "Raster":
|
|
1160
1117
|
"""Multiply the array values with *."""
|
|
1161
1118
|
self._check_for_array()
|
|
1162
|
-
self.
|
|
1119
|
+
self.values = self.values * scalar
|
|
1163
1120
|
return self
|
|
1164
1121
|
|
|
1165
1122
|
def __add__(self, scalar: int | float) -> "Raster":
|
|
1166
1123
|
"""Add to the array values with +."""
|
|
1167
1124
|
self._check_for_array()
|
|
1168
|
-
self.
|
|
1125
|
+
self.values = self.values + scalar
|
|
1169
1126
|
return self
|
|
1170
1127
|
|
|
1171
1128
|
def __sub__(self, scalar: int | float) -> "Raster":
|
|
1172
1129
|
"""Subtract the array values with -."""
|
|
1173
1130
|
self._check_for_array()
|
|
1174
|
-
self.
|
|
1131
|
+
self.values = self.values - scalar
|
|
1175
1132
|
return self
|
|
1176
1133
|
|
|
1177
1134
|
def __truediv__(self, scalar: int | float) -> "Raster":
|
|
1178
1135
|
"""Divide the array values with /."""
|
|
1179
1136
|
self._check_for_array()
|
|
1180
|
-
self.
|
|
1137
|
+
self.values = self.values / scalar
|
|
1181
1138
|
return self
|
|
1182
1139
|
|
|
1183
1140
|
def __floordiv__(self, scalar: int | float) -> "Raster":
|
|
1184
1141
|
"""Floor divide the array values with //."""
|
|
1185
1142
|
self._check_for_array()
|
|
1186
|
-
self.
|
|
1143
|
+
self.values = self.values // scalar
|
|
1187
1144
|
return self
|
|
1188
1145
|
|
|
1189
1146
|
def __pow__(self, exponent: int | float) -> "Raster":
|
|
1190
1147
|
"""Exponentiate the array values with **."""
|
|
1191
1148
|
self._check_for_array()
|
|
1192
|
-
self.
|
|
1149
|
+
self.values = self.values**exponent
|
|
1193
1150
|
return self
|
|
1194
1151
|
|
|
1195
1152
|
def _has_nessecary_attrs(self, dict_like: dict) -> bool:
|
|
@@ -1204,11 +1161,11 @@ class Raster:
|
|
|
1204
1161
|
|
|
1205
1162
|
def _return_self_or_copy(self, array: np.ndarray, copy: bool) -> "Raster":
|
|
1206
1163
|
if not copy:
|
|
1207
|
-
self.
|
|
1164
|
+
self.values = array
|
|
1208
1165
|
return self
|
|
1209
1166
|
else:
|
|
1210
1167
|
copy = self.copy()
|
|
1211
|
-
copy.
|
|
1168
|
+
copy.values = array
|
|
1212
1169
|
return copy
|
|
1213
1170
|
|
|
1214
1171
|
@classmethod
|
|
@@ -1251,35 +1208,35 @@ class Raster:
|
|
|
1251
1208
|
def _write(
|
|
1252
1209
|
self, dst: rasterio.io.DatasetReader, window: rasterio.windows.Window
|
|
1253
1210
|
) -> None:
|
|
1254
|
-
if np.ma.is_masked(self.
|
|
1255
|
-
if len(self.
|
|
1211
|
+
if np.ma.is_masked(self.values):
|
|
1212
|
+
if len(self.values.shape) == 2:
|
|
1256
1213
|
return dst.write(
|
|
1257
|
-
self.
|
|
1214
|
+
self.values.filled(self.nodata), indexes=1, window=window
|
|
1258
1215
|
)
|
|
1259
1216
|
|
|
1260
1217
|
for i in range(len(self.indexes_as_tuple())):
|
|
1261
1218
|
dst.write(
|
|
1262
|
-
self.
|
|
1219
|
+
self.values[i].filled(self.nodata),
|
|
1263
1220
|
indexes=i + 1,
|
|
1264
1221
|
window=window,
|
|
1265
1222
|
)
|
|
1266
1223
|
|
|
1267
1224
|
else:
|
|
1268
|
-
if len(self.
|
|
1269
|
-
return dst.write(self.
|
|
1225
|
+
if len(self.values.shape) == 2:
|
|
1226
|
+
return dst.write(self.values, indexes=1, window=window)
|
|
1270
1227
|
|
|
1271
1228
|
for i, idx in enumerate(self.indexes_as_tuple()):
|
|
1272
|
-
dst.write(self.
|
|
1229
|
+
dst.write(self.values[i], indexes=idx, window=window)
|
|
1273
1230
|
|
|
1274
1231
|
def _get_indexes(self, indexes: int | tuple[int] | None) -> int | tuple[int] | None:
|
|
1275
1232
|
if isinstance(indexes, numbers.Number):
|
|
1276
1233
|
return int(indexes)
|
|
1277
1234
|
if indexes is None:
|
|
1278
|
-
if self.
|
|
1279
|
-
return tuple(i + 1 for i in range(self.
|
|
1280
|
-
elif self.
|
|
1235
|
+
if self.values is not None and len(self.values.shape) == 3:
|
|
1236
|
+
return tuple(i + 1 for i in range(self.values.shape[0]))
|
|
1237
|
+
elif self.values is not None and len(self.values.shape) == 2:
|
|
1281
1238
|
return 1
|
|
1282
|
-
elif self.
|
|
1239
|
+
elif self.values is not None:
|
|
1283
1240
|
raise ValueError("Array must be 2 or 3 dimensional.")
|
|
1284
1241
|
else:
|
|
1285
1242
|
return None
|
|
@@ -1320,24 +1277,30 @@ class Raster:
|
|
|
1320
1277
|
|
|
1321
1278
|
@staticmethod
|
|
1322
1279
|
def _array_to_geojson(array: np.ndarray, transform: Affine) -> list[tuple]:
|
|
1280
|
+
if np.ma.is_masked(array):
|
|
1281
|
+
array = array.data
|
|
1323
1282
|
try:
|
|
1324
1283
|
return [
|
|
1325
1284
|
(value, shape(geom))
|
|
1326
|
-
for geom, value in features.shapes(
|
|
1285
|
+
for geom, value in features.shapes(
|
|
1286
|
+
array, transform=transform, mask=None
|
|
1287
|
+
)
|
|
1327
1288
|
]
|
|
1328
1289
|
except ValueError:
|
|
1329
1290
|
array = array.astype(np.float32)
|
|
1330
1291
|
return [
|
|
1331
1292
|
(value, shape(geom))
|
|
1332
|
-
for geom, value in features.shapes(
|
|
1293
|
+
for geom, value in features.shapes(
|
|
1294
|
+
array, transform=transform, mask=None
|
|
1295
|
+
)
|
|
1333
1296
|
]
|
|
1334
1297
|
|
|
1335
1298
|
def _add_indexes_from_array(self, indexes: int | tuple[int]) -> int | tuple[int]:
|
|
1336
1299
|
if indexes is not None:
|
|
1337
1300
|
return indexes
|
|
1338
|
-
elif len(self.
|
|
1339
|
-
return tuple(x + 1 for x in range(len(self.
|
|
1340
|
-
elif len(self.
|
|
1301
|
+
elif len(self.values.shape) == 3:
|
|
1302
|
+
return tuple(x + 1 for x in range(len(self.values)))
|
|
1303
|
+
elif len(self.values.shape) == 2:
|
|
1341
1304
|
return 1
|
|
1342
1305
|
else:
|
|
1343
1306
|
raise ValueError
|
|
@@ -1364,10 +1327,15 @@ class Raster:
|
|
|
1364
1327
|
# except AttributeError:
|
|
1365
1328
|
# pass
|
|
1366
1329
|
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
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
|
|
1371
1339
|
|
|
1372
1340
|
# if not hasattr(self, "_indexes") or self._indexes is None:
|
|
1373
1341
|
# self._indexes = src.indexes
|
|
@@ -1407,7 +1375,7 @@ class Raster:
|
|
|
1407
1375
|
if hasattr(self, "_warped_crs"):
|
|
1408
1376
|
src = WarpedVRT(src, crs=self.crs)
|
|
1409
1377
|
|
|
1410
|
-
self.
|
|
1378
|
+
self.values = src.read(
|
|
1411
1379
|
out_shape=out_shape,
|
|
1412
1380
|
**(self.read_kwargs | kwargs),
|
|
1413
1381
|
)
|
|
@@ -1439,11 +1407,17 @@ class Raster:
|
|
|
1439
1407
|
if hasattr(self, "_warped_crs"):
|
|
1440
1408
|
src = WarpedVRT(src, crs=self.crs)
|
|
1441
1409
|
|
|
1442
|
-
self.
|
|
1410
|
+
self.values = src.read(out_shape=out_shape, **kwargs)
|
|
1443
1411
|
|
|
1444
1412
|
if not masked:
|
|
1445
|
-
|
|
1446
|
-
|
|
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
|
|
1447
1421
|
|
|
1448
1422
|
if boundless:
|
|
1449
1423
|
self._bounds = src.window_bounds(window=window)
|
|
@@ -1454,7 +1428,7 @@ class Raster:
|
|
|
1454
1428
|
else:
|
|
1455
1429
|
self._bounds = intersected.bounds
|
|
1456
1430
|
|
|
1457
|
-
if not np.size(self.
|
|
1431
|
+
if not np.size(self.values):
|
|
1458
1432
|
return
|
|
1459
1433
|
|
|
1460
1434
|
if self._dtype:
|
|
@@ -1462,8 +1436,8 @@ class Raster:
|
|
|
1462
1436
|
else:
|
|
1463
1437
|
self = self.as_minimum_dtype()
|
|
1464
1438
|
|
|
1465
|
-
if self.
|
|
1466
|
-
with memfile_from_array(self.
|
|
1439
|
+
if self.values is not None:
|
|
1440
|
+
with memfile_from_array(self.values, **self.profile) as src:
|
|
1467
1441
|
_read(self, src, **kwargs)
|
|
1468
1442
|
else:
|
|
1469
1443
|
with opener(self.path, file_system=self.file_system) as file:
|
|
@@ -1471,7 +1445,7 @@ class Raster:
|
|
|
1471
1445
|
_read(self, src, **kwargs)
|
|
1472
1446
|
|
|
1473
1447
|
def _check_for_array(self, text=""):
|
|
1474
|
-
if self.
|
|
1448
|
+
if self.values is None:
|
|
1475
1449
|
raise ValueError("Arrays are not loaded. " + text)
|
|
1476
1450
|
|
|
1477
1451
|
|
|
@@ -1499,82 +1473,3 @@ def get_shape_from_bounds(
|
|
|
1499
1473
|
width = int(diffx / resx)
|
|
1500
1474
|
heigth = int(diffy / resy)
|
|
1501
1475
|
return heigth, width
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
def get_gradient(raster: Raster, degrees: bool = False, copy: bool = False) -> Raster:
|
|
1505
|
-
"""Get the slope of an elevation raster.
|
|
1506
|
-
|
|
1507
|
-
Calculates the absolute slope between the grid cells
|
|
1508
|
-
based on the image resolution.
|
|
1509
|
-
|
|
1510
|
-
For multiband images, the calculation is done for each band.
|
|
1511
|
-
|
|
1512
|
-
Args:
|
|
1513
|
-
raster: Raster instance.
|
|
1514
|
-
degrees: If False (default), the returned values will be in ratios,
|
|
1515
|
-
where a value of 1 means 1 meter up per 1 meter forward. If True,
|
|
1516
|
-
the values will be in degrees from 0 to 90.
|
|
1517
|
-
copy: Whether to copy or overwrite the original Raster.
|
|
1518
|
-
Defaults to False to save memory.
|
|
1519
|
-
|
|
1520
|
-
Returns:
|
|
1521
|
-
The class instance with new array values, or a copy if copy is True.
|
|
1522
|
-
|
|
1523
|
-
Examples:
|
|
1524
|
-
--------
|
|
1525
|
-
Making an array where the gradient to the center is always 10.
|
|
1526
|
-
|
|
1527
|
-
>>> import sgis as sg
|
|
1528
|
-
>>> import numpy as np
|
|
1529
|
-
>>> arr = np.array(
|
|
1530
|
-
... [
|
|
1531
|
-
... [100, 100, 100, 100, 100],
|
|
1532
|
-
... [100, 110, 110, 110, 100],
|
|
1533
|
-
... [100, 110, 120, 110, 100],
|
|
1534
|
-
... [100, 110, 110, 110, 100],
|
|
1535
|
-
... [100, 100, 100, 100, 100],
|
|
1536
|
-
... ]
|
|
1537
|
-
... )
|
|
1538
|
-
|
|
1539
|
-
Now let's create a Raster from this array with a resolution of 10.
|
|
1540
|
-
|
|
1541
|
-
>>> r = sg.Raster.from_array(arr, crs=None, bounds=(0, 0, 50, 50), res=10)
|
|
1542
|
-
|
|
1543
|
-
The gradient will be 1 (1 meter up for every meter forward).
|
|
1544
|
-
The calculation is by default done in place to save memory.
|
|
1545
|
-
|
|
1546
|
-
>>> r.gradient()
|
|
1547
|
-
>>> r.array
|
|
1548
|
-
array([[0., 1., 1., 1., 0.],
|
|
1549
|
-
[1., 1., 1., 1., 1.],
|
|
1550
|
-
[1., 1., 0., 1., 1.],
|
|
1551
|
-
[1., 1., 1., 1., 1.],
|
|
1552
|
-
[0., 1., 1., 1., 0.]])
|
|
1553
|
-
"""
|
|
1554
|
-
out_array = []
|
|
1555
|
-
for array in raster:
|
|
1556
|
-
results = _slope_2d(array, raster.res, degrees=degrees)
|
|
1557
|
-
out_array.append(results)
|
|
1558
|
-
|
|
1559
|
-
if len(raster.shape) == 2:
|
|
1560
|
-
out_array = out_array[0]
|
|
1561
|
-
else:
|
|
1562
|
-
out_array = np.array(out_array)
|
|
1563
|
-
|
|
1564
|
-
return raster._return_self_or_copy(out_array, copy)
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
def _slope_2d(array: np.ndarray, res: int, degrees: int) -> np.ndarray:
|
|
1568
|
-
gradient_x, gradient_y = np.gradient(array, res, res)
|
|
1569
|
-
|
|
1570
|
-
gradient = abs(gradient_x) + abs(gradient_y)
|
|
1571
|
-
|
|
1572
|
-
if not degrees:
|
|
1573
|
-
return gradient
|
|
1574
|
-
|
|
1575
|
-
radians = np.arctan(gradient)
|
|
1576
|
-
degrees = np.degrees(radians)
|
|
1577
|
-
|
|
1578
|
-
assert np.max(degrees) <= 90
|
|
1579
|
-
|
|
1580
|
-
return degrees
|