--- 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).