Skip to content

Getting Started

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

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.

Before you start

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.

1. Load sample data

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 recording

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

2. Process: raw intensity → hemoglobin

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

3. Look at the data

A single channel's time course

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 correction

To 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');

A topographic map across the probe

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.

4. Block-average a task response

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 1

Walking through what each step returns:

  • defineBlocks(proc, 50, 15, 'Embed', false) scans proc.markers for every marker with code 50 and builds one block per onset, each 15 seconds long. It returns a struct array (one element per block) with .startTime, .endTime, .markerCode, and an .info struct. Pass 'Embed', true (the default) instead to get back the proc struct with the blocks stored on proc.blocks, so a later call can omit the second argument: pf2.data.extractBlocks(proc).
  • extractBlocks cuts one fNIRS-shaped struct per block, padded by PreTime seconds before onset and PostTime seconds after the block ends, and (with SetT0, the default) shifts each segment's time axis so the block onset sits at t = 0. The result is a cell array {1 x N}, one struct per epoch — exactly the shape exploreFNIRS.core.Experiment and the group-analysis layer expect.
  • blockAverage resamples every segment onto one shared time grid anchored at t = 0 (segments cut around markers rarely land on identical sample times) and then averages across them. The returned struct ga holds, for each biomarker (HbO, HbR, HbTotal, HbDiff, CBSI), a sub-struct with .Mean, .SEM, .SD, .N, .Median, .Max, .Min — each [T x C] — plus ga.time and ga.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 51

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

Where to go next

  • 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.Experiment class: grouping, aggregation, LME statistics, and group plots

See also

Clone this wiki locally