Skip to content

Commit 43dd983

Browse files
committed
Add support for get_NOBSperc flag for calculating observation percentage and refactor job options and preprocessing functions for enhanced flexibility.
1 parent 8263bde commit 43dd983

4 files changed

Lines changed: 178 additions & 101 deletions

File tree

README.md

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,20 @@ SLC_masking_algo: str = 'mask_scl_dilation'
8181
Masking method for Sentinel-2 optical data ('satio', 'mask_scl_dilation', None)
8282
Note: if set to None, no masking is applied and the S2 L2A data is used as is.
8383
84-
apply_cloud_mask : bool = True
84+
apply_cloud_mask: bool = True
8585
if True, the Sentinel-2 or PlanetScope data is masked for clouds (based on Sentinel-2 QA band).
86-
If False, no masking is applied but the mask band is still created and added to the cube.
87-
Note: no effect when 'mask_scl_dilation' parameter is set to None.
88-
89-
append : bool = True
86+
If False, no masking is applied but the mask band is still created and added to the cube. (Note: the cloud
87+
mask band is removed from the feature cube as soon the time domain is aggregated.)
88+
Note: no effect when 'mask_scl_dilation' parameter is set to None.
89+
90+
get_NOBSperc: bool = False
91+
if True, the number of observations per pixel is calculated and added to the feature cube.
92+
Note: this flag is only relevant for Sentinel-2 data as well as the 'band' NOBSperc is only added
93+
to cubes with time domain aggregation.
94+
Note: 'apply_cloud_mask' has to set to 'True' AND 'SLC_masking_algo' has to be 'mask_scl_dilation'
95+
for this flag to work.
96+
97+
append: bool = True
9098
if the VI's are appended to the reflectance/radar time series cube OR replace them
9199
92100
S2_scaling: list = [0, 10000, 0, 1.0]

src/eo_processing/config/settings.py

Lines changed: 66 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
S2_MAX_CLOUD_COVER: int = 95
2121
MASKING_ALGO: str = 'mask_scl_dilation'
2222
APPLY_CLOUD_MASK: bool = True
23+
GET_NOBS: bool = False
2324
S2_TILEID_LIST: Optional[List[str]] = None
2425
SKIP_CHECK_S1: bool = False
2526
SKIP_CHECK_S2: bool = False
@@ -68,7 +69,7 @@
6869

6970
# ---------------------------------------------------
7071
# Job options for OpenEO
71-
OPENEO_EXTRACT_JOB_OPTIONS: Dict[str, str] = {
72+
OPENEO_EXTRACT_JOB_OPTIONS: Dict[str, Union[str, int, float, List]] = {
7273
"driver-memory": "4G",
7374
"driver-memoryOverhead": "8G",
7475
"driver-cores": 2,
@@ -80,7 +81,7 @@
8081
"stac-version":"1.1"
8182
}
8283

83-
OPENEO_EXTRACT_CREO_JOB_OPTIONS: Dict[str, str] = {
84+
OPENEO_EXTRACT_CREO_JOB_OPTIONS: Dict[str, Union[str, int, float, List]] = {
8485
"driver-memory": "4G",
8586
"driver-memoryOverhead": "2G",
8687
"driver-cores": 1,
@@ -91,7 +92,7 @@
9192
"max-executors": 200
9293
}
9394

94-
OPENEO_EXTRACT_CDSE_JOB_OPTIONS: Dict[str, str] = {
95+
OPENEO_EXTRACT_CDSE_JOB_OPTIONS: Dict[str, Union[str, int, float, List]] = {
9596
"driver-memory": "8G",
9697
"driver-memoryOverhead": "5G",
9798
"driver-cores": 1,
@@ -103,7 +104,7 @@
103104
"logging-threshold": "info"
104105
}
105106

106-
OPENEO_INFERENCE_CDSE_JOB_OPTIONS: Dict[str, str] = {
107+
OPENEO_INFERENCE_CDSE_JOB_OPTIONS: Dict[str, Union[str, int, float, List]] = {
107108
"driver-memory": "1000m",
108109
"driver-memoryOverhead": "1000m",
109110
"driver-cores": 1,
@@ -118,7 +119,7 @@
118119
]
119120
}
120121

121-
OPENEO_POINTEXTRACTION_CDSE_JOB_OPTIONS: Dict[str, str] = {
122+
OPENEO_POINTEXTRACTION_CDSE_JOB_OPTIONS: Dict[str, Union[str, int, float, List]] = {
122123
"driver-memory": "4G",
123124
"driver-memoryOverhead": "2G",
124125
"driver-cores": 1,
@@ -130,7 +131,7 @@
130131
"logging-threshold": "info"
131132
}
132133

133-
OPENEO_CUBEEXTRACTION_CDSE_JOB_OPTIONS: Dict[str, str] = {
134+
OPENEO_CUBEEXTRACTION_CDSE_JOB_OPTIONS: Dict[str, Union[str, int, float, List]] = {
134135
"driver-memory": "4G",
135136
"driver-memoryOverhead": "4G",
136137
"driver-cores": 1,
@@ -168,7 +169,7 @@
168169
'S1_collection': "SENTINEL1_GRD"
169170
}
170171

171-
def _get_default_job_options() -> Dict[str, str]:
172+
def _get_default_job_options() -> Dict[str, Union[str, int, float, List]]:
172173
"""
173174
Retrieves the default job options for OpenEO extract operations.
174175
@@ -181,7 +182,7 @@ def _get_default_job_options() -> Dict[str, str]:
181182
"""
182183
return OPENEO_EXTRACT_JOB_OPTIONS.copy()
183184

184-
def get_job_options(provider: str = None, task: str = 'raw_extraction') -> Dict[str, str]:
185+
def get_job_options(provider: str = None, task: str = 'raw_extraction') -> Dict[str, Union[str, int, float, List]]:
185186
"""
186187
Retrieve job options based on the specified provider and task.
187188
@@ -288,6 +289,7 @@ def get_standard_processing_options(provider: str, task: str = 'raw_extraction')
288289
"skip_check_S1": SKIP_CHECK_S1,
289290
"skip_check_S2": SKIP_CHECK_S2,
290291
"apply_cloud_mask": APPLY_CLOUD_MASK,
292+
"get_NOBSperc": GET_NOBS,
291293
"openeo_chunk_size": CHUNK_SIZE,
292294
}
293295
elif (task == 'feature_generation') or (task == 'vi_generation'):
@@ -307,6 +309,7 @@ def get_standard_processing_options(provider: str, task: str = 'raw_extraction')
307309
"skip_check_S1": SKIP_CHECK_S1,
308310
"skip_check_S2": SKIP_CHECK_S2,
309311
"apply_cloud_mask": APPLY_CLOUD_MASK,
312+
"get_NOBSperc": GET_NOBS,
310313
"optical_vi_list" : VI_LIST,
311314
"radar_vi_list" : RADAR_LIST,
312315
"S2_scaling" : S2_SCALING,
@@ -325,62 +328,57 @@ def get_advanced_options(provider: str, s1_orbitdirection: Optional[str] = S1_OR
325328
S1_temporal_reducer: str = S1_TEMPORAL_REDUCER, slc_masking: Optional[str] = MASKING_ALGO,
326329
S2_bands: List[str] = S2_BANDS, s2_tileid_list: Optional[List[str]] = S2_TILEID_LIST,
327330
skip_check_S1: bool = SKIP_CHECK_S1, skip_check_S2: bool = SKIP_CHECK_S2,
328-
apply_cloud_mask: bool = APPLY_CLOUD_MASK, S2_max_cloud_cover: int = S2_MAX_CLOUD_COVER,
331+
apply_cloud_mask: bool = APPLY_CLOUD_MASK, get_NOBSperc: bool = GET_NOBS,
332+
S2_max_cloud_cover: int = S2_MAX_CLOUD_COVER,
329333
optical_vi_list: List[str] = VI_LIST, radar_vi_list: List[str] = RADAR_LIST,
330334
S2_scaling: List[int | float] = S2_SCALING, S1_db_rescale: bool = True,
331335
append: bool = True) -> Dict[str, Union[str, bool, int | float, List[str], List[int | float]]]:
332336
"""
333-
Generate a dictionary of advanced options for processing satellite imagery.
334-
335-
This function validates input parameters and prepares a dictionary of options
336-
required for processing satellite imagery data. Validation is applied for
337-
various parameters to ensure that the provided values conform to the expected
338-
formats or constraints. The function checks for valid values for key options
339-
like satellite orbit direction, target coordinate reference system (CRS),
340-
resolution, and others before constructing the dictionary.
341-
342-
:param provider: The provider of satellite data.
343-
:param s1_orbitdirection: Direction of Sentinel-1 pass, can be 'ASCENDING',
344-
'DESCENDING' or None.
345-
:param target_crs: An integer representing the target coordinate reference
346-
system (CRS).
347-
:param resolution: The spatial resolution of the output in integer or float
348-
format.
349-
:param ts_interpolation: Boolean indicating whether to apply linear time-series
350-
interpolation.
351-
:param ts_interval: Interval for time series aggregation (temporal binning). Accepted values
352-
are 'day', 'week', 'dekad', 'month', 'season', 'year', or
353-
None.
354-
:param S2_temporal_reducer: Method for reducing temporal data for Sentinel-2.
355-
Valid options include 'median', 'mean', 'max',
356-
'min', 'first', 'last', 'product', 'sd', 'sum',
357-
or 'variance'.
358-
:param S1_temporal_reducer: Method for reducing temporal data for Sentinel-1.
359-
Valid options include 'median', 'mean', 'max',
360-
'min', 'first', 'last', 'product', 'sd', 'sum',
361-
or 'variance'.
362-
:param slc_masking: Masking approach to be applied. Valid options are
363-
'mask_scl_dilation', 'satio', or None.
364-
:param S2_bands: List of selected Sentinel-2 reflectance bands.
365-
:param s2_tileid_list: Optional list of Sentinel-2 tiles for processing. Can
366-
be None if tiles are not specified.
367-
:param skip_check_S1: Boolean indicating whether to skip validation checks
368-
for Sentinel-1 data.
369-
:param skip_check_S2: Boolean indicating whether to skip validation checks
370-
for Sentinel-2 data.
371-
:param apply_cloud_mask: Boolean indicating whether to apply cloud masking or to add it as own band.
372-
:param S2_max_cloud_cover: Maximum allowable cloud cover percentage for
373-
Sentinel-2 data. Acceptable values are integers
374-
between 0 and 100.
375-
:param optical_vi_list: List of selected vegetation indices for optical data.
376-
:param radar_vi_list: List of selected vegetation indices for radar data.
377-
:param S2_scaling: List of scaling factors to be applied to Sentinel-2 data.
378-
:param S1_db_rescale: Boolean controlling whether Sentinel-1 data must be
379-
rescaled in decibels.
380-
:param append: Boolean indicating whether to append the VI's to the reflectance cube or to replace them.
381-
:param openeo_chunk_size: Chunk size for OpenEO processing.
382-
383-
:return: A dictionary containing all validated options as key-value pairs.
337+
Constructs a dictionary of advanced processing options for remote sensing data.
338+
339+
This function validates input parameters and generates an options dictionary
340+
tailored for processing remote sensing datasets, including Sentinel-1 and
341+
Sentinel-2 data. It handles settings like temporal reducers, orbit directions,
342+
spatial resolutions, and others. Input validation ensures proper parameter
343+
types and value ranges.
344+
345+
:param provider: (str) The data provider identifier.
346+
:param s1_orbitdirection: (Optional[str]) The orbit direction for Sentinel-1
347+
data, can be 'ASCENDING', 'DESCENDING', or None.
348+
:param target_crs: (Optional[int]) Target coordinate reference system code.
349+
:param resolution: (int or float) Spatial resolution for the output data.
350+
:param ts_interpolation: (bool) Whether to enable time-series interpolation.
351+
:param ts_interval: (Optional[str]) Time-series interval, allowed values:
352+
'day', 'week', 'dekad', 'month', 'season', 'year', or None.
353+
:param S2_temporal_reducer: (str) Temporal reducer applied to Sentinel-2 data,
354+
permitted values: 'median', 'mean', 'max', 'min', 'first', 'last',
355+
'product', 'sd', 'sum', 'variance'.
356+
:param openeo_chunk_size: (int) Chunk size for OpenEO processing.
357+
:param S1_temporal_reducer: (str) Temporal reducer applied to Sentinel-1 data,
358+
permitted values: 'median', 'mean', 'max', 'min', 'first', 'last',
359+
'product', 'sd', 'sum', 'variance'.
360+
:param slc_masking: (Optional[str]) Masking algorithm for SLC, either
361+
'mask_scl_dilation', 'satio', or None.
362+
:param S2_bands: (List[str]) List of Sentinel-2 reflectance bands to include.
363+
:param s2_tileid_list: (Optional[List[str]]) List of Sentinel-2 tile IDs to
364+
process, or None.
365+
:param skip_check_S1: (bool) Whether to skip checking for Sentinel-1 data.
366+
:param skip_check_S2: (bool) Whether to skip checking for Sentinel-2 data.
367+
:param apply_cloud_mask: (bool) Whether to apply a cloud mask to Sentinel-2
368+
data.
369+
:param get_NOBSperc: (bool) Whether to calculate the percentage of observations.
370+
:param S2_max_cloud_cover: (int) Maximum cloud cover percentage allowed
371+
for Sentinel-2 scenes (0-100 inclusive).
372+
:param optical_vi_list: (List[str]) List of vegetation indices to calculate
373+
for optical data.
374+
:param radar_vi_list: (List[str]) List of indices to calculate for radar data.
375+
:param S2_scaling: (List[int or float]) List of scaling factors for Sentinel-2 data.
376+
:param S1_db_rescale: (bool) Whether to rescale Sentinel-1 data to decibels.
377+
:param append: (bool) Whether to append new processing options to an existing
378+
configuration dictionary.
379+
380+
:return: (Dict[str, Union[str, bool, int or float, List[str], List[int or float]]])
381+
A dictionary containing all validated and formatted processing options.
384382
"""
385383

386384
if s1_orbitdirection not in ['ASCENDING', 'DESCENDING', None]:
@@ -448,6 +446,13 @@ def get_advanced_options(provider: str, s1_orbitdirection: Optional[str] = S1_OR
448446
if type(apply_cloud_mask) != bool:
449447
raise ValueError(f'parameter for apply_cloud_mask must be an boolean.')
450448

449+
if type(get_NOBSperc) != bool:
450+
raise ValueError(f'parameter for get_NOBSperc must be an boolean.')
451+
452+
if (get_NOBSperc == True) and ((apply_cloud_mask == False) or (slc_masking != 'mask_scl_dilation')):
453+
raise ValueError(f'parameter for get_NOBSperc can be only set to True if apply_cloud_mask is set to True '
454+
f'AND slc_masking is set to mask_scl_dilation.')
455+
451456
proc_opt = {
452457
"provider": provider,
453458
"s1_orbitdirection": s1_orbitdirection,
@@ -464,6 +469,7 @@ def get_advanced_options(provider: str, s1_orbitdirection: Optional[str] = S1_OR
464469
"skip_check_S1": skip_check_S1,
465470
"skip_check_S2": skip_check_S2,
466471
"apply_cloud_mask": apply_cloud_mask,
472+
"get_NOBSperc": get_NOBSperc,
467473
"optical_vi_list": optical_vi_list,
468474
"radar_vi_list": radar_vi_list,
469475
"S2_scaling": S2_scaling,

src/eo_processing/openeo/preprocessing.py

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22
import openeo
3+
from openeo import DataCube
34
from openeo.processes import array_create, if_, is_nodata, power, array_contains
45
from openeo.rest.datacube import DataCube
56

@@ -8,7 +9,7 @@
89
catalogue_check_CDSE_S1, catalogue_check_CDSE_S2)
910
from eo_processing.config.settings import S2_BANDS, PLANET_BANDS, CHUNK_SIZE
1011

11-
from typing import Optional, Dict, Union, List, TYPE_CHECKING
12+
from typing import Optional, Dict, Union, List, TYPE_CHECKING, Tuple
1213
if TYPE_CHECKING:
1314
from eo_processing.config.data_formats import openEO_bbox_format
1415

@@ -29,11 +30,15 @@ def ts_datacube_extraction(
2930
resolution, ts_interval, time_interpolation, SLC_masking_algo, s1_orbitdirection, S2_bands)
3031
:return: DataCube
3132
"""
32-
# get the Sentinel-2 datacube as starting point
33-
bands = extract_S2_datacube(connection, bbox, start, end,
34-
S2_collection=S2_collection,
35-
**processing_options)
33+
# Note: for cubes with a time dimension, no nobs_perc band can be attached
34+
get_NOBSperc: bool = processing_options.get("get_NOBSperc", False)
35+
if get_NOBSperc:
36+
processing_options["get_NOBSperc"] = False
3637

38+
# get the Sentinel-2 datacube as starting point
39+
bands: DataCube = extract_S2_datacube(connection, bbox, start, end,
40+
S2_collection=S2_collection,
41+
**processing_options)
3742
# add the Sentinel-1 data
3843
if S1_collection is not None:
3944
bands = bands.merge_cubes(extract_S1_datacube(connection, bbox, start, end,
@@ -170,7 +175,8 @@ def extract_S1_datacube(
170175
def extract_S2_datacube(
171176
connection: openeo.Connection, bbox: Optional[openEO_bbox_format], start: str, end: str,
172177
S2_collection: str='SENTINEL2_L2A',
173-
**processing_options: Dict[str, Union[str, bool, int | float, List[str], List[int | float]]]) -> DataCube:
178+
**processing_options: Dict[str, Union[str, bool, int | float, List[str], List[int | float]]]) \
179+
-> tuple[DataCube, DataCube | None] | DataCube:
174180
""" extract the Sentinel-2 data for requested time period and preprocess the data
175181
176182
:param connection: active openEO connection object
@@ -179,7 +185,7 @@ def extract_S2_datacube(
179185
:param end: str, End date for requested input data (yyyy-mm-dd)
180186
:param S2_collection: (str, optional): Collection name for S2 data
181187
:param processing_options: (dict, optional), processing options for preprocessing routine (provider, target_crs,
182-
resolution, ts_interval, time_interpolation, SLC_masking_algo, S2_bands)
188+
resolution, ts_interval, time_interpolation, SLC_masking_algo, S2_bands, apply_cloud_mask, getNOBSperc)
183189
:return: DataCube
184190
"""
185191
# evaluate additional processing_options
@@ -203,6 +209,7 @@ def extract_S2_datacube(
203209
max_cloud_max: int = processing_options.get("S2_max_cloud_cover", 95)
204210
chunk_size: int = processing_options.get("openeo_chunk_size", CHUNK_SIZE)
205211
s2_tileid_list: Optional[List[str]] = processing_options.get("s2_tileid_list", None)
212+
get_NOBSperc: bool = processing_options.get("get_NOBSperc", False)
206213

207214
# check if the masking parameter is valid
208215
if masking not in ['satio', 'mask_scl_dilation', None]:
@@ -240,6 +247,9 @@ def extract_S2_datacube(
240247
else:
241248
bands = bands.resample_spatial(resolution=target_res)
242249

250+
# for the NOBSperc to work we need to init the cube as None
251+
nobs_perc_band = None
252+
243253
# apply cloud masking
244254
if masking == 'mask_scl_dilation':
245255
# we have to load the SCL mask as an extra cube to get it correctly working
@@ -271,8 +281,22 @@ def extract_S2_datacube(
271281
).rename_labels("bands", ["S2-CLOUD-MASK"])
272282

273283
if apply_mask:
284+
if get_NOBSperc:
285+
# get temporal count of valid pixels before masking
286+
tobs = sub_collection.count_time().rename_labels("bands", ["tobs"]).convert_data_type('float32')
287+
288+
# apply the cloud mask to the reference data
274289
bands = bands.mask(scl_dilated_mask) # here I do trust the automatic resampling of the mask
290+
291+
if get_NOBSperc:
292+
# get temporal count after applying cloud mask = nobs
293+
nobs = bands.count_time().rename_labels("bands", ["nobs"]).convert_data_type('float32')
294+
# make sure TOBS is in right projection/resolution
295+
tobs = tobs.resample_cube_spatial(target=nobs, method="near")
296+
# calculate nobs_perc
297+
nobs_perc_band = nobs.divide(tobs).multiply(100.).rename_labels("bands", ["nobs_perc"])
275298
else:
299+
scl_dilated_mask = scl_dilated_mask.resample_cube_spatial(target=bands, method="near")
276300
bands = bands.merge_cubes(scl_dilated_mask)
277301
elif masking == 'satio':
278302
# Apply satio-based mask
@@ -287,6 +311,7 @@ def extract_S2_datacube(
287311
if apply_mask:
288312
bands = bands.mask(mask) # masks are automatically resampled/warped
289313
else:
314+
mask = mask.resample_cube_spatial(target=bands, method="near")
290315
bands = bands.merge_cubes(mask)
291316

292317
# time aggregation if wished
@@ -299,8 +324,12 @@ def extract_S2_datacube(
299324
process="array_interpolate_linear")
300325
# forcing 16bit --> UInt16
301326
bands = bands.linear_scale_range(0, 65534, 0, 65534)
302-
303-
return bands
327+
328+
# the return is tricky since we have to be backwards compatible and still get nobs_perc working
329+
if get_NOBSperc:
330+
return bands, nobs_perc_band
331+
else:
332+
return bands
304333

305334
def extract_planet_datacube(
306335
connection: openeo.Connection, bbox: Optional[openEO_bbox_format], start: str, end: str,

0 commit comments

Comments
 (0)