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.
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:
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")
# Step 1: Upload images
result = client.predict(
files=[
handle_file("plate_d01.jpg"),
handle_file("plate_d03.jpg"),
handle_file("plate_d05.jpg"),
],
api_name="/on_upload",
)
# Step 2: Run the full analysis pipeline
analysis = client.predict(
en="GLS_Exp01", # experiment name
ed="2026-04-01", # experiment start date
un="YourName", # user name
pc=1, # plates count
thresh=0.5, # mask confidence threshold
full_pipeline=True, # enable full morphometrics
api_name="/on_run",
)
status_msg = analysis[0]
overlays = analysis[1] # list of {image: filepath, caption: str}
charts = analysis[2] # list of {image: filepath, caption: str}
results_table = analysis[3] # {"headers": [...], "data": [[...], ...]}
zip_path = analysis[4] # local path to downloaded analysis_full.zipprint(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",
)
# meta[0] = status message# meta[1] = metadata dataframe# meta[2] = path to image_metadata.zip
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")
# Collect all images
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")
# Upload
client.predict(
files=[handle_file(str(p)) for p in images],
api_name="/on_upload",
)
# Run pipeline
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}")
# Access results as a DataFrameimport pandas as pd
df = pd.DataFrame(table["data"], columns=table["headers"])
print(df[["image_path", "area_mm2", "diameter_mm", "crack_coverage_pct"]].to_string())
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