The Filamentous Fungi Detection Algorithm#
Filamentous fungi grow as sprawling networks of thin, branching hyphae that radiate from a dense inoculation point. Standard threshold detectors fail on this morphology for three reasons:
No single intensity cutoff captures both the dense colony centre and the thin peripheral hyphae.
Hyphae frequently appear fragmented in imaging: gaps in signal break what is biologically one colony into many disconnected pieces.
Neighbouring colonies interleave their hyphae and must still be individually labelled.
FilamentousFungiDetector handles all three in six phases. The class
exposes ~10 user-facing knobs grouped into three tiers:
Scene parameters — describe the sample’s geometry.
Tolerance knobs — describe image quality and species morphology.
Overrides — escape hatches for non-standard imaging.
Plus four hidden class-level tunables (beta, gamma, gauss_n_iter,
delta, pct_n_orient) that are fixed to robust defaults. Override
only via subclassing if you really need to.
Pipeline Overview#
Phase 1: Inoculum Detection ──▶ dense colony centres
Phase 2: Branch Detection ──▶ binary mask of all hyphal pixels
Phase 3: Centre Filtering + ──▶ initial colony labels
Voronoi Partition (connectivity not yet enforced)
Phase 4: Dijkstra Reconnection ──▶ fragments routed back to centres
Phase 5: Final Voronoi ──▶ final colony labels
Every hyphal pixel ends up labelled with its parent colony, respecting both physical connectivity and spatial proximity.
Phase 1: Inoculum Detection#
Identify the dense colony centres first. These are the high-confidence
anchors around which the rest of the algorithm operates — they determine
both the Voronoi seeds and the Dijkstra source pixels. It’s recommended if your
image is uniform enough to instead use ManualGridDetector, as it provides
guaranteed detection where you expect. InoculumDetector generally works, but can
fail depending on noise in your scene
Parameters:
Param |
Role |
|---|---|
|
ObjectDetector or ImagePipeline for centres. Defaults to |
Phase 2: Branch Detection#
A deliberately sensitive detector captures every plausible hyphal pixel, intentionally overestimating so that nothing real gets missed. False positives are filtered in later phases.
Two signals are combined:
Background-subtracted Gaussian response —
SubtractGaussianwith σ sized to blur out the whole colony, leaving just the residual signal.Phase congruency — an illumination-invariant edge response that highlights thin filaments even when their absolute intensity is low [1]. Hyphae are line-like; phase congruency is a line-sensitive feature detector.
The two signals are max-merged, then thresholded with a hysteresis detector (triangle low, Otsu high) to produce a binary branch mask.
Parameters:
Param |
Role |
Default |
|---|---|---|
|
Phase congruency noise floor. ↑ = stricter (rejects more pixels as noise; preserves fewer thin hyphae). |
6.0 |
|
If True, drops branch components touching the image border. |
True |
|
SubtractGaussian σ. Auto-derived from |
— |
|
Log-Gabor minimum wavelength. Auto-derived from |
— |
Hidden tunables: pct_n_orient = 8 (angular resolution for PCT),
gauss_n_iter = 2 (background-subtraction iterations).
Phase 3: Centre Filtering & Initial Voronoi Partition#
The inoculum centroids seed a Euclidean Voronoi tessellation that assigns every branch-mask pixel to its nearest centre.
First, branch components that don’t overlap any inoculum centre are dropped — debris and noise that happened to pass Phase 2 but don’t correspond to a real colony. Then centroids of the remaining overlapping components become Voronoi seeds.
The result is an initial colony label map: every mask pixel has a colony ID, but pixels within a given colony aren’t necessarily physically connected yet. That’s what Phase 4 fixes.
Parameters: none directly — uses the Phase 1 centroids and the Phase 2 mask.
Phase 4: Dijkstra Reconnection#
This is the algorithm’s heavy lifting. “Pseudo-fragment” components (assigned to a colony by Voronoi proximity but not physically touching the main body) are bridged back via low-cost paths through the phase congruency response.
4a. Cost surface construction#
Each pixel’s routing cost combines four features:
Phase congruency energy — paths prefer high-PCT-energy (signal-rich) pixels.
Anisotropy (weighted by
β) — rewards directional (line-like) regions. Anisotropy appears in the denominator of the cost formula, so high anisotropy lowers cost; isotropic/non-directional pixels therefore appear expensive by comparison.Orientation coherence — rewards contiguous oriented structure.
Local MAD (weighted by
γ) — penalises noisy/textured regions.
Then two post-assembly penalties are added:
Gap penalty (
gap_crossing_penalty) — penalises paths that cross low-PCT-energy gaps proportional to the gap length.Border penalty (
border_margin_px) — inflates cost within this many pixels of the image border, preventing shortcut paths that hug edges.
Parameters:
Param |
Role |
Default |
|---|---|---|
|
Strength of the distance-gap penalty. ↑ = harder to cross low-energy gaps. |
4.0 |
|
Width of the border penalty buffer. |
50 |
|
Local MAD window. Auto-derived from |
— |
|
Orientation coherence window. Auto-derived from |
— |
Hidden tunables: beta = 2.0 (anisotropy exponent), gamma = 1.2
(MAD penalty weight).
4b. Fragment pre-screening#
Fragments stranded far from any routable territory are dropped before
Dijkstra runs — no point wasting compute on hopeless cases. A minimum
filter of radius frag_reach_px precomputes the lowest cost within
that 2D neighbourhood of every pixel. A fragment passes if at least
one of its boundary pixels sees a filtered value below tau_screen —
a threshold calibrated from known-good colony boundaries (specifically,
the 99th percentile of their min-cost envelope distribution), not an
absolute cutoff.
Parameters:
Param |
Role |
Default |
|---|---|---|
|
Max 2D distance from fragment boundary to the nearest routable pixel. |
10 |
4c. Tiled multi-source Dijkstra#
For memory efficiency, the image is split into overlapping tiles. Each tile runs a multi-source Dijkstra whose wavefront is seeded from the boundary pixels of every colony body: all colony pixels (interior and boundary) are pre-initialised to cost zero, but interior pixels are marked visited immediately and never enter the heap, so propagation starts cleanly from the boundary outward. The result is shortest-cost paths from every pseudo-fragment back to its nearest main colony body.
Tile size must fit several colonies with routing headroom; tile overlap must exceed the maximum reconnection distance so fragments near a tile boundary can still see their parent.
Parameters:
Param |
Role |
Default |
|---|---|---|
|
Tile side length. Auto-derived from |
— |
|
Overlap between adjacent tiles. Auto-derived from |
— |
Hidden tunable: delta = 1.0 — radial retreat penalty that biases
Dijkstra against backtracking through already-visited territory.
4d. Path quality filtering#
Not every Dijkstra path is accepted. A quality-filter cascade compares each candidate against a distribution of known-good colony-skeleton branches (extracted from the main colony bodies). Five metrics per path:
Median raw cost along the path (is it consistently cheap?).
Max of windowed median costs — the cost surface is sampled along the path; a sliding window of length
max_gap_lengthcomputes the median cost within each window; the maximum of those per-window medians becomes the metric. A single-pixel cost spike is absorbed by the median, but a sustained bad stretch of length ≥max_gap_lengthdrives the windowed median up and flags the path.Band cost variance — noisiness of the dilated band around the path.
PCT energy band median — average signal level along the path.
Grayscale SNR — local contrast vs. background.
Thresholds for each metric are set using reconnection_tolerance as an
IQR multiplier on the known-good distribution.
Parameters:
Param |
Role |
Default |
|---|---|---|
|
Sliding window length for the “bad stretch” detection. Longer ⇒ tolerates longer gaps. |
30 |
|
IQR multiplier for acceptance thresholds. ↑ = more permissive. |
2.5 |
|
Path thickness for band sampling. Auto-derived from |
— |
|
Background-ring offset for SNR filter. Auto-derived from |
— |
Paths that survive the cascade get painted into the colony label map with their assigned colony ID; the fragment pixels plus the dilated reconnection path are labelled together.
Phase 5: Final Voronoi Partition#
With reconnected fragments now labelled, a final Voronoi partition combines the Phase 3 filtered branch mask (components that overlapped an inoculum centre) with all painted reconnection paths from Phase 4. This ensures every foreground pixel gets a colony label that’s consistent with physical connectivity after reconnection, and that connected components don’t end up split across colony labels.
Parameters: none directly.
Parameter Tuning Guide#
The knobs fall into four tuning tiers. Work top-down: get scene parameters right first, then adjust tolerances if results look wrong, only touch overrides as a last resort.
Tier 1: Always tune first (scene parameters)#
These describe your sample. Wrong values here cascade into wrong derived values for ~8 other parameters.
Param |
How to set it |
|---|---|
|
Measure the largest colony radius you expect in pixels. Too small ⇒ σ fails to blur out colonies, background subtraction breaks; too large ⇒ wasted memory on oversized tiles. |
|
Measure the narrowest hyphal width you want to resolve. Too small ⇒ MAD/dilation windows shrink below Nyquist, missing real hyphae; too large ⇒ wavelength and windows merge neighbouring branches. |
Tier 2: Tune based on image quality and species#
These depend on your imaging conditions and organism. Start at defaults, adjust if Phase 2 results look wrong (missing real hyphae / too much noise) or Phase 4 reconnection behaves poorly.
Param |
When to raise |
When to lower |
|---|---|---|
|
Clean, high-contrast images with obvious hyphae |
Dim or noisy images where real hyphae are being lost in Phase 2 |
|
Legitimate fragments are not getting reconnected (permissiveness needed) |
Obvious over-merging of nearby colonies (strictness needed) |
|
Species with long sparse hyphal segments |
Over-merging colonies across short gaps |
Tier 3: Usually fine at defaults (spatial tolerances)#
Sensible defaults cover most scenes. Touch only for specific failures.
Param |
Default |
Typical reason to change |
|---|---|---|
|
|
Set |
|
|
Raise if legitimate but distant fragments are being dropped before Dijkstra. |
|
|
Lower if image edges carry useful signal; raise for noisy edges. |
|
|
Lower (2.0) if gap-crossing paths are suppressed too aggressively; raise for very clean images where gaps truly mean absence. |
Tier 4: Overrides (advanced; rarely needed)#
If scene knobs capture your geometry well, auto-derivation works. Only override when you have non-standard imaging (anisotropic pixels, unusual magnification) or you’re reproducing a pipeline with exact fixed values.
Override |
Derived from |
Formula |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Experimental tuning: what the defaults assume#
The defaults are calibrated for:
Plates imaged at a typical dissecting-microscope resolution.
Colony radii roughly 50–300 px.
Hyphal widths of 2–6 px.
Moderate imaging noise (clean but not studio-quality).
If your setup differs substantially from these — imaging at very high magnification, unusual organisms with atypical aspect ratios, or phase-contrast optics with ringing artefacts — expect to iterate. Specifically:
High-resolution imaging (w > 6): raise
max_gap_lengthandfrag_reach_pxproportionally.Low-contrast / dim hyphae: lower
edge_noise_threshold, raisereconnection_tolerance.Very dense plates with interleaved colonies: lower
reconnection_tolerance, raisegap_crossing_penalty. Accept a few extra fragments in exchange for cleaner separation.
Preprocessing Requirements#
The detector works best with upstream denoising and illumination correction:
StableDenoise(BM3D) — removes Poisson-Gaussian noise without destroying thin filaments.HomomorphicFilter— corrects uneven illumination so phase congruency isn’t driven by intensity gradients.
The FilamentousFungiPipeline prefab chains these automatically.
Quick Reference#
from phenotypic.detect import FilamentousFungiDetector
detector = FilamentousFungiDetector(
# Scene: describe your sample
max_colony_radius_px=250, # tier 1 — always tune
min_branch_width_px=3, # tier 1 — always tune
# Tolerances: describe your image quality
edge_noise_threshold=6.0, # tier 2 — tune if Phase 2 looks wrong
reconnection_tolerance=2.5, # tier 2 — tune if Phase 4 over/under-merges
max_gap_length=30, # tier 2 — species-dependent
# Spatial tolerances (usually fine)
ignore_borders=True, # tier 3
frag_reach_px=10, # tier 3
border_margin_px=50, # tier 3
gap_crossing_penalty=4.0, # tier 3
)
References#
[1] P. Kovesi, “Image features from phase congruency,” Videre: J. Computer Vision Research, vol. 1, no. 3, pp. 1–26, 1999.
[2] A. F. Frangi et al., “Multiscale vessel enhancement filtering,” in MICCAI, 1998, pp. 130–137.