Contents

Abstract

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.

Interoperability with Bioconductor

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.


0.1 Package overview

0.1.1 What the package does

Given a folder of plate photographs, grayleafspotr:

  1. Detects the petri dish boundary using classical circle fitting.
  2. Segments the colony inside the dish with a SmallUNet model (best_area_w_0.7.pt).
  3. Extracts per-image features: area (mm²), equivalent diameter, circularity, eccentricity, edge roughness, crack coverage, texture entropy, and a radial intensity profile.
  4. Returns a tidy result object (grayleafspot_run) containing a data frame indexed by filename and imaging day.
  5. Provides template plots, one function per figure, all returning ggplot2 objects ready for further customisation.
  6. Supports visual QC: plot_grayleafspot_overlay() draws the detected ROIs on the source image via EBImage.

0.1.2 Input requirements

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

0.1.3 Workflow at a glance

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

0.2 Installation

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.


0.3 Bundled example data

The package ships three pre-computed example results so that all plotting and data-wrangling functions can be explored without running the full pipeline.

0.3.1 Load the example run

example_run <- example_grayleafspot_results()

0.3.2 Inspect the feature table

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

0.3.3 Locate bundled source images

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"

0.4 Template visualisations

Every plotting function accepts a grayleafspot_run object and returns a ggplot2 object that can be customised with standard ggplot2 calls.

0.4.1 Colony expansion over time

Colony equivalent diameter (mm) plotted against imaging day.

plot_colony_expansion(example_run)

0.4.2 Growth rate and edge roughness

Daily growth increment (mm/day) alongside a measure of colony edge irregularity.

plot_growth_roughness(example_run)

0.4.3 Crack coverage and count

Proportion of the colony mask classified as cracked tissue, alongside the total crack count, a proxy for structural stress.

plot_stress_remodeling(example_run)

0.4.4 Texture organisation

Texture entropy and the centre-to-edge entropy gradient reflect internal colony heterogeneity.

plot_texture_organization(example_run)

0.4.5 Shape versus stress

Eccentricity (0 = circular, 1 = elongated) plotted against crack coverage to reveal coupling between morphology and structural remodelling.

plot_shape_vs_stress(example_run)

0.4.6 Feature correlation heatmap

Pearson correlations between all numeric features.

plot_feature_heatmap(example_run)

0.4.7 Radial intensity profile

Mean pixel intensity as a function of normalised radial distance from the colony centre.

plot_radial_profile(example_run)


0.5 Work with tidy data

Convert the run object to a plain data frame for any downstream workflow:

growth_data <- as_grayleafspot_growth_data(example_run)
growth_data
## # 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>, …

0.5.1 Custom plot

Because every template function returns a ggplot2 object, you can layer on additional components:

plot_colony_expansion(example_run) +
  ggplot2::labs(
    title    = "Colony expansion: 06-FEB experiment",
    subtitle = "WT strain, PCBM substrate"
  ) +
  ggplot2::theme_classic()


0.6 Analyze your own images

0.6.1 Prepare your image folder

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.

0.6.2 Run the analysis: simple entry point

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
res$run       # run manifest
## $engine
## [1] "local"
## 
## $engine_model
## [1] "localunet-smallunet"
## 
## $created_at
## [1] "2026-10-06T22:16:46.469908+00:00"
## 
## $output_dir
## [1] "/tmp/RtmpX8pukS/grayleafspotr-workflow-demo/20261006T221642Z_localunet_isolate_A"
## 
## $overlay_dir
## [1] "/tmp/RtmpX8pukS/grayleafspotr-workflow-demo/20261006T221642Z_localunet_isolate_A/overlays"
## 
## $analysis_json
## [1] "/tmp/RtmpX8pukS/grayleafspotr-workflow-demo/20261006T221642Z_localunet_isolate_A/analysis.json"
## 
## $analysis_csv
## [1] "/tmp/RtmpX8pukS/grayleafspotr-workflow-demo/20261006T221642Z_localunet_isolate_A/analysis.csv"

0.6.4 Visual QC: overlay detected ROIs on the source image

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

0.6.5 Reload saved results

Every run writes outputs to a timestamped sub-folder. Reload a previous run without re-running the pipeline:

saved_dir <- run$run$output_dir
reloaded <- read_grayleafspot_results(saved_dir)
plot_colony_expansion(reloaded)


0.7 Developer note: Python override

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.


References

Related datasets and software, by the same author:


Session information

sessionInfo()
## R version 4.6.1 (2026-06-24)
## Platform: x86_64-pc-linux-gnu
## Running under: Ubuntu 24.04.5 LTS
## 
## Matrix products: default
## BLAS:   /home/biocbuild/bbs-3.24-bioc/R/lib/libRblas.so 
## LAPACK: /usr/lib/x86_64-linux-gnu/lapack/liblapack.so.3.12.0  LAPACK version 3.12.0
## 
## locale:
##  [1] LC_CTYPE=en_US.UTF-8       LC_NUMERIC=C              
##  [3] LC_TIME=en_GB              LC_COLLATE=C              
##  [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: America/New_York
## tzcode source: system (glibc)
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## other attached packages:
## [1] grayleafspotr_0.99.12 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] tinytex_0.61         tiff_0.1-12          htmltools_0.5.9     
## [31] sass_0.4.10          RCurl_1.98-1.20      yaml_2.3.12         
## [34] pillar_1.11.1        crayon_1.5.3         jquerylib_0.1.4     
## [37] cachem_1.1.0         magick_2.9.1         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] bookdown_0.48        labeling_0.4.3       fastmap_1.2.0       
## [49] grid_4.6.1           cli_3.6.6            magrittr_2.0.5      
## [52] dichromat_2.0-1      utf8_1.2.6           readr_2.2.0         
## [55] withr_3.0.3          filelock_1.0.3       scales_1.4.0        
## [58] rappdirs_0.3.4       bit64_4.8.6          rmarkdown_2.32      
## [61] fftwtools_0.9-11     jpeg_0.1-11          bit_4.6.0           
## [64] otel_0.2.0           reticulate_1.47.0    png_0.1-9           
## [67] hms_1.1.4            memoise_2.0.1        evaluate_1.0.5      
## [70] knitr_1.52           BiocFileCache_3.3.0  rlang_1.3.0         
## [73] Rcpp_1.1.2           glue_1.8.1           DBI_1.3.0           
## [76] BiocManager_1.30.27  BiocGenerics_0.59.12 vroom_1.7.1         
## [79] jsonlite_2.0.0       R6_2.6.1