easy-eo 0.1.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.
@@ -0,0 +1,94 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Any, Optional, Tuple
5
+
6
+ import numpy as np
7
+ from rasterio.coords import BoundingBox
8
+ from rasterio.crs import CRS
9
+ from rasterio.transform import Affine
10
+
11
+
12
+ class BaseRasterAdapter(ABC):
13
+ ###########################
14
+ # METADATA
15
+ ##########################
16
+ @abstractmethod
17
+ def get_crs(self) -> CRS:
18
+ ...
19
+
20
+ @abstractmethod
21
+ def get_transform(self) -> Affine:
22
+ ...
23
+
24
+ @abstractmethod
25
+ def get_bounds(self) -> BoundingBox:
26
+ ...
27
+
28
+ @abstractmethod
29
+ def get_shape(self) -> Tuple[int, int]:
30
+ ...
31
+
32
+ @abstractmethod
33
+ def get_width(self) -> int:
34
+ ...
35
+
36
+ @abstractmethod
37
+ def get_height(self) -> int:
38
+ ...
39
+
40
+ @abstractmethod
41
+ def get_count(self) -> int:
42
+ ...
43
+
44
+ @abstractmethod
45
+ def get_nodata(self) -> Optional[float]:
46
+ ...
47
+
48
+ @abstractmethod
49
+ def get_metadata(self) -> dict[Any, Any]:
50
+ ...
51
+
52
+ ###########################
53
+ # DATA ACCESS
54
+ ##########################
55
+
56
+ @abstractmethod
57
+ def read(self, *args, **kwargs) -> np.ndarray:
58
+ """
59
+ Read the entire raster as a NumPy array
60
+ For multiband rasters, returns an array of shape (bands, height, width)
61
+ """
62
+ ...
63
+
64
+ @abstractmethod
65
+ def read_band(self, idx: int) -> np.ndarray:
66
+ """
67
+ Read a single band (1-based index).
68
+ """
69
+ ...
70
+
71
+ ###########################
72
+ # Persistence
73
+ ##########################
74
+ @abstractmethod
75
+ def write(self, path: str, driver: str = "GTiff") -> None:
76
+ ...
77
+
78
+
79
+ @abstractmethod
80
+ def close(self) -> None:
81
+ ...
82
+
83
+ ###########################
84
+ # BACKEND ACCESS - RETURNING THE UNDERLYING DATASET
85
+ ##########################
86
+ @property
87
+ @abstractmethod
88
+ def backend(self) -> Any:
89
+ """
90
+ Return the underlying backend object (rasterio.DatasetReader or numpy.ndarray)
91
+
92
+ WARNING: This bypasses Easy-EO abstractions
93
+ """
94
+ ...
@@ -0,0 +1,102 @@
1
+ from typing import Optional
2
+
3
+ import numpy as np
4
+ import rasterio as rio
5
+ from rasterio.crs import CRS
6
+ from rasterio.transform import Affine
7
+
8
+ from eeo.core.adapters import BaseRasterAdapter, RasterioAdapter
9
+
10
+
11
+ class NumpyRasterioAdapter(BaseRasterAdapter):
12
+ """ Numpy backend adapter for EEORasterDataset """
13
+ def __init__(
14
+ self,
15
+ array: np.ndarray,
16
+ transform: Affine,
17
+ crs: CRS,
18
+ driver: str = "GTiff",
19
+ nodata: Optional[float] = None,
20
+ ):
21
+ if array.ndim == 2:
22
+ array = array[np.newaxis, ...]
23
+
24
+ self._array = array
25
+ self._transform = transform
26
+ self._crs = crs
27
+ self._nodata = nodata
28
+ self._driver = driver
29
+
30
+ # ========================
31
+ # Metadata
32
+ # ========================
33
+ def get_crs(self) -> CRS:
34
+ return self._crs
35
+
36
+ def get_transform(self) -> Affine:
37
+ return self._transform
38
+
39
+ def get_bounds(self):
40
+ h, w = self.get_shape()
41
+ return rio.transform.array_bounds(h, w, self._transform)
42
+
43
+ def get_shape(self) -> tuple[int, int]:
44
+ return self._array.shape[-2:]
45
+
46
+ def get_width(self) -> int:
47
+ return self.get_shape()[1]
48
+
49
+ def get_height(self) -> int:
50
+ return self.get_shape()[0]
51
+
52
+ def get_count(self) -> int:
53
+ return self._array.shape[0]
54
+
55
+ def get_nodata(self) -> Optional[float]:
56
+ return self._nodata
57
+
58
+ def get_metadata(self) -> dict:
59
+ return {
60
+ "dtype": self._array.dtype,
61
+ "nodata": self._nodata,
62
+ "transform": self._transform,
63
+ "crs": self._crs,
64
+ "driver": self._driver,
65
+ "count": self.get_count(),
66
+ "width": self.get_width(),
67
+ "height": self.get_height(),
68
+ }
69
+
70
+ # ========================
71
+ # Data Access
72
+ # ========================
73
+ def read(self, *args, **kwargs) -> np.ndarray:
74
+ return self._array
75
+
76
+ def read_band(self, idx: int) -> np.ndarray:
77
+ if idx < 1 or idx > self.get_count():
78
+ raise IndexError(f"Band index {idx} out of range")
79
+ return self._array[idx - 1]
80
+
81
+ # ========================
82
+ # Persistence
83
+ # ========================
84
+ def write(self, path: str, driver: str = "GTiff") -> None:
85
+ adapter = RasterioAdapter.from_array(
86
+ array=self._array,
87
+ transform=self._transform,
88
+ crs=self._crs,
89
+ nodata=self._nodata,
90
+ )
91
+ adapter.write(path, driver=driver)
92
+ adapter.close()
93
+
94
+ def close(self) -> None:
95
+ pass
96
+
97
+ # ========================
98
+ # Backend Access
99
+ # ========================
100
+ @property
101
+ def backend(self) -> np.ndarray:
102
+ return self._array
@@ -0,0 +1,131 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional
4
+
5
+ import numpy as np
6
+ import rasterio as rio
7
+ from rasterio.io import DatasetReader, MemoryFile
8
+
9
+ from .base import BaseRasterAdapter
10
+
11
+
12
+ class RasterioAdapter(BaseRasterAdapter):
13
+ """
14
+ Rasterio-backend adapter for EEORasterDataset
15
+ """
16
+ def __init__(
17
+ self,
18
+ dataset: DatasetReader,
19
+ *,
20
+ memory_file: MemoryFile | None = None,
21
+ ) -> None:
22
+ self._ds = dataset
23
+ self._memory_file = memory_file
24
+
25
+ # ========================
26
+ # Factories
27
+ # ========================
28
+ @classmethod
29
+ def from_path(cls, path: str) -> "RasterioAdapter":
30
+ try:
31
+ dataset = rio.open(path)
32
+ except Exception as e:
33
+ raise RuntimeError(f"Failed to open raster: {path}") from e
34
+ return cls(dataset)
35
+
36
+ @classmethod
37
+ def from_array(
38
+ cls,
39
+ array: np.ndarray,
40
+ *,
41
+ transform,
42
+ crs,
43
+ nodata: Optional[float] = None,
44
+ dtype: Optional[str] = None,
45
+ ) -> "RasterioAdapter":
46
+ """
47
+ Create an in-memory adapter from a NumPy array
48
+ """
49
+ if array.ndim == 2:
50
+ array = array[np.newaxis, ...]
51
+
52
+ count, height, width = array.shape
53
+ memfile = MemoryFile()
54
+ dataset = memfile.open(
55
+ driver="GTiff",
56
+ height=height,
57
+ width=width,
58
+ count=count,
59
+ transform=transform,
60
+ crs=crs,
61
+ nodata=nodata,
62
+ dtype=dtype or array.dtype,
63
+ )
64
+ dataset.write(array)
65
+ return cls(dataset, memory_file=memfile)
66
+
67
+ # ========================
68
+ # Metadata
69
+ # ========================
70
+ def get_crs(self):
71
+ return self._ds.crs
72
+
73
+ def get_transform(self):
74
+ return self._ds.transform
75
+
76
+ def get_bounds(self):
77
+ return self._ds.bounds
78
+
79
+ def get_shape(self):
80
+ return self._ds.shape
81
+
82
+ def get_width(self):
83
+ return self._ds.width
84
+
85
+ def get_height(self):
86
+ return self._ds.height
87
+
88
+ def get_count(self):
89
+ return self._ds.count
90
+
91
+ def get_nodata(self):
92
+ return self._ds.nodata
93
+
94
+ def get_metadata(self):
95
+ return self._ds.meta.copy()
96
+
97
+ # ========================
98
+ # Data Access
99
+ # ========================
100
+ def read(self, *args, **kwargs) -> np.ndarray:
101
+ return self._ds.read(*args, **kwargs)
102
+
103
+ def read_band(self, idx: int) -> np.ndarray:
104
+ if idx < 1 or idx > self._ds.count:
105
+ raise IndexError(f"Band index {idx} out of range")
106
+ return self._ds.read(idx)
107
+
108
+ # ========================
109
+ # Persistence
110
+ # ========================
111
+ def write(self, path: str, driver: str = "GTiff") -> None:
112
+ meta = self._ds.meta.copy()
113
+ meta.update(driver=driver)
114
+
115
+ with rio.open(path, "w", **meta) as dst:
116
+ for i in range(1, self._ds.count + 1):
117
+ dst.write(self._ds.read(i), i)
118
+
119
+ def close(self) -> None:
120
+ try:
121
+ self._ds.close()
122
+ finally:
123
+ if self._memory_file is not None:
124
+ self._memory_file.close()
125
+
126
+ # ========================
127
+ # Backend Access
128
+ # ========================
129
+ @property
130
+ def backend(self) -> DatasetReader:
131
+ return self._ds
eeo/core/core.py ADDED
@@ -0,0 +1,198 @@
1
+ """
2
+ Core functionalities for easy-eo
3
+ """
4
+ from __future__ import annotations
5
+
6
+ from typing import Union
7
+
8
+ import numpy as np
9
+ import rasterio as rio
10
+ from rasterio import CRS
11
+ from rasterio.transform import Affine
12
+
13
+ from eeo.core.adapters import NumpyRasterioAdapter
14
+ from eeo.core.adapters import RasterioAdapter, BaseRasterAdapter
15
+
16
+
17
+ # IO helper
18
+ def _save_raster(dataset: rio.DatasetReader, path: str, driver: str = "GTiff") -> None:
19
+ profile = dataset.profile.copy()
20
+ if driver != "GTiff":
21
+ profile.update(driver=driver)
22
+ with rio.open(path, mode="w", **profile) as dst:
23
+ dst.write(dataset.read())
24
+
25
+
26
+ # Core class
27
+ class EEORasterDataset:
28
+ def __init__(self, adapter: BaseRasterAdapter, path: str | None = None):
29
+ self._adapter = adapter
30
+ self.path = path
31
+
32
+ # ========================
33
+ # Constructors
34
+ # ========================
35
+ @classmethod
36
+ def from_path(cls, path: str) -> "EEORasterDataset":
37
+ adapter = RasterioAdapter.from_path(path)
38
+ return cls(adapter=adapter, path=path)
39
+
40
+ @classmethod
41
+ def from_rasterio(cls, dataset: rio.DatasetReader) -> "EEORasterDataset":
42
+ return cls(adapter=RasterioAdapter(dataset))
43
+
44
+ @classmethod
45
+ def from_array(
46
+ cls,
47
+ array: np.ndarray,
48
+ transform: Affine,
49
+ crs: Union[CRS, str, int],
50
+ driver: str = "GTiff",
51
+ nodata=None,
52
+ ) -> "EEORasterDataset":
53
+ adapter = NumpyRasterioAdapter(
54
+ array=array,
55
+ transform=transform,
56
+ crs=crs,
57
+ nodata=nodata,
58
+ driver=driver,
59
+ )
60
+ return cls(adapter=adapter)
61
+
62
+ # ========================
63
+ # Conversion between adapters
64
+ # ========================
65
+
66
+ def to_rasterio(self) -> "EEORasterDataset":
67
+ backend = self._adapter.backend
68
+
69
+ # already a rasterio backend
70
+ if isinstance(backend, rio.DatasetReader):
71
+ return self
72
+
73
+ array = self.read()
74
+ transform = self.get_transform()
75
+ crs = self.get_crs()
76
+ nodata = self._adapter.get_nodata()
77
+
78
+ adapter = RasterioAdapter.from_array(
79
+ array=array,
80
+ transform=transform,
81
+ crs=crs,
82
+ nodata=nodata,
83
+ )
84
+ return EEORasterDataset(adapter=adapter)
85
+
86
+ def to_array(self) -> np.ndarray:
87
+ return self.read()
88
+
89
+ # ========================
90
+ # Metadata
91
+ # ========================
92
+
93
+ def read(self, *args, **kwargs):
94
+ """Forward rasterio.read"""
95
+ return self._adapter.read(*args, **kwargs)
96
+
97
+ def get_crs(self):
98
+ return self._adapter.get_crs()
99
+
100
+ def get_transform(self):
101
+ return self._adapter.get_transform()
102
+
103
+ def get_shape(self):
104
+ return self._adapter.get_shape()
105
+
106
+ def get_bounds(self):
107
+ return self._adapter.get_bounds()
108
+
109
+ def get_metadata(self):
110
+ return self._adapter.get_metadata()
111
+
112
+ def get_width(self):
113
+ return self._adapter.get_width()
114
+
115
+ def get_height(self):
116
+ return self._adapter.get_height()
117
+
118
+ def get_count(self):
119
+ return self._adapter.get_count()
120
+
121
+ def get_index(self):
122
+ return self.ds.index
123
+
124
+ def get_band(self, idx: int) -> np.ndarray:
125
+ return self._adapter.read_band(idx)
126
+
127
+ # ========================
128
+ # Saving
129
+ # ========================
130
+ def save_raster(self, path: str, driver: str="GTiff") -> None:
131
+ self._adapter.write(path=path, driver=driver)
132
+
133
+ # ========================
134
+ # Lifecycle
135
+ # ========================
136
+ def close(self) -> None:
137
+ self._adapter.close()
138
+
139
+ def __del__(self):
140
+ try:
141
+ self.close()
142
+ except Exception:
143
+ pass
144
+
145
+ # ========================
146
+ # Constructors
147
+ # ========================
148
+ def _bind(self, func):
149
+ """Helper that wraps external functions as bound methods"""
150
+ def method(*args, **kwargs):
151
+ result = func(*args, **kwargs)
152
+ return self if result is None else result
153
+ return method
154
+
155
+ # ========================
156
+ # Adapter access
157
+ # ========================
158
+ @property
159
+ def ds(self):
160
+ return self._adapter.backend
161
+
162
+
163
+ # ========================
164
+ # Arithmetic Operators
165
+ # ========================
166
+ def __add__(self, other):
167
+ return self.add(other)
168
+
169
+ def __radd__(self, other):
170
+ return self.add(other)
171
+
172
+ def __sub__(self, other):
173
+ return self.subtract(other)
174
+
175
+ def __rsub__(self, other):
176
+ # implement for only raster - scalar
177
+ if isinstance(other, (int, float)):
178
+ return self.multiply(-1).add(other)
179
+ return NotImplemented
180
+
181
+ def __mul__(self, other):
182
+ return self.multiply(other)
183
+
184
+ def __rmul__(self, other):
185
+ return self.multiply(other)
186
+
187
+ def __truediv__(self, other):
188
+ return self.divide(other)
189
+
190
+ def __rtruediv__(self, other):
191
+ # implement scalar / raster
192
+ if isinstance(other, (int, float)):
193
+ return self.power(-1).multiply(other)
194
+ return NotImplemented
195
+
196
+ def __pow__(self, exponent):
197
+ return self.power(exponent)
198
+