-
Notifications
You must be signed in to change notification settings - Fork 0
Quality Control
processFNIRS2 ships two parallel ways to review channel quality: an interactive GUI for eyes-on review, and a headless programmatic pipeline for scripts and batch runs. This page is for anyone deciding which bad channels to drop before processing, or automating that decision across many subjects.
| Path | Use when | Entry point |
|---|---|---|
| Interactive GUI | Reviewing one dataset (or a stack of files) by eye, on a display | pf2.qc.ChannelCheck |
| Headless pipeline | Batch/-batch runs, unattended processing, reproducible thresholds |
pf2.qc.pipeline.assess → apply
|
| One-call summary | A quick QC report + figures to disk, no GUI, no manual pipeline calls | pf2.qc.snapshot |
All three read/write the same data.fchMask convention: 1 = good,
0.5 = marginal/noisy, 0 = rejected.
pf2.qc.ChannelCheck is an App Designer GUI for interactive channel review —
per-channel raw/Hb traces, a probe-arranged mini-plot grid, undo/redo, bulk
accept/reject, and integrated QC metrics.
data = pf2.import.sampleData.fNIR2000();
% Single dataset
app = pf2.qc.ChannelCheck(data);
% ... review channels in the window, then close it ...
data = app.OutputData;
delete(app);
% Multiple files: prev/next navigation between datasets in the cell array
allData = {data1, data2, data3};
app = pf2.qc.ChannelCheck(allData);
allData = app.OutputData;
delete(app);After the window closes, read the result from app.OutputData (a struct for
single-dataset mode, or a cell array for multi-file mode) — always follow with
delete(app).
Under -batch MATLAB, the GUI's save/close path opens a blocking
uiconfirm dialog and errors — there is no display to click through it.
Pass 'SkipConfirmation', true to suppress all confirmation dialogs and make
the app usable non-interactively:
app = pf2.qc.ChannelCheck(data, 'SkipConfirmation', true);
data = app.OutputData;
delete(app);For fully unattended runs (no GUI at all), prefer the programmatic pipeline below instead of driving the GUI headlessly.
pf2.qc.snapshot runs the full QC pipeline and writes a set of diagnostic
figures straight to disk — no GUI, no manual assess/plotReport calls. It
is the fast, scriptable counterpart to ChannelCheck.
data = pf2.import.sampleData.fNIR2000();
report = pf2.qc.snapshot(data, 'SaveDir', 'qc_out');
pf2.qc.pipeline.report(report); % also print the text summaryFiles written under SaveDir (optionally prefixed with 'Prefix'):
| File | Content |
|---|---|
qc_dashboard.png |
SCI / cardiac / CoV / Takizawa 4-panel dashboard |
qc_psd.png |
Per-channel power spectra (tiled) |
qc_sci.png |
SCI bar chart (only written when SCI was not skipped) |
Any extra name-value pairs ('Checks', 'SCIThreshold', 'CoVThreshold',
...) forward straight to pf2.qc.pipeline.assess. The returned report is
the same QC report struct as assess, with an added
report.snapshot.files listing the saved paths.
This is the recommended path for unattended/batch runs, since it needs no display and its thresholds are explicit and reproducible.
data = pf2.import.sampleData.fNIR2000();
report = pf2.qc.pipeline.assess(data); % all applicable checks, defaults
report = pf2.qc.pipeline.assess(data, ... % custom thresholds
'SCIThreshold', 0.8, 'CoVThreshold', 0.15);
report = pf2.qc.pipeline.assess(data, ... % run only some checks
'Checks', {'saturation', 'sci', 'cov'});assess does its own lightweight internal Raw → OD → Hb conversion (simple
Beer-Lambert, default DPF) purely to feed the Takizawa rules — it does not
touch or require the main processFNIRS2 pipeline, and it does not modify
data.
Checks (run in this order, default set {'saturation','sci','cardiac','cov','takizawa'}):
| Check | What it measures | Signal stage | Key parameters |
|---|---|---|---|
saturation |
Fraction of samples at the raw intensity floor (0) or device ceiling | raw |
SaturationThreshold (default 0.1) |
sci |
Scalp Coupling Index — cross-correlation of cardiac pulsations across the two wavelengths (Pollonini et al. 2014, DOI: 10.1016/j.heares.2013.11.007) | raw |
SCIThreshold (default 0.75), CardiacBand (default [0.5 2.5]) |
cardiac |
Cardiac peak presence/SNR in the power spectrum | raw |
CardiacSNR (default 3), CardiacBand
|
cov |
Coefficient of variation of the raw signal (works at any sampling rate) | raw |
CoVThreshold (default 0.2) |
takizawa |
Four empirical hemoglobin-signal rules — high-freq noise, low-freq/anti-correlation noise, zero variance, body-movement jumps (Takizawa et al. 2008, DOI: 10.1016/j.schres.2007.10.025) | filtered Hb (internal) |
TakizawaStrict (default false) |
sci and cardiac need cardiac-band content and are only meaningful above
roughly 5 Hz; at lower sampling rates (e.g. the fNIR 1200 at ~2 Hz) they are
automatically skipped — every channel passes them by default so a
low-fs device is never penalized for a check it structurally cannot run —
and the reason is recorded on report.<check>.skipReason. cov and
takizawa work at any sampling rate ≥ 2 Hz. For a low-fs device, restrict to
what applies: pf2.qc.pipeline.assess(data, 'Checks', {'cov','takizawa'}).
data = pf2.qc.pipeline.apply(data, report); % reject on all checks
data = pf2.qc.pipeline.apply(data, report, 'Checks', {'sci', 'cov'}); % only these two
data = pf2.qc.pipeline.apply(data, report, 'MarkNoisy', true); % 0.5 instead of 0apply ANDs the selected checks' pass/fail with the existing fchMask,
so a channel that was already rejected (e.g. by a prior manual review) is
never promoted back to good. With 'MarkNoisy', true, channels that fail QC
but were previously good are set to 0.5 (marginal) rather than 0
(rejected). The full report is stored back on data.qcReport for
traceability.
pf2.qc.pipeline.report(report); % channel-by-channel text table
fig = pf2.qc.pipeline.plotReport(report, 'Title', 'Subject 01');
pf2.qc.pipeline.plotReport(report, 'Visible', 'off', 'SavePath', 'qc.png'); % headless-safereport prints a per-channel table (one row per channel, one column per
check, * marking a failed value) plus a summary line and any skipped-check
notes. plotReport renders the same 4-panel dashboard snapshot saves to
disk (SCI bars, cardiac SNR bars, CoV bars, Takizawa rule heatmap) — pass
'SavePath' for headless saving (2D figure, so the ordinary
figure('Visible','off') + saveas pattern also works, but 'SavePath' is
preferred).
Individual metrics can also be computed and plotted standalone:
sciResult = pf2.qc.sci(data); % SCI only
psdResult = pf2.qc.powerSpectrum(data, 'Signal', 'raw'); % PSD + peak detection
pf2.qc.plotQuality(sciResult); % dispatches on result type
pf2.qc.plotQuality(psdResult, 'Layout', 'tiled');report.channels % 1:nChannels
report.fs % sampling rate used
report.checkNames % cellstr of checks that were run, e.g. {'saturation','sci','cardiac','cov','takizawa'}
report.pass % [1 x nChannels] logical — AND across ALL run checks
report.summary % per-channel summary table
% Per-check sub-structs, one per name in report.checkNames:
report.sci.values report.sci.pass report.sci.skipped report.sci.threshold
report.cardiac.detected report.cardiac.snr report.cardiac.pass report.cardiac.skipped
report.cov.values report.cov.pass
report.takizawa.rules report.takizawa.ruleNames report.takizawa.pass
report.saturation.floorPct report.saturation.ceilPct report.saturation.passEvery check struct carries a .pass mask sized [1 x nChannels]; checks that
can measure a continuous quantity also carry .values (or a check-specific
name like .snr), and checks that can be skipped at low sampling rates carry
.skipped (+ .skipReason when true). report.pass is the AND of every
.pass mask across report.checkNames — this is what apply uses by
default.
% Step 1: Import (raw data)
raw = pf2.import.sampleData.fNIR2000();
% Step 2: QC assessment (headless, reproducible)
qcReport = pf2.qc.pipeline.assess(raw);
% Step 3: Apply QC — reject bad channels BEFORE processing
raw = pf2.qc.pipeline.apply(raw, qcReport);
% Step 4: Process
processed = processFNIRS2(raw);
% Step 5: The QC report travels with the data for traceability
disp(raw.qcReport.checkNames);QC runs on raw data, before the three-stage processing pipeline — see Processing Pipeline.
Importers (pf2.import.importNIR, importNIRX, importSNIRF,
importHitachiMES, ...) normally open the interactive channel-check GUI when
no saved *_CH.mat mask exists for a recording. That GUI is automatically
skipped whenever it cannot or should not block:
- a headless/
-batchMATLAB session, - code running under the
matlab.unittestframework, - or the GUI explicitly disabled via the flag below.
When suppressed, import instead loads a saved mask if one exists, or
defaults to all channels good — emitting a
pf2:loadExistingMaskOrCheck:guiSuppressed warning so the omission is not
silent. This means an unattended -batch import will not reject anything on
its own; always follow batch imports with the programmatic QC pipeline
(assess → apply) so bad/saturated channels are actually rejected.
The path taken is recorded on data.info.qcStatus:
qcStatus value |
Meaning |
|---|---|
'mask_loaded' |
A saved *_CH.mat sidecar was found and loaded |
'gui_reviewed' |
The channel-check GUI ran and a human reviewed channels |
'unreviewed_default' |
GUI was suppressed; all channels defaulted to good — run assess/apply
|
To force the GUI off explicitly (e.g. scripting on a machine with a live display, or forcing it on inside a test):
prev = pf2_base.channelCheckGUIEnabled(false); % force the GUI off
c = onCleanup(@() pf2_base.channelCheckGUIEnabled(prev)); % restore on exit
data = pf2.import.sampleData(); % no GUI, even on a displayprocessFNIRS2
Getting Started
Core Workflow
Group Analysis
Visualization & Export
Reference