Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
@@ -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")),
Expand Down
7 changes: 7 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
34 changes: 20 additions & 14 deletions R/sample_saving.R
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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)
}
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions inst/CITATION
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
)
47 changes: 40 additions & 7 deletions inst/app/server.R
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -241,14 +243,19 @@ 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%")),
shinyDirButton("browse_output_folder", "Browse", "Select Output Folder",
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;",
Expand Down Expand Up @@ -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)
Expand All @@ -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
))
Expand Down Expand Up @@ -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.",
Expand All @@ -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...", {
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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
)
})

Expand Down Expand Up @@ -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)
Expand Down
Binary file modified man/figures/settings-dialog.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 7 additions & 1 deletion man/save_sample_annotations.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.