openeo-gfmap 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.
- openeo_gfmap/__init__.py +23 -0
- openeo_gfmap/backend.py +122 -0
- openeo_gfmap/features/__init__.py +17 -0
- openeo_gfmap/features/feature_extractor.py +389 -0
- openeo_gfmap/fetching/__init__.py +21 -0
- openeo_gfmap/fetching/commons.py +213 -0
- openeo_gfmap/fetching/fetching.py +98 -0
- openeo_gfmap/fetching/generic.py +165 -0
- openeo_gfmap/fetching/meteo.py +126 -0
- openeo_gfmap/fetching/s1.py +195 -0
- openeo_gfmap/fetching/s2.py +236 -0
- openeo_gfmap/inference/__init__.py +3 -0
- openeo_gfmap/inference/model_inference.py +347 -0
- openeo_gfmap/manager/__init__.py +31 -0
- openeo_gfmap/manager/job_manager.py +469 -0
- openeo_gfmap/manager/job_splitters.py +144 -0
- openeo_gfmap/metadata.py +24 -0
- openeo_gfmap/preprocessing/__init__.py +22 -0
- openeo_gfmap/preprocessing/cloudmasking.py +268 -0
- openeo_gfmap/preprocessing/compositing.py +74 -0
- openeo_gfmap/preprocessing/interpolation.py +12 -0
- openeo_gfmap/preprocessing/sar.py +64 -0
- openeo_gfmap/preprocessing/scaling.py +65 -0
- openeo_gfmap/preprocessing/udf_cldmask.py +36 -0
- openeo_gfmap/preprocessing/udf_rank.py +37 -0
- openeo_gfmap/preprocessing/udf_score.py +103 -0
- openeo_gfmap/spatial.py +53 -0
- openeo_gfmap/stac/__init__.py +2 -0
- openeo_gfmap/stac/constants.py +51 -0
- openeo_gfmap/temporal.py +22 -0
- openeo_gfmap/utils/__init__.py +23 -0
- openeo_gfmap/utils/build_df.py +48 -0
- openeo_gfmap/utils/catalogue.py +248 -0
- openeo_gfmap/utils/intervals.py +64 -0
- openeo_gfmap/utils/netcdf.py +25 -0
- openeo_gfmap/utils/tile_processing.py +64 -0
- openeo_gfmap-0.1.0.dist-info/METADATA +57 -0
- openeo_gfmap-0.1.0.dist-info/RECORD +40 -0
- openeo_gfmap-0.1.0.dist-info/WHEEL +4 -0
- openeo_gfmap-0.1.0.dist-info/licenses/LICENSE +201 -0
openeo_gfmap/__init__.py
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
"""OpenEO General Framework for Mapping.
|
2
|
+
|
3
|
+
Simplify the development of mapping applications through Remote Sensing data
|
4
|
+
by leveraging the power of OpenEO (http://openeo.org/).
|
5
|
+
|
6
|
+
More information available in the README.md file.
|
7
|
+
"""
|
8
|
+
|
9
|
+
from .backend import Backend, BackendContext
|
10
|
+
from .fetching import FetchType
|
11
|
+
from .metadata import FakeMetadata
|
12
|
+
from .spatial import BoundingBoxExtent, SpatialContext
|
13
|
+
from .temporal import TemporalContext
|
14
|
+
|
15
|
+
__all__ = [
|
16
|
+
"Backend",
|
17
|
+
"BackendContext",
|
18
|
+
"SpatialContext",
|
19
|
+
"BoundingBoxExtent",
|
20
|
+
"TemporalContext",
|
21
|
+
"FakeMetadata",
|
22
|
+
"FetchType",
|
23
|
+
]
|
openeo_gfmap/backend.py
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
"""Backend Contct.
|
2
|
+
|
3
|
+
Defines on which backend the pipeline is being currently used.
|
4
|
+
"""
|
5
|
+
|
6
|
+
import logging
|
7
|
+
import os
|
8
|
+
from dataclasses import dataclass
|
9
|
+
from enum import Enum
|
10
|
+
from typing import Callable, Dict, Optional
|
11
|
+
|
12
|
+
import openeo
|
13
|
+
|
14
|
+
_log = logging.getLogger(__name__)
|
15
|
+
|
16
|
+
|
17
|
+
class Backend(Enum):
|
18
|
+
"""Enumerating the backends supported by the Mapping Framework."""
|
19
|
+
|
20
|
+
TERRASCOPE = "terrascope"
|
21
|
+
EODC = "eodc" # Dask implementation. Do not test on this yet.
|
22
|
+
CDSE = "cdse" # Terrascope implementation (pyspark) #URL: openeo.dataspace.copernicus.eu (need to register)
|
23
|
+
CDSE_STAGING = "cdse-staging"
|
24
|
+
LOCAL = "local" # Based on the same components of EODc
|
25
|
+
FED = "fed" # Federation backend
|
26
|
+
|
27
|
+
|
28
|
+
@dataclass
|
29
|
+
class BackendContext:
|
30
|
+
"""Backend context and information.
|
31
|
+
|
32
|
+
Containing backend related information useful for the framework to
|
33
|
+
adapt the process graph.
|
34
|
+
"""
|
35
|
+
|
36
|
+
backend: Backend
|
37
|
+
|
38
|
+
|
39
|
+
def _create_connection(
|
40
|
+
url: str, *, env_var_suffix: str, connect_kwargs: Optional[dict] = None
|
41
|
+
):
|
42
|
+
"""
|
43
|
+
Generic helper to create an openEO connection
|
44
|
+
with support for multiple client credential configurations from environment variables
|
45
|
+
"""
|
46
|
+
connection = openeo.connect(url, **(connect_kwargs or {}))
|
47
|
+
|
48
|
+
if (
|
49
|
+
os.environ.get("OPENEO_AUTH_METHOD") == "client_credentials"
|
50
|
+
and f"OPENEO_AUTH_CLIENT_ID_{env_var_suffix}" in os.environ
|
51
|
+
):
|
52
|
+
# Support for multiple client credentials configs from env vars
|
53
|
+
client_id = os.environ[f"OPENEO_AUTH_CLIENT_ID_{env_var_suffix}"]
|
54
|
+
client_secret = os.environ[f"OPENEO_AUTH_CLIENT_SECRET_{env_var_suffix}"]
|
55
|
+
provider_id = os.environ.get(f"OPENEO_AUTH_PROVIDER_ID_{env_var_suffix}")
|
56
|
+
_log.info(
|
57
|
+
f"Doing client credentials from env var with {env_var_suffix=} {provider_id} {client_id=} {len(client_secret)=} "
|
58
|
+
)
|
59
|
+
|
60
|
+
connection.authenticate_oidc_client_credentials(
|
61
|
+
client_id=client_id, client_secret=client_secret, provider_id=provider_id
|
62
|
+
)
|
63
|
+
else:
|
64
|
+
# Standard authenticate_oidc procedure: refresh token, device code or default env var handling
|
65
|
+
# See https://open-eo.github.io/openeo-python-client/auth.html#oidc-authentication-dynamic-method-selection
|
66
|
+
|
67
|
+
# Use a shorter max poll time by default to alleviate the default impression that the test seem to hang
|
68
|
+
# because of the OIDC device code poll loop.
|
69
|
+
max_poll_time = int(
|
70
|
+
os.environ.get("OPENEO_OIDC_DEVICE_CODE_MAX_POLL_TIME") or 30
|
71
|
+
)
|
72
|
+
connection.authenticate_oidc(max_poll_time=max_poll_time)
|
73
|
+
return connection
|
74
|
+
|
75
|
+
|
76
|
+
def vito_connection() -> openeo.Connection:
|
77
|
+
"""Performs a connection to the VITO backend using the oidc authentication."""
|
78
|
+
return _create_connection(
|
79
|
+
url="openeo.vito.be",
|
80
|
+
env_var_suffix="VITO",
|
81
|
+
)
|
82
|
+
|
83
|
+
|
84
|
+
def cdse_connection() -> openeo.Connection:
|
85
|
+
"""Performs a connection to the CDSE backend using oidc authentication."""
|
86
|
+
return _create_connection(
|
87
|
+
url="openeo.dataspace.copernicus.eu",
|
88
|
+
env_var_suffix="CDSE",
|
89
|
+
)
|
90
|
+
|
91
|
+
|
92
|
+
def cdse_staging_connection() -> openeo.Connection:
|
93
|
+
"""Performs a connection to the CDSE backend using oidc authentication."""
|
94
|
+
return _create_connection(
|
95
|
+
url="openeo-staging.dataspace.copernicus.eu",
|
96
|
+
env_var_suffix="CDSE_STAGING",
|
97
|
+
)
|
98
|
+
|
99
|
+
|
100
|
+
def eodc_connection() -> openeo.Connection:
|
101
|
+
"""Perfroms a connection to the EODC backend using the oidc authentication."""
|
102
|
+
return _create_connection(
|
103
|
+
url="https://openeo.eodc.eu/openeo/1.1.0",
|
104
|
+
env_var_suffix="EODC",
|
105
|
+
)
|
106
|
+
|
107
|
+
|
108
|
+
def fed_connection() -> openeo.Connection:
|
109
|
+
"""Performs a connection to the OpenEO federated backend using the oidc
|
110
|
+
authentication."""
|
111
|
+
return _create_connection(
|
112
|
+
url="http://openeofed.dataspace.copernicus.eu/",
|
113
|
+
env_var_suffix="FED",
|
114
|
+
)
|
115
|
+
|
116
|
+
|
117
|
+
BACKEND_CONNECTIONS: Dict[Backend, Callable] = {
|
118
|
+
Backend.TERRASCOPE: vito_connection,
|
119
|
+
Backend.CDSE: cdse_connection,
|
120
|
+
Backend.CDSE_STAGING: cdse_staging_connection,
|
121
|
+
Backend.FED: fed_connection,
|
122
|
+
}
|
@@ -0,0 +1,17 @@
|
|
1
|
+
from openeo_gfmap.features.feature_extractor import (
|
2
|
+
LAT_HARMONIZED_NAME,
|
3
|
+
LON_HARMONIZED_NAME,
|
4
|
+
PatchFeatureExtractor,
|
5
|
+
PointFeatureExtractor,
|
6
|
+
apply_feature_extractor,
|
7
|
+
apply_feature_extractor_local,
|
8
|
+
)
|
9
|
+
|
10
|
+
__all__ = [
|
11
|
+
"PatchFeatureExtractor",
|
12
|
+
"PointFeatureExtractor",
|
13
|
+
"LAT_HARMONIZED_NAME",
|
14
|
+
"LON_HARMONIZED_NAME",
|
15
|
+
"apply_feature_extractor",
|
16
|
+
"apply_feature_extractor_local",
|
17
|
+
]
|
@@ -0,0 +1,389 @@
|
|
1
|
+
"""Feature extractor functionalities. Such as a base class to assist the
|
2
|
+
implementation of feature extractors of a UDF.
|
3
|
+
"""
|
4
|
+
import functools
|
5
|
+
import inspect
|
6
|
+
import logging
|
7
|
+
import re
|
8
|
+
import shutil
|
9
|
+
import urllib.request
|
10
|
+
from abc import ABC, abstractmethod
|
11
|
+
from pathlib import Path
|
12
|
+
|
13
|
+
import numpy as np
|
14
|
+
import openeo
|
15
|
+
import xarray as xr
|
16
|
+
from openeo.udf import XarrayDataCube
|
17
|
+
from openeo.udf.udf_data import UdfData
|
18
|
+
from pyproj import Transformer
|
19
|
+
from pyproj.crs import CRS
|
20
|
+
|
21
|
+
LAT_HARMONIZED_NAME = "GEO-LAT"
|
22
|
+
LON_HARMONIZED_NAME = "GEO-LON"
|
23
|
+
EPSG_HARMONIZED_NAME = "GEO-EPSG"
|
24
|
+
|
25
|
+
|
26
|
+
class FeatureExtractor(ABC):
|
27
|
+
"""Base class for all feature extractor UDFs. It provides some common
|
28
|
+
methods and attributes to be used by other feature extractor.
|
29
|
+
|
30
|
+
The inherited classes are supposed to take care of VectorDataCubes for
|
31
|
+
point based extraction or dense Cubes for tile/polygon based extraction.
|
32
|
+
"""
|
33
|
+
|
34
|
+
def __init__(self) -> None:
|
35
|
+
logging.basicConfig(level=logging.INFO)
|
36
|
+
self.logger = logging.getLogger(self.__class__.__name__)
|
37
|
+
|
38
|
+
@classmethod
|
39
|
+
@functools.lru_cache(maxsize=6)
|
40
|
+
def extract_dependencies(cls, base_url: str, dependency_name: str) -> str:
|
41
|
+
"""Extract the dependencies from the given URL. Unpacking a zip
|
42
|
+
file in the current working directory and return the path to the
|
43
|
+
unpacked directory.
|
44
|
+
|
45
|
+
Parameters:
|
46
|
+
- base_url: The base public URL where the dependencies are stored.
|
47
|
+
- dependency_name: The name of the dependency file to download. This
|
48
|
+
parameter is added to `base_url` as a download path to the .zip
|
49
|
+
archive
|
50
|
+
Returns:
|
51
|
+
- The absolute path to the extracted dependencies directory, to be added
|
52
|
+
to the python path with the `sys.path.append` method.
|
53
|
+
"""
|
54
|
+
|
55
|
+
# Generate absolute path for the dependencies folder
|
56
|
+
dependencies_dir = Path.cwd() / "dependencies"
|
57
|
+
|
58
|
+
# Create the directory if it doesn't exist
|
59
|
+
dependencies_dir.mkdir(exist_ok=True, parents=True)
|
60
|
+
|
61
|
+
# Download and extract the model file
|
62
|
+
modelfile_url = f"{base_url}/{dependency_name}"
|
63
|
+
modelfile, _ = urllib.request.urlretrieve(
|
64
|
+
modelfile_url, filename=dependencies_dir / Path(modelfile_url).name
|
65
|
+
)
|
66
|
+
shutil.unpack_archive(modelfile, extract_dir=dependencies_dir)
|
67
|
+
|
68
|
+
# Add the model directory to system path if it's not already there
|
69
|
+
abs_path = str(
|
70
|
+
dependencies_dir / Path(modelfile_url).name.split(".zip")[0]
|
71
|
+
) # NOQA
|
72
|
+
|
73
|
+
return abs_path
|
74
|
+
|
75
|
+
def _common_preparations(
|
76
|
+
self, inarr: xr.DataArray, parameters: dict
|
77
|
+
) -> xr.DataArray:
|
78
|
+
"""Common preparations to be executed before the feature extractor is
|
79
|
+
executed. This method should be called by the `_execute` method of the
|
80
|
+
feature extractor.
|
81
|
+
"""
|
82
|
+
self._epsg = parameters.pop(EPSG_HARMONIZED_NAME)
|
83
|
+
self._parameters = parameters
|
84
|
+
return inarr
|
85
|
+
|
86
|
+
@property
|
87
|
+
def epsg(self) -> int:
|
88
|
+
"""Returns the EPSG code of the datacube."""
|
89
|
+
return self._epsg
|
90
|
+
|
91
|
+
def dependencies(self) -> list:
|
92
|
+
"""Returns the additional dependencies such as wheels or zip files.
|
93
|
+
Dependencies should be returned as a list of string, which will set-up at the top of the
|
94
|
+
generated UDF. More information can be found at:
|
95
|
+
https://open-eo.github.io/openeo-python-client/udf.html#standard-for-declaring-python-udf-dependencies
|
96
|
+
"""
|
97
|
+
self.logger.warning(
|
98
|
+
"No additional dependencies are defined. If you wish to add "
|
99
|
+
"dependencies to your feature extractor, override the "
|
100
|
+
"`dependencies` method in your class."
|
101
|
+
)
|
102
|
+
return []
|
103
|
+
|
104
|
+
@abstractmethod
|
105
|
+
def output_labels(self) -> list:
|
106
|
+
"""Returns a list of output labels to be assigned on the output bands,
|
107
|
+
needs to be overriden by the user."""
|
108
|
+
raise NotImplementedError(
|
109
|
+
"FeatureExtractor is a base abstract class, please implement the "
|
110
|
+
"output_labels property."
|
111
|
+
)
|
112
|
+
|
113
|
+
@abstractmethod
|
114
|
+
def _execute(self, cube: XarrayDataCube, parameters: dict) -> XarrayDataCube:
|
115
|
+
raise NotImplementedError(
|
116
|
+
"FeatureExtractor is a base abstract class, please implement the "
|
117
|
+
"_execute method."
|
118
|
+
)
|
119
|
+
|
120
|
+
|
121
|
+
class PatchFeatureExtractor(FeatureExtractor):
|
122
|
+
"""Base class for all the tile/polygon based feature extractors. An user
|
123
|
+
implementing a feature extractor should take care of
|
124
|
+
"""
|
125
|
+
|
126
|
+
def get_latlons(self, inarr: xr.DataArray) -> xr.DataArray:
|
127
|
+
"""Returns the latitude and longitude coordinates of the given array in
|
128
|
+
a dataarray. Returns a dataarray with the same width/height of the input
|
129
|
+
array, but with two bands, one for latitude and one for longitude. The
|
130
|
+
metadata coordinates of the output array are the same as the input
|
131
|
+
array, as the array wasn't reprojected but instead new features were
|
132
|
+
computed.
|
133
|
+
|
134
|
+
The latitude and longitude band names are standardized to the names
|
135
|
+
`LAT_HARMONIZED_NAME` and `LON_HARMONIZED_NAME` respectively.
|
136
|
+
"""
|
137
|
+
|
138
|
+
lon = inarr.coords["x"]
|
139
|
+
lat = inarr.coords["y"]
|
140
|
+
lon, lat = np.meshgrid(lon, lat)
|
141
|
+
|
142
|
+
if self.epsg is None:
|
143
|
+
raise Exception(
|
144
|
+
"EPSG code was not defined, cannot extract lat/lon array "
|
145
|
+
"as the CRS is unknown."
|
146
|
+
)
|
147
|
+
|
148
|
+
# If the coordiantes are not in EPSG:4326, we need to reproject them
|
149
|
+
if self.epsg != 4326:
|
150
|
+
# Initializes a pyproj reprojection object
|
151
|
+
transformer = Transformer.from_crs(
|
152
|
+
crs_from=CRS.from_epsg(self.epsg),
|
153
|
+
crs_to=CRS.from_epsg(4326),
|
154
|
+
always_xy=True,
|
155
|
+
)
|
156
|
+
lon, lat = transformer.transform(xx=lon, yy=lat)
|
157
|
+
|
158
|
+
# Create a two channel numpy array of the lat and lons together by stacking
|
159
|
+
latlon = np.stack([lat, lon])
|
160
|
+
|
161
|
+
# Repack in a dataarray
|
162
|
+
return xr.DataArray(
|
163
|
+
latlon,
|
164
|
+
dims=["bands", "y", "x"],
|
165
|
+
coords={
|
166
|
+
"bands": [LAT_HARMONIZED_NAME, LON_HARMONIZED_NAME],
|
167
|
+
"y": inarr.coords["y"],
|
168
|
+
"x": inarr.coords["x"],
|
169
|
+
},
|
170
|
+
)
|
171
|
+
|
172
|
+
def _rescale_s1_backscatter(self, arr: xr.DataArray) -> xr.DataArray:
|
173
|
+
"""Rescales the input array from uint16 to float32 decibel values.
|
174
|
+
The input array should be in uint16 format, as this optimizes memory usage in Open-EO
|
175
|
+
processes. This function is called automatically on the bands of the input array, except
|
176
|
+
if the parameter `rescale_s1` is set to False.
|
177
|
+
"""
|
178
|
+
s1_bands = ["S1-SIGMA0-VV", "S1-SIGMA0-VH", "S1-SIGMA0-HV", "S1-SIGMA0-HH"]
|
179
|
+
s1_bands_to_select = list(set(arr.bands.values) & set(s1_bands))
|
180
|
+
|
181
|
+
if len(s1_bands_to_select) == 0:
|
182
|
+
return arr
|
183
|
+
|
184
|
+
data_to_rescale = arr.sel(bands=s1_bands_to_select).astype(np.float32).data
|
185
|
+
|
186
|
+
# Assert that the values are set between 1 and 65535
|
187
|
+
if data_to_rescale.min().item() < 1 or data_to_rescale.max().item() > 65535:
|
188
|
+
raise ValueError(
|
189
|
+
"The input array should be in uint16 format, with values between 1 and 65535. "
|
190
|
+
"This restriction assures that the data was processed according to the S1 fetcher "
|
191
|
+
"preprocessor. The user can disable this scaling manually by setting the "
|
192
|
+
"`rescale_s1` parameter to False in the feature extractor."
|
193
|
+
)
|
194
|
+
|
195
|
+
# Converting back to power values
|
196
|
+
data_to_rescale = 20.0 * np.log10(data_to_rescale) - 83.0
|
197
|
+
data_to_rescale = np.power(10, data_to_rescale / 10.0)
|
198
|
+
data_to_rescale[~np.isfinite(data_to_rescale)] = np.nan
|
199
|
+
|
200
|
+
# Converting power values to decibels
|
201
|
+
data_to_rescale = 10.0 * np.log10(data_to_rescale)
|
202
|
+
|
203
|
+
# Change the bands within the array
|
204
|
+
arr.loc[dict(bands=s1_bands_to_select)] = data_to_rescale
|
205
|
+
return arr
|
206
|
+
|
207
|
+
def _execute(self, cube: XarrayDataCube, parameters: dict) -> XarrayDataCube:
|
208
|
+
arr = cube.get_array().transpose("bands", "t", "y", "x")
|
209
|
+
arr = self._common_preparations(arr, parameters)
|
210
|
+
if self._parameters.get("rescale_s1", True):
|
211
|
+
arr = self._rescale_s1_backscatter(arr)
|
212
|
+
|
213
|
+
arr = self.execute(arr).transpose("bands", "y", "x")
|
214
|
+
return XarrayDataCube(arr)
|
215
|
+
|
216
|
+
@abstractmethod
|
217
|
+
def execute(self, inarr: xr.DataArray) -> xr.DataArray:
|
218
|
+
pass
|
219
|
+
|
220
|
+
|
221
|
+
class PointFeatureExtractor(FeatureExtractor):
|
222
|
+
def __init__(self):
|
223
|
+
raise NotImplementedError(
|
224
|
+
"Point based feature extraction on Vector Cubes is not supported yet."
|
225
|
+
)
|
226
|
+
|
227
|
+
def _execute(self, cube: XarrayDataCube, parameters: dict) -> XarrayDataCube:
|
228
|
+
arr = cube.get_array().transpose("bands", "t")
|
229
|
+
|
230
|
+
arr = self._common_preparations(arr, parameters)
|
231
|
+
|
232
|
+
outarr = self.execute(cube.to_array()).transpose("bands", "t")
|
233
|
+
return XarrayDataCube(outarr)
|
234
|
+
|
235
|
+
@abstractmethod
|
236
|
+
def execute(self, inarr: xr.DataArray) -> xr.DataArray:
|
237
|
+
pass
|
238
|
+
|
239
|
+
|
240
|
+
def apply_udf_data(udf_data: UdfData) -> XarrayDataCube:
|
241
|
+
feature_extractor_class = "<feature_extractor_class>"
|
242
|
+
|
243
|
+
# User-defined, feature extractor class initialized here
|
244
|
+
feature_extractor = feature_extractor_class()
|
245
|
+
|
246
|
+
is_pixel_based = issubclass(feature_extractor_class, PointFeatureExtractor)
|
247
|
+
|
248
|
+
if not is_pixel_based:
|
249
|
+
assert (
|
250
|
+
len(udf_data.datacube_list) == 1
|
251
|
+
), "OpenEO GFMAP Feature extractor pipeline only supports single input cubes for the tile."
|
252
|
+
|
253
|
+
cube = udf_data.datacube_list[0]
|
254
|
+
parameters = udf_data.user_context
|
255
|
+
|
256
|
+
proj = udf_data.proj
|
257
|
+
if proj is not None:
|
258
|
+
proj = proj["EPSG"]
|
259
|
+
|
260
|
+
parameters[EPSG_HARMONIZED_NAME] = proj
|
261
|
+
|
262
|
+
cube = feature_extractor._execute(cube, parameters=parameters)
|
263
|
+
|
264
|
+
udf_data.datacube_list = [cube]
|
265
|
+
|
266
|
+
return udf_data
|
267
|
+
|
268
|
+
|
269
|
+
def _get_imports() -> str:
|
270
|
+
with open(__file__, "r", encoding="UTF-8") as f:
|
271
|
+
script_source = f.read()
|
272
|
+
|
273
|
+
lines = script_source.split("\n")
|
274
|
+
|
275
|
+
imports = []
|
276
|
+
static_globals = []
|
277
|
+
|
278
|
+
for line in lines:
|
279
|
+
if line.strip().startswith(("import ", "from ")):
|
280
|
+
imports.append(line)
|
281
|
+
# All the global variables with the style
|
282
|
+
# UPPER_CASE_GLOBAL_VARIABLE = "constant"
|
283
|
+
elif re.match("^[A-Z_0-9]+\s*=.*$", line):
|
284
|
+
static_globals.append(line)
|
285
|
+
|
286
|
+
return "\n".join(imports) + "\n\n" + "\n".join(static_globals)
|
287
|
+
|
288
|
+
|
289
|
+
def _get_apply_udf_data(feature_extractor: FeatureExtractor) -> str:
|
290
|
+
source_lines = inspect.getsource(apply_udf_data)
|
291
|
+
source = "".join(source_lines)
|
292
|
+
# replace in the source function the `feature_extractor_class`
|
293
|
+
return source.replace('"<feature_extractor_class>"', feature_extractor.__name__)
|
294
|
+
|
295
|
+
|
296
|
+
def _generate_udf_code(
|
297
|
+
feature_extractor_class: FeatureExtractor, dependencies: list
|
298
|
+
) -> openeo.UDF:
|
299
|
+
"""Generates the udf code by packing imports of this file, the necessary
|
300
|
+
superclass and subclasses as well as the user defined feature extractor
|
301
|
+
class and the apply_datacube function.
|
302
|
+
"""
|
303
|
+
|
304
|
+
# UDF code that will be built here
|
305
|
+
udf_code = ""
|
306
|
+
|
307
|
+
assert issubclass(
|
308
|
+
feature_extractor_class, FeatureExtractor
|
309
|
+
), "The feature extractor class must be a subclass of FeatureExtractor."
|
310
|
+
|
311
|
+
dependencies_code = ""
|
312
|
+
dependencies_code += "# /// script\n"
|
313
|
+
dependencies_code += "# dependencies = [\n"
|
314
|
+
for dep in dependencies:
|
315
|
+
dependencies_code += f'# "{dep}",\n'
|
316
|
+
dependencies_code += "# ]\n"
|
317
|
+
dependencies_code += "# ///\n"
|
318
|
+
|
319
|
+
udf_code += dependencies_code + "\n"
|
320
|
+
udf_code += _get_imports() + "\n\n"
|
321
|
+
udf_code += f"{inspect.getsource(FeatureExtractor)}\n\n"
|
322
|
+
udf_code += f"{inspect.getsource(PatchFeatureExtractor)}\n\n"
|
323
|
+
udf_code += f"{inspect.getsource(PointFeatureExtractor)}\n\n"
|
324
|
+
udf_code += f"{inspect.getsource(feature_extractor_class)}\n\n"
|
325
|
+
udf_code += _get_apply_udf_data(feature_extractor_class)
|
326
|
+
return udf_code
|
327
|
+
|
328
|
+
|
329
|
+
def apply_feature_extractor(
|
330
|
+
feature_extractor_class: FeatureExtractor,
|
331
|
+
cube: openeo.rest.datacube.DataCube,
|
332
|
+
parameters: dict,
|
333
|
+
size: list,
|
334
|
+
overlap: list = [],
|
335
|
+
) -> openeo.rest.datacube.DataCube:
|
336
|
+
"""Applies an user-defined feature extractor on the cube by using the
|
337
|
+
`openeo.Cube.apply_neighborhood` method. The defined class as well as the
|
338
|
+
required subclasses will be packed into a generated UDF file that will be
|
339
|
+
executed.
|
340
|
+
|
341
|
+
Optimization can be achieved by passing integer values for the cube. By
|
342
|
+
default, the feature extractor expects to receive S1 and S2 data stored in
|
343
|
+
uint16 with the harmonized naming as implemented in the fetching module.
|
344
|
+
"""
|
345
|
+
feature_extractor = feature_extractor_class()
|
346
|
+
feature_extractor._parameters = parameters
|
347
|
+
output_labels = feature_extractor.output_labels()
|
348
|
+
dependencies = feature_extractor.dependencies()
|
349
|
+
|
350
|
+
udf_code = _generate_udf_code(feature_extractor_class, dependencies)
|
351
|
+
|
352
|
+
udf = openeo.UDF(code=udf_code, context=parameters)
|
353
|
+
|
354
|
+
cube = cube.apply_neighborhood(process=udf, size=size, overlap=overlap)
|
355
|
+
return cube.rename_labels(dimension="bands", target=output_labels)
|
356
|
+
|
357
|
+
|
358
|
+
def apply_feature_extractor_local(
|
359
|
+
feature_extractor_class: FeatureExtractor, cube: xr.DataArray, parameters: dict
|
360
|
+
) -> xr.DataArray:
|
361
|
+
"""Applies and user-defined feature extractor, but locally. The
|
362
|
+
parameters are the same as in the `apply_feature_extractor` function,
|
363
|
+
excepts for the cube parameter which expects a `xarray.DataArray` instead of
|
364
|
+
a `openeo.rest.datacube.DataCube` object.
|
365
|
+
"""
|
366
|
+
# Trying to get the local EPSG code
|
367
|
+
if EPSG_HARMONIZED_NAME not in parameters:
|
368
|
+
raise ValueError(
|
369
|
+
f"Please specify an EPSG code in the parameters with key: {EPSG_HARMONIZED_NAME} when "
|
370
|
+
f"running a Feature Extractor locally."
|
371
|
+
)
|
372
|
+
|
373
|
+
feature_extractor = feature_extractor_class()
|
374
|
+
output_labels = feature_extractor.output_labels()
|
375
|
+
dependencies = feature_extractor.dependencies()
|
376
|
+
|
377
|
+
if len(dependencies) > 0:
|
378
|
+
feature_extractor.logger.warning(
|
379
|
+
"Running UDFs locally with pip dependencies is not supported yet, "
|
380
|
+
"dependencies will not be installed."
|
381
|
+
)
|
382
|
+
|
383
|
+
cube = XarrayDataCube(cube)
|
384
|
+
|
385
|
+
return (
|
386
|
+
feature_extractor._execute(cube, parameters)
|
387
|
+
.get_array()
|
388
|
+
.assign_coords({"bands": output_labels})
|
389
|
+
)
|
@@ -0,0 +1,21 @@
|
|
1
|
+
"""Extraction sub-module.
|
2
|
+
|
3
|
+
Logic behind the extraction of training or inference data. Different backends
|
4
|
+
are supported in order to obtain a very similar result at the end of this
|
5
|
+
component.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import logging
|
9
|
+
|
10
|
+
from .fetching import CollectionFetcher, FetchType
|
11
|
+
from .s1 import build_sentinel1_grd_extractor
|
12
|
+
from .s2 import build_sentinel2_l2a_extractor
|
13
|
+
|
14
|
+
_log = logging.getLogger(__name__)
|
15
|
+
|
16
|
+
__all__ = [
|
17
|
+
"build_sentinel2_l2a_extractor",
|
18
|
+
"CollectionFetcher",
|
19
|
+
"FetchType",
|
20
|
+
"build_sentinel1_grd_extractor",
|
21
|
+
]
|