--- 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.