diff --git a/src/spatialdata_plot/pl/render.py b/src/spatialdata_plot/pl/render.py index 49f288a9..230066f8 100644 --- a/src/spatialdata_plot/pl/render.py +++ b/src/spatialdata_plot/pl/render.py @@ -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: diff --git a/tests/pl/test_render_labels.py b/tests/pl/test_render_labels.py index 3e8160dc..d308396f 100644 --- a/tests/pl/test_render_labels.py +++ b/tests/pl/test_render_labels.py @@ -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. diff --git a/tests/pl/test_utils.py b/tests/pl/test_utils.py index edc4a2b9..fac573a5 100644 --- a/tests/pl/test_utils.py +++ b/tests/pl/test_utils.py @@ -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).