π Fungal Colony Image Analysis Pipeline

End-to-end analysis pipeline for Magnaporthe (and other fungal) colony
morphometry on 90 mm petri-dish images.
βΆ Try the live demo β upload images, run inference, see overlays & growth charts in your browser.
Designed for Apple Silicon Mac (M1/M2/M3 Pro/Max, MPS backend, float32).
Also works on CPU (Linux/Windows).
Model
Weights: rotsl/grayleafspot-segmentation/grayleafspot.pt
| Property |
Value |
| Architecture |
smp.Unet(encoder_name="resnet34") via segmentation-models-pytorch |
| Parameters |
24.4M |
| Input |
256Γ256 RGB |
| Output |
1-channel sigmoid mask (threshold 0.5) |
| Dish detection |
OpenCV HoughCircles on Gaussian-blurred grayscale |
| MPS compatible |
β
Pure PyTorch β no custom CUDA kernels |
Pipeline Overview
input.py pipeline.py / app.py (Space)
ββββββββββββββββββββ ββββββββββββββββββββββββββββββββββββββββ
β Gradio GUI β β Read image_metadata.csv β
β - Scan folder β image_metadata. β Load smp.Unet (thread-local) β
β - Tag metadata βββββ csv/json ββββββΆβ For each image: β
β - Export CSV β β 1. OpenCV HoughCircles β dish β
β - Export ICS β β 2. U-Net β colony mask β
ββββββββββββββββββββ β 3. Crack detection (adaptive) β
β 4. Hyphae (Frangi + Meijering) β
β 5. Morphometrics (mm/mmΒ²) β
β 6 overlay panels per image β
β Growth charts (β₯2 images) β
β Output: analysis_full.csv/json β
ββββββββββββββββββββββββββββββββββββββββ
Visualisation Outputs
6 Overlay Panels Per Image
| Panel |
Colour |
Shows |
| Raw + Dish |
Green circle, red contour |
Detected dish boundary + colony outline |
| Colony Mask |
White on black |
Binary segmentation mask |
| Colony Overlay |
Red 50% blend |
Colony area highlighted on raw image |
| Cracks |
Yellow |
Detected cracks inside colony (dilated for visibility) |
| Hyphae |
Cyan |
Hyphae skeleton (Frangi + Meijering hybrid filter) |
| All Combined |
Red + yellow + cyan |
Colony + cracks + hyphae together |
Growth Charts (when β₯2 images)
- Colony Area (mmΒ²) vs days β with fill + data labels
- Diameter (mm) over time
- Relative Growth Rate (RGR) β bar chart per interval
- Crack Coverage (%) over time
- Hyphae Network Length (mm) over time
- Morphology β eccentricity + edge roughness dual panel
All charts are included as PNGs in the download zip.
Installation (Local)
Prerequisites
- Python 3.10+
- macOS with Apple Silicon recommended (MPS acceleration) β also works on CPU
- ~100 MB disk for model weights (cached in
~/.cache/huggingface)
Setup
git clone https://huggingface.co/rotsl/fungal-colony-pipeline
cd fungal-colony-pipeline
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
python -c "
from huggingface_hub import hf_hub_download
p = hf_hub_download('rotsl/grayleafspot-segmentation', 'grayleafspot.pt')
print(f'Downloaded to: {p}')
"
Usage (Local)
Step 1: Tag Images with Metadata
python input.py
- Open
http://localhost:7860
- Paste image folder path β π Scan
- Fill experiment details (name, start date, user, plates count)
- Click thumbnails β edit per-image dates β πΎ Save
- π₯ Export β writes
image_metadata.csv, .json, reminders.ics to your folder
Step 2: Run Analysis
IMG_DIR=./data python pipeline.py
| Variable |
Default |
Description |
IMG_DIR |
. |
Folder with image_metadata.csv + images |
MAX_WORKERS |
2 |
Parallel threads (hard cap 2 for 16 GB) |
MODEL_REPO |
rotsl/grayleafspot-segmentation |
HF model repo |
MODEL_FILE |
grayleafspot.pt |
Weight file |
Outputs: analysis_full.csv + analysis_full.json in IMG_DIR
Usage via HF API (Programmatic Access)
You can run the full pipeline remotely via the
Gradio Client without
installing anything locally. The Space exposes five API endpoints.
Install
pip install gradio_client
Quick Start β Upload + Run Pipeline
from gradio_client import Client, handle_file
client = Client("rotsl/fungal-colony-input")
result = client.predict(
files=[
handle_file("plate_d01.jpg"),
handle_file("plate_d03.jpg"),
handle_file("plate_d05.jpg"),
],
api_name="/on_upload",
)
analysis = client.predict(
en="MagExp01",
ed="2025-04-01",
un="YourName",
pc=1,
api_name="/on_run",
)
status_msg = analysis[0]
overlays = analysis[1]
charts = analysis[2]
results_table = analysis[3]
zip_path = analysis[4]
print(status_msg)
print(f"Overlays: {len(overlays)} panels")
print(f"Charts: {len(charts)}")
print(f"Results: {len(results_table['data'])} rows Γ {len(results_table['headers'])} cols")
print(f"Download: {zip_path}")
Export Metadata Only (no inference)
meta = client.predict(
en="MagExp01",
ed="2025-04-01",
un="YourName",
pc=1,
api_name="/on_export",
)
Available API Endpoints
| Endpoint |
Description |
Key Parameters |
/on_upload |
Upload images β gallery |
files: list of filepaths |
/on_sel |
Select image in gallery |
ed: experiment date |
/on_save |
Save per-image date/reminder |
nd: date, nr: reminder, ed: exp date |
/on_export |
Export metadata CSV/JSON/ICS |
en, ed, un, pc |
/on_run |
Run full pipeline (segmentation + morphometrics + charts) |
en, ed, un, pc |
cURL Example
curl -X POST https://rotsl-fungal-colony-input.hf.space/gradio_api/call/on_upload \
-H "Content-Type: application/json" \
-d '{
"data": [
[{"path": "https://your-server.com/plate_d01.jpg"}]
]
}'
Note: For multi-step workflows (upload β run), use the Python gradio_client
which handles session state automatically. Direct REST calls require managing
the session hash between requests.
Batch Processing Script
"""Process a folder of petri dish images via the HF Space API."""
from pathlib import Path
from gradio_client import Client, handle_file
IMAGE_DIR = Path("./my_experiment")
EXPERIMENT = "MagExp01"
START_DATE = "2025-04-01"
client = Client("rotsl/fungal-colony-input")
images = sorted(
p for p in IMAGE_DIR.rglob("*")
if p.suffix.lower() in {".jpg", ".jpeg", ".png", ".tif", ".bmp", ".webp"}
)
print(f"Found {len(images)} images")
client.predict(
files=[handle_file(str(p)) for p in images],
api_name="/on_upload",
)
status, overlays, charts, table, zip_path = client.predict(
en=EXPERIMENT,
ed=START_DATE,
un="BatchUser",
pc=1,
api_name="/on_run",
)
print(status)
print(f"Results zip: {zip_path}")
import pandas as pd
df = pd.DataFrame(table["data"], columns=table["headers"])
print(df[["image_path", "area_mm2", "diameter_mm", "crack_coverage_pct"]].to_string())
Output Columns
Metadata
| Column |
Description |
image_path |
Relative path from IMG_DIR |
experiment_name |
Experiment identifier |
experiment_date |
Start date (YYYY-MM-DD) |
image_date |
Auto-detected capture date |
day_code |
d01, d02, β¦ |
user_name |
Researcher |
plates_count |
Number of plates |
Calibration
| Column |
Unit |
Description |
dish_cx, dish_cy |
px |
Dish centre |
dish_radius_px |
px |
Dish radius |
px_to_mm |
mm/px |
Scale factor |
calibration_diameter_mm |
mm |
Should be β90.0 |
calibration_error_pct |
% |
Target <2% |
Colony Morphometry
| Column |
Unit |
Description |
area_mm2 |
mmΒ² |
Colony area |
diameter_mm |
mm |
Equivalent circular diameter |
perimeter_mm |
mm |
Colony perimeter |
eccentricity |
β |
0=circle, 1=line |
edge_roughness |
β |
Perimeter / equivalent circle perimeter |
centre_delta_mm |
mm |
Colony centre to dish centre |
Texture
| Column |
Description |
entropy |
Shannon entropy |
texture_std |
Pixel intensity Ο |
Cracks
| Column |
Unit |
Description |
crack_px |
px |
Total crack pixels |
crack_area_mm2 |
mmΒ² |
Total crack area |
crack_coverage_pct |
% |
Crack / colony area Γ 100 |
crack_count |
β |
Distinct crack count |
Hyphae
| Column |
Unit |
Description |
hyph_frangi_mm |
mm |
Frangi vesselness skeleton length |
hyph_meijering_mm |
mm |
Meijering neuriteness skeleton length |
hyph_hybrid_mm |
mm |
Union of both |
Time-Series
| Column |
Unit |
Description |
days_since_start |
days |
From first image |
rgr_per_day |
dayβ»ΒΉ |
(ln Aβ β ln Aβ) / Ξdays |
relative_growth_per_day |
mmΒ²/day |
(Aβ β Aβ) / Ξdays |
R Studio Integration
library(readr)
library(dplyr)
library(ggplot2)
df <- read_csv("analysis_full.csv")
df %>%
filter(is.na(error) | error == "") %>%
ggplot(aes(x = days_since_start, y = area_mm2, color = experiment_name)) +
geom_line() + geom_point() +
labs(x = "Days", y = "Colony Area (mmΒ²)", title = "Magnaporthe Growth") +
theme_minimal()
df %>%
filter(is.na(error) | error == "") %>%
group_by(experiment_name) %>%
summarise(
n = n(),
mean_area = mean(area_mm2, na.rm = TRUE),
mean_roughness = mean(edge_roughness, na.rm = TRUE),
mean_crack_pct = mean(crack_coverage_pct, na.rm = TRUE),
total_hyphae = sum(hyph_hybrid_mm, na.rm = TRUE)
)
df %>%
filter(!is.na(rgr_per_day) & rgr_per_day != "") %>%
mutate(rgr_per_day = as.numeric(rgr_per_day)) %>%
ggplot(aes(x = days_since_start, y = rgr_per_day)) +
geom_col(fill = "steelblue") +
facet_wrap(~ experiment_name) +
labs(x = "Days", y = "RGR (dayβ»ΒΉ)") +
theme_minimal()
library(jsonlite)
df <- fromJSON("analysis_full.json")
meta <- read_csv("image_metadata.csv")
Technical Notes
Segmentation Strategy
- Dish detection: OpenCV
GaussianBlur β HoughCircles (HOUGH_GRADIENT, dp=1.2)
- Colony segmentation: Resize full image to 256Γ256 β
smp.Unet(resnet34) β sigmoid β threshold 0.5
- Resize mask back to original resolution (nearest-neighbour)
- Restrict to dish interior (95% of detected radius)
- Cleanup: OpenCV morphological close/open, keep largest connected component
Crack Detection
- Local adaptive thresholding inside colony mask
- Filter by elongation (aspect ratio > 2.5 or eccentricity > 0.85)
- Edge artefacts removed via erosion
Hyphae Detection
- Frangi filter: multi-scale vesselness (Ο = 1β4)
- Meijering filter: neuriteness (Ο = 1β4)
- Hybrid: union of both skeletonised responses
- Analysis region extends 20 px beyond colony boundary
Memory Management
- Max 2 parallel threads β prevents OOM on 16 GB
torch.mps.empty_cache() after each image
- Thread-local model loading
- Float32 throughout
File Structure
your_image_folder/
βββ subdir_a/
β βββ mag01_20250401_01.jpg
β βββ mag01_20250402_01.jpg
βββ image_metadata.csv β input.py
βββ image_metadata.json β input.py
βββ reminders.ics β input.py (if reminders)
βββ analysis_full.csv β pipeline.py
βββ analysis_full.json β pipeline.py
Troubleshooting
| Issue |
Fix |
torch.mps not available |
macOS 13+ and PyTorch 2.1+ required |
| OOM on 16 GB |
MAX_WORKERS=1 |
| Model download fails |
Check internet + HF_TOKEN for gated repo |
| Dish not detected |
Full rim must be visible, avoid heavy shadows |
| Colony not detected |
Verify image has visible colony contrast against agar |
Citation
@misc{rohan_r_2026,
author = { rohan r },
title = { fungal-colony-pipeline (Revision e51373b) },
year = 2026,
url = { https://huggingface.co/rotsl/fungal-colony-pipeline },
doi = { 10.57967/hf/8570 },
publisher = { Hugging Face }
}
License
Apache License 2.0