Skip to content

Commit 1ddcb19

Browse files
authored
Use new convention for validity mask (#38)
* Use new convention for validity mask in prepare + shadow mask * Use new convention for validity mask in prepare + shadow mask * Use new convention for validity mask in prepare + shadow mask * New convention for valid_stack in stack_masks * New convention in vegetation mask * New convention in urbanmask * New convention in input valid_stack * Improve unitary test to add nodata in input image, and also a fake cloud mask to test validity mask * Fix veg mask test * fix validation and feature tests * Update CHANGELOG
1 parent 0df8d7b commit 1ddcb19

24 files changed

Lines changed: 265 additions & 208 deletions

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ A new section Unreleased is opened then for next dev phase.
1212
- Add possibility to use a nearly void config files : almost all config files shall be initiated with default values #14
1313

1414
### Changed
15-
- Change convention for validity mask : 0 values shall stand for "valid" pixels, and other values shall stand for user specified reasons (ex : 1 for NO_DATA in input VHR image, 2 for clouds, etc.)
15+
- Change convention for validity mask : 0 values shall stand for "valid" pixels, and other values shall stand for user specified reasons (ex : 1 for NO_DATA in input VHR image, 2 for clouds, etc.) --> see #32
1616

1717
### Fixed
1818
- fix precommit (#26)

docs/source/usage_api.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Then, you can import it in your Python scripts or notebooks.
1616

1717
.. code-block:: python
1818
19-
import slurp-masks as slurp
19+
import slurp
2020
2121
Data preparation
2222
----------------

pytest.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
[pytest]
2222
addopts = -ra
2323
markers =
24-
ci: for CI ('pytest -m ci --config config_tests_ci.json --main_config main_config_tests_ci.json --cov=slurp' to launch CI tests and compute code coverage)
24+
ci: for CI ('pytest -m ci --config config_tests_ci.json --main_config main_config_tests_ci.json --cov=slurp --cov-report html' to launch CI tests and compute code coverage (HTML report))
2525
validation: validate all algorithms (compute and compare to reference)
2626
features: validate specific features (launch pytest -m "features or validation" to launch all tests)
2727
testpaths = tests

slurp/masks/shadowmask.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,10 @@ def compute_shadowmask(
9292
9393
:param list input_buffers: 0 -> image, 1 -> valid_stack, 2 -> watermask
9494
:param list input_profiles: image profiles (not used but necessary for eoscale)
95-
:param dict params: must contain the keys "thresholds", "binary_opening" and "small_objects"
96-
:returns: valid_phr (boolean numpy array, True = valid data, False = no data)
95+
:param dict params: must contain the keys "thresholds",
96+
"binary_opening" and "small_objects"
97+
:returns: valid_mask (int numpy array) with following legend
98+
0: no shadows, 1: small shadows, 2: big shadows, + NODATA
9799
"""
98100
raw_shadow_mask = np.zeros(input_buffers[0][0].shape, dtype=int)
99101
raw_shadow_mask.fill(1)
@@ -126,7 +128,9 @@ def compute_shadowmask(
126128
final_shadow_mask += raw_shadow_mask
127129

128130
# apply NO_DATA mask
129-
final_shadow_mask[np.logical_not(input_buffers[1][0])] = NODATA_INT8
131+
final_shadow_mask = np.where(
132+
input_buffers[1][0] == 0, final_shadow_mask, NODATA_INT8
133+
)
130134

131135
return final_shadow_mask
132136

slurp/masks/stack_masks.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ def post_process(
293293
)
294294

295295
# Apply NODATA
296-
stack[0][np.logical_not(valid_stack[0])] = NODATA_INT8
296+
stack[0][np.where(valid_stack[0] != 0)] = NODATA_INT8
297297

298298
height_layer = np.zeros((1, input_image.shape[1], input_image.shape[2]))
299299

@@ -310,12 +310,12 @@ def post_process(
310310
height_layer[0][watermask[0] == 1] = 0
311311
height_layer[0][shadowmask[0] == 2] = 0
312312

313-
height_layer[0][np.logical_not(valid_stack[0])] = NODATA_INT8
313+
height_layer[0][np.where(valid_stack[0] != 0)] = NODATA_INT8
314314

315315
markers_layer = np.zeros((1, input_image.shape[1], input_image.shape[2]))
316316
# Markers
317317
markers_layer[0] = markers
318-
markers_layer[0][np.logical_not(valid_stack[0])] = NODATA_INT8
318+
markers_layer[0][np.where(valid_stack[0] != 0)] = NODATA_INT8
319319

320320
return [stack, height_layer, markers_layer]
321321

@@ -547,7 +547,8 @@ def slurp_stackmask(
547547
argsdict, cli_params = utils.parse_args(keys, logs_to_file, main_config)
548548

549549
for param in cli_params:
550-
# If the parameter from the CLI is not None, we update argsdict with the value from the CLI
550+
# If the parameter from the CLI is not None,
551+
# we update argsdict with the value from the CLI
551552
if locals()[param] is not None:
552553
argsdict[param] = locals()[param]
553554

slurp/masks/urbanmask.py

Lines changed: 34 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def apply_vegetationmask(
6767
non_veg_dilated = apply_morpho(
6868
non_veg[0], "binary_dilation", params["veg_binary_dilation"]
6969
)
70-
valid_stack = np.logical_and(input_buffer[0], [non_veg_dilated])
70+
valid_stack = np.logical_and(input_buffer[0] == 0, [non_veg_dilated])
7171

7272
return valid_stack
7373

@@ -83,9 +83,7 @@ def apply_watermask(
8383
:param dict params: dictionary of arguments (not used but necessary for eoscale)
8484
:returns: valid_phr (boolean numpy array, True = valid data, False = no data)
8585
"""
86-
valid_stack = np.logical_and(
87-
input_buffer[0], np.where(input_buffer[1] == 0, True, False)
88-
)
86+
valid_stack = np.logical_and(input_buffer[0] == 0, input_buffer[1] == 0)
8987

9088
return valid_stack
9189

@@ -95,14 +93,14 @@ def get_grid_indexes_from_mask(nb_samples, valid_mask, mask_ground_truth):
9593
Recover of row and columns indices selected on the valid pixel of the image
9694
9795
:param int nb_samples:
98-
:param boolean numpy array valid_mask :
99-
:param boolean numpy array mask_ground_truth :
96+
:param boolean numpy array valid_mask : shape (height, width)
97+
:param boolean numpy array mask_ground_truth : shape (height, width)
10098
:return: tuple of list , row indices and columns indices
10199
"""
102100
valid_samples = np.logical_and(mask_ground_truth, valid_mask).astype(
103101
np.uint8
104102
)
105-
_, rows, cols = np.where(valid_samples)
103+
rows, cols = np.where(valid_samples)
106104

107105
if 1 <= nb_samples <= len(rows):
108106
# np.arange(0, len(rows) -1, ...) : to be sure to exclude index len(rows)
@@ -130,6 +128,7 @@ def build_samples(
130128
"""
131129
# Beware that WSF ground truth contains 0 (non building), 255 (building)
132130
# but sometimes 1 (invalid pixels ?)
131+
validity_mask = (input_buffer[0] == 0)[0]
133132
mask_building_before_erosion = np.where(
134133
input_buffer[1] == params["value_classif"], True, False
135134
)
@@ -144,10 +143,10 @@ def build_samples(
144143

145144
# Retrieve number of pixels for each class
146145
nb_built_subset = np.count_nonzero(
147-
np.logical_and(mask_building, input_buffer[0])
146+
np.logical_and(mask_building, validity_mask)
148147
)
149148
nb_other_subset = np.count_nonzero(
150-
np.logical_and(mask_non_building, input_buffer[0])
149+
np.logical_and(mask_non_building, validity_mask)
151150
)
152151
# Ratio of pixel class compare to the full image ratio
153152
urban_ratio = nb_built_subset / params["nb_valid_built_pixels"]
@@ -161,18 +160,18 @@ def build_samples(
161160
if nb_urban_subsamples > 0:
162161
# Building samples
163162
rows_b, cols_b = get_grid_indexes_from_mask(
164-
nb_urban_subsamples, input_buffer[0][0], mask_building
163+
nb_urban_subsamples, validity_mask, mask_building[0]
165164
)
166165

167166
if nb_other_subsamples > 0:
168167
rows_nob, cols_nob = get_grid_indexes_from_mask(
169-
nb_other_subsamples, input_buffer[0][0], mask_non_building
168+
nb_other_subsamples, validity_mask, mask_non_building[0]
170169
)
171170
else:
172171

173172
if nb_other_subsamples > 0:
174173
rows_nob, cols_nob = get_grid_indexes_from_mask(
175-
nb_other_subsamples, input_buffer[0][0], mask_non_building
174+
nb_other_subsamples, validity_mask, mask_non_building[0]
176175
)
177176

178177
rows = np.concatenate((rows_b, rows_nob))
@@ -200,8 +199,9 @@ def rf_prediction(
200199
:returns: predicted mask (proba)
201200
"""
202201
im_stack = np.concatenate((input_buffer[2:]), axis=0)
203-
nodata_mask = (1 - input_buffer[0]).astype(bool)
204-
valid_mask = input_buffer[1].astype(bool)
202+
nodata_mask = input_buffer[0] != 0
203+
valid_mask = input_buffer[1] == 0
204+
205205
buffer_to_predict = np.transpose(im_stack[:, valid_mask[0]])
206206
# buffer_to_predict are non NODATA pixels, defined by all the primitives
207207
# (R-G-B-NIR-NDVI-NDWI-[+ features]
@@ -213,20 +213,19 @@ def rf_prediction(
213213
res_classif = classifier.classes_.take(np.argmax(proba, axis=1), axis=0)
214214
res_classif[res_classif == 255] = 1
215215

216-
prediction = np.zeros(valid_mask.shape)
217-
prediction[0][valid_mask[0]] = res_classif
218-
prediction[0][nodata_mask[0]] = NODATA_INT8
216+
prediction = np.zeros(valid_mask[0].shape)
217+
prediction[valid_mask[0]] = res_classif
218+
prediction[nodata_mask[0]] = NODATA_INT8
219219

220-
proba_buildings = np.zeros(valid_mask.shape)
221-
proba_buildings[0][valid_mask[0]] = (
220+
proba_buildings = np.zeros(valid_mask[0].shape)
221+
proba_buildings[valid_mask[0]] = (
222222
100 * proba[:, 1]
223223
) # Proba for class 1 (buildings)
224-
proba_buildings[0][nodata_mask[0]] = NODATA_INT8
225-
224+
proba_buildings[nodata_mask[0]] = NODATA_INT8
226225
else:
227226
# corner case : only NO_DATA !
228-
prediction = np.full(valid_mask.shape, NODATA_INT8)
229-
proba_buildings = np.full(valid_mask.shape, NODATA_INT8)
227+
prediction = np.full(valid_mask[0].shape, NODATA_INT8)
228+
proba_buildings = np.full(valid_mask[0].shape, NODATA_INT8)
230229

231230
return [proba_buildings, prediction]
232231

@@ -338,7 +337,7 @@ def nominal_case_urbanmask(
338337

339338
key_predict = eoexe.n_images_to_m_images_filter(
340339
inputs=[key_original_valid_stack],
341-
image_filter=add_nodata,
340+
image_filter=fill_constant_mask,
342341
filter_parameters={"fill_value": 0},
343342
generate_output_profiles=eo_utils.single_uint8_profile,
344343
context_manager=eoscale_manager,
@@ -508,7 +507,7 @@ def samples_train_and_predict(
508507
return time_random_forest
509508

510509

511-
def add_nodata(
510+
def fill_constant_mask(
512511
input_buffer: list, input_profiles: list, params: dict
513512
) -> np.ndarray:
514513
"""
@@ -519,9 +518,10 @@ def add_nodata(
519518
:param dict params: dictionary of arguments
520519
:returns: updated predicted mask (proba)
521520
"""
522-
nodata_mask = (1 - input_buffer[0]).astype(bool)
523-
proba_buildings = np.full(nodata_mask.shape, params["fill_value"])
524-
proba_buildings[nodata_mask] = NODATA_INT8
521+
522+
proba_buildings = np.where(
523+
input_buffer[0] == 0, params["fill_value"], NODATA_INT8
524+
)
525525

526526
return proba_buildings
527527

@@ -752,9 +752,11 @@ def slurp_urbanmask(
752752
]
753753

754754
# Calculation of valid pixels
755-
nb_valid_pixels = np.count_nonzero(valid_stack)
755+
nb_valid_pixels = len(
756+
np.where(valid_stack == 0)[0]
757+
) # np.count_nonzero(valid_stack)
756758
args.nb_valid_built_pixels = np.count_nonzero(
757-
np.logical_and(local_gt, valid_stack)
759+
np.logical_and(local_gt, valid_stack == 0)
758760
)
759761
args.nb_valid_other_pixels = (
760762
nb_valid_pixels - args.nb_valid_built_pixels
@@ -791,7 +793,7 @@ def slurp_urbanmask(
791793

792794
key_predict = eoexe.n_images_to_m_images_filter(
793795
inputs=[key_original_valid_stack],
794-
image_filter=add_nodata,
796+
image_filter=fill_constant_mask,
795797
filter_parameters={"fill_value": 100},
796798
generate_output_profiles=eo_utils.single_uint8_profile,
797799
context_manager=eoscale_manager,
@@ -812,7 +814,7 @@ def slurp_urbanmask(
812814

813815
key_predict = eoexe.n_images_to_m_images_filter(
814816
inputs=[key_original_valid_stack],
815-
image_filter=add_nodata,
817+
image_filter=fill_constant_mask,
816818
filter_parameters={"fill_value": 0},
817819
generate_output_profiles=eo_utils.single_uint8_profile,
818820
context_manager=eoscale_manager,

slurp/masks/vegetationmask.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
from slurp.post_process.morphology import apply_morpho
4040
from slurp.tools import eoscale_utils as eo_utils
4141
from slurp.tools import utils
42-
from slurp.tools.constant import NB_CLUSTERS, NODATA_INT8, NODATA_INT16
42+
from slurp.tools.constant import NB_CLUSTERS, NODATA_INT8
4343

4444
logger = logging.getLogger("slurp")
4545

@@ -108,8 +108,8 @@ def segmentation_task(
108108
segments = compute_segmentation(params, input_buffers[0], input_buffers[1])
109109

110110
# minimum segment is 1, attribute 0 to no_data pixel
111-
segments[np.logical_not(input_buffers[2])] = 0
112-
segments[np.where(input_buffers[1] == NODATA_INT16)] = 0
111+
# valid_stack contains valid pixels (0) and invalid pixels (any other value)
112+
segments = np.where(input_buffers[2] == 0, segments, 0)
113113

114114
return segments
115115

@@ -533,7 +533,7 @@ def finalize_task(input_buffers: list, input_profiles: list, params: dict):
533533
final_mask = ts_stats.finalize(input_buffers[0], clustering)
534534

535535
# Add nodata in final_mask (input_buffers[1] : valid mask)
536-
final_mask[np.logical_not(input_buffers[1][0])] = NODATA_INT8
536+
final_mask = np.where(input_buffers[1][0] == 0, final_mask, NODATA_INT8)
537537

538538
return final_mask
539539

@@ -599,7 +599,7 @@ def clean_task(
599599
im_classif,
600600
)
601601

602-
im_classif[np.logical_not(valid_stack)] = NODATA_INT8
602+
im_classif = np.where(valid_stack == 0, im_classif, NODATA_INT8)
603603

604604
return im_classif
605605

0 commit comments

Comments
 (0)