44import org .esa .s3tbx .processor .rad2refl .Rad2ReflConstants ;
55import org .esa .snap .core .datamodel .Band ;
66import org .esa .snap .core .datamodel .FlagCoding ;
7+ import org .esa .snap .core .datamodel .GeoCoding ;
78import org .esa .snap .core .datamodel .GeoPos ;
89import org .esa .snap .core .datamodel .Product ;
910import org .esa .snap .core .datamodel .ProductData ;
2223import org .esa .snap .idepix .core .util .IdepixIO ;
2324import org .esa .snap .idepix .core .util .IdepixUtils ;
2425import org .esa .snap .idepix .core .util .SchillerNeuralNetWrapper ;
26+ import org .esa .snap .watermask .operator .WatermaskClassifier ;
2527import org .locationtech .jts .geom .Coordinate ;
2628import org .locationtech .jts .geom .GeometryFactory ;
2729import org .locationtech .jts .geom .Polygon ;
3436import java .util .Calendar ;
3537import java .util .Map ;
3638
39+ import static org .esa .snap .idepix .core .IdepixConstants .*;
40+
3741/**
3842 * OLCI pixel classification operator.
3943 * Processes both land and water, so we get rid of separate land/water merge operator.
6266public class IdepixOlciClassificationOp extends Operator {
6367
6468 @ Parameter (defaultValue = "false" ,
65- label = "Write NN value to the target product." ,
66- description = "If applied, write Schiller NN value to the target product " )
69+ label = " Write NN value to the target product." ,
70+ description = " If applied, write Schiller NN value to the target product " )
6771 private boolean outputSchillerNNValue ;
6872
6973 @ Parameter (description = "Alternative NN file. " +
7074 "If set, it MUST follow format and input/output as used in default '11x10x4x3x2_207.9.net. " ,
71- label = "Alternative NN file" )
75+ label = " Alternative NN file" )
7276 private File alternativeNNFile ;
7377
7478 @ Parameter (defaultValue = "false" ,
@@ -77,6 +81,13 @@ public class IdepixOlciClassificationOp extends Operator {
7781 )
7882 private boolean ignoreSeaIceClimatology ;
7983
84+ @ Parameter (defaultValue = "false" ,
85+ label = " Use SRTM Land/Water mask" ,
86+ description = "If selected, SRTM Land/Water mask is used instead of L1b land flag. " +
87+ "Slower, but in general more precise." )
88+ private boolean useSrtmLandWaterMask ;
89+
90+
8091 @ SourceProduct (alias = "l1b" , description = "The L1b product." )
8192 private Product l1bProduct ;
8293
@@ -91,14 +102,10 @@ public class IdepixOlciClassificationOp extends Operator {
91102 @ SourceProduct (alias = "o2Corr" , optional = true )
92103 private Product o2CorrProduct ;
93104
94- @ SourceProduct (alias = "rBRR" , optional = true )
95- private Product rBRRProduct ;
96-
97105 @ TargetProduct (description = "The target product." )
98106 Product targetProduct ;
99107
100108 private Band [] olciReflBands ;
101- private Band [] rBRRBands ;
102109
103110 private Band surface13Band ;
104111 private Band trans13Band ;
@@ -119,17 +126,26 @@ public class IdepixOlciClassificationOp extends Operator {
119126 private GeometryFactory gf ;
120127 private Polygon arcticPolygon ;
121128 private Polygon antarcticaPolygon ;
129+ private WatermaskClassifier watermaskClassifier ;
122130
123131 private LakeSeaIceClassification lakeSeaIceClassification ;
124132
125133
126134 @ Override
127135 public void initialize () throws OperatorException {
128136 setBands ();
129- setrBRRBands ();
130137 nnInterpreter = IdepixOlciCloudNNInterpreter .create ();
131138 readSchillerNeuralNets ();
132139 createTargetProduct ();
140+ if (useSrtmLandWaterMask ) {
141+ try {
142+ watermaskClassifier = new WatermaskClassifier (LAND_WATER_MASK_RESOLUTION ,
143+ OVERSAMPLING_FACTOR_X ,
144+ OVERSAMPLING_FACTOR_Y );
145+ } catch (IOException e ) {
146+ throw new OperatorException ("Could not initialise SRTM land-water mask" , e );
147+ }
148+ }
133149
134150 initLakeSeaIceClassification ();
135151
@@ -177,14 +193,6 @@ private void setBands() {
177193 }
178194 }
179195
180- private void setrBRRBands () {
181- rBRRBands = new Band [5 ];
182- String [] rBRRBandname = new String []{"01" , "04" , "06" , "08" , "17" };
183- for (int i = 0 ; i < rBRRBandname .length ; i ++) {
184- rBRRBands [i ] = rBRRProduct .getBand ("rBRR_" + rBRRBandname [i ]);
185- }
186- }
187-
188196 private void createTargetProduct () throws OperatorException {
189197 int sceneWidth = l1bProduct .getSceneRasterWidth ();
190198 int sceneHeight = l1bProduct .getSceneRasterHeight ();
@@ -226,111 +234,61 @@ public void computeTileStack(Map<Band, Tile> targetTiles, Rectangle rectangle, P
226234 olciReflectanceTiles [i ] = getSourceTile (olciReflBands [i ], rectangle );
227235 }
228236
229- Tile [] rBRRTiles = new Tile [rBRRBands .length ];
230- for (int i = 0 ; i < rBRRBands .length ; i ++) {
231- rBRRTiles [i ] = getSourceTile (rBRRBands [i ], rectangle );
232- }
233-
234-
235237 final Band cloudFlagTargetBand = targetProduct .getBand (IdepixConstants .CLASSIF_BAND_NAME );
236238 final Tile cloudFlagTargetTile = targetTiles .get (cloudFlagTargetBand );
237239
240+
238241 Tile nnTargetTile = null ;
239242 if (outputSchillerNNValue ) {
240243 nnTargetTile = targetTiles .get (targetProduct .getBand (IdepixConstants .NN_OUTPUT_BAND_NAME ));
241244 }
242245 try {
246+ GeoCoding geoCoding = l1bProduct .getSceneGeoCoding ();
243247 for (int y = rectangle .y ; y < rectangle .y + rectangle .height ; y ++) {
244248 checkForCancellation ();
245249 for (int x = rectangle .x ; x < rectangle .x + rectangle .width ; x ++) {
246-
247- final boolean coastalZone = isCoastalZone (olciQualityFlagTile , x , y );
248- cloudFlagTargetTile .setSample (x , y , IdepixConstants .IDEPIX_COASTLINE , coastalZone );
249-
250- final boolean isStaticLand = olciQualityFlagTile .getSampleBit (x , y , IdepixOlciConstants .L1_F_LAND ) &&
251- !olciQualityFlagTile .getSampleBit (x , y , IdepixOlciConstants .L1_F_FRESH_INLAND_WATER );
252- cloudFlagTargetTile .setSample (x , y , IdepixConstants .IDEPIX_LAND , isStaticLand );
253-
254- boolean isLAND ;
255- if (coastalZone ) {
256- isLAND = isOLCILandUpdatedInCoastalZone (x , y , isStaticLand , olciQualityFlagTile , rBRRTiles );
257- } else {
258- isLAND = cloudFlagTargetTile .getSampleBit (x , y , IdepixConstants .IDEPIX_LAND );
250+ int waterFraction = -1 ;
251+ if (useSrtmLandWaterMask ) {
252+ waterFraction = watermaskClassifier .getWaterMaskFraction (geoCoding , x , y );
259253 }
260254
261255 initCloudFlag (olciQualityFlagTile , targetTiles .get (cloudFlagTargetBand ), olciReflectanceTiles , y , x );
262256 final boolean isBright = olciQualityFlagTile .getSampleBit (x , y , IdepixOlciConstants .L1_F_BRIGHT );
263257 cloudFlagTargetTile .setSample (x , y , IdepixConstants .IDEPIX_BRIGHT , isBright );
258+ final boolean isCoastlineFromAppliedMask = classifyCoastline (olciQualityFlagTile , x , y , waterFraction );
259+ cloudFlagTargetTile .setSample (x , y , IdepixConstants .IDEPIX_COASTLINE , isCoastlineFromAppliedMask );
260+
261+ final boolean isLandFromAppliedMask = isOlciLandPixel (x , y , olciQualityFlagTile , waterFraction );
262+ final boolean isInlandWaterFromAppliedMask = isOlciInlandWaterPixel (x , y , olciQualityFlagTile , waterFraction );
263+ //todo: for CGLOPS, coastlines are added to LAND to exclude them from L2 processing
264+ cloudFlagTargetTile .setSample (x , y , IdepixConstants .IDEPIX_LAND , isLandFromAppliedMask ||
265+ isCoastlineFromAppliedMask );
264266
265- if (isLAND ) {
267+ // todo: for cglops, coastlines are treated as LAND
268+ if ((isLandFromAppliedMask && !isInlandWaterFromAppliedMask ) || isCoastlineFromAppliedMask ) {
266269 classifyOverLand (olciReflectanceTiles , cloudFlagTargetTile , nnTargetTile ,
267270 surface13Tile , trans13Tile , x , y );
268271 } else {
269- // test on ambiguous clouds in inland water remains in the classification
270- boolean isProcessableWater = classifyOverWater (olciQualityFlagTile , olciReflectanceTiles ,
271- cloudFlagTargetTile , nnTargetTile , y , x );
272- cloudFlagTargetTile .setSample (x , y , IdepixOlciConstants .IDEPIX_WATER_PROCESSABLE , isProcessableWater );
272+ classifyOverWater (olciQualityFlagTile , olciReflectanceTiles ,
273+ cloudFlagTargetTile , nnTargetTile , x , y , isInlandWaterFromAppliedMask );
273274 }
274-
275275 }
276276 }
277277 } catch (Exception e ) {
278278 throw new OperatorException ("Failed to provide GA cloud screening:\n " + e .getMessage (), e );
279279 }
280280 }
281281
282- private boolean isOLCILandUpdatedInCoastalZone (int x , int y , boolean staticLand , Tile sourceFlagTile , Tile [] rBRRTiles ) {
283- // LAND_updated = (staticLand and not coastline) or (staticLand and coastline and NDVI>0.07 and not quality_flags.bright) or ((coastline and staticLand and quality_flags.bright))
284- // LAND from Water: (staticLand==0 and coastline and not quality_flags.bright) and NDVI>0.07
285- // Bright beach: (rBRR_06 - rBRR_04)/(560-490) > 0 and (rBRR_04 - rBRR_01)/(490-400) > 0 and ( coastline) and rBRR_06 > 0.1
286-
287- Rectangle rectangle = sourceFlagTile .getRectangle ();
288- if (rectangle .contains (x , y ) && !sourceFlagTile .getSampleBit (x , y , IdepixOlciConstants .L1_F_INVALID )) {
289- float rBrr0 = rBRRTiles [0 ].getSampleFloat (x , y );
290- float rBrr1 = rBRRTiles [1 ].getSampleFloat (x , y );
291- float rBrr2 = rBRRTiles [2 ].getSampleFloat (x , y );
292- float rBrr3 = rBRRTiles [3 ].getSampleFloat (x , y );
293- float rBrr4 = rBRRTiles [4 ].getSampleFloat (x , y );
294-
295- float ndvi = (rBrr4 - rBrr3 ) / (rBrr4 + rBrr3 );
296- boolean isBright = sourceFlagTile .getSampleBit (x , y , IdepixOlciConstants .L1_F_BRIGHT );
297-
298- boolean isLandUpdated = (staticLand && ndvi > 0.07 && !isBright ) || (staticLand && isBright );
299- boolean isLandFromWater = (!staticLand && !isBright ) && ndvi > 0.07 ;
300- boolean isBrightBeach = (rBrr2 - rBrr1 ) / (560 - 490. ) > 0 &&
301- (rBrr1 - rBrr0 ) / (490 - 400 ) > 0 &&
302- rBrr2 > 0.1 && ndvi > 0. ;
303-
304- return isLandUpdated || isLandFromWater || isBrightBeach ;
305- } else {
306- return staticLand ;
307- }
282+ private boolean classifyCoastline (Tile olciQualityFlagTile , int x , int y , int waterFraction ) {
283+ return waterFraction < 0 ?
284+ olciQualityFlagTile .getSampleBit (x , y , IdepixOlciConstants .L1_F_COASTLINE ) :
285+ isCoastlinePixel (x , y , waterFraction );
308286 }
309287
310- private boolean isCoastalZone (Tile sourceFlagTile , int x , int y ) {
311- int pixelCount = 0 ;
312- int sizeRectangle = 0 ;
313- Rectangle rectangle = sourceFlagTile .getRectangle ();
314- int buffer = 3 ; //default value 3 for dilation 7x7
315- for (int i = x - buffer ; i <= x + buffer ; i ++) {
316- for (int j = y - buffer ; j <= y + buffer ; j ++) {
317- if (rectangle .contains (i , j )) {
318- sizeRectangle ++;
319- if (sourceFlagTile .getSampleBit (i , j , IdepixOlciConstants .L1_F_LAND ) &&
320- !sourceFlagTile .getSampleBit (i , j , IdepixOlciConstants .L1_F_FRESH_INLAND_WATER )) {
321- pixelCount ++;
322- }
323- }
288+ private void classifyOverWater (Tile olciQualityFlagTile , Tile [] olciReflectanceTiles ,
289+ Tile cloudFlagTargetTile , Tile nnTargetTile , int x , int y , boolean isInlandWater ) {
324290
325- }
326- }
327- return (pixelCount > 0 && pixelCount < sizeRectangle );
328- }
329291
330- private boolean classifyOverWater (Tile olciQualityFlagTile , Tile [] olciReflectanceTiles ,
331- Tile cloudFlagTargetTile , Tile nnTargetTile , int y , int x ) {
332-
333- boolean isProcessableWater = true ;
334292 double nnOutput = getOlciNNOutput (x , y , olciReflectanceTiles );
335293 if (!cloudFlagTargetTile .getSampleBit (x , y , IdepixConstants .IDEPIX_INVALID )) {
336294 cloudFlagTargetTile .setSample (x , y , IdepixConstants .IDEPIX_CLOUD_AMBIGUOUS , false );
@@ -357,29 +315,25 @@ private boolean classifyOverWater(Tile olciQualityFlagTile, Tile[] olciReflectan
357315 cloudFlagTargetTile .setSample (x , y , IdepixConstants .IDEPIX_CLOUD , false );
358316 }
359317
360- final boolean isInlandWater = olciQualityFlagTile .getSampleBit (x , y , IdepixOlciConstants .L1_F_LAND );
361-
362318 if (isInlandWater && cloudAmbiguous ) {
363319 final double NDVI = getNDVI (x , y , olciReflectanceTiles );
364320 if (NDVI > 0.07 ) {
365321 //catches mixed pixels at coast lines.
366322 cloudFlagTargetTile .setSample (x , y , IdepixConstants .IDEPIX_CLOUD_AMBIGUOUS , false );
367323 cloudFlagTargetTile .setSample (x , y , IdepixConstants .IDEPIX_CLOUD , false );
368- isProcessableWater = false ;
324+ // todo: for CGLOPS it is OK, if these mixed pixels are classified as LAND!
325+ cloudFlagTargetTile .setSample (x , y , IDEPIX_LAND , true );
369326
370327 }
371328 if (olciQualityFlagTile .getSampleBit (x , y , IdepixOlciConstants .L1_F_BRIGHT )) {
372329 cloudFlagTargetTile .setSample (x , y , IdepixConstants .IDEPIX_CLOUD_AMBIGUOUS , true );
373330 cloudFlagTargetTile .setSample (x , y , IdepixConstants .IDEPIX_CLOUD , true );
374331 }
375332 }
376-
377-
378333 }
379334 if (outputSchillerNNValue ) {
380335 nnTargetTile .setSample (x , y , nnOutput );
381336 }
382- return isProcessableWater ;
383337 }
384338
385339 private void classifyOverLand (Tile [] olciReflectanceTiles ,
@@ -405,6 +359,8 @@ private void classifyOverLand(Tile[] olciReflectanceTiles,
405359
406360 boolean isSnowIce = nnInterpreter .isSnowIce (nnOutput );
407361
362+ // cloud over snow from harmonisation approach:
363+ // pixel_classif_flags.IDEPIX_LAND && ((Oa21_reflectance > 0.5 && surface_13 - trans_13 < 0.01) || Oa21_reflectance > 0.76)
408364 double surface13 ;
409365 double trans13 ;
410366 if (surface13Tile != null && trans13Tile != null ) {
@@ -444,6 +400,50 @@ private void classifyOverLand(Tile[] olciReflectanceTiles,
444400 }
445401 }
446402
403+ private boolean isOlciLandPixel (int x , int y , Tile olciL1bFlagTile , int waterFraction ) {
404+ if (waterFraction < 0 ) {
405+ return olciL1bFlagTile .getSampleBit (x , y , IdepixOlciConstants .L1_F_LAND );
406+ } else {
407+ // the water mask ends at 59 Degree south, stop earlier to avoid artefacts
408+ if (IdepixUtils .getGeoPos (l1bProduct .getSceneGeoCoding (), x , y ).lat > -58f ) {
409+ // values bigger than 100 indicate no data
410+ if (waterFraction <= 100 ) {
411+ // todo: this does not work if we have a PixelGeocoding. In that case, waterFraction
412+ // is always 0 or 100!! (TS, OD, 20140502)
413+ return waterFraction == 0 ;
414+ } else {
415+ return olciL1bFlagTile .getSampleBit (x , y , IdepixOlciConstants .L1_F_LAND );
416+ }
417+ } else {
418+ return olciL1bFlagTile .getSampleBit (x , y , IdepixOlciConstants .L1_F_LAND );
419+ }
420+ }
421+ }
422+
423+ private boolean isOlciInlandWaterPixel (int x , int y , Tile olciL1bFlagTile , int waterFraction ) {
424+ if (waterFraction < 0 ) {
425+ // SRTM has not been used! Rely on OLCI flags!
426+ return olciL1bFlagTile .getSampleBit (x , y , IdepixOlciConstants .L1_F_LAND ) &&
427+ olciL1bFlagTile .getSampleBit (x , y , IdepixOlciConstants .L1_F_FRESH_INLAND_WATER );
428+ } else {
429+ // SRTM water mask is used.
430+ // the water mask ends at 59 Degree south, stop earlier to avoid artefacts
431+ if (IdepixUtils .getGeoPos (l1bProduct .getSceneGeoCoding (), x , y ).lat > -58f ) {
432+ // values bigger than 100 indicate no data
433+ if (waterFraction <= 100 ) {
434+ // todo: this does not work if we have a PixelGeocoding. In that case, waterFraction
435+ // is always 0 or 100!! (TS, OD, 20140502)
436+ return waterFraction > 0 && olciL1bFlagTile .getSampleBit (x , y , IdepixOlciConstants .L1_F_LAND );
437+ } else {
438+ return olciL1bFlagTile .getSampleBit (x , y , IdepixOlciConstants .L1_F_LAND ) &&
439+ olciL1bFlagTile .getSampleBit (x , y , IdepixOlciConstants .L1_F_FRESH_INLAND_WATER );
440+ }
441+ } else {
442+ return olciL1bFlagTile .getSampleBit (x , y , IdepixOlciConstants .L1_F_LAND ) &&
443+ olciL1bFlagTile .getSampleBit (x , y , IdepixOlciConstants .L1_F_FRESH_INLAND_WATER );
444+ }
445+ }
446+ }
447447
448448 private double getOlciNNOutput (int x , int y , Tile [] rhoToaTiles ) {
449449 SchillerNeuralNetWrapper nnWrapper ;
@@ -502,6 +502,9 @@ private void initCloudFlag(Tile olciL1bFlagTile, Tile targetTile, Tile[] olciRef
502502 targetTile .setSample (x , y , IdepixConstants .IDEPIX_SNOW_ICE , false );
503503 targetTile .setSample (x , y , IdepixConstants .IDEPIX_CLOUD_BUFFER , false );
504504 targetTile .setSample (x , y , IdepixConstants .IDEPIX_CLOUD_SHADOW , false );
505+ targetTile .setSample (x , y , IdepixConstants .IDEPIX_COASTLINE , false );
506+ targetTile .setSample (x , y , IdepixConstants .IDEPIX_LAND , false );
507+ targetTile .setSample (x , y , IdepixConstants .IDEPIX_BRIGHT , false );
505508 }
506509
507510 /**
0 commit comments