rotsl commited on
Commit
841fe6d
·
verified ·
1 Parent(s): 90d94bd

Add comprehensive model card for grayleafspot-segmentation-demo

Browse files
Files changed (1) hide show
  1. README.md +446 -0
README.md ADDED
@@ -0,0 +1,446 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ library_name: pytorch
4
+ pipeline_tag: image-segmentation
5
+ tags:
6
+ - image-segmentation
7
+ - pytorch
8
+ - unet
9
+ - fungal-colony
10
+ - petri-dish
11
+ - morphometry
12
+ - magnaporthe
13
+ - area-consistency
14
+ language:
15
+ - en
16
+ ---
17
+
18
+ # 🔬 Gray Leaf Spot Colony Segmentation — Demo Pipeline
19
+
20
+ End-to-end analysis pipeline for **gray leaf spot** (*Magnaporthe* and related
21
+ fungal) colony morphometry on 90 mm petri-dish images, powered by a lightweight
22
+ **SmallUNet** trained with area-consistency loss (w=0.7).
23
+
24
+ **[▶ Try the live demo](https://huggingface.co/spaces/rotsl/grayleafspot-segmentation-demo)** — upload images, run inference, see overlays & 16 growth charts in your browser.
25
+
26
+ ---
27
+
28
+ ## Model
29
+
30
+ **Weights:** [`rotsl/grayleafspot-segmentation/best_area_w_0.7.pt`](https://huggingface.co/rotsl/grayleafspot-segmentation)
31
+
32
+ | Property | Value |
33
+ |---|---|
34
+ | **Architecture** | SmallUNet (custom lightweight U-Net) |
35
+ | **Parameters** | ~250 K |
36
+ | **Base channels** | 16 → 32 → 64 → 128 → 256 (bottleneck) |
37
+ | **Input** | 256 × 256 RGB |
38
+ | **Output** | 1-channel sigmoid mask |
39
+ | **Training loss** | BCE + area-consistency loss (weight = 0.7) |
40
+ | **Dish detection** | OpenCV `HoughCircles` on Gaussian-blurred grayscale |
41
+ | **CPU compatible** | ✅ Pure PyTorch — no custom CUDA kernels |
42
+
43
+ ### SmallUNet Architecture
44
+
45
+ ```
46
+ Input (3 × 256 × 256)
47
+
48
+ ├─ enc1: ConvBlock(3 → 16) ─── skip s1
49
+ ├─ enc2: MaxPool2d → ConvBlock(16 → 32) ─── skip s2
50
+ ├─ enc3: MaxPool2d → ConvBlock(32 → 64) ─── skip s3
51
+ ├─ enc4: MaxPool2d → ConvBlock(64 → 128) ─── skip s4
52
+
53
+ ├─ bottleneck: MaxPool2d → ConvBlock(128 → 256)
54
+
55
+ ├─ up4: Upsample + cat(s4) → ConvBlock(384 → 128)
56
+ ├─ up3: Upsample + cat(s3) → ConvBlock(192 → 64)
57
+ ├─ up2: Upsample + cat(s2) → ConvBlock(96 → 32)
58
+ ├─ up1: Upsample + cat(s1) → ConvBlock(48 → 16)
59
+
60
+ └─ head: Conv2d(16 → 1) → Sigmoid
61
+ ```
62
+
63
+ Each `ConvBlock` = Conv3×3 (no bias) → ReLU → Conv3×3 (no bias) → ReLU.
64
+ `DownBlock` = MaxPool2d(2) → ConvBlock.
65
+ `UpBlock` = Bilinear upsample(×2, align_corners=False) → cat([skip, x]) → ConvBlock.
66
+
67
+ ### Area-Consistency Weights
68
+
69
+ The model repo contains variants trained with different area-consistency loss
70
+ weights. Higher weights enforce stronger agreement between predicted mask area
71
+ and ground-truth polygon area:
72
+
73
+ | Weight file | Loss weight | Description |
74
+ |---|---|---|
75
+ | `best_area_w_0.1.pt` | 0.1 | Light area regularisation |
76
+ | `best_area_w_0.3.pt` | 0.3 | Moderate area regularisation |
77
+ | `best_area_w_0.5.pt` | 0.5 | Balanced BCE + area |
78
+ | **`best_area_w_0.7.pt`** | **0.7** | **Strong area consistency (used by demo)** |
79
+ | `grayleafspot.pt` | — | Main smp.Unet (ResNet-34) model (24.4M params) |
80
+
81
+ ---
82
+
83
+ ## Pipeline Overview
84
+
85
+ ```
86
+ ┌──────────────────────────────────────────────────────────────────┐
87
+ │ Gradio Space (rotsl/grayleafspot-segmentation-demo) │
88
+ │ │
89
+ │ Upload images │
90
+ │ ├─ Fast mode: SmallUNet → mask → overlay (per image) │
91
+ │ └─ Full pipeline (per image): │
92
+ │ 1. OpenCV HoughCircles → dish detection → px_to_mm │
93
+ │ 2. SmallUNet → colony mask (threshold configurable) │
94
+ │ 3. Crack detection (adaptive thresholding + morphology) │
95
+ │ 4. Hyphae detection (Frangi + Meijering + hybrid skeleton) │
96
+ │ 5. Morphometrics — all in mm/mm² via per-image calibration │
97
+ │ 6. 6 overlay panels per image │
98
+ │ 7. 16 growth charts (≥2 images) │
99
+ │ 8. Export: analysis_full.csv / .json / .zip │
100
+ └──────────────────────────────────────────────────────────────────┘
101
+ ```
102
+
103
+ ---
104
+
105
+ ## Visualisation Outputs
106
+
107
+ ### 6 Overlay Panels Per Image
108
+
109
+ | Panel | Colour | Shows |
110
+ |---|---|---|
111
+ | **Raw + Dish** | Green circle, red contour | Detected dish boundary + colony outline |
112
+ | **Colony Mask** | White on black | Binary segmentation mask |
113
+ | **Colony Overlay** | Red 50% blend | Colony area highlighted on raw image |
114
+ | **Cracks** | Yellow | Detected cracks inside colony (dilated for visibility) |
115
+ | **Hyphae** | Cyan | Hyphae skeleton (Frangi + Meijering hybrid filter) |
116
+ | **All Combined** | Red + yellow + cyan | Colony + cracks + hyphae together |
117
+
118
+ ### 16 Growth Charts (when ≥2 images)
119
+
120
+ All spatial metrics are in **mm** (or mm²) via per-image `px_to_mm` calibration
121
+ from dish detection, so images of different resolutions are correctly comparable.
122
+
123
+ | Category | Charts | Units |
124
+ |---|---|---|
125
+ | **Colony geometry** | Colony Area, Colony Diameter, Colony Perimeter | mm², mm, mm |
126
+ | **Shape descriptors** | Eccentricity, Edge Roughness (P/πd), Colony Centre Offset | unitless, unitless, mm |
127
+ | **Texture** | Colony Texture Entropy, Colony Texture Std Dev | unitless, unitless |
128
+ | **Cracks** | Crack Area, Crack Coverage, Number of Cracks | mm², %, count |
129
+ | **Hyphae** | Hyphae Length — Frangi, Meijering, Hybrid | mm, mm, mm |
130
+ | **Growth rates** | Relative Growth Rate (RGR), Absolute Growth Rate | ln mm²/day, mm²/day |
131
+
132
+ Charts are only generated when ≥2 valid data points exist for that metric.
133
+ All charts are included as PNGs in the download zip.
134
+
135
+ ---
136
+
137
+ ## Usage via HF API (Programmatic Access)
138
+
139
+ Run the full pipeline remotely via the
140
+ [Gradio Client](https://www.gradio.app/docs/python-client/introduction) without
141
+ installing anything locally.
142
+
143
+ ### Install
144
+
145
+ ```bash
146
+ pip install gradio_client
147
+ ```
148
+
149
+ ### Quick Start — Upload + Run Pipeline
150
+
151
+ ```python
152
+ from gradio_client import Client, handle_file
153
+
154
+ client = Client("rotsl/grayleafspot-segmentation-demo")
155
+
156
+ # Step 1: Upload images
157
+ result = client.predict(
158
+ files=[
159
+ handle_file("plate_d01.jpg"),
160
+ handle_file("plate_d03.jpg"),
161
+ handle_file("plate_d05.jpg"),
162
+ ],
163
+ api_name="/on_upload",
164
+ )
165
+
166
+ # Step 2: Run the full analysis pipeline
167
+ analysis = client.predict(
168
+ en="GLS_Exp01", # experiment name
169
+ ed="2026-04-01", # experiment start date
170
+ un="YourName", # user name
171
+ pc=1, # plates count
172
+ thresh=0.5, # mask confidence threshold
173
+ full_pipeline=True, # enable full morphometrics
174
+ api_name="/on_run",
175
+ )
176
+
177
+ status_msg = analysis[0]
178
+ overlays = analysis[1] # list of {image: filepath, caption: str}
179
+ charts = analysis[2] # list of {image: filepath, caption: str}
180
+ results_table = analysis[3] # {"headers": [...], "data": [[...], ...]}
181
+ zip_path = analysis[4] # local path to downloaded analysis_full.zip
182
+
183
+ print(status_msg)
184
+ print(f"Overlays: {len(overlays)} panels")
185
+ print(f"Charts: {len(charts)}")
186
+ print(f"Download: {zip_path}")
187
+ ```
188
+
189
+ ### Export Metadata Only (no inference)
190
+
191
+ ```python
192
+ meta = client.predict(
193
+ en="GLS_Exp01",
194
+ ed="2026-04-01",
195
+ un="YourName",
196
+ pc=1,
197
+ api_name="/on_export",
198
+ )
199
+ # meta[0] = status message
200
+ # meta[1] = metadata dataframe
201
+ # meta[2] = path to image_metadata.zip
202
+ ```
203
+
204
+ ### Available API Endpoints
205
+
206
+ | Endpoint | Description | Key Parameters |
207
+ |---|---|---|
208
+ | `/on_upload` | Upload images → gallery | `files`: list of filepaths |
209
+ | `/on_sel` | Select image in gallery | `ed`: experiment date |
210
+ | `/on_save` | Save per-image date/reminder | `nd`: date, `nr`: reminder, `ed`: exp date |
211
+ | `/on_export` | Export metadata CSV/JSON/ICS | `en`, `ed`, `un`, `pc` |
212
+ | `/on_run` | **Run full pipeline** (segmentation + morphometrics + 16 charts) | `en`, `ed`, `un`, `pc`, `thresh`, `full_pipeline` |
213
+
214
+ ### Batch Processing Script
215
+
216
+ ```python
217
+ """Process a folder of petri dish images via the HF Space API."""
218
+ from pathlib import Path
219
+ from gradio_client import Client, handle_file
220
+
221
+ IMAGE_DIR = Path("./my_experiment")
222
+ EXPERIMENT = "GLS_Exp01"
223
+ START_DATE = "2026-04-01"
224
+
225
+ client = Client("rotsl/grayleafspot-segmentation-demo")
226
+
227
+ # Collect all images
228
+ images = sorted(
229
+ p for p in IMAGE_DIR.rglob("*")
230
+ if p.suffix.lower() in {".jpg", ".jpeg", ".png", ".tif", ".bmp", ".webp"}
231
+ )
232
+ print(f"Found {len(images)} images")
233
+
234
+ # Upload
235
+ client.predict(
236
+ files=[handle_file(str(p)) for p in images],
237
+ api_name="/on_upload",
238
+ )
239
+
240
+ # Run pipeline
241
+ status, overlays, charts, table, zip_path = client.predict(
242
+ en=EXPERIMENT,
243
+ ed=START_DATE,
244
+ un="BatchUser",
245
+ pc=1,
246
+ thresh=0.5,
247
+ full_pipeline=True,
248
+ api_name="/on_run",
249
+ )
250
+
251
+ print(status)
252
+ print(f"Results zip: {zip_path}")
253
+
254
+ # Access results as a DataFrame
255
+ import pandas as pd
256
+ df = pd.DataFrame(table["data"], columns=table["headers"])
257
+ print(df[["image_path", "area_mm2", "diameter_mm", "crack_coverage_pct"]].to_string())
258
+ ```
259
+
260
+ ---
261
+
262
+ ## Output Columns
263
+
264
+ ### Metadata
265
+
266
+ | Column | Description |
267
+ |---|---|
268
+ | `image_path` | Image filename |
269
+ | `experiment_name` | Experiment identifier |
270
+ | `experiment_date` | Start date (YYYY-MM-DD) |
271
+ | `image_date` | Auto-detected capture date |
272
+ | `day_code` | d01, d02, … |
273
+ | `user_name` | Researcher |
274
+ | `plates_count` | Number of plates |
275
+
276
+ ### Calibration
277
+
278
+ | Column | Unit | Description |
279
+ |---|---|---|
280
+ | `dish_detected` | bool | Whether dish was found |
281
+ | `dish_radius_px` | px | Dish radius in pixels |
282
+ | `px_to_mm` | mm/px | Per-image scale factor from dish detection |
283
+ | `calibration_diameter_mm` | mm | Should be ≈ 90.0 |
284
+ | `calibration_error_pct` | % | Target < 2% |
285
+
286
+ ### Colony Morphometry
287
+
288
+ | Column | Unit | Description |
289
+ |---|---|---|
290
+ | `area_mm2` | mm² | Colony area |
291
+ | `diameter_mm` | mm | Equivalent circular diameter |
292
+ | `perimeter_mm` | mm | Colony perimeter |
293
+ | `eccentricity` | – | 0 = circle, 1 = line |
294
+ | `edge_roughness` | – | Perimeter / equivalent circle perimeter |
295
+ | `centre_delta_mm` | mm | Colony centre to dish centre |
296
+
297
+ ### Texture
298
+
299
+ | Column | Description |
300
+ |---|---|
301
+ | `entropy` | Shannon entropy (local rank filter) |
302
+ | `texture_std` | Pixel intensity standard deviation |
303
+
304
+ ### Cracks
305
+
306
+ | Column | Unit | Description |
307
+ |---|---|---|
308
+ | `crack_px` | px | Total crack pixels |
309
+ | `crack_area_mm2` | mm² | Total crack area |
310
+ | `crack_coverage_pct` | % | Crack / colony area × 100 |
311
+ | `crack_count` | – | Distinct crack count |
312
+
313
+ ### Hyphae
314
+
315
+ | Column | Unit | Description |
316
+ |---|---|---|
317
+ | `hyph_frangi_mm` | mm | Frangi vesselness skeleton length |
318
+ | `hyph_meijering_mm` | mm | Meijering neuriteness skeleton length |
319
+ | `hyph_hybrid_mm` | mm | Union of both |
320
+
321
+ ### Time-Series
322
+
323
+ | Column | Unit | Description |
324
+ |---|---|---|
325
+ | `days_since_start` | days | From first image |
326
+ | `rgr_per_day` | day⁻¹ | (ln A₂ − ln A₁) / Δdays |
327
+ | `relative_growth_per_day` | mm²/day | (A₂ − A₁) / Δdays |
328
+
329
+ ---
330
+
331
+ ## R Studio Integration
332
+
333
+ ```r
334
+ library(readr)
335
+ library(dplyr)
336
+ library(ggplot2)
337
+
338
+ df <- read_csv("analysis_full.csv")
339
+
340
+ # Growth curve
341
+ df %>%
342
+ filter(is.na(error) | error == "") %>%
343
+ ggplot(aes(x = days_since_start, y = area_mm2, color = experiment_name)) +
344
+ geom_line() + geom_point() +
345
+ labs(x = "Days", y = "Colony Area (mm²)", title = "Gray Leaf Spot Growth") +
346
+ theme_minimal()
347
+
348
+ # Morphology summary
349
+ df %>%
350
+ filter(is.na(error) | error == "") %>%
351
+ group_by(experiment_name) %>%
352
+ summarise(
353
+ n = n(),
354
+ mean_area = mean(area_mm2, na.rm = TRUE),
355
+ mean_roughness = mean(edge_roughness, na.rm = TRUE),
356
+ mean_crack_pct = mean(crack_coverage_pct, na.rm = TRUE),
357
+ total_hyphae = sum(hyph_hybrid_mm, na.rm = TRUE)
358
+ )
359
+
360
+ # RGR
361
+ df %>%
362
+ filter(!is.na(rgr_per_day) & rgr_per_day != "") %>%
363
+ mutate(rgr_per_day = as.numeric(rgr_per_day)) %>%
364
+ ggplot(aes(x = days_since_start, y = rgr_per_day)) +
365
+ geom_col(fill = "steelblue") +
366
+ facet_wrap(~ experiment_name) +
367
+ labs(x = "Days", y = "RGR (day⁻¹)") +
368
+ theme_minimal()
369
+ ```
370
+
371
+ ```r
372
+ library(jsonlite)
373
+ df <- fromJSON("analysis_full.json")
374
+ ```
375
+
376
+ ---
377
+
378
+ ## Technical Notes
379
+
380
+ ### Per-Image Pixel-to-mm Calibration
381
+
382
+ Each image gets its own `px_to_mm` conversion factor derived from dish detection.
383
+ The pipeline detects the 90 mm petri dish via `HoughCircles` and computes:
384
+
385
+ ```
386
+ px_to_mm = 90.0 / (2 × dish_radius_px)
387
+ ```
388
+
389
+ This means images of **different resolutions** (e.g. phone camera vs DSLR vs
390
+ microscope) are correctly converted to physical mm units independently.
391
+ If dish detection fails for an image, `px_to_mm` defaults to 1.0 and
392
+ `dish_detected` is set to `False`.
393
+
394
+ ### Segmentation
395
+
396
+ 1. Resize full image to 256 × 256 → SmallUNet → sigmoid probability map
397
+ 2. Threshold at user-configurable confidence level (default 0.5)
398
+ 3. Resize mask back to original resolution (nearest-neighbour)
399
+
400
+ ### Crack Detection
401
+
402
+ - Local adaptive thresholding (Gaussian, block_size=51) inside colony mask
403
+ - Filter by elongation: aspect ratio > 2.5 or eccentricity > 0.85
404
+ - Interior erosion (disk radius 5) to remove edge artefacts
405
+
406
+ ### Hyphae Detection
407
+
408
+ - **Frangi filter**: multi-scale vesselness (σ = 1–4)
409
+ - **Meijering filter**: neuriteness (σ = 1–4)
410
+ - **Hybrid**: union of both skeletonised responses
411
+ - Analysis region extends 20 px beyond colony boundary
412
+
413
+ ---
414
+
415
+ ## Troubleshooting
416
+
417
+ | Issue | Fix |
418
+ |---|---|
419
+ | Model download fails | Check internet; for gated repos set `HF_TOKEN` |
420
+ | Dish not detected | Full rim must be visible; avoid heavy shadows |
421
+ | Colony not detected | Verify image has visible colony contrast against agar |
422
+ | `px_to_mm = 1.0` | Dish detection failed — check `dish_detected` column |
423
+ | Charts missing | Need ≥2 images with valid data for that metric |
424
+
425
+ ---
426
+
427
+ ## Citation
428
+
429
+ ```bibtex
430
+ @misc{grayleafspot-segmentation-demo-2026,
431
+ author = {rohan r},
432
+ title = {Gray Leaf Spot Colony Segmentation — Demo Pipeline},
433
+ year = {2026},
434
+ url = {https://huggingface.co/rotsl/grayleafspot-segmentation-demo},
435
+ note = {SmallUNet (area-consistency w=0.7) with full morphometric analysis}
436
+ }
437
+ ```
438
+
439
+ ## License
440
+
441
+ Apache License 2.0
442
+
443
+ ## Access
444
+
445
+ This repository is private and gated with manual approval.
446
+ Users must request access before they can view or download the model card and associated files.