Skip to content

Commit

Permalink
use regionprops_dict by default when regionprops_table is called
Browse files Browse the repository at this point in the history
  • Loading branch information
grlee77 committed Mar 4, 2025
1 parent 3988723 commit bcd94fb
Show file tree
Hide file tree
Showing 3 changed files with 136 additions and 39 deletions.
77 changes: 52 additions & 25 deletions python/cucim/src/cucim/skimage/measure/_regionprops.py
Original file line number Diff line number Diff line change
Expand Up @@ -948,6 +948,7 @@ def regionprops_table(
separator="-",
extra_properties=None,
spacing=None,
batch_processing=True,
):
"""Compute image properties and return them as a pandas-compatible table.
Expand Down Expand Up @@ -999,6 +1000,14 @@ def regionprops_table(
spacing: tuple of float, shape (ndim,)
The pixel spacing along each axis of the image.
Extra Parameters
----------------
batch_processing : bool, optional
If true, use much faster batch processing of region properties. Most
properties will be computed for all regions much more efficiently via a
single pass over the full image instead of on an individual label
basis. In this mode, the `RegionProperties` class is not used at all.
Returns
-------
out_dict : dict
Expand Down Expand Up @@ -1082,41 +1091,59 @@ def regionprops_table(
4 5 112.50 113.0 114.0
"""
regions = regionprops(
label_image,
intensity_image=intensity_image,
cache=cache,
extra_properties=extra_properties,
spacing=spacing,
)
if extra_properties is not None:
properties = list(properties) + [
prop.__name__ for prop in extra_properties
]
if len(regions) == 0:
ndim = label_image.ndim
label_image = np.zeros((3,) * ndim, dtype=int)
label_image[(1,) * ndim] = 1
label_image = cp.asarray(label_image)
if intensity_image is not None:
intensity_image = cp.zeros(
label_image.shape + intensity_image.shape[ndim:],
dtype=intensity_image.dtype,
)
if batch_processing:
from ._regionprops_gpu import regionprops_dict

table = regionprops_dict(
label_image,
intensity_image=intensity_image,
spacing=spacing,
moment_order=None,
properties=properties,
extra_properties=extra_properties,
to_table=True,
table_separator=separator,
table_on_host=False,
)
return table
else:
regions = regionprops(
label_image,
intensity_image=intensity_image,
cache=cache,
extra_properties=extra_properties,
spacing=spacing,
)
if extra_properties is not None:
properties = list(properties) + [
prop.__name__ for prop in extra_properties
]
if len(regions) == 0:
ndim = label_image.ndim
label_image = np.zeros((3,) * ndim, dtype=int)
label_image[(1,) * ndim] = 1
label_image = cp.asarray(label_image)
if intensity_image is not None:
intensity_image = cp.zeros(
label_image.shape + intensity_image.shape[ndim:],
dtype=intensity_image.dtype,
)
regions = regionprops(
label_image,
intensity_image=intensity_image,
cache=cache,
extra_properties=extra_properties,
spacing=spacing,
)

out_d = _props_to_dict(
regions, properties=properties, separator=separator
)
return {k: v[:0] for k, v in out_d.items()}

out_d = _props_to_dict(
return _props_to_dict(
regions, properties=properties, separator=separator
)
return {k: v[:0] for k, v in out_d.items()}

return _props_to_dict(regions, properties=properties, separator=separator)


def regionprops(
Expand Down
8 changes: 6 additions & 2 deletions python/cucim/src/cucim/skimage/measure/_regionprops_gpu.py
Original file line number Diff line number Diff line change
Expand Up @@ -792,8 +792,12 @@ def regionprops_dict(
out[name] = cp.stack(results, axis=0)
else:
out[name] = results
# only return the properties that were explicitly requested
out = {k: out[k] for k in properties}

# retain only the properties that were explicitly requested by the user
out_properties = list(properties) + list(
func.__name__ for func in extra_properties
)
out = {k: out[k] for k in out_properties}

if to_table:
out = _props_dict_to_table(
Expand Down
90 changes: 78 additions & 12 deletions python/cucim/src/cucim/skimage/measure/tests/test_regionprops.py
Original file line number Diff line number Diff line change
Expand Up @@ -1138,8 +1138,9 @@ def test_props_to_dict():
assert_array_equal(out["coords"][1], coords[1])


def test_regionprops_table():
out = regionprops_table(SAMPLE)
@pytest.mark.parametrize("batch_processing", [False, True])
def test_regionprops_table(batch_processing):
out = regionprops_table(SAMPLE, batch_processing=batch_processing)
assert out == {
"label": cp.array([1]),
"bbox-0": cp.array([0]),
Expand All @@ -1149,7 +1150,10 @@ def test_regionprops_table():
}

out = regionprops_table(
SAMPLE, properties=("label", "area", "bbox"), separator="+"
SAMPLE,
properties=("label", "area", "bbox"),
separator="+",
batch_processing=batch_processing,
)
assert out == {
"label": cp.array([1]),
Expand All @@ -1160,7 +1164,11 @@ def test_regionprops_table():
"bbox+3": cp.array([18]),
}

out = regionprops_table(SAMPLE_MULTIPLE, properties=("coords",))
out = regionprops_table(
SAMPLE_MULTIPLE,
properties=("coords",),
batch_processing=batch_processing,
)
coords = np.empty(2, object)
coords[0] = cp.stack((cp.arange(10),) * 2, axis=-1)
coords[1] = cp.array([[3, 7], [4, 7]])
Expand All @@ -1169,22 +1177,35 @@ def test_regionprops_table():
assert_array_equal(out["coords"][1], coords[1])


def test_regionprops_table_deprecated_vector_property():
out = regionprops_table(SAMPLE, properties=("local_centroid",))
@pytest.mark.parametrize("batch_processing", [False, True])
def test_regionprops_table_deprecated_vector_property(batch_processing):
out = regionprops_table(
SAMPLE,
properties=("local_centroid",),
batch_processing=batch_processing,
)
for key in out.keys():
# key reflects the deprecated name, not its new (centroid_local) value
assert key.startswith("local_centroid")


def test_regionprops_table_deprecated_scalar_property():
out = regionprops_table(SAMPLE, properties=("bbox_area",))
@pytest.mark.parametrize("batch_processing", [False, True])
def test_regionprops_table_deprecated_scalar_property(batch_processing):
out = regionprops_table(
SAMPLE,
properties=("bbox_area",),
batch_processing=batch_processing,
)
assert list(out.keys()) == ["bbox_area"]


def test_regionprops_table_equal_to_original():
def test_regionprops_table_equal_to_original_no_batch_processing():
regions = regionprops(SAMPLE, INTENSITY_FLOAT_SAMPLE)
out_table = regionprops_table(
SAMPLE, INTENSITY_FLOAT_SAMPLE, properties=COL_DTYPES.keys()
SAMPLE,
INTENSITY_FLOAT_SAMPLE,
properties=COL_DTYPES.keys(),
batch_processing=False,
)

for prop, dtype in COL_DTYPES.items():
Expand All @@ -1205,11 +1226,54 @@ def test_regionprops_table_equal_to_original():
assert_array_equal(rp[loc], out_table[modified_prop][i])


def test_regionprops_table_no_regions():
def test_regionprops_table_batch_close_to_original():
regions = regionprops(SAMPLE, INTENSITY_FLOAT_SAMPLE)
out_table = regionprops_table(
SAMPLE,
INTENSITY_FLOAT_SAMPLE,
properties=COL_DTYPES.keys(),
batch_processing=True,
)

for prop, dtype in COL_DTYPES.items():
print(f"{prop=}")
for i, reg in enumerate(regions):
rp = reg[prop]
if (
cp.isscalar(rp)
or (isinstance(rp, cp.ndarray) and rp.ndim == 0)
or prop in OBJECT_COLUMNS
or dtype is np.object_
):
if prop == "feret_diameter_max":
cp.testing.assert_allclose(
rp, out_table[prop][i], atol=math.sqrt(2)
)
elif prop == "slice":
assert_array_equal(rp, out_table[prop][i])
else:
assert_array_almost_equal(rp, out_table[prop][i])
else:
shape = rp.shape if isinstance(rp, cp.ndarray) else (len(rp),)
if "moments" in prop:
# will not match because moments > order are not computed in
# the batch_processing=True case.
continue
for ind in np.ndindex(shape):
modified_prop = "-".join(map(str, (prop,) + ind))
loc = ind if len(ind) > 1 else ind[0]
assert_array_almost_equal(
rp[loc], out_table[modified_prop][i]
)


@pytest.mark.parametrize("batch_processing", [False, True])
def test_regionprops_table_no_regions(batch_processing):
out = regionprops_table(
cp.zeros((2, 2), dtype=int),
properties=("label", "area", "bbox"),
separator="+",
batch_processing=batch_processing,
)
assert len(out) == 6
assert len(out["label"]) == 0
Expand Down Expand Up @@ -1347,12 +1411,14 @@ def test_extra_properties_mixed():
assert region.pixelcount == cp.sum(SAMPLE == 1)


def test_extra_properties_table():
@pytest.mark.parametrize("batch_processing", [False, True])
def test_extra_properties_table(batch_processing):
out = regionprops_table(
SAMPLE_MULTIPLE,
intensity_image=INTENSITY_SAMPLE_MULTIPLE,
properties=("label",),
extra_properties=(intensity_median, pixelcount, bbox_list),
batch_processing=batch_processing,
)
assert_array_almost_equal(out["intensity_median"], np.array([2.0, 4.0]))
assert_array_equal(out["pixelcount"], np.array([10, 2]))
Expand Down

0 comments on commit bcd94fb

Please sign in to comment.