Skip to content

Visualization

Adrian Curtin edited this page Jul 24, 2026 · 1 revision

Visualization

processFNIRS2 ships a plotting stack that spans single-subject timeseries and topographic maps, publication-quality 3D cortical renders, statistical projections, connectivity/synchrony diagrams, and group-level plots from exploreFNIRS. This page is a practical catalog of the main entry points — for group-analysis plot types (plotTemporal, plotBar, plotLME, ...) see Group Analysis.

Timeseries plots

pf2.data.plot.oxy plots hemoglobin concentration time series, with channel selection, biomarker choice, baseline correction, and marker overlay:

data = pf2.import.sampleData.fNIR2000();
processed = processFNIRS2(data);

pf2.data.plot.oxy(processed);                      % all channels, arranged by probe geometry
pf2.data.plot.oxy(processed, 5);                    % single channel
pf2.data.plot.oxy(processed, 1:4);                  % specific channels
pf2.data.plot.oxy(processed, 5, 'biomarkers', 'all');   % HbO, HbR, HbDiff, HbTotal, CBSI
pf2.data.plot.oxy(processed, 'baseline', 10);       % subtract mean of first 10 s
pf2.data.plot.oxy(processed, 1:4, 'ylim', [-5 5]);  % fixed y-axis
pf2.data.plot.oxy(processed, 1:4, 'markers', 10);   % show only marker code 10

% Headless save (2D plots): built-in savePath option
fig = pf2.data.plot.oxy(processed, 1:4, ...
    'savePath', 'oxy_channels.png', 'saveWidth', 800, 'saveHeight', 400, 'saveDPI', 150);
close(fig);

The raw-intensity counterpart is pf2.data.plot.raw(data, ...) — same channel selection and savePath options, plus 'wavelengths', 730 to view a single wavelength before any processing.

Topographic maps

pf2.probe.plot.topo is the shortest path from processed data to a topographic activation map. It pulls a per-channel value for the requested biomarker (at a time point, over a window, or time-averaged) and renders it as a 2D probe heatmap, a 3D cortical projection, or a time-animated movie:

pf2.probe.plot.topo(processed, 'HbO');                   % 2D heatmap, time-averaged
pf2.probe.plot.topo(processed, 'HbO', 'Time', 30);        % snapshot at t = 30 s
pf2.probe.plot.topo(processed, 'HbO', 'Time', [20 40]);   % window mean, 20-40 s
pf2.probe.plot.topo(processed, 'HbO', 'View', '3d');      % cortical surface
pf2.probe.plot.topo(processed, 'HbO', 'View', 'movie', ...
    'Time', [0 40], 'savePath', 'topo_movie.mp4');        % shortcut to pf2.probe.plot.movie
pf2.probe.plot.topo(processed, 'HbO', 'Time', 30, 'savePath', 'topo.png');
View Renders via
'2d' (default) pf2.probe.plot.imageValues — flat probe heatmap
'3d' pf2.probe.project.biomarker — cortical surface (needs MNI coords)
'movie' pf2.probe.plot.movie — animates over Time/TimeRange (pass savePath, FPS, NFrames)

Underlying primitives (data2plot is a [1 x C] vector, not a biomarker name):

meanHbO = mean(processed.HbO, 1);
pf2.probe.plot.imageValues(meanHbO, processed, [], [], 'Mean HbO', '\muM');       % 2D
pf2.probe.plot.interpolateValues3D(meanHbO, processed, [], [], 'Mean HbO', '\muM'); % 3D

3D render quality: Style presets

The 3D surface renderer (pf2.probe.plot.interpolateValues3D, and everything built on it — topo 'View','3d', pf2.probe.project.*) supports a 'Style' option controlled by pf2_base.plot.RenderStyle:

