yirgacheffe 1.4.0__py3-none-any.whl → 1.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of yirgacheffe might be problematic. Click here for more details.
- yirgacheffe/__init__.py +3 -6
- yirgacheffe/_backends/mlx.py +2 -1
- yirgacheffe/_backends/numpy.py +2 -1
- yirgacheffe/_core.py +133 -0
- yirgacheffe/constants.py +6 -0
- yirgacheffe/layers/base.py +6 -6
- yirgacheffe/layers/constant.py +2 -7
- yirgacheffe/layers/group.py +16 -7
- yirgacheffe/layers/rasters.py +7 -7
- yirgacheffe/layers/rescaled.py +3 -2
- yirgacheffe/layers/vectors.py +15 -13
- yirgacheffe/operators.py +84 -20
- yirgacheffe/window.py +27 -0
- {yirgacheffe-1.4.0.dist-info → yirgacheffe-1.5.0.dist-info}/METADATA +111 -55
- yirgacheffe-1.5.0.dist-info/RECORD +25 -0
- yirgacheffe-1.4.0.dist-info/RECORD +0 -24
- {yirgacheffe-1.4.0.dist-info → yirgacheffe-1.5.0.dist-info}/WHEEL +0 -0
- {yirgacheffe-1.4.0.dist-info → yirgacheffe-1.5.0.dist-info}/entry_points.txt +0 -0
- {yirgacheffe-1.4.0.dist-info → yirgacheffe-1.5.0.dist-info}/licenses/LICENSE +0 -0
- {yirgacheffe-1.4.0.dist-info → yirgacheffe-1.5.0.dist-info}/top_level.txt +0 -0
yirgacheffe/__init__.py
CHANGED
|
@@ -5,10 +5,7 @@ try:
|
|
|
5
5
|
except ModuleNotFoundError:
|
|
6
6
|
__version__ = "unknown"
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
from ._core import read_raster, read_rasters, read_shape, read_shape_like
|
|
9
|
+
from .constants import WGS_84_PROJECTION
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
WGS_84_PROJECTION = 'GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,'\
|
|
12
|
-
'AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0],'\
|
|
13
|
-
'UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AXIS["Latitude",NORTH],'\
|
|
14
|
-
'AXIS["Longitude",EAST],AUTHORITY["EPSG","4326"]]'
|
|
11
|
+
gdal.UseExceptions()
|
yirgacheffe/_backends/mlx.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from typing import Callable, Dict
|
|
1
2
|
|
|
2
3
|
import numpy as np
|
|
3
4
|
import mlx.core as mx # type: ignore
|
|
@@ -182,7 +183,7 @@ def backend_to_dtype(val):
|
|
|
182
183
|
def astype_op(data, datatype):
|
|
183
184
|
return data.astype(dtype_to_backed(datatype))
|
|
184
185
|
|
|
185
|
-
operator_map = {
|
|
186
|
+
operator_map : Dict[op,Callable] = {
|
|
186
187
|
op.ADD: mx.array.__add__,
|
|
187
188
|
op.SUB: mx.array.__sub__,
|
|
188
189
|
op.MUL: mul_op,
|
yirgacheffe/_backends/numpy.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from typing import Callable, Dict
|
|
1
2
|
|
|
2
3
|
import numpy as np
|
|
3
4
|
import torch
|
|
@@ -120,7 +121,7 @@ def backend_to_dtype(val):
|
|
|
120
121
|
def astype_op(data, datatype):
|
|
121
122
|
return data.astype(dtype_to_backed(datatype))
|
|
122
123
|
|
|
123
|
-
operator_map = {
|
|
124
|
+
operator_map : Dict[op,Callable] = {
|
|
124
125
|
op.ADD: np.ndarray.__add__,
|
|
125
126
|
op.SUB: np.ndarray.__sub__,
|
|
126
127
|
op.MUL: np.ndarray.__mul__,
|
yirgacheffe/_core.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import List, Optional, Tuple, Union
|
|
3
|
+
|
|
4
|
+
from .layers.base import YirgacheffeLayer
|
|
5
|
+
from .layers.group import GroupLayer, TiledGroupLayer
|
|
6
|
+
from .layers.rasters import RasterLayer
|
|
7
|
+
from .layers.vectors import VectorLayer
|
|
8
|
+
from .window import PixelScale
|
|
9
|
+
from .operators import DataType
|
|
10
|
+
|
|
11
|
+
def read_raster(
|
|
12
|
+
filename: Union[Path,str],
|
|
13
|
+
band: int = 1
|
|
14
|
+
) -> RasterLayer:
|
|
15
|
+
"""Open a raster file (e.g., GeoTIFF).
|
|
16
|
+
|
|
17
|
+
Parameters
|
|
18
|
+
----------
|
|
19
|
+
filename : Path
|
|
20
|
+
Path of raster file to open.
|
|
21
|
+
band : int, default=1
|
|
22
|
+
For multi-band rasters, which band to use (defaults to first if not specified).
|
|
23
|
+
|
|
24
|
+
Returns
|
|
25
|
+
-------
|
|
26
|
+
RasterLayer
|
|
27
|
+
Returns an layer representing the raster data.
|
|
28
|
+
"""
|
|
29
|
+
return RasterLayer.layer_from_file(filename, band)
|
|
30
|
+
|
|
31
|
+
def read_rasters(
|
|
32
|
+
filenames : Union[List[Path],List[str]],
|
|
33
|
+
tiled: bool=False
|
|
34
|
+
) -> GroupLayer:
|
|
35
|
+
"""Open a set of raster files (e.g., GeoTIFFs) as a single layer.
|
|
36
|
+
|
|
37
|
+
Parameters
|
|
38
|
+
----------
|
|
39
|
+
filenames : List[Path]
|
|
40
|
+
List of paths of raster files to open.
|
|
41
|
+
tiled : bool, default=False
|
|
42
|
+
If you know that the rasters for a regular tileset, then setting this flag allows
|
|
43
|
+
Yirgacheffe to perform certain optimisations that significantly improve performance for
|
|
44
|
+
this use case.
|
|
45
|
+
|
|
46
|
+
Returns
|
|
47
|
+
-------
|
|
48
|
+
GroupLayer
|
|
49
|
+
Returns an layer representing the raster data.
|
|
50
|
+
"""
|
|
51
|
+
if not tiled:
|
|
52
|
+
return GroupLayer.layer_from_files(filenames)
|
|
53
|
+
else:
|
|
54
|
+
return TiledGroupLayer.layer_from_files(filenames)
|
|
55
|
+
|
|
56
|
+
def read_shape(
|
|
57
|
+
filename: Union[Path,str],
|
|
58
|
+
scale: Union[PixelScale, Tuple[float,float]],
|
|
59
|
+
projection: str,
|
|
60
|
+
where_filter: Optional[str] = None,
|
|
61
|
+
datatype: Optional[DataType] = None,
|
|
62
|
+
burn_value: Union[int,float,str] = 1,
|
|
63
|
+
) -> VectorLayer:
|
|
64
|
+
"""Open a polygon file (e.g., GeoJSON, GPKG, or ESRI Shape File).
|
|
65
|
+
|
|
66
|
+
Parameters
|
|
67
|
+
----------
|
|
68
|
+
filename : Path
|
|
69
|
+
Path of raster file to open.
|
|
70
|
+
scale: PixelScale or tuple of float
|
|
71
|
+
The dimensions of each pixel.
|
|
72
|
+
projection: str
|
|
73
|
+
The map projection to use
|
|
74
|
+
where_filter : str, optional
|
|
75
|
+
For use with files with many entries (e.g., GPKG), applies this filter to the data.
|
|
76
|
+
datatype: DataType, default=DataType.Byte
|
|
77
|
+
Specify the data type of the raster data generated.
|
|
78
|
+
burn_value: int or float or str, default=1
|
|
79
|
+
The value of each pixel in the polygon.
|
|
80
|
+
|
|
81
|
+
Returns
|
|
82
|
+
-------
|
|
83
|
+
VectorLayer
|
|
84
|
+
Returns an layer representing the vector data.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
if not isinstance(scale, PixelScale):
|
|
88
|
+
scale = PixelScale(scale[0], scale[1])
|
|
89
|
+
|
|
90
|
+
return VectorLayer.layer_from_file(
|
|
91
|
+
filename,
|
|
92
|
+
where_filter,
|
|
93
|
+
scale,
|
|
94
|
+
projection,
|
|
95
|
+
datatype,
|
|
96
|
+
burn_value
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def read_shape_like(
|
|
100
|
+
filename: Union[Path,str],
|
|
101
|
+
like: YirgacheffeLayer,
|
|
102
|
+
where_filter: Optional[str] = None,
|
|
103
|
+
datatype: Optional[DataType] = None,
|
|
104
|
+
burn_value: Union[int,float,str] = 1,
|
|
105
|
+
) -> VectorLayer:
|
|
106
|
+
"""Open a polygon file (e.g., GeoJSON, GPKG, or ESRI Shape File).
|
|
107
|
+
|
|
108
|
+
Parameters
|
|
109
|
+
----------
|
|
110
|
+
filename : Path
|
|
111
|
+
Path of raster file to open.
|
|
112
|
+
like: YirgacheffeLayer
|
|
113
|
+
Another layer that has a projection and pixel scale set. This layer will
|
|
114
|
+
use the same projection and pixel scale as that one.
|
|
115
|
+
where_filter : str, optional
|
|
116
|
+
For use with files with many entries (e.g., GPKG), applies this filter to the data.
|
|
117
|
+
datatype: DataType, default=DataType.Byte
|
|
118
|
+
Specify the data type of the raster data generated.
|
|
119
|
+
burn_value: int or float or str, default=1
|
|
120
|
+
The value of each pixel in the polygon.
|
|
121
|
+
|
|
122
|
+
Returns
|
|
123
|
+
-------
|
|
124
|
+
VectorLayer
|
|
125
|
+
Returns an layer representing the vector data.
|
|
126
|
+
"""
|
|
127
|
+
return VectorLayer.layer_from_file_like(
|
|
128
|
+
filename,
|
|
129
|
+
like,
|
|
130
|
+
where_filter,
|
|
131
|
+
datatype,
|
|
132
|
+
burn_value,
|
|
133
|
+
)
|
yirgacheffe/constants.py
CHANGED
|
@@ -1,2 +1,8 @@
|
|
|
1
1
|
YSTEP = 512
|
|
2
2
|
MINIMUM_CHUNKS_PER_THREAD = 1
|
|
3
|
+
|
|
4
|
+
# I don't really want this here, but it's just too useful having it exposed
|
|
5
|
+
WGS_84_PROJECTION = 'GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,'\
|
|
6
|
+
'AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0],'\
|
|
7
|
+
'UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AXIS["Latitude",NORTH],'\
|
|
8
|
+
'AXIS["Longitude",EAST],AUTHORITY["EPSG","4326"]]'
|
yirgacheffe/layers/base.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
from typing import Any,
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Any, Optional, Sequence, Tuple
|
|
3
3
|
|
|
4
4
|
from ..operators import DataType, LayerMathMixin
|
|
5
5
|
from ..rounding import almost_equal, are_pixel_scales_equal_enough, round_up_pixels, round_down_pixels
|
|
@@ -67,7 +67,7 @@ class YirgacheffeLayer(LayerMathMixin):
|
|
|
67
67
|
return self._window
|
|
68
68
|
|
|
69
69
|
@staticmethod
|
|
70
|
-
def find_intersection(layers:
|
|
70
|
+
def find_intersection(layers: Sequence[YirgacheffeLayer]) -> Area:
|
|
71
71
|
if not layers:
|
|
72
72
|
raise ValueError("Expected list of layers")
|
|
73
73
|
|
|
@@ -87,7 +87,7 @@ class YirgacheffeLayer(LayerMathMixin):
|
|
|
87
87
|
return intersection
|
|
88
88
|
|
|
89
89
|
@staticmethod
|
|
90
|
-
def find_union(layers:
|
|
90
|
+
def find_union(layers: Sequence[YirgacheffeLayer]) -> Area:
|
|
91
91
|
if not layers:
|
|
92
92
|
raise ValueError("Expected list of layers")
|
|
93
93
|
|
|
@@ -183,7 +183,7 @@ class YirgacheffeLayer(LayerMathMixin):
|
|
|
183
183
|
self._window = new_window
|
|
184
184
|
self._active_area = new_area
|
|
185
185
|
|
|
186
|
-
def reset_window(self):
|
|
186
|
+
def reset_window(self) -> None:
|
|
187
187
|
self._active_area = self._underlying_area
|
|
188
188
|
if self._pixel_scale:
|
|
189
189
|
abs_xstep, abs_ystep = abs(self._pixel_scale.xstep), abs(self._pixel_scale.ystep)
|
|
@@ -194,7 +194,7 @@ class YirgacheffeLayer(LayerMathMixin):
|
|
|
194
194
|
ysize=round_up_pixels((self.area.bottom - self.area.top) / self._pixel_scale.ystep, abs_ystep),
|
|
195
195
|
)
|
|
196
196
|
|
|
197
|
-
def offset_window_by_pixels(self, offset: int):
|
|
197
|
+
def offset_window_by_pixels(self, offset: int) -> None:
|
|
198
198
|
"""Grows (if pixels is positive) or shrinks (if pixels is negative) the window for the layer."""
|
|
199
199
|
if offset == 0:
|
|
200
200
|
return
|
yirgacheffe/layers/constant.py
CHANGED
|
@@ -4,19 +4,14 @@ from ..operators import DataType
|
|
|
4
4
|
from ..window import Area, PixelScale, Window
|
|
5
5
|
from .base import YirgacheffeLayer
|
|
6
6
|
from .._backends import backend
|
|
7
|
-
from .. import WGS_84_PROJECTION
|
|
7
|
+
from ..constants import WGS_84_PROJECTION
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class ConstantLayer(YirgacheffeLayer):
|
|
11
11
|
"""This is a layer that will return the identity value - can be used when an input layer is
|
|
12
12
|
missing (e.g., area) without having the calculation full of branches."""
|
|
13
13
|
def __init__(self, value: Union[int,float]): # pylint: disable=W0231
|
|
14
|
-
area = Area(
|
|
15
|
-
left = -180.0,
|
|
16
|
-
top = 90.0,
|
|
17
|
-
right = 180.0,
|
|
18
|
-
bottom = -90.0
|
|
19
|
-
)
|
|
14
|
+
area = Area.world()
|
|
20
15
|
super().__init__(area, None, WGS_84_PROJECTION)
|
|
21
16
|
self.value = float(value)
|
|
22
17
|
|
yirgacheffe/layers/group.py
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
import copy
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
from typing import Any, List, Optional
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, List, Optional, Union
|
|
6
5
|
|
|
7
6
|
import numpy as np
|
|
8
7
|
from yirgacheffe.operators import DataType
|
|
@@ -23,19 +22,25 @@ class GroupLayer(YirgacheffeLayer):
|
|
|
23
22
|
@classmethod
|
|
24
23
|
def layer_from_directory(
|
|
25
24
|
cls,
|
|
26
|
-
directory_path: str,
|
|
25
|
+
directory_path: Union[Path,str],
|
|
27
26
|
name: Optional[str] = None,
|
|
28
27
|
matching: str = "*.tif"
|
|
29
28
|
) -> GroupLayer:
|
|
30
29
|
if directory_path is None:
|
|
31
30
|
raise ValueError("Directory path is None")
|
|
32
|
-
|
|
31
|
+
if isinstance(directory_path, str):
|
|
32
|
+
directory_path = Path(directory_path)
|
|
33
|
+
files = list(directory_path.glob(matching))
|
|
33
34
|
if len(files) < 1:
|
|
34
35
|
raise GroupLayerEmpty(directory_path)
|
|
35
36
|
return cls.layer_from_files(files, name)
|
|
36
37
|
|
|
37
38
|
@classmethod
|
|
38
|
-
def layer_from_files(
|
|
39
|
+
def layer_from_files(
|
|
40
|
+
cls,
|
|
41
|
+
filenames: Union[List[Path],List[str]],
|
|
42
|
+
name: Optional[str] = None
|
|
43
|
+
) -> GroupLayer:
|
|
39
44
|
if filenames is None:
|
|
40
45
|
raise ValueError("filenames argument is None")
|
|
41
46
|
if len(filenames) < 1:
|
|
@@ -43,7 +48,11 @@ class GroupLayer(YirgacheffeLayer):
|
|
|
43
48
|
rasters: List[YirgacheffeLayer] = [RasterLayer.layer_from_file(x) for x in filenames]
|
|
44
49
|
return cls(rasters, name)
|
|
45
50
|
|
|
46
|
-
def __init__(
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
layers: List[YirgacheffeLayer],
|
|
54
|
+
name: Optional[str] = None
|
|
55
|
+
) -> None:
|
|
47
56
|
if not layers:
|
|
48
57
|
raise GroupLayerEmpty("Expected one or more layers")
|
|
49
58
|
if not are_pixel_scales_equal_enough([x.pixel_scale for x in layers]):
|
yirgacheffe/layers/rasters.py
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
import math
|
|
3
|
-
import
|
|
3
|
+
from pathlib import Path
|
|
4
4
|
from typing import Any, Optional, Tuple, Union
|
|
5
5
|
|
|
6
6
|
import numpy as np
|
|
7
7
|
from osgeo import gdal
|
|
8
8
|
|
|
9
|
-
from .. import WGS_84_PROJECTION
|
|
9
|
+
from ..constants import WGS_84_PROJECTION
|
|
10
10
|
from ..window import Area, PixelScale, Window
|
|
11
11
|
from ..rounding import round_up_pixels
|
|
12
12
|
from .base import YirgacheffeLayer
|
|
@@ -88,7 +88,7 @@ class RasterLayer(YirgacheffeLayer):
|
|
|
88
88
|
@staticmethod
|
|
89
89
|
def empty_raster_layer_like(
|
|
90
90
|
layer: Any,
|
|
91
|
-
filename: Optional[str]=None,
|
|
91
|
+
filename: Optional[Union[Path,str]]=None,
|
|
92
92
|
area: Optional[Area]=None,
|
|
93
93
|
datatype: Optional[Union[int, DataType]]=None,
|
|
94
94
|
compress: bool=True,
|
|
@@ -214,7 +214,7 @@ class RasterLayer(YirgacheffeLayer):
|
|
|
214
214
|
return RasterLayer(dataset)
|
|
215
215
|
|
|
216
216
|
@classmethod
|
|
217
|
-
def layer_from_file(cls, filename: str, band: int = 1) -> RasterLayer:
|
|
217
|
+
def layer_from_file(cls, filename: Union[Path,str], band: int = 1) -> RasterLayer:
|
|
218
218
|
try:
|
|
219
219
|
dataset = gdal.Open(filename, gdal.GA_ReadOnly)
|
|
220
220
|
except RuntimeError as exc:
|
|
@@ -224,7 +224,7 @@ class RasterLayer(YirgacheffeLayer):
|
|
|
224
224
|
_ = dataset.GetRasterBand(band)
|
|
225
225
|
except RuntimeError as exc:
|
|
226
226
|
raise InvalidRasterBand(band) from exc
|
|
227
|
-
return cls(dataset, filename, band)
|
|
227
|
+
return cls(dataset, str(filename), band)
|
|
228
228
|
|
|
229
229
|
def __init__(self, dataset: gdal.Dataset, name: Optional[str] = None, band: int = 1):
|
|
230
230
|
if not dataset:
|
|
@@ -252,7 +252,7 @@ class RasterLayer(YirgacheffeLayer):
|
|
|
252
252
|
assert self.window == Window(0, 0, dataset.RasterXSize, dataset.RasterYSize)
|
|
253
253
|
|
|
254
254
|
self._dataset = dataset
|
|
255
|
-
self._dataset_path = dataset.GetDescription()
|
|
255
|
+
self._dataset_path = Path(dataset.GetDescription())
|
|
256
256
|
self._band = band
|
|
257
257
|
self._raster_xsize = dataset.RasterXSize
|
|
258
258
|
self._raster_ysize = dataset.RasterYSize
|
|
@@ -275,7 +275,7 @@ class RasterLayer(YirgacheffeLayer):
|
|
|
275
275
|
|
|
276
276
|
def __getstate__(self) -> object:
|
|
277
277
|
# Only support pickling on file backed layers (ideally read only ones...)
|
|
278
|
-
if not
|
|
278
|
+
if not self._dataset_path.exists():
|
|
279
279
|
raise ValueError("Can not pickle layer that is not file backed.")
|
|
280
280
|
odict = self.__dict__.copy()
|
|
281
281
|
self._park()
|
yirgacheffe/layers/rescaled.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
from math import floor, ceil
|
|
3
|
-
from
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, Optional, Union
|
|
4
5
|
|
|
5
6
|
from skimage import transform
|
|
6
7
|
from yirgacheffe.operators import DataType
|
|
@@ -17,7 +18,7 @@ class RescaledRasterLayer(YirgacheffeLayer):
|
|
|
17
18
|
@classmethod
|
|
18
19
|
def layer_from_file(
|
|
19
20
|
cls,
|
|
20
|
-
filename: str,
|
|
21
|
+
filename: Union[Path,str],
|
|
21
22
|
pixel_scale: PixelScale,
|
|
22
23
|
band: int = 1,
|
|
23
24
|
nearest_neighbour: bool = True,
|
yirgacheffe/layers/vectors.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
-
import os
|
|
3
2
|
from math import ceil, floor
|
|
3
|
+
from pathlib import Path
|
|
4
4
|
from typing import Any, Optional, Tuple, Union
|
|
5
5
|
from typing_extensions import NotRequired
|
|
6
6
|
|
|
@@ -62,7 +62,7 @@ class RasteredVectorLayer(RasterLayer):
|
|
|
62
62
|
@classmethod
|
|
63
63
|
def layer_from_file( # type: ignore[override] # pylint: disable=W0221
|
|
64
64
|
cls,
|
|
65
|
-
filename: str,
|
|
65
|
+
filename: Union[Path,str],
|
|
66
66
|
where_filter: Optional[str],
|
|
67
67
|
scale: PixelScale,
|
|
68
68
|
projection: str,
|
|
@@ -172,7 +172,7 @@ class VectorLayer(YirgacheffeLayer):
|
|
|
172
172
|
@classmethod
|
|
173
173
|
def layer_from_file_like(
|
|
174
174
|
cls,
|
|
175
|
-
filename: str,
|
|
175
|
+
filename: Union[Path,str],
|
|
176
176
|
other_layer: YirgacheffeLayer,
|
|
177
177
|
where_filter: Optional[str]=None,
|
|
178
178
|
datatype: Optional[Union[int, DataType]] = None,
|
|
@@ -198,7 +198,7 @@ class VectorLayer(YirgacheffeLayer):
|
|
|
198
198
|
layer,
|
|
199
199
|
other_layer.pixel_scale,
|
|
200
200
|
other_layer.projection,
|
|
201
|
-
name=filename,
|
|
201
|
+
name=str(filename),
|
|
202
202
|
datatype=datatype if datatype is not None else other_layer.datatype,
|
|
203
203
|
burn_value=burn_value,
|
|
204
204
|
anchor=(other_layer.area.left, other_layer.area.top),
|
|
@@ -208,14 +208,14 @@ class VectorLayer(YirgacheffeLayer):
|
|
|
208
208
|
# a SIGSEGV when using the layers from it later, as some SWIG pointers outlive
|
|
209
209
|
# the original object being around
|
|
210
210
|
vector_layer._original = vectors
|
|
211
|
-
vector_layer._dataset_path = filename
|
|
211
|
+
vector_layer._dataset_path = filename if isinstance(filename, Path) else Path(filename)
|
|
212
212
|
vector_layer._filter = where_filter
|
|
213
213
|
return vector_layer
|
|
214
214
|
|
|
215
215
|
@classmethod
|
|
216
216
|
def layer_from_file(
|
|
217
217
|
cls,
|
|
218
|
-
filename: str,
|
|
218
|
+
filename: Union[Path,str],
|
|
219
219
|
where_filter: Optional[str],
|
|
220
220
|
scale: PixelScale,
|
|
221
221
|
projection: str,
|
|
@@ -223,9 +223,11 @@ class VectorLayer(YirgacheffeLayer):
|
|
|
223
223
|
burn_value: Union[int,float,str] = 1,
|
|
224
224
|
anchor: Tuple[float,float] = (0.0, 0.0)
|
|
225
225
|
) -> VectorLayer:
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
226
|
+
try:
|
|
227
|
+
vectors = ogr.Open(filename)
|
|
228
|
+
except RuntimeError as exc:
|
|
229
|
+
# With exceptions on GDAL now returns the wrong (IMHO) exception
|
|
230
|
+
raise FileNotFoundError(filename) from exc
|
|
229
231
|
layer = vectors.GetLayer()
|
|
230
232
|
if where_filter is not None:
|
|
231
233
|
layer.SetAttributeFilter(where_filter)
|
|
@@ -243,7 +245,7 @@ class VectorLayer(YirgacheffeLayer):
|
|
|
243
245
|
layer,
|
|
244
246
|
scale,
|
|
245
247
|
projection,
|
|
246
|
-
name=filename,
|
|
248
|
+
name=str(filename),
|
|
247
249
|
datatype=datatype_arg,
|
|
248
250
|
burn_value=burn_value,
|
|
249
251
|
anchor=anchor
|
|
@@ -253,7 +255,7 @@ class VectorLayer(YirgacheffeLayer):
|
|
|
253
255
|
# a SIGSEGV when using the layers from it later, as some SWIG pointers outlive
|
|
254
256
|
# the original object being around
|
|
255
257
|
vector_layer._original = vectors
|
|
256
|
-
vector_layer._dataset_path = filename
|
|
258
|
+
vector_layer._dataset_path = filename if isinstance(filename, Path) else Path(filename)
|
|
257
259
|
vector_layer._filter = where_filter
|
|
258
260
|
return vector_layer
|
|
259
261
|
|
|
@@ -282,7 +284,7 @@ class VectorLayer(YirgacheffeLayer):
|
|
|
282
284
|
self.burn_value = burn_value
|
|
283
285
|
|
|
284
286
|
self._original = None
|
|
285
|
-
self._dataset_path: Optional[
|
|
287
|
+
self._dataset_path: Optional[Path] = None
|
|
286
288
|
self._filter: Optional[str] = None
|
|
287
289
|
|
|
288
290
|
# work out region for mask
|
|
@@ -323,7 +325,7 @@ class VectorLayer(YirgacheffeLayer):
|
|
|
323
325
|
|
|
324
326
|
def __getstate__(self) -> object:
|
|
325
327
|
# Only support pickling on file backed layers (ideally read only ones...)
|
|
326
|
-
if self._dataset_path is None or not
|
|
328
|
+
if self._dataset_path is None or not self._dataset_path.exists():
|
|
327
329
|
raise ValueError("Can not pickle layer that is not file backed.")
|
|
328
330
|
odict = self.__dict__.copy()
|
|
329
331
|
del odict['_original']
|
yirgacheffe/operators.py
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import math
|
|
3
3
|
import multiprocessing
|
|
4
|
+
import os
|
|
4
5
|
import sys
|
|
6
|
+
import tempfile
|
|
5
7
|
import time
|
|
6
8
|
import types
|
|
7
9
|
from enum import Enum
|
|
8
10
|
from multiprocessing import Semaphore, Process
|
|
9
11
|
from multiprocessing.managers import SharedMemoryManager
|
|
10
|
-
from
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Callable, Dict, Optional, Union
|
|
11
14
|
|
|
12
15
|
import numpy as np
|
|
16
|
+
import numpy.typing as npt
|
|
13
17
|
from osgeo import gdal
|
|
14
18
|
from dill import dumps, loads # type: ignore
|
|
15
19
|
|
|
@@ -40,6 +44,9 @@ class LayerConstant:
|
|
|
40
44
|
def _eval(self, _area, _index, _step, _target_window):
|
|
41
45
|
return self.val
|
|
42
46
|
|
|
47
|
+
@property
|
|
48
|
+
def area(self):
|
|
49
|
+
return Area.world()
|
|
43
50
|
|
|
44
51
|
class LayerMathMixin:
|
|
45
52
|
|
|
@@ -346,25 +353,19 @@ class LayerOperation(LayerMathMixin):
|
|
|
346
353
|
self.__dict__.update(state)
|
|
347
354
|
|
|
348
355
|
@property
|
|
349
|
-
def area(self) ->
|
|
356
|
+
def area(self) -> Area:
|
|
350
357
|
# The type().__name__ here is to avoid a circular import dependancy
|
|
351
|
-
lhs_area = self.lhs.area
|
|
358
|
+
lhs_area = self.lhs.area
|
|
352
359
|
try:
|
|
353
|
-
rhs_area = self.rhs.area
|
|
360
|
+
rhs_area = self.rhs.area
|
|
354
361
|
except AttributeError:
|
|
355
362
|
rhs_area = None
|
|
356
363
|
try:
|
|
357
|
-
other_area = self.other.area
|
|
364
|
+
other_area = self.other.area
|
|
358
365
|
except AttributeError:
|
|
359
366
|
other_area = None
|
|
360
367
|
|
|
361
|
-
all_areas = []
|
|
362
|
-
if lhs_area is not None:
|
|
363
|
-
all_areas.append(lhs_area)
|
|
364
|
-
if rhs_area is not None:
|
|
365
|
-
all_areas.append(rhs_area)
|
|
366
|
-
if other_area is not None:
|
|
367
|
-
all_areas.append(other_area)
|
|
368
|
+
all_areas = [x for x in [lhs_area, rhs_area, other_area] if (x is not None) and (not x.is_world)]
|
|
368
369
|
|
|
369
370
|
match self.window_op:
|
|
370
371
|
case WindowOperation.NONE:
|
|
@@ -453,7 +454,7 @@ class LayerOperation(LayerMathMixin):
|
|
|
453
454
|
return lhs_data
|
|
454
455
|
|
|
455
456
|
try:
|
|
456
|
-
operator = backend.operator_map[self.operator]
|
|
457
|
+
operator: Callable = backend.operator_map[self.operator]
|
|
457
458
|
except KeyError:
|
|
458
459
|
# Handles things like `numpy_apply` where a custom operator is provided
|
|
459
460
|
operator = self.operator
|
|
@@ -511,7 +512,7 @@ class LayerOperation(LayerMathMixin):
|
|
|
511
512
|
res = chunk_max
|
|
512
513
|
return res
|
|
513
514
|
|
|
514
|
-
def save(self, destination_layer, and_sum=False, callback=None, band=1):
|
|
515
|
+
def save(self, destination_layer, and_sum=False, callback=None, band=1) -> Optional[float]:
|
|
515
516
|
"""
|
|
516
517
|
Calling save will write the output of the operation to the provied layer.
|
|
517
518
|
If you provide sum as true it will additionall compute the sum and return that.
|
|
@@ -608,7 +609,14 @@ class LayerOperation(LayerMathMixin):
|
|
|
608
609
|
except AttributeError:
|
|
609
610
|
pass
|
|
610
611
|
|
|
611
|
-
def _parallel_save(
|
|
612
|
+
def _parallel_save(
|
|
613
|
+
self,
|
|
614
|
+
destination_layer,
|
|
615
|
+
and_sum=False,
|
|
616
|
+
callback=None,
|
|
617
|
+
parallelism=None,
|
|
618
|
+
band=1
|
|
619
|
+
) -> Optional[float]:
|
|
612
620
|
assert (destination_layer is not None) or and_sum
|
|
613
621
|
try:
|
|
614
622
|
computation_window = self.window
|
|
@@ -648,7 +656,7 @@ class LayerOperation(LayerMathMixin):
|
|
|
648
656
|
or (computation_window.ysize != destination_window.ysize):
|
|
649
657
|
raise ValueError("Destination raster window size does not match input raster window size.")
|
|
650
658
|
|
|
651
|
-
|
|
659
|
+
np_type_map : Dict[int, np.dtype] = {
|
|
652
660
|
gdal.GDT_Byte: np.dtype('byte'),
|
|
653
661
|
gdal.GDT_Float32: np.dtype('float32'),
|
|
654
662
|
gdal.GDT_Float64: np.dtype('float64'),
|
|
@@ -659,7 +667,8 @@ class LayerOperation(LayerMathMixin):
|
|
|
659
667
|
gdal.GDT_UInt16: np.dtype('uint16'),
|
|
660
668
|
gdal.GDT_UInt32: np.dtype('uint32'),
|
|
661
669
|
gdal.GDT_UInt64: np.dtype('uint64'),
|
|
662
|
-
}
|
|
670
|
+
}
|
|
671
|
+
np_dtype = np_type_map[band.DataType]
|
|
663
672
|
else:
|
|
664
673
|
band = None
|
|
665
674
|
np_dtype = np.dtype('float64')
|
|
@@ -674,9 +683,13 @@ class LayerOperation(LayerMathMixin):
|
|
|
674
683
|
with SharedMemoryManager() as smm:
|
|
675
684
|
|
|
676
685
|
mem_sem_cast = []
|
|
677
|
-
for
|
|
686
|
+
for _ in range(worker_count):
|
|
678
687
|
shared_buf = smm.SharedMemory(size=np_dtype.itemsize * self.ystep * computation_window.xsize)
|
|
679
|
-
cast_buf = np.ndarray(
|
|
688
|
+
cast_buf : npt.NDArray = np.ndarray(
|
|
689
|
+
(self.ystep, computation_window.xsize),
|
|
690
|
+
dtype=np_dtype,
|
|
691
|
+
buffer=shared_buf.buf
|
|
692
|
+
)
|
|
680
693
|
cast_buf[:] = np.zeros((self.ystep, computation_window.xsize), np_dtype)
|
|
681
694
|
mem_sem_cast.append((shared_buf, Semaphore(), cast_buf))
|
|
682
695
|
|
|
@@ -746,7 +759,14 @@ class LayerOperation(LayerMathMixin):
|
|
|
746
759
|
|
|
747
760
|
return total if and_sum else None
|
|
748
761
|
|
|
749
|
-
def parallel_save(
|
|
762
|
+
def parallel_save(
|
|
763
|
+
self,
|
|
764
|
+
destination_layer,
|
|
765
|
+
and_sum=False,
|
|
766
|
+
callback=None,
|
|
767
|
+
parallelism=None,
|
|
768
|
+
band=1
|
|
769
|
+
) -> Optional[float]:
|
|
750
770
|
if destination_layer is None:
|
|
751
771
|
raise ValueError("Layer is required")
|
|
752
772
|
return self._parallel_save(destination_layer, and_sum, callback, parallelism, band)
|
|
@@ -754,6 +774,50 @@ class LayerOperation(LayerMathMixin):
|
|
|
754
774
|
def parallel_sum(self, callback=None, parallelism=None, band=1):
|
|
755
775
|
return self._parallel_save(None, True, callback, parallelism, band)
|
|
756
776
|
|
|
777
|
+
def to_geotiff(
|
|
778
|
+
self,
|
|
779
|
+
filename: Union[Path,str],
|
|
780
|
+
and_sum: bool = False,
|
|
781
|
+
parallelism:Optional[int]=None
|
|
782
|
+
) -> Optional[float]:
|
|
783
|
+
"""Saves a calculation to a raster file, optionally also returning the sum of pixels.
|
|
784
|
+
|
|
785
|
+
Parameters
|
|
786
|
+
----------
|
|
787
|
+
filename : Path
|
|
788
|
+
Path of the raster to save the result to.
|
|
789
|
+
and_sum : bool, default=False
|
|
790
|
+
If true then the function will also calculate the sum of the raster as it goes and return that value.
|
|
791
|
+
parallelism : int, optional, default=None
|
|
792
|
+
If passed, attempt to use multiple CPU cores up to the number provided.
|
|
793
|
+
|
|
794
|
+
Returns
|
|
795
|
+
-------
|
|
796
|
+
float, optional
|
|
797
|
+
Either returns None, or the sum of the pixels in the resulting raster if `and_sum` was specified.
|
|
798
|
+
"""
|
|
799
|
+
|
|
800
|
+
# We want to write to a tempfile before we move the result into place, but we can't use
|
|
801
|
+
# the actual $TMPDIR as that might be on a different device, and so we use a file next to where
|
|
802
|
+
# the final file will be, so we just need to rename the file at the end, not move it.
|
|
803
|
+
if isinstance(filename, str):
|
|
804
|
+
filename = Path(filename)
|
|
805
|
+
target_dir = filename.parent
|
|
806
|
+
|
|
807
|
+
with tempfile.NamedTemporaryFile(dir=target_dir, delete=False) as tempory_file:
|
|
808
|
+
# Local import due to circular dependancy
|
|
809
|
+
from yirgacheffe.layers.rasters import RasterLayer # type: ignore # pylint: disable=C0415
|
|
810
|
+
with RasterLayer.empty_raster_layer_like(self, filename=tempory_file.name) as layer:
|
|
811
|
+
if parallelism is None:
|
|
812
|
+
result = self.save(layer, and_sum=and_sum)
|
|
813
|
+
else:
|
|
814
|
+
result = self.parallel_save(layer, and_sum=and_sum, parallelism=parallelism)
|
|
815
|
+
|
|
816
|
+
os.makedirs(target_dir, exist_ok=True)
|
|
817
|
+
os.rename(src=tempory_file.name, dst=filename)
|
|
818
|
+
|
|
819
|
+
return result
|
|
820
|
+
|
|
757
821
|
class ShaderStyleOperation(LayerOperation):
|
|
758
822
|
|
|
759
823
|
def _eval(self, area, index, step, target_window=None):
|
yirgacheffe/window.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
1
2
|
import math
|
|
2
3
|
import sys
|
|
3
4
|
from collections import namedtuple
|
|
@@ -37,6 +38,17 @@ class Area:
|
|
|
37
38
|
right: float
|
|
38
39
|
bottom: float
|
|
39
40
|
|
|
41
|
+
@staticmethod
|
|
42
|
+
def world() -> Area:
|
|
43
|
+
"""Creates an area that covers the entire planet.
|
|
44
|
+
|
|
45
|
+
Returns
|
|
46
|
+
-------
|
|
47
|
+
Area
|
|
48
|
+
An area where the extents are nan, but is_world returns true.
|
|
49
|
+
"""
|
|
50
|
+
return Area(float("nan"), float("nan"), float("nan"), float("nan"))
|
|
51
|
+
|
|
40
52
|
def __hash__(self):
|
|
41
53
|
return (self.left, self.top, self.right, self.bottom).__hash__()
|
|
42
54
|
|
|
@@ -69,6 +81,17 @@ class Area:
|
|
|
69
81
|
bottom=self.bottom - offset
|
|
70
82
|
)
|
|
71
83
|
|
|
84
|
+
@property
|
|
85
|
+
def is_world(self) -> bool:
|
|
86
|
+
"""Returns true if this is a global area, independant of projection.
|
|
87
|
+
|
|
88
|
+
Returns
|
|
89
|
+
-------
|
|
90
|
+
bool
|
|
91
|
+
True is the Area was created with `world` otherwise False.
|
|
92
|
+
"""
|
|
93
|
+
return math.isnan(self.left)
|
|
94
|
+
|
|
72
95
|
def overlaps(self, other) -> bool:
|
|
73
96
|
"""Check if this area overlaps with another area.
|
|
74
97
|
|
|
@@ -82,6 +105,10 @@ class Area:
|
|
|
82
105
|
bool
|
|
83
106
|
True if the two areas intersect, otherwise false.
|
|
84
107
|
"""
|
|
108
|
+
|
|
109
|
+
if self.is_world or other.is_world:
|
|
110
|
+
return True
|
|
111
|
+
|
|
85
112
|
return (
|
|
86
113
|
(self.left <= other.left <= self.right) or
|
|
87
114
|
(self.left <= other.right <= self.right) or
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: yirgacheffe
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.0
|
|
4
4
|
Summary: Abstraction of gdal datasets for doing basic math operations
|
|
5
5
|
Author-email: Michael Dales <mwd24@cam.ac.uk>
|
|
6
6
|
License-Expression: ISC
|
|
@@ -13,6 +13,7 @@ Requires-Dist: numpy
|
|
|
13
13
|
Requires-Dist: gdal[numpy]
|
|
14
14
|
Requires-Dist: scikit-image
|
|
15
15
|
Requires-Dist: torch
|
|
16
|
+
Requires-Dist: dill
|
|
16
17
|
Provides-Extra: dev
|
|
17
18
|
Requires-Dist: mypy; extra == "dev"
|
|
18
19
|
Requires-Dist: pylint; extra == "dev"
|
|
@@ -52,12 +53,12 @@ The motivation for Yirgacheffe layers is to make working with gdal data slightly
|
|
|
52
53
|
For example, if we wanted to do a simple [Area of Habitat](https://github.com/quantifyearth/aoh-calculator/) calculation, whereby we find the pixels where a species resides by combining its range polygon, its habitat preferences, and its elevation preferences, the code would be like this:
|
|
53
54
|
|
|
54
55
|
```python
|
|
55
|
-
|
|
56
|
+
import yirgaceffe as yg
|
|
56
57
|
|
|
57
|
-
habitat_map =
|
|
58
|
-
elevation_map =
|
|
59
|
-
range_polygon =
|
|
60
|
-
area_per_pixel_map =
|
|
58
|
+
habitat_map = yg.read_raster("habitats.tif")
|
|
59
|
+
elevation_map = yg.read_raster('elevation.tif')
|
|
60
|
+
range_polygon = yg.read_shape_like('species123.geojson', like=habitat_map)
|
|
61
|
+
area_per_pixel_map = yg.read_raster('area_per_pixel.tif')
|
|
61
62
|
|
|
62
63
|
refined_habitat = habitat_map.isin([...species habitat codes...])
|
|
63
64
|
refined_elevation = (elevation_map >= species_min) && (elevation_map <= species_max)
|
|
@@ -70,8 +71,8 @@ print(f'area for species 123: {aoh.sum()}')
|
|
|
70
71
|
Similarly, you could save the result to a new raster layer:
|
|
71
72
|
|
|
72
73
|
```python
|
|
73
|
-
|
|
74
|
-
aoh.
|
|
74
|
+
...
|
|
75
|
+
aoh.to_geotiff("result.tif")
|
|
75
76
|
```
|
|
76
77
|
|
|
77
78
|
Yirgacheffe will automatically infer if you want to do an intersection of maps or a union of the maps based on the operators you use (see below for a full table). You can explicitly override that if you want.
|
|
@@ -109,27 +110,28 @@ If you have set either the intersection window or union window on a layer and yo
|
|
|
109
110
|
If doing per-layer operations isn't applicable for your application, you can read the pixel values for all layers (including VectorLayers) by calling `read_array` similarly to how you would for gdal. The data you access will be relative to the specified window - that is, if you've called either `set_window_for_intersection` or `set_window_for_union` then `read_array` will be relative to that and Yirgacheffe will clip or expand the data with zero values as necessary.
|
|
110
111
|
|
|
111
112
|
|
|
112
|
-
### Todo but not supported
|
|
113
|
-
|
|
114
|
-
Yirgacheffe is work in progress, so things planned but not supported currently:
|
|
115
|
-
|
|
116
|
-
* Dynamic pixel scale adjustment - all raster layers must be provided at the same pixel scale currently *NOW IN EXPERIMENTAL TESTING, SEE BELOW*
|
|
117
|
-
* A fold operation
|
|
118
|
-
* CUDA/Metal support via CUPY/MLX
|
|
119
|
-
* Dispatching work across multiple CPUs *NOW IN EXPERIMENTAL TESTING, SEE BELOW*
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
113
|
## Layer types
|
|
124
114
|
|
|
115
|
+
Note that as part of the move to the next major release, 2.0, we are adding simpler ways to create layers. Not all of those have been implemented yet, which is why this section has some inconsistencies. However, given many of the common cases are already covered, we present the new 2.0 style methods (`read_raster` and similar) here so you can write cleaner code today rather than making people wait for the final 2.0 release.
|
|
116
|
+
|
|
125
117
|
### RasterLayer
|
|
126
118
|
|
|
127
119
|
This is your basic GDAL raster layer, which you load from a geotiff.
|
|
128
120
|
|
|
129
121
|
```python
|
|
122
|
+
from yirgaceffe.layers import RasterLayer
|
|
123
|
+
|
|
130
124
|
with RasterLayer.layer_from_file('test1.tif') as layer:
|
|
131
|
-
|
|
132
|
-
|
|
125
|
+
total = layer.sum()
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The new 2.0 way of doing this is:
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
import yirgacheffe as yg
|
|
132
|
+
|
|
133
|
+
with yg.read_raster('test.tif') as layer:
|
|
134
|
+
total = layer.sum()
|
|
133
135
|
```
|
|
134
136
|
|
|
135
137
|
You can also create empty layers ready for you to store results, either by taking the dimensions from an existing layer. In both these cases you can either provide a filename to which the data will be written, or if you do not provide a filename then the layer will only exist in memory - this will be more efficient if the layer is being used for intermediary results.
|
|
@@ -166,51 +168,30 @@ This layer will load vector data and rasterize it on demand as part of a calcula
|
|
|
166
168
|
Because it will be rasterized you need to specify the pixel scale and map projection to be used when rasterising the data, and the common way to do that is by using one of your other layers.
|
|
167
169
|
|
|
168
170
|
```python
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
This class was formerly called `DynamicVectorRangeLayer`, a name now deprecated.
|
|
174
|
-
|
|
171
|
+
from yirgaceffe import WGS_84_PROJECTION
|
|
172
|
+
from yirgaceffe.window import PixelScale
|
|
173
|
+
from yirgaceffe.layers import VectorLayer
|
|
175
174
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
In certain calculations you find you have a layer where all the rows of data are the same - notably geotiffs that contain the area of a given pixel do this due to how conventional map projections work. It's hugely inefficient to load the full map into memory, so whilst you could just load them as `Layer` types, we recommend you do:
|
|
179
|
-
|
|
180
|
-
```python
|
|
181
|
-
with UniformAreaLayer('area.tiff') as layer:
|
|
182
|
-
....
|
|
175
|
+
with VectorLayer.layer_from_file('range.gpkg', PixelScale(0.001, -0.001), WGS_84_PROJECTION) as layer:
|
|
176
|
+
...
|
|
183
177
|
```
|
|
184
178
|
|
|
185
|
-
|
|
179
|
+
The new 2.0 way of doing this is:
|
|
186
180
|
|
|
187
181
|
```python
|
|
188
|
-
|
|
189
|
-
UniformAreaLayer.generate_narrow_area_projection('area.tiff', 'yirgacheffe_area.tiff')
|
|
190
|
-
area_layer = UniformAreaLayer('yirgacheffe_area.tiff')
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
### ConstantLayer
|
|
182
|
+
import yirgacheffe as yg
|
|
195
183
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
```python
|
|
199
|
-
try:
|
|
200
|
-
area_layer = UniformAreaLayer('myarea.tiff')
|
|
201
|
-
except FileDoesNotExist:
|
|
202
|
-
area_layer = ConstantLayer(0.0)
|
|
184
|
+
with yg.read_shape('range.gpkg', (0.001, -0.001), WGS_84_PROJECTION) as layer:
|
|
185
|
+
...
|
|
203
186
|
```
|
|
204
187
|
|
|
188
|
+
It is more common that when a shape file is loaded that its pixel size and projection will want to be made to match that of an existing raster (as per the opening area of habitat example). For that there is the following convenience method:
|
|
205
189
|
|
|
206
|
-
### H3CellLayer
|
|
207
|
-
|
|
208
|
-
If you have H3 installed, you can generate a mask layer based on an H3 cell identifier, where pixels inside the cell will have a value of 1, and those outside will have a value of 0.
|
|
209
|
-
|
|
210
|
-
Becuase it will be rasterized you need to specify the pixel scale and map projection to be used when rasterising the data, and the common way to do that is by using one of your other layers.
|
|
211
190
|
|
|
212
191
|
```python
|
|
213
|
-
|
|
192
|
+
with yg.read_raster("test.tif") as raster_layer:
|
|
193
|
+
with yg.read_shape_like('range.gpkg', raster_layer) as shape_layer:
|
|
194
|
+
...
|
|
214
195
|
```
|
|
215
196
|
|
|
216
197
|
### GroupLayer
|
|
@@ -239,6 +220,15 @@ with GroupLayer.layer_from_directory('.') as all_tiles:
|
|
|
239
220
|
...
|
|
240
221
|
```
|
|
241
222
|
|
|
223
|
+
The new 2.0 way of doing this is:
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
import yirgacheffe as yg
|
|
227
|
+
|
|
228
|
+
with yg.read_rasters(['tile_N10_E10.tif', 'tile_N20_E10.tif']) as all_tiles:
|
|
229
|
+
...
|
|
230
|
+
```
|
|
231
|
+
|
|
242
232
|
### TiledGroupLayer
|
|
243
233
|
|
|
244
234
|
This is a specialisation of GroupLayer, which you can use if your layers are all the same size and form a grid, as is often the case with map tiles. In this case the rendering code can be optimised and this class is significantly faster that GroupLayer.
|
|
@@ -249,11 +239,59 @@ tile2 = RasterLayer.layer_from_file('tile_N20_E10.tif')
|
|
|
249
239
|
all_tiles = TiledGroupLayer([tile1, tile2])
|
|
250
240
|
```
|
|
251
241
|
|
|
242
|
+
The new 2.0 way of doing this is:
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
import yirgacheffe as yg
|
|
246
|
+
|
|
247
|
+
with yg.read_rasters(['tile_N10_E10.tif', 'tile_N20_E10.tif'], tiled=True) as all_tiles:
|
|
248
|
+
...
|
|
249
|
+
```
|
|
250
|
+
|
|
252
251
|
Notes:
|
|
253
252
|
|
|
254
253
|
* You can have missing tiles, and these will be filled in with zeros.
|
|
255
254
|
* You can have tiles that overlap, so long as they still conform to the rule that all tiles are the same size and on a grid.
|
|
256
255
|
|
|
256
|
+
### ConstantLayer
|
|
257
|
+
|
|
258
|
+
This is there to simplify code when you have some optional layers. Rather than littering your code with checks, you can just use a constant layer, which can be included in calculations and will just return an fixed value as if it wasn't there. Useful with 0.0 or 1.0 for sum or multiplication null layers.
|
|
259
|
+
|
|
260
|
+
```python
|
|
261
|
+
try:
|
|
262
|
+
area_layer = RasterLayer.layer_from_file('myarea.tiff')
|
|
263
|
+
except FileDoesNotExist:
|
|
264
|
+
area_layer = ConstantLayer(0.0)
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### H3CellLayer
|
|
268
|
+
|
|
269
|
+
If you have H3 installed, you can generate a mask layer based on an H3 cell identifier, where pixels inside the cell will have a value of 1, and those outside will have a value of 0.
|
|
270
|
+
|
|
271
|
+
Becuase it will be rasterized you need to specify the pixel scale and map projection to be used when rasterising the data, and the common way to do that is by using one of your other layers.
|
|
272
|
+
|
|
273
|
+
```python
|
|
274
|
+
hex_cell_layer = H3CellLayer('88972eac11fffff', layer1.pixel_scale, layer1.projection)
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
### UniformAreaLayer
|
|
279
|
+
|
|
280
|
+
In certain calculations you find you have a layer where all the rows of data are the same - notably geotiffs that contain the area of a given pixel do this due to how conventional map projections work. It's hugely inefficient to load the full map into memory, so whilst you could just load them as `Layer` types, we recommend you do:
|
|
281
|
+
|
|
282
|
+
```python
|
|
283
|
+
with UniformAreaLayer('area.tiff') as layer:
|
|
284
|
+
....
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Note that loading this data can still be very slow, due to how image compression works. So if you plan to use area.tiff more than once, we recommend use save an optimised version - this will do the slow uncompression once and then save a minimal file to speed up future processing:
|
|
288
|
+
|
|
289
|
+
```python
|
|
290
|
+
if not os.path.exists('yirgacheffe_area.tiff'):
|
|
291
|
+
UniformAreaLayer.generate_narrow_area_projection('area.tiff', 'yirgacheffe_area.tiff')
|
|
292
|
+
area_layer = UniformAreaLayer('yirgacheffe_area.tiff')
|
|
293
|
+
```
|
|
294
|
+
|
|
257
295
|
|
|
258
296
|
## Supported operations on layers
|
|
259
297
|
|
|
@@ -280,6 +318,24 @@ with RasterLayer.layer_from_file('test1.tif') as layer1:
|
|
|
280
318
|
calc.save(result)
|
|
281
319
|
```
|
|
282
320
|
|
|
321
|
+
|
|
322
|
+
The new 2.0 way of doing these are:
|
|
323
|
+
|
|
324
|
+
```python
|
|
325
|
+
with yg.read_raster('test1.tif') as layer1:
|
|
326
|
+
with yg.read_raster('test2.tif') as layer2:
|
|
327
|
+
result = layer1 + layer2
|
|
328
|
+
result.to_geotiff("result.tif")
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
or
|
|
332
|
+
|
|
333
|
+
```python
|
|
334
|
+
with yg.read_raster('test1.tif') as layer1:
|
|
335
|
+
result = layer1 * 42.0
|
|
336
|
+
result.to_geotiff("result.tif")
|
|
337
|
+
```
|
|
338
|
+
|
|
283
339
|
### Boolean testing
|
|
284
340
|
|
|
285
341
|
Testing for equality, less than, less than or equal, greater than, and greater than or equal are supported on layers, along with logical or and logical and, as per this example, where `elevation_upper` and `elevation_lower` are scalar values:
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
yirgacheffe/__init__.py,sha256=n0v998xPOMMS6hdAQ16UNzmJUdnN5612HRtdudwjYa4,302
|
|
2
|
+
yirgacheffe/_core.py,sha256=6g_gqTBj4XdJ8TOiMPos5asLOWwSeKqIhpvYP49RgtI,3916
|
|
3
|
+
yirgacheffe/constants.py,sha256=uCWJwec3-ND-zVxYbsk1sdHKANl3ToNCTPg7MZb0j2g,434
|
|
4
|
+
yirgacheffe/operators.py,sha256=cEgnngjj_fZOg9BileC7WTp6VP9TnmKibR84pDK7A8I,31362
|
|
5
|
+
yirgacheffe/rounding.py,sha256=ggBG4lMyLMtHLW3dBxr3gBCcF2qhRrY5etZiFGlIoqA,2258
|
|
6
|
+
yirgacheffe/window.py,sha256=kR8sHQ6lcixpndOWC18wLRgbgS8y00tq4Fkh8PLESvM,8209
|
|
7
|
+
yirgacheffe/_backends/__init__.py,sha256=jN-2iRrHStnPI6cNL7XhwhsROtI0EaGfIrbF5c-ECV0,334
|
|
8
|
+
yirgacheffe/_backends/enumeration.py,sha256=pADawllxpW_hW-IVVvZpHWIKzvEMs9aaqfkZRD1zjnY,1003
|
|
9
|
+
yirgacheffe/_backends/mlx.py,sha256=2vOTMqHbQbeqt81Eq_8hxWDXZHaPsDpbXkALRVGEnnw,6130
|
|
10
|
+
yirgacheffe/_backends/numpy.py,sha256=cYO628s4-5K_-Bp3CrnHegzYSZfkt2QC8iE9oOOMtvA,4069
|
|
11
|
+
yirgacheffe/layers/__init__.py,sha256=mYKjw5YTcMNv_hMy7a6K4yRzIuNUbR8WuBTw4WIAmSk,435
|
|
12
|
+
yirgacheffe/layers/area.py,sha256=yIRXzeeLi3MMyuh4LG_VgZrKNWe5xwZgDGdgaoYRpP0,3805
|
|
13
|
+
yirgacheffe/layers/base.py,sha256=1B3s4cyC-DBeM357U9ds4mqwE23iIiD8x5F4aVEfc_c,12057
|
|
14
|
+
yirgacheffe/layers/constant.py,sha256=c3ugo58jkzImCiEqpzJoGLuBKJPGHRni1kxnKKcZs2E,1328
|
|
15
|
+
yirgacheffe/layers/group.py,sha256=5TICz6bQREzah1MdcEttYbTIOk0zX-KCqyuuVdaz7HU,15472
|
|
16
|
+
yirgacheffe/layers/h3layer.py,sha256=kWDqs6fIkHLR7gDL2E6F5y9-XxT6ws4M3Tj_7qF090U,9863
|
|
17
|
+
yirgacheffe/layers/rasters.py,sha256=nJKgvDpPEbj_7dG55SLGFBqPGMXjkR2oGWIRne0b558,12682
|
|
18
|
+
yirgacheffe/layers/rescaled.py,sha256=W-AhFuTIYAJVgMvcLHAZdrFwUxuJuVnAQWv8d4x1mDM,3048
|
|
19
|
+
yirgacheffe/layers/vectors.py,sha256=Y7zQkbNWYY9iNyIxfE0iN3BeWJt8yPgfSOcWbR1JDKM,15781
|
|
20
|
+
yirgacheffe-1.5.0.dist-info/licenses/LICENSE,sha256=dNSHwUCJr6axStTKDEdnJtfmDdFqlE3h1NPCveqPfnY,757
|
|
21
|
+
yirgacheffe-1.5.0.dist-info/METADATA,sha256=zjsKlcKx50LdA4D2eIdHw17E4Zl4nH9ldVSS6eS1TPc,21842
|
|
22
|
+
yirgacheffe-1.5.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
23
|
+
yirgacheffe-1.5.0.dist-info/entry_points.txt,sha256=j4KgHXbVGbGyfTySc1ypBdERpfihO4WNjppvCdE9HjE,52
|
|
24
|
+
yirgacheffe-1.5.0.dist-info/top_level.txt,sha256=9DBFlKO2Ld3hG6TuE3qOTd3Tt8ugTiXil4AN4Wr9_y0,12
|
|
25
|
+
yirgacheffe-1.5.0.dist-info/RECORD,,
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
yirgacheffe/__init__.py,sha256=nrVZPE_4DgReqmqEOfUaXJiRtxjOt-OJSIVi51Z5k98,587
|
|
2
|
-
yirgacheffe/constants.py,sha256=WccPcISG1FqL_Kw1tI72BJGjHy6RvEcGEx_I9RK776U,42
|
|
3
|
-
yirgacheffe/operators.py,sha256=eeDlIGFrspHqQBWdcXVjbP2mBEDsPxhXZucWo74geTc,29343
|
|
4
|
-
yirgacheffe/rounding.py,sha256=ggBG4lMyLMtHLW3dBxr3gBCcF2qhRrY5etZiFGlIoqA,2258
|
|
5
|
-
yirgacheffe/window.py,sha256=PVh9EIg1PMcUENjC2yahmrolGWaJP4OeM44uKqQ6I0U,7504
|
|
6
|
-
yirgacheffe/_backends/__init__.py,sha256=jN-2iRrHStnPI6cNL7XhwhsROtI0EaGfIrbF5c-ECV0,334
|
|
7
|
-
yirgacheffe/_backends/enumeration.py,sha256=pADawllxpW_hW-IVVvZpHWIKzvEMs9aaqfkZRD1zjnY,1003
|
|
8
|
-
yirgacheffe/_backends/mlx.py,sha256=oIWJULZ9NBPTqydnCTQxVwbfmBjQPOsKvY9Rhr6_wl8,6076
|
|
9
|
-
yirgacheffe/_backends/numpy.py,sha256=qQYvff1oHIuGUimV04rcFyPxnwEf0acvVd3ijeom4T4,4015
|
|
10
|
-
yirgacheffe/layers/__init__.py,sha256=mYKjw5YTcMNv_hMy7a6K4yRzIuNUbR8WuBTw4WIAmSk,435
|
|
11
|
-
yirgacheffe/layers/area.py,sha256=yIRXzeeLi3MMyuh4LG_VgZrKNWe5xwZgDGdgaoYRpP0,3805
|
|
12
|
-
yirgacheffe/layers/base.py,sha256=l2spGsDsv2DaUqkK-nuJCC97fw0kVWaLntvvLqZgJS4,11959
|
|
13
|
-
yirgacheffe/layers/constant.py,sha256=LWQuGGuUkJirLoQomZHs44oksmMKcvNGM8pOuRk9vHo,1427
|
|
14
|
-
yirgacheffe/layers/group.py,sha256=Fys5BvYmWRQMc95YE8Gy6bxxZRqvHUU7gILFE_X-CpY,15330
|
|
15
|
-
yirgacheffe/layers/h3layer.py,sha256=kWDqs6fIkHLR7gDL2E6F5y9-XxT6ws4M3Tj_7qF090U,9863
|
|
16
|
-
yirgacheffe/layers/rasters.py,sha256=I9bRuqb0W8EEvdSq3rAeKNmezIRK-yajFaHtDFR81sw,12630
|
|
17
|
-
yirgacheffe/layers/rescaled.py,sha256=v3ZXo3aNNDP4b8O6QnQHgrhBu0GslTgiZQmy6O8vb8k,3004
|
|
18
|
-
yirgacheffe/layers/vectors.py,sha256=3QWjg0E9deZyLIBp-BewgDGY90Ufqd6OcUA7jzJXJPo,15515
|
|
19
|
-
yirgacheffe-1.4.0.dist-info/licenses/LICENSE,sha256=dNSHwUCJr6axStTKDEdnJtfmDdFqlE3h1NPCveqPfnY,757
|
|
20
|
-
yirgacheffe-1.4.0.dist-info/METADATA,sha256=XamBMRPMRWZvzTu4yXqyhYIdlQkJV8mb5qCIAnt06Ls,20496
|
|
21
|
-
yirgacheffe-1.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
22
|
-
yirgacheffe-1.4.0.dist-info/entry_points.txt,sha256=j4KgHXbVGbGyfTySc1ypBdERpfihO4WNjppvCdE9HjE,52
|
|
23
|
-
yirgacheffe-1.4.0.dist-info/top_level.txt,sha256=9DBFlKO2Ld3hG6TuE3qOTd3Tt8ugTiXil4AN4Wr9_y0,12
|
|
24
|
-
yirgacheffe-1.4.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|