π¬ Gray Leaf Spot Colony Segmentation β Demo Pipeline

End-to-end analysis pipeline for gray leaf spot (Magnaporthe and related
fungal) colony morphometry on 90 mm petri-dish images, powered by a lightweight
SmallUNet trained with area-consistency loss (w=0.7).
βΆ Try the live demo β upload images, run inference, see overlays & 16 growth charts in your browser.
Model
Weights: rotsl/grayleafspot-segmentation/best_area_w_0.7.pt
| Property |
Value |
| Architecture |
SmallUNet (custom lightweight U-Net) |
| Parameters |
~250 K |
| Base channels |
16 β 32 β 64 β 128 β 256 (bottleneck) |
| Input |
256 Γ 256 RGB |
| Output |
1-channel sigmoid mask |
| Training loss |
BCE + area-consistency loss (weight = 0.7) |
| Dish detection |
OpenCV HoughCircles on Gaussian-blurred grayscale |
| CPU compatible |
β
Pure PyTorch β no custom CUDA kernels |
SmallUNet Architecture
Input (3 Γ 256 Γ 256)
β
ββ enc1: ConvBlock(3 β 16) βββ skip s1
ββ enc2: MaxPool2d β ConvBlock(16 β 32) βββ skip s2
ββ enc3: MaxPool2d β ConvBlock(32 β 64) βββ skip s3
ββ enc4: MaxPool2d β ConvBlock(64 β 128) βββ skip s4
β
ββ bottleneck: MaxPool2d β ConvBlock(128 β 256)
β
ββ up4: Upsample + cat(s4) β ConvBlock(384 β 128)
ββ up3: Upsample + cat(s3) β ConvBlock(192 β 64)
ββ up2: Upsample + cat(s2) β ConvBlock(96 β 32)
ββ up1: Upsample + cat(s1) β ConvBlock(48 β 16)
β
ββ head: Conv2d(16 β 1) β Sigmoid
Each ConvBlock = Conv3Γ3 (no bias) β ReLU β Conv3Γ3 (no bias) β ReLU.
DownBlock = MaxPool2d(2) β ConvBlock.
UpBlock = Bilinear upsample(Γ2, align_corners=False) β cat([skip, x]) β ConvBlock.
Area-Consistency Weights
The model repo contains variants trained with different area-consistency loss
weights. Higher weights enforce stronger agreement between predicted mask area
and ground-truth polygon area:
| Weight file |
Loss weight |
Description |
best_area_w_0.1.pt |
0.1 |
Light area regularisation |
best_area_w_0.3.pt |
0.3 |
Moderate area regularisation |
best_area_w_0.5.pt |
0.5 |
Balanced BCE + area |
best_area_w_0.7.pt |
0.7 |
Strong area consistency (used by demo) |
grayleafspot.pt |
β |
Main smp.Unet (ResNet-34) model (24.4M params) |
Pipeline Overview
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Gradio Space (rotsl/grayleafspot-segmentation-demo) β
β β
β Upload images β
β ββ Fast mode: SmallUNet β mask β overlay (per image) β
β ββ Full pipeline (per image): β
β 1. OpenCV HoughCircles β dish detection β px_to_mm β
β 2. SmallUNet β colony mask (threshold configurable) β
β 3. Crack detection (adaptive thresholding + morphology) β
β 4. Hyphae detection (Frangi + Meijering + hybrid skeleton) β
β 5. Morphometrics β all in mm/mmΒ² via per-image calibration β
β 6. 6 overlay panels per image β
β 7. 16 growth charts (β₯2 images) β
β 8. Export: analysis_full.csv / .json / .zip β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
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 |
16 Growth Charts (when β₯2 images)
All spatial metrics are in mm (or mmΒ²) via per-image px_to_mm calibration
from dish detection, so images of different resolutions are correctly comparable.
| Category |
Charts |
Units |
| Colony geometry |
Colony Area, Colony Diameter, Colony Perimeter |
mmΒ², mm, mm |
| Shape descriptors |
Eccentricity, Edge Roughness (P/Οd), Colony Centre Offset |
unitless, unitless, mm |
| Texture |
Colony Texture Entropy, Colony Texture Std Dev |
unitless, unitless |
| Cracks |
Crack Area, Crack Coverage, Number of Cracks |
mmΒ², %, count |
| Hyphae |
Hyphae Length β Frangi, Meijering, Hybrid |
mm, mm, mm |
| Growth rates |
Relative Growth Rate (RGR), Absolute Growth Rate |
ln mmΒ²/day, mmΒ²/day |
Charts are only generated when β₯2 valid data points exist for that metric.
All charts are included as PNGs in the download zip.
Usage via HF API (Programmatic Access)
Run the full pipeline remotely via the
Gradio Client without
installing anything locally.
Install
pip install gradio_client
Quick Start β Upload + Run Pipeline
from gradio_client import Client, handle_file
client = Client("rotsl/grayleafspot-segmentation-demo")
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="GLS_Exp01",
ed="2026-04-01",
un="YourName",
pc=1,
thresh=0.5,
full_pipeline=True,
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"Download: {zip_path}")
Export Metadata Only (no inference)
meta = client.predict(
en="GLS_Exp01",
ed="2026-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 + 16 charts) |
en, ed, un, pc, thresh, full_pipeline |
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 = "GLS_Exp01"
START_DATE = "2026-04-01"
client = Client("rotsl/grayleafspot-segmentation-demo")
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,
thresh=0.5,
full_pipeline=True,
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 |
Image filename |
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_detected |
bool |
Whether dish was found |
dish_radius_px |
px |
Dish radius in pixels |
px_to_mm |
mm/px |
Per-image scale factor from dish detection |
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 (local rank filter) |
texture_std |
Pixel intensity standard deviation |
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 = "Gray Leaf Spot 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")
Technical Notes
Per-Image Pixel-to-mm Calibration
Each image gets its own px_to_mm conversion factor derived from dish detection.
The pipeline detects the 90 mm petri dish via HoughCircles and computes:
px_to_mm = 90.0 / (2 Γ dish_radius_px)
This means images of different resolutions (e.g. phone camera vs DSLR vs
microscope) are correctly converted to physical mm units independently.
If dish detection fails for an image, px_to_mm defaults to 1.0 and
dish_detected is set to False.
Segmentation
- Resize full image to 256 Γ 256 β SmallUNet β sigmoid probability map
- Threshold at user-configurable confidence level (default 0.5)
- Resize mask back to original resolution (nearest-neighbour)
Crack Detection
- Local adaptive thresholding (Gaussian, block_size=51) inside colony mask
- Filter by elongation: aspect ratio > 2.5 or eccentricity > 0.85
- Interior erosion (disk radius 5) to remove edge artefacts
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
Troubleshooting
| Issue |
Fix |
| Model download fails |
Check internet; for gated repos set HF_TOKEN |
| Dish not detected |
Full rim must be visible; avoid heavy shadows |
| Colony not detected |
Verify image has visible colony contrast against agar |
px_to_mm = 1.0 |
Dish detection failed β check dish_detected column |
| Charts missing |
Need β₯2 images with valid data for that metric |
Citation
@misc{rohan_r_2026,
author = { rohan r },
title = { grayleafspot-segmentation-demo (Revision d2b8555) },
year = 2026,
url = { https://huggingface.co/rotsl/grayleafspot-segmentation-demo },
doi = { 10.57967/hf/8569 },
publisher = { Hugging Face }
}
License
Apache License 2.0