0
# Post-processing and Quality Enhancement
1
2
Post-processing functions improve skeleton quality by removing artifacts, joining disconnected components, eliminating loops, and trimming small branches. These functions are essential for preparing skeletons for visualization and analysis.
3
4
## Imports
5
6
```python
7
from typing import Sequence
8
from osteoid import Skeleton
9
```
10
11
## Capabilities
12
13
### Comprehensive Post-processing
14
15
Applies a complete post-processing pipeline to clean and improve skeleton quality.
16
17
```python { .api }
18
def postprocess(
19
skeleton: Skeleton,
20
dust_threshold: float = 1500.0,
21
tick_threshold: float = 3000.0
22
) -> Skeleton:
23
"""
24
Apply comprehensive post-processing to improve skeleton quality.
25
26
The following steps are applied in order:
27
1. Remove disconnected components smaller than dust_threshold
28
2. Remove loops (skeletons should be trees)
29
3. Join close components within radius threshold
30
4. Remove small branches (ticks) smaller than tick_threshold
31
32
Parameters:
33
- skeleton (Skeleton): Input skeleton to process
34
- dust_threshold (float): Remove components smaller than this (physical units)
35
- tick_threshold (float): Remove branches smaller than this (physical units)
36
37
Returns:
38
Skeleton: Cleaned and improved skeleton
39
"""
40
```
41
42
#### Usage Example
43
44
```python
45
import kimimaro
46
47
# Generate initial skeletons
48
skeletons = kimimaro.skeletonize(labels, anisotropy=(16, 16, 40))
49
50
# Post-process each skeleton
51
cleaned_skeletons = {}
52
for label_id, skeleton in skeletons.items():
53
cleaned_skeletons[label_id] = kimimaro.postprocess(
54
skeleton,
55
dust_threshold=2000, # Remove components < 2000 nm
56
tick_threshold=5000 # Remove branches < 5000 nm
57
)
58
59
# The cleaned skeletons have:
60
# - No small disconnected pieces
61
# - No loops (proper tree structure)
62
# - Connected nearby components
63
# - No tiny branches/artifacts
64
```
65
66
### Component Joining
67
68
Connects nearby skeleton components by finding the closest vertices and linking them, useful for joining skeletons from adjacent image chunks or reconnecting artificially separated components.
69
70
```python { .api }
71
def join_close_components(
72
skeletons: Sequence[Skeleton],
73
radius: float = float('inf'),
74
restrict_by_radius: bool = False
75
) -> Skeleton:
76
"""
77
Join nearby skeleton components within specified distance.
78
79
Given a set of skeletons which may contain multiple connected components,
80
attempts to connect each component to the nearest other component via
81
the nearest two vertices. Repeats until no components remain or no
82
points closer than radius are available.
83
84
Parameters:
85
- skeletons (list or Skeleton): Input skeleton(s) to process
86
- radius (float): Maximum distance for joining components (default: infinity)
87
- restrict_by_radius (bool): If True, only join within boundary distance radius
88
89
Returns:
90
Skeleton: Single connected skeleton with components joined
91
"""
92
```
93
94
#### Usage Example
95
96
```python
97
import kimimaro
98
from osteoid import Skeleton
99
100
# Join multiple skeletons from the same neuron
101
skeleton_chunks = [skel1, skel2, skel3] # From adjacent image volumes
102
103
# Join all components without distance restriction
104
merged_skeleton = kimimaro.join_close_components(skeleton_chunks)
105
106
# Join only components within 1500 nm of each other
107
selective_merge = kimimaro.join_close_components(
108
skeleton_chunks,
109
radius=1500,
110
restrict_by_radius=True
111
)
112
113
# Join components of a single fragmented skeleton
114
fragmented_skeleton = skeletons[neuron_id]
115
connected_skeleton = kimimaro.join_close_components([fragmented_skeleton])
116
```
117
118
## Advanced Post-processing Workflow
119
120
For complex skeletonization tasks, you may want to apply post-processing steps individually:
121
122
```python
123
import kimimaro
124
125
# Get initial skeleton
126
skeleton = skeletons[target_neuron_id]
127
128
# Step 1: Remove small artifacts first
129
clean_skeleton = kimimaro.postprocess(
130
skeleton,
131
dust_threshold=1000, # Conservative dust removal
132
tick_threshold=0 # Skip tick removal for now
133
)
134
135
# Step 2: Join components from different image chunks
136
if len(skeleton_chunks) > 1:
137
joined_skeleton = kimimaro.join_close_components(
138
skeleton_chunks,
139
radius=2000, # 2 micron joining threshold
140
restrict_by_radius=True
141
)
142
else:
143
joined_skeleton = clean_skeleton
144
145
# Step 3: Final cleanup with aggressive tick removal
146
final_skeleton = kimimaro.postprocess(
147
joined_skeleton,
148
dust_threshold=2000, # More aggressive dust removal
149
tick_threshold=3000 # Remove small branches
150
)
151
152
# Step 4: Validate result
153
print(f"Original vertices: {len(skeleton.vertices)}")
154
print(f"Final vertices: {len(final_skeleton.vertices)}")
155
print(f"Connected components: {final_skeleton.n_components}")
156
```
157
158
## Quality Assessment
159
160
After post-processing, you can assess skeleton quality:
161
162
```python
163
# Check connectivity
164
n_components = skeleton.n_components
165
if n_components == 1:
166
print("Skeleton is fully connected")
167
else:
168
print(f"Skeleton has {n_components} disconnected components")
169
170
# Check for loops (should be 0 for proper tree structure)
171
n_cycles = len(skeleton.cycles())
172
if n_cycles == 0:
173
print("Skeleton is a proper tree")
174
else:
175
print(f"Warning: Skeleton has {n_cycles} loops")
176
177
# Measure total cable length
178
total_length = skeleton.cable_length()
179
print(f"Total cable length: {total_length:.1f} physical units")
180
181
# Check branch complexity
182
branch_points = len([v for v in skeleton.vertices if len(skeleton.edges[v]) > 2])
183
endpoints = len([v for v in skeleton.vertices if len(skeleton.edges[v]) == 1])
184
print(f"Branch points: {branch_points}, Endpoints: {endpoints}")
185
```
186
187
## Integration with Analysis Pipeline
188
189
Post-processing typically fits into a larger analysis workflow:
190
191
```python
192
import kimimaro
193
import numpy as np
194
195
# 1. Skeletonization
196
labels = np.load("segmentation.npy")
197
skeletons = kimimaro.skeletonize(
198
labels,
199
anisotropy=(16, 16, 40),
200
parallel=4,
201
progress=True
202
)
203
204
# 2. Post-processing
205
cleaned_skeletons = {}
206
for label_id, skeleton in skeletons.items():
207
cleaned_skeletons[label_id] = kimimaro.postprocess(
208
skeleton,
209
dust_threshold=1500,
210
tick_threshold=3000
211
)
212
213
# 3. Cross-sectional analysis
214
analyzed_skeletons = kimimaro.cross_sectional_area(
215
labels,
216
cleaned_skeletons,
217
anisotropy=(16, 16, 40),
218
smoothing_window=5
219
)
220
221
# 4. Export results
222
for label_id, skeleton in analyzed_skeletons.items():
223
with open(f"neuron_{label_id}.swc", "w") as f:
224
f.write(skeleton.to_swc())
225
```
226
227
## Common Post-processing Issues
228
229
### Over-aggressive Dust Removal
230
- **Problem**: Important small structures removed
231
- **Solution**: Use lower dust_threshold or inspect removed components
232
233
### Incomplete Component Joining
234
- **Problem**: Components remain disconnected despite proximity
235
- **Solution**: Check radius parameter, verify component spacing
236
237
### Loop Preservation
238
- **Problem**: Biological loops incorrectly removed
239
- **Solution**: Use custom post-processing instead of full `postprocess()`
240
241
### Branch Over-trimming
242
- **Problem**: Important dendrites removed as "ticks"
243
- **Solution**: Lower tick_threshold or use length-based filtering