File size: 14,660 Bytes
841fe6d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47b72e2
 
 
 
841fe6d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ad49283
 
 
 
 
 
 
841fe6d
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
---
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