Style Look
'showcase' (default for the 3D renderer) Procedural matcap shading, neutral-gray cortex so activation pops, sulcal ambient occlusion, an elevated 3/4 "hero" camera view, 2x supersampled export. MRIcroGL/Surfice-inspired.
'publication' Smooth Gouraud matte cortex, gentle ambient occlusion, a conservative data-facing view. (Note: RenderStyle.get() itself defaults to 'publication' when called with no argument — pass the name explicitly to avoid ambiguity.)
data = pf2.import.sampleData.fNIR2000();
proc = processFNIRS2(data);
vals = mean(proc.HbO, 1, 'omitnan');

pf2.probe.plot.interpolateValues3D(vals, proc, 'Style', 'showcase');      % default look
pf2.probe.plot.interpolateValues3D(vals, proc, 'Style', 'publication');   % conservative look

% Override individual fields on a resolved style struct
sty = pf2_base.plot.RenderStyle.get('showcase');
sty.matcapMaterial = 'porcelain';   % clay | porcelain | matte | glossy | pewter | jade
sty.aoStrength = 0.5;               % 0..0.8, sulcal ambient-occlusion strength
pf2.probe.plot.interpolateValues3D(vals, proc, 'Style', sty);

Colormap names resolve through pf2_base.plot.brainColormap: MRIcroGL/Surfice LUTs ('actc', 'warm', 'cool', 'blue2red', 'bone') plus perceptually-uniform, colour-blind-safe defaults ('rdbu' diverging, 'viridis', 'cividis', or any MATLAB built-in name):

pf2.probe.plot.interpolateValues3D(vals, proc, 'cmap', 'rdbu');
pf2.probe.plot.interpolateValues3D(vals, proc, 'cmap', 'actc');

For interactive tuning, pf2.probe.plot.Explore3D(proc) opens a live preview with controls for style, matcap material, ambient-occlusion strength, camera view, colormap, interpolation, biomarker, and time point — every change re-renders immediately and the panel prints the equivalent command for copy-paste into a script.

pf2.probe.plot.Explore3D(proc);   % interactive; skip under -batch

Time-animation movies

pf2.probe.plot.movie sweeps a biomarker over time and writes an MP4, AVI, or GIF, with a single shared color scale across frames and an optional time/marker stamp per frame:

pf2.probe.plot.movie(processed, 'HbO', 'savePath', 'hbo.mp4');                    % 3D cortex (default)
pf2.probe.plot.movie(processed, 'HbO', 'View', '2d', 'savePath', 'hbo.gif');      % flat 2D heatmap
pf2.probe.plot.movie(processed, 'HbO', ...
    'TimeRange', [0 40], 'NFrames', 80, 'FPS', 20, 'savePath', 'hbo.mp4');

The file extension on savePath selects the writer (.mp4 -> MPEG-4, .avi -> Motion JPEG AVI, .gif -> animated GIF, paletted/lower-fidelity — prefer MP4 for smooth gradients). Key options: TimeRange, NFrames (target frame count, default 60) or FrameStep (explicit stride), Window (per-frame averaging), FPS, Range (fixed color limits). pf2.probe.plot.topo(..., 'View', 'movie', ...) is a shortcut that delegates here.

Stat projections (pf2.probe.project.*)

Each wrapper projects a per-channel statistic onto the 3D cortical surface, handling colormap, range, significance thresholding, and transparency of non-significant channels for you. All accept savePath for headless saving and forward camera/style options (initCamPosition, Style, ForceLightMode, ...) to interpolateValues3D.

