--- title: "Standard protocols, DNN training and QuPath/StarDist exchange" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Standard protocols, DNN training and QuPath/StarDist exchange} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} resource_files: - figures/logo.svg - figures/logo-dark.svg --- ```{r setup, include = FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>") ``` ```{r} library(segmantR) ``` This vignette walks through the reproducible, exchangeable side of segmantR: versioned segmentation protocols, the neutral interchange format, training datasets and model bundles, and the QuPath/StarDist adapter. Everything executed here runs in R alone; steps that need Python or QuPath are shown but not run. ## 1. Standard protocols Protocols are declarative JSON definitions with whitelisted parameters. ```{r} protocols <- sg_protocol_list() protocols[, c("id", "version", "status", "backend")] ``` Each parameter has a type, bounds, unit and calibration requirement: ```{r} spec <- sg_protocol_schema("threshold.otsu.v1") spec[!spec$name %in% c("band_a_nm", "band_b_nm", "band_min_nm", "band_max_nm"), c("name", "type", "unit", "maps_to")] ``` Running a protocol delegates to the direct function with a recorded argument mapping. The run envelope makes declarative and direct runs comparable: ```{r} img <- sg_example_image("fluorescence_nuclei") run <- sg_protocol_run(img, "threshold.otsu.v1", min_area = 5L, output = "run") run str(run$record$delegate$arguments) direct <- sg_segment_threshold(img, method = "otsu", min_area = 5L) identical(direct$labels, run$mask$labels) ``` Invalid input is refused with classified conditions instead of being guessed: ```{r} res <- sg_protocol_validate("threshold.otsu.v1", img, min_area = -1L, error = FALSE) res$error$code try(sg_protocol_run(img, "mesmer.2d.v1")) ``` Post-processing is a protocol too: ```{r} clean <- sg_protocol_run(run$mask, "postprocess.label-cleanup.v1", min_area = 40L, fill_holes = TRUE) clean$n_cells ``` ### Hyperspectral channels Bands are selected by index, name, wavelength or a registered operation; value semantics are declared, never inferred. ```{r} cube <- array(runif(32 * 32 * 3), dim = c(32, 32, 3)) hsi <- new_sg_image(cube, channels = c("b460", "b550", "b650"), bands = data.frame(wavelength_nm = c(460, 550, 650)), value_semantics = "reflectance") nd <- sg_select_channel(hsi, band_operation = "normalized_difference", band_a_nm = 650, band_b_nm = 550) nd$provenance$band_selection[c("operation", "wavelength_nm")] ``` Optional DNN protocols check their backend when they run and never fall back to another method: ```{r} dnn <- sg_protocol_run(img, "stardist.2d.v1", output = "run") dnn$record$status ``` ## 2. Neutral interchange A bundle contains an integer label TIFF, exact pixel-edge GeoJSON with stable object ids, the legend, long-format measurements, a manifest and a SHA-256 inventory. ```{r} dest <- file.path(tempdir(), "segmantR-bundle") unlink(dest, recursive = TRUE) bundle <- sg_export_interchange(run, dest, image = img) list.files(dest) bundle$bundle_digest ``` Imports verify integrity, schema, dtype, orientation, plane, legend and the agreement between GeoJSON and TIFF, and return a **staged** mask: ```{r} imported <- sg_import_interchange(dest, expected_digest = bundle$bundle_digest) imported$checks[, c("check", "status")] sg_mask_status(imported$mask)$status ``` Reviewed masks are protected: ```{r} reviewed <- sg_review_mask(imported$mask, expected_revision = sg_mask_revision(imported$mask)) candidate <- clean try(sg_replace_mask(reviewed, candidate)) replaced <- sg_replace_mask(reviewed, candidate, expected_revision = sg_mask_revision(reviewed), overwrite = TRUE) sg_mask_status(replaced)$status ``` ## 3. Training data, StarDist training and prediction Dataset manifests tile image/mask pairs and split them by origin so tiles of one subject never appear in two splits. ```{r} mask <- sg_cleanup_labels(sg_example_mask("fluorescence_nuclei"), disconnected = "split") pairs <- lapply(c("A", "B", "C"), function(s) { list(image = img, mask = mask, subject_id = s) }) ts <- sg_prepare_training_data(pairs, tile_size = 32L, split = c(train = 0.34, validation = 0.33, test = 0.33)) ts sg_validate_training_manifest(ts$manifest, mask_type = "instance")$ok ``` A foreground/background mask is not accepted as instance training data: ```{r} binary <- new_sg_mask(ifelse(mask$labels > 0L, 1L, 0L)) try(sg_dataset_manifest(list(list(image = img, mask = binary, subject_id = "A")), tile_size = 32L)) ``` Training needs the Python `stardist` and `tensorflow` modules; all data checks run before Python is touched: ```{r, eval = FALSE} model <- sg_train_stardist(ts, n_epochs = 50L, save_path = "models", model_card = list( data_domain = "synthetic fluorescence nuclei", limitations = "demonstration only", license = list(id = "CC-BY-4.0"))) sg_package_model(model, "nuclei.segmantR") loaded <- sg_load_model("nuclei.segmantR") # checksums verified pred <- sg_predict_model(loaded, img, output = "run") sg_mask_status(pred$mask)$status # "staged" ``` ## 4. QuPath and StarDist `sg_stardist_parameter_map()` shows how every protocol parameter maps to the QuPath StarDist extension, including what cannot be honoured: ```{r} map <- sg_stardist_parameter_map("stardist.2d.v1", "qupath", normalize_scope = "tile", normalize_low = 1, normalize_high = 99) map[, c("parameter", "target_option", "status")] ``` A QuPath program contains `run.json`, the Groovy templates and hashed resources. segmantR never starts QuPath itself. ```{r} calibrated <- img calibrated$resolution <- list(x_um = 0.5, y_um = 0.5) prog <- sg_export_qupath(run$mask, file.path(tempdir(), "qupath-import"), image = calibrated, image_name = "nuclei.tif", save_policy = "project", overwrite = TRUE) prog$commands ``` For StarDist in QuPath, pass a model file (`.pb`, SavedModel or bioimage.io) and an image describing the binding, then run the template headless or from the Script Editor and import the results: ```{r, eval = FALSE} sd <- sg_export_qupath("stardist.2d.v1", "qupath-stardist", image = calibrated, image_name = "nuclei.tif", model = "dsb2018_heavy_augment.pb", normalize_scope = "tile", normalize_low = 1, normalize_high = 99, pixel_size_um = 0.5, save_policy = "project") # In a shell, from qupath-stardist/: # QuPath script --project=project.qpproj --image="nuclei.tif" \ # --args=run.json --save segmantR_stardist.groovy back <- sg_import_qupath("qupath-stardist/out", image = calibrated, expected_run_id = sd$run$run_id) back$qupath$agreement ``` ## 5. Capabilities ```{r} caps <- sg_interchange_capabilities() vapply(caps$profiles, function(p) p$status, character(1)) ``` See `system.file("interop", "INTEROP.md", package = "segmantR")` for the full contract, migration notes and security boundaries.