Skip to contents

annotatR logo

Annotating one image is a job for at_project() and the ROI constructors. Annotating a queue of images — a plate of serial sections, a batch of slides, a folder of cubes — is what the annotation app is for. It wraps a Shiny canvas around a resumable session, so you can work through many images without losing your place, your labels, or your unsaved edits. at_app() builds the app as an ordinary shiny.appobj from explicit arguments (for embedding or testing), and at_annotate() launches it.

Building a session

A session is a queue of image paths plus a shared annotation template. Build one explicitly with at_session(), or grab a ready-made one for experimentation with at_example_session(), which copies the bundled tissue image into a temporary directory so the queue has real files to iterate.

sess <- at_example_session(3)
sess
#> <annot_session>
#> images: 3  |  complete: 0  |  cursor: 1
#> out_dir: /tmp/RtmpY5Pp42/annotatR-example-session-3bb21299b023  |  autosave: TRUE

Every image starts with status "pending", the cursor sits on the first image, and per-image projects are materialised lazily — nothing is read from disk until you visit it.

Layer templates and label vocabularies

Two things are shared across the whole queue. The label vocabulary is the flat list of allowed labels, bound to the number keys 1–9. The layer template is a list of at_layer() objects — with styles from at_style() — stamped onto every image the first time you open it, so each slide starts with the same empty layers ready to receive ROIs.

regions <- at_layer("regions", labels = c("tumour", "necrosis", "stroma"),
                    style = at_style(colour = "#E64B35", fill_alpha = 0.3))
lesions <- at_layer("lesions", labels = c("focus", "margin"),
                    style = at_style(colour = "#4DBBD5", z = 2))

paths <- at_session_status(sess)$path
templated <- at_session(paths,
                        labels = c("tumour", "necrosis", "stroma", "focus", "margin"),
                        layers = list(regions, lesions),
                        out_dir = tempdir())

at_layers(at_current(templated))
#> # A tibble: 2 × 6
#>   name    n_rois labels    visible locked     z
#>   <chr>    <int> <list>    <lgl>   <lgl>  <int>
#> 1 regions      0 <chr [3]> TRUE    FALSE      1
#> 2 lesions      0 <chr [2]> TRUE    FALSE      2

Launching the app

With a session in hand, launch the application. This opens a browser and blocks, so it is never run inside a vignette:

at_annotate(templated)

at_annotate() is forgiving about its first argument: pass a session, a project, an image, a vector of paths, a directory, or NULL to open the bundled example.

To embed the app, or to exercise it in tests without a browser, build the app object instead. Nothing is passed through global options:

app <- at_app(session = templated, read_only = FALSE)
class(app)
#> [1] "shiny.appobj"

Hyperspectral cubes in the app

For spectral cubes the Display panel switches the canvas between natural colour, a single band, a pseudo-RGB / false-colour triple and the registered band operations listed by at_band_operations() (ratio, normalised difference, band means). These are display products: masks, spectra and exports always use the original cube values. The Cube table shows the image kind, wavelength range and gaps, value unit, calibration state, plane and how many bytes have been read. The probe tool picks a pixel whose spectrum appears on the Summary page next to ROI and layer spectra, and Download region exports the raw values of a selected ROI, the current view or custom bounds as an ENVI cube with provenance.

Staged changes and status

Annotations proposed by a partner package, such as a qupflowR handoff imported on the Data page or a control command, arrive as a staged patch. The Annotate page lists its creates, updates and conflicts; reviewed or locked ROIs are never changed automatically. Commit or discard it explicitly. The badge next to the save controls always shows the state: read-only, unsaved, staged, committed or saved. See vignette("partner-interop") for the contract behind this.

Keyboard shortcuts

The canvas is built for speed, so almost everything has a key. Draw with the tool keys, assign a label with a digit, then commit.

