Skip to content
Merged
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
9 changes: 3 additions & 6 deletions src/spatialdata_plot/pl/render.py
Original file line number Diff line number Diff line change
Expand Up @@ -2189,12 +2189,9 @@ def _render_labels(
is_label=True,
)

if np.issubdtype(label.dtype, np.floating):
raise ValueError(
f"Label element '{element}' has dtype {label.dtype}. Label arrays must use an "
f"integer dtype (e.g. int32 or uint16). Cast before plotting, e.g.:\n"
f" sdata['{element}'] = sdata['{element}'].astype('int32')"
)
# Label dtype is validated upstream: spatialdata rejects non-integer label rasters at the model
# boundary (parse / SpatialData construction / __setitem__), so a validly built element always
# reaches here with an integer dtype. No local guard needed (see #606, resolved upstream).

# rasterize spatial image if necessary to speed up performance
if rasterize:
Expand Down
17 changes: 0 additions & 17 deletions tests/pl/test_render_labels.py
Original file line number Diff line number Diff line change
Expand Up @@ -710,23 +710,6 @@ def test_render_labels_lognorm_with_zeros_does_not_crash(sdata_blobs: SpatialDat
plt.close(fig)


@pytest.mark.parametrize("dtype", [np.float16, np.float32, np.float64])
def test_render_labels_rejects_float_dtype(dtype):
# Regression test for #606: float-dtype labels must raise a clear
# ValueError naming the element and dtype, not a cryptic skimage TypeError.
arr = np.zeros((20, 20), dtype=dtype)
arr[3:8, 3:8] = 1
arr[12:17, 12:17] = 2
sdata = SpatialData(labels={"lbl": Labels2DModel.parse(arr, dims=["y", "x"])})

fig, ax = plt.subplots()
try:
with pytest.raises(ValueError, match=r"Label element 'lbl'.*integer dtype"):
sdata.pl.render_labels("lbl").pl.show(ax=ax)
finally:
plt.close(fig)


def test_render_labels_rejects_background_instance_id_in_table():
# Regression test for #607: table row with instance_id=0 (background)
# used to crash with obnscure error.
Expand Down
20 changes: 14 additions & 6 deletions tests/pl/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -628,12 +628,20 @@ def test_unmatched_instance_ids_warn_and_write_nan(self, sdata_blobs: SpatialDat
measure_obs(sdata_blobs, "blobs_labels")
assert np.isnan(table.obsm["spatial"]).all()

def test_float_dtype_labels_supported(self, sdata_blobs: SpatialData) -> None:
# #3: a float-typed (but integer-valued) labels raster must not crash np.bincount.
arr = np.asarray(sdata_blobs["blobs_labels"].data).astype(np.float32)
sd = _labels_sdata(arr)
measure_obs(sd, "lab", table_name="t")
assert np.isfinite(sd["t"].obsm["spatial"]).all()
def test_float_dtype_labels_handled_by_centroid_stats(self, sdata_blobs: SpatialData) -> None:
# #3: an integer-valued but float-typed raster must be cast to int, not crash np.bincount.
# spatialdata now rejects float labels at the model boundary, so this can no longer reach
# `measure_obs` through a SpatialData; the cast lives in `_stream_label_centroid_stats`, so
# exercise it there: a float raster must yield exactly what its integer counterpart yields.
from spatialdata_plot.pl.utils import _stream_label_centroid_stats

arr_int = np.asarray(sdata_blobs["blobs_labels"].data).astype(np.int64)
lbl_i, x_i, y_i, area_i = _stream_label_centroid_stats(arr_int)
lbl_f, x_f, y_f, area_f = _stream_label_centroid_stats(arr_int.astype(np.float32))
np.testing.assert_array_equal(lbl_f, lbl_i)
np.testing.assert_allclose(x_f, x_i)
np.testing.assert_allclose(y_f, y_i)
np.testing.assert_array_equal(area_f, area_i)

def test_existing_nonnumeric_column_raises_before_any_write(self, sdata_blobs: SpatialData) -> None:
# #4: a non-numeric collision raises BEFORE obsm is mutated (no half-written table).
Expand Down
Loading