yirgacheffe 1.5.0__tar.gz → 1.6.0__tar.gz
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-1.5.0/yirgacheffe.egg-info → yirgacheffe-1.6.0}/PKG-INFO +6 -2
- {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/README.md +5 -1
- {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/pyproject.toml +1 -1
- {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/tests/test_datatypes.py +0 -1
- yirgacheffe-1.6.0/tests/test_nodata.py +64 -0
- {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/tests/test_raster.py +6 -6
- {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/yirgacheffe/_core.py +6 -3
- {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/yirgacheffe/layers/area.py +10 -3
- {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/yirgacheffe/layers/base.py +40 -4
- {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/yirgacheffe/layers/constant.py +16 -2
- {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/yirgacheffe/layers/group.py +39 -10
- {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/yirgacheffe/layers/h3layer.py +8 -1
- {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/yirgacheffe/layers/rasters.py +37 -7
- {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/yirgacheffe/layers/rescaled.py +8 -1
- {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/yirgacheffe/layers/vectors.py +10 -3
- {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/yirgacheffe/operators.py +2 -2
- {yirgacheffe-1.5.0 → yirgacheffe-1.6.0/yirgacheffe.egg-info}/PKG-INFO +6 -2
- {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/yirgacheffe.egg-info/SOURCES.txt +1 -0
- {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/LICENSE +0 -0
- {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/MANIFEST.in +0 -0
- {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/setup.cfg +0 -0
- {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/tests/test_area.py +0 -0
- {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/tests/test_auto_windowing.py +0 -0
- {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/tests/test_base.py +0 -0
- {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/tests/test_constants.py +0 -0
- {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/tests/test_group.py +0 -0
- {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/tests/test_h3layer.py +0 -0
- {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/tests/test_intersection.py +0 -0
- {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/tests/test_multiband.py +0 -0
- {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/tests/test_openers.py +0 -0
- {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/tests/test_operators.py +0 -0
- {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/tests/test_optimisation.py +0 -0
- {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/tests/test_parallel_operators.py +0 -0
- {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/tests/test_pickle.py +0 -0
- {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/tests/test_rescaling.py +0 -0
- {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/tests/test_rounding.py +0 -0
- {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/tests/test_save_with_window.py +0 -0
- {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/tests/test_sum_with_window.py +0 -0
- {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/tests/test_uniform_area_layer.py +0 -0
- {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/tests/test_union.py +0 -0
- {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/tests/test_vectors.py +0 -0
- {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/tests/test_window.py +0 -0
- {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/yirgacheffe/__init__.py +0 -0
- {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/yirgacheffe/_backends/__init__.py +0 -0
- {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/yirgacheffe/_backends/enumeration.py +0 -0
- {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/yirgacheffe/_backends/mlx.py +0 -0
- {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/yirgacheffe/_backends/numpy.py +0 -0
- {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/yirgacheffe/constants.py +0 -0
- {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/yirgacheffe/layers/__init__.py +0 -0
- {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/yirgacheffe/rounding.py +0 -0
- {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/yirgacheffe/window.py +0 -0
- {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/yirgacheffe.egg-info/dependency_links.txt +0 -0
- {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/yirgacheffe.egg-info/entry_points.txt +0 -0
- {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/yirgacheffe.egg-info/requires.txt +0 -0
- {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/yirgacheffe.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: yirgacheffe
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.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
|
|
@@ -107,7 +107,7 @@ If you have set either the intersection window or union window on a layer and yo
|
|
|
107
107
|
|
|
108
108
|
### Direct access to data
|
|
109
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
|
|
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.
|
|
111
111
|
|
|
112
112
|
|
|
113
113
|
## Layer types
|
|
@@ -160,6 +160,7 @@ with RasterLayer.layer_from_file('test1.tif') as source:
|
|
|
160
160
|
scaled = RasterLayer.scaled_raster_from_raster(source, PixelScale(0.0001, -0.0001), 'scaled.tif')
|
|
161
161
|
```
|
|
162
162
|
|
|
163
|
+
If the data is from a GeoTIFF that has a nodata value specified, then pixel values with that specified nodata value in them will be converted to NaN. You can override that by providing `ignore_nodata=True` as an optional argument to `layer_from_file` (or with the new 2.0 API, `read_raster`). You can find out if a layer has a nodata value by accessing the `nodata` property - it is None if there is no such value.
|
|
163
164
|
|
|
164
165
|
### VectorLayer
|
|
165
166
|
|
|
@@ -229,6 +230,9 @@ with yg.read_rasters(['tile_N10_E10.tif', 'tile_N20_E10.tif']) as all_tiles:
|
|
|
229
230
|
...
|
|
230
231
|
```
|
|
231
232
|
|
|
233
|
+
If any of the layers have a `nodata` value specified, then any pixel with that value will be masked out to allow data from other layers to be visible.
|
|
234
|
+
|
|
235
|
+
|
|
232
236
|
### TiledGroupLayer
|
|
233
237
|
|
|
234
238
|
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.
|
|
@@ -82,7 +82,7 @@ If you have set either the intersection window or union window on a layer and yo
|
|
|
82
82
|
|
|
83
83
|
### Direct access to data
|
|
84
84
|
|
|
85
|
-
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
|
|
85
|
+
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.
|
|
86
86
|
|
|
87
87
|
|
|
88
88
|
## Layer types
|
|
@@ -135,6 +135,7 @@ with RasterLayer.layer_from_file('test1.tif') as source:
|
|
|
135
135
|
scaled = RasterLayer.scaled_raster_from_raster(source, PixelScale(0.0001, -0.0001), 'scaled.tif')
|
|
136
136
|
```
|
|
137
137
|
|
|
138
|
+
If the data is from a GeoTIFF that has a nodata value specified, then pixel values with that specified nodata value in them will be converted to NaN. You can override that by providing `ignore_nodata=True` as an optional argument to `layer_from_file` (or with the new 2.0 API, `read_raster`). You can find out if a layer has a nodata value by accessing the `nodata` property - it is None if there is no such value.
|
|
138
139
|
|
|
139
140
|
### VectorLayer
|
|
140
141
|
|
|
@@ -204,6 +205,9 @@ with yg.read_rasters(['tile_N10_E10.tif', 'tile_N20_E10.tif']) as all_tiles:
|
|
|
204
205
|
...
|
|
205
206
|
```
|
|
206
207
|
|
|
208
|
+
If any of the layers have a `nodata` value specified, then any pixel with that value will be masked out to allow data from other layers to be visible.
|
|
209
|
+
|
|
210
|
+
|
|
207
211
|
### TiledGroupLayer
|
|
208
212
|
|
|
209
213
|
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.
|
|
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
|
|
|
6
6
|
|
|
7
7
|
[project]
|
|
8
8
|
name = "yirgacheffe"
|
|
9
|
-
version = "1.
|
|
9
|
+
version = "1.6.0"
|
|
10
10
|
description = "Abstraction of gdal datasets for doing basic math operations"
|
|
11
11
|
readme = "README.md"
|
|
12
12
|
authors = [{ name = "Michael Dales", email = "mwd24@cam.ac.uk" }]
|
|
@@ -44,7 +44,6 @@ def test_round_trip_from_gdal(ytype) -> None:
|
|
|
44
44
|
def test_round_trip_float64() -> None:
|
|
45
45
|
backend_type = backend.dtype_to_backed(DataType.Float64)
|
|
46
46
|
ytype = backend.backend_to_dtype(backend_type)
|
|
47
|
-
print(BACKEND, "sad")
|
|
48
47
|
match BACKEND:
|
|
49
48
|
case "NUMPY":
|
|
50
49
|
assert ytype == DataType.Float64
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
from yirgacheffe.layers.rasters import RasterLayer
|
|
4
|
+
from yirgacheffe.layers.group import GroupLayer
|
|
5
|
+
|
|
6
|
+
from tests.helpers import gdal_dataset_with_data
|
|
7
|
+
|
|
8
|
+
def test_raster_without_nodata_value() -> None:
|
|
9
|
+
data1 = np.array([[1.0, 2.0, 3.0, 4.0], [5.0, 6.0, 5.0, 8.0]])
|
|
10
|
+
dataset = gdal_dataset_with_data((0.0, 0.0), 0.02, data1)
|
|
11
|
+
with RasterLayer(dataset) as layer:
|
|
12
|
+
assert layer.nodata is None
|
|
13
|
+
actual = layer.read_array(0, 0, 4, 2)
|
|
14
|
+
assert np.array_equal(data1, actual, equal_nan=True)
|
|
15
|
+
|
|
16
|
+
def test_raster_with_nodata_value() -> None:
|
|
17
|
+
data1 = np.array([[1.0, 2.0, 3.0, 4.0], [5.0, 6.0, 5.0, 8.0]])
|
|
18
|
+
dataset = gdal_dataset_with_data((0.0, 0.0), 0.02, data1)
|
|
19
|
+
dataset.GetRasterBand(1).SetNoDataValue(5.0)
|
|
20
|
+
with RasterLayer(dataset) as layer:
|
|
21
|
+
assert layer.nodata == 5.0
|
|
22
|
+
data1[data1 == 5.0] = np.nan
|
|
23
|
+
actual = layer.read_array(0, 0, 4, 2)
|
|
24
|
+
assert np.array_equal(data1, actual, equal_nan=True)
|
|
25
|
+
|
|
26
|
+
def test_raster_with_nodata_value_ignored() -> None:
|
|
27
|
+
data1 = np.array([[1.0, 2.0, 3.0, 4.0], [5.0, 6.0, 5.0, 8.0]])
|
|
28
|
+
dataset = gdal_dataset_with_data((0.0, 0.0), 0.02, data1)
|
|
29
|
+
dataset.GetRasterBand(1).SetNoDataValue(5.0)
|
|
30
|
+
with RasterLayer(dataset, ignore_nodata=True) as layer:
|
|
31
|
+
assert layer.nodata == 5.0
|
|
32
|
+
actual = layer.read_array(0, 0, 4, 2)
|
|
33
|
+
assert np.array_equal(data1, actual, equal_nan=True)
|
|
34
|
+
|
|
35
|
+
def test_group_layer_with_nodata_values() -> None:
|
|
36
|
+
data1 = np.array([[1.0, 2.0, 3.0, 4.0], [5.0, 5.0, 5.0, 5.0]])
|
|
37
|
+
dataset1 = gdal_dataset_with_data((0.0, 0.0), 0.02, data1)
|
|
38
|
+
dataset1.GetRasterBand(1).SetNoDataValue(5.0)
|
|
39
|
+
|
|
40
|
+
data2 = np.array([[1.0, 1.0, 1.0, 1.0], [5.0, 6.0, 7.0, 8.0]])
|
|
41
|
+
dataset2 = gdal_dataset_with_data((0.0, 0.0), 0.02, data2)
|
|
42
|
+
dataset2.GetRasterBand(1).SetNoDataValue(1.0)
|
|
43
|
+
|
|
44
|
+
with RasterLayer(dataset1) as layer1:
|
|
45
|
+
with RasterLayer(dataset2) as layer2:
|
|
46
|
+
with GroupLayer([layer1, layer2]) as group:
|
|
47
|
+
actual = group.read_array(0, 0, 4, 2)
|
|
48
|
+
expected = np.array([[1.0, 2.0, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0]])
|
|
49
|
+
assert np.array_equal(expected, actual, equal_nan=True)
|
|
50
|
+
|
|
51
|
+
def test_group_layer_with_nodata_values_ignore_nodata() -> None:
|
|
52
|
+
data1 = np.array([[1.0, 2.0, 3.0, 4.0], [5.0, 5.0, 5.0, 5.0]])
|
|
53
|
+
dataset1 = gdal_dataset_with_data((0.0, 0.0), 0.02, data1)
|
|
54
|
+
dataset1.GetRasterBand(1).SetNoDataValue(5.0)
|
|
55
|
+
|
|
56
|
+
data2 = np.array([[1.0, 1.0, 1.0, 1.0], [5.0, 6.0, 7.0, 8.0]])
|
|
57
|
+
dataset2 = gdal_dataset_with_data((0.0, 0.0), 0.02, data2)
|
|
58
|
+
dataset2.GetRasterBand(1).SetNoDataValue(1.0)
|
|
59
|
+
|
|
60
|
+
with RasterLayer(dataset1, ignore_nodata=True) as layer1:
|
|
61
|
+
with RasterLayer(dataset2, ignore_nodata=True) as layer2:
|
|
62
|
+
with GroupLayer([layer1, layer2]) as group:
|
|
63
|
+
actual = group.read_array(0, 0, 4, 2)
|
|
64
|
+
assert np.array_equal(data1, actual, equal_nan=True)
|
|
@@ -56,12 +56,12 @@ def test_open_file() -> None:
|
|
|
56
56
|
dataset = gdal_dataset_of_region(area, 0.02, filename=path)
|
|
57
57
|
dataset.Close()
|
|
58
58
|
assert os.path.exists(path)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
59
|
+
with RasterLayer.layer_from_file(path) as layer:
|
|
60
|
+
assert layer.area == area
|
|
61
|
+
assert layer.pixel_scale == (0.02, -0.02)
|
|
62
|
+
assert layer.geo_transform == (-10, 0.02, 0.0, 10, 0.0, -0.02)
|
|
63
|
+
assert layer.window == Window(0, 0, 1000, 1000)
|
|
64
|
+
del layer
|
|
65
65
|
|
|
66
66
|
@pytest.mark.parametrize("initial_area",
|
|
67
67
|
[
|
|
@@ -10,7 +10,8 @@ from .operators import DataType
|
|
|
10
10
|
|
|
11
11
|
def read_raster(
|
|
12
12
|
filename: Union[Path,str],
|
|
13
|
-
band: int = 1
|
|
13
|
+
band: int = 1,
|
|
14
|
+
ignore_nodata: bool = False,
|
|
14
15
|
) -> RasterLayer:
|
|
15
16
|
"""Open a raster file (e.g., GeoTIFF).
|
|
16
17
|
|
|
@@ -19,14 +20,16 @@ def read_raster(
|
|
|
19
20
|
filename : Path
|
|
20
21
|
Path of raster file to open.
|
|
21
22
|
band : int, default=1
|
|
22
|
-
For multi-band rasters, which band to use (defaults to first if not specified)
|
|
23
|
+
For multi-band rasters, which band to use (defaults to first if not specified)
|
|
24
|
+
ignore_nodata : bool, default=False
|
|
25
|
+
If the GeoTIFF has a NODATA value, don't subsitute that value for NaN
|
|
23
26
|
|
|
24
27
|
Returns
|
|
25
28
|
-------
|
|
26
29
|
RasterLayer
|
|
27
30
|
Returns an layer representing the raster data.
|
|
28
31
|
"""
|
|
29
|
-
return RasterLayer.layer_from_file(filename, band)
|
|
32
|
+
return RasterLayer.layer_from_file(filename, band, ignore_nodata)
|
|
30
33
|
|
|
31
34
|
def read_rasters(
|
|
32
35
|
filenames : Union[List[Path],List[str]],
|
|
@@ -56,12 +56,12 @@ class UniformAreaLayer(RasterLayer):
|
|
|
56
56
|
return False
|
|
57
57
|
return True
|
|
58
58
|
|
|
59
|
-
def __init__(self, dataset, name: Optional[str] = None, band: int = 1):
|
|
59
|
+
def __init__(self, dataset, name: Optional[str] = None, band: int = 1, ignore_nodata: bool = False):
|
|
60
60
|
if dataset.RasterXSize > 1:
|
|
61
61
|
raise ValueError("Expected a shrunk dataset")
|
|
62
62
|
self.databand = dataset.GetRasterBand(1).ReadAsArray(0, 0, 1, dataset.RasterYSize)
|
|
63
63
|
|
|
64
|
-
super().__init__(dataset, name, band)
|
|
64
|
+
super().__init__(dataset, name, band, ignore_nodata)
|
|
65
65
|
|
|
66
66
|
transform = dataset.GetGeoTransform()
|
|
67
67
|
|
|
@@ -84,7 +84,14 @@ class UniformAreaLayer(RasterLayer):
|
|
|
84
84
|
)
|
|
85
85
|
self._raster_xsize = self.window.xsize
|
|
86
86
|
|
|
87
|
-
def
|
|
87
|
+
def _read_array_with_window(
|
|
88
|
+
self,
|
|
89
|
+
xoffset: int,
|
|
90
|
+
yoffset: int,
|
|
91
|
+
xsize: int,
|
|
92
|
+
ysize: int,
|
|
93
|
+
window: Window,
|
|
94
|
+
) -> Any:
|
|
88
95
|
if ysize <= 0:
|
|
89
96
|
raise ValueError("Request dimensions must be positive and non-zero")
|
|
90
97
|
offset = window.yoff + yoffset
|
|
@@ -66,6 +66,10 @@ class YirgacheffeLayer(LayerMathMixin):
|
|
|
66
66
|
raise AttributeError("Layer has no window")
|
|
67
67
|
return self._window
|
|
68
68
|
|
|
69
|
+
@property
|
|
70
|
+
def nodata(self) -> None:
|
|
71
|
+
return None
|
|
72
|
+
|
|
69
73
|
@staticmethod
|
|
70
74
|
def find_intersection(layers: Sequence[YirgacheffeLayer]) -> Area:
|
|
71
75
|
if not layers:
|
|
@@ -227,10 +231,24 @@ class YirgacheffeLayer(LayerMathMixin):
|
|
|
227
231
|
self._window = new_window
|
|
228
232
|
self._active_area = new_area
|
|
229
233
|
|
|
230
|
-
def
|
|
234
|
+
def _read_array_with_window(
|
|
235
|
+
self,
|
|
236
|
+
_x: int,
|
|
237
|
+
_y: int,
|
|
238
|
+
_xsize: int,
|
|
239
|
+
_ysize: int,
|
|
240
|
+
_window: Window,
|
|
241
|
+
) -> Any:
|
|
231
242
|
raise NotImplementedError("Must be overridden by subclass")
|
|
232
243
|
|
|
233
|
-
def
|
|
244
|
+
def _read_array_for_area(
|
|
245
|
+
self,
|
|
246
|
+
target_area: Area,
|
|
247
|
+
x: int,
|
|
248
|
+
y: int,
|
|
249
|
+
width: int,
|
|
250
|
+
height: int,
|
|
251
|
+
) -> Any:
|
|
234
252
|
assert self._pixel_scale is not None
|
|
235
253
|
|
|
236
254
|
target_window = Window(
|
|
@@ -247,10 +265,28 @@ class YirgacheffeLayer(LayerMathMixin):
|
|
|
247
265
|
(self._pixel_scale.ystep * -1.0)
|
|
248
266
|
),
|
|
249
267
|
)
|
|
250
|
-
return self.
|
|
268
|
+
return self._read_array_with_window(x, y, width, height, target_window)
|
|
251
269
|
|
|
252
270
|
def read_array(self, x: int, y: int, width: int, height: int) -> Any:
|
|
253
|
-
|
|
271
|
+
"""Reads data from the layer based on the current reference window.
|
|
272
|
+
|
|
273
|
+
Arguments
|
|
274
|
+
---------
|
|
275
|
+
x : int
|
|
276
|
+
X axis offset for reading
|
|
277
|
+
y : int
|
|
278
|
+
Y axis offset for reading
|
|
279
|
+
width : int
|
|
280
|
+
Width of data to read
|
|
281
|
+
height : int
|
|
282
|
+
Height of data to read
|
|
283
|
+
|
|
284
|
+
Results
|
|
285
|
+
-------
|
|
286
|
+
Any
|
|
287
|
+
An array of values from the layer.
|
|
288
|
+
"""
|
|
289
|
+
return self._read_array_with_window(x, y, width, height, self.window)
|
|
254
290
|
|
|
255
291
|
def latlng_for_pixel(self, x_coord: int, y_coord: int) -> Tuple[float,float]:
|
|
256
292
|
"""Get geo coords for pixel. This is relative to the set view window."""
|
|
@@ -28,8 +28,22 @@ class ConstantLayer(YirgacheffeLayer):
|
|
|
28
28
|
def read_array(self, _x: int, _y: int, width: int, height: int) -> Any:
|
|
29
29
|
return backend.full((height, width), self.value)
|
|
30
30
|
|
|
31
|
-
def
|
|
31
|
+
def _read_array_with_window(
|
|
32
|
+
self,
|
|
33
|
+
_x: int,
|
|
34
|
+
_y: int,
|
|
35
|
+
width: int,
|
|
36
|
+
height: int,
|
|
37
|
+
_window: Window,
|
|
38
|
+
) -> Any:
|
|
32
39
|
return backend.full((height, width), self.value)
|
|
33
40
|
|
|
34
|
-
def
|
|
41
|
+
def _read_array_for_area(
|
|
42
|
+
self,
|
|
43
|
+
_target_area: Area,
|
|
44
|
+
x: int,
|
|
45
|
+
y: int,
|
|
46
|
+
width: int,
|
|
47
|
+
height: int,
|
|
48
|
+
) -> Any:
|
|
35
49
|
return self.read_array(x, y, width, height)
|
|
@@ -4,8 +4,9 @@ from pathlib import Path
|
|
|
4
4
|
from typing import Any, List, Optional, Union
|
|
5
5
|
|
|
6
6
|
import numpy as np
|
|
7
|
-
from
|
|
7
|
+
from numpy import ma
|
|
8
8
|
|
|
9
|
+
from ..operators import DataType
|
|
9
10
|
from ..rounding import are_pixel_scales_equal_enough, round_down_pixels
|
|
10
11
|
from ..window import Area, Window
|
|
11
12
|
from .base import YirgacheffeLayer
|
|
@@ -104,8 +105,14 @@ class GroupLayer(YirgacheffeLayer):
|
|
|
104
105
|
except AttributeError:
|
|
105
106
|
pass # called from Base constructor before we've added the extra field
|
|
106
107
|
|
|
107
|
-
def
|
|
108
|
-
|
|
108
|
+
def _read_array_with_window(
|
|
109
|
+
self,
|
|
110
|
+
xoffset: int,
|
|
111
|
+
yoffset: int,
|
|
112
|
+
xsize: int,
|
|
113
|
+
ysize: int,
|
|
114
|
+
window: Window,
|
|
115
|
+
) -> Any:
|
|
109
116
|
if (xsize <= 0) or (ysize <= 0):
|
|
110
117
|
raise ValueError("Request dimensions must be positive and non-zero")
|
|
111
118
|
|
|
@@ -139,12 +146,15 @@ class GroupLayer(YirgacheffeLayer):
|
|
|
139
146
|
if len(contributing_layers) == 1:
|
|
140
147
|
layer, adjusted_layer_window, intersection = contributing_layers[0]
|
|
141
148
|
if target_window == intersection:
|
|
142
|
-
|
|
149
|
+
data = layer.read_array(
|
|
143
150
|
intersection.xoff - adjusted_layer_window.xoff,
|
|
144
151
|
intersection.yoff - adjusted_layer_window.yoff,
|
|
145
152
|
intersection.xsize,
|
|
146
153
|
intersection.ysize
|
|
147
154
|
)
|
|
155
|
+
if layer.nodata is not None:
|
|
156
|
+
data[data.isnan()] = 0.0
|
|
157
|
+
return data
|
|
148
158
|
|
|
149
159
|
result = np.zeros((ysize, xsize), dtype=float)
|
|
150
160
|
for layer, adjusted_layer_window, intersection in contributing_layers:
|
|
@@ -152,14 +162,26 @@ class GroupLayer(YirgacheffeLayer):
|
|
|
152
162
|
intersection.xoff - adjusted_layer_window.xoff,
|
|
153
163
|
intersection.yoff - adjusted_layer_window.yoff,
|
|
154
164
|
intersection.xsize,
|
|
155
|
-
intersection.ysize
|
|
165
|
+
intersection.ysize,
|
|
156
166
|
)
|
|
157
167
|
result_x_offset = (intersection.xoff - xoffset) - window.xoff
|
|
158
168
|
result_y_offset = (intersection.yoff - yoffset) - window.yoff
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
169
|
+
if layer.nodata is None:
|
|
170
|
+
result[
|
|
171
|
+
result_y_offset:result_y_offset + intersection.ysize,
|
|
172
|
+
result_x_offset:result_x_offset + intersection.xsize
|
|
173
|
+
] = data
|
|
174
|
+
else:
|
|
175
|
+
masked = ma.masked_invalid(data)
|
|
176
|
+
before = result[
|
|
177
|
+
result_y_offset:result_y_offset + intersection.ysize,
|
|
178
|
+
result_x_offset:result_x_offset + intersection.xsize
|
|
179
|
+
]
|
|
180
|
+
merged = ma.where(masked.mask, before, masked)
|
|
181
|
+
result[
|
|
182
|
+
result_y_offset:result_y_offset + intersection.ysize,
|
|
183
|
+
result_x_offset:result_x_offset + intersection.xsize
|
|
184
|
+
] = merged
|
|
163
185
|
|
|
164
186
|
return backend.promote(result)
|
|
165
187
|
|
|
@@ -214,7 +236,14 @@ class TiledGroupLayer(GroupLayer):
|
|
|
214
236
|
* You can have missing tiles, and it'll fill in zeros.
|
|
215
237
|
* The tiles can overlap - e.g., JRC Annual Change tiles all overlap by a few pixels on all edges.
|
|
216
238
|
"""
|
|
217
|
-
def
|
|
239
|
+
def _read_array_with_window(
|
|
240
|
+
self,
|
|
241
|
+
xoffset: int,
|
|
242
|
+
yoffset: int,
|
|
243
|
+
xsize: int,
|
|
244
|
+
ysize: int,
|
|
245
|
+
window: Window,
|
|
246
|
+
) -> Any:
|
|
218
247
|
if (xsize <= 0) or (ysize <= 0):
|
|
219
248
|
raise ValueError("Request dimensions must be positive and non-zero")
|
|
220
249
|
|
|
@@ -82,7 +82,14 @@ class H3CellLayer(YirgacheffeLayer):
|
|
|
82
82
|
def datatype(self) -> DataType:
|
|
83
83
|
return DataType.Float64
|
|
84
84
|
|
|
85
|
-
def
|
|
85
|
+
def _read_array_with_window(
|
|
86
|
+
self,
|
|
87
|
+
xoffset: int,
|
|
88
|
+
yoffset: int,
|
|
89
|
+
xsize: int,
|
|
90
|
+
ysize: int,
|
|
91
|
+
window: Window,
|
|
92
|
+
) -> Any:
|
|
86
93
|
assert self._pixel_scale is not None
|
|
87
94
|
|
|
88
95
|
if (xsize <= 0) or (ysize <= 0):
|
|
@@ -214,7 +214,12 @@ class RasterLayer(YirgacheffeLayer):
|
|
|
214
214
|
return RasterLayer(dataset)
|
|
215
215
|
|
|
216
216
|
@classmethod
|
|
217
|
-
def layer_from_file(
|
|
217
|
+
def layer_from_file(
|
|
218
|
+
cls,
|
|
219
|
+
filename: Union[Path,str],
|
|
220
|
+
band: int = 1,
|
|
221
|
+
ignore_nodata: bool = False,
|
|
222
|
+
) -> RasterLayer:
|
|
218
223
|
try:
|
|
219
224
|
dataset = gdal.Open(filename, gdal.GA_ReadOnly)
|
|
220
225
|
except RuntimeError as exc:
|
|
@@ -224,9 +229,15 @@ class RasterLayer(YirgacheffeLayer):
|
|
|
224
229
|
_ = dataset.GetRasterBand(band)
|
|
225
230
|
except RuntimeError as exc:
|
|
226
231
|
raise InvalidRasterBand(band) from exc
|
|
227
|
-
return cls(dataset, str(filename), band)
|
|
228
|
-
|
|
229
|
-
def __init__(
|
|
232
|
+
return cls(dataset, str(filename), band, ignore_nodata)
|
|
233
|
+
|
|
234
|
+
def __init__(
|
|
235
|
+
self,
|
|
236
|
+
dataset: gdal.Dataset,
|
|
237
|
+
name: Optional[str] = None,
|
|
238
|
+
band: int = 1,
|
|
239
|
+
ignore_nodata: bool = False,
|
|
240
|
+
) -> None:
|
|
230
241
|
if not dataset:
|
|
231
242
|
raise ValueError("None is not a valid dataset")
|
|
232
243
|
|
|
@@ -256,6 +267,7 @@ class RasterLayer(YirgacheffeLayer):
|
|
|
256
267
|
self._band = band
|
|
257
268
|
self._raster_xsize = dataset.RasterXSize
|
|
258
269
|
self._raster_ysize = dataset.RasterYSize
|
|
270
|
+
self._ignore_nodata = ignore_nodata
|
|
259
271
|
|
|
260
272
|
@property
|
|
261
273
|
def _raster_dimensions(self) -> Tuple[int,int]:
|
|
@@ -305,7 +317,21 @@ class RasterLayer(YirgacheffeLayer):
|
|
|
305
317
|
assert self._dataset
|
|
306
318
|
return DataType.of_gdal(self._dataset.GetRasterBand(1).DataType)
|
|
307
319
|
|
|
308
|
-
|
|
320
|
+
@property
|
|
321
|
+
def nodata(self) -> Optional[Any]:
|
|
322
|
+
if self._dataset is None:
|
|
323
|
+
self._unpark()
|
|
324
|
+
assert self._dataset
|
|
325
|
+
return self._dataset.GetRasterBand(self._band).GetNoDataValue()
|
|
326
|
+
|
|
327
|
+
def _read_array_with_window(
|
|
328
|
+
self,
|
|
329
|
+
xoffset: int,
|
|
330
|
+
yoffset: int,
|
|
331
|
+
xsize: int,
|
|
332
|
+
ysize: int,
|
|
333
|
+
window: Window,
|
|
334
|
+
) -> Any:
|
|
309
335
|
if self._dataset is None:
|
|
310
336
|
self._unpark()
|
|
311
337
|
assert self._dataset
|
|
@@ -335,7 +361,6 @@ class RasterLayer(YirgacheffeLayer):
|
|
|
335
361
|
if target_window == intersection:
|
|
336
362
|
# The target window is a subset of or equal to the source, so we can just ask for the data
|
|
337
363
|
data = backend.promote(self._dataset.GetRasterBand(self._band).ReadAsArray(*intersection.as_array_args))
|
|
338
|
-
return data
|
|
339
364
|
else:
|
|
340
365
|
# We should read the intersection from the array, and the rest should be zeros
|
|
341
366
|
subset = backend.promote(self._dataset.GetRasterBand(self._band).ReadAsArray(*intersection.as_array_args))
|
|
@@ -350,4 +375,9 @@ class RasterLayer(YirgacheffeLayer):
|
|
|
350
375
|
)
|
|
351
376
|
)).astype(int)
|
|
352
377
|
data = backend.pad(subset, region, mode='constant')
|
|
353
|
-
|
|
378
|
+
|
|
379
|
+
nodata = self.nodata
|
|
380
|
+
if not self._ignore_nodata and nodata is not None:
|
|
381
|
+
data = backend.where(data == nodata, float("nan"), data)
|
|
382
|
+
|
|
383
|
+
return data
|
|
@@ -62,7 +62,14 @@ class RescaledRasterLayer(YirgacheffeLayer):
|
|
|
62
62
|
def datatype(self) -> DataType:
|
|
63
63
|
return self._src.datatype
|
|
64
64
|
|
|
65
|
-
def
|
|
65
|
+
def _read_array_with_window(
|
|
66
|
+
self,
|
|
67
|
+
xoffset: int,
|
|
68
|
+
yoffset: int,
|
|
69
|
+
xsize: int,
|
|
70
|
+
ysize: int,
|
|
71
|
+
window: Window,
|
|
72
|
+
) -> Any:
|
|
66
73
|
|
|
67
74
|
# to avoid aliasing issues, we try to scale to the nearest pixel
|
|
68
75
|
# and recrop when scaling bigger
|
|
@@ -359,7 +359,14 @@ class VectorLayer(YirgacheffeLayer):
|
|
|
359
359
|
def datatype(self) -> DataType:
|
|
360
360
|
return self._datatype
|
|
361
361
|
|
|
362
|
-
def
|
|
362
|
+
def _read_array_for_area(
|
|
363
|
+
self,
|
|
364
|
+
target_area: Area,
|
|
365
|
+
x: int,
|
|
366
|
+
y: int,
|
|
367
|
+
width: int,
|
|
368
|
+
height: int,
|
|
369
|
+
) -> Any:
|
|
363
370
|
assert self._pixel_scale is not None
|
|
364
371
|
|
|
365
372
|
if self._original is None:
|
|
@@ -399,8 +406,8 @@ class VectorLayer(YirgacheffeLayer):
|
|
|
399
406
|
res = backend.promote(dataset.ReadAsArray(0, 0, width, height))
|
|
400
407
|
return res
|
|
401
408
|
|
|
402
|
-
def
|
|
409
|
+
def _read_array_with_window(self, _x, _y, _width, _height, _window) -> Any:
|
|
403
410
|
assert NotRequired
|
|
404
411
|
|
|
405
412
|
def read_array(self, x: int, y: int, width: int, height: int) -> Any:
|
|
406
|
-
return self.
|
|
413
|
+
return self._read_array_for_area(self._active_area, x, y, width, height)
|
|
@@ -98,9 +98,9 @@ class LayerMathMixin:
|
|
|
98
98
|
def _eval(self, area, index, step, target_window=None):
|
|
99
99
|
try:
|
|
100
100
|
window = self.window if target_window is None else target_window
|
|
101
|
-
return self.
|
|
101
|
+
return self._read_array_for_area(area, 0, index, window.xsize, step)
|
|
102
102
|
except AttributeError:
|
|
103
|
-
return self.
|
|
103
|
+
return self._read_array_for_area(area, 0, index, target_window.xsize if target_window else 1, step)
|
|
104
104
|
|
|
105
105
|
def nan_to_num(self, nan=0, posinf=None, neginf=None):
|
|
106
106
|
return LayerOperation(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: yirgacheffe
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.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
|
|
@@ -107,7 +107,7 @@ If you have set either the intersection window or union window on a layer and yo
|
|
|
107
107
|
|
|
108
108
|
### Direct access to data
|
|
109
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
|
|
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.
|
|
111
111
|
|
|
112
112
|
|
|
113
113
|
## Layer types
|
|
@@ -160,6 +160,7 @@ with RasterLayer.layer_from_file('test1.tif') as source:
|
|
|
160
160
|
scaled = RasterLayer.scaled_raster_from_raster(source, PixelScale(0.0001, -0.0001), 'scaled.tif')
|
|
161
161
|
```
|
|
162
162
|
|
|
163
|
+
If the data is from a GeoTIFF that has a nodata value specified, then pixel values with that specified nodata value in them will be converted to NaN. You can override that by providing `ignore_nodata=True` as an optional argument to `layer_from_file` (or with the new 2.0 API, `read_raster`). You can find out if a layer has a nodata value by accessing the `nodata` property - it is None if there is no such value.
|
|
163
164
|
|
|
164
165
|
### VectorLayer
|
|
165
166
|
|
|
@@ -229,6 +230,9 @@ with yg.read_rasters(['tile_N10_E10.tif', 'tile_N20_E10.tif']) as all_tiles:
|
|
|
229
230
|
...
|
|
230
231
|
```
|
|
231
232
|
|
|
233
|
+
If any of the layers have a `nodata` value specified, then any pixel with that value will be masked out to allow data from other layers to be visible.
|
|
234
|
+
|
|
235
|
+
|
|
232
236
|
### TiledGroupLayer
|
|
233
237
|
|
|
234
238
|
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.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|