Key Action
n / p Next / previous image
N Jump to next pending image
1–9 Assign label from the vocabulary
q Rectangle tool
w Polygon tool
e Ellipse / circle tool
r Freehand tool
t Point tool
y Pan / move tool
d Delete selected ROI
Ctrl+Z / Ctrl+Shift+Z Undo / redo
Space Toggle mask overlay
Enter Commit the current ROI
Shift+Enter Commit and advance to next image
Shift+V Copy annotations forward from previous image
f Flag image for review
s Save session
Ctrl+E Export
? Show help

Copy-forward for serial sections

Adjacent serial sections often share most of their anatomy. Rather than redraw it, press Shift+V to copy every ROI from the previous image onto the current one, then nudge the vertices to fit. Combined with Shift+Enter (commit and advance), a run of near-identical sections can be annotated in a few keystrokes each.

Autosave and recovery

By default a session autosaves to _session.rds in its out_dir on every commit, so a crashed browser or a closed laptop costs you nothing. Save on demand with at_save_session() (bound to s), and pick up exactly where you left off with at_resume().

sess <- at_set_status(sess, 1, "complete")
sess <- at_next(sess)

at_save_session(sess)
recovered <- at_resume(file.path(sess$out_dir, "_session.rds"))
recovered
#> <annot_session>
#> images: 3  |  complete: 1  |  cursor: 2
#> out_dir: /tmp/RtmpY5Pp42/annotatR-example-session-3bb21299b023  |  autosave: TRUE

The cursor, statuses, materialised projects, and templates all come back intact.

The session API without the app

Everything the canvas does to a session is available as plain functions, which is what makes batches scriptable and testable. Inspect the queue with at_session_status(), walk it with at_next() / at_prev() / at_goto(), and read a richer progress report — one column per label — with at_manifest().

at_session_status(sess)
#> # A tibble: 3 × 7
#>     idx path                name  status project_path n_rois modified
#>   <int> <chr>               <chr> <chr>  <chr>         <int> <dttm>  
#> 1     1 /tmp/RtmpY5Pp42/an… imag… compl… NA                0 NA      
#> 2     2 /tmp/RtmpY5Pp42/an… imag… pendi… NA                0 NA      
#> 3     3 /tmp/RtmpY5Pp42/an… imag… pendi… NA                0 NA

at_manifest(sess)
#> # A tibble: 3 × 9
#>     idx name     path              status n_layers n_rois tumour necrosis stroma
#>   <int> <chr>    <chr>             <chr>     <int>  <int>  <int>    <int>  <int>
#> 1     1 image_01 /tmp/RtmpY5Pp42/… compl…        0      0      0        0      0
#> 2     2 image_02 /tmp/RtmpY5Pp42/… pendi…        0      0      0        0      0
#> 3     3 image_03 /tmp/RtmpY5Pp42/… pendi…        0      0      0        0      0

Use these to seed a session programmatically, pre-fill statuses, or audit progress in a report — no Shiny required.

Export scoping

Exports (Ctrl+E) are scoped deliberately. You choose the layers to include, the format (GeoJSON, QuPath, or a rasterised TIFF mask), and the breadth — the current image only, every completed image, or the whole queue. Flagged and skipped images are excluded from batch exports unless you opt them back in, so a “export all complete” pass yields exactly the reviewed annotations and nothing half-finished.

Those export formats — and how annotatR round-trips with QuPath, GeoJSON, and mask TIFFs — are the subject of the next vignette, “Interoperability”.

Use of LLM tools

Portions of this package were prepared with assistance from large language model tooling for narrowly defined, non-authorial tasks: copyediting, prose smoothing, Markdown/LaTeX formatting, scaffolding of boilerplate files (CI configs, build scripts), code refactoring. The tools used were Chat AI, the LLM service of KISSKI (GWDG), and a self-hosted Mistral Small (24B, Apache-2.0) run locally via Ollama and the ollamar R package — local inference only, with no data sent to third parties for the self-hosted model.