---
title: Visualize and export results
name: visualize-export
description: Read parcel maps, inspect temporal summaries, and verify NIfTI export.
output:
rmarkdown::html_vignette:
toc: yes
toc_depth: 2
css: albers.css
includes:
in_header: albers-header.html
params:
family: red
base_size: 13
content_width: 72
preset: homage
resource_files:
- albers.css
- albers.js
- albers-header.html
- fonts
vignette: |
%\VignetteIndexEntry{Visualize and export results}
%\VignetteEngine{knitr::rmarkdown}
%\VignetteEncoding{UTF-8}
---
```{r setup, include=FALSE}
if (requireNamespace("albersdown", quietly = TRUE)) albersdown::albers_register_fonts()
knitr::opts_chunk$set(
collapse = TRUE, comment = "#>", fig.align = "center", fig.retina = 2,
out.width = "100%", fig.width = 8.5, fig.asp = 0.58,
message = FALSE, warning = FALSE
)
options(width = 86)
suppressPackageStartupMessages({
library(neurocluster)
library(neuroim2)
})
parcel_colors <- c("#0072B2", "#E69F00", "#009E73", "#CC79A7",
"#56B4E9", "#D55E00", "#F0E442", "#000000")
```
```{r albers-classes, echo=FALSE, results='asis'}
cat(sprintf(paste0(
''
), params$family, params$preset, params$content_width))
```
# What should you inspect before export? {#inspect-first}
A label file can be written successfully and still be unhelpful. Before export,
inspect all three anatomical planes, the parcel-size distribution, and the
parcel mean time series. Check the result contract against the original data and
mask as a separate step.
```{r visualization-data}
base_syn <- generate_synthetic_volume(
scenario = "gaussian_blobs", dims = c(16, 16, 8),
n_clusters = 4, n_time = 40, noise_sd = 0.08, seed = 42
)
dims <- base_syn$dims
ijk <- arrayInd(seq_len(prod(dims)), dims)
inside <- ((ijk[, 1] - 8.5) / 7.5)^2 +
((ijk[, 2] - 8.5) / 7.0)^2 + ((ijk[, 3] - 4.5) / 3.8)^2 <= 1
mask_array <- array(as.integer(inside), dims)
spatial_space <- neuroim2::NeuroSpace(
dims, spacing = c(2, 2.5, 3), origin = c(-18, 7, 11)
)
series_space <- neuroim2::NeuroSpace(
c(dims, 40L), spacing = c(2, 2.5, 3), origin = c(-18, 7, 11)
)
series_values <- array(
as.numeric(base_syn$vec), dim = dim(base_syn$vec)
)
syn <- list(
vec = neuroim2::NeuroVec(series_values, series_space),
mask = neuroim2::NeuroVol(mask_array, spatial_space),
truth = base_syn$truth[inside], dims = dims
)
stopifnot(all(sort(unique(syn$truth)) == 1:4), any(!inside), any(inside))
fit <- cluster4d(
syn$vec, syn$mask, 4, method = "rena",
connectivity = 26, parallel = FALSE
)
```
```{r check-visualization-data, include=FALSE}
fit_check <- validate_cluster4d(fit, syn$vec, syn$mask)
stopifnot(
fit_check$valid,
identical(fit$actual_k, 4L),
identical(dim(fit$centers), c(4L, 40L)),
all(is.finite(fit$centers))
)
```
# How do you inspect the parcel map? {#plot}
The plot method uses voxel-index coordinates and displays axial, sagittal, and
coronal slices through the requested location. Label colors are categorical;
their numeric IDs carry no ordering or distance.
```{r orthogonal-view, fig.cap='Orthogonal views through the fitted four-parcel map. The public plot method applies one fixed label range to every panel, so a label keeps the same categorical color even when another label is absent from one plane.', fig.alt='Three categorical slice plots labeled axial, sagittal, and coronal through the middle of a four-parcel ellipsoidal mask. Parcel colors are consistent across planes and excluded space is white.'}
plot_receipt <- plot(
fit, slice = c(8, 8, 4), view = "all",
colors = parcel_colors[seq_len(fit$actual_k)]
)
stopifnot(identical(plot_receipt$zlim, c(0.5, fit$actual_k + 0.5)))
```
The three planes are complementary: a parcel that looks contiguous axially may
split along z. For a real analysis, inspect several slices rather than treating
the middle slice as a topology check.
# What do the parcel signals look like? {#centers}
`fit$centers` contains one mean time series per final label in the original
feature space. The row-to-label mapping is explicit in `fit$label_ids`.
```{r center-summary}
data.frame(
label = fit$label_ids,
voxels = tabulate(fit$labels, nbins = fit$actual_k),
temporal_sd = round(apply(fit$centers, 1, sd), 3)
)
```
```{r center-figure, echo=FALSE, fig.cap='Final parcel mean time series in the original feature space. Distinct profiles are expected in this simulation; real fMRI parcel means also contain nuisance and acquisition structure unless preprocessing addresses it.', fig.alt='Four colored line traces over 40 time points, one for each parcel, with visibly different waveforms.'}
matplot(
t(fit$centers), type = "l", lty = 1, lwd = 2,
col = parcel_colors[seq_len(fit$actual_k)], bty = "l",
xlab = "Time point", ylab = "Mean signal"
)
legend("topright", legend = paste("Parcel", fit$label_ids),
col = parcel_colors[seq_len(fit$actual_k)], lty = 1, lwd = 2,
ncol = 2, bty = "n")
```
# How do you write the complete label map? {#export}
`fit$clusvol` is a `ClusteredNeuroVol` with the original geometry and final
labels. `neuroim2::write_vol()` has a method for this class, so no manual array
reconstruction is needed.
```{r write-label-map}
output_file <- file.path(tempdir(), "neurocluster-parcels.nii.gz")
neuroim2::write_vol(fit$clusvol, output_file)
c(path = output_file, bytes = file.info(output_file)$size)
```
Read the file back before handing it to another tool. This catches path,
compression, geometry, and datatype surprises at the workflow boundary.
```{r read-label-map}
exported <- neuroim2::read_vol(output_file)
c(dimensions = paste(dim(exported), collapse = " x "),
minimum_label = min(as.array(exported)),
maximum_label = max(as.array(exported)))
```
```{r check-label-roundtrip, include=FALSE}
stopifnot(
file.exists(output_file),
identical(dim(exported), dim(fit$clusvol)),
isTRUE(all.equal(neuroim2::space(exported), neuroim2::space(fit$clusvol))),
identical(as.integer(as.array(exported)),
as.integer(as.array(fit$clusvol))),
all(as.array(exported) == floor(as.array(exported))),
all(as.array(exported)[as.array(syn$mask) == 0] == 0)
)
```
# How do you export one parcel as a mask? {#single-parcel}
Comparison on a `ClusteredNeuroVol` produces a geometry-preserving logical
volume. This is useful when a downstream tool expects one binary ROI.
```{r write-single-parcel}
parcel_one <- fit$clusvol == 1L
parcel_file <- file.path(tempdir(), "neurocluster-parcel-01.nii.gz")
neuroim2::write_vol(parcel_one, parcel_file)
sum(as.array(parcel_one))
```
```{r check-single-parcel, include=FALSE}
parcel_back <- neuroim2::read_vol(parcel_file)
stopifnot(
file.exists(parcel_file),
identical(dim(parcel_back), dim(syn$mask)),
isTRUE(all.equal(neuroim2::space(parcel_back), neuroim2::space(syn$mask))),
sum(as.array(parcel_back) > 0) == sum(fit$labels == 1L)
)
unlink(c(output_file, parcel_file))
```
For a complete read-cluster-write workflow, continue with
[End-to-end NIfTI export](end-to-end-export.html).