--- 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 [![Downloads](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fhuggingface.co%2Fapi%2Fmodels%2Frotsl%2Fgrayleafspot-segmentation-demo&query=%24.downloads&label=%F0%9F%A4%97%20Downloads%20%2830d%29&color=blue)](https://huggingface.co/rotsl/grayleafspot-segmentation-demo) [![License](https://img.shields.io/badge/License-Apache%202.0-green.svg)](https://opensource.org/licenses/Apache-2.0) [![DOI](https://img.shields.io/badge/DOI-10.57967%2Fhf%2F8569-orange)](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