---
title: Working with 3D Image Volumes
date: '`r Sys.Date()`'
output: rmarkdown::html_vignette
vignette: >
%\VignetteIndexEntry{Working with 3D Image Volumes}
%\VignetteEngine{knitr::rmarkdown}
%\VignetteEncoding{UTF-8}
params:
family: red
css: albers.css
resource_files:
- albers.css
- albers.js
includes:
in_header: |-
---
```{r, echo = FALSE, message = FALSE}
knitr::opts_chunk$set(collapse = T, comment = "#>")
```
## Reading a NIFTI formatted image volume
The way to read an volumetric image file is to use `read_vol`:
```{r}
library(neuroim2)
file_name <- system.file("extdata", "global_mask2.nii.gz", package="neuroim2")
vol <- read_vol(file_name)
```
## Working with image volumes
Information about the geometry of the image volume is shown here:
```{r}
print(vol)
```
`read_vol` returns an object of class `NeuroVol` object which extends an R `array` and has 3 dimensions (x,y,z).
```{r}
class(vol)
is.array(vol)
dim(vol)
vol[1,1,1]
vol[64,64,24]
```
Arithmetic can be performed on images as if they were ordinary `array`s:
```{r}
vol2 <- vol + vol
sum(vol2) == 2 * sum(vol)
vol3 <- vol2 - 2*vol
all(vol3 == 0)
```
## Inspecting geometry and spatial metadata
Each `NeuroVol` has an associated `NeuroSpace` describing its geometry (dimensions, spacing, origin, axes/orientation).
```{r}
sp <- space(vol)
sp # human-readable summary
dim(vol) # spatial dimensions (x, y, z)
spacing(vol) # voxel size in mm
origin(vol) # image origin
```
You can convert between indices, voxel grid coordinates, and real-world coordinates:
```{r}
idx <- 1:5
g <- index_to_grid(vol, idx) # 1D index -> (i,j,k)
w <- index_to_coord(vol, idx) # 1D index -> world coords
idx2 <- coord_to_index(vol, w) # back to indices
all.equal(idx, idx2)
```
A numeric image volume can be converted to a binary image as follows:
```{r}
vol2 <- as.logical(vol)
class(vol2)
print(vol2[1,1,1])
```
## Masks and LogicalNeuroVol
Create a mask from a threshold or an explicit set of indices. Masks are `LogicalNeuroVol` and align with the 3D space.
```{r}
# Threshold-based mask
mask1 <- as.mask(vol > 0.5)
mask1
# Index-based mask
idx_hi <- which(vol > 0.8)
mask2 <- as.mask(vol, idx_hi)
sum(mask2) == length(idx_hi)
# Use a mask to compute a summary
mean_in_mask <- mean(vol[mask1@.Data])
mean_in_mask
```
We can also create a `NeuroVol` instance from an `array` or `numeric` vector. First we consruct a standard R `array`:
```{r}
x <- array(0, c(64,64,64))
```
Now we reate a `NeuroSpace` instance that describes the geometry of the image including, at minimum, its dimensions and voxel spacing.
```{r}
bspace <- NeuroSpace(dim=c(64,64,64), spacing=c(1,1,1))
vol <- NeuroVol(x, bspace)
vol
```
We do not usually have to create `NeuroSpace` objects, because geometric information about an image is automatically determined from information stored in the image file header. Thus, `NeuroSpace` objects are usually copied from existing images using the `space` extractor function when needed:
```{r}
vol2 <- NeuroVol((vol+1)*25, space(vol))
max(vol2)
space(vol2)
```
## Slicing and quick visualization
Extract a single 2D slice for display using standard array indexing:
```{r slice_mid, fig.alt='Mid-slice of example volume (grayscale image).', fig.cap='Mid-slice of example volume'}
z <- ceiling(dim(vol)[3] / 2)
image(vol[,,z], main = paste("Slice z=", z))
```
## Reorienting and resampling
You can change an image’s orientation and voxel spacing. Use `reorient()` to remap axes (e.g., to RAS) and `resample_to()` to match a target space.
```{r}
# Reorient the space (LPI -> RAS) and compare coordinate mappings
sp_lpi <- space(vol)
sp_ras <- reorient(sp_lpi, c("R","A","S"))
g <- t(matrix(c(10, 10, 10)))
world_lpi <- grid_to_coord(sp_lpi, g)
world_ras <- grid_to_coord(sp_ras, g)
# world_lpi and world_ras differ due to axis remapping
```
Resample to a new spacing or match a target `NeuroSpace`:
```{r eval=FALSE}
# Create a target space with 2x finer resolution
sp <- space(vol)
sp2 <- NeuroSpace(sp@dim * c(2,2,2), sp@spacing/2, origin=sp@origin, trans=trans(vol))
# Resample (trilinear)
vol_resamp <- resample_to(vol, sp2, method = "linear")
dim(vol_resamp)
```
## Downsampling
Reduce spatial resolution to speed up downstream operations.
```{r}
# Downsample by target spacing
vol_ds1 <- downsample(vol, spacing = spacing(vol)[1:3] * 2)
dim(vol_ds1)
# Or by factor
vol_ds2 <- downsample(vol, factor = 0.5)
dim(vol_ds2)
```
## Writing a NIFTI formatted image volume
When we're ready to write an image volume to disk, we use `write_vol`
```{r eval=FALSE}
write_vol(vol2, "output.nii")
## adding a '.gz' extension results ina gzipped file.
write_vol(vol2, "output.nii.gz")
```
You can also write to a temporary file during workflows:
```{r}
tmp <- tempfile(fileext = ".nii.gz")
write_vol(vol2, tmp)
file.exists(tmp)
unlink(tmp)
```