Gray leaf spot (GLS) is a fungal disease caused by the ascomycete Magnaporthe oryzae, an economically important pathogen best known as the causal agent of rice blast and also associated with gray leaf spot on other grass hosts. In laboratory settings, fungal isolates are grown on petri dishes and photographed at successive time points to characterise colony expansion, morphology, and structural organisation. Manual measurement of these traits is time-consuming and operator-dependent.
grayleafspotr provides a fully automated pipeline for
quantitative phenotyping of GLS colonies from time-lapse plate
photographs. The package uses a bundled SmallUNet deep-learning
segmentation model to identify colonies in each image, then extracts
morphometric and texture features that describe colony growth, shape,
and internal organisation over time. The resulting tidy data frame can
be explored with included template ggplot2 visualisations
or passed to any downstream R analysis.
Python dependencies are managed automatically through
basilisk; no manual Python environment setup is
required.
grayleafspotr integrates with the Bioconductor ecosystem
in two ways. Python environment management uses basilisk,
so the segmentation model runs in a versioned, reproducible environment
rather than depending on the user’s system Python. For visual quality
control, plot_grayleafspot_overlay() uses
EBImage to draw the detected dish boundary, colony outline,
and crack segments directly on the source photograph, so the numeric
summary features in $results can be checked against the
underlying image rather than trusted as an opaque black box.
General-purpose plate-image segmentation tools (e.g. ImageJ/Fiji macros,
or ad hoc OpenCV scripts) typically require users to re-implement this
kind of feature extraction and visual-QC step themselves;
grayleafspotr packages the full path from raw photograph to
tidy, ggplot2-ready results and an
EBImage-based visual audit trail in one reproducible
pipeline.
Given a folder of plate photographs, grayleafspotr:
best_area_w_0.7.pt).grayleafspot_run) containing a data frame indexed by
filename and imaging day.ggplot2 objects ready for further
customisation.plot_grayleafspot_overlay() draws the detected ROIs on the
source image via EBImage.| Requirement | Detail |
|---|---|
| Image formats | JPEG, PNG, BMP, TIFF, WEBP |
| Naming convention | Encode the day with a d\d+ token:
*_d04_*.jpg for day 4 |
| Plate diameter | Standard 90 mm (adjustable via plate_diameter_mm) |
| Folder structure | One folder per experiment; one image per plate per time point |
Example filenames and the day values they produce:
20260210_P001_06-FEB_WT_PCBM_SUB_d04_TOP.jpg → day 4
20260212_P001_06-FEB_WT_PCBM_SUB_d06_TOP.jpg → day 6
20260216_P001_06-FEB_WT_PCBM_SUB_d10_TOP.jpg → day 10
Plate images (folder)
│
▼
grayleafspot_analyze() / grayleafspot_run()
│
├─ Dish geometry detection (classical Hough transform)
├─ SmallUNet segmentation (best_area_w_0.7.pt)
└─ Feature extraction (area, shape, texture, cracks, radial)
│
▼
grayleafspot_run S3 object
│
├─ $results per-image feature data frame
└─ $run run manifest (paths, timestamp, engine)
│
▼
Template ggplot2 plots / EBImage ROI overlay / tidy data / custom analysis
Install from Bioconductor:
if (!requireNamespace("BiocManager", quietly = TRUE))
install.packages("BiocManager")
BiocManager::install("grayleafspotr")basilisk installs Python dependencies when you first run
the analysis pipeline. For setup details and manual configuration, see
Developer note: Python
override.
The package ships three pre-computed example results so that all plotting and data-wrangling functions can be explored without running the full pipeline.
example_run$results[, c("filename", "day", "area_mm2", "diameter_mm",
"circularity", "crack_coverage_pct", "qc_status")]## # A tibble: 2 × 7
## filename day area_mm2 diameter_mm circularity crack_coverage_pct qc_status
## <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <chr>
## 1 demo_day1… 1 12 3.9 0.8 0 pass
## 2 demo_day2… 2 18 4.78 0.77 2.5 pass
The three plate photographs used to generate this example are also
included in the package and are accessible via
system.file():
image_dir <- system.file("extdata", "testdata", "06FEB", package = "grayleafspotr")
list.files(image_dir)## [1] "20260210_P001_06-FEB_WT_PCBM_SUB_d04_TOP.jpg"
## [2] "20260212_P001_06-FEB_WT_PCBM_SUB_d06_TOP.jpg"
## [3] "20260216_P001_06-FEB_WT_PCBM_SUB_d10_TOP.jpg"
Every plotting function accepts a grayleafspot_run
object and returns a ggplot2 object that can be customised
with standard ggplot2 calls.
Colony equivalent diameter (mm) plotted against imaging day.
Daily growth increment (mm/day) alongside a measure of colony edge irregularity.
Proportion of the colony mask classified as cracked tissue, alongside the total crack count, a proxy for structural stress.
Texture entropy and the centre-to-edge entropy gradient reflect internal colony heterogeneity.
Eccentricity (0 = circular, 1 = elongated) plotted against crack coverage to reveal coupling between morphology and structural remodelling.
Pearson correlations between all numeric features.
Convert the run object to a plain data frame for any downstream workflow:
## # A tibble: 2 × 34
## id filename day area_mm2 radius_mm diameter_mm perimeter_mm circularity
## <chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 demo-1 demo_day… 1 12 1.95 3.9 8 0.8
## 2 demo-2 demo_day… 2 18 2.39 4.78 9.1 0.77
## # ℹ 26 more variables: eccentricity <dbl>, edge_roughness <dbl>,
## # contrast <dbl>, correlation <dbl>, energy <dbl>, homogeneity <dbl>,
## # entropy <dbl>, center_edge_delta <dbl>, density_index <dbl>, core <dbl>,
## # middle <dbl>, outer <dbl>, crack_count <dbl>, crack_length_mm <dbl>,
## # crack_coverage_pct <dbl>, proportional_crack_coverage_pct <dbl>,
## # radial_velocity_mm_per_day <dbl>, area_growth_rate_mm2_per_day <dbl>,
## # relative_growth_rate_per_day <dbl>, radial_acceleration <dbl>, …
Place all plate photographs for one experiment in a single directory.
The filename must contain a d\d+ token that encodes the
imaging day:
my_experiment/
├── isolate_A_d03_rep1.jpg
├── isolate_A_d05_rep1.jpg
├── isolate_A_d07_rep1.jpg
└── ...
The examples below use the three bundled 06FEB test images
(inst/extdata/testdata/06FEB/) in place of
my_experiment/. When the Python availability check returns
FALSE, the vignette shows the code without running these
chunks.
grayleafspot_run() is the recommended entry point for
most users. It returns the raw JSON payload as a named list.
img_dir <- system.file("extdata", "testdata", "06FEB", package = "grayleafspotr")
out_dir <- file.path(tempdir(), "grayleafspotr-workflow-demo")
res <- grayleafspot_run(
input_dir = img_dir,
output_dir = out_dir,
run_name = "isolate_A"
)
res$results # per-image feature table (data frame)## id
## 1 20260210_P001_06-FEB_WT_PCBM_SUB_d04_TOP
## 2 20260212_P001_06-FEB_WT_PCBM_SUB_d06_TOP
## 3 20260216_P001_06-FEB_WT_PCBM_SUB_d10_TOP
## filename day
## 1 20260210_P001_06-FEB_WT_PCBM_SUB_d04_TOP.jpg 4
## 2 20260212_P001_06-FEB_WT_PCBM_SUB_d06_TOP.jpg 6
## 3 20260216_P001_06-FEB_WT_PCBM_SUB_d10_TOP.jpg 10
## imageUrl pixelToMm
## 1 /input_images/20260210_P001_06-FEB_WT_PCBM_SUB_d04_TOP.jpg 0.08474201
## 2 /input_images/20260212_P001_06-FEB_WT_PCBM_SUB_d06_TOP.jpg 0.04508659
## 3 /input_images/20260216_P001_06-FEB_WT_PCBM_SUB_d10_TOP.jpg 0.05934821
## morphology.areaMm2 morphology.equivalentRadiusMm morphology.diameterMm
## 1 577.9939 13.56397 27.12793
## 2 1284.5859 20.22119 40.44237
## 3 3915.6662 35.30432 70.60865
## morphology.perimeterMm morphology.circularity morphology.eccentricity
## 1 104.1886 0.6691023 0.3644399
## 2 158.7753 0.6403348 0.3301117
## 3 275.5996 0.6478262 0.2916511
## morphology.edgeRoughness texture.contrast texture.correlation texture.energy
## 1 0.2225137 16.76452 0.9830331 0.6737501
## 2 0.2496732 22.99046 0.9381108 0.7421464
## 3 0.2424266 30.76392 0.9877357 0.4190036
## texture.homogeneity texture.radialZonation.core texture.radialZonation.middle
## 1 0.3550674 13.32438 12.36510
## 2 0.1441021 25.09181 16.89554
## 3 0.3478771 17.88381 25.16256
## texture.radialZonation.outer texture.entropy texture.centerToEdgeDelta
## 1 6.776219 0.7486480 -36.142937
## 2 21.551428 0.8069173 3.056347
## 3 24.167657 0.8371823 52.718883
## texture.densityIndex cracks.count cracks.totalLengthMm cracks.coveragePct
## 1 0.1818142 139 276.4284 100
## 2 0.1432516 3942 2757.4510 100
## 3 0.3640370 3760 3984.9952 100
## cracks.proportionalCoveragePct
## 1 100
## 2 100
## 3 100
## cracks.internalBandSummary
## 1 Internal-band crack analysis used a 1.36 mm inward band with 1395 detected crack segments.
## 2 Internal-band crack analysis used a 2.02 mm inward band with 18610 detected crack segments.
## 3 Internal-band crack analysis used a 3.53 mm inward band with 22229 detected crack segments.
## kinematics.radialVelocity kinematics.areaGrowthRate
## 1 0.000000 0.0000
## 2 3.328610 353.2960
## 3 3.770784 657.7701
## kinematics.relativeGrowthRate kinematics.radialAcceleration qcStatus
## 1 0.0000000 0.0000000 pass
## 2 0.6112451 0.0000000 pass
## 3 0.5120484 0.1105435 pass
## qcNotes
## 1 Local inference using localunet-smallunet. Dish geometry calibrated from grayscale image edges using an assumed 90 mm petri dish. Saved colony geometry is clipped to remain inside the detected dish. Crack analysis computed classically from the final internal-band mask.
## 2 Local inference using localunet-smallunet. Dish geometry calibrated from grayscale image edges using an assumed 90 mm petri dish. Saved colony geometry is clipped to remain inside the detected dish. Crack analysis computed classically from the final internal-band mask.
## 3 Local inference using localunet-smallunet. Dish geometry calibrated from grayscale image edges using an assumed 90 mm petri dish. Saved colony geometry is clipped to remain inside the detected dish. Crack analysis computed classically from the final internal-band mask.
## rawAnalysis.dish_center.x rawAnalysis.dish_center.y rawAnalysis.dish_radius
## 1 494.5 494.5 438.5
## 2 469.5 555.5 412.6
## 3 495.5 548.5 415.7
## rawAnalysis.colony_polygon rawAnalysis.cracks
## 1 c(437.65.... list(x =....
## 2 c(445.63.... list(x =....
## 3 c(433.66.... list(x =....
## rawAnalysis.internal_band_description
## 1 Internal-band crack analysis used a 1.36 mm inward band with 1395 detected crack segments.
## 2 Internal-band crack analysis used a 2.02 mm inward band with 18610 detected crack segments.
## 3 Internal-band crack analysis used a 3.53 mm inward band with 22229 detected crack segments.
## rawAnalysis.radial_profile.radiusFraction rawAnalysis.radial_profile.radiusMm
## 1 0.020833.... 0.295830....
## 2 0.020833.... 0.441458....
## 3 0.020833.... 0.764061....
## rawAnalysis.radial_profile.meanIntensity
## 1 223.9870....
## 2 195.9385....
## 3 112.0833....
## rawAnalysis.radial_profile.ringSpacingMm
## 1 0.00000
## 2 11.47793
## 3 0.00000
## rawAnalysis.radial_profile.centerToEdgeDelta
## 1 -36.142937
## 2 3.056347
## 3 52.718883
## rawAnalysis.radial_profile.densityIndex
## 1 0.1818142
## 2 0.1432516
## 3 0.3640370
## rawAnalysis.crack_analysis.analysis_band_mm
## 1 1.356397
## 2 2.022119
## 3 3.530432
## rawAnalysis.crack_analysis.analysis_threshold
## 1 0.16031707
## 2 0.15632853
## 3 0.05923726
## rawAnalysis.crack_analysis.crack_area_px
## 1 38067
## 2 439032
## 3 582989
## rawAnalysis.crack_analysis.total_length_px
## 1 3262
## 2 61159
## 3 67146
## rawAnalysis.crack_analysis.num_segments
## 1 1395
## 2 18610
## 3 22229
## rawAnalysis.crack_analysis.mean_segment_length_px
## 1 2.338351
## 2 3.286351
## 3 3.020649
## rawAnalysis.morphology_estimates.area_mm2
## 1 577.9939
## 2 1284.5859
## 3 3915.6662
## rawAnalysis.morphology_estimates.perimeter_mm
## 1 104.1886
## 2 158.7753
## 3 275.5996
## rawAnalysis.morphology_estimates.diameter_mm
## 1 27.12793
## 2 40.44237
## 3 70.60865
## rawAnalysis.segmentation_diagnostics.petri_dish_diameter_mm
## 1 90
## 2 90
## 3 90
## rawAnalysis.segmentation_diagnostics.refined_mask_area_px
## 1 80487
## 2 631929
## 3 1111707
## rawAnalysis.segmentation_diagnostics.hybrid_strategy
## 1 localunet
## 2 localunet
## 3 localunet
## rawAnalysis.segmentation_diagnostics.hybrid_strategy_reason
## 1 SmallUNet-only segmentation, no SAM or classical refinement.
## 2 SmallUNet-only segmentation, no SAM or classical refinement.
## 3 SmallUNet-only segmentation, no SAM or classical refinement.
## rawAnalysis.segmentation_diagnostics.sam_decision
## 1 not_run
## 2 not_run
## 3 not_run
## rawAnalysis.overlayImage
## 1 overlays/20260210_P001_06-FEB_WT_PCBM_SUB_d04_TOP_overlay.png
## 2 overlays/20260212_P001_06-FEB_WT_PCBM_SUB_d06_TOP_overlay.png
## 3 overlays/20260216_P001_06-FEB_WT_PCBM_SUB_d10_TOP_overlay.png
## $engine
## [1] "local"
##
## $engine_model
## [1] "localunet-smallunet"
##
## $created_at
## [1] "2026-10-11T09:31:38.802787+00:00"
##
## $output_dir
## [1] "/tmp/Rtmphtjr6T/grayleafspotr-workflow-demo/20261011T093135Z_localunet_isolate_A"
##
## $overlay_dir
## [1] "/tmp/Rtmphtjr6T/grayleafspotr-workflow-demo/20261011T093135Z_localunet_isolate_A/overlays"
##
## $analysis_json
## [1] "/tmp/Rtmphtjr6T/grayleafspotr-workflow-demo/20261011T093135Z_localunet_isolate_A/analysis.json"
##
## $analysis_csv
## [1] "/tmp/Rtmphtjr6T/grayleafspotr-workflow-demo/20261011T093135Z_localunet_isolate_A/analysis.csv"
grayleafspot_analyze()grayleafspot_analyze() returns a
grayleafspot_run S3 object with direct access to the
template plots:
run <- grayleafspot_analyze(
input_dir = img_dir,
output_dir = out_dir,
run_name = "isolate_A_analyze",
verbose = FALSE
)
plot_colony_expansion(run)plot_grayleafspot_overlay() draws the detected dish
boundary (yellow), colony outline (red), and crack segments (cyan) on
top of the original photograph, using EBImage. This
directly answers “did the pipeline segment what I expect?” without
having to trust the summary statistics alone:
image_file <- list.files(img_dir, full.names = TRUE)[1]
plot_grayleafspot_overlay(run, image_path = image_file)## Image
## colorMode : Color
## storage.mode : double
## dim : 1211 1211 3
## frames.total : 3
## frames.render: 1
##
## imageData(object)[1:5,1:6,1]
## [,1] [,2] [,3] [,4] [,5] [,6]
## [1,] 0.06274510 0.06274510 0.06274510 0.06274510 0.06274510 0.06274510
## [2,] 0.05882353 0.05882353 0.05490196 0.05490196 0.05490196 0.05490196
## [3,] 0.05098039 0.05098039 0.05098039 0.05490196 0.05490196 0.05490196
## [4,] 0.05490196 0.05882353 0.05882353 0.05882353 0.05882353 0.06274510
## [5,] 0.06666667 0.06666667 0.06666667 0.07058824 0.07450980 0.07450980
basilisk installs NumPy, OpenCV, PyTorch, scikit-image,
and the other Python dependencies on the first pipeline call. Setup may
take a few minutes; subsequent calls use the cached environment.
If basilisk cannot create an environment on your system,
you can set up one manually. This may be necessary on some Linux systems
or HPC clusters. Follow the manual
Python environment setup instructions in the README. Create the
environment outside the package directory so development tools do not
have to scan its contents.
To use an existing environment, set the path to its Python interpreter:
# ~/.Rprofile: optional Python override
Sys.setenv(GRAYLEAFSPOTR_PYTHON = "/path/to/rvenv/bin/python")When GRAYLEAFSPOTR_PYTHON is set, the pipeline uses that
interpreter instead of the basilisk-managed environment.
Related datasets and software, by the same author:
## R version 4.6.1 (2026-06-24)
## Platform: x86_64-pc-linux-gnu
## Running under: Ubuntu 26.04.1 LTS
##
## Matrix products: default
## BLAS: /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3
## LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.32.so; LAPACK version 3.12.0
##
## locale:
## [1] LC_CTYPE=en_US.UTF-8 LC_NUMERIC=C
## [3] LC_TIME=en_US.UTF-8 LC_COLLATE=en_US.UTF-8
## [5] LC_MONETARY=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8
## [7] LC_PAPER=en_US.UTF-8 LC_NAME=C
## [9] LC_ADDRESS=C LC_TELEPHONE=C
## [11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C
##
## time zone: Etc/UTC
## tzcode source: system (glibc)
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] grayleafspotr_0.99.14 BiocStyle_2.41.0
##
## loaded via a namespace (and not attached):
## [1] gtable_0.3.6 dir.expiry_1.21.0 xfun_0.61
## [4] bslib_0.12.0 ggplot2_4.0.3 httr2_1.3.0
## [7] htmlwidgets_1.6.4 lattice_0.23-1 tzdb_0.5.0
## [10] bitops_1.1-0 vctrs_0.7.3 tools_4.6.1
## [13] generics_0.1.4 curl_8.0.0 parallel_4.6.1
## [16] tibble_3.3.1 RSQLite_3.53.3 blob_1.3.0
## [19] pkgconfig_2.0.3 Matrix_1.7-6 EBImage_4.55.2
## [22] dbplyr_2.6.0 RColorBrewer_1.1-3 S7_0.2.2
## [25] lifecycle_1.0.5 compiler_4.6.1 farver_2.1.2
## [28] tiff_0.1-12 htmltools_0.5.9 sys_3.4.3
## [31] buildtools_1.0.0 sass_0.4.10 RCurl_1.98-1.20
## [34] yaml_2.3.12 pillar_1.11.1 crayon_1.5.3
## [37] jquerylib_0.1.4 cachem_1.1.0 abind_1.4-8
## [40] basilisk_1.25.0 locfit_1.5-9.12 tidyselect_1.2.1
## [43] digest_0.6.39 dplyr_1.2.1 purrr_1.2.2
## [46] maketools_1.3.2 labeling_0.4.3 fastmap_1.2.0
## [49] grid_4.6.1 cli_3.6.6 magrittr_2.0.5
## [52] utf8_1.2.6 readr_2.2.0 withr_3.0.3
## [55] filelock_1.0.3 scales_1.4.0 rappdirs_0.3.4
## [58] bit64_4.8.6 rmarkdown_2.32 fftwtools_0.9-11
## [61] jpeg_0.1-11 bit_4.6.0 otel_0.2.0
## [64] reticulate_1.47.0 png_0.1-9 hms_1.1.4
## [67] memoise_2.0.1 evaluate_1.0.5 knitr_1.52
## [70] BiocFileCache_3.3.0 rlang_1.3.0 Rcpp_1.1.2
## [73] glue_1.8.1 DBI_1.3.0 BiocManager_1.30.27
## [76] BiocGenerics_0.59.12 vroom_1.7.1 jsonlite_2.0.0
## [79] R6_2.6.1