diff --git a/DESCRIPTION b/DESCRIPTION index d99cd2e..6c26539 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: ClassiPyR Title: A Shiny App for Manual Image Classification and Validation of IFCB Data -Version: 0.1.0 +Version: 0.1.1 Authors@R: c( person("Anders", "Torstensson", email = "anders.torstensson@smhi.se", role = c("aut", "cre"), comment = c("Swedish Meteorological and Hydrological Institute", ORCID = "0000-0002-8283-656X")), diff --git a/NEWS.md b/NEWS.md index f8e5637..2db4d50 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,10 @@ +# ClassiPyR 0.1.1 + +## New features + +- Added **"Export validation statistics"** checkbox in Settings (below the output folder path). When unchecked, per-sample CSV files are not written to the `validation_statistics/` subfolder. Useful when annotating from scratch where validation statistics are not relevant (#9). +- Added a **confirmation dialog** before bulk export of SQLite annotations to `.mat` files. The dialog explains that existing `.mat` files in the output folder will be overwritten, preventing accidental data loss (#10). + # ClassiPyR 0.1.0 Initial release of ClassiPyR, a Shiny application for manual classification and validation of Imaging FlowCytobot (IFCB) plankton images. diff --git a/R/sample_saving.R b/R/sample_saving.R index 0857a87..fb021d5 100644 --- a/R/sample_saving.R +++ b/R/sample_saving.R @@ -30,6 +30,10 @@ NULL #' @param db_folder Path to the database folder for SQLite storage. Defaults to #' \code{\link{get_default_db_dir}()}. Should be a local filesystem path, #' not a network drive. +#' @param export_statistics Logical. When \code{TRUE} (default), validation +#' statistics CSV files are written to a \code{validation_statistics/} +#' subfolder inside \code{output_folder}. Set to \code{FALSE} to skip this +#' export, e.g. when annotating from scratch. #' @return TRUE on success, FALSE on failure #' @export #' @examples @@ -61,7 +65,8 @@ save_sample_annotations <- function(sample_name, annotator = "Unknown", adc_folder = NULL, save_format = "sqlite", - db_folder = get_default_db_dir()) { + db_folder = get_default_db_dir(), + export_statistics = TRUE) { if (is.null(sample_name) || is.null(classifications) || is.null(class2use_path)) { return(FALSE) @@ -74,14 +79,9 @@ save_sample_annotations <- function(sample_name, tryCatch({ # Create output folders if needed - stats_folder <- file.path(output_folder, "validation_statistics") - if (!dir.exists(output_folder)) { dir.create(output_folder, recursive = TRUE) } - if (!dir.exists(stats_folder)) { - dir.create(stats_folder, recursive = TRUE) - } if (!dir.exists(png_output_folder)) { dir.create(png_output_folder, recursive = TRUE) } @@ -126,14 +126,20 @@ save_sample_annotations <- function(sample_name, ) } - # Save statistics - save_validation_statistics( - sample_name = sample_name, - classifications = classifications, - original_classifications = original_classifications, - stats_folder = stats_folder, - annotator = annotator - ) + # Save statistics (optional) + if (isTRUE(export_statistics)) { + stats_folder <- file.path(output_folder, "validation_statistics") + if (!dir.exists(stats_folder)) { + dir.create(stats_folder, recursive = TRUE) + } + save_validation_statistics( + sample_name = sample_name, + classifications = classifications, + original_classifications = original_classifications, + stats_folder = stats_folder, + annotator = annotator + ) + } # Clean up temp folder unlink(temp_annotate_folder, recursive = TRUE) diff --git a/inst/CITATION b/inst/CITATION index 4108272..8b93f47 100644 --- a/inst/CITATION +++ b/inst/CITATION @@ -9,12 +9,12 @@ bibentry( comment = c(ORCID = "0000-0002-8283-656X")) ), year = "2026", - note = "R package version 0.1.0", + note = "R package version 0.1.1", url = "https://doi.org/10.5281/zenodo.18414999", textVersion = paste( "Torstensson, A. (2026).", "ClassiPyR: A Shiny Application for Manual Image Classification and Validation of IFCB Data.", - "R package version 0.1.0.", + "R package version 0.1.1.", "https://doi.org/10.5281/zenodo.18414999" ) ) diff --git a/inst/app/server.R b/inst/app/server.R index 44d0ecb..de99b4c 100644 --- a/inst/app/server.R +++ b/inst/app/server.R @@ -103,7 +103,8 @@ server <- function(input, output, session) { auto_sync = TRUE, # Automatically sync folders on startup class2use_path = NULL, # Path to class2use file for auto-loading python_venv_path = NULL, # NULL = use ./venv in working directory - save_format = "sqlite" # "sqlite" (default), "mat", or "both" + save_format = "sqlite", # "sqlite" (default), "mat", or "both" + export_statistics = TRUE # Write validation statistics CSV files ) if (file.exists(settings_file)) { @@ -158,7 +159,8 @@ server <- function(input, output, session) { pixels_per_micron = saved_settings$pixels_per_micron, auto_sync = saved_settings$auto_sync, python_venv_path = saved_settings$python_venv_path, - save_format = saved_settings$save_format + save_format = saved_settings$save_format, + export_statistics = saved_settings$export_statistics ) # Initialize class dropdown with default class list on startup @@ -241,7 +243,7 @@ server <- function(input, output, session) { ), div( - style = "display: flex; gap: 5px; align-items: flex-end; margin-bottom: 15px;", + style = "display: flex; gap: 5px; align-items: flex-end; margin-bottom: 5px;", div(style = "flex: 1;", textInput("cfg_output_folder", "Output Folder (MAT/statistics)", value = config$output_folder, width = "100%")), @@ -249,6 +251,11 @@ server <- function(input, output, session) { class = "btn-outline-secondary", style = "margin-bottom: 15px;") ), + checkboxInput("cfg_export_statistics", "Export validation statistics", + value = config$export_statistics), + tags$small(class = "text-muted", style = "display: block; margin-bottom: 15px;", + "Write per-sample CSV files with classification accuracy to the output folder."), + div( style = "display: flex; gap: 5px; align-items: flex-end; margin-bottom: 15px;", div(style = "flex: 1;", @@ -688,6 +695,7 @@ server <- function(input, output, session) { config$pixels_per_micron <- input$cfg_pixels_per_micron config$auto_sync <- input$cfg_auto_sync config$save_format <- input$cfg_save_format + config$export_statistics <- input$cfg_export_statistics # Persist settings to file for next session # python_venv_path is kept from config (set via run_app() or previous save) @@ -701,6 +709,7 @@ server <- function(input, output, session) { pixels_per_micron = input$cfg_pixels_per_micron, auto_sync = input$cfg_auto_sync, save_format = input$cfg_save_format, + export_statistics = input$cfg_export_statistics, class2use_path = rv$class2use_path, python_venv_path = config$python_venv_path )) @@ -749,7 +758,7 @@ server <- function(input, output, session) { } }) - # Export SQLite -> .mat bulk handler + # Export SQLite -> .mat bulk handler: show confirmation dialog first observeEvent(input$export_db_to_mat_btn, { if (is.null(config$output_folder) || config$output_folder == "") { showNotification("Output folder is not configured. Set it in Settings first.", @@ -762,6 +771,27 @@ server <- function(input, output, session) { return() } + showModal(modalDialog( + title = "Confirm .mat export", + p("This will export all annotated samples from the SQLite database as", + tags$strong(".mat files"), "into:"), + tags$code(config$output_folder), + tags$br(), tags$br(), + p(tags$strong("Existing .mat files in this folder will be overwritten"), + "and cannot be recovered. Make sure you have a backup if needed."), + p("Do you want to continue?"), + footer = tagList( + modalButton("Cancel"), + actionButton("confirm_export_mat_btn", "Export", class = "btn-danger") + ), + easyClose = TRUE + )) + }) + + # Confirmed: run the actual export + observeEvent(input$confirm_export_mat_btn, { + removeModal() + db_path <- get_db_path(config$db_folder) withProgress(message = "Exporting SQLite to .mat files...", { @@ -1561,7 +1591,8 @@ server <- function(input, output, session) { annotator = input$annotator_name, adc_folder = adc_folder_for_save, save_format = config$save_format, - db_folder = config$db_folder + db_folder = config$db_folder, + export_statistics = config$export_statistics ) # Only update annotated samples list if changes were actually saved if (isTRUE(saved)) { @@ -2269,7 +2300,8 @@ server <- function(input, output, session) { annotator = annotator, adc_folder = adc_folder, save_format = save_fmt, - db_folder = config$db_folder + db_folder = config$db_folder, + export_statistics = config$export_statistics ) }) @@ -2578,7 +2610,8 @@ server <- function(input, output, session) { class2use_path = class2use_path, annotator = annotator, save_format = isolate(config$save_format), - db_folder = isolate(config$db_folder) + db_folder = isolate(config$db_folder), + export_statistics = isolate(config$export_statistics) ) }, error = function(e) { message("Failed to auto-save ", sample_name, " on session end: ", e$message) diff --git a/man/figures/settings-dialog.png b/man/figures/settings-dialog.png index b9db08e..464830b 100644 Binary files a/man/figures/settings-dialog.png and b/man/figures/settings-dialog.png differ diff --git a/man/save_sample_annotations.Rd b/man/save_sample_annotations.Rd index 24bb82f..f065f78 100644 --- a/man/save_sample_annotations.Rd +++ b/man/save_sample_annotations.Rd @@ -18,7 +18,8 @@ save_sample_annotations( annotator = "Unknown", adc_folder = NULL, save_format = "sqlite", - db_folder = get_default_db_dir() + db_folder = get_default_db_dir(), + export_statistics = TRUE ) } \arguments{ @@ -55,6 +56,11 @@ This supports non-standard folder structures.} \item{db_folder}{Path to the database folder for SQLite storage. Defaults to \code{\link{get_default_db_dir}()}. Should be a local filesystem path, not a network drive.} + +\item{export_statistics}{Logical. When \code{TRUE} (default), validation +statistics CSV files are written to a \code{validation_statistics/} +subfolder inside \code{output_folder}. Set to \code{FALSE} to skip this +export, e.g. when annotating from scratch.} } \value{ TRUE on success, FALSE on failure