Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
103 changes: 55 additions & 48 deletions slurp/masks/stack_masks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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"
)
Expand All @@ -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)
Expand Down Expand Up @@ -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,
Expand All @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -845,14 +850,35 @@ 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
image[0][1], # vhr2
image[0][2], # vhr3
image[0][3], # vhr4
validstack[0][0],
watermask[0][0],
wmask,
vegmask[0][0],
urbanmask[0][0],
shadowmask[0][0],
Expand Down Expand Up @@ -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,
Expand All @@ -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:

Expand Down
35 changes: 19 additions & 16 deletions slurp/masks/watermask.py
Original file line number Diff line number Diff line change
Expand Up @@ -527,43 +527,43 @@ 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
# -> probably a false positive (shadows, etc.) we need to clean
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 ----
Expand Down Expand Up @@ -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")
Expand Down
Loading