1- from typing import Mapping , Sequence
1+ import operator
2+ from typing import Tuple
23import openeo
34import numpy as np
45import scipy
6+ from collections .abc import Callable , Mapping , Iterable
57
68
79def connect ():
810 return openeo .connect ("https://openeo.dataspace.copernicus.eu/" )
911
1012
11- # TODO move somewhere else, not specific to sentinel-2
13+
1214class TestArea :
1315 # aoi_wkt = "POINT (-15.432283 15.402828)"
1416 directions = ["west" , "south" , "east" , "north" ]
@@ -19,14 +21,14 @@ def __init__(
1921 self ,
2022 * ,
2123 bbox : Mapping [str , float ] = bbox ,
22- s2_bands : Sequence [str ] = ["B02" , "B03" , "B04" , "B8A" , "SCL" ],
23- s3_bands : Sequence [str ] = [
24+ s2_bands : Iterable [str ] = ["B02" , "B03" , "B04" , "B8A" , "SCL" ],
25+ s3_bands : Iterable [str ] = [
2426 "Syn_Oa04_reflectance" ,
2527 "Syn_Oa06_reflectance" ,
2628 "Syn_Oa08_reflectance" ,
2729 "Syn_Oa17_reflectance" ,
2830 ],
29- temporal_extent : Sequence [str ] = ["2022-06-01" , "2022-06-30" ],
31+ temporal_extent : Iterable [str ] = ["2022-06-01" , "2022-06-30" ],
3032 ) -> None :
3133 self .bbox = bbox
3234 self .s2_bands = s2_bands
@@ -50,39 +52,6 @@ def get_s3_cube(self, connection):
5052 )
5153
5254
53- def extract_cloud_mask (cube : openeo .DataCube ):
54- scl = cube .filter_bands (["SCL" ])
55- # 0: No data
56- # 3: Cloud shadow
57- # 8-10: Clouds
58- # 11: Snow or ice
59-
60- mask = (scl == 0 ) | (scl == 3 ) | (scl > 7 )
61- return mask
62-
63-
64- def calculate_large_grid_cloud_mask (cube : openeo .DataCube , tolerance_percentage : float = 0.05 , grid_side_length : int = 300 ):
65- cloud_mask = extract_cloud_mask (cube )
66- # FIXME check also if there is negative or zero data, otherwise results will differ
67-
68- # TODO this could better be resample_cube_spatial, because we are matching to a sentinel-3 cube
69- cloud_mask = cloud_mask * 1.0 # convert to float
70- cloud_mask_resampled = cloud_mask .resample_spatial (grid_side_length , method = "average" ) # resample to sentinel-3 size
71-
72- # TODO extract UDF to file
73- # UDF to apply an element-wise less than operation. Normal "<" does not properly work on openEO datacubes
74- udf = openeo .UDF (f"""
75- import numpy as np
76- import xarray as xr
77- def apply_datacube(cube: XarrayDataCube, context: dict) -> XarrayDataCube:
78- array = cube.get_array()
79- array = array < { tolerance_percentage }
80- #return XarrayDataCube(xr.DataArray(array, dims=["t", "x", "y", "bands"]))
81- return XarrayDataCube(xr.DataArray(array, dims=["bands", "x", "y"]))
82- """ )
83- return cloud_mask_resampled .apply (process = udf )
84-
85-
8655def distance_to_clouds (
8756 cube : openeo .DataCube , tolerance_percentage = 0.05 , ratio = 30 , max_distance = 255
8857):
@@ -116,3 +85,66 @@ def _distance_to_clouds_udf(
11685 overlap = [],
11786 )
11887 return dtc
88+
89+
90+ def extract_mask (
91+ cube : openeo .DataCube ,
92+ mask_values : Mapping [str , Iterable [int ]],
93+ * ,
94+ operations : Mapping [Tuple [str , int ], Callable ],
95+ ) -> openeo .DataCube :
96+ """
97+ Generic method to extract a mask from a data cube.
98+ Generate a mask that has a value of ``True`` whereever the band specified
99+ as a key in ``mask_values`` is equal to one of the values speicified
100+ as a value. Operations other than equality comparison can be specified
101+ via ``operations``.
102+
103+ import operator
104+ extract_mask(
105+ cube,
106+ mask_values = {
107+ "SCL": (0, 3, 7),
108+ },
109+ operations = {
110+ # use ``>`` instead of ``==``
111+ # to compare band ``"SCL"`` to value ``7``
112+ ("SCL", 7): operator.gt,
113+ }
114+ )
115+
116+ # scl = cube.band("SCL")
117+ # mask = (scl == 0) | (scl == 3) | (scl > 7)
118+
119+
120+ :param cube: The data cube containing at least the band(s) used for masking
121+ :param mask_values: Mapping of band names to the values that should be masked.
122+ :param operations: Used to specify a comparison operation different to ``==``
123+ when comparing bands and values. Operations are applied as ``op(band, value)``
124+
125+ """
126+
127+ assert (len (mask_values ) > 0 ), "'mask_values' cannot be empty."
128+ def reduce_single_band (band_name , values_to_mask , mask = None ):
129+ band = cube .band (band_name )
130+ vm_iter = iter (values_to_mask )
131+
132+ if mask is None :
133+ vm_first = next (vm_iter )
134+ mask = band == vm_first
135+
136+ for vm in vm_iter :
137+ op = operations .get ((band_name , vm ), operator .eq )
138+ mask |= op (band , vm )
139+
140+ return mask
141+
142+ first_band_name , * bands = mask_values .keys ()
143+ first_vm , * values_to_mask = mask_values .values ()
144+
145+ mask = reduce_single_band (first_band_name , first_vm , mask = None )
146+
147+ for band_name , values_to_mask in zip (bands , values_to_mask ):
148+ mask |= reduce_single_band (band_name , values_to_mask , mask )
149+
150+ return mask
0 commit comments