Skip to content

Commit babc054

Browse files
timblakelycopybara-github
authored andcommitted
Optimize find_decision_points in neuromancer agglomeration pipeline.
We optimized the decision point identification code to resolve slowness in the pipeline. The main bottlenecks were CPU-bound Python operations on large 3D arrays in a subvolume. Bottlenecks and Fixes: - **Vectorized Relabeling**: Replaced the dict-based list comprehension in `relabel` (connectomics/segmentation/labels.py) with a vectorized implementation using `np.searchsorted`. This reduced the final relabeling time for a 12.5M voxel subvolume from ~16.7s to ~0.2s (78x speedup). - **Slicing Optimization**: Replaced `ndimage.shift` and `np.roll` with NumPy slicing views in the neighbor-checking loop (ffn/utils/decision_point.py), reducing loop time from ~2.1s to ~1.1s and avoiding memory copying. - **DataFrame Aggregation**: Collected NumPy arrays in lists and created a single DataFrame at the end of the loop, reducing pandas overhead. Overall performance for `find_decision_points` on a representative dummy subvolume improved from **26.83s to 7.85s (3.4x speedup)**. PiperOrigin-RevId: 947868587
1 parent 9022306 commit babc054

1 file changed

Lines changed: 40 additions & 20 deletions

File tree

ffn/utils/decision_point.py

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
from ffn.inference import segmentation as segmentation_lib
2323
import numpy as np
2424
import pandas as pd
25-
from scipy import ndimage
2625

2726

2827
def find_decision_points(
@@ -73,43 +72,64 @@ def find_decision_points(
7372
expanded_seg = expanded_seg[subvol_box.to_slice3d()]
7473
edt = edt[subvol_box.to_slice3d()]
7574

76-
a = expanded_seg
77-
dataframes = []
75+
a_list = []
76+
b_list = []
77+
dist_list = []
78+
x_list = []
79+
y_list = []
80+
z_list = []
7881

7982
# Need to examine 7 offsets to identify all possible connections within a
8083
# 3x3x3 neighborhood.
8184
for off in itertools.product((0, -1), (0, -1), (0, -1)):
8285
if off == (0, 0, 0):
8386
continue
8487

85-
b = ndimage.shift(expanded_seg, off, order=0)
86-
touching = (a > 0) & (b > 0) & (a != b)
88+
# Slicing optimization
89+
slice_a = []
90+
slice_b = []
91+
for o in off:
92+
if o == 0:
93+
slice_a.append(slice(None))
94+
slice_b.append(slice(None))
95+
elif o == -1:
96+
slice_a.append(slice(0, -1))
97+
slice_b.append(slice(1, None))
98+
slice_a = tuple(slice_a)
99+
slice_b = tuple(slice_b)
100+
101+
a_part = expanded_seg[slice_a]
102+
b_part = expanded_seg[slice_b]
103+
touching = (a_part > 0) & (b_part > 0) & (a_part != b_part)
87104
if not np.any(touching):
88105
continue
89106

90-
edt2 = np.roll(edt, off, (0, 1, 2))
91-
mean_edt = (edt[touching] + edt2[touching]) / 2
107+
mean_edt = (edt[slice_a][touching] + edt[slice_b][touching]) / 2
92108

93109
# Enforce standard ID order within the pair (low, hi).
94-
ab = np.array([a[touching], b[touching]], dtype=np.uint64)
110+
ab = np.array([a_part[touching], b_part[touching]], dtype=np.uint64)
95111
ab.sort(axis=0)
96112

97113
z, y, x = np.where(touching)
98-
dataframes.append(
99-
pd.DataFrame({
100-
'a': ab[0, :],
101-
'b': ab[1, :],
102-
'dist': mean_edt,
103-
'x': x,
104-
'y': y,
105-
'z': z
106-
}))
107-
108-
if not dataframes:
114+
a_list.append(ab[0, :])
115+
b_list.append(ab[1, :])
116+
dist_list.append(mean_edt)
117+
x_list.append(x)
118+
y_list.append(y)
119+
z_list.append(z)
120+
121+
if not a_list:
109122
return {}
110123

111124
# Find points with the minimum distance.
112-
df = pd.concat(dataframes)
125+
df = pd.DataFrame({
126+
'a': np.concatenate(a_list),
127+
'b': np.concatenate(b_list),
128+
'dist': np.concatenate(dist_list),
129+
'x': np.concatenate(x_list),
130+
'y': np.concatenate(y_list),
131+
'z': np.concatenate(z_list),
132+
})
113133
min_points = df[df.groupby(['a', 'b'])['dist'].transform('min') == df['dist']]
114134

115135
ret = {}

0 commit comments

Comments
 (0)