% GLM Analysis Script for fMRI Data using SPM
%
% This script performs a General Linear Model (GLM) analysis on fMRI data that has been preprocessed using fMRIprep and
% is organized in BIDS format. It is designed to run in MATLAB using SPM, specifically for researchers who may be
% unfamiliar with SPM scripting but have fMRI data analysis needs. The script follows standard steps for GLM setup,
% specification, estimation, and contrast definition, which are essential in understanding task-related brain activity.
%
% Overview of What This Script Does:
% - Sets up paths and parameters.
% - Loads event and confound data for each run and prepares it for GLM analysis.
% - Specifies the GLM parameters, including timing, high-pass filter, and basis function.
% - Runs model specification and estimation to fit the GLM to the fMRI data.
% - Defines contrasts to test specific hypotheses.
% - Saves the final model and a copy of this script for future reference.
%
% Steps in this Script:
% 1. **Set Parameters**: Define paths to data, fMRI timing parameters, selected tasks, contrasts, and subjects.
% 2. **Load Events and Confounds**: Each run`s event (trial timing) and confound (noise regressors) data are loaded
% from BIDS-compliant TSV and JSON files produced by fMRIprep. This prepares each run for GLM analysis.
% 3. **SPM Model Specification**: Sets up a structure (called`matlabbatch`) that stores all necessary parameters
% for defining the GLM model. Each parameter here mirrors options in the SPM GUI.
% 4. **Run Model Specification and Estimation**: Once all parameters are set in`matlabbatch`, we run batches 1
% and 2. Batch 1 specifies the model, and batch 2 estimates the model using the data and settings we’ve defined.
% 5. **Check SPM.mat File**: Verifies the existence of the SPM.mat file after running the model, confirming the
% model specification and estimation steps were successful.
% 6. **Define and Run Contrasts**: Sets up contrasts based on hypotheses about brain activity, applies them, and
% stores results. This is done in batch 3.
% 7. **Save Script**: Saves a copy of this script in the output folder for reproducibility and future reference.
% Working with matlabbatch and SPM Batches:
% - `matlabbatch` is a structure used by SPM to store each step of an analysis, organized as separate fields for
% each processing stage (e.g., model specification, model estimation, contrast definition). Each entry in
% `matlabbatch` corresponds to a batch operation you could run manually in the SPM GUI. SPM’s batch system is
% powerful because it allows us to set parameters programmatically in a way that is identical to the GUI.
% - **Setting Parameters**: Each field in `matlabbatch` represents a setting or action, matching options in the
% SPM GUI. For example, in this script, `matlabbatch{1}.spm.stats.fmri_spec.timing.RT` corresponds to setting
% the Repetition Time (RT) in the SPM GUI.
% - **Retrieving GUI Code**: To see the code for any parameter set in the SPM GUI, configure the GUI settings and
% then select **File > Save Batch and Script**. SPM will generate MATLAB code that mirrors the GUI settings,
% which you can use to understand how `matlabbatch` fields align with GUI options.
%
% Understanding SPM Batches and Sessions:
% - In SPM, **sessions** represent individual runs in a study. Each run (session) is specified separately within
% `matlabbatch`, which enables you to specify different conditions or timing settings per run if necessary.
% - We use indexing within `matlabbatch` to indicate which session or contrast we’re defining. For instance,
% `matlabbatch{1}.spm.stats.fmri_spec.sess(runIdx).scans` refers to the scans (images) in session `runIdx`.
%
% How SPM Executes Batches:
% - SPM uses a deferred execution approach: parameters are specified first by configuring `matlabbatch`, and
% then `spm_jobman('run', matlabbatch)` initiates all specified processing steps in sequence, similar to
% clicking "Run" in the GUI. In this script, we define `matlabbatch` elements in sequence (batches 1 to 3)
% and run them together in one command to automate the workflow.
%
% Assumptions about Input Data:
% - Data must be preprocessed using fMRIprep and follow the BIDS format.
% - Event files (in TSV format) must adhere to BIDS specifications.
% - Confound files (in JSON and TSV formats) must adhere to fMRIprep output standards.
%
% Requirements:
% - SPM12 (or a compatible version) must be installed and added to the MATLAB path.
%
% Note: This script automates the process typically done in the SPM GUI.
% Parameters:
% - selectedSubjectsList: List of subject IDs to include in the analysis. Can be a list of integers or '*' to include all subjects.
% - selectedRuns: List of run numbers to include in the analysis. Can be a list of integers or '*' to include all runs.
% - selectedTasks: Structure array with information about the task(s) to analyze.
% Each task must have the following fields:
% - name: String. The name of the task.
% - contrasts: Cell array of strings. The name(s) of the contrast(s).
% - weights: Cell array of structs. The weights for the contrast(s).
% - smoothBool: Boolean. Whether to smooth the data before the GLM.
%
% Example of defining tasks, weights, and contrasts:
% selectedTasks(1).name = 'loc';
% selectedTasks(1).contrasts = {'Faces > Objects', 'Objects > Scrambled', 'Scenes > Objects'};
% selectedTasks(1).weights(1) = struct('Faces', 1, 'Objects', -1, 'Scrambled', 0, 'Scenes', 0);
% selectedTasks(1).weights(2) = struct('Faces', 0, 'Objects', 1, 'Scrambled', -1, 'Scenes', 0);
% selectedTasks(1).weights(3) = struct('Faces', 0, 'Objects', -1, 'Scrambled', 0, 'Scenes', 1);
% selectedTasks(1).smoothBool = true;
%
% Paths:
% - fmriprepRoot: Path to the root folder of the fMRIprep output.
% - BIDSRoot: Path to the root folder of the BIDS dataset.
% - outRoot: Path to the output root folder for the analysis results.
%
% Preprocessing Options (uses fMRIprep confounds table):
% - pipeline: Denoising pipeline strategy for SPM confound regression.
% It should be a cell array of strings specifying the strategies to use.
% Possible strategies:
% HMP - Head motion parameters (6,12,24)
% GS - Brain mask global signal (1,2,4)
% CSF_WM - CSF and WM masks global signal (2,4,8)
% aCompCor - aCompCor (10,50)
% MotionOutlier - motion outliers FD > 0.5, DVARS > 1.5, non-steady volumes
% Cosine - Discrete cosine-basis regressors (low frequencies) -> HPF
% FD - Raw framewise displacement
% Null - Returns a blank data frame
%
% Example:
% pipeline = {'HMP-6','GS-1','FD'};
% This selects the 6 head motion parameters, the global signal, and the raw Framewise Displacement value.
%
% Author: Andrea Ivan Costantino
% Date: 5 July 2023
% This section of the script defines parameters and settings necessary for a GLM analysis in SPM, using fMRI data
% preprocessed with fMRIPrep. We first set up the required paths, select specific subjects, tasks, and contrasts,
% and define analysis parameters like repetition time and timing information. The final block iterates through
% subjects and tasks, setting up each run of the GLM and checking for the existence of the required data before
% beginning the analysis. This setup ensures a consistent pipeline for different subjects and tasks.
clc
clear
%% PARAMETERS
% Define the space of the images ('T1w' or 'MNI'); in this case, we use MNI space at 2mm resolution.
niftiSpace = 'MNI152NLin2009cAsym_res-2';
% Define the root directory for the BIDS dataset (where the data is stored).
BIDSRoot = '/data/projects/chess/data/BIDS';
% Define paths relative to the BIDS root:
fmriprepRoot = fullfile(BIDSRoot, 'derivatives', 'fmriprep'); % Path to fMRIprep output data.
outRoot = fullfile(BIDSRoot, 'derivatives', 'fmriprep-SPM-TEST', niftiSpace); % Directory for storing analysis results.
tempDir = fullfile(BIDSRoot, 'derivatives', 'fmriprep-preSPM'); % Temporary storage for intermediate files.
% Define fMRI timing parameters.
RT = 2; % Repetition time (TR) in seconds, the time between consecutive volume acquisitions.
fmri_t = 60; % Microtime resolution (number of slices).
fmri_t0 = fmri_t / 2; % Microtime onset (reference slice for alignment).
% Define the subjects and runs to include in the analysis.
selectedSubjectsList = [1,2,3]; % List of subject IDs to include (or use '*' for all subjects).
selectedRuns = '*'; % Runs to include, with '*' indicating all available runs.
% Define tasks with contrasts and associated weights for GLM analysis.
% Each task contains fields: name, contrasts, weights, and smoothBool.
% Task 1: Localizer Task
selectedTasks(1).name = 'loc'; % Name of task as used in BIDS.
selectedTasks(1).contrasts = {'Faces > Objects', 'Objects > Scrambled', 'Scenes > Objects'}; % Contrasts of interest.
selectedTasks(1).weights(1) = struct('Faces', 1, 'Objects', -1, 'Scrambled', 0, 'Scenes', 0); % Weights for contrast 1.
selectedTasks(1).weights(2) = struct('Faces', 0, 'Objects', 1, 'Scrambled', -1, 'Scenes', 0); % Weights for contrast 2.
selectedTasks(1).weights(3) = struct('Faces', 0, 'Objects', -1, 'Scrambled', 0, 'Scenes', 1); % Weights for contrast 3.
selectedTasks(1).smoothBool = true; % Apply smoothing before GLM for this task.
% Task 2: Experimental Task
selectedTasks(2).name = 'exp';
selectedTasks(2).contrasts = {'Check > No-Check'};
selectedTasks(2).weights(1) = struct('check', 1, 'nocheck', -1); % Weights for contrast 1.
selectedTasks(2).smoothBool = false; % Do not apply smoothing for this task.
% Specify confound regressors to include in the GLM.
pipeline = {'HMP-6','GS-1','FD'}; % Use head motion parameters, global signal, and framewise displacement.
% Find folders for each subject based on the selected subjects list.
sub_paths = findSubjectsFolders(fmriprepRoot, selectedSubjectsList);
%% RUN THE GLM FOR EACH SUBJECT AND TASK
% Loop through each subject in the list.
for sub_path_idx = 1:length(sub_paths)
% Loop through each defined task for analysis.
for selected_task_idx = 1:length(selectedTasks)
% Clear previous configurations in matlabbatch to avoid conflicts.
clearvars matlabbatch
%% SUBJECT AND TASK INFO
% Get the current task details (name, contrasts, smoothing preference).
selectedTask = selectedTasks(selected_task_idx).name; % Task name.
contrasts = selectedTasks(selected_task_idx).contrasts; % Task contrasts.
smoothBool = selectedTasks(selected_task_idx).smoothBool; % Boolean: smooth images before GLM?
% Extract the subject ID from the folder name (formatted as 'sub-01').
subPath = sub_paths(sub_path_idx);
subName = subPath.name; % e.g., 'sub-01'
sub_id = strsplit(subName,'-'); % Splits 'sub-01' into {'sub', '01'}
selectedSub = str2double(sub_id{2}); % Extracts and converts the subject ID to a number.
% Define the output path for storing GLM results for this subject and task.
outPath = fullfile(outRoot, 'GLM', subName, selectedTask);
% Define paths to the functional data for this subject.
funcPathSub = fullfile(fmriprepRoot, subName, 'func'); % Functional data path.
bidsPathSub = fullfile(BIDSRoot, subName, 'func'); % BIDS formatted path.
% Check if the specified task exists in the subject’s data.
files = dir(funcPathSub); % List files in the functional data directory.
fileNames = {files.name}; % Extract file names.
containsTask = any(contains(fileNames, ['task-', selectedTask])); % Check for the task in file names.
% If the task is missing for this subject, skip to the next iteration.
if ~containsTask
warning(['Task ', selectedTask, ' not found for ' subName ' in ' funcPathSub '. Skipping...']);
continue;
end
% Print a status update showing the subject and task being processed.
fprintf('############################### \n# STEP: running %s - %s #\n############################### \n', subName, selectedTask);
% This first section finds and loads the event and confound files for each run, based on selections specified earlier.
% We retrieve and organize the event data (which includes onset times and durations) and confound data
% (like motion parameters or physiological noise) for each run of a task. These inputs are essential for setting up the
% GLM model, as they provide information needed for modeling the BOLD signal in relation to task events and controlling
% for noise.
%% FIND AND LOAD EVENTS AND CONFOUNDS FROM FMRIPREP FOLDER
% Retrieve event and confound files for selected runs.
if ismember('*', selectedRuns)
% If '*' is specified, include all available runs for this subject and task.
eventsTsvFiles = dir(fullfile(bidsPathSub, strcat(subName, '_task-', selectedTask, '_run-*_events.tsv')));
json_confounds_files = dir(fullfile(funcPathSub, strcat(subName, '_task-', selectedTask, '_run-*_desc-confounds_timeseries.json')));
tsv_confounds_files = dir(fullfile(funcPathSub, strcat(subName, '_task-', selectedTask, '_run-*_desc-confounds_timeseries.tsv')));
else
% Include only the specific runs listed in `selectedRuns`.
eventsTsvFiles = arrayfun(@(x) dir(fullfile(bidsPathSub, strcat(subName, '_task-', selectedTask, '_run-', sprintf('%01d', x), '_events.tsv'))), selectedRuns, 'UniformOutput', true);
json_confounds_files = arrayfun(@(x) dir(fullfile(funcPathSub, strcat(subName, '_task-', selectedTask, '_run-', sprintf('%01d', x), '_events.json'))), selectedRuns, 'UniformOutput', true);
tsv_confounds_files = arrayfun(@(x) dir(fullfile(funcPathSub, strcat(subName, '_task-', selectedTask, '_run-', sprintf('%01d', x), '_events.tsv'))), selectedRuns, 'UniformOutput', true);
end
% Sort the retrieved files alphabetically by name for consistent ordering.
eventsTsvFiles = table2struct(sortrows(struct2table(eventsTsvFiles), 'name'));
json_confounds_files = table2struct(sortrows(struct2table(json_confounds_files), 'name'));
tsv_confounds_files = table2struct(sortrows(struct2table(tsv_confounds_files), 'name'));
% Ensure the number of event and confound files matches across all run files.
assert(numel(eventsTsvFiles) == numel(json_confounds_files) && numel(json_confounds_files) == numel(tsv_confounds_files), ...
'Mismatch in number of TSV events, TSV confounds, and JSON confounds files in %s', funcPathSub)
% This second section sets non-run-specific SPM model parameters, defining the core structure of the GLM model.
% Here we specify settings such as the repetition time, microtime resolution, basis functions, and whether global
% normalization should be applied. These parameters remain consistent across different runs and subjects, creating
% a standardized foundation for the GLM analysis.
%% SPM MODEL PARAMETERS (NON RUN DEPENDENT)
% Define non-run-specific parameters for the GLM in SPM.
matlabbatch{1}.spm.stats.fmri_spec.dir = cellstr(outPath); % Directory where SPM output will be saved.
matlabbatch{1}.spm.stats.fmri_spec.timing.units = 'secs'; % Units for event timing (set to seconds).
matlabbatch{1}.spm.stats.fmri_spec.timing.RT = RT; % Repetition time (TR) defines time between successive scans.
matlabbatch{1}.spm.stats.fmri_spec.timing.fmri_t = fmri_t; % Microtime resolution - number of time bins within one TR.
matlabbatch{1}.spm.stats.fmri_spec.timing.fmri_t0 = fmri_t0; % Microtime onset - reference slice for alignment.
matlabbatch{1}.spm.stats.fmri_spec.fact = struct('name', {}, 'levels', {}); % No factorial design applied here.
matlabbatch{1}.spm.stats.fmri_spec.bases.hrf.derivs = [0 0]; % Canonical HRF without derivatives (time/dispersion).
matlabbatch{1}.spm.stats.fmri_spec.volt = 1; % Volterra interactions disabled (linear model).
matlabbatch{1}.spm.stats.fmri_spec.global = 'None'; % No global normalization applied.
matlabbatch{1}.spm.stats.fmri_spec.mthresh = 0.8; % Masking threshold excludes voxels below 80% of mean signal.
matlabbatch{1}.spm.stats.fmri_spec.mask = {''}; % No explicit mask specified.
matlabbatch{1}.spm.stats.fmri_spec.cvi = 'AR(1)'; % AR(1) autocorrelation correction for temporal dependencies.
% Set up parameters for estimating the GLM in the second stage.
matlabbatch{2}.spm.stats.fmri_est.spmmat(1) = cfg_dep('fMRI model specification: SPM.mat File', ...
substruct('.','val', '{}',{1}, '.','val', '{}',{1}, '.','val', '{}',{1}), substruct('.','spmmat'));
matlabbatch{2}.spm.stats.fmri_est.write_residuals = 0; % Do not save residuals.
matlabbatch{2}.spm.stats.fmri_est.method.Classical = 1; % Use classical estimation method (ReML).
% This section iterates over each run of the task for the current subject, setting up the required SPM parameters
% for each run. We load event and confound data, identify the correct NIfTI files, apply smoothing if required,
% and configure session-specific settings in the SPM model specification. Each step is verified to ensure data
% integrity, and any issues with missing or multiple files are flagged for user review.
%% SPM RUN SETTINGS (E.G., EVENTS, CONFOUNDS, IMAGES)
% Loop over each run of the task.
for runIdx = 1:numel(eventsTsvFiles)
% Load event information into a structure compatible with SPM.
events_struct = eventsBIDS2SPM(fullfile(eventsTsvFiles(runIdx).folder, eventsTsvFiles(runIdx).name));
selectedRun = findRunSubstring(eventsTsvFiles(runIdx).name); % Identify the current run (e.g., 'run-01').
fprintf('## STEP: TASK %s - %s\n', selectedTask, selectedRun)
% Select the corresponding confound files for the current run.
jsonRows = filterRowsBySubstring(json_confounds_files, selectedRun);
confoundsRows = filterRowsBySubstring(tsv_confounds_files, selectedRun);
% Check for a single JSON confounds file, and flag errors if multiple or none are found.
if length(jsonRows) ~= 1
error('More than one JSON file found for the specified run. Please check the dataset.');
elseif isempty(jsonRows)
error('No JSON file found for the specified run. Please check the dataset.');
else
jsonRow = jsonRows{1, :};
end
% Check for a single TSV confounds file, and handle errors similarly.
if length(confoundsRows) ~= 1
error('More than one TSV confounds file found for the specified run. Please check the dataset.');
elseif isempty(confoundsRows)
error('No TSV confounds file found for the specified run. Please check the dataset.');
else
confoundsRow = confoundsRows{1, :};
end
% Extract confound data based on the specified pipeline, loading it into an array.
confounds_array = fMRIprepConfounds2SPM(fullfile(jsonRow.folder, jsonRow.name),...
fullfile(confoundsRow.folder, confoundsRow.name), pipeline);
% Define the pattern to locate the NIfTI file and check for its existence.
filePattern = strcat(subName, '_task-', selectedTask, '_', selectedRun, '_space-', niftiSpace, '_desc-preproc_bold');
niiFileStruct = dir(fullfile(fmriprepRoot, subName, 'func', strcat(filePattern, '.nii')));
% Handle cases with missing or multiple NIfTI files.
if size(niiFileStruct, 1) > 1
error('Multiple NIFTI files found for %s.', selectedRun)
elseif isempty(niiFileStruct)
% If no NIfTI files are found, check for compressed (.nii.gz) files and decompress if necessary.
niiGzFilePattern = fullfile(funcPathSub, strcat(filePattern, '.nii.gz'));
niiGzFileStruct = dir(niiGzFilePattern);
if isempty(niiGzFileStruct)
warning('No NIFTI file found for run %s. SKIPPING!', selectedRun)
continue
elseif size(niiGzFileStruct, 1) > 1
error('Multiple NIFTI.GZ files found for this run.')
else
% Decompress the .nii.gz file if found.
niiGzFileString = fullfile(niiGzFileStruct.folder, niiGzFileStruct.name);
gunzippedNii = gunzipNiftiFile(niiGzFileString, fullfile(tempDir, 'gunzipped', subName));
niiFileStruct = dir(gunzippedNii{1});
end
end
% Get the full path of the (possibly decompressed) NIfTI file.
niiFileString = fullfile(niiFileStruct.folder, niiFileStruct.name);
% Smooth the NIfTI file if smoothing is enabled for this task.
if smoothBool
niiFileString = smoothNiftiFile(niiFileString, fullfile(tempDir, 'smoothed', subName));
else
fprintf('SMOOTH: smoothBool set to false for this task. Skipping...\n')
end
% Specify functional images in SPM model specification for the current run.
niiFileCell = {niiFileString};
matlabbatch{1}.spm.stats.fmri_spec.sess(runIdx).scans = spm_select('expand', niiFileCell); % Expanded list of images.
% Set high-pass filter (HPF) to a value higher than the run duration to avoid filtering.
matlabbatch{1}.spm.stats.fmri_spec.sess(runIdx).hpf = (matlabbatch{1}.spm.stats.fmri_spec.timing.RT * ...
size(matlabbatch{1}.spm.stats.fmri_spec.sess(runIdx).scans, 1)) + 100;
% Add conditions (events) to the model for the current run.
for cond_id = 1:length(events_struct.names)
matlabbatch{1}.spm.stats.fmri_spec.sess(runIdx).cond(cond_id).name = events_struct.names{cond_id};
matlabbatch{1}.spm.stats.fmri_spec.sess(runIdx).cond(cond_id).onset = events_struct.onsets{cond_id};
matlabbatch{1}.spm.stats.fmri_spec.sess(runIdx).cond(cond_id).duration = events_struct.durations{cond_id};
end
% Add confound regressors to the model for the current run.
for reg_id = 1:size(confounds_array, 2)
matlabbatch{1}.spm.stats.fmri_spec.sess(runIdx).regress(reg_id).name = confounds_array.Properties.VariableNames{reg_id};
matlabbatch{1}.spm.stats.fmri_spec.sess(runIdx).regress(reg_id).val = confounds_array{:, reg_id};
end
end
% This final section initializes the SPM defaults, executes the GLM model specification and estimation,
% verifies that the model ran successfully by checking for the SPM.mat file, and then defines and
% applies contrasts. Finally, the script is copied to the output folder for replicability. Each step
% is verified with status messages, providing clear feedback for the user.
%% RUN BATCHES 1 AND 2
% Initialize SPM defaults for fMRI analysis.
spm('defaults','fmri'); % Load SPM defaults for fMRI.
spm_jobman('initcfg'); % Initialize the SPM job configuration.
% Execute the model specification (batch 1) and model estimation (batch 2).
fprintf('GLM: Running GLM for: %s - TASK: %s\n', subName, selectedTask)
spm_jobman('run', matlabbatch(1:2));
fprintf('GLM: DONE!\n')
%% FIND SPM.mat
% After running the GLM, check for the existence of the SPM.mat file in the output path.
% The SPM.mat file is essential for contrast specification.
spmMatPath = fullfile(outPath, 'SPM.mat');
if ~exist(spmMatPath, 'file')
error('SPM.mat file not found in the specified output directory.');
end
%% RUN BATCH 3 (CONTRASTS)
% Define the path to the SPM.mat file in the batch structure for contrast estimation.
matlabbatch{3}.spm.stats.con.spmmat(1) = {spmMatPath};
% Loop through each contrast defined for the current task.
for k = 1:length(contrasts)
% Adjust contrast weights to fit the current design matrix structure.
weights = adjust_contrasts(spmMatPath, selectedTasks(selected_task_idx).weights(k));
% Define each contrast and its properties in the SPM batch structure.
matlabbatch{3}.spm.stats.con.consess{k}.tcon.weights = weights; % Contrast weights.
matlabbatch{3}.spm.stats.con.consess{k}.tcon.name = contrasts{k}; % Name of the contrast.
matlabbatch{3}.spm.stats.con.consess{k}.tcon.sessrep = 'none'; % No session-specific replication.
end
% Execute the contrast batch (batch 3).
fprintf('Setting contrasts...\n');
spm_jobman('run', matlabbatch(3));
fprintf('DONE!\n');
end
%% SAVE SCRIPT
% Save a copy of this script in the output directory for documentation and replicability.
FileNameAndLocation = [mfilename('fullpath')]; % Get full path to this script.
script_outdir = fullfile(outPath,'spmGLMautoContrast.m'); % Set destination path for script copy.
currentfile = strcat(FileNameAndLocation, '.m'); % Get filename with extension.
copyfile(currentfile, script_outdir); % Copy script to output directory.
end