---
title: "Slice visualization with neuroim2"
output: rmarkdown::html_vignette
vignette: |
%\VignetteIndexEntry{Slice visualization with neuroim2}
%\VignetteEngine{knitr::rmarkdown}
%\VignetteEncoding{UTF-8}
params:
family: red
preset: homage
css: albers.css
resource_files:
- albers.css
- albers.js
includes:
in_header: |-
---
```{r setup, include = FALSE}
knitr::opts_chunk$set(
collapse = TRUE,
comment = "#>",
message = FALSE,
warning = FALSE,
fig.width = 7,
fig.height = 4,
dpi = 120,
fig.alt = "Plot output"
)
library(neuroim2)
```
You often need to answer a simple question before doing anything more
complicated with a brain image: *does this volume look plausible, and does the
map or registration result line up with it?*
The plotting helpers in neuroim2 are meant for that first visual check. They
work with `NeuroVol` objects, preserve image aspect ratio, use robust intensity
scaling by default, and return ordinary `ggplot2` objects or lists of panels
when you want to customize or save the result.
## What are we plotting?
This vignette uses a small anatomical image shipped with the package. We also
build a synthetic signed statistical map and simple anatomical edge masks on the
same `NeuroSpace` grid so the overlay and registration-QC examples are
reproducible.
```{r load-demo-data}
anat_path <- system.file("extdata", "mni_downsampled.nii.gz", package = "neuroim2")
raw_anat <- read_vol(anat_path)
data.frame(
object = "raw_anat",
class = class(raw_anat)[1],
dimensions = paste(dim(raw_anat), collapse = " x ")
)
```
```{r prepare-demo-objects, include = FALSE}
set.seed(42)
dims <- dim(raw_anat)
display_arr <- array(as.numeric(raw_anat), dim = dims)
anat <- raw_anat
anatomy_cmap <- c("#242424", "#9a9a9a", "#f4f4f4")
shift_array <- function(x, offset, fill = 0) {
stopifnot(length(offset) == length(dim(x)))
out <- array(fill, dim = dim(x))
src <- Map(seq_len, dim(x))
dst <- Map(seq_len, dim(x))
for (axis in seq_along(offset)) {
step <- offset[[axis]]
if (step > 0) {
src[[axis]] <- seq_len(dim(x)[[axis]] - step)
dst[[axis]] <- seq.int(step + 1L, dim(x)[[axis]])
} else if (step < 0) {
src[[axis]] <- seq.int(1L - step, dim(x)[[axis]])
dst[[axis]] <- seq_len(dim(x)[[axis]] + step)
}
}
out[dst[[1]], dst[[2]], dst[[3]]] <- x[src[[1]], src[[2]], src[[3]]]
out
}
edge_from_mask <- function(mask) {
offsets <- rbind(
c(1, 0, 0), c(-1, 0, 0),
c(0, 1, 0), c(0, -1, 0),
c(0, 0, 1), c(0, 0, -1)
)
edge <- array(FALSE, dim = dim(mask))
for (row in seq_len(nrow(offsets))) {
neighbor <- shift_array(mask, offsets[row, ], fill = FALSE)
edge <- edge | (mask & !neighbor)
}
edge
}
dilate_once <- function(mask) {
offsets <- rbind(
c(0, 0, 0),
c(1, 0, 0), c(-1, 0, 0),
c(0, 1, 0), c(0, -1, 0)
)
out <- array(FALSE, dim = dim(mask))
for (row in seq_len(nrow(offsets))) {
out <- out | shift_array(mask, offsets[row, ], fill = FALSE)
}
out
}
grid <- expand.grid(
i = seq_len(dims[1]),
j = seq_len(dims[2]),
k = seq_len(dims[3])
)
scaled <- within(grid, {
x <- (i - mean(range(i))) / diff(range(i))
y <- (j - mean(range(j))) / diff(range(j))
z <- (k - mean(range(k))) / diff(range(k))
})
blob <- function(cx, cy, cz, sigma) {
exp(-((scaled$x - cx)^2 + (scaled$y - cy)^2 + (scaled$z - cz)^2) / (2 * sigma^2))
}
stat_arr <- 4.2 * blob(0.18, -0.10, 0.05, 0.075) -
3.6 * blob(-0.16, 0.16, -0.08, 0.070)
stat_arr <- array(stat_arr, dim = dims)
stat_arr[abs(stat_arr) < 0.35] <- 0
stat_map <- NeuroVol(stat_arr, space(anat))
shift_voxels <- 3L
comparison_arr <- shift_array(display_arr, c(shift_voxels, 0, 0), fill = 0)
comparison <- NeuroVol(comparison_arr, space(anat))
brain_threshold <- stats::quantile(display_arr[display_arr > 0], 0.12, na.rm = TRUE)
fixed_mask <- display_arr > brain_threshold
moving_mask <- shift_array(fixed_mask, c(shift_voxels, 0, 0), fill = FALSE)
fixed_edge_arr <- dilate_once(edge_from_mask(fixed_mask))
moving_edge_arr <- dilate_once(edge_from_mask(moving_mask))
fixed_edges <- NeuroVol(fixed_edge_arr, space(anat))
moving_edges <- NeuroVol(moving_edge_arr, space(anat))
zlevels <- unique(round(seq(dims[3] * 0.25, dims[3] * 0.82, length.out = 6)))
center_voxel <- round(dims / 2)
selected_anat <- display_arr[, , zlevels, drop = FALSE]
selected_stat <- stat_arr[, , zlevels, drop = FALSE]
stopifnot(
length(dim(anat)) == 3L,
identical(dim(anat), dim(stat_map)),
identical(space(anat), space(stat_map)),
any(is.finite(selected_anat)),
diff(range(selected_anat, finite = TRUE)) > 0,
sum(display_arr > 0) > 0,
any(selected_stat > 0),
any(selected_stat < 0),
sum(fixed_edge_arr) > 0,
sum(moving_edge_arr) > 0
)
```
## How do I make a slice montage?
Use `plot_montage()` when you want to scan several slices at once. The function
returns a single `ggplot2` object, so you can print it, add labels, or save it
with your usual plotting workflow.
```{r montage-dark, fig.cap = "A dark-style anatomical montage with robust intensity scaling.", fig.alt = "Six axial anatomical slices on a dark background.", fig.width = 8, fig.height = 3.6, dev.args = list(bg = "#141414")}
plot_montage(
anat,
zlevels = zlevels,
ncol = 6,
cmap = anatomy_cmap,
range = "robust",
title = "Anatomical montage",
style = "dark"
)
```
Use the light style when you are preparing a figure for a manuscript page or a
white-background report.
```{r montage-light, fig.cap = "The same slices using the light plotting style.", fig.alt = "Six axial anatomical slices on a light background."}
plot_montage(
anat,
zlevels = zlevels[1:4],
ncol = 4,
cmap = anatomy_cmap,
range = "robust",
title = "Light style montage",
style = "light"
)
```
## How do I inspect one location in three planes?
Use `plot_ortho()` when you care about one voxel or world coordinate. The
default crosshairs make it easier to confirm that the same location is shown in
the axial, coronal, and sagittal panels.
```{r ortho-view, fig.cap = "Orthogonal view through the center voxel.", fig.alt = "Axial, coronal, and sagittal slices with crosshairs.", fig.width = 8, fig.height = 4, dev.args = list(bg = "#141414")}
plot_ortho(
anat,
coord = center_voxel,
unit = "index",
cmap = anatomy_cmap,
title = "Three-plane view",
style = "dark"
)
```
If you want the panels without drawing them immediately, set `draw = FALSE`.
That is useful in scripts that save plots or compose figures later.
```{r no-draw-panels}
ortho_panels <- plot_ortho(anat, coord = center_voxel, draw = FALSE)
names(ortho_panels)
```
## How do I overlay a signed statistical map?
Use `plot_overlay()` when you have a background image and a second volume on the
same grid. For signed maps, a diverging palette and symmetric overlay limits
make positive and negative clusters comparable.
```{r overlay-signed, fig.cap = "Signed statistical map overlaid on the anatomical image.", fig.alt = "An anatomical montage with blue and orange statistical clusters.", fig.width = 8, fig.height = 3.8, dev.args = list(bg = "#141414")}
plot_overlay(
bgvol = anat,
overlay = stat_map,
zlevels = zlevels,
bg_cmap = anatomy_cmap,
bg_range = "robust",
ov_range = "robust",
ov_thresh = 1.8,
ov_alpha = 0.75,
ov_cmap = "coldhot",
ov_symmetric = TRUE,
ncol = 6,
title = "Thresholded signed overlay",
style = "dark"
)
```
```{r overlay-diagnostics}
data.frame(
positive_voxels = sum(stat_arr > 1.8),
negative_voxels = sum(stat_arr < -1.8),
max_stat = round(max(stat_arr), 2),
min_stat = round(min(stat_arr), 2)
)
```
## How do I check registration by eye?
Registration QC usually asks whether two images or edge maps agree on the same
grid. The QC helpers intentionally require matching `NeuroSpace` objects; if
the grids differ, resample first rather than relying on a plot to hide the
mismatch.
`plot_checkerboard()` alternates tiles from a reference and comparison volume.
Misalignment shows up as broken anatomy across tile boundaries.
```{r checkerboard-qc, fig.cap = "Checkerboard comparison between the anatomical image and a shifted copy.", fig.alt = "Checkerboard slices showing discontinuities from a shifted comparison image.", fig.width = 8, fig.height = 3.8, dev.args = list(bg = "#141414")}
plot_checkerboard(
anat,
comparison,
zlevels = zlevels,
tile = 8,
cmap = anatomy_cmap,
ncol = 6,
title = "Checkerboard registration QC",
style = "dark"
)
```
`plot_edge_overlay()` is useful when you have fixed and moving edge maps. Here
the moving edge mask is shifted by three voxels, so the mismatch is visible
where the colored edges separate.
```{r edge-overlay-qc, fig.cap = "Fixed and moving edge maps overlaid on the anatomical image.", fig.alt = "Edge overlay QC slices with colored edge disagreement.", fig.width = 8, fig.height = 3.8, dev.args = list(bg = "#141414")}
plot_edge_overlay(
anat,
fixed_edges,
moving_edges,
zlevels = zlevels,
bg_cmap = anatomy_cmap,
ncol = 6,
title = "Edge overlay registration QC",
style = "dark"
)
```
## Which helper should I reach for?
| Task | Helper |
|---|---|
| Quick axial scan of one volume | `plot()` or `plot_montage()` |
| Publication-style slice grid | `plot_montage()` |
| One location in axial/coronal/sagittal planes | `plot_ortho()` |
| Statistical or mask overlay on anatomy | `plot_overlay()` |
| Registration check from two images | `plot_checkerboard()` |
| Registration check from edge maps | `plot_edge_overlay()` |
| Shared package styling | `theme_neuro()` |
## Practical tips
- Use `range = "robust"` for anatomical images unless outliers are the point.
- Use `ov_symmetric = TRUE` with diverging palettes for signed overlays.
- Use `style = "dark"` for visual inspection and `style = "light"` for reports
that need a white page.
- Use `draw = FALSE` when a helper returns multiple panels and you want to save
or arrange them yourself.
For related workflows, see `vignette("ImageVolumes")` for volume objects,
`vignette("coordinate-systems")` for voxel/world coordinates, and
`vignette("Resampling")` before plotting images that do not already share a
grid.