Skip to content

Commit 61e43ff

Browse files
timblakelycopybara-github
authored andcommitted
Optimizations for finding decision points on sparse volumes
- If the number of voxels is below a certain threshold do not count it as a segment to expand. - If number of unique segments in a subvolume is <2 do not expand. PiperOrigin-RevId: 896687880
1 parent dccf620 commit 61e43ff

2 files changed

Lines changed: 62 additions & 8 deletions

File tree

ffn/utils/decision_point.py

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,32 +19,53 @@
1919

2020
from connectomics.common import bounding_box
2121
from connectomics.segmentation import labels
22+
from ffn.inference import segmentation as segmentation_lib
2223
import numpy as np
2324
import pandas as pd
2425
from scipy import ndimage
2526

2627

27-
def find_decision_points(seg: np.ndarray,
28-
voxel_size: Sequence[float],
29-
max_distance: Optional[float] = None,
30-
subvol_box: Optional[bounding_box.BoundingBox] = None
31-
) -> dict[tuple[int, int], tuple[float, np.ndarray]]:
28+
def find_decision_points(
29+
seg: np.ndarray,
30+
voxel_size: Sequence[float],
31+
max_distance: Optional[float] = None,
32+
subvol_box: Optional[bounding_box.BoundingBox] = None,
33+
optimize_sparse: bool = False,
34+
sparse_noise_threshold: int = 0,
35+
) -> dict[tuple[int, int], tuple[float, np.ndarray]]:
3236
"""Identifies decision points in a segmentation subvolume.
3337
3438
Args:
3539
seg: 3d uint64 ndarray of segmentation data
3640
voxel_size: 3-tuple (xyz) defining the physical voxel size
3741
max_distance: maximum distance between the segment and the decision point
3842
(same units as voxel_size); if None, distances will not be limited
39-
subvol_box: selector for a subvolume within `seg` within which
40-
to search for decision points; the whole subvolume is always used
41-
to compute the distance transform
43+
subvol_box: selector for a subvolume within `seg` within which to search for
44+
decision points; the whole subvolume is always used to compute the
45+
distance transform
46+
optimize_sparse: if True, first counts the number of segments in `seg` and
47+
returns early if there are fewer than 2.
48+
sparse_noise_threshold: if > 0 and `optimize_sparse` is True, ignores
49+
components with voxel counts < this threshold when counting segments.
4250
4351
Returns:
4452
dict from segment ID pairs to tuples of:
4553
approximate physical distance from the segment to the decision point
4654
(x, y, z) decision point
4755
"""
56+
if optimize_sparse:
57+
_, counts = segmentation_lib.clean_up_and_count(
58+
seg,
59+
split_cc=False,
60+
min_size=sparse_noise_threshold,
61+
compute_id_map=False,
62+
)
63+
64+
if counts is not None and len([k for k in counts.keys() if k > 0]) <= 1:
65+
# If there are 0 or 1 unique segments (excluding background),
66+
# they cannot possibly touch another segment.
67+
return {}
68+
4869
# EDT is the Euclidean Distance Transform, specifying how far voxels added
4970
# in 'expanded_seg' are from the seeds in 'seg'.
5071
expanded_seg, edt = labels.watershed_expand(seg, voxel_size, max_distance)

ffn/utils/tests/decision_point_test.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,39 @@ def test_find_decision_point(self):
5555
self.assertIn((1, 2), points)
5656
self.assertLen(points, 1)
5757

58+
def test_find_decision_point_optimize_sparse(self):
59+
# 2 segments, but one is very small and should be filtered out
60+
seg = np.zeros((100, 80, 60), dtype=np.uint64)
61+
seg[:40, :, :] = 1
62+
# 2 voxels of label 2
63+
seg[60, 0, 0] = 2
64+
seg[61, 0, 0] = 2
65+
66+
# Without optimization, 1 and 2 might connect if they are grown far enough.
67+
points = decision_point.find_decision_points(seg, (1, 1, 1))
68+
self.assertIn((1, 2), points)
69+
70+
# With optimization but threshold 0, they still connect (no size filtering)
71+
points = decision_point.find_decision_points(
72+
seg, (1, 1, 1), optimize_sparse=True, sparse_noise_threshold=0
73+
)
74+
self.assertIn((1, 2), points)
75+
76+
# With optimization and threshold >= 2, label 2 is zeroed.
77+
# We're left with label 1 (size > 2), so 1 segment -> returns empty
78+
points = decision_point.find_decision_points(
79+
seg, (1, 1, 1), optimize_sparse=True, sparse_noise_threshold=3
80+
)
81+
self.assertEmpty(points)
82+
83+
# With just 1 segment from the start
84+
seg = np.zeros((100, 80, 60), dtype=np.uint64)
85+
seg[:40, :, :] = 1
86+
points = decision_point.find_decision_points(
87+
seg, (1, 1, 1), optimize_sparse=True, sparse_noise_threshold=0
88+
)
89+
self.assertEmpty(points)
90+
5891

5992
if __name__ == '__main__':
6093
absltest.main()

0 commit comments

Comments
 (0)