diff --git a/src/zedprofiler/featurization/texture.py b/src/zedprofiler/featurization/texture.py index a76bd50..5503e8c 100644 --- a/src/zedprofiler/featurization/texture.py +++ b/src/zedprofiler/featurization/texture.py @@ -7,7 +7,7 @@ We want this module to be python api callable and scalable. """ -import contextlib +import warnings import mahotas import numpy @@ -112,7 +112,7 @@ def resample_to_isotropic( ) -def compute_texture( # noqa: C901 +def compute_texture( # noqa: C901, PLR0915 object_loader: ObjectLoader, distance: int = 1, grayscale: int = 256, @@ -261,7 +261,7 @@ def compute_texture( # noqa: C901 image_object[~resampled_mask.astype(bool)] = 0 image_object = scale_image(image_object, num_gray_levels=grayscale) - with contextlib.suppress(ValueError): + try: # calculates 13 Haralick features for each direction (13) # and each object, and stores them in a 3D array features[:, :, idx] = mahotas.features.haralick( @@ -270,6 +270,15 @@ def compute_texture( # noqa: C901 distance=distance, compute_14th_feature=False, ) + except ValueError: + # mahotas cannot compute GLCM features when an object's extent + # is smaller than the distance parameter; the object's texture + # values remain NaN in this case. + warnings.warn( + f"Object {label} is smaller than distance={distance}; " + "Texture features are undefined (NaN) for this object.", + stacklevel=2, + ) # iterate through the direction, feature, and object dimensions # of the features array to populate the output dictionary for direction, direction_features in enumerate(features): diff --git a/tests/featurization/test_texture.py b/tests/featurization/test_texture.py index c99126a..d08e6a9 100644 --- a/tests/featurization/test_texture.py +++ b/tests/featurization/test_texture.py @@ -159,6 +159,38 @@ def test_value_error_in_one_object_does_not_corrupt_others() -> None: assert np.isnan(obj2_asm), "Object 2 (single pixel) should have NaN texture values" +def test_undersized_object_warns() -> None: + """An object smaller than the distance parameter must trigger a warning. + + mahotas cannot compute GLCM features for an object below a minimum pixel + extent for the given distance; this is expected, but it must not happen + silently. Regression test for compute_texture previously swallowing the + resulting ValueError via contextlib.suppress with no warning at all. + """ + shape = (20, 20, 20) + image = np.zeros(shape, dtype=np.uint8) + label = np.zeros(shape, dtype=np.int32) + + # single pixel — no neighbour pairs → ValueError from mahotas + image[15, 15, 15] = 100 + label[15, 15, 15] = LABEL_OBJ_2 + + imgset = ImageSetLoaderModel() + loader = ObjectLoaderModel( + image=image, + label_image=label, + object_ids=[LABEL_OBJ_2], + image_set_loader=imgset, + ) + + with pytest.warns(UserWarning, match=r"Object 2 is smaller than distance=1"): + df = compute_texture(loader, distance=1, grayscale=256) + + asm_cols = [c for c in df.columns if "AngularSecondMoment" in c and "-00-" in c] + assert asm_cols + assert np.isnan(df[asm_cols[0]].iloc[0]) + + def test_texture_values_correct_per_object() -> None: """Each object must receive its own Haralick values, not another object's.