Skeletonize densely labeled image volumes using TEASAR-derived algorithms for neuroscience and connectomics research.
—
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.
from typing import Sequence
from osteoid import SkeletonApplies a complete post-processing pipeline to clean and improve skeleton quality.
def postprocess(
skeleton: Skeleton,
dust_threshold: float = 1500.0,
tick_threshold: float = 3000.0
) -> Skeleton:
"""
Apply comprehensive post-processing to improve skeleton quality.
The following steps are applied in order:
1. Remove disconnected components smaller than dust_threshold
2. Remove loops (skeletons should be trees)
3. Join close components within radius threshold
4. Remove small branches (ticks) smaller than tick_threshold
Parameters:
- skeleton (Skeleton): Input skeleton to process
- dust_threshold (float): Remove components smaller than this (physical units)
- tick_threshold (float): Remove branches smaller than this (physical units)
Returns:
Skeleton: Cleaned and improved skeleton
"""import kimimaro
# Generate initial skeletons
skeletons = kimimaro.skeletonize(labels, anisotropy=(16, 16, 40))
# Post-process each skeleton
cleaned_skeletons = {}
for label_id, skeleton in skeletons.items():
cleaned_skeletons[label_id] = kimimaro.postprocess(
skeleton,
dust_threshold=2000, # Remove components < 2000 nm
tick_threshold=5000 # Remove branches < 5000 nm
)
# The cleaned skeletons have:
# - No small disconnected pieces
# - No loops (proper tree structure)
# - Connected nearby components
# - No tiny branches/artifactsConnects nearby skeleton components by finding the closest vertices and linking them, useful for joining skeletons from adjacent image chunks or reconnecting artificially separated components.
def join_close_components(
skeletons: Sequence[Skeleton],
radius: float = float('inf'),
restrict_by_radius: bool = False
) -> Skeleton:
"""
Join nearby skeleton components within specified distance.
Given a set of skeletons which may contain multiple connected components,
attempts to connect each component to the nearest other component via
the nearest two vertices. Repeats until no components remain or no
points closer than radius are available.
Parameters:
- skeletons (list or Skeleton): Input skeleton(s) to process
- radius (float): Maximum distance for joining components (default: infinity)
- restrict_by_radius (bool): If True, only join within boundary distance radius
Returns:
Skeleton: Single connected skeleton with components joined
"""import kimimaro
from osteoid import Skeleton
# Join multiple skeletons from the same neuron
skeleton_chunks = [skel1, skel2, skel3] # From adjacent image volumes
# Join all components without distance restriction
merged_skeleton = kimimaro.join_close_components(skeleton_chunks)
# Join only components within 1500 nm of each other
selective_merge = kimimaro.join_close_components(
skeleton_chunks,
radius=1500,
restrict_by_radius=True
)
# Join components of a single fragmented skeleton
fragmented_skeleton = skeletons[neuron_id]
connected_skeleton = kimimaro.join_close_components([fragmented_skeleton])For complex skeletonization tasks, you may want to apply post-processing steps individually:
import kimimaro
# Get initial skeleton
skeleton = skeletons[target_neuron_id]
# Step 1: Remove small artifacts first
clean_skeleton = kimimaro.postprocess(
skeleton,
dust_threshold=1000, # Conservative dust removal
tick_threshold=0 # Skip tick removal for now
)
# Step 2: Join components from different image chunks
if len(skeleton_chunks) > 1:
joined_skeleton = kimimaro.join_close_components(
skeleton_chunks,
radius=2000, # 2 micron joining threshold
restrict_by_radius=True
)
else:
joined_skeleton = clean_skeleton
# Step 3: Final cleanup with aggressive tick removal
final_skeleton = kimimaro.postprocess(
joined_skeleton,
dust_threshold=2000, # More aggressive dust removal
tick_threshold=3000 # Remove small branches
)
# Step 4: Validate result
print(f"Original vertices: {len(skeleton.vertices)}")
print(f"Final vertices: {len(final_skeleton.vertices)}")
print(f"Connected components: {final_skeleton.n_components}")After post-processing, you can assess skeleton quality:
# Check connectivity
n_components = skeleton.n_components
if n_components == 1:
print("Skeleton is fully connected")
else:
print(f"Skeleton has {n_components} disconnected components")
# Check for loops (should be 0 for proper tree structure)
n_cycles = len(skeleton.cycles())
if n_cycles == 0:
print("Skeleton is a proper tree")
else:
print(f"Warning: Skeleton has {n_cycles} loops")
# Measure total cable length
total_length = skeleton.cable_length()
print(f"Total cable length: {total_length:.1f} physical units")
# Check branch complexity
branch_points = len([v for v in skeleton.vertices if len(skeleton.edges[v]) > 2])
endpoints = len([v for v in skeleton.vertices if len(skeleton.edges[v]) == 1])
print(f"Branch points: {branch_points}, Endpoints: {endpoints}")Post-processing typically fits into a larger analysis workflow:
import kimimaro
import numpy as np
# 1. Skeletonization
labels = np.load("segmentation.npy")
skeletons = kimimaro.skeletonize(
labels,
anisotropy=(16, 16, 40),
parallel=4,
progress=True
)
# 2. Post-processing
cleaned_skeletons = {}
for label_id, skeleton in skeletons.items():
cleaned_skeletons[label_id] = kimimaro.postprocess(
skeleton,
dust_threshold=1500,
tick_threshold=3000
)
# 3. Cross-sectional analysis
analyzed_skeletons = kimimaro.cross_sectional_area(
labels,
cleaned_skeletons,
anisotropy=(16, 16, 40),
smoothing_window=5
)
# 4. Export results
for label_id, skeleton in analyzed_skeletons.items():
with open(f"neuron_{label_id}.swc", "w") as f:
f.write(skeleton.to_swc())postprocess()Install with Tessl CLI
npx tessl i tessl/pypi-kimimaro