Skip to content

Commit c07bc97

Browse files
feat: add oversegment function (#85)
* feat: add oversegmentation using skeletons * refactor: put binary image iterator code in a separate function * install: bump dijkstra3d to 1.15.0 This has the new EDF feature map * feat: return copies of skeletons with segments attribute * merge: add visualize_section_planes feature * docs: show how to use oversegment
1 parent e34fb09 commit c07bc97

File tree

4 files changed

+148
-48
lines changed

4 files changed

+148
-48
lines changed

README.md

+14-1
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ skel = kimimaro.join_close_components([skel1, skel2], radius=None) # no threshol
127127
extra_targets = kimimaro.synapses_to_targets(labels, synapses)
128128

129129

130-
# LISTING 3: Drawing a centerline between
130+
# LISTING 4: Drawing a centerline between
131131
# preselected points on a binary image.
132132
# This is a much simpler option for when
133133
# you know exactly what you want, but may
@@ -139,6 +139,19 @@ skel = kimimaro.connect_points(
139139
end=(121, 426, 227),
140140
anisotropy=(32,32,40),
141141
)
142+
143+
# LISTING 5: Using skeletons to oversegment existing
144+
# segmentations for integration into proofreading systems
145+
# that on merging atomic labels. oversegmented_labels
146+
# is returned numbered from 1. skels is a copy returned
147+
# with the property skel.segments that associates a label
148+
# to each vertex (labels will not be unique if downsampling
149+
# is used)
150+
oversegmented_labels, skels = kimimaro.oversegment(
151+
labels, skels,
152+
anisotropy=(32,32,40),
153+
downsample=10,
154+
)
142155
```
143156

144157
`connectomics.npy` is multilabel connectomics data derived from pinky40, a 2018 experimental automated segmentation of ~1.5 million cubic micrometers of mouse visual cortex. It is an early predecessor to the now public pinky100_v185 segmentation that can be found at https://microns-explorer.org/phase1 You will need to run `lzma -d connectomics.npy.lzma` to obtain the 512x512x512 uint32 volume at 32x32x40 nm<sup>3</sup> resolution.

kimimaro/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,4 @@
1717

1818
from .intake import skeletonize, DimensionError, synapses_to_targets, connect_points
1919
from .postprocess import postprocess, join_close_components
20-
from .utility import extract_skeleton_from_binary_image, cross_sectional_area
20+
from .utility import extract_skeleton_from_binary_image, cross_sectional_area, oversegment

kimimaro/utility.py

+132-45
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
from typing import Dict, Union, List
1+
from typing import Dict, Union, List, Tuple
2+
3+
import copy
24

35
import numpy as np
46
import scipy.ndimage
@@ -8,6 +10,7 @@
810
import kimimaro.skeletontricks
911

1012
import cc3d
13+
import dijkstra3d
1114
import fastremap
1215
import fill_voids
1316
import xs3d
@@ -50,6 +53,54 @@ def add_property(skel, prop):
5053
if needs_prop:
5154
skel.extra_attributes.append(prop)
5255

56+
def shape_iterator(all_labels, skeletons, fill_holes, in_place, progress, fn):
57+
iterator = skeletons
58+
if type(skeletons) == dict:
59+
iterator = skeletons.values()
60+
total = len(skeletons)
61+
elif type(skeletons) == Skeleton:
62+
iterator = [ skeletons ]
63+
total = 1
64+
else:
65+
total = len(skeletons)
66+
67+
if all_labels.dtype == bool:
68+
remapping = { True: 1, False: 0, 1:1, 0:0 }
69+
else:
70+
all_labels, remapping = fastremap.renumber(all_labels, in_place=in_place)
71+
72+
all_slices = find_objects(all_labels)
73+
74+
for skel in tqdm(iterator, desc="Labels", disable=(not progress), total=total):
75+
if all_labels.dtype == bool:
76+
label = 1
77+
else:
78+
label = skel.id
79+
80+
if label == 0:
81+
continue
82+
83+
label = remapping[label]
84+
slices = all_slices[label - 1]
85+
if slices is None:
86+
continue
87+
88+
roi = Bbox.from_slices(slices)
89+
if roi.volume() <= 1:
90+
continue
91+
92+
roi.grow(1)
93+
roi.minpt = Vec.clamp(roi.minpt, Vec(0,0,0), roi.maxpt)
94+
slices = roi.to_slices()
95+
96+
binimg = np.asfortranarray(all_labels[slices] == label)
97+
if fill_holes:
98+
binimg = fill_voids.fill(binimg, in_place=True)
99+
100+
fn(skel, binimg, roi)
101+
102+
return iterator
103+
53104
def cross_sectional_area(
54105
all_labels:np.ndarray,
55106
skeletons:Union[Dict[int,Skeleton],List[Skeleton],Skeleton],
@@ -100,54 +151,11 @@ def cross_sectional_area(
100151
"num_components": 1,
101152
}
102153

103-
iterator = skeletons
104-
if type(skeletons) == dict:
105-
iterator = skeletons.values()
106-
total = len(skeletons)
107-
elif type(skeletons) == Skeleton:
108-
iterator = [ skeletons ]
109-
total = 1
110-
else:
111-
total = len(skeletons)
112-
113-
if all_labels.dtype == bool:
114-
remapping = { True: 1, False: 0, 1:1, 0:0 }
115-
else:
116-
all_labels, remapping = fastremap.renumber(all_labels, in_place=in_place)
117-
118-
all_slices = find_objects(all_labels)
119-
120-
for skel in tqdm(iterator, desc="Labels", disable=(not progress), total=total):
121-
if all_labels.dtype == bool:
122-
label = 1
123-
else:
124-
label = skel.id
125-
126-
if label == 0:
127-
continue
128-
129-
label = remapping[label]
130-
slices = all_slices[label - 1]
131-
if slices is None:
132-
continue
133-
134-
roi = Bbox.from_slices(slices)
135-
if roi.volume() <= 1:
136-
continue
137-
138-
roi.grow(1)
139-
roi.minpt = Vec.clamp(roi.minpt, Vec(0,0,0), roi.maxpt)
140-
slices = roi.to_slices()
141-
142-
binimg = np.asfortranarray(all_labels[slices] == label)
143-
154+
def cross_sectional_area_helper(skel, binimg, roi):
144155
cross_sections = None
145156
if visualize_section_planes:
146157
cross_sections = np.zeros(binimg.shape, dtype=np.uint32, order="F")
147158

148-
if fill_holes:
149-
binimg = fill_voids.fill(binimg, in_place=True)
150-
151159
all_verts = (skel.vertices / anisotropy).round().astype(int)
152160
all_verts -= roi.minpt
153161

@@ -207,11 +215,90 @@ def cross_sectional_area(
207215
microviewer.view(cross_sections, seg=True)
208216

209217
add_property(skel, prop)
218+
210219
skel.cross_sectional_area = areas
211220
skel.cross_sectional_area_contacts = contacts
212221

222+
shape_iterator(
223+
all_labels, skeletons,
224+
fill_holes, in_place, progress,
225+
cross_sectional_area_helper
226+
)
227+
213228
return skeletons
214229

230+
def oversegment(
231+
all_labels:np.ndarray,
232+
skeletons:Union[Dict[int,Skeleton],List[Skeleton],Skeleton],
233+
anisotropy:np.ndarray = np.array([1,1,1], dtype=np.float32),
234+
progress:bool = False,
235+
fill_holes:bool = False,
236+
in_place:bool = False,
237+
downsample:int = 0,
238+
) -> Tuple[np.ndarray, Union[Dict[int,Skeleton],List[Skeleton],Skeleton]]:
239+
"""
240+
Use skeletons to create an oversegmentation of a pre-existing set
241+
of labels. This is useful for proofreading systems that work by merging
242+
labels.
243+
244+
For each skeleton, get the feature map from its euclidean distance
245+
field. The final image is the composite of all these feature maps
246+
numbered from 1.
247+
248+
Each skeleton will have a new property skel.segments that associates
249+
a label to each vertex.
250+
"""
251+
prop = {
252+
"id": "segments",
253+
"data_type": "uint64",
254+
"num_components": 1,
255+
}
256+
257+
skeletons = copy.deepcopy(skeletons)
258+
259+
all_features = np.zeros(all_labels.shape, dtype=np.uint64, order="F")
260+
next_label = 0
261+
262+
def oversegment_helper(skel, binimg, roi):
263+
nonlocal next_label
264+
nonlocal all_features
265+
266+
segment_skel = skel
267+
if downsample > 0:
268+
segment_skel = skel.downsample(downsample)
269+
270+
vertices = (segment_skel.vertices / anisotropy).round().astype(int)
271+
vertices -= roi.minpt
272+
273+
field, feature_map = dijkstra3d.euclidean_distance_field(
274+
binimg, vertices,
275+
anisotropy=anisotropy,
276+
return_feature_map=True
277+
)
278+
del field
279+
280+
add_property(skel, prop)
281+
282+
vertices = (skel.vertices / anisotropy).round().astype(int)
283+
vertices -= roi.minpt
284+
285+
feature_map[binimg] += next_label
286+
skel.segments = feature_map[vertices[:,0], vertices[:,1], vertices[:,2]]
287+
next_label += vertices.shape[0]
288+
all_features[roi.to_slices()] += feature_map
289+
290+
# iterator is an iterable list of skeletons, not the shape iterator
291+
iterator = shape_iterator(
292+
all_labels, skeletons, fill_holes, in_place, progress,
293+
oversegment_helper
294+
)
295+
296+
all_features, mapping = fastremap.renumber(all_features)
297+
for skel in iterator:
298+
skel.segments = fastremap.remap(skel.segments, mapping, in_place=True)
299+
300+
return all_features, skeletons
301+
215302
# From SO: https://stackoverflow.com/questions/14313510/how-to-calculate-rolling-moving-average-using-python-numpy-scipy
216303
def moving_average(a:np.ndarray, n:int, mode:str = "symmetric") -> np.ndarray:
217304
if n <= 0:

requirements.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
click
22
connected-components-3d>=1.5.0
33
cloud-volume>=0.57.6
4-
dijkstra3d>=1.12.0
4+
dijkstra3d>=1.15.0
55
fill-voids>=2.0.0
66
edt>=2.1.0
77
fastremap>=1.10.2

0 commit comments

Comments
 (0)