| --- |
| license: apache-2.0 |
| library_name: pytorch |
| pipeline_tag: image-segmentation |
| tags: |
| - image-segmentation |
| - pytorch |
| - unet |
| - fungal-colony |
| - petri-dish |
| - morphometry |
| - magnaporthe |
| - area-consistency |
| language: |
| - en |
| --- |
| |
| # 🔬 Gray Leaf Spot Colony Segmentation — Demo Pipeline |
|
|
| [](https://huggingface.co/rotsl/grayleafspot-segmentation-demo) |
| [](https://opensource.org/licenses/Apache-2.0) |
| [](https://doi.org/10.57967/hf/8569) |
|
|
| 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](https://huggingface.co/spaces/rotsl/grayleafspot-segmentation-demo)** — upload images, run inference, see overlays & 16 growth charts in your browser. |
|
|
| --- |
|
|
| ## Model |
|
|
| **Weights:** [`rotsl/grayleafspot-segmentation/best_area_w_0.7.pt`](https://huggingface.co/rotsl/grayleafspot-segmentation) |
|
|
| | 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](https://www.gradio.app/docs/python-client/introduction) without |
| installing anything locally. |
|
|
| ### Install |
|
|
| ```bash |
| pip install gradio_client |
| ``` |
|
|
| ### Quick Start — Upload + Run Pipeline |
|
|
| ```python |
| 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.zip |
| |
| print(status_msg) |
| print(f"Overlays: {len(overlays)} panels") |
| print(f"Charts: {len(charts)}") |
| print(f"Download: {zip_path}") |
| ``` |
|
|
| ### Export Metadata Only (no inference) |
|
|
| ```python |
| 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 |
|
|
| ```python |
| """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 DataFrame |
| 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 |
|
|
| ```r |
| library(readr) |
| library(dplyr) |
| library(ggplot2) |
| |
| df <- read_csv("analysis_full.csv") |
| |
| # Growth curve |
| 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() |
| |
| # Morphology summary |
| 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) |
| ) |
| |
| # RGR |
| 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() |
| ``` |
|
|
| ```r |
| 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 |
|
|
| 1. Resize full image to 256 × 256 → SmallUNet → sigmoid probability map |
| 2. Threshold at user-configurable confidence level (default 0.5) |
| 3. 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 |
|
|
| ```bibtex |
| @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 |
|
|