From 30ce78c9c5f5b9cb1c0cd3dfd0e4df5e2e4baaea Mon Sep 17 00:00:00 2001 From: Yannick TANGUY Date: Mon, 15 Jun 2026 09:12:47 +0000 Subject: [PATCH 1/2] Correct NDWI threshold + deactivate Pekel filtering when NDWI threshold is used + invert order of regularization/cleaning steps --- slurp/masks/watermask.py | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/slurp/masks/watermask.py b/slurp/masks/watermask.py index e58923f..1b380ac 100644 --- a/slurp/masks/watermask.py +++ b/slurp/masks/watermask.py @@ -527,7 +527,6 @@ def post_process( logger.warning( "\nWARNING: hand_filter and hand_strict are incompatible." ) - # ---- Filter for final classification ---- if not no_pekel_filter: # mask_pekel_pp : filter areas with Pekel below that threshold @@ -535,35 +534,36 @@ def post_process( im_classif = mask_filter(im_predict, mask_pekel_pp) else: im_classif = im_predict.copy() + logger.debug("Pekel filter is deactivated") # ---- Morphological operations ---- - if binary_closing: + if remove_small_objects: im_classif[:, :] = apply_morpho( - im_classif[:, :].astype(bool), "binary_closing", binary_closing + im_classif[:, :].astype(bool), + "remove_small_objects", + remove_small_objects, ).astype(np.uint8) - if binary_opening: + if remove_small_holes: im_classif[:, :] = apply_morpho( - im_classif[:, :].astype(bool), "binary_opening", binary_opening + im_classif[:, :].astype(bool), + "remove_small_holes", + remove_small_holes, ).astype(np.uint8) - if area_closing: + if binary_closing: im_classif[:, :] = apply_morpho( - im_classif[:, :], "area_closing", area_closing + im_classif[:, :].astype(bool), "binary_closing", binary_closing ).astype(np.uint8) - if remove_small_holes: + if binary_opening: im_classif[:, :] = apply_morpho( - im_classif[:, :].astype(bool), - "remove_small_holes", - remove_small_holes, + im_classif[:, :].astype(bool), "binary_opening", binary_opening ).astype(np.uint8) - if remove_small_objects: + if area_closing: im_classif[:, :] = apply_morpho( - im_classif[:, :].astype(bool), - "remove_small_objects", - remove_small_objects, + im_classif[:, :], "area_closing", area_closing ).astype(np.uint8) # ---- Add nodata ---- @@ -1457,12 +1457,15 @@ def slurp_watermask( output_profiles=[output_profile], output_keys=[path.basename(args.watermask)], func=utils.compute_mask_threshold, - func_parameters={"ndwi_threshold": 1000}, + func_parameters={"ndwi_threshold": args.ndwi_threshold}, context_manager=slurp_manager, stable_margin=margin, binary=True, ) predict_profile = output_profile + # don't filter with Pekel, otherwise all detecteds areas + # will be discarded + args.no_pekel_filter = True else: logger.info("[3] Step: RF WATER") From 755f91cffc12e693f61bd0a1652f31a492dba0d7 Mon Sep 17 00:00:00 2001 From: Yannick TANGUY Date: Thu, 9 Jul 2026 09:33:10 +0000 Subject: [PATCH 2/2] stackmask will process water bodies categorization before other steps of post-process --- CHANGELOG.md | 5 ++ slurp/masks/stack_masks.py | 103 ++++++++++++++++++++----------------- 2 files changed, 60 insertions(+), 48 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e1daae..7fd9d35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ Only the first "Unreleased" section of this file corresponding of next release c When publication of a new release, the section "Unreleased" is blocked to the next chosen version and name of the milestone at a given date. A new section Unreleased is opened then for next dev phase. +## Unreleased (several bug fixes) +### Fixed + - fix #83 : stackmask will process water bodies categorization before other steps of post-process. + + ## 0.1 First stable version for CO3D. - Compliant with Docker environment (no more dependency to EOScale / shared memory) - New convention for validity masks diff --git a/slurp/masks/stack_masks.py b/slurp/masks/stack_masks.py index 2253e94..ee6ac54 100644 --- a/slurp/masks/stack_masks.py +++ b/slurp/masks/stack_masks.py @@ -47,7 +47,6 @@ from slurp.eomultiprocessing.slurp_manager import slurpContextManager from slurp.eomultiprocessing.utils import read, read_and_get_profile from slurp.post_process.morphology import apply_morpho, morpho_clean -from slurp.tools import io_utils from slurp.tools import profile_utils as eo_utils from slurp.tools import utils from slurp.tools.constant import HIGH, LOW, NODATA_INT8 @@ -130,7 +129,7 @@ def watershed_regul_buildings( urbanmask: np.ndarray, wsf: np.ndarray, vegmask: np.ndarray, - watermask: np.ndarray, + categorized_watermask: np.ndarray, shadowmask: np.ndarray, *, sobel_image: str, @@ -160,8 +159,8 @@ def watershed_regul_buildings( World Settlement Footprint mask used as building ground truth. vegmask : np.ndarray Vegetation classification mask. - watermask : np.ndarray - Binary water mask. + categorized_watermask: np.ndarray + Categorized water mask. shadowmask : np.ndarray Shadow classification mask. sobel_image : str @@ -273,7 +272,7 @@ def watershed_regul_buildings( markers[0][eroded_shadow] = value_classif_background # Water - markers[0][watermask == 1] = value_classif_background + markers[0][categorized_watermask != 0] = value_classif_background seg = segmentation.watershed(edges, markers[0].astype(np.uint8)) return seg, markers @@ -289,26 +288,29 @@ def infer_waterbodies_type(wbm, watermask, params): :return: categorized mask """ nb_iter = 2 + + # Clean watermask + # 1) apply morpho operation (opening) + # 2) remove holes + # 3) only keep significant water bodies clean_watermask = watermask == 1 for _ in range(nb_iter): clean_watermask = apply_morpho(clean_watermask, "binary_opening", 2) - # remove small objects in order to reduce the segmentation - watermask_remove = apply_morpho( + clean_watermask = apply_morpho( clean_watermask, - "remove_small_objects", - params["minimal_size_water_area"], - ) - - watermask_remove = apply_morpho( - watermask_remove, "remove_small_holes", + params["remove_small_holes"], + ) + clean_watermask = apply_morpho( + clean_watermask, + "remove_small_objects", params["minimal_size_water_area"], ) # 2nd step: segmentation # label image regions - label_image = label(watermask_remove) + label_image = label(clean_watermask) logger.debug( f"Infer waterbodies type --> {len(np.unique(label_image))} water bodies found" ) @@ -319,14 +321,14 @@ def infer_waterbodies_type(wbm, watermask, params): SEA: params["value_classif_sea"], LAKE: params["value_classif_lake"], RIVER: params["value_classif_river"], - 0: 0, # params["value_classif_water"] + 0: params["value_classif_water"], } # loop on all water bodies val_uniques = np.unique(label_image) for val in val_uniques: mask_val = label_image == val # values from WBM covered by value 'val' in label_image - if 1 in watermask_remove[mask_val]: + if 1 in clean_watermask[mask_val]: # water body sub_set_wbm = wbm[mask_val] kind_waterbodies = np.unique(sub_set_wbm) @@ -355,7 +357,7 @@ def post_process( vhr3: np.ndarray, vhr4: np.ndarray, valid_stack: np.ndarray, - watermask: np.ndarray, + categorized_watermask: np.ndarray, vegmask: np.ndarray, urbanmask: np.ndarray, shadowmask: np.ndarray, @@ -380,6 +382,7 @@ def post_process( binary_opening: int | None = None, remove_small_holes: int | None = None, remove_small_objects: int | None = None, + minimal_size_water_area: int | None = None, ): """ Perform SLURP post-processing to generate final classification layers. @@ -390,8 +393,8 @@ def post_process( Four spectral bands of the VHR image. valid_stack : np.ndarray Validity mask defining nodata regions. - watermask : np.ndarray - Binary water mask. + categorized_watermask : np.ndarray + Categorized water mask. vegmask : np.ndarray Vegetation classification mask. urbanmask : np.ndarray @@ -469,7 +472,7 @@ def post_process( urbanmask=urbanmask, wsf=wsf, vegmask=vegmask, - watermask=watermask, + categorized_watermask=categorized_watermask, shadowmask=shadowmask, sobel_image=sobel_image, regul_type=regul_type, @@ -514,7 +517,9 @@ def post_process( ) stack[0][clean_buildings] = value_classif_buildings - stack[0][watermask == 1] = value_classif_water + stack[0] = np.where( + categorized_watermask != 0, categorized_watermask, stack[0] + ) clean_low_veg = vegmask == 21 stack[0][clean_low_veg] = value_classif_low_veg @@ -532,7 +537,7 @@ def post_process( height_layer[0][clean_low_veg] = LOW height_layer[0][clean_buildings] = HIGH height_layer[0][clean_high_veg] = HIGH - height_layer[0][watermask == 1] = 0 + height_layer[0][categorized_watermask != 0] = 0 height_layer[0][shadowmask == 2] = 0 height_layer[0][valid_stack != 0] = NODATA_INT8 @@ -845,6 +850,27 @@ def slurp_stackmask( [deepcopy(image_profile)] ) + if args.categorized_watermask: + # Categorize watermask + # -> classify detected waterbodies according + # to Copernicus Water Bodies Mask + # ==================== + key_wbm = read(args.extracted_wbm) + wbm = key_wbm[0] + water = watermask[0][0] + + categorized = infer_waterbodies_type( + wbm, + water, + vars(args), + ) + wmask = categorized + else: + # Keep all detected water areas + wmask = np.where( + watermask[0][0] == 1, args.value_classif_water, 0 + ) + stack, height, markers = mp_n_to_m_images( inputs=[ image[0][0], # vhr1 @@ -852,7 +878,7 @@ def slurp_stackmask( image[0][2], # vhr3 image[0][3], # vhr4 validstack[0][0], - watermask[0][0], + wmask, vegmask[0][0], urbanmask[0][0], shadowmask[0][0], @@ -885,6 +911,7 @@ def slurp_stackmask( "binary_opening": args.binary_opening, "remove_small_holes": args.remove_small_holes, "remove_small_objects": args.remove_small_objects, + "minimal_size_water_area": args.minimal_size_water_area, }, context_manager=slurp_manager, stable_margin=args.margin, @@ -894,31 +921,11 @@ def slurp_stackmask( # WRITE STACK # ============================== - if args.categorized_watermask: - - key_wbm = read(args.extracted_wbm) - wbm = key_wbm[0] - water = watermask[0][0] - - categorized = infer_waterbodies_type( - wbm, - water, - vars(args), - ) - - stack[:] = np.where(categorized != 0, categorized, stack) - io_utils.save_image( - stack, - args.stackmask, - crs=output_profile[0]["crs"], - transform=output_profile[0]["transform"], - ) - else: - slurp_manager.write_tif( - data=stack, - path=args.stackmask, - target_profile=output_profile[0], - ) + slurp_manager.write_tif( + data=stack, + path=args.stackmask, + target_profile=output_profile[0], + ) if args.debug: