Skip to content
Open
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
57 changes: 12 additions & 45 deletions cellfinder/core/detect/filters/volume/structure_splitting.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,33 +179,6 @@ def iterative_ball_filter(
return ns, centres


def check_centre_in_cuboid(centre: np.ndarray, max_coords: np.ndarray) -> bool:
"""
Checks whether a coordinate is in a cuboid.

Parameters
----------

centre : np.ndarray
x, y, z coordinate.
max_coords : np.ndarray
Far corner of cuboid.

Returns
-------
True if within cuboid, otherwise False.
"""
relative_coords = centre
if (relative_coords > max_coords).all():
logger.info(
'Relative coordinates "{}" exceed maximum volume '
'dimension of "{}"'.format(relative_coords, max_coords)
)
return False
else:
return True


def split_cells(
cell_points: np.ndarray, settings: DetectionSettings
) -> np.ndarray:
Expand Down Expand Up @@ -235,13 +208,7 @@ def split_cells(
# corner coordinates in absolute pixels
orig_corner = np.array([xs.min(), ys.min(), zs.min()])
# volume center relative to corner
relative_orig_centre = np.array(
[
orig_centre[0] - orig_corner[0],
orig_centre[1] - orig_corner[1],
orig_centre[2] - orig_corner[2],
]
)
relative_orig_centre = orig_centre - orig_corner

# total volume enclosing all points
original_bounding_cuboid_shape = get_shape(xs, ys, zs)
Expand Down Expand Up @@ -290,18 +257,18 @@ def split_cells(
if not settings.outlier_keep:
# TODO: change to checking whether in original cluster shape
original_max_coords = np.array(original_bounding_cuboid_shape)
relative_centres = np.array(
[
x
for x in relative_centres
if check_centre_in_cuboid(x, original_max_coords)
]
)
# keep centres where not all coordinates exceed max
mask = ~np.all(relative_centres > original_max_coords, axis=1)
if not mask.all():
outliers = relative_centres[~mask]
for outlier in outliers:
logger.info(
'Relative coordinates "{}" exceed maximum volume '
'dimension of "{}"'.format(outlier, original_max_coords)
)
relative_centres = relative_centres[mask]

absolute_centres = np.empty((len(relative_centres), 3))
# convert centers to absolute pixels
absolute_centres[:, 0] = orig_corner[0] + relative_centres[:, 0]
absolute_centres[:, 1] = orig_corner[1] + relative_centres[:, 1]
absolute_centres[:, 2] = orig_corner[2] + relative_centres[:, 2]
absolute_centres = relative_centres + orig_corner

return absolute_centres
50 changes: 50 additions & 0 deletions tests/core/test_integration/test_detection_structure_splitting.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,53 @@ def test_underflow_issue_435():
expected = {(10, 11, 11), (20, 11, 11)}
got = set(map(tuple, centers.tolist()))
assert expected == got


def test_split_cells_outlier_filtering():
"""
Test that split_cells removes centres where all coordinates exceed
the bounding cuboid when outlier_keep=False.
"""
from unittest.mock import patch

# Points [4,4,4] to [6,6,6] → bounding shape (3,3,3), corner (4,4,4)
cell_points = np.array(
[
[4, 4, 4],
[5, 5, 5],
[6, 6, 6],
]
)

settings = DetectionSettings(
plane_shape=(100, 100),
plane_original_np_dtype=np.float32,
voxel_sizes=(1, 1, 1),
ball_xy_size_um=3,
ball_z_size_um=3,
ball_overlap_fraction=0.8,
soma_diameter_um=7,
outlier_keep=False,
)

# Mock iterative_ball_filter to return centres including an outlier.
# Centres are relative to the expanded volume, but the outlier check
# compares against original_bounding_cuboid_shape = (3,3,3).
# [1,1,1] and [2,2,2] are valid; [10,10,10] exceeds (3,3,3) in all dims.
mock_centres = np.array(
[[1.0, 1.0, 1.0], [2.0, 2.0, 2.0], [10.0, 10.0, 10.0]]
)
mock_return = ([3], [mock_centres])

with patch(
"cellfinder.core.detect.filters.volume.structure_splitting"
".iterative_ball_filter",
return_value=mock_return,
):
centers = split_cells(cell_points, settings)

# Outlier [10,10,10] should be filtered; valid relative centres [1,1,1]
# and [2,2,2] become absolute [5,5,5] and [6,6,6] (corner + relative)
expected = {(5.0, 5.0, 5.0), (6.0, 6.0, 6.0)}
got = set(map(tuple, centers.tolist()))
assert got == expected
Loading