---
title: Advanced ROI Construction
date: '`r Sys.Date()`'
output: rmarkdown::html_vignette
vignette: |
%\VignetteIndexEntry{Advanced ROI Construction}
%\VignetteEngine{knitr::rmarkdown}
%\VignetteEncoding{UTF-8}
params:
family: red
preset: homage
css: albers.css
resource_files:
- albers.css
- albers.js
includes:
in_header: |-
---
```{r, echo = FALSE, message = FALSE}
knitr::opts_chunk$set(collapse = T, comment = "#>", message = FALSE, warning = FALSE)
suppressPackageStartupMessages({
library(purrr)
library(neuroim2)
})
options(mc.cores=1)
```
This article is now the detailed ROI reference to read after
`vignette("AnalysisWorkflows")`. The core workflow vignette shows when to use
ROIs; this article goes deeper into the different ROI constructors and ROI data
structures available in `neuroim2`.
## ROI types in neuroim2
The package supports several ROI styles, depending on whether you want one
simple neighborhood, a grid-based box, a parcel map, or a weighted kernel:
| ROI Type | Function | Description | Use Case |
|----------|----------|-------------|----------|
| **Spherical** | `spherical_roi()` | Sphere around a center point | Searchlight analysis, seed-based connectivity |
| **Cuboid** | `cuboid_roi()` | Rectangular box region | Grid-based parcellation |
| **Square** | `square_roi()` | 2D square in one plane | Slice-based analysis |
| **Clustered** | `ClusteredNeuroVol` | Parcellation-based regions | Atlas-based analysis |
| **Custom** | `Kernel` | Weighted regions | Gaussian-weighted, gradient-based |
## Quick start
```{r}
file_name <- system.file("extdata", "global_mask2.nii.gz", package="neuroim2")
vol <- read_vol(file_name)
roi <- spherical_roi(vol, c(20, 20, 20), radius = 5)
cat("ROI contains", length(roi), "voxels\n")
```
---
# Basic ROI operations
The introductory ROI extraction and searchlight workflow now lives in
`vignette("AnalysisWorkflows")`. This article stays focused on ROI
construction, representation, and less common ROI variants.
## Creating a Spherical ROI
Spherical ROIs are still the base constructor most other workflows build on, so
this article starts there and then moves into less common patterns.
```{r}
# Load an example brain volume
file_name <- system.file("extdata", "global_mask2.nii.gz", package="neuroim2")
vol <- read_vol(file_name)
# Create a spherical ROI centered at voxel coordinates [20,20,20]
# with a 5mm radius, filling all values with 100
sphere <- spherical_roi(vol, c(20, 20, 20), radius = 5, fill = 100)
# Examine the ROI
cat("Number of voxels in ROI:", length(sphere), "\n")
cat("ROI dimensions:", dim(sphere), "\n")
cat("All values are 100:", all(sphere == 100), "\n")
```
### Performance Note: C++ vs Pure R Implementation
```{r}
# The spherical_roi function can use either C++ (fast) or pure R (slower)
# C++ is the default and recommended for performance
sphere_cpp <- spherical_roi(vol, c(20, 20, 20), radius = 5, use_cpp = TRUE)
sphere_r <- spherical_roi(vol, c(20, 20, 20), radius = 5, use_cpp = FALSE)
# They produce the same voxel set (up to ordering and type)
all.equal(
coords(sphere_cpp)[order(coords(sphere_cpp)[,1], coords(sphere_cpp)[,2], coords(sphere_cpp)[,3]),],
coords(sphere_r)[order(coords(sphere_r)[,1], coords(sphere_r)[,2], coords(sphere_r)[,3]),],
check.attributes = FALSE
)
```
## Creating a Spherical ROI Around Real-World Coordinates
Often, you'll have coordinates in millimeter space (e.g., from published studies) rather than voxel indices.
```{r}
# Define a real-world coordinate in mm
rpoint <- c(-34, -28, 10)
# Convert real-world coordinates to voxel coordinates
vox <- coord_to_grid(vol, rpoint)
cat("Real coordinate:", rpoint, "-> Voxel coordinate:", vox, "\n")
# Create spherical ROI with 10mm radius
sphere <- spherical_roi(vol, vox, radius = 10, fill = 1)
cat("ROI contains", length(sphere), "voxels\n")
# Verify the center of mass is close to our target
coords <- index_to_coord(vol, indices(sphere))
center_of_mass <- colMeans(coords)
cat("Center of mass:", center_of_mass, "\n")
cat("Original point:", rpoint, "\n")
```
## Creating Multiple Spherical ROIs Efficiently
When creating many ROIs, use the vectorized `spherical_roi_set()` function for better performance:
```{r}
library(neuroim2)
vol <- read_vol(system.file("extdata", "global_mask_v4.nii", package = "neuroim2"))
d <- dim(vol)
# Define multiple ROI centers
centers <- rbind(
c(floor(d[1]/3), floor(d[2]/3), floor(d[3]/3)),
c(floor(d[1]/2), floor(d[2]/2), floor(d[3]/2)),
c(floor(2*d[1]/3), floor(2*d[2]/3), floor(2*d[3]/3))
)
# Efficient vectorized creation
rois <- spherical_roi_set(bvol = vol, centroids = centers, radius = 5, fill = 1)
cat("Created", length(rois), "ROIs\n")
# Compare with loop approach (slower but equivalent)
rois2 <- lapply(seq_len(nrow(centers)), function(i) {
spherical_roi(vol, centers[i,], 5, fill = 1)
})
cat("Loop method also created", length(rois2), "ROIs\n")
```
## Creating Cuboid and Square ROIs
### Cuboid ROIs (3D boxes)
```{r}
# Create a cuboid ROI - a 3D rectangular box
sp1 <- NeuroSpace(c(20, 20, 20), c(1, 1, 1))
# Create a 7x7x7 cube (surround=3 means 3 voxels on each side of center)
cube <- cuboid_roi(sp1, c(10, 10, 10), surround = 3, fill = 5)
cat("Cuboid ROI contains", length(cube), "voxels\n")
cat("All values are 5:", all(cube == 5), "\n")
# Get the coordinates
vox_coords <- coords(cube)
cat("Coordinate ranges:\n")
cat(" X:", range(vox_coords[,1]), "\n")
cat(" Y:", range(vox_coords[,2]), "\n")
cat(" Z:", range(vox_coords[,3]), "\n")
```
### Square ROIs (2D in 3D space)
```{r}
# Create a square ROI - a 2D square fixed in one dimension
sp1 <- NeuroSpace(c(20, 20, 20), c(1, 1, 1))
# Create a 5x5 square in the z=10 plane (fixdim=3 fixes the z dimension)
square <- square_roi(sp1, c(10, 10, 10), surround = 2, fill = 3, fixdim = 3)
cat("Square ROI contains", length(square), "voxels (should be 25)\n")
# Verify it's planar
vox_coords <- coords(square)
cat("All z-coordinates are the same:",
length(unique(vox_coords[,3])) == 1, "\n")
cat("Z-plane:", unique(vox_coords[,3]), "\n")
```
---
# Working with ROI Data
## Converting ROIs to Sparse Volumes
Sparse volumes are memory-efficient representations that only store non-zero values:
```{r}
# Create a spherical ROI
sphere <- spherical_roi(vol, c(50, 10, 10), radius = 10, fill = 1)
cat("ROI contains", length(sphere), "voxels\n")
# Convert to SparseNeuroVol - memory efficient storage
# Prefer the provided coercion helper
sparsevol <- as.sparse(sphere)
# Verify properties
cat("Sum of values match:", sum(sparsevol) == sum(sphere), "\n")
cat("Dimensions match:", all(dim(sparsevol) == dim(vol)), "\n")
# Memory comparison
roi_size <- object.size(sphere)
sparse_size <- object.size(sparsevol)
cat("Memory usage - ROI:", format(roi_size, units = "auto"), "\n")
cat("Memory usage - Sparse:", format(sparse_size, units = "auto"), "\n")
```
The basic 4D ROI extraction workflow with `series_roi()` now lives in
`vignette("AnalysisWorkflows")`. The current article keeps the lower-level ROI
data structures and construction details.
## Using ROIVec for 4D ROI Data
The `ROIVec` class is designed for efficient storage of 4D ROI data:
```{r}
# Create a 4D NeuroSpace
vspace <- NeuroSpace(dim = c(10, 10, 10, 20), spacing = c(1, 1, 1))
# Define ROI coordinates
roi_coords <- matrix(c(5,5,5, 6,5,5, 5,6,5, 5,5,6), ncol = 3, byrow = TRUE)
# Create synthetic time-series data for each voxel
n_timepoints <- 20
n_voxels <- nrow(roi_coords)
roi_data <- matrix(rnorm(n_timepoints * n_voxels),
nrow = n_timepoints, ncol = n_voxels)
# Create ROIVec object
roi_vec <- ROIVec(vspace, roi_coords, roi_data)
cat("ROIVec created with", nrow(roi_coords), "voxels and",
nrow(roi_data), "timepoints\n")
# Access as matrix of values (T x N)
roi_matrix <- values(roi_vec)
cat("Matrix dimensions (T x N):", dim(roi_matrix), "\n")
```
Searchlight workflows, including `searchlight()`, `random_searchlight()`, and
`clustered_searchlight()`, are now covered in `vignette("AnalysisWorkflows")`.
That keeps the current article focused on constructing ROIs and ROI-like data
objects rather than on the first-pass analyses built on top of them.
---
# Image Patches
Patches are regular, fixed-size regions that tile the image space, useful for convolutional approaches:
```{r}
# Create 3x3x1 patches covering the volume
pset <- patch_set(vol, dims = c(3, 3, 1))
cat("Number of patches:", length(pset), "\n")
# Compute statistics for each patch
patch_means <- pset %>% purrr::map_dbl(~ mean(.))
cat("Patch mean range:", range(patch_means, na.rm = TRUE), "\n")
# Restrict patches to masked regions only
pset_masked <- patch_set(vol, dims = c(3, 3, 1), mask = as.logical(vol))
cat("Masked patches:", length(pset_masked), "\n")
# Patches are guaranteed to be equal size (padded at edges if needed)
patch_sizes <- pset_masked %>% purrr::map_int(~ length(.))
cat("All patches same size:", length(unique(patch_sizes)) == 1, "\n")
cat("Patch size:", unique(patch_sizes), "voxels\n")
```
---
# Advanced ROI Techniques
## Custom Weighted ROIs with Kernels
Create Gaussian-weighted or other custom-shaped ROIs:
```{r}
# Create a Gaussian kernel for weighted ROI
kern_dim <- c(5, 5, 5) # 5x5x5 kernel
voxel_dim <- c(1, 1, 1) # 1mm isotropic voxels
# Create Gaussian-weighted kernel
gauss_kernel <- Kernel(kerndim = kern_dim, vdim = voxel_dim,
FUN = dnorm, sd = 1.5)
# Embed kernel at a specific location
embedded <- embed_kernel(gauss_kernel, space(vol),
center_voxel = c(20, 20, 20))
cat("Kernel weights sum to:", sum(embedded), "\n")
```
## Working with ClusteredNeuroVec for Parcellated Data
Efficiently handle parcellated 4D data where voxels are grouped into regions:
```{r}
# Create synthetic 4D data
sp4 <- NeuroSpace(c(10, 10, 10, 20), c(1, 1, 1))
arr <- array(rnorm(10*10*10*20), dim = c(10, 10, 10, 20))
vec <- NeuroVec(arr, sp4)
# Create mask for central region
sp3 <- NeuroSpace(c(10, 10, 10), c(1, 1, 1))
mask_arr <- array(FALSE, dim = c(10, 10, 10))
mask_arr[3:8, 3:8, 3:8] <- TRUE
mask <- LogicalNeuroVol(mask_arr, sp3)
# Assign voxels to 5 random clusters
n_voxels <- sum(mask_arr)
clusters <- sample(1:5, n_voxels, replace = TRUE)
cvol <- ClusteredNeuroVol(mask, clusters)
# Create clustered representation - one time-series per cluster
cv <- ClusteredNeuroVec(vec, cvol)
# Access cluster time-series efficiently
cluster_matrix <- as.matrix(cv) # T x K matrix
cat("Cluster matrix dimensions:", dim(cluster_matrix), "\n")
cat("(", dim(cluster_matrix)[1], "timepoints x",
dim(cluster_matrix)[2], "clusters)\n")
```
## ROI Set Operations
Combine and manipulate multiple ROIs:
```{r}
# Create two overlapping spherical ROIs
roi1 <- spherical_roi(vol, c(20, 20, 20), radius = 6, fill = 1)
roi2 <- spherical_roi(vol, c(23, 20, 20), radius = 6, fill = 1)
# Get indices for set operations
idx1 <- indices(roi1)
idx2 <- indices(roi2)
# Intersection - voxels in both ROIs
intersection_idx <- intersect(idx1, idx2)
cat("Intersection:", length(intersection_idx), "voxels\n")
# Union - voxels in either ROI
union_idx <- union(idx1, idx2)
cat("Union:", length(union_idx), "voxels\n")
# Difference - voxels in roi1 but not roi2
diff_idx <- setdiff(idx1, idx2)
cat("Difference (roi1 - roi2):", length(diff_idx), "voxels\n")
# Calculate overlap percentage
overlap_pct <- length(intersection_idx) / length(union_idx) * 100
cat("Overlap:", round(overlap_pct, 1), "%\n")
```
---
# Best Practices and Performance
## Memory Management
When working with many ROIs, consider memory usage:
```{r}
# Compare memory usage of different ROI storage methods
n_rois <- 100
centers <- matrix(runif(n_rois * 3, 5, 15), ncol = 3)
# Method 1: List of ROI objects (flexible but more memory)
roi_list <- lapply(1:nrow(centers), function(i) {
spherical_roi(vol, centers[i,], radius = 6, fill = 1)
})
# Method 2: Single sparse volume with labeled regions (ensure increasing indices)
all_indices <- list()
all_labels <- list()
for (i in 1:nrow(centers)) {
roi <- spherical_roi(vol, centers[i,], radius = 6)
idx <- indices(roi)
idx <- sort(unique(idx))
all_indices[[i]] <- idx
all_labels[[i]] <- rep(i, length(idx))
}
idx_all <- unlist(all_indices)
lab_all <- unlist(all_labels)
ord <- order(idx_all)
idx_all <- idx_all[ord]
lab_all <- lab_all[ord]
# Ensure strictly increasing indices by removing duplicates
keep <- !duplicated(idx_all)
idx_all <- idx_all[keep]
lab_all <- lab_all[keep]
combined_sparse <- SparseNeuroVol(lab_all, space(vol), indices = idx_all)
cat("Memory usage:\n")
cat(" ROI list:", format(object.size(roi_list), units = "auto"), "\n")
cat(" Sparse combined:", format(object.size(combined_sparse), units = "auto"), "\n")
```
## Choosing ROI Sizes
Guidelines for selecting appropriate ROI sizes:
```{r}
# Demonstrate effect of ROI size on coverage and overlap
radii <- c(6, 8, 10, 12)
coverage_stats <- data.frame(
radius = radii,
n_voxels = numeric(length(radii)),
pct_overlap = numeric(length(radii))
)
center1 <- c(20, 20, 20)
center2 <- c(25, 20, 20) # 5 voxels apart
for (i in seq_along(radii)) {
roi1 <- spherical_roi(vol, center1, radius = radii[i])
roi2 <- spherical_roi(vol, center2, radius = radii[i])
coverage_stats$n_voxels[i] <- length(roi1)
overlap <- length(intersect(indices(roi1), indices(roi2)))
total <- length(union(indices(roi1), indices(roi2)))
coverage_stats$pct_overlap[i] <- overlap / total * 100
}
print(coverage_stats)
```
## Parallel Processing Tips
For large-scale ROI analyses, consider parallel processing:
```{r, eval=FALSE}
# Example of parallel searchlight analysis (not run in vignette)
library(parallel)
library(foreach)
library(doParallel)
# Setup parallel backend
n_cores <- detectCores() - 1
cl <- makeCluster(n_cores)
registerDoParallel(cl)
# Parallel searchlight computation
searchlight_results <- foreach(roi = searchlight_list,
.packages = c("neuroim2")) %dopar% {
# Your analysis function here
mean(data[coords(roi)])
}
# Clean up
stopCluster(cl)
```
---
# Troubleshooting and Tips
## Common Issues and Solutions
### Issue: ROI extends outside brain mask
```{r}
# Problem: ROI at edge of brain
edge_vox <- c(2, 2, 2) # Near edge of volume
# Solution 1: Use nonzero=TRUE to keep only mask voxels
roi_filtered <- spherical_roi(vol, edge_vox, radius = 5, nonzero = TRUE)
cat("Filtered ROI size:", length(roi_filtered), "voxels\n")
# Solution 2: Check if ROI is valid before analysis
if (length(roi_filtered) < 10) {
cat("Warning: ROI too small for reliable analysis\n")
}
```
### Issue: Different coordinate systems
```{r}
# Always verify your coordinate system
test_coord_mm <- c(-34, -28, 10) # MNI coordinates
test_coord_vox <- coord_to_grid(vol, test_coord_mm)
# Verify round-trip conversion
back_to_mm <- grid_to_coord(vol, matrix(test_coord_vox, nrow = 1))
cat("Original (mm):", test_coord_mm, "\n")
cat("Voxel:", test_coord_vox, "\n")
cat("Back to mm:", back_to_mm, "\n")
```
---
# See Also
For related functionality, see these other vignettes and help pages:
- `vignette("NeuroVector")` - Working with 4D neuroimaging data
- `?read_vec` and `?read_vol` - Reading neuroimaging files
- `?NeuroSpace` - Understanding coordinate systems and spaces
- `?ClusteredNeuroVol` - Parcellation-based analyses
- `?SparseNeuroVol` - Memory-efficient sparse representations
- `?series` and `?series_roi` - Time-series extraction
For the complete list of ROI-related functions:
```{r, eval=FALSE}
help(package = "neuroim2", topic = "roi")
```