---
title: "Get started with hyperannotatR"
output: rmarkdown::html_vignette
vignette: >
%\VignetteIndexEntry{Get started with hyperannotatR}
%\VignetteEngine{knitr::rmarkdown}
%\VignetteEncoding{UTF-8}
resource_files:
- figures/logo.svg
- figures/logo-dark.svg
---
```{r setup, include=FALSE}
# The example images are a PNG (read with magick) and TIFFs (read and written
# with tiff); without them the code is shown but not run.
has_deps <- requireNamespace("magick", quietly = TRUE) &&
requireNamespace("tiff", quietly = TRUE)
knitr::opts_chunk$set(collapse = TRUE, comment = "#>", fig.width = 6, fig.height = 4,
eval = has_deps)
library(hyperannotatR)
```
hyperannotatR helps you draw, organise and export regions of interest (ROIs) across
ordinary images, whole-slide microscopy and hyperspectral data cubes. Every
annotation is stored as a validated simple-feature geometry in image pixel
coordinates, so it survives round-trips to GeoJSON, QuPath and TIFF, and can be
rasterised into the integer masks that downstream segmentation and
classification pipelines expect.
This vignette is a five-minute, end-to-end tour. We begin from a bundled example
image, build a small project, add a couple of regions, inspect them as a table
and as a plot, summarise them, and finish by writing a labelled mask to disk.
Everything here uses only the data that ships with the package, so you can run it
verbatim; nothing touches the network, and the single file we write goes to a
temporary directory. Function names share the `at_` prefix, which makes the API
easy to explore by tab-completion once you have the broad shape in mind.
## An example image
The package bundles three small example assets, reachable through
`at_example_image()`: a `"tissue"` RGB raster, a four-channel `"multiplex"`
pyramid, and a 40-band spectral `"cube"`. We will use the tissue image
throughout.
```{r image}
img <- at_example_image("tissue")
img
```
An `annot_image` is a handle over a pluggable backend: it carries the image's
metadata and serves pixels through `at_tile()`. A handful of accessors tell you
what you are working with:
```{r image-meta}
at_dims(img)
at_n_bands(img)
at_is_spectral(img)
at_bands(img)
```
This is an ordinary 512 by 512 RGB image with three bands and no spectral axis.
A multiplex or cube image would report several bands, wavelengths and, often,
multiple pyramid levels; the accessors above are the same regardless, which is
what lets one annotation workflow span very different data. Coordinates follow
the image convention used everywhere in hyperannotatR: the origin sits at the
top-left corner, `y` increases downward, and pixel `(i, j)` has its centre at
`(j - 0.5, i - 0.5)`. Keeping that in mind makes every ROI you draw land exactly
where you expect.
## Building a project
A *layer* groups ROIs that share a purpose and a controlled set of labels; a
*project* pairs an image with one or more layers plus provenance. We create an
empty layer and hand it to `at_project()`.
```{r project}
regions <- at_layer("regions", labels = c("tumour", "immune"))
project <- at_project(img, regions, name = "quick tour")
project
```
Every project mutator is pure: it returns a modified copy and never alters its
input, which makes annotation pipelines safe to compose and easy to reason
about.
## Adding some regions
Now we draw two ROIs and attach each to the `regions` layer with `at_add_roi()`.
A rectangle is given by its corners, a circle by a centre and radius; both accept
a `label` drawn from the layer's vocabulary. The native pipe keeps the flow
readable.
```{r rois}
project <- project |>
at_add_roi("regions", at_roi_rect(120, 150, 260, 290, label = "tumour")) |>
at_add_roi("regions", at_roi_circle(400, 400, 60, label = "immune"))
project
```
Each addition is recorded in the project's edit log, so the provenance of the
finished annotation set is always available.
## Inspecting the annotations
`at_rois()` returns a tidy tibble with one row per ROI. Geometry, area and
centroid are reported at pyramid level 0, and the table carries an `sf` geometry
column; here we drop it to focus on the attributes.
```{r rois-table}
roi_tbl <- at_rois(project)
sf::st_drop_geometry(roi_tbl)[, c("roi_id", "layer", "label", "geom_type",
"level", "area_px", "n_vertices")]
```
Notice that the circle has been stored as a many-vertex polygon: hyperannotatR keeps
every ROI as a genuine simple feature, so areas, overlaps and set operations are
computed on the stored geometry. `at_rois()` also accepts `layer` and `label` arguments when you want
to pull out a subset, and `at_summary()` rolls the same table up into per-label
counts and areas — a quick sanity check before you commit to a mask.
```{r summary}
at_summary(project)
```
## Seeing them in context
`at_plot_overlay()` draws the ROI outlines on top of the image and returns a
`ggplot` object. Referencing it on its own line renders the figure.
```{r overlay, fig.alt = "Tissue image with a tumour rectangle and immune circle drawn on top."}
at_plot_overlay(project)
```
## Rasterising to a mask
The core deliverable is a *mask*: an integer raster that segmentation and
classification code can consume directly. `at_mask()` offers three flavours — a
`"binary"` foreground mask, a `"labelled"` mask with one value per ROI, and a
`"multiclass"` mask with one value per label class. We build a labelled mask.
```{r mask}
m <- at_mask(project, type = "labelled")
m
```
A pixel is filled when its centre lies inside the polygon (the default,
`touches = FALSE`), using a half-open tie rule so that shared edges are never
double-counted; set `touches = TRUE` if you would rather include any pixel the
polygon so much as grazes. Where ROIs overlap, the `overlap` argument decides
who wins: by default the region drawn last, but `"first"`, `"max"`, `"min"` and
`"error"` are available, and `"bitor"` combines power-of-two class codes
bitwise so that overlapping classes stay recoverable in a multiclass mask. The mask carries a self-describing legend, and
because it is a plain integer matrix underneath you can tabulate it directly.
```{r mask-legend}
at_mask_legend(m)
table(as.integer(m))
```
## Writing the mask to disk
Finally we write the mask out. `at_write_mask()` defaults to a 16-bit TIFF and,
alongside it, a sidecar `.legend.json` that records the value-to-label
mapping, pyramid level and dimensions — so the file is never a bag of anonymous
integers. We write to a temporary directory.
```{r write}
out_path <- file.path(tempdir(), "quick_tour_mask.tiff")
at_write_mask(m, out_path, overwrite = TRUE)
list.files(tempdir(), pattern = "quick_tour_mask")
```
Both the raster and its legend appear, ready to hand to the next stage of your
pipeline (or to reload with `at_read_mask()`).
That is the whole loop: read, annotate, inspect, rasterise, export. From here,
the **Images and backends** vignette explains how hyperannotatR reads pyramids and
spectral cubes, and how to register a backend of your own.