--- title: Visualization output: rmarkdown::html_vignette: css: albers.css includes: in_header: - albers-header.inc - albers-header.html toc: yes toc_depth: 2.0 resource_files: - albers.css - albers-fonts.css - albers.js - albers-header.inc - albers-header.html - fonts params: family: red preset: interaction vignette: | %\VignetteIndexEntry{Visualization} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include = FALSE} if (requireNamespace("ragg", quietly = TRUE)) knitr::opts_chunk$set(dev = "ragg_png") if (requireNamespace("systemfonts", quietly = TRUE)) albersdown::albers_register_fonts() if (requireNamespace("ggplot2", quietly = TRUE) && requireNamespace("albersdown", quietly = TRUE)) ggplot2::theme_set(albersdown::theme_albers(family = params$family, preset = params$preset)) source("_common.R") knitr::opts_chunk$set(fig.width = 8, dpi = 120) ``` ```{r albers-classes, echo=FALSE, results='asis'} cat(sprintf( paste0( '' ), params$family, params$preset )) ``` ```{r load} library(neuroim2) ``` Before anything else, you need to answer one question: *does this look right?* The plotting helpers exist for that first visual check. They take `NeuroVol` objects, preserve aspect ratio, scale intensities robustly by default, and return ordinary `ggplot2` objects you can modify or save. ```{r data} anat <- demo_anatomy() brain <- demo_anatomy_mask() stat_map <- demo_stat_map(brain, radius_mm = 6) identical(space(anat), space(stat_map)) ``` The statistical map here is fitted, not drawn: a design is planted in two spheres of a simulated series and a t-statistic is computed at every voxel. It shares a `NeuroSpace` with the anatomy, which every overlay helper requires. ```{r zlevels} zlevels <- round(seq(12, 38, length.out = 6)) ``` The plotting helpers read anatomical direction from the image affine. Axis permutations and left-right, anterior-posterior, or inferior-superior flips are corrected for display without changing voxel values. An oblique image remains an oblique native slice: use `deoblique()` first when you need resampled, cardinal-plane sections rather than a view of the stored voxel planes. ## A montage of slices `plot_montage()` scans several slices at once. Like every `plot_*` function it returns a figure object that prints like a ggplot and can be passed to `ggsave()`; its layout is re-fitted to whatever size you save it at. ```{r montage, fig.cap = "Six axial slices with robust intensity scaling.", fig.alt = "Six axial anatomical slices on a dark background.", fig.height = 3.4, dev.args = list(bg = "#141414")} plot_montage(anat, zlevels = zlevels, ncol = 6, cmap = anatomy_cmap, range = "robust", title = "Anatomical montage", style = "dark" ) ``` `style = "light"` is the version for a manuscript page: ```{r montage-light, fig.cap = "The same slices in the light style.", fig.alt = "Four axial anatomical slices, dark brain tiles on a light page.", fig.height = 3.4} plot_montage(anat, zlevels = zlevels[1:4], ncol = 4, cmap = anatomy_cmap, range = "robust", title = "Light style", style = "light" ) ``` `range = "robust"` clips to percentiles rather than the extremes. Use it unless the outliers are the point — a single bright voxel on the default full range flattens everything else to black. ## One location in three planes `plot_ortho()` is for when you care about a specific point rather than a set of slices. The crosshairs confirm that all three panels show the same place. ```{r ortho, fig.cap = "Axial, coronal and sagittal views through one voxel.", fig.alt = "Three orthogonal slices with crosshairs.", fig.height = 3.6, dev.args = list(bg = "#141414")} plot_ortho(anat, coord = round(dim(anat) / 2), unit = "index", cmap = anatomy_cmap, title = "Three-plane view", style = "dark" ) ``` `assemble = FALSE` returns the individual panels instead of one figure, which is what you want when arranging them yourself: ```{r ortho-panels} names(plot_ortho(anat, coord = round(dim(anat) / 2), assemble = FALSE)) ``` ## Overlaying a statistical map `plot_overlay()` puts one volume on top of another. For a signed map, a diverging palette with symmetric limits keeps positive and negative comparable: ```{r overlay, fig.cap = "Thresholded signed t-map over the anatomy.", fig.alt = "Six anatomical slices with blue and orange statistical clusters.", fig.height = 3.6, dev.args = list(bg = "#141414")} plot_overlay( bgvol = anat, overlay = stat_map, zlevels = zlevels, ncol = 6, bg_cmap = anatomy_cmap, bg_range = "robust", ov_cmap = "coldhot", ov_thresh = 3, ov_symmetric = TRUE, ov_alpha = 0.8, title = "t > 3", style = "dark" ) ``` It is worth knowing how much survives the threshold — on a real map this is the first number a reviewer asks for: ```{r overlay-counts} a <- as.array(stat_map) c(positive = sum(a > 3), negative = sum(a < -3), total_brain = sum(a != 0)) ``` `ov_symmetric = TRUE` forces the colour scale to be centred on zero. Without it, an asymmetric map assigns different colours to `+4` and `-4`, which is misleading with a diverging palette. ### Cleaning up a noisy map Unsmoothed statistical maps often look like salt and pepper. `enhance_stat_map()` despikes, smooths in an edge-preserving way, and then sharpens what survives, so clusters read clearly at figure size: ```{r enhance} enhanced <- enhance_stat_map(stat_map) raw <- as.array(stat_map) enh <- as.array(enhanced) # Judge the noise floor on voxels chosen from the RAW map, so both columns # describe the same voxels rather than each map's own quiet region. quiet <- which(as.vector(brain) & abs(raw) < 2) round(rbind( raw = c(peak = max(raw), above_3 = sum(raw > 3), quiet_sd = sd(raw[quiet])), enhanced = c(max(enh), sum(enh > 3), sd(enh[quiet])) ), 3) ``` Read that table carefully, because it is the reason this is a display tool. The peak grows by about a third — and the spread of the *quiet* voxels grows by more. Enhancement amplifies the noise floor at least as hard as the signal, so the extra voxels clearing `t > 3` are not new discoveries. Enhance for the figure; threshold and count on the original. Choosing those quiet voxels from the raw map matters. Score each map on its own sub-threshold voxels and both come out near 1.0, because you are measuring a truncated sample rather than the noise floor. `plot_overlay()` and `plot_ortho()` take `enhance = TRUE` to apply it inline. ## Checking a registration Two images on the same grid can be compared directly. `plot_checkerboard()` alternates tiles between them, so misalignment shows up as anatomy breaking across tile edges. ```{r checkerboard, fig.cap = "Checkerboard against a copy shifted by four voxels. Anatomy steps across tile boundaries.", fig.alt = "Three large checkerboard slices; brain structures are offset between alternating tiles.", fig.height = 4.6, dev.args = list(bg = "#141414")} shifted <- demo_shifted(anat, by = 4L) plot_checkerboard(anat, shifted, zlevels = zlevels[2:4], tile = 12, ncol = 3, cmap = anatomy_cmap, title = "Checkerboard QC", style = "dark" ) ``` Checkerboards need room: at six panels across a page the tile steps are too small to see, so prefer a few large slices over many small ones. `plot_edge_overlay()` compares boundaries instead, which is easier to judge when the two images have different contrast: ```{r edges, fig.cap = "Fixed and moving edge maps. Where the colours separate, the images disagree.", fig.alt = "Edge overlay slices with two colours of outline separating.", fig.height = 3.6, dev.args = list(bg = "#141414")} fixed_edges <- demo_edges(brain) moving_edges <- demo_edges(demo_shifted(brain, by = 4L)) plot_edge_overlay(anat, fixed_edges, moving_edges, zlevels = zlevels, ncol = 6, bg_cmap = anatomy_cmap, title = "Edge overlay QC", style = "dark" ) ``` Both helpers **require** matching `NeuroSpace` objects and error otherwise. That is deliberate: a QC plot that silently resampled would hide the very problem you are looking for. Resample first — see `vignette("resampling-and-orientation")`. ## Colours `resolve_cmap()` turns a palette name into colours, and `mapToColors()` maps data onto one. Together they cover the cases the helpers do not: ```{r colours} coldhot <- resolve_cmap("coldhot") head(coldhot, 3) probe <- c(-4, -1, 0, 1, 4) mapToColors(probe, col = coldhot, irange = c(-4, 4)) ``` Positive values come back warm, negative cold, and zero is transparent. Two arguments control the blanking and they are not interchangeable: `zero_col` recolours exactly-zero voxels, while `threshold` takes a **two-element band** and blanks everything inside it to transparent regardless of `zero_col`. ```{r colours-threshold} mapToColors(probe, col = coldhot, irange = c(-4, 4), zero_col = "#00FF00") mapToColors(probe, col = coldhot, irange = c(-4, 4), threshold = c(-2, 2)) ``` A scalar `threshold` errors, so pass both ends. Note also `irange`: left at its default it spans the data, which for an asymmetric map puts zero off-centre — the same trap `ov_symmetric` exists to avoid. A palette is a plain character vector, so any vector of colours works: the `anatomy_cmap` used throughout this article is three greys. ## Which helper? | Task | Helper | |:--|:--| | Quick look at one volume | `plot()`, as used in the tour | | Publication slice grid | `plot_montage()` | | One location in three planes | `plot_ortho()` | | Statistic or mask over anatomy | `plot_overlay()` | | Registration, two images | `plot_checkerboard()` | | Registration, two edge maps | `plot_edge_overlay()` | | Tidy a noisy map for display | `enhance_stat_map()` | | Consistent styling for your own ggplots | `theme_neuro()` | Four habits worth keeping: `range = "robust"` for anatomy, `ov_symmetric = TRUE` for signed maps, `style = "dark"` to inspect and `"light"` to publish, and `assemble = FALSE` whenever you want to arrange panels yourself. ## Where to go next - `vignette("regions-and-searchlights")` — producing maps worth plotting - `vignette("resampling-and-orientation")` — getting two images onto one grid - `?plot_montage`, `?plot_overlay`, `?enhance_stat_map`, `?theme_neuro`