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 at_annotate() is for. It wraps a Shiny and OpenSeadragon canvas around a resumable session, so you can work through many images without losing your place, your labels, or your unsaved edits.

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/RtmpgdYIfQ/annotatR-example-session-2e5535bd1178  |  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 19. 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.

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
19 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/RtmpgdYIfQ/annotatR-example-session-2e5535bd1178  |  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/RtmpgdYIfQ/an… imag… compl… NA                0 NA      
#> 2     2 /tmp/RtmpgdYIfQ/an… imag… pendi… NA                0 NA      
#> 3     3 /tmp/RtmpgdYIfQ/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/RtmpgdYIfQ/… compl…        0      0      0        0      0
#> 2     2 image_02 /tmp/RtmpgdYIfQ/… pendi…        0      0      0        0      0
#> 3     3 image_03 /tmp/RtmpgdYIfQ/… 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.