Function Projects Notes
pf2.probe.project.pvalues(pvals, data) Per-channel p-values, as -log10(p) 'pThreshold', 'FDR', 'PTicks'; non-significant channels render transparent
pf2.probe.project.fstats(Fvals, data) F-statistics (positive-only) Threshold via companion 'pvalues'/'pThreshold' or an explicit 'Fcritical'
pf2.probe.project.correlation(rho, data) Signed correlation in [-1, 1] Two-colorbar diverging treatment; 'pvalues', 'DeadZone'
pf2.probe.project.biomarker(vals, data) Signed biomarker values (HbO, HbR, a contrast) Diverging two-colorbar; 'Range', 'DeadZone', 'pvalues'; 'interpolateType','sensitivity' for a Gaussian optical-sensitivity kernel instead of nearest/IDW
pf2.probe.project.counts(Nvals, data) Per-channel sample counts (N) Sequential, positive-only colorbar — coverage after rejection
pf2.probe.project.regions(regionValues, data) Per-Brodmann-region values, flat-filled Needs data.canonical from pf2.probe.canonicalize; accepts a biomarker name (takes the time-mean) or an explicit [1 x R] vector
pf2.probe.project.parcels(data) / parcels(channelValues, data) Per-channel Voronoi "parcel" cells (nearest-optode) A channel-assignment cartoon, NOT DOT/image reconstruction; 'Highlight' prints a subset of optode numbers
pf2.probe.project.pmdf(vals, data) Channel values through the physical PMDF sensitivity footprint The "honest banana" — masked to real depth-dependent coverage, vs. biomarker's Gaussian lateral kernel
pf2.probe.project.tomography(recon) A DOT image reconstruction (pf2.probe.dot.reconstruct output) Masked to sensitivity coverage; see Diffuse Optical Tomography
raw = pf2.import.sampleData.fNIR2000();
processed = processFNIRS2(raw);
K = size(processed.HbO, 2);

pvals = rand(1, K);
Fvals = abs(randn(1, K)) * 2 + 1;
rho   = 2 * rand(1, K) - 1;

pf2.probe.project.pvalues(pvals, processed, 'pThreshold', 0.05, 'FDR', true);
pf2.probe.project.fstats(Fvals, processed, 'pvalues', pvals, 'pThreshold', 0.05);
pf2.probe.project.correlation(rho, processed, 'pvalues', pvals, 'pThreshold', 0.05);
pf2.probe.project.biomarker(processed.HbO(100, :), processed, 'Range', [-1 1]);

% Anatomical parcels need canonicalized channels
procC = pf2.probe.canonicalize(processed, 'MaxDistance', 25);
pf2.probe.project.regions('HbO', procC);   % time-mean per Brodmann region

% Headless save (all project.* functions)
pf2.probe.project.fstats(Fvals, processed, 'Fcritical', 3.84, 'savePath', 'fstats.png');

Connectome, chord diagrams, and dual-brain synchrony

pf2.probe.plot.connectome draws a connectivity matrix as edges anchored at real channel/ROI positions on the cortex (3D) or flat probe layout (2D) — spatial structure (front-back, long- vs short-range) is directly visible:

conn = exploreFNIRS.connectivity.computeMatrix(processed, 'Method', 'pearson');
pf2.probe.plot.connectome(conn, processed, 'View', '3d', 'TopN', 30);
pf2.probe.plot.connectome(conn, processed, 'View', '2d', 'Threshold', 0.5);

exploreFNIRS.connectivity.plotChord draws the same kind of result as a circular chord diagram (nodes on a ring, Bezier arcs for coupled pairs) — better for a compact all-pairs overview than for spatial layout. Nodes can be colored by a per-node statistic with their own colorbar:

exploreFNIRS.connectivity.plotChord(conn);                         % flat nodes, sign-colored arcs
exploreFNIRS.connectivity.plotChord(conn, 'NodeValues', 'auto', ...  % signed node strength + colorbar
    'GroupLabels', 'auto');

exploreFNIRS.hyperscanning.plotDualBrain places two subjects' probes side by side at their real anatomical layout with cross-brain synchrony edges between them, optionally with a linked wavelet-coherence panel:

A = processed; B = processFNIRS2(pf2.import.sampleData.fNIR2000());
dyad = exploreFNIRS.hyperscanning.computeDyad(A, B, 'ChannelPairing', 'all');
wc = exploreFNIRS.coupling.wcoherence(A.HbO(:,1), B.HbO(:,1), A.fs);

exploreFNIRS.hyperscanning.plotDualBrain(dyad, A, B, 'TopN', 30, 'Wcoherence', wc, ...
    'BrainLabels', {'Child', 'Parent'}, 'SavePath', 'dualbrain.png');

See Group Analysis for exploreFNIRS.connectivity.computeMatrix and exploreFNIRS.hyperscanning.computeDyad themselves.

Head and probe models

pf2.probe.plot.showProbe3D overlays source/detector optodes on a 3D brain surface — the quickest way to sanity-check probe placement:

pf2.probe.plot.showProbe3D(processed);
pf2.probe.plot.showProbe3D(processed, 'initCamPosition', 'top');   % also: front, top-left, front-left, ...

pf2.probe.plot.showHead3D renders the Colin27 head model in real MNI millimetres: a translucent scalp shell over a brain surface, with the probe overlaid. This is a qualitative visualization/QC aid (not a forward/DOT model):

pf2.probe.plot.showHead3D(processed);                          % scalp + brain + sources/detectors
pf2.probe.plot.showHead3D(processed, 'ShowScalp', false);      % brain only
pf2.probe.plot.showHead3D(processed, 'Markers', 'channels');   % channel midpoints instead of optodes
pf2.probe.plot.showHead3D(processed, 'savePath', 'head.png');

Group-analysis plots

Multi-subject plots (plotTemporal, plotBar, plotScatter, plotHeatmap, plotTopo, plotLME/plotTopoLME, plotComposite) live on the exploreFNIRS.core.Experiment class and share a common Colors/ ColorScheme and SavePath/SaveWidth/SaveHeight/SaveDPI convention. See Group Analysis for the full option catalog; in brief:

[ex, allData] = pf2.import.sampleData.group();
ex.groupby({'Group'}); ex.aggregate();

ex.plotTemporal('Biomarkers', {'HbO'}, 'ErrorType', 'SEM');
ex.plotBar('Biomarker', 'HbO', 'ShowIndividual', true);
ex.plotHeatmap('Biomarker', 'HbO', 'Colormap', 'RdBu');
[fig, results] = ex.plotLME('Biomarkers', {'HbO'}, 'SigType', 'q');   % FDR-corrected F-stats

Color customization applies to every plot type — an [N x 3] RGB matrix, a named palette ('Set1', 'tab10', 'viridis'), or an exploreFNIRS.core.ColorScheme for hierarchical per-factor colors:

cs = exploreFNIRS.core.ColorScheme();
cs = cs.set('Group', 'Young', [0.2, 0.6, 0.9]);
cs = cs.set('Condition', 'Task', 'darken', 0.15);
ex.colorScheme = cs;             % auto-applies to all subsequent plots
cs.preview();                    % preview resolved colors before plotting

Headless saving rules

2D plots (pf2.data.plot.oxy/raw, imageValues, topo with the default '2d' view, group-analysis plots): either pattern works —

fig = figure('Visible', 'off');
pf2.data.plot.oxy(processed);
saveas(fig, 'output.png');

or (preferred) the built-in 'savePath' option, which every plotting function above accepts.

3D renders (showProbe3D, showHead3D, interpolateValues3D, topo 'View','3d', movie, and every pf2.probe.project.* function) must use the built-in savePath option, or capture imgOut and imwrite it. The generic off-screen figure('Visible','off') + saveas/exportgraphics pattern is unreliable for 3D — its failure mode is platform-dependent: a blank image on some headless setups (Linux/SSH, software GL), or a dark-background image plus an exit-time crash under macOS -nodisplay.

pf2.probe.project.fstats(Fvals, processed, 'savePath', 'fstats.png');     % correct, stable
[~, imgOut] = pf2.probe.plot.interpolateValues3D(vals, processed);
imwrite(imgOut, 'brain.png');                                              % also correct

All plots automatically adapt to MATLAB's dark mode theme on screen, but saved figures always export with a white background regardless of theme. A persistent preference can force light mode everywhere:

pf2_base.plot.PlotStyle.setForceLightMode(true);   % disable dark mode for pf2
pf2_base.plot.PlotStyle.setForceLightMode(false);  % re-enable theme detection

3D renders also accept a per-call 'ForceLightMode', true override (white background, dark axes/labels) without touching the persistent preference — useful for headless batch scripts that render into figures created with 'Color', 'w'.

See also

Clone this wiki locally