-
Notifications
You must be signed in to change notification settings - Fork 0
Getting Started
This page walks a new user through the complete first workflow in processFNIRS2 (pf2): load a bundled sample recording, convert it to hemoglobin concentrations, look at the result, and produce a trial-averaged waveform — all with no files of your own and no GUI required. Every block below is copy-paste runnable.
You need MATLAB with processFNIRS2 on the path. From the repo root:
addpath(genpath('processFNIRS2'))See Installation if you have not set this up yet.
processFNIRS2 ships a small bundled recording (fNIR Devices fNIR1200, 16 channels) that already contains event markers, so you can exercise the whole pipeline — including block averaging — without supplying your own file.
data = pf2.import.sampleData();Important: assign the output. pf2.import.sampleData() with no output argument opens the interactive channel-check GUI; data = pf2.import.sampleData() loads headlessly. This same assignment rule applies to processFNIRS2 in the next step, and to the importers in general — see Importing Data.
data is a struct with the fields every importer produces:
| Field | Contents |
|---|---|
data.raw |
Raw light intensity, [T x C]
|
data.time |
Time vector in seconds, [T x 1]
|
data.fs |
Sampling rate in Hz |
data.fchMask |
Per-channel good/bad mask (1 = good) |
data.markers |
Event marker table — columns Time, Code, Duration, Amplitude (see below) |
data.info |
Metadata struct (device name, QC status, etc.) |
data.device |
A pf2.Device object describing the probe/montage |
data.markers is a canonical MATLAB table, not a plain matrix — read it by column name:
data.markers.Code % all marker codes
data.markers.Time % all marker onset times (seconds)
unique(data.markers.Code) % which codes are present in this recordingThe sample recording's task marker is code 50; you will use it in Step 4 to build blocks. (A few other codes are present too — inspect unique(data.markers.Code) if you are curious — but 50 is the one this tutorial and the block-averaging recipe use.)
processFNIRS2 runs the three-stage pipeline — raw intensity → optical density → hemoglobin — using the Modified Beer-Lambert Law:
processed = processFNIRS2(data);GUI-suppression rule: assigning an output (processed = processFNIRS2(data)) automatically runs headlessly. A bare call with no output, processFNIRS2(data), opens the interactive GUI instead. Never pass 'ShowGUI', false alongside a captured output — it is redundant; that flag exists only to force the GUI while still capturing an output (processFNIRS2(data, 'ShowGUI', true)).
processed carries everything data had, plus the converted biomarkers:
processed.HbO % [T x C] oxygenated hemoglobin
processed.HbR % [T x C] deoxygenated hemoglobin
processed.HbTotal % [T x C] HbO + HbR
processed.HbDiff % [T x C] HbO - HbR
processed.CBSI % [T x C] cerebral blood saturation index
processed.units % e.g. 'uM'
processed.processingInfo % DPF mode, baseline settings, methods used, etc.You can pass processing options directly (baseline window, DPF mode, subject age, named methods) — see Processing Pipeline for the full parameter list. Defaults are reasonable for a first look:
processed = processFNIRS2(data, ...
'DPFmode', 'Calc', ... % age-dependent DPF (Scholkmann & Wolf, 2013)
'defaultSubjectAge', 30, ...
'blLength', 10, ... % 10 s baseline
'blStartTime', 0);pf2.data.plot.oxy(processed, 5); % HbO/HbR for channel 5
pf2.data.plot.oxy(processed); % all channels
pf2.data.plot.oxy(processed, 'baseline', 10); % with a 10 s baseline correctionTo save instead of (or in addition to) displaying, use the built-in 'savePath' option — this is the headless-safe pattern for 2D plots:
pf2.data.plot.oxy(processed, 5, 'savePath', 'ch5.png');pf2.probe.plot.topo(processed, 'HbO'); % 2D heatmap, time-averaged
pf2.probe.plot.topo(processed, 'HbO', 'Time', [10 30]); % mean over a 10-30 s window
pf2.probe.plot.topo(processed, 'HbO', 'savePath', 'topo.png');View', '3d' renders the same data on a cortical surface instead of a flat probe layout (requires MNI coordinates on the device). See Visualization for the rendering options, including the headless-3D 'savePath' requirement.
This is the canonical recipe for turning a continuous recording with event markers into a trial-averaged waveform: define blocks → extract time-locked epochs → average.
data = pf2.import.sampleData(); % recording WITH markers
proc = processFNIRS2(data); % -> HbO/HbR/...
% 1. Define blocks from a marker code (Embed=false returns the block ARRAY;
% Embed=true, the default, returns the data struct with .blocks embedded)
blocks = pf2.data.defineBlocks(proc, 50, 15, 'Embed', false); % code 50, 15 s
% 2. Cut time-locked epochs. ALWAYS set PreTime/PostTime — left unset, each
% side falls back to a small default Buffer (~2 s), so set them to size
% the epoch deliberately.
segments = pf2.data.extractBlocks(proc, blocks, ...
'PreTime', 5, 'PostTime', 15, 'SetT0', true); % onset at t=0
% 3. Trial/grand average onto a common grid (one call)
ga = pf2.data.blockAverage(segments); % or pf2.data.grandAverage
plot(ga.time, ga.HbO.Mean(:,1)); % averaged HbO, channel 1Walking through what each step returns:
-
defineBlocks(proc, 50, 15, 'Embed', false)scansproc.markersfor every marker with code50and builds one block per onset, each 15 seconds long. It returns a struct array (one element per block) with.startTime,.endTime,.markerCode, and an.infostruct. Pass'Embed', true(the default) instead to get back theprocstruct with the blocks stored onproc.blocks, so a later call can omit the second argument:pf2.data.extractBlocks(proc). -
extractBlockscuts one fNIRS-shaped struct per block, padded byPreTimeseconds before onset andPostTimeseconds after the block ends, and (withSetT0, the default) shifts each segment's time axis so the block onset sits att = 0. The result is a cell array{1 x N}, one struct per epoch — exactly the shapeexploreFNIRS.core.Experimentand the group-analysis layer expect. -
blockAverageresamples every segment onto one shared time grid anchored att = 0(segments cut around markers rarely land on identical sample times) and then averages across them. The returned structgaholds, for each biomarker (HbO,HbR,HbTotal,HbDiff,CBSI), a sub-struct with.Mean,.SEM,.SD,.N,.Median,.Max,.Min— each[T x C]— plusga.timeandga.units.
Add a baseline correction window and plot with error bands:
segments = pf2.data.extractBlocks(proc, blocks, ...
'PreTime', 5, 'PostTime', 15, ...
'BaselineWindow', [-5, 0], ... % subtract the pre-onset mean
'SetT0', true);
ga = pf2.data.blockAverage(segments);
figure;
plot(ga.time, ga.HbO.Mean(:,1), 'r', 'LineWidth', 1.5); hold on;
plot(ga.time, ga.HbR.Mean(:,1), 'b', 'LineWidth', 1.5);
xlabel('Time (s)'); ylabel(['\Delta Hb (' ga.units ')']);
legend({'HbO','HbR'});For marker inspection before you epoch, or for multiple/OR-combined codes:
times = pf2.data.getMarkers(proc, 50); % all onsets of code 50
times = pf2.data.getMarkers(proc, [50; 51]); % column vector = code 50 OR 51This single-subject recipe is the foundation for the multi-condition and multi-subject workflows on Block Averaging and Epoching and Group Analysis, which use the same segments cell array as input to exploreFNIRS.core.Experiment.
- Importing Data — every supported device format, batch/directory import, and metadata attachment
- Processing Pipeline — the three processing stages, DPF modes, and available methods in depth
- Block Averaging and Epoching — multi-condition epoching, sliding windows, and marker dictionaries
-
Group Analysis — the
exploreFNIRS.core.Experimentclass: grouping, aggregation, LME statistics, and group plots
processFNIRS2
Getting Started
Core Workflow
Group Analysis
Visualization & Export
Reference