rotsl's picture
Add live download count badge via Shields.io
47b72e2 verified
|
raw
history blame
14.7 kB
---
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