diff --git a/demos/MoAE/moae_03_slice_display.m b/demos/MoAE/moae_03_slice_display.m index e08c7a465..de4847d02 100644 --- a/demos/MoAE/moae_03_slice_display.m +++ b/demos/MoAE/moae_03_slice_display.m @@ -18,104 +18,15 @@ this_dir = fileparts(mfilename('fullpath')); -subLabel = '01'; - opt.pipeline.type = 'stats'; -opt.dir.raw = fullfile(this_dir, 'inputs', 'raw'); opt.dir.derivatives = fullfile(this_dir, 'outputs', 'derivatives'); opt.dir.preproc = fullfile(opt.dir.derivatives, 'bidspm-preproc'); -opt.dir.roi = fullfile(opt.dir.derivatives, 'bidspm-roi'); -opt.dir.stats = fullfile(opt.dir.derivatives, 'bidspm-stats'); - opt.model.file = fullfile(this_dir, 'models', 'model-MoAE_smdl.json'); -% Specify the result to compute -opt.results(1).nodeName = 'run_level'; - -opt.results(1).name = 'listening'; -% MONTAGE FIGURE OPTIONS -opt.results(1).montage.do = true(); -opt.results(1).montage.slices = -4:2:16; % in mm -% axial is default 'sagittal', 'coronal' -opt.results(1).montage.orientation = 'axial'; -% will use the MNI T1 template by default but the underlay image can be changed. -opt.results(1).montage.background = ... - fullfile(spm('dir'), 'canonical', 'avg152T1.nii'); +opt.subjects = {'01'}; opt = checkOptions(opt); -use_schema = false; -BIDS_ROI = bids.layout(opt.dir.roi, 'use_schema', use_schema); - -filter = struct('sub', subLabel, ... - 'hemi', 'R', ... - 'desc', 'auditoryCortex'); - -rightRoiFile = bids.query(BIDS_ROI, 'data', filter); - -filter.hemi = 'L'; - -leftRoiFile = bids.query(BIDS_ROI, 'data', filter); - -% we get the con image to extract data -ffxDir = getFFXdir(subLabel, opt); -maskImage = spm_select('FPList', ffxDir, '^.*_mask.nii$'); -bf = bids.File(spm_file(maskImage, 'filename')); -conImage = spm_select('FPList', ffxDir, ['^con_' bf.entities.label '.nii$']); - -%% Layers to put on the figure -layers = sd_config_layers('init', {'truecolor', 'dual', 'contour', 'contour'}); - -% Layer 1: Anatomical map -[anat_normalized_file, anatRange] = return_normalized_anat_file(opt, subLabel); -layers(1).color.file = anat_normalized_file; -layers(1).color.range = [0 anatRange(2)]; - -layers(1).color.map = gray(256); - -%% Layer 2: Dual-coded layer -% -% - contrast estimates color-coded; - -layers(2).color.file = conImage; - -color_map_folder = fullfile(fileparts(which('map_luminance')), '..', 'mat_maps'); -load(fullfile(color_map_folder, 'diverging_bwr_iso.mat')); -layers(2).color.map = diverging_bwr; - -layers(2).color.range = [-4 4]; -layers(2).color.label = '\beta_{listening} - \beta_{baseline} (a.u.)'; - -%% Layer 2: Dual-coded layer -% -% - t-statistics opacity-coded - -spmTImage = spm_select('FPList', ffxDir, ['^spmT_' bf.entities.label '.nii$']); -layers(2).opacity.file = spmTImage; - -layers(2).opacity.range = [2 3]; -layers(2).opacity.label = '| t |'; - -%% Layer 3 and 4: Contour of ROI - -layers(3).color.file = rightRoiFile{1}; -layers(3).color.map = [0 0 0]; -layers(3).color.line_width = 2; - -layers(4).color.file = leftRoiFile{1}; -layers(4).color.map = [1 1 1]; -layers(4).color.line_width = 2; - -%% Settings -settings = sd_config_settings('init'); - -% we reuse the details for the SPM montage -settings.slice.orientation = opt.results(1).montage.orientation; -settings.slice.disp_slices = -15:3:18; -settings.fig_specs.n.slice_column = 4; -settings.fig_specs.title = opt.results(1).name; - -%% Display the layers -[settings, p] = sd_display(layers, settings); +transparentMontage(opt); diff --git a/demos/MoAE/return_normalized_anat_file.m b/demos/MoAE/return_normalized_anat_file.m deleted file mode 100644 index 6a786ac9c..000000000 --- a/demos/MoAE/return_normalized_anat_file.m +++ /dev/null @@ -1,26 +0,0 @@ -function [anat_normalized_file, anat_range] = return_normalized_anat_file(opt, sub_label) - % - - % (C) Copyright 2021 Remi Gau - - [BIDS, opt] = getData(opt, opt.dir.preproc); - - anat_normalized_file = bids.query(BIDS, 'data', ... - 'modality', 'anat', ... - 'space', 'IXI549Space', ... - 'suffix', 'T1w'); - - if isempty(anat_normalized_file) - opt.query.space = 'IXI549Space'; - [anat_normalized_file, anatDataDir] = getAnatFilename(BIDS, opt, sub_label); - anat_normalized_file = fullfile(anatDataDir, anat_normalized_file); - else - anat_normalized_file = anat_normalized_file{1}; - end - - hdr = spm_vol(anat_normalized_file); - vol = spm_read_vols(hdr); - - anat_range = [min(vol(:)) max(vol(:))]; - -end diff --git a/lib/slice_display b/lib/slice_display index 4326779c8..f6f1ce42f 160000 --- a/lib/slice_display +++ b/lib/slice_display @@ -1 +1 @@ -Subproject commit 4326779c8e9d7681e0b13827196aad64c801e170 +Subproject commit f6f1ce42f56ae7bc40e697a1083bc6514a631a9f diff --git a/src/bids_model/BidsModel.m b/src/bids_model/BidsModel.m index 0c49baeae..65e6ad43f 100644 --- a/src/bids_model/BidsModel.m +++ b/src/bids_model/BidsModel.m @@ -312,6 +312,7 @@ end function results = getResults(obj) + % return results from all nodes results = struct([]); idx = 1; diff --git a/src/defaults/defaultResultsStructure.m b/src/defaults/defaultResultsStructure.m index 9bc93fede..3a73b9881 100644 --- a/src/defaults/defaultResultsStructure.m +++ b/src/defaults/defaultResultsStructure.m @@ -32,16 +32,16 @@ % load(fullfile(color_map_folder, 'diverging_bwr_iso.mat')); layers{2} = struct('color', struct('file', [], ... % con image - 'map', 'diverging_bwr_iso', ... - 'range', [-4 4]), ... - 'label', '\beta_{listening} - \beta_{baseline} (a.u.)', ... - 'opacity', struct('file', [], ... % spmT image + 'range', [-4 4], ... + 'label', ''), ... + 'opacity', struct('file', [], ... % assume spmT image 'range', [2 3], ... - 'label', '| t |')); + 'label', ''), ... + 'type', 'dual'); layers{3} = struct('color', struct('file', [], ... % spmT mask thresholded at 0.05 FWD - 'map', [0 0 0], ... - 'line_width', 2)); + 'map', 'w', ... + 'line_width', 1)); result.sdConfig.layers = layers; diff --git a/src/stats/results/checkMontage.m b/src/stats/results/checkMontage.m new file mode 100644 index 000000000..ecc34e965 --- /dev/null +++ b/src/stats/results/checkMontage.m @@ -0,0 +1,70 @@ +function [opt, BIDS] = checkMontage(opt, iRes, node, BIDS, subLabel) + % + % Check values for create a slice montage. + % + % Set default values if they are missing. + % + % USAGE:: + % + % [opt, BIDS] = checkMontage(opt, iRes, node, BIDS, subLabel) + % + % + + % (C) Copyright 2019 bidspm developers + + if nargin < 4 + BIDS = ''; + subLabel = ''; + end + + if isfield(opt.results(iRes), 'montage') && any(opt.results(iRes).montage.do) + + background = opt.results(iRes).montage.background; + + % TODO refactor with getInclusiveMask + if isstruct(background) + + if ismember(lower(node.Level), {'run', 'session', 'subject'}) + + if isempty(BIDS) + BIDS = bids.layout(opt.dir.preproc, ... + 'use_schema', false, ... + 'index_dependencies', false, ... + 'filter', struct('sub', {opt.subjects})); + end + + background.sub = subLabel; + background.space = opt.space; + file = bids.query(BIDS, 'data', background); + + if iscell(file) + if isempty(file) + % let checkMaskOrUnderlay figure it out + file = ''; + + elseif numel(file) == 1 + file = file{1}; + + elseif numel(file) > 1 + file = file{1}; + + msg = sprintf('More than 1 overlay image found for %s.\n Taking the first one.', ... + bids.internal.create_unordered_list(background)); + id = 'tooManyMontageBackground'; + logger('WARNING', msg, 'id', id, 'options', opt, 'filename', mfilename()); + end + + end + + background = file; + + end + + end + + background = checkMaskOrUnderlay(background, opt, 'background'); + opt.results(iRes).montage.background = background; + + end + +end diff --git a/src/stats/results/renameSpmT.m b/src/stats/results/renameSpmT.m index fc715ddb5..9e771681f 100644 --- a/src/stats/results/renameSpmT.m +++ b/src/stats/results/renameSpmT.m @@ -8,18 +8,23 @@ function renameSpmT(result) % % (C) Copyright 2023 bidspm developers - outputFiles = spm_select('FPList', result.dir, '^spmT_[0-9].*_sub-.*nii$'); + prefixes = {'spmT', 'spmF'}; + for i_prefix = 1:numel(prefixes) - for iFile = 1:size(outputFiles, 1) + outputFiles = spm_select('FPList', result.dir, ['^' prefixes{i_prefix} '_[0-9].*_sub-.*nii$']); - source = deblank(outputFiles(iFile, :)); + for iFile = 1:size(outputFiles, 1) - basename = spm_file(source, 'basename'); - split = strfind(basename, '_sub'); - bf = bids.File(basename(split + 1:end)); + source = deblank(outputFiles(iFile, :)); - target = spm_file(source, 'basename', bf.filename); + basename = spm_file(source, 'basename'); + split = strfind(basename, '_sub'); + bf = bids.File(basename(split + 1:end)); + + target = spm_file(source, 'basename', bf.filename); + + movefile(source, target); + end - movefile(source, target); end end diff --git a/src/stats/results/transparentMontage.m b/src/stats/results/transparentMontage.m new file mode 100644 index 000000000..d7fb413f6 --- /dev/null +++ b/src/stats/results/transparentMontage.m @@ -0,0 +1,179 @@ +function transparentMontage(opt) + % + % Generate montage with transparent plotting using slice_display toolbox. + % + % USAGE:: + % + % transparentMontage(opt) + % + % EXAMPLE:: + % + % opt.pipeline.type = 'stats'; + % + % opt.dir.derivatives = fullfile(this_dir, 'outputs', 'derivatives'); + % opt.dir.preproc = fullfile(opt.dir.derivatives, 'bidspm-preproc'); + % opt.model.file = fullfile(this_dir, 'models', 'model-MoAE_smdl.json'); + % + % opt.subjects = {'01'}; + % + % opt = checkOptions(opt); + % + % transparentMontage(opt); + % + + % (C) Copyright 2025 bidspm developers + + bm = opt.model.bm; + + modelResults = bm.getResults(); + if ~isempty(modelResults) + opt.results = modelResults; + end + + % loop through the steps to compute for each contrast mentioned for each node + for iRes = 1:length(opt.results) + + node = bm.get_nodes('Name', opt.results(iRes).nodeName); + + if isempty(node) + + id = 'unknownModelNode'; + msg = sprintf('no Node named %s in model\n %s.', ... + opt.results(iRes).nodeName, ... + opt.model.file); + logger('WARNING', msg, 'id', id, 'filename', mfilename(), 'options', opt); + continue + end + + opt.results(iRes); + + if ~isfield(opt.results(iRes), 'montage') || ~opt.results(iRes).montage.do + continue + end + + msg = sprintf('\n PROCESSING NODE: %s\n', node.Name); + logger('INFO', msg, 'options', opt, 'filename', mfilename()); + + if any(strcmp(node.Level, {'Run', 'Subject'})) + + for iSub = 1:numel(opt.subjects) + + subLabel = opt.subjects{iSub}; + + ffxDir = getFFXdir(subLabel, opt); + load(fullfile(ffxDir, 'SPM.mat')); + + % set defaults + % TODO check plotting is done on the right background + [optThisSubject, ~] = checkMontage(opt, iRes, node, struct([]), subLabel); + optThisSubject = checkOptions(optThisSubject); + optThisSubject.results(iRes).montage = setMontage(optThisSubject.results(iRes)); + + for iName = 1:numel(optThisSubject.results(iRes).name) + + plotTransparentMontage(optThisSubject, SPM, subLabel, iRes, iName); + + end + + end + + end + + end + +end + +function plotTransparentMontage(opt, SPM, subLabel, iRes, iName) + % Generate a single transparent plot. + % + overwrite = true; + + color_map_folder = fullfile(returnRootDir(), 'lib', 'brain_colours', 'mat_maps'); + + if opt.results(iRes).binary + layers = sd_config_layers('init', {'truecolor', 'dual', 'contour'}); + else + layers = sd_config_layers('init', {'truecolor', 'dual'}); + end + + %% Layer 1: Anatomical map + layers(1) = setFields(layers(1), opt.results(iRes).sdConfig.layers{1}, overwrite); + + layers(1).color.file = opt.results(iRes).montage.background{1}; + + hdr = spm_vol(layers(1).color.file); + [max_val, ~] = slover('volmaxmin', hdr); + layers(1).color.range = [0 max_val]; + + %% Layer 2: Dual-coded layer + + % - contrast estimates color-coded; + layers(2) = setFields(layers(2), opt.results(iRes).sdConfig.layers{2}, overwrite); + + name = opt.results(iRes).name{iName}; + tmp = struct('name', name); + contrastNb = getContrastNb(tmp, opt, SPM); + % keep track if this is a t test or F test + stat = SPM.xCon(contrastNb).STAT; + contrastNb = sprintf('%04.0f', contrastNb); + + % - statistics opacity-coded + ffxDir = getFFXdir(subLabel, opt); + if strcmp(stat, 'T') + colorFile = spm_select('FPList', ffxDir, ['^con_' contrastNb '.nii$']); + opacityFile = spm_select('FPList', ffxDir, ['^spmT_' contrastNb '.nii$']); + + layers(2).opacity.label = '| t |'; + + load(fullfile(color_map_folder, 'diverging_bwr_iso.mat')); %#ok<*LOAD> + layers(2).color.map = diverging_bwr; + + else + colorFile = spm_select('FPList', ffxDir, ['^ess_' contrastNb '.nii$']); + opacityFile = spm_select('FPList', ffxDir, ['^spmF_' contrastNb '.nii$']); + + layers(2).opacity.label = 'F'; + + load(fullfile(color_map_folder, '1hot_iso.mat')); + layers(2).color.map = hot; + + hdr = spm_vol(opacityFile); + [max_val, ~] = slover('volmaxmin', hdr); + layers(2).color.range = [0 max_val]; + + layers(2).opacity.range = [0 5]; + end + layers(2).color.file = colorFile; + layers(2).opacity.file = opacityFile; + + title = strrep(name, '_', ' '); + layers(2).color.label = [title ' (a.u.)']; + + %% Contour + if opt.results(iRes).binary + layers(3) = setFields(layers(3), opt.results(iRes).sdConfig.layers{3}, overwrite); + contour = spm_select('FPList', ffxDir, ['^sub.*' contrastNb '.*_mask.nii']); + layers(3).color.file = contour; + end + + %% Settings + settings = opt.results(iRes).sdConfig.settings; + + % we reuse the details for the SPM montage + settings.slice.disp_slices = opt.results(1).montage.slices; + settings.slice.orientation = opt.results(1).montage.orientation; + + settings.fig_specs.title = title; + + %% Display the layers + settings.slice.zmm; + [~, ~, h_figure] = sd_display(layers, settings); + + outputFile = fullfile(ffxDir, [contrastNb '_' name '.png']); + print(h_figure, outputFile, '-dpng'); + close(h_figure); + + % TODO + % rename file + +end diff --git a/src/workflows/stats/bidsResults.m b/src/workflows/stats/bidsResults.m index e766fbec6..8ac56e50c 100644 --- a/src/workflows/stats/bidsResults.m +++ b/src/workflows/stats/bidsResults.m @@ -271,6 +271,8 @@ cleanUpWorkflow(opt); + transparentMontage(opt); + end function [opt, listNodeLevels] = keepRequestedNodes(opt, nodeName, analysisLevel) @@ -547,62 +549,3 @@ matlabbatch{end}.result = result; end - -function [opt, BIDS] = checkMontage(opt, iRes, node, BIDS, subLabel) - - if nargin < 4 - BIDS = ''; - subLabel = ''; - end - - if isfield(opt.results(iRes), 'montage') && any(opt.results(iRes).montage.do) - - background = opt.results(iRes).montage.background; - - % TODO refactor with getInclusiveMask - if isstruct(background) - - if ismember(lower(node.Level), {'run', 'session', 'subject'}) - - if isempty(BIDS) - BIDS = bids.layout(opt.dir.preproc, ... - 'use_schema', false, ... - 'index_dependencies', false, ... - 'filter', struct('sub', {opt.subjects})); - end - - background.sub = subLabel; - background.space = opt.space; - file = bids.query(BIDS, 'data', background); - - if iscell(file) - if isempty(file) - % let checkMaskOrUnderlay figure it out - file = ''; - - elseif numel(file) == 1 - file = file{1}; - - elseif numel(file) > 1 - file = file{1}; - - msg = sprintf('More than 1 overlay image found for %s.\n Taking the first one.', ... - bids.internal.create_unordered_list(background)); - id = 'tooManyMontageBackground'; - logger('WARNING', msg, 'id', id, 'options', opt, 'filename', mfilename()); - end - - end - - background = file; - - end - - end - - background = checkMaskOrUnderlay(background, opt, 'background'); - opt.results(iRes).montage.background = background; - - end - -end