├── README.md ├── batch_eb.py ├── beta-example ├── createSynthesizedData.m ├── templateFile.nii ├── templateJobfile.mat └── writeCsv.m ├── calculateTsnr.m ├── clustsim.m ├── erodeImage.m ├── evaluateMotion.m ├── extractVectors.m ├── extract_betas.py ├── lss ├── batchLssSpm.m ├── lssCorrelation.m ├── lssGenerateBetasSpm.m └── py_lss.ipynb ├── meanImageVal.m ├── moveRoisInImage.m ├── organizeDicoms.m ├── split4dTo3d.m ├── univariate-tables ├── batchEffectSize.m ├── batchGetBaAndRegion.m ├── getBaAndRegion.m ├── loadTableFile.m ├── saveClustersAndEffectSize.m └── spm_list_edited.m ├── visualization ├── batchQuickViewGif.m ├── quickViewGif.m └── xjviewModified.m └── writeSummaryImage.m /README.md: -------------------------------------------------------------------------------- 1 | misc-fmri-code 2 | ============== 3 | 4 | Just small functions for preprocessing, analysis, etc. of fMRI data. 5 | 6 | * [batchLssSpm](batchLssSpm.m)/[lssGenerateBetasSpm](lssGenerateBetasSpm.m)/[lssCorrelation](lssCorrelation.m): Scripts to perform the Least Squares- Separate procedure from Mumford et al. 2012 and improved in Turner et al. 2012, as well as to perform functional connectivity analysis on the results. The base, [generate_spm_singletrial](https://github.com/ritcheym/fmri_misc/blob/master/generate_spm_singletrial.m), written by Maureen Ritchey, matched the earlier version of LSS, and I altered it to match the later version. The functional connectivity portion (lssCorrelation) is largely an amalgamation of several scripts written by Dennis Thompson, with some slight alterations to make it compatible with LSS. BatchLssSpm is just a wrapper that sets paths and settings and calls both lssGenerateBetasSpm and lssCorrelation. 7 | * To do: 8 | 1. Figure out graph theory analyses to apply to roi2roi results. 9 | * [clustsim](clustsim.m) calls AFNI functions to perform Monte Carlo simulations on a second-level SPM analysis. The paths to the AFNI functions will need to be altered by anyone using the function. 10 | * To do: 11 | 1. Add in code to read the outputted text file so that the results can be outputted as a variable. 12 | * [batchEffectSize](univariate-tables/batchEffectSize.m)/[saveClustersAndEffectSize](univariate-tables/saveClustersAndEffectSize.m) take a second-level analysis, create a Cohen’s d image for each t contrast, create masks of each significant cluster in each t contrast, and extract the mean Cohen’s d for each cluster, summarizing it all in a csv file. The Cohen’s d image can also be used to determine the Cohen’s d of the peak voxel in each cluster. The script batchEffectSize is simply a wrapper to call the function saveClustersAndEffectSize. 13 | -------------------------------------------------------------------------------- /batch_eb.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Sun Nov 16 17:40:54 2014 4 | Summarizes betas and calls extract_betas. 5 | @author: taylorsalo 6 | """ 7 | 8 | import edit_csv as ec 9 | import extract_betas as eb 10 | import pandas as pd 11 | import numpy as np 12 | import matplotlib.pyplot as plt 13 | 14 | subject_list = "/nfs/ep2/AX/project_folders/072313_EPC_BP_SZ/subject_list.csv" 15 | file_sup_path = "/nfs/ep2/AX/first_levels/00_MONTH/" 16 | file_sub_path = "/MTU/func_an_SPM8/" 17 | mask_file = "/nfs/ep2/masks/PickAtlas/B46_L_d0.nii" 18 | group_list = ["HC", "SZ", "BP"] 19 | 20 | subjects = ec.read_csv(subject_list, group_list) 21 | 22 | wanted_contrasts = eb.get_spm_contrast_list(file_sup_path + subjects[0][1] + 23 | file_sub_path + "SPM.mat") 24 | 25 | headers = ["ID", "Group"] + wanted_contrasts 26 | out_struct = pd.DataFrame(columns=headers) 27 | for group in subjects: 28 | for j_subj in range(1, len(group)): 29 | beta_values = [] 30 | wanted_file_list = eb.get_spm_contrast_files(file_sup_path + 31 | group[j_subj] + 32 | file_sub_path + 33 | "SPM.mat", 34 | wanted_contrasts) 35 | for k_con, contrast in enumerate(wanted_contrasts): 36 | beta_file = (file_sup_path + group[j_subj] + file_sub_path + 37 | wanted_file_list[k_con]) 38 | print(beta_file) 39 | beta_value = eb.main(beta_file, mask_file) 40 | beta_values.append(beta_value) 41 | out_struct.loc[len(out_struct)] = ([group[j_subj], group[0]] + 42 | beta_values) 43 | 44 | grouped = out_struct.groupby('Group') 45 | for con in wanted_contrasts: 46 | out_struct[con] = out_struct[con].astype(float) 47 | 48 | mean_ = grouped.aggregate(lambda x: np.mean(x)).transpose() 49 | mean_ = mean_.reindex_axis(group_list, axis=1) 50 | mean_ = mean_.reindex_axis(wanted_contrasts, axis=0) 51 | ste_ = grouped.aggregate(lambda x: np.std(x) / np.sqrt(x.count())).transpose() 52 | ste_ = ste_.reindex_axis(group_list, axis=1) 53 | ste_ = ste_.reindex_axis(wanted_contrasts, axis=0) 54 | 55 | max_value = (mean_ + ste_).max().max() 56 | min_value = (mean_ - ste_).min().min() 57 | range_ = max_value - min_value 58 | 59 | # Use max_value, min_value, and range_ to determine scale for y-axis. 60 | 61 | 62 | ax = mean_.plot(kind='bar', yerr=ste_, rot=45) 63 | plt.axhline(0, color='k') 64 | plt.tight_layout() 65 | -------------------------------------------------------------------------------- /beta-example/createSynthesizedData.m: -------------------------------------------------------------------------------- 1 | % createSynthesizedData 2 | codeFile = mfilename('fullpath'); 3 | [codeDir, ~] = fileparts(codeFile); 4 | 5 | spm('defaults', 'fMRI'); 6 | 7 | if ~exist([codeDir '/simulated_data/'], 'dir') 8 | mkdir([codeDir '/simulated_data/']); 9 | end 10 | if ~exist([codeDir '/first_level/'], 'dir') 11 | mkdir([codeDir '/first_level/']); 12 | end 13 | 14 | % Our base onsets and durations (0). 15 | sessLength = 100; % in volumes 16 | onsets{1} = [6 10 20 26 40 80 140 142 144 146]; % in seconds 17 | durations{1} = 0; 18 | names{1} = 'Impulses'; 19 | convNames = {'Regular', '3x Multiplier', '6 second Duration', '6 second Delay'}; 20 | 21 | save([codeDir '/vectors.mat'], 'names', 'onsets', 'durations') 22 | 23 | % General settings 24 | tr = 2; 25 | xBF.dt = tr; 26 | xBF.name = 'hrf'; 27 | bf = spm_get_bf(xBF); 28 | orig_onsets = onsets{1} ./ tr; 29 | 30 | % Create our regular convolved vector 31 | vec = zeros(sessLength, 1); 32 | singleCond = (int16(onsets{1}) / tr) + 1; 33 | vec(singleCond) = 1; 34 | for jDur = 1:int16(durations{1} / tr) 35 | vec(singleCond + jDur) = 1; 36 | end 37 | 38 | U.u = vec; 39 | U.name = {'reg'}; 40 | convRegs{1} = spm_Volterra(U, bf.bf); 41 | 42 | % Our 3x multiplier vector is just our regular convolved vector times 3 43 | convRegs{2} = convRegs{1} .* 3; 44 | 45 | % Create our 6 second duration convolved vector 46 | durations{1} = 6; 47 | 48 | vec = zeros(sessLength, 1); 49 | singleCond = (int16(onsets{1}) / tr) + 1; 50 | vec(singleCond) = 1; 51 | for jDur = 1:int16(durations{1} / tr) 52 | vec(singleCond + jDur) = 1; 53 | end 54 | 55 | bf = spm_get_bf(xBF); 56 | 57 | U.u = vec; 58 | U.name = {'reg'}; 59 | convRegs{3} = spm_Volterra(U, bf.bf); 60 | 61 | % Create our 6 second delay convolved vector 62 | onsets{1} = onsets{1} + 6; 63 | durations{1} = 0; 64 | 65 | vec = zeros(sessLength, 1); 66 | singleCond = (int16(onsets{1}) / tr) + 1; 67 | vec(singleCond) = 1; 68 | for jDur = 1:int16(durations{1} / tr) 69 | vec(singleCond + jDur) = 1; 70 | end 71 | 72 | xBF.dt = tr; 73 | xBF.name = 'hrf'; 74 | bf = spm_get_bf(xBF); 75 | U.u = vec; 76 | U.name = {'reg'}; 77 | convRegs{4} = spm_Volterra(U, bf.bf); 78 | 79 | % Create and save plot of our four convolved vectors 80 | x_ax = 1:sessLength; 81 | figure 82 | plot(x_ax, convRegs{1}, 'b', x_ax, convRegs{2}, 'c',... 83 | x_ax, convRegs{3}, 'g', x_ax, convRegs{4}, 'r'); 84 | hx = graph2d.constantlineseries(orig_onsets, 'LineStyle', ':', 'Color', [1 0 1]); 85 | changedependvar(hx, 'x'); 86 | legend(convNames{1}, convNames{2}, convNames{3}, convNames{4}); 87 | title('Convolved Vectors','FontWeight','bold'); 88 | print(gcf, '-djpeg', [codeDir '/convolvedVectors.jpeg']); 89 | close(gcf); 90 | 91 | % Create niftis of our simulated data, plus 100, in a block of noise 92 | Y = zeros(40, 40, 40, length(convRegs{1})); 93 | Y(5:35, 5:35, 5:35, :) = 100; 94 | X = rand([40, 40, 40, length(convRegs{1})]); 95 | mnX = mean(X(:)); 96 | X = X - mnX; 97 | Y = Y + X; 98 | for iConv = 1:length(convNames) 99 | Y(20 + iConv, 20, 20, :) = 100 + convRegs{iConv}; 100 | end 101 | 102 | [x, y, z, nVolumes] = size(Y); 103 | 104 | Vin = spm_vol([codeDir '/templateFile.nii']); 105 | 106 | for iVolume = 1:nVolumes 107 | V(iVolume) = Vin(1); 108 | V(iVolume).fname = [codeDir '/simulated_data/volume_' sprintf('%03d', iVolume) '.nii']; 109 | V(iVolume).dim = [x y z]; 110 | V(iVolume).private.dat.dim = [x y z nVolumes]; 111 | V(iVolume).private.dat.fname = V(iVolume).fname; 112 | 113 | V2 = spm_create_vol(V(iVolume)); 114 | spm_write_vol(V2, Y(:, :, :, iVolume)); 115 | 116 | scans{iVolume} = [V(iVolume).fname ',1']; 117 | end 118 | clear V Y 119 | 120 | % Run very basic first level GLM with regular vectors 121 | load([codeDir '/templateJobfile.mat']); 122 | matlabbatch{1}.spm.stats.fmri_spec.sess.scans = scans; 123 | matlabbatch{1}.spm.stats.fmri_spec.sess.multi{1} = [codeDir '/vectors.mat']; 124 | matlabbatch{1}.spm.stats.fmri_spec.dir{1} = [codeDir '/first_level/']; 125 | matlabbatch{1}.spm.stats.fmri_spec.timing.RT = tr; 126 | matlabbatch{3}.spm.stats.con.consess{1}.tcon.name = names{1}; 127 | save([codeDir '/jobfile.mat'], 'matlabbatch'); 128 | 129 | spm_jobman('initcfg'); 130 | spm_jobman('run',matlabbatch); 131 | close(gcf); 132 | 133 | V = spm_vol([codeDir '/first_level/beta_0001.img']); 134 | [Y, ~] = spm_read_vols(V); 135 | 136 | outStruct{1}.header{1} = 'Convolution Version'; 137 | outStruct{2}.header{1} = 'Beta Value'; 138 | outStruct{3}.header{1} = 'Coordinates'; 139 | for iConv = 1:length(convNames) 140 | coords{iConv} = [20 + iConv, 20, 20] * V.mat(1:3, 1:3) + V.mat(1:3, 4)'; 141 | outStruct{1}.col{iConv} = convNames{iConv}; 142 | outStruct{2}.col{iConv} = Y(20 + iConv, 20, 20); 143 | outStruct{3}.col{iConv} = num2str(coords{iConv}); 144 | fprintf([convNames{iConv} ':\t\t' num2str(coords{iConv}) '\n']); 145 | end 146 | 147 | writeCsv(outStruct, [codeDir '/betas.csv']); 148 | 149 | % Now, just use SPM to plot those four coordinates to get an idea of how 150 | % well the GLM fit the convolved vectors: 151 | % Load the contrast in SPM Results (with p <= 1, to be safe). 152 | % Navigate to one of the four coordinates. 153 | % Hit "Plot" --> "Fitted Responses" --> "Adjusted" --> "scan or time" --> SAVE. THAT. FIGURE. 154 | -------------------------------------------------------------------------------- /beta-example/templateFile.nii: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsalo/misc-fmri-code/eecc2f52834ef4ac147bd9421bf0a5805502624c/beta-example/templateFile.nii -------------------------------------------------------------------------------- /beta-example/templateJobfile.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsalo/misc-fmri-code/eecc2f52834ef4ac147bd9421bf0a5805502624c/beta-example/templateJobfile.mat -------------------------------------------------------------------------------- /beta-example/writeCsv.m: -------------------------------------------------------------------------------- 1 | function writeCsv(data, loc, varargin) 2 | % FORMAT writeCsv(data, loc, varargin) 3 | % Description: This function accepts a data structure and and save location 4 | % and file name e.g. /home/name/output.csv. The data structure is a sort of 5 | % pseudo-standard. The structure should have an area for a .header and .col 6 | % field. This is requisite. Essentially the format should mirror that of an 7 | % excel file. I think this should work for any data set in the given 8 | % format. 9 | % 10 | % TLDR 11 | % This function saves structures as .csv files. 12 | % data => structure with .header and .col fields 13 | % loc => save location and filename 14 | % B.R. Geib Winter 2012 15 | 16 | default='w+'; nohead=0; 17 | if ~isempty(varargin), 18 | default = varargin{1}; 19 | if exist(loc, 'file') == 0 20 | nohead = 0; 21 | default = 'w+'; 22 | else 23 | nohead = 1; 24 | end 25 | end 26 | fid = fopen(loc, default); 27 | 28 | % Print out the headers first (if it's a new file) 29 | if nohead == 0 30 | for i = 1:length(data) 31 | current_class = class(data{i}.header); 32 | switch current_class 33 | case 'cell' 34 | fprintf(fid, [char(data{i}.header) ',']); 35 | case 'char' 36 | fprintf(fid, [data{i}.header ',']); 37 | otherwise 38 | fprintf(fid, [num2str(data{i}.header) ',']); 39 | end 40 | end 41 | fprintf(fid, '\n'); 42 | end 43 | 44 | % Check the field col 45 | if ~isfield(data{1}, 'col') 46 | fprintf(['Error writing: ' loc '\n']); 47 | fprintf('\tData does not appear to exist with the variable "data"\n'); 48 | return; 49 | end 50 | 51 | % Examine the data structure 52 | for j = 1:length(data{1}.col) 53 | for i = 1:length(data) 54 | if ~isfield(data{i}, 'col') 55 | fprintf(['Error writing: ' loc '\n']); 56 | fprintf('\tData does not appear to exist with the variable "data"\n'); 57 | return; 58 | end 59 | % Not all columns are demanded to be the same length. If a column 60 | % is empty at the end we want it to just print as empty. 61 | L = length(data{i}.col); 62 | if j <= L 63 | % Determine the class of the {} or (), we don't know which it 64 | % should be 65 | current_class1 = class(data{i}.col(j)); 66 | % Here we try out both classes, an error in one causes a 67 | % default occurance of the second. 68 | try 69 | % If the object is empty, print as such. 70 | if ~isempty(data{i}.col(j)) 71 | switch current_class1 72 | case 'cell' 73 | fprintf(fid, char(data{i}.col(j))); 74 | case 'char' 75 | fprintf(fid, data{i}.col(j)); 76 | otherwise 77 | fprintf(fid, num2str(data{i}.col(j))); 78 | end 79 | end 80 | catch err 81 | current_class2 = class(data{i}.col{j}); 82 | if ~isempty(data{i}.col{j}) 83 | switch current_class2 84 | case 'cell' 85 | fprintf(fid, char(data{i}.col{j})); 86 | case 'char' 87 | fprintf(fid, data{i}.col{j}); 88 | otherwise 89 | fprintf(fid, num2str(data{i}.col{j})); 90 | end 91 | end 92 | end 93 | end 94 | fprintf(fid, ','); 95 | end 96 | fprintf(fid, '\n'); 97 | end 98 | 99 | fclose(fid); 100 | fprintf(['Saved: ' loc '\n']); 101 | end 102 | -------------------------------------------------------------------------------- /calculateTsnr.m: -------------------------------------------------------------------------------- 1 | function outputFiles = calculateTsnr(inputFiles) 2 | % FORMAT outputFiles = calculateTsnr(inputFiles) 3 | % Calculate temporal signal to noise ratio for volumes in inputFiles. 4 | % 5 | % 6 | % INPUT 7 | % inputFiles: Functional image filenames, either 8 | % 1. Character array (such as from SPM.xY.P) or cell 9 | % array of strings. 10 | % 2. SPM structure. 11 | % 12 | % OUTPUT 13 | % outputFiles: Image(s) with voxel-wise mean / SD of input images. 14 | % 15 | % Does not include any implicit masking. 16 | % PW 25/03/2012 17 | % Aesthetic changes (to match Matlab conventions) by Taylor Salo 141209. 18 | 19 | if isstruct(inputFiles) 20 | if isfield(inputFiles,'Sess') 21 | nSessions = length(inputFiles.Sess); 22 | for iSession = 1:nSessions 23 | rowIndex = inputFiles.Sess(iSession).row; 24 | sessionFiles = inputFiles.xY.P(rowIndex); 25 | [sessionPath, ~, ~] = spm_fileparts(inputFiles(1, :)); 26 | sessionTsnrFilename = fullfile(sessionPath, sprintf('tsnr_sess_%03d', iSession)); 27 | if iSession == 1 28 | outputFiles = docalc(sessionFiles, sessionTsnrFilename); 29 | else 30 | outputFiles = char(outputFiles, docalc(sessionFiles, sessionTsnrFilename)); 31 | end 32 | end 33 | else 34 | error('Input variable is a structure, but does not have field Sess, so it probably is not a valid SPM structure') 35 | end 36 | elseif iscellstr(inputFiles) 37 | inputFiles = char(inputFiles); 38 | [sessionPath, ~, ~] = spm_fileparts(inputFiles(1,:)); 39 | tsnrFilename = fullfile(sessionPath, 'tsnr.nii'); 40 | outputFiles = docalc(inputFiles, tsnrFilename); 41 | elseif ischar(inputFiles) 42 | [sessionPath, ~, ~] = spm_fileparts(inputFiles(1,:)); 43 | tsnrFilename = fullfile(sessionPath, 'tsnr.nii'); 44 | outputFiles = docalc(inputFiles, tsnrFilename); 45 | else 46 | error('Could not figure out input variable P') 47 | end 48 | 49 | end 50 | 51 | function outputFiles = docalc(inputFiles, outputFilename) 52 | V = spm_vol(inputFiles); 53 | Vo = V(1); 54 | Vo.fname = outputFilename; 55 | Vo = spm_imcalc(V, Vo, 'mean(X) ./ std(X)', {1 0 0}); 56 | outputFiles = Vo.fname; 57 | end 58 | 59 | 60 | -------------------------------------------------------------------------------- /clustsim.m: -------------------------------------------------------------------------------- 1 | function clustsim(path, pthr, athr, extras, open_file) 2 | % FORMAT clustsim(path, pthr, athr, extras, open_file) 3 | % Runs AFNI's 3dClustSim (Monte Carlo simulations) to determine optimal 4 | % cluster size threshold for second-level whole brain results. 5 | % Uses 3dcalc to create sqrt_ResMS image, then uses 3dFWHMx to obtain 6 | % smoothness of the noise in x, y, and z dimensions. Finally, it runs 7 | % 3dClustSim (10000 iterations) to get a table of minimum cluster sizes for 8 | % different p and alpha thresholds. 9 | % 10 | % 11 | % path: Path to folder containing second level SPM.mat. 12 | % String. 13 | % pthr: Optional. P threshold(s) at which clustsim will be run. 14 | % Double vector. 15 | % athr: Optional. Alpha value(s) for which clustsim will 16 | % present results. Double vector. 17 | % extras: Optional. Additional inputs (e.g. -nodec -2sided 18 | % -quiet). String. 19 | % open_file: Optional. Chooses whether or not to open the 20 | % results file at the end. 1 to open file, 0 to not. 21 | 22 | if ~exist(path,'dir') 23 | fprintf('Directory does not exist: %s\n',path); 24 | return 25 | end 26 | 27 | if ~exist('pthr','var') 28 | fprintf('pthr is empty, setting to default\n'); 29 | pthr = [0.05 0.01 0.005 0.001]; 30 | %pthr = [0.001]; 31 | elseif ischar(pthr) 32 | fprintf('pthr is string, setting to default\n'); 33 | pthr = [0.05 0.01 0.005 0.001]; 34 | end 35 | 36 | if ~exist('athr','var') 37 | fprintf('athr is empty, setting to default\n'); 38 | athr = 0.05; 39 | elseif ischar(athr) 40 | fprintf('athr is string, setting to default\n'); 41 | athr = 0.05; 42 | end 43 | 44 | if ~exist('extras','var') 45 | extras = ''; 46 | end 47 | 48 | if ~exist(open_file, 'var') 49 | open_file = 0; 50 | end 51 | 52 | system(['/nfs/pkg64/afni/3dcalc -a ' path '/ResMS.hdr -expr "sqrt(a)" -prefix ' path '/sqrt_ResMS.nii -overwrite']); 53 | system(['/nfs/pkg64/afni/3dFWHMx -mask ' path '/mask.hdr -input ' path '/sqrt_ResMS.nii -output ' path '/fwhm.txt -overwrite']); 54 | fid = fopen([path '/fwhm.txt']); 55 | fwhm = textscan(fid,'%f'); 56 | fwhmx = fwhm{1}(1); 57 | fwhmy = fwhm{1}(2); 58 | fwhmz = fwhm{1}(3); 59 | 60 | delete([path '/fwhm.txt']); 61 | 62 | system(['/nfs/pkg64/afni/3dClustSim -mask ' path '/mask.hdr -iter 10000 -pthr ' num2str(pthr) ' -athr ' num2str(athr) ' -fwhmxyz ' ... 63 | num2str(fwhmx) ' ' num2str(fwhmy) ' ' num2str(fwhmz) ' ' extras ' -prefix ' path '/montecarlo']); 64 | 65 | if open_file == 1 66 | system(['emacs22-gtk ' path '/montecarlo.NN1.1D']); 67 | else 68 | fprintf('open_file is not 1, not opening output file.\n'); 69 | end 70 | end 71 | 72 | -------------------------------------------------------------------------------- /erodeImage.m: -------------------------------------------------------------------------------- 1 | function outFilename = erodeImage(filename, THR, ERODE) 2 | % FORMAT outFilename = erodeImage(filename, THR, ERODE) 3 | % Adapted from the Conn toolbox's mask erosion code to threshold and erode 4 | % nifti images. 5 | % 6 | % 7 | % filename: Nifti image to be binarized and eroded. String. 8 | % THR: Threshold to apply to file for binarization. Default is 0.5. 9 | % Double. 10 | % ERODE: Number of layers to erode. Default is 1. Double. 11 | % 12 | % 13 | % Modified by Taylor Salo 140902 from code written by Alphonso Nieto-Castanon 14 | 15 | if ~exist('THR', 'var') 16 | THR = 0.5; 17 | end 18 | if ~exist('ERODE', 'var') 19 | ERODE = 1; 20 | end 21 | 22 | V0 = spm_vol(filename); 23 | [X0, ~] = spm_read_vols(V0); 24 | 25 | % Conn code 26 | idx1 = find(X0(:) > THR); 27 | if isempty(idx1) 28 | error('No suprathreshold voxels in ROI file.'); 29 | end 30 | [idxX, idxY, idxZ] = ind2sub(size(X0), idx1); 31 | 32 | idxT = find((idxX > ERODE) & (idxX < (size(X0, 1) + 1 - ERODE)) & ... 33 | (idxY > ERODE) & (idxY < (size(X0, 2) + 1 - ERODE)) & ... 34 | (idxZ > ERODE) & (idxZ < (size(X0, 3) + 1 - ERODE))); 35 | for n1 = 1:length(idxT) 36 | if (sum(sum(sum(X0(idxX(idxT(n1)) + (-ERODE:ERODE), idxY(idxT(n1)) + (-ERODE:ERODE), idxZ(idxT(n1)) + (-ERODE:ERODE)) < THR, 3), 2), 1)) >= 1 37 | idxT(n1) = 0; 38 | end 39 | end 40 | idxT = idxT(idxT > 0); 41 | idx1 = idx1(idxT); 42 | X1 = zeros(size(X0)); 43 | X1(idx1) = 1; 44 | [fPath, fName, fExt] = fileparts(V0.fname); 45 | outFilename = fullfile(fPath, ['e' fName fExt]); 46 | V0.fname = outFilename; 47 | spm_write_vol(V0, X1); 48 | end 49 | -------------------------------------------------------------------------------- /evaluateMotion.m: -------------------------------------------------------------------------------- 1 | function [outMotionData, stimulusCorrelatedMotion] = evaluateMotion(motionFile, vectorFile, tr) 2 | % FORMAT [outMotionData, stimulusCorrelatedMotion] = evaluateMotion(motionFile, vectorFile, tr) 3 | % Calculates framewise displacement and stimulus-correlated motion based on 4 | % condition onsets/durations, motion information provided by SPM8's 5 | % realignment tool, and TR. 6 | % Author: Taylor Salo 141002-141209 7 | % 8 | % 9 | % Inputs: 10 | % motionFile: String pointing to rp*.txt file outputted by 11 | % SPM's realignment tool. Contains nVolumes X 6 12 | % array. 13 | % vectorFile: String pointing to mat file where single 14 | % block's vector information is stored. 15 | % tr: Time to repetition, in seconds. Double. 16 | % 17 | % Outputs: 18 | % outMotionData: Motion parameter array from motionFile with 19 | % framewise displacement appended as 7th column. 20 | % stimulusCorrelatedMotion: Cell array of structures. One cell for each 21 | % condition. Structure contains name field with 22 | % condition name and corr field with correlation 23 | % between expected BOLD response and motion. 24 | 25 | motionData = importdata(motionFile); 26 | 27 | % In Power et al. 2012, "rotational displacements were converted from 28 | % degrees to millimeters by calculating displacement on the surface of a 29 | % sphere of radius 50 mm, which is approximately the mean distance from the 30 | % cerebral cortex to the center of the head." Rotation is given in radians 31 | % by SPM8, and the formula for length of an arc from radians and radius is 32 | % length = radians * radius. 33 | motionDataDisp = motionData; 34 | motionDataDisp(:, 4:6) = motionData(:, 4:6) .* 50; 35 | motionDataDeriv = [0 0 0 0 0 0; diff(motionDataDisp)]; 36 | framewiseDisplacement = sum(abs(motionDataDeriv), 2); 37 | motionDataDerivAndFd = [motionDataDeriv framewiseDisplacement]; 38 | [nScans, ~] = size(framewiseDisplacement); 39 | outMotionData = [motionData framewiseDisplacement]; 40 | vectors = load(vectorFile); 41 | 42 | % Quantify Stimulus-Correlated Motion. 43 | % Convolution method from spm.martinpyka.de/?p=41 44 | stimulusCorrelatedMotion = cell(size(vectors.onsets)); 45 | for iCond = 1:length(vectors.onsets) 46 | vec = zeros(nScans, 1); 47 | singleCond = (int16(vectors.onsets{iCond}) / tr) + 1; 48 | vec(singleCond) = 1; 49 | for jDur = 1:int16(vectors.durations{iCond} / tr) 50 | vec(singleCond + jDur) = 1; 51 | end 52 | 53 | xBF.dt = tr; 54 | xBF.name = 'hrf'; 55 | bf = spm_get_bf(xBF); 56 | 57 | U.u = vec; 58 | U.name = {'reg'}; 59 | convreg = spm_Volterra(U, bf.bf); 60 | 61 | stimulusCorrelatedMotion{iCond}.name = vectors.names{iCond}; 62 | for jMot = 1:7 63 | stimulusCorrelatedMotion{iCond}.corr(jMot) = abs(corr(convreg, motionDataDerivAndFd(:, jMot))); 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /extractVectors.m: -------------------------------------------------------------------------------- 1 | function extractVectors(spmFile, outDir) 2 | % FORMAT extractVectors(spmFile, outDir) 3 | % Creates vectors from an existing SPM.mat file. 4 | % 5 | % 6 | % Created by Taylor Salo 141217 7 | 8 | load(spmFile); 9 | 10 | if ~exist(outDir, 'dir') 11 | mkdir(outDir); 12 | end 13 | 14 | for iSess = 1:length(SPM.Sess) 15 | onsets = {}; 16 | durations = {}; 17 | names = {}; 18 | 19 | for jCond = 1:length(SPM.Sess(iSess).U) 20 | onsets = [onsets SPM.Sess(iSess).U(jCond).ons]; 21 | durations = [durations SPM.Sess(iSess).U(jCond).dur]; 22 | names = [names SPM.Sess(iSess).U(jCond).name{1}]; 23 | end 24 | save([outDir '/vectors_' sprintf('%03d', iSess)], 'onsets', 'durations', 'names'); 25 | end 26 | 27 | -------------------------------------------------------------------------------- /extract_betas.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Fri Sep 26 10:39:30 2014 4 | Extracts the mean value of the masked voxels from a nifti image. 5 | 6 | Dependencies: nibabel, numpy, scipy. 7 | @author: tsalo 8 | """ 9 | 10 | import sys 11 | import os.path 12 | import nibabel as nb 13 | import numpy as np 14 | import scipy.io 15 | 16 | global yes, y, no, n 17 | yes = "yes" 18 | y = "yes" 19 | no = "no" 20 | n = "no" 21 | 22 | 23 | def str2bool(string): 24 | """ 25 | Returns boolean value based on string. True strings include 'yes', 'true', 26 | 't', '1', and 'y', along with any of their case variations. 27 | """ 28 | return string.lower() in ("yes", "true", "t", "1", "y") 29 | 30 | 31 | def _try_index(list_, val): 32 | """ 33 | Indexes a list without throwing an error if the value isn't found. 34 | """ 35 | try: 36 | return list_.index(val) 37 | except: 38 | return False 39 | 40 | 41 | def get_spm_contrast_list(spm_file): 42 | """ 43 | Generates a list of contrasts tied to an SPM.mat file based on the 44 | SPM.xCon field. Then allows for input of a vector of desired contrasts. 45 | The names of the desired contrasts are outputted in a list 46 | (wanted_contrasts). 47 | To do: 48 | - Extend to check multiple SPM.mat files for contrasts. 49 | But do I really want that? 50 | """ 51 | mat = scipy.io.loadmat(spm_file, squeeze_me=True, struct_as_record=False) 52 | spm = mat["SPM"] 53 | contrast_names = [] 54 | contrast_files = [] 55 | for i, i_con in enumerate(spm.xCon): 56 | contrast_names.append(str(i_con.name)) 57 | contrast_files.append(str(i_con.Vspm.fname)) 58 | print("%s\t%s" % (str(i), str(i_con.name))) 59 | 60 | sure = False 61 | while not sure: 62 | wanted_index = input("Contrast list: ") 63 | sure = str2bool(input("You sure (yes/no)? ")) 64 | wanted_contrasts = [contrast_names[i] for i in wanted_index] 65 | return wanted_contrasts 66 | 67 | 68 | def get_spm_contrast_files(spm_file, contrast_list): 69 | """ 70 | Generates a list of files corresponding to contrasts of interest, based on 71 | the contrast name and the fields within the SPM.mat file. I'm not sure if 72 | it can handle missing contrasts. 73 | """ 74 | mat = scipy.io.loadmat(spm_file, squeeze_me=True, struct_as_record=False) 75 | spm = mat["SPM"] 76 | full_file_list = [str(i_con.Vcon.fname) for i_con in spm.xCon] 77 | full_contrast_list = [str(i_con.name) for i_con in spm.xCon] 78 | contrast_index = [_try_index(full_contrast_list, contrast) for contrast in 79 | contrast_list] 80 | file_list = [full_file_list[index] for index in contrast_index] 81 | return file_list 82 | 83 | 84 | def extract_betas(nifti_image, mask_image): 85 | """ 86 | Extracts mean value from nifti_image within region defined by mask_image. 87 | Attempts have been made to account for header differences between 88 | nifti_image and mask_image. Not 100% sure they work at present. 89 | """ 90 | if os.path.isfile(nifti_image): 91 | nii_img = nb.load(nifti_image) 92 | nii_vals = np.array(nii_img.get_data()) 93 | nii_header = nii_img.get_header() 94 | nii_shape = nii_header.get_data_shape() 95 | nii_affine = nii_header.get_qform() 96 | else: 97 | print("Variable nifti_image points to non-existent file.") 98 | return "" 99 | 100 | if os.path.isfile(mask_image): 101 | mask_img = nb.load(mask_image) 102 | mask_header = mask_img.get_header() 103 | mask_shape = mask_header.get_data_shape() 104 | mask_affine = mask_header.get_qform() 105 | 106 | if mask_shape == nii_shape: 107 | mask_index = np.where(mask_img.get_data()) 108 | if ~(mask_affine == nii_affine).all(): 109 | print("Affine matrices don't match up! " + 110 | "Mask index must be adjusted.") 111 | zero_row = np.zeros(len(mask_index[0])) 112 | m_idx_4d = np.vstack((mask_index, zero_row)) 113 | shift_matrix = np.linalg.inv(nii_affine).dot(mask_affine) 114 | adj_m_index = shift_matrix.dot(m_idx_4d).astype(int)[:3] 115 | mask_index = tuple(map(tuple, adj_m_index)) 116 | masked_vals = nii_vals[mask_index] 117 | nanmasked_nii = np.ma.masked_array(masked_vals, 118 | np.isnan(masked_vals)) 119 | else: 120 | raise ValueError("Mask and nifti image are " + 121 | "different shapes/sizes.") 122 | return nanmasked_nii.mean() 123 | else: 124 | raise ValueError("Variable mask_image points to non-existent file.") 125 | 126 | 127 | def return_spm_betas(spm_path, wanted_contrasts, mask_file): 128 | """ 129 | Given the path to an SPM.mat file, a list of desired contrasts, and a mask, 130 | calls other extract_betas functions to create list of mean values (one mean 131 | for each contrast). 132 | """ 133 | beta_values = [] 134 | wanted_file_list = get_spm_contrast_files(spm_path + "/SPM.mat", 135 | wanted_contrasts) 136 | for i_con, contrast in enumerate(wanted_contrasts): 137 | beta_file = spm_path + wanted_file_list[i_con] 138 | beta_value = extract_betas(beta_file, mask_file) 139 | beta_values.append(beta_value) 140 | 141 | return beta_values 142 | 143 | 144 | def extract_spm_timeseries(spm_file, contrast_list): 145 | print("Let's do this! At some point...") 146 | 147 | 148 | def get_fsl_contrast_list(design_file): 149 | print("Just kidding! I don't know how to do this yet. " + 150 | "But it's on the to-do list!") 151 | 152 | 153 | def get_fsl_contrast_files(design_file, contrast_list): 154 | print("Just kidding! I don't know how to do this yet. " + 155 | "But it's on the to-do list!") 156 | 157 | 158 | def return_fsl_betas(feat_dir, wanted_contrasts, mask_file): 159 | print("Just kidding! I don't know how to do this yet. " + 160 | "But it's on the to-do list!") 161 | 162 | 163 | if __name__ == "__main__": 164 | if len(sys.argv) == 1: 165 | raise ValueError("Variable nifti_file not given.") 166 | elif len(sys.argv) == 2: 167 | nifti_file = sys.argv[1] 168 | mean = extract_betas(nifti_file) 169 | elif len(sys.argv) == 3: 170 | nifti_file = sys.argv[1] 171 | mask_file = sys.argv[2] 172 | mean = extract_betas(nifti_file, mask_file) 173 | else: 174 | raise ValueError("Too many inputs given.") 175 | -------------------------------------------------------------------------------- /lss/batchLssSpm.m: -------------------------------------------------------------------------------- 1 | % batch_newLSS 2 | % General settings 3 | tStart = tic; 4 | subjects = {'epc138'}; 5 | 6 | spmFolder = '/nfs/ep2/AX/first_levels/00_MONTH/'; 7 | spmSubFolder = '/MTU/func_an_SPM8/'; 8 | 9 | outFolder = '/home/tsalo/lssForUnivariate/'; 10 | outSubFolder = '/'; 11 | 12 | % LSS settings 13 | includeConditions = {'CueA' 'CueB'}; 14 | settings.model = 2; % 1- Rissman, 2- LSS 15 | settings.useTempFS = 1; % 0- do not use temporary files, 1- use temporary files 16 | settings.overwrite = 0; % 0- do not overwrite, 1- overwrite 17 | 18 | % Connectivity settings 19 | rois = load('/nfs/cntracs/lssForKim/gt_rois_final.mat'); 20 | settings.fConnType = 'roi2roi'; % seed2voxel or roi2roi 21 | 22 | for iSubj = 1:length(subjects) 23 | spmDir = [spmFolder subjects{iSubj} spmSubFolder]; 24 | outDir = [outFolder subjects{iSubj} outSubFolder]; 25 | 26 | images = lssGenerateBetasSpm(subjects{iSubj}, spmDir, outDir, includeConditions, settings); 27 | lssCorrelation(images, rois.rois, settings); 28 | end 29 | hourToc(tStart); -------------------------------------------------------------------------------- /lss/lssCorrelation.m: -------------------------------------------------------------------------------- 1 | function lssCorrelation(images, rois, settings) 2 | % FORMAT lssCorrelation(images, rois, settings) 3 | % Takes cell array of 4D images and, for each image, extracts mean beta 4 | % series from masks and computes one of two forms of functional 5 | % connectivity: seed2voxel or roi2roi. In seed2voxel functional 6 | % connectivity, the function calculates voxel-wise correlation between 7 | % mean beta series from a given mask and beta series from the rest of the 8 | % brain. It then converts that R image to a Z image before taking the mean 9 | % of Z images across all sessions for a given condition and saving that 10 | % image. In roi2roi functional connectivity, it extracts the mean beta 11 | % series from each mask given and correlates those beta series to produce a 12 | % matrix, which it then averages across sessions. The outputted matrix is 13 | % also in Z values. I have not done much research into graph theory so 14 | % nothing is included for analyzing the roi2roi results. 15 | % 16 | % 17 | % images: Cell array of 4D images from 18 | % generate_spm_singletrial_newLSS in format 19 | % images{conds}{sessImages} 20 | % rois: Cell array of paths to masks from which beta series 21 | % will be extracted. For seed2voxel connectivity this 22 | % beta series will be correlated with each of the 23 | % voxels in the brain, while in roi2roi connectivity 24 | % each roi's beta series will be correlated with every 25 | % other roi's beta series. 26 | % settings: Additional settings. Structure. 27 | % settings.fConnType: Which form of functional connectivity will be 28 | % performed. Options are "seed2voxel" and "roi2roi". 29 | % String. 30 | % settings.overwrite: Overwrite any pre-existing files (1) or not (0). 31 | % Double. 32 | % 33 | % 34 | % License: 35 | % lssCorrelation.m is the proprietary property of The Regents of the 36 | % University of California (“The Regents.”) 37 | % Copyright © 2015 The Regents of the University of California, Davis 38 | % campus. All Rights Reserved. 39 | % Redistribution and use in source and binary forms, with or without 40 | % modification, are permitted by nonprofit, research institutions for 41 | % research use only, provided that the following conditions are met: 42 | % - Redistributions of source code must retain the above copyright 43 | % notice, this list of conditions and the following disclaimer. 44 | % - Redistributions in binary form must reproduce the above copyright 45 | % notice, this list of conditions and the following disclaimer in the 46 | % documentation and/or other materials provided with the distribution. 47 | % - The name of The Regents may not be used to endorse or promote 48 | % products derived from this software without specific prior written 49 | % permission. 50 | % The end-user understands that the program was developed for research 51 | % purposes and is advised not to rely exclusively on the program for any 52 | % reason. 53 | % THE SOFTWARE PROVIDED IS ON AN "AS IS" BASIS, AND THE REGENTS HAVE NO 54 | % OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR 55 | % MODIFICATIONS. THE REGENTS SPECIFICALLY DISCLAIM ANY EXPRESS OR IMPLIED 56 | % WARRANTIES, INCLUDING BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 57 | % MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 58 | % NO EVENT SHALL THE REGENTS BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, 59 | % SPECIAL, INCIDENTAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES, INCLUDING BUT 60 | % NOT LIMITED TO PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, LOSS OF USE, 61 | % DATA OR PROFITS, OR BUSINESS INTERRUPTION, HOWEVER CAUSED AND UNDER ANY 62 | % THEORY OF LIABILITY WHETHER IN CONTRACT, STRICT LIABILITY OR TORT 63 | % (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 64 | % THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY 65 | % OF SUCH DAMAGE. 66 | % If you do not agree to these terms, do not download or use the software. 67 | % This license may be modified only in a writing signed by authorized 68 | % signatory of both parties. 69 | % For commercial license information please contact copyright@ucdavis.edu. 70 | 71 | imageDir = fileparts(images{1}{1}); 72 | parentDir = fileparts(imageDir); 73 | switch settings.fConnType 74 | case 'seed2voxel' 75 | outDir = [parentDir '/seed2voxel_correlations/']; 76 | if ~exist(outDir, 'dir') 77 | mkdir(outDir); 78 | end 79 | 80 | minVoxels = 20; 81 | 82 | for iCond = 1:length(images) 83 | for jSess = 1:length(images{iCond}) 84 | % Get rid of 4D from file name. 85 | [~, fileName] = fileparts(images{iCond}{jSess}); 86 | separatedFilename = strsplit(fileName, '_'); 87 | separatedFilename(1) = []; 88 | fileName = strjoin(separatedFilename, '_'); 89 | for kROI = 1:length(rois) 90 | % Get ROI name 91 | [~, roiName] = fileparts(rois{kROI}); 92 | 93 | % Create file names for condition outputs. 94 | corrFilename{1} = [outDir '/Rcorr_' roiName '_' fileName '.nii']; 95 | corrFilename{2} = [outDir '/R_atanh_corr_' roiName '_' fileName '.nii']; % Also known as z' 96 | corrFilename{3} = [outDir '/Zcorr_' roiName '_' fileName '.nii']; % Also known as z 97 | zImages{iCond}{kROI}{jSess} = corrFilename{2}; 98 | 99 | if settings.overwrite || ~exist(corrFilename{2}, 'file') 100 | % Correlate rest of brain with extracted timeseries from mask (R, R_atahn, Z). 101 | meanRoi = extractBetaSeries(images{iCond}{jSess}, rois{kROI}, minVoxels)'; 102 | niiHeader = spm_vol(images{iCond}{jSess}); 103 | [Y, ~] = spm_read_vols(niiHeader(1)); 104 | nanIdx = find(~isnan(Y)); 105 | [x, y, z] = ind2sub(size(Y), nanIdx); 106 | xVol.XYZ = [x y z].'; 107 | xVol.DIM = size(Y); 108 | xVol.M = niiHeader(1).mat; 109 | correlation = correlateBetaSeries(niiHeader, xVol, meanRoi); 110 | 111 | % Write correlation (data) to corrFilename (name of output file). 112 | writeCorrelationImage(correlation{1}, corrFilename{1}, xVol); 113 | writeCorrelationImage(correlation{2}, corrFilename{2}, xVol); 114 | else 115 | fprintf('Exists: %s\n', corrFilename{2}); 116 | end 117 | end 118 | end 119 | for jROI = 1:length(rois) 120 | [outDir, fname] = fileparts(zImages{iCond}{jROI}{1}); 121 | outName = [outDir '/mean_' fname(1:end-8) '.nii']; 122 | if settings.overwrite || ~exist(outName, 'file') 123 | % If there are multiple runs (i.e. LSS), create a mean 124 | % file. Or should I create a "mean" file even if there 125 | % aren't multiple runs, for the consistency? 126 | if length(zImages{iCond}{jROI}) > 1 127 | writeSummaryImage(zImages{iCond}{jROI}, outName, 'mean(X)'); 128 | end 129 | else 130 | fprintf('Exists: %s\n', outName); 131 | end 132 | end 133 | end 134 | case 'roi2roi' 135 | outDir = [parentDir '/roi2roi_correlations/']; 136 | if ~exist(outDir, 'dir') 137 | mkdir(outDir); 138 | end 139 | 140 | % If there are NaNs in a given ROI at a given beta, we will simply 141 | % average/correlate around them. However, if there are too many 142 | % NaNs (and consequently too few real values), we need to flag that 143 | % ROI and/or timepoint. 144 | minVoxels = 20; 145 | minTimepoints = 5; 146 | 147 | for iCond = 1:length(images) 148 | for jSess = 1:length(images{iCond}) 149 | % Get rid of 4D from file name. 150 | [~, fileName, ~] = fileparts(images{iCond}{jSess}); 151 | separatedFilename = strsplit(fileName, '_'); 152 | separatedFilename(1) = []; 153 | fileName = strjoin(separatedFilename, '_'); 154 | 155 | % Create file names for condition outputs. 156 | corrMatrixName{1} = [outDir '/Rcorr' fileName '.mat']; 157 | corrMatrixName{2} = [outDir '/Zcorr' fileName '.mat']; 158 | zImages{iCond}{jSess} = corrMatrixName{2}; 159 | 160 | if settings.overwrite || ~exist(corrMatrixName{2}, 'file') 161 | % Preallocate matrices and cell arrays. 162 | rCorrMatrix = zeros(length(rois)); zCorrMatrix = zeros(length(rois)); 163 | roiNames = cell(length(rois), 1); roiBetaSeries = cell(length(rois), 1); 164 | 165 | % Get ROI names 166 | for kROI = 1:length(rois) 167 | [~, roiNames{kROI}] = fileparts(rois{kROI}); 168 | roiBetaSeries{kROI} = extractBetaSeries(images{iCond}{jSess}, rois{kROI}, minVoxels)'; 169 | end 170 | 171 | badRois = {}; 172 | nTrials = length(roiBetaSeries{1}); 173 | for kROI = 1:length(rois) 174 | kRoiValueTimepoints = find(~isnan(roiBetaSeries{kROI})); 175 | if length(kRoiValueTimepoints) >= minTimepoints 176 | for mROI = 1:length(rois) 177 | mRoiValueTimepoints = find(~isnan(roiBetaSeries{mROI})); 178 | bothRoiValueTimepoints = intersect(kRoiValueTimepoints, mRoiValueTimepoints); 179 | if length(bothRoiValueTimepoints) >= minTimepoints 180 | pairwiseCorrelations = corrcoef(roiBetaSeries{kROI}, roiBetaSeries{mROI}, 'rows', 'complete'); 181 | rCorrMatrix(kROI, mROI) = pairwiseCorrelations(1, 2); 182 | else 183 | rCorrMatrix(kROI, mROI) = NaN; 184 | end 185 | 186 | % corr(x, y, 'type', 'Pearson') will return NaN if any values of x or y is NaN. 187 | % corrcoef(x, y, 'rows', 'complete') works around NaNs and 188 | % only returns NaN if all (xi, yi) pairs contain a NaN. 189 | zCorrMatrix(kROI, mROI) = atanh(rCorrMatrix(kROI, mROI)); 190 | end 191 | else 192 | badRois = unique([badRois roiNames{kROI}]); 193 | rCorrMatrix(kROI, 1:end) = NaN; 194 | zCorrMatrix(kROI, 1:end) = NaN; 195 | end 196 | end 197 | corrStruct.rois = roiNames; 198 | corrStruct.badRois = badRois; 199 | corrStruct.corrMatrix = rCorrMatrix; 200 | save(corrMatrixName{1}, 'corrStruct'); 201 | corrStruct.corrMatrix = zCorrMatrix; 202 | save(corrMatrixName{2}, 'corrStruct'); 203 | end 204 | end 205 | [outDir, fname] = fileparts(zImages{iCond}{1}); 206 | outName = [outDir '/mean_' fname(1:end-8) '.mat']; 207 | 208 | if settings.overwrite || ~exist(outName, 'file') 209 | % If there are multiple runs (i.e. LSS), create a mean file. 210 | if length(zImages{iCond}) > 1 211 | allCorr = []; 212 | meanStruct.badRois = {}; 213 | for jSess = 1:length(images{iCond}) 214 | load(zImages{iCond}{jSess}); 215 | allCorr(:, :, jSess) = corrStruct.corrMatrix; 216 | meanStruct.rois = corrStruct.rois; 217 | % This method of determining bad ROIs basically 218 | % considers an ROI bad if it's bad in ANY session 219 | % for that condition. It could be changed to ALL or 220 | % majority or something, but I don't know what's 221 | % best. -TS 222 | meanStruct.badRois = unique([meanStruct.badRois corrStruct.badRois]); 223 | clear corrStruct 224 | end 225 | meanStruct.meanCorr = nanmean(allCorr, 3); 226 | save(outName, 'meanStruct'); 227 | end 228 | else 229 | fprintf('Exists: %s\n', outName); 230 | end 231 | end 232 | otherwise 233 | error(['I cannot make sense of this: ' settings.fConnType]); 234 | end 235 | end 236 | 237 | %% Extract beta series 238 | function meanRoi = extractBetaSeries(niiLoc, roiLoc, minVoxels, trimStd) 239 | % FORMAT meanRoi = extractBetaSeries(niiLoc, roiLoc, minVoxels, trimStd) 240 | % Extracts and returns mean beta series from 4D niiLoc within ROI. 241 | % Adapted from beta_series_correlation_nomars by Dennis Thompson 242 | % without Events information. 243 | % Calls spm_vol, roiFindIndex, adjustXyz, and spm_get_data. 244 | % 245 | % 246 | % niiLoc: Path to LSS file (4D nifti) for particular condition. 247 | % Contains data to be correlated. String. 248 | % roiLoc: Path to mask file to be used. String. 249 | % minVoxels: The minimum number of voxels that must have real values 250 | % (not NaNs) to return a real mean. If below this 251 | % threshold, will return NaN. 252 | % trimStd: The number of standard deviations to use if you wish 253 | % the raw data to be Windsorized. Set to 0 if the raw 254 | % data are to be used. LSS default is 3. Double. 255 | 256 | if ~exist('trimStd', 'var') 257 | trimStd = 3; 258 | end 259 | threshold = 0; % Find mask values greater than this 260 | 261 | % Get header info for beta data. 262 | niiHeader = spm_vol(niiLoc); 263 | 264 | % Get ROI index and transform matrix. 265 | if exist(roiLoc, 'file') 266 | [xyz, roiMatrix] = roiFindIndex(roiLoc, threshold); 267 | else 268 | error('Mask not found: %s\n', roiLoc); 269 | end 270 | 271 | % Generate XYZ locations for each beta image correcting for alignment 272 | % issues and preallocate meanRoi vector. 273 | betaXyz = adjustXyz(xyz, roiMatrix, niiHeader); 274 | meanRoi = zeros(1, length(betaXyz)); 275 | 276 | % Extract mean of ROI from each beta. 277 | for iBeta = 1:length(betaXyz), 278 | betasInRoi = spm_get_data(niiHeader(iBeta), betaXyz{iBeta}); 279 | nNotNans = sum(~isnan(betasInRoi(:))); 280 | if nNotNans >= minVoxels 281 | meanRoi(iBeta) = nanmean(betasInRoi(:)); 282 | else 283 | meanRoi(iBeta) = NaN; 284 | end 285 | end 286 | 287 | clear betasInRoi 288 | 289 | if trimStd > 0, 290 | meanRoi = trimTimeseries(meanRoi, trimStd); 291 | end 292 | end 293 | 294 | %% Create Correlation image 295 | function correlationMatrix = correlateBetaSeries(niiHeader, xVol, meanRoi, trimStd) 296 | % FORMAT correlationMatrix = correlateBetaSeries(niiHeader, xVol, meanRoi, trimStd) 297 | % Correlates meanRoi beta series with beta series of all other voxels in 298 | % brain. Converts resulting R matrix to Z matrix. 299 | % Adapted from beta_series_correlation_nomars by Dennis Thompson 300 | % without Events information. 301 | % Calls spm_vol and spm_get_data. 302 | % 303 | % 304 | % niiHeader: Header of LSS file (4D nifti) for particular condition. 305 | % Contains data to be correlated. String. 306 | % xVol: Select information from LSS file. Structure. 307 | % meanRoi: Mean beta series correlated with all other voxels' beta 308 | % series in brain. Double vector. 309 | % trimStd: The number of standard deviations to use if you wish 310 | % the raw data to be Windsorized. Set to 0 if the raw 311 | % data are to be used. LSS default is 3. Double. 312 | 313 | if ~exist('trimStd', 'var'), trimStd = 3; end 314 | 315 | allBetasAcrossVolumes = spm_get_data(niiHeader, xVol.XYZ); 316 | correlationMatrix = cell(1, 3); correlationMatrix{1} = zeros(1, size(allBetasAcrossVolumes, 2)); 317 | for iVoxel = 1:size(allBetasAcrossVolumes, 2), 318 | if trimStd > 0 319 | allBetasAcrossVolumes(:, iVoxel) = trimTimeseries(allBetasAcrossVolumes(:, iVoxel), trimStd); 320 | end 321 | correlationMatrix{1}(iVoxel) = corr(meanRoi, allBetasAcrossVolumes(:, iVoxel), 'type', 'Pearson'); 322 | end 323 | correlationMatrix{2} = atanh(correlationMatrix{1}); 324 | steZ = 1 / sqrt(length(niiHeader) - 3); 325 | correlationMatrix{3} = correlationMatrix{2} / steZ; 326 | end 327 | 328 | %% Write Correlation Image 329 | function writeCorrelationImage(correlationMatrix, outNiftiName, xVol) 330 | % FORMAT writeCorrelationImage(correlationMatrix, outNiftiName, xVol) 331 | % Writes out whole brain correlation data into a nifti image. Verbatim 332 | % Dennis Thompson's write_correlation_image. 333 | % 334 | % 335 | % correlationMatrix: A vector containing the correlations. 336 | % outNiftiName: What the file is to be called, can include the full path. 337 | % xVol: A sub set of the SPM data structure. 338 | 339 | nVoxels = size(xVol.XYZ, 2); % length of real data in brain - should match the length of correlationMatrix 340 | corrData = NaN * ones(xVol.DIM(1), xVol.DIM(2), xVol.DIM(3)); % array of NaN size of brain volumne 341 | 342 | % replace NaN's with corr results 343 | for iVoxel = 1:nVoxels 344 | corrData(xVol.XYZ(1, iVoxel), xVol.XYZ(2, iVoxel), xVol.XYZ(3, iVoxel)) = correlationMatrix(iVoxel); 345 | end 346 | 347 | [outPath, outName, outExtension] = fileparts(outNiftiName); 348 | 349 | % make nifti data structure see spm_vol - dt is [32 bit float, little endian] 350 | corrIm = struct ('fname', [outName, outExtension], ... 351 | 'dim', xVol.DIM, ... 352 | 'dt', [16, 0], ... 353 | 'mat', xVol.M, ... 354 | 'pinfo', [1 0 0]', ... 355 | 'descript', 'beta-correlation'); 356 | 357 | cwd = pwd; 358 | cd(outPath); 359 | 360 | corrIm = spm_create_vol(corrIm, 'noopen'); 361 | [~] = spm_write_vol(corrIm, corrData); 362 | 363 | cd(cwd); 364 | end 365 | 366 | %% Trim Timeseries 367 | function [y, nTrimmed] = trimTimeseries(y, sd) 368 | % FORMAT [y, nTrimmed] = trimTimeseries(y, sd) 369 | % Windsorizes data y by number of standard deviations sd. 370 | % By Dennis Thompson. 371 | % 372 | % y: 1D vector of data. 373 | % sd: Number of standard deviations. Values out of this range 374 | % are to be replaced. 375 | % y (output): 1D vectors of Windsorized data. 376 | % nTrimmed: Number of values replaced. 377 | 378 | nTrimmed = 0; 379 | idx = find(abs(y - mean(y)) > sd * std(y)); 380 | if ~isempty(idx) 381 | y(idx) = mean(y) + (sign(y(idx)) * sd * std(y)); 382 | nTrimmed = length(idx); 383 | end 384 | end 385 | 386 | %% Extract Coordinates of ROI 387 | function [index, mat] = roiFindIndex(roiLoc, thresh) 388 | % FORMAT [index, mat] = roiFindIndex(roiLoc, thresh) 389 | % Returns the XYZ address of voxels with values greater than threshold. 390 | % By Dennis Thompson. 391 | % 392 | % 393 | % roiLoc: String pointing to nifti image. 394 | % thresh: Threshold value, defaults to zero. Double. 395 | 396 | if ~exist('thresh','var'), 397 | thresh = 0; 398 | end 399 | 400 | data = nifti(roiLoc); 401 | Y = double(data.dat); 402 | Y(isnan(Y)) = 0; 403 | index = []; 404 | for n = 1:size(Y, 3) 405 | % find values greater > thresh 406 | [xx, yy] = find(squeeze(Y(:, :, n)) > thresh); 407 | if ~isempty(xx) 408 | zz = ones(size(xx)) * n; 409 | index = [index [xx'; yy'; zz']]; 410 | end 411 | end 412 | 413 | mat = data.mat; 414 | end 415 | 416 | %% Adjust Coordinates of ROI 417 | function funcXyz = adjustXyz(xyz, roiMatrix, niiHeader) 418 | % FORMAT funcXyz = adjustXyz(xyz, roiMatrix, niiHeader) 419 | % By Dennis Thompson. 420 | % 421 | % 422 | % xyz: Output from roiFindIndex. 423 | % roiMatrix: Output from roiFindIndex. 424 | % niiHeader: Header information of nifti file from spm_vol. 425 | 426 | xyz(4, :) = 1; 427 | funcXyz = cell(length(niiHeader)); 428 | for n = 1:length(niiHeader) 429 | if(iscell(niiHeader)) 430 | tmp = inv(niiHeader{n}.mat) * (roiMatrix * xyz); 431 | else 432 | tmp = inv(niiHeader(n).mat) * (roiMatrix * xyz); 433 | end 434 | funcXyz{n} = tmp(1:3, :); 435 | end 436 | end 437 | 438 | %% Write Out Mean Image 439 | function writeSummaryImage(cellVols, outName, expression) 440 | % FORMAT writeSummaryImage(cellVols, outName, expression) 441 | % Calls spm_imcalc to perform calculations (sum, mean, std) on a cell array 442 | % of volumes to output one summary volume. 443 | % 444 | % cellVols: Cell array of images upon which calculations will be 445 | % performed. 446 | % outName: The name (with full path) of the image to be written. String. 447 | % expression: The calculation to be performed upon the images in cellVols. 448 | % String. Possible values: sum(X), mean(X), std(X), maybe others. 449 | 450 | for iVol = 1:length(cellVols) 451 | inputHeader(iVol) = spm_vol(cellVols{iVol}); 452 | end 453 | outputHeader = inputHeader(1); 454 | outputHeader.fname = outName; 455 | 456 | spm_imcalc(inputHeader, outputHeader, expression, {1, 0, 0}); 457 | end 458 | -------------------------------------------------------------------------------- /lss/lssGenerateBetasSpm.m: -------------------------------------------------------------------------------- 1 | function images = lssGenerateBetasSpm(subject, spmDir, outDir, includeConditions, settings) 2 | % FORMAT images = lssGenerateBetasSpm(subject, spmDir, outDir, includeConditions, settings) 3 | % This function takes an existing first-level SPM.mat file uses it to 4 | % create one of two possible models: multi-regressor and multi-model. 5 | % The multi-regressor approach estimates a single model with all trials 6 | % represented by individual regressors. The multi-model approach estimates 7 | % a model for each individual trial, setting the first regressor to the 8 | % individual trial and all other regressors to be the same as the original 9 | % model. Beta images are then moved and renamed in a single betas 10 | % directory. The multi-regressor approach is similar to that described in 11 | % Rissman et al. 2004 NI, and the multi-model approach is similar to the 12 | % LS-S approach described in Turner et al. 2012 NI. 13 | % This function is integrated with newLSS_correlation for beta-series 14 | % functional connectivity with the multi-model approach through 15 | % batch_newLSS. 16 | % 17 | % 18 | % Inputs: 19 | % subject: Subject ID. String. 20 | % spmDir: Path to folder containing SPM.mat file. String. 21 | % outDir: Path to output directory, where generated files 22 | % will be saved. String. 23 | % ignoreConditions: Conditions to be ignored. Set to NONE if you do 24 | % not want to ignore any conditions. Cell array of 25 | % strings. 26 | % settings: Additional settings. Structure. 27 | % settings.model: Which model type you wish to run: Rissman beta 28 | % series (1) or LS-S multiple models (2). Double. 29 | % settings.overwrite: Overwrite any pre-existing files (1) or not (0). 30 | % Double. 31 | % settings.deleteFiles: Delete intermediate files (1) or not (0). Double. 32 | % 33 | % Outputs: 34 | % images: Cell array of 4D images generated by function in 35 | % format images{conds}{sessImages} 36 | % 37 | % Requirements: SPM8, cellstrfind (Matlab function written by Taylor Salo) 38 | % 39 | % 40 | % Created by Maureen Ritchey 121010 41 | % Modified by Taylor Salo 140806-141216 according to adjustments suggested 42 | % by Jeanette Mumford. LS-S now matches Turner et al. 2012 NI, where the 43 | % design matrix basically matches the original design matrix (for a given 44 | % block), except for the single trial being evaluated, which gets its own 45 | % regressor. Also now you can ignore multiple conditions. I also added some 46 | % overwrite stuff so that it won't re-run existing subjects if you don't 47 | % want it to. 48 | 49 | %% MAIN CODE 50 | % Load pre-existing SPM file containing model information 51 | fprintf('\nLoading previous model for %s:\n%s\n', subject, [spmDir, '/SPM.mat']); 52 | if exist([spmDir '/SPM.mat'],'file') 53 | OrigSpm = load([spmDir '/SPM.mat']); 54 | else 55 | error('Cannot find SPM.mat file.'); 56 | end 57 | 58 | if ~exist(outDir, 'dir') 59 | fprintf('\nCreating directory:\n%s\n', outDir); 60 | mkdir(outDir) 61 | end 62 | 63 | temporaryRootFolder = ['/run/shm/' getenv('USER') '/']; 64 | tempDir = [temporaryRootFolder subject '/']; 65 | 66 | % Get model information from SPM file 67 | fprintf('\nGetting model information...\n'); 68 | files = OrigSpm.SPM.xY.P; 69 | fprintf('Modeling %i timepoints across %i sessions.\n', size(files, 1), length(OrigSpm.SPM.Sess)); 70 | % Make trial directory 71 | betaDir = [outDir 'betas/']; 72 | if ~exist(betaDir, 'dir') 73 | mkdir(betaDir) 74 | end 75 | 76 | % MULTI-MODEL APPROACH 77 | if settings.model == 2 78 | spm_jobman('initcfg') 79 | spm('defaults', 'FMRI'); 80 | matlabpool open 81 | 82 | if settings.useTempFS 83 | [nRows, nCols] = size(files); 84 | index = 1:nCols; 85 | for iRow = 1:nRows 86 | index = intersect(index, find(OrigSpm.SPM.xY.P(1, :) == OrigSpm.SPM.xY.P(iRow, :))); 87 | end 88 | 89 | indexDiff = diff(index)==1; 90 | consecutiveIndex = find([false, indexDiff]~=[indexDiff, false]); 91 | lastConsecutive = consecutiveIndex(2:2:end); 92 | newFiles = cellstr(files); 93 | 94 | for iFile = 1:nRows 95 | oldPath = fileparts(files(iFile, 1:lastConsecutive)); 96 | newFile = strrep(files(iFile, :), oldPath, tempDir); 97 | newFiles{iFile} = newFile; 98 | newDataDir = fileparts(newFile); 99 | if ~exist(newDataDir, 'dir') 100 | mkdir(newDataDir); 101 | end 102 | system(['cp ' files(iFile, 1:end-2) ' ' newFile(1:end-2)]); 103 | end 104 | end 105 | 106 | % Loop across sessions 107 | for iSess = 1:length(OrigSpm.SPM.Sess) 108 | rows = OrigSpm.SPM.Sess(iSess).row; 109 | if settings.useTempFS 110 | sessFiles = newFiles(rows); 111 | else 112 | sessFiles = files(rows', :); 113 | sessFiles = cellstr(sessFiles); 114 | end 115 | covariates = OrigSpm.SPM.Sess(iSess).C.C; 116 | originalNames = cell(1, length(OrigSpm.SPM.Sess(iSess).U)); 117 | originalOnsets = cell(1, length(OrigSpm.SPM.Sess(iSess).U)); 118 | originalDurations = cell(1, length(OrigSpm.SPM.Sess(iSess).U)); 119 | for jCond = 1:length(OrigSpm.SPM.Sess(iSess).U) 120 | originalNames{jCond} = OrigSpm.SPM.Sess(iSess).U(jCond).name{1}; 121 | originalOnsets{jCond} = OrigSpm.SPM.Sess(iSess).U(jCond).ons; 122 | originalDurations{jCond} = OrigSpm.SPM.Sess(iSess).U(jCond).dur; 123 | end 124 | [lssNames, lssOnsets, lssDurations] = lssMakeVectors(originalNames, originalOnsets, originalDurations, includeConditions); 125 | 126 | for jCond = 1:length(includeConditions) 127 | if settings.overwrite || ~exist([betaDir '4D_' includeConditions{jCond} '_Sess' sprintf('%03d', iSess) '.nii'], 'file') 128 | % As long as the current condition is in includeConditions, 129 | % set up a model for each individual trial. 130 | parfor kTrial = 1:length(lssOnsets{jCond}) 131 | singleName = lssNames{jCond}{kTrial}{1}; 132 | names = lssNames{jCond}{kTrial}; 133 | onsets = lssOnsets{jCond}{kTrial}; 134 | durations = lssDurations{jCond}{kTrial}; 135 | 136 | % Make trial directory 137 | if settings.useTempFS 138 | trialDir = [tempDir 'Sess' sprintf('%03d', iSess) '/' singleName '/']; 139 | else 140 | trialDir = [betaDir 'Sess' sprintf('%03d', iSess) '/' singleName '/']; 141 | end 142 | if ~exist(trialDir,'dir') 143 | mkdir(trialDir) 144 | end 145 | 146 | % Save regressor onset files 147 | regFile = [trialDir 'st_regs.mat']; 148 | parsave(regFile, names, onsets, durations); 149 | 150 | covFile = [trialDir 'st_covs.txt']; 151 | dlmwrite(covFile, covariates, '\t'); 152 | 153 | % Create matlabbatch for creating new SPM.mat file 154 | matlabbatch = createSpmBatch(trialDir, OrigSpm.SPM); 155 | matlabbatch = addSessionToBatch(matlabbatch, 1, sessFiles, regFile, covFile, OrigSpm.SPM); 156 | 157 | % Run matlabbatch to create new SPM.mat file using SPM batch tools 158 | if settings.overwrite || ~exist([trialDir 'beta_0001.img'], 'file') 159 | fprintf('\nCreating SPM.mat file:\n%s\n\n', [trialDir 'SPM.mat']); 160 | spm_jobman('serial', matlabbatch); 161 | runBatches = 1; 162 | else 163 | runBatches = 0; 164 | end 165 | 166 | if runBatches 167 | fprintf('\nEstimating model from SPM.mat file.\n'); 168 | spmFile = [trialDir 'SPM.mat']; 169 | matlabbatch = estimateSpmFile(spmFile); 170 | spm_jobman('serial', matlabbatch); 171 | 172 | % Copy first beta image to beta directory 173 | NewSpm = load(spmFile); 174 | stopThat = false 175 | for mBeta = 1:length(NewSpm.SPM.Vbeta) 176 | if ~isempty(strfind(NewSpm.SPM.Vbeta(mBeta).descrip, singleName)) && ~stopThat 177 | betaFile = [trialDir NewSpm.SPM.Vbeta(mBeta).fname]; 178 | stopThat = true 179 | end 180 | end 181 | 182 | if strfind(betaFile, '.img') 183 | [~, betaFileName, ~] = fileparts(betaFile); 184 | system(['cp ' betaFile ' ' betaDir 'Sess' sprintf('%03d', iSess) '_' singleName '.img']); 185 | system(['cp ' trialDir betaFileName '.hdr ' betaDir 'Sess' sprintf('%03d', iSess) '_' singleName '.hdr']); 186 | elseif strfind(betaFile, '.nii') 187 | system(['cp ' betaFile ' ' betaDir 'Sess' sprintf('%03d', iSess) '_' singleName '.nii']); 188 | end 189 | 190 | % Discard extra files, if desired. 191 | if settings.useTempFS 192 | system(['rm -rf ' trialDir]); 193 | end 194 | end 195 | end 196 | end 197 | end 198 | 199 | % Make 4D image for each condition of interest in block. 200 | for jCond = 1:length(includeConditions) 201 | condVols = dir([betaDir 'Sess' sprintf('%03d', iSess) '_' includeConditions{jCond} '*.img']); 202 | if isempty(condVols) 203 | condVols = dir([betaDir 'Sess' sprintf('%03d', iSess) '_' includeConditions{jCond} '*.nii']); 204 | end 205 | 206 | cellVols = struct2cell(condVols); 207 | cellVols = cellVols(1, :); 208 | for kVol = 1:length(cellVols) 209 | cellVols{kVol} = [betaDir cellVols{kVol} ',1']; 210 | end 211 | images{jCond}{iSess} = [betaDir '4D_' includeConditions{jCond} '_Sess' sprintf('%03d', iSess) '.nii']; 212 | matlabbatch{1}.spm.util.cat.name = [betaDir '4D_' includeConditions{jCond} '_Sess' sprintf('%03d', iSess) '.nii']; 213 | matlabbatch{1}.spm.util.cat.vols = cellVols'; 214 | matlabbatch{1}.spm.util.cat.dtype = 0; 215 | 216 | if settings.overwrite || ~exist([betaDir '4D_' includeConditions{jCond} '_Sess' sprintf('%03d', iSess) '.nii'], 'file') 217 | save([betaDir '3Dto4D_jobfile.mat'], 'matlabbatch'); 218 | spm_jobman('run', matlabbatch); 219 | else 220 | fprintf('Exists: %s\n', [betaDir '4D_' includeConditions{jCond} '_Sess' sprintf('%03d', iSess) '.nii']); 221 | end 222 | end 223 | end 224 | 225 | % Delete data folder, if desired. 226 | if settings.useTempFS 227 | system(['rm -rf ' tempDir]); 228 | end 229 | matlabpool close 230 | 231 | % MULTI-REGRESSOR APPROACH 232 | elseif settings.model == 1 233 | spmFile = fullfile(outDir, 'SPM.mat'); 234 | counter = 1; 235 | 236 | % Loop across sessions 237 | for iSess = 1:length(OrigSpm.SPM.Sess) 238 | rows = OrigSpm.SPM.Sess(iSess).row; 239 | sessFiles = files(rows', :); 240 | sessFiles = cellstr(sessFiles); 241 | covariates = OrigSpm.SPM.Sess(iSess).C.C; 242 | 243 | onsets = {}; 244 | durations = {}; 245 | names = {}; 246 | 247 | for jCond = 1:length(OrigSpm.SPM.Sess(iSess).U) 248 | % Check for special condition names to lump together 249 | if ~cellstrfind(OrigSpm.SPM.Sess(iSess).U(jCond).name{1}, includeConditions, '') 250 | onsets = [onsets OrigSpm.SPM.Sess(iSess).U(jCond).ons']; 251 | durations = [durations OrigSpm.SPM.Sess(iSess).U(jCond).dur']; 252 | singleName = [OrigSpm.SPM.Sess(iSess).U(jCond).name{1}]; 253 | names = [names singleName]; 254 | counter = counter + 1; 255 | % Otherwise set up a regressor for each individual trial 256 | else 257 | includeConditions{length(includeConditions) + 1} = OrigSpm.SPM.Sess(iSess).U(jCond).name{1}; 258 | for kTrial = 1:length(OrigSpm.SPM.Sess(iSess).U(jCond).ons) 259 | onsets = [onsets OrigSpm.SPM.Sess(iSess).U(jCond).ons(kTrial)]; 260 | durations = [durations OrigSpm.SPM.Sess(iSess).U(jCond).dur(kTrial)]; 261 | singleName = [OrigSpm.SPM.Sess(iSess).U(jCond).name{1} '_' num2str(kTrial)]; 262 | names = [names singleName]; 263 | counter = counter + 1; 264 | end 265 | end 266 | end 267 | 268 | % Save regressor onset files 269 | if settings.overwrite || ~exist(spmFile, 'file') 270 | fprintf('Saving regressor onset files for Session %i: %i trials included\n', iSess, length(names)); 271 | regFile = [outDir 'st_regs_session_' num2str(iSess) '.mat']; 272 | save(regFile, 'names', 'onsets', 'durations'); 273 | 274 | % Save covariates (e.g., motion parameters) that were specified 275 | % in the original model 276 | covFile = [outDir 'st_covs_session_' num2str(iSess) '.txt']; 277 | dlmwrite(covFile, covariates, '\t'); 278 | 279 | % Create matlabbatch for creating new SPM.mat file 280 | if iSess == 1 281 | matlabbatch = createSpmBatch(outDir, OrigSpm.SPM); 282 | end 283 | matlabbatch = addSessionToBatch(matlabbatch, iSess, sessFiles, regFile, covFile, OrigSpm.SPM); 284 | end 285 | end 286 | 287 | % Run matlabbatch to create new SPM.mat file using SPM batch tools 288 | if settings.overwrite || ~exist(fullfile(outDir, 'SPM.mat'), 'file') 289 | fprintf('\nCreating SPM.mat file:\n%s\n', fullfile(outDir, 'SPM.mat')); 290 | spm_jobman('initcfg') 291 | spm('defaults', 'FMRI'); 292 | spm_jobman('serial', matlabbatch); 293 | clear matlabbatch 294 | 295 | fprintf('\nEstimating model from SPM.mat file.\n'); 296 | matlabbatch = estimateSpmFile(spmFile); 297 | spm_jobman('serial', matlabbatch); 298 | else 299 | fprintf('Exists: %s\n', [outDir 'SPM.mat']); 300 | end 301 | 302 | clear matlabbatch 303 | NewSpm = load(spmFile); 304 | includeConditions = unique(includeConditions); 305 | for iCond = 1:length(includeConditions) 306 | counter = 1; 307 | for jBeta = 1:length(NewSpm.SPM.Vbeta) 308 | if strfind(NewSpm.SPM.Vbeta(jBeta).descrip, includeConditions{iCond}) 309 | cellVols{counter} = fullfile(NewSpm.SPM.swd, [NewSpm.SPM.Vbeta(jBeta).fname ',1']); 310 | counter = counter + 1; 311 | end 312 | end 313 | images{iCond}{1} = [betaDir '4D_' includeConditions{iCond} '.nii']; 314 | matlabbatch{iCond}.spm.util.cat.name = [betaDir '4D_' includeConditions{iCond} '.nii']; 315 | matlabbatch{iCond}.spm.util.cat.vols = cellVols'; 316 | matlabbatch{iCond}.spm.util.cat.dtype = 0; 317 | clear cellVols 318 | end 319 | if settings.overwrite || ~exist([betaDir '4D_' includeConditions{end} '.nii'], 'file') 320 | save([betaDir '3Dto4D_jobfile.mat'], 'matlabbatch'); 321 | spm_jobman('run', matlabbatch); 322 | else 323 | fprintf('Exists: %s\n', [betaDir '4D_' includeConditions{end} '.nii']); 324 | end 325 | clear spmVar matlabbatch newSPM 326 | 327 | % If you didn't set settings.model properly. 328 | else 329 | error('Specify model type as 1 or 2'); 330 | end 331 | 332 | clear SPM 333 | end 334 | 335 | %% SUBFUNCTIONS 336 | function matlabbatch = createSpmBatch(outDir, SPM) 337 | % FORMAT matlabbatch = createSpmBatch(outDir, SPM) 338 | % Subfunction for initializing the matlabbatch structure to create the SPM 339 | % file. 340 | % 341 | % 342 | % 140311 Created by Maureen Ritchey 343 | matlabbatch{1}.spm.stats.fmri_spec.dir = {outDir}; 344 | matlabbatch{1}.spm.stats.fmri_spec.timing.units = SPM.xBF.UNITS; 345 | matlabbatch{1}.spm.stats.fmri_spec.timing.RT = SPM.xY.RT; 346 | matlabbatch{1}.spm.stats.fmri_spec.timing.fmri_t = SPM.xBF.T; 347 | matlabbatch{1}.spm.stats.fmri_spec.timing.fmri_t0 = SPM.xBF.T0; 348 | matlabbatch{1}.spm.stats.fmri_spec.fact = struct('name', {}, 'levels', {}); 349 | matlabbatch{1}.spm.stats.fmri_spec.bases.hrf.derivs = [0 0]; 350 | matlabbatch{1}.spm.stats.fmri_spec.volt = SPM.xBF.Volterra; 351 | matlabbatch{1}.spm.stats.fmri_spec.global = 'None'; 352 | if isempty(SPM.xM.VM) 353 | matlabbatch{1}.spm.stats.fmri_spec.mask = {''}; 354 | else 355 | matlabbatch{1}.spm.stats.fmri_spec.mask = {SPM.xM.VM.fname}; 356 | end 357 | matlabbatch{1}.spm.stats.fmri_spec.cvi = SPM.xVi.form; 358 | end 359 | 360 | function matlabbatch = addSessionToBatch(matlabbatch, iSess, sessFiles, regFile, covFile, SPM) 361 | % FORMAT matlabbatch = addSessionToBatch(matlabbatch, iSess, sessFiles, regFile, covFile, SPM) 362 | % Subfunction for adding sessions to the matlabbatch structure. 363 | % 364 | % 365 | % 140311 Created by Maureen Ritchey 366 | matlabbatch{1}.spm.stats.fmri_spec.sess(iSess).scans = sessFiles; %fix this 367 | matlabbatch{1}.spm.stats.fmri_spec.sess(iSess).cond = struct('name', {}, 'onset', {}, 'duration', {}, 'tmod', {}, 'pmod', {}); 368 | matlabbatch{1}.spm.stats.fmri_spec.sess(iSess).multi = {regFile}; 369 | matlabbatch{1}.spm.stats.fmri_spec.sess(iSess).regress = struct('name', {}, 'val', {}); 370 | matlabbatch{1}.spm.stats.fmri_spec.sess(iSess).multi_reg = {covFile}; 371 | matlabbatch{1}.spm.stats.fmri_spec.sess(iSess).hpf = SPM.xX.K(iSess).HParam; 372 | end 373 | 374 | function matlabbatch = estimateSpmFile(spmFile) 375 | % FORMAT matlabbatch = estimateSpmFile(spmFile) 376 | % Subfunction for creating a matlabbatch structure to estimate the SPM 377 | % file. 378 | % 379 | % 380 | % 140311 Created by Maureen Ritchey 381 | matlabbatch{1}.spm.stats.fmri_est.spmmat = {spmFile}; 382 | matlabbatch{1}.spm.stats.fmri_est.method.Classical = 1; 383 | end 384 | 385 | function [lssNames, lssOnsets, lssDurations] = lssMakeVectors(originalNames, originalOnsets, originalDurations, includeConditions) 386 | % FORMAT [lssNames, lssOnsets, lssDurations] = lssMakeVectors(originalNames, originalOnsets, originalDurations, includeConditions) 387 | % Uses SPM-format vectors (in variables corresponding to names, onsets, and 388 | % durations) to create cell arrays of names, onsets, and durations for each 389 | % LS-S model for conditions of interest. 390 | % 391 | % 392 | % input 393 | % originalNames: Cell array of condition names for a single block. 394 | % originalOnsets: Cell array of trial onset vectors for a single block. 395 | % originalDurations: Cell array of trial duration vectors for a single block. 396 | % includeConditions: A cell array of events we wish to model with the 397 | % beta LSS method. 398 | % 399 | % output 400 | % lssNames: Cell array of LS-S condition names for a single 401 | % block. Format lssNames{includedCondition}{conditionName} 402 | % lssOnsets: Cell array of LS-S condition onsets for a single 403 | % block. Format lssOnsets{includedCondition}{trialVersion}(trial) 404 | % lssDurations: Cell array of LS-S condition durations for a single 405 | % block. Format lssDurations{includedCondition}{trialVersion}(trial) 406 | for iCond = 1:length(includeConditions) 407 | % Determine where conditions of interest are in vectors. 408 | % Setdiff reorders conditions, otherConditions must be reordered. 409 | otherConditionsIdx = ~strcmp(includeConditions{iCond}, originalNames); 410 | [otherConditions, originalOrder] = setdiff(originalNames, includeConditions{iCond}); 411 | [~, sortedOrder] = sort(originalOrder); 412 | otherConditions = otherConditions(sortedOrder); 413 | includeConditionIdx = find(~otherConditionsIdx); 414 | 415 | % Check that condition of interest has more than one trial. 416 | % If condition A only has one trial, you don't need both ConditionA_001 417 | % and Other_ConditionA, because Other_ConditionA would be empty. 418 | if ~isempty(setdiff(originalOnsets{includeConditionIdx}, originalOnsets{includeConditionIdx}(1))) 419 | for jOnset = 1:length(originalOnsets{includeConditionIdx}), 420 | % Create list of condition names 421 | % (e.g. ConditionA_001, Other_ConditionA, ConditionB, ConditionC, etc.) 422 | lssNames{iCond}{jOnset} = [{[originalNames{includeConditionIdx} '_' sprintf('%03d', jOnset)]... 423 | ['Other_' originalNames{includeConditionIdx}]}... 424 | otherConditions]; 425 | 426 | % Single trial 427 | lssOnsets{iCond}{jOnset}{1} = originalOnsets{includeConditionIdx}(jOnset); 428 | lssDurations{iCond}{jOnset}{1} = originalDurations{includeConditionIdx}(jOnset); 429 | 430 | % Other trials of same condition 431 | lssOnsets{iCond}{jOnset}{2} = originalOnsets{includeConditionIdx}; 432 | lssOnsets{iCond}{jOnset}{2}(jOnset) = []; 433 | lssDurations{iCond}{jOnset}{2} = originalDurations{includeConditionIdx}; 434 | lssDurations{iCond}{jOnset}{2}(jOnset) = []; 435 | 436 | % Other conditions 437 | counter = 3; % A counter adjusts around the skipped condition. 438 | for kCond = find(otherConditionsIdx) 439 | lssOnsets{iCond}{jOnset}{counter} = originalOnsets{kCond}; 440 | lssDurations{iCond}{jOnset}{counter} = originalDurations{kCond}; 441 | counter = counter + 1; 442 | end 443 | end 444 | else 445 | % Single trial 446 | lssNames{iCond}{1} = [{[originalNames{includeConditionIdx} '_' sprintf('%03d', 1)]} otherConditions]; 447 | lssOnsets{iCond}{1}{1} = originalOnsets{includeConditionIdx}(1); 448 | lssDurations{iCond}{1}{1} = originalDurations{includeConditionIdx}(1); 449 | 450 | % Other conditions 451 | conditionCounter = 2; % A counter adjusts around the skipped condition. 452 | for kCond = find(otherConditionsIdx) 453 | lssOnsets{iCond}{1}{conditionCounter} = originalOnsets{kCond}; 454 | lssDurations{iCond}{1}{conditionCounter} = originalDurations{kCond}; 455 | conditionCounter = conditionCounter + 1; 456 | end 457 | end 458 | end 459 | end 460 | 461 | function parsave(outFile, names, onsets, durations) 462 | save(outFile, 'names', 'onsets', 'durations'); 463 | end 464 | -------------------------------------------------------------------------------- /lss/py_lss.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Playing with SPM.mat files in Python" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": 1, 13 | "metadata": { 14 | "collapsed": true 15 | }, 16 | "outputs": [], 17 | "source": [ 18 | "import scipy.io as sio\n", 19 | "import numpy as np\n", 20 | "from itertools import groupby\n", 21 | "from operator import itemgetter\n", 22 | "from os.path import dirname" 23 | ] 24 | }, 25 | { 26 | "cell_type": "code", 27 | "execution_count": 2, 28 | "metadata": { 29 | "collapsed": true 30 | }, 31 | "outputs": [], 32 | "source": [ 33 | "def get_consecutive(index):\n", 34 | " ranges = []\n", 35 | " for key, group in groupby(enumerate(index),\n", 36 | " lambda (index, item): index - item):\n", 37 | " group = map(itemgetter(1), group)\n", 38 | " if len(group) > 1:\n", 39 | " ranges.append(xrange(group[0], group[-1]))\n", 40 | " else:\n", 41 | " ranges.append(group[0])\n", 42 | " return ranges[0]" 43 | ] 44 | }, 45 | { 46 | "cell_type": "code", 47 | "execution_count": 3, 48 | "metadata": { 49 | "collapsed": true 50 | }, 51 | "outputs": [], 52 | "source": [ 53 | "def make_lss_vectors(vectors):\n", 54 | " all_names, all_onsets, all_durations = vectors\n", 55 | " lss_names = []\n", 56 | " lss_onsets = []\n", 57 | " lss_durations = []\n", 58 | " for i in range(len(all_names)):\n", 59 | " temp_all_names = list(all_names)\n", 60 | " temp_all_onsets = list(all_onsets)\n", 61 | " temp_all_durations = list(all_durations)\n", 62 | " name = temp_all_names.pop(i)\n", 63 | " onsets = temp_all_onsets.pop(i)\n", 64 | " durations = temp_all_durations.pop(i)\n", 65 | " for trial in range(len(onsets)):\n", 66 | " trial_name = '{0}_{1}'.format(name, trial)\n", 67 | " all_trial_names = [trial_name, name] + temp_all_names\n", 68 | " lss_names.append(all_trial_names)\n", 69 | "\n", 70 | " temp_onsets = list(onsets)\n", 71 | " onset = temp_onsets.pop(trial)\n", 72 | " all_trial_onsets = [[onset], temp_onsets] + temp_all_onsets\n", 73 | " lss_onsets.append(all_trial_onsets)\n", 74 | "\n", 75 | " temp_durations = list(durations)\n", 76 | " duration = temp_durations.pop(trial)\n", 77 | " all_trial_durations = [[duration], temp_durations] + temp_all_durations\n", 78 | " lss_durations.append(all_trial_durations)\n", 79 | " lss_vectors = [lss_names, lss_onsets, lss_durations]\n", 80 | " return lss_vectors" 81 | ] 82 | }, 83 | { 84 | "cell_type": "code", 85 | "execution_count": 4, 86 | "metadata": { 87 | "collapsed": true 88 | }, 89 | "outputs": [], 90 | "source": [ 91 | "spm_file = '/home/tsalo006/misc-fmri-code/beta-example/first_level/SPM.mat'" 92 | ] 93 | }, 94 | { 95 | "cell_type": "code", 96 | "execution_count": 5, 97 | "metadata": { 98 | "collapsed": true 99 | }, 100 | "outputs": [], 101 | "source": [ 102 | "LV = sio.loadmat(spm_file)" 103 | ] 104 | }, 105 | { 106 | "cell_type": "code", 107 | "execution_count": 6, 108 | "metadata": { 109 | "collapsed": false 110 | }, 111 | "outputs": [ 112 | { 113 | "name": "stdout", 114 | "output_type": "stream", 115 | "text": [ 116 | "Modeling 200 timepoints across 2 sessions.\n" 117 | ] 118 | } 119 | ], 120 | "source": [ 121 | "files = LV['SPM'][0]['xY'][0]['P'][0][0] # <- one of these may point to run\n", 122 | "sessions = LV['SPM'][0]['Sess'][0][0]\n", 123 | "print('Modeling {0} timepoints across {1} sessions.'.format(len(files), len(sessions)))" 124 | ] 125 | }, 126 | { 127 | "cell_type": "code", 128 | "execution_count": 7, 129 | "metadata": { 130 | "collapsed": false 131 | }, 132 | "outputs": [], 133 | "source": [ 134 | "# Find shared filepath across files\n", 135 | "idx = range(len(files[0]))\n", 136 | "f0 = np.array(list(files[0]), str)\n", 137 | "for f in files:\n", 138 | " fl = np.array(list(f), str)\n", 139 | " min_len = np.min((len(f0), len(fl)))\n", 140 | " temp_idx = np.where(f0[:min_len]==fl[:min_len])\n", 141 | " idx = np.intersect1d(idx, temp_idx)" 142 | ] 143 | }, 144 | { 145 | "cell_type": "code", 146 | "execution_count": 8, 147 | "metadata": { 148 | "collapsed": false, 149 | "scrolled": true 150 | }, 151 | "outputs": [ 152 | { 153 | "name": "stdout", 154 | "output_type": "stream", 155 | "text": [ 156 | "[['Impulses_0', u'Impulses'], ['Impulses_1', u'Impulses'], ['Impulses_2', u'Impulses'], ['Impulses_3', u'Impulses'], ['Impulses_4', u'Impulses'], ['Impulses_5', u'Impulses'], ['Impulses_6', u'Impulses'], ['Impulses_7', u'Impulses'], ['Impulses_8', u'Impulses'], ['Impulses_9', u'Impulses']]\n", 157 | "[['Impulses_0', u'Impulses'], ['Impulses_1', u'Impulses'], ['Impulses_2', u'Impulses'], ['Impulses_3', u'Impulses'], ['Impulses_4', u'Impulses'], ['Impulses_5', u'Impulses'], ['Impulses_6', u'Impulses'], ['Impulses_7', u'Impulses'], ['Impulses_8', u'Impulses'], ['Impulses_9', u'Impulses']]\n" 158 | ] 159 | } 160 | ], 161 | "source": [ 162 | "idx2 = get_consecutive(idx)\n", 163 | "shared_str = ''.join([f0[i] for i in idx2])\n", 164 | "old_path = dirname(shared_str)\n", 165 | "\n", 166 | "temp_dir = '/scratch/tsalo006'\n", 167 | "new_files = [f.replace(old_path, temp_dir) for f in files]\n", 168 | "new_data_dirs = list(set([dirname(f) for f in new_files]))\n", 169 | "# for data_dir in new_data_dirs:\n", 170 | "# if not isdir(data_dir):\n", 171 | "# mkdir(data_dir)\n", 172 | "# \n", 173 | "# for i, f in enumerate(files):\n", 174 | "# copyfile(f, new_files[i])\n", 175 | "\n", 176 | "for i_sess in range(len(sessions)):\n", 177 | " row_idx = sessions[i_sess]['row'][0] - 1 # don't forget python zero-indexes\n", 178 | " sess_files = [nf for j, nf in enumerate(new_files) if j in row_idx]\n", 179 | " covariates = sessions[i_sess]['C'][0]['C'][0]\n", 180 | " orig_names = []\n", 181 | " orig_onsets = []\n", 182 | " orig_durations = []\n", 183 | " for cond in sessions[i_sess]['U'][0]:\n", 184 | " orig_names.append(cond['name'][0][0][0])\n", 185 | " orig_onsets.append(np.squeeze(cond['ons']))\n", 186 | " orig_durations.append(np.squeeze(cond['dur']))\n", 187 | " params = [orig_names, orig_onsets, orig_durations]\n", 188 | " lss_vectors = make_lss_vectors(params)\n", 189 | " lss_names, lss_onsets, lss_durations = lss_vectors\n", 190 | " print lss_names" 191 | ] 192 | } 193 | ], 194 | "metadata": { 195 | "kernelspec": { 196 | "display_name": "Python 2", 197 | "language": "python", 198 | "name": "python2" 199 | }, 200 | "language_info": { 201 | "codemirror_mode": { 202 | "name": "ipython", 203 | "version": 2 204 | }, 205 | "file_extension": ".py", 206 | "mimetype": "text/x-python", 207 | "name": "python", 208 | "nbconvert_exporter": "python", 209 | "pygments_lexer": "ipython2", 210 | "version": "2.7.11" 211 | } 212 | }, 213 | "nbformat": 4, 214 | "nbformat_minor": 0 215 | } 216 | -------------------------------------------------------------------------------- /meanImageVal.m: -------------------------------------------------------------------------------- 1 | function meanVal = meanImageVal(valuesFile, maskFile) 2 | % FORMAT meanVal = meanImageVal(valuesFile, maskFile) 3 | % Gives you the mean across valuesFile (a nifti file), either masked by maskFile or not. 4 | 5 | if exist('maskFile', 'var') 6 | maskedVals = spm_summarise(valuesFile, maskFile); 7 | meanVal = mean(maskedVals); 8 | else 9 | disp('Warning. No mask given. Extracting mean from whole volume.') 10 | Vvals = spm_vol(valuesFile); 11 | imageVals = spm_read_vols(Vvals); 12 | imageVals(isnan(imageVals)) = 0; 13 | meanVal = mean(imageVals(:)); 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /moveRoisInImage.m: -------------------------------------------------------------------------------- 1 | % Copies data in nifti image from location of one ROI to location of another, identically shaped/sized ROI. 2 | % Take the movedRoi images that are created and use them in the second-level model. 3 | subjects = {'subj1' 'subj2'}; 4 | subjectMasks = {'subj1_mask.nii' 'subj2_mask.nii'}; 5 | subjConFile = 'con_0001.img'; 6 | generalMask = 'dlpfc.nii'; 7 | outPath = '/whereverTheDataShouldGo/'; 8 | 9 | subjPath = '/firstHalfOfPath/'; 10 | subjSubPath = '/secondHalfOfPath/'; 11 | 12 | outPath = '/home/tsalo/'; 13 | 14 | for iSubj = 1:length(subjects) 15 | % Load subject's data 16 | vSubjCon = spm_vol([subjPath subjects{iSubj} subjSubPath subjConFile]); 17 | [ySubjCon, ~] = spm_read_vols(vSubjCon); 18 | yOut = ySubjCon; 19 | 20 | % Mask where you want to see it in the brain. 21 | vMniRoi = spm_vol(generalMask); 22 | [yMniRoi, ~] = spm_read_vols(vMniRoi); 23 | mniRoiIndex = find(yMniRoi == 1); 24 | [mniX, mniY, mniZ] = ind2sub(size(yMniRoi), mniRoiIndex); 25 | 26 | % Mask for a given subject. 27 | vSubjRoi = spm_vol(subjectMasks{iSubj}); 28 | [ySubjRoi, ~] = spm_read_vols(vSubjRoi); 29 | subjRoiIndex = find(ySubjRoi == 1); 30 | 31 | % Create a barrier to make boundaries clear. 32 | minX = min(mniX) - 1; 33 | minY = min(mniY) - 1; 34 | minZ = min(mniZ) - 1; 35 | 36 | maxX = max(mniX) + 1; 37 | maxY = max(mniY) + 1; 38 | maxZ = max(mniZ) + 1; 39 | 40 | yOut(minX:maxX, minY:maxY, minZ:maxZ) = ones(size(yOut(minX:maxX, minY:maxY, minZ:maxZ))) .* 100; 41 | 42 | % Copy the values. 43 | yOut(mniRoiIndex) = ySubjCon(subjRoiIndex); 44 | 45 | % Write out new file. 46 | [~, fileName, fileSuffix] = fileparts(vSubjCon.fname); 47 | vSubjCon.fname = [outPath '/' subjects{iSubj} '_movedRoi_' fileName fileSuffix]; 48 | 49 | vOut = spm_create_vol(vSubjCon); 50 | spm_write_vol(vOut, yOut); 51 | 52 | clear vOut vSubjCon yOut ySubjCon subjRoiIndex ySubjRoi 53 | end 54 | -------------------------------------------------------------------------------- /organizeDicoms.m: -------------------------------------------------------------------------------- 1 | function organizeDicoms(dicomFolder, outFolder) 2 | % FORMAT organizeDicoms(dicomFolder, outFolder) 3 | % Copy and rename dicom files based on ProtocolName and SeriesNumber. 4 | % 5 | % 6 | % Inputs: 7 | % dicomFolder: Single folder where all of the dicoms are for a single 8 | % appointment. 9 | % outFolder: Folder where dicoms will be organized (in generated 10 | % subfolders). 11 | 12 | dicomFiles = dir([dicomFolder '/*.dcm']); 13 | 14 | disp('Organizing dicoms.'); 15 | for iFile = 1:length(dicomFiles) 16 | dicomHeader = dicominfo([dicomFolder '/' dicomFiles(iFile).name]); 17 | seriesNumber = num2str(dicomHeader.SeriesNumber); 18 | protocolName = strrep(dicomHeader.ProtocolName, ' ', '_'); 19 | 20 | outSubFolder = [seriesNumber '_' protocolName]; 21 | 22 | if ~exist([outFolder '/' outSubFolder '/'], 'dir') 23 | outFolders{counter} = [outFolder '/' outSubFolder '/']; 24 | mkdir(outFolders{counter}); 25 | counter = counter + 1; 26 | end 27 | 28 | system(['cp ' dicomFolder '/' dicomFiles(iFile).name ' ' outFolder '/' outSubFolder]); 29 | end 30 | 31 | disp('Renaming dicoms.'); 32 | for iFolder = 1:length(outFolders) 33 | seriesDicoms = dir([outFolders{iFolder} '/*.dcm']); 34 | for jFile = 1:length(seriesDicoms) 35 | system(['mv ' outFolders{iFolder} '/' seriesDicoms(jFile).name ' ' outFolders{iFolder} '/' sprintf('%04d', jFile)]); 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /split4dTo3d.m: -------------------------------------------------------------------------------- 1 | function files = split4dTo3d(fname) 2 | % FORMAT files = split4dTo3d(fname) 3 | % 4 | % Splits a 4D nifti file into a series of 3D nifti files. Needs SPM 5 | % functions to work. If input file is fdata.nii, the output files will have 6 | % filenames like fdata_001.nii, fdata_002.nii, etc. 7 | % Written by "baggy" 20111112. 8 | 9 | if (nargin < 1) 10 | [fname, sts] = spm_select; 11 | if (sts == 0) 12 | fprintf('split4dTo3d: Operation cancelled.\n'); 13 | return 14 | end 15 | end 16 | 17 | vol = spm_vol(fname); 18 | img = spm_read_vols(vol); 19 | sz = size(img); 20 | 21 | tvol = vol(1); 22 | tvol = rmfield(tvol, 'private'); 23 | tvol.descrip = 'generated by split4dTo3d.m'; 24 | 25 | [dn, fn, ext] = fileparts(fname); 26 | 27 | for ctr = 1:sz(4) 28 | tvol.fname = sprintf('%s%s%s_%.3d%s', dn, filesep, fn, ctr, ext); 29 | files{ctr} = tvol.fname; 30 | spm_write_vol(tvol, img(:, :, :, ctr)); 31 | end 32 | end 33 | 34 | -------------------------------------------------------------------------------- /univariate-tables/batchEffectSize.m: -------------------------------------------------------------------------------- 1 | % Example wrapper to call save_clusters_and_effect_size.m function. 2 | spmFile = '/nfs/ep2/AX/analysis/MTU/func_an_SPM8/150406_SOBP/EP32vsHC32_paired_t-test/CueB-CueA/SPM.mat'; 3 | 4 | maskFile = ''; 5 | 6 | pThr = {0.005}; 7 | correction = {'unc'}; 8 | k = 10; % minimum cluster size 9 | 10 | saveClustersAndEffectSize(spmFile, pThr, correction, k, maskFile); 11 | -------------------------------------------------------------------------------- /univariate-tables/batchGetBaAndRegion.m: -------------------------------------------------------------------------------- 1 | % batchGetBaAndRegion 2 | clusterImage = '/nfs/ep2/AX/analysis/MTU/func_an_SPM8/021113_HC30_SZ31/HCvsSZ/CueB-CueA_unequalvariance/0.001_unc_clusters/SZ-HC/Cluster_001.nii'; 3 | 4 | [outString, baString] = getBaAndRegion(clusterImage); 5 | -------------------------------------------------------------------------------- /univariate-tables/getBaAndRegion.m: -------------------------------------------------------------------------------- 1 | function [outString, baString] = getBaAndRegion(input) 2 | % FORMAT [outString, baString] = getBaAndRegion(clusterImage) 3 | % Takes cluster matrix, determines regions. 4 | spmDir = fileparts(which('spm')); 5 | spmDirs = strsplit(spmDir, '/'); 6 | spmDirs = spmDirs(1:end-1); 7 | spmDir = strjoin(spmDirs, '/'); 8 | spmDir = fullfile(spmDir, 'spm8'); 9 | pickatlasDir = fullfile(spmDir, 'toolbox/wfu_pickatlas'); 10 | 11 | typeNiiFile = fullfile(pickatlasDir, 'MNI_atlas_templates/TD_type.nii'); 12 | typeTextFile = fullfile(pickatlasDir, 'MNI_atlas_templates/TD_type.txt'); 13 | baNiiFile = fullfile(pickatlasDir, 'MNI_atlas_templates/TD_brodmann.nii'); 14 | baTextFile = fullfile(pickatlasDir, 'MNI_atlas_templates/TD_brodmann.txt'); 15 | labelNiiFile = fullfile(pickatlasDir, 'MNI_atlas_templates/atlas116.nii'); 16 | labelTextFile = fullfile(pickatlasDir, 'MNI_atlas_templates/atlas116.txt'); 17 | 18 | [typeHeader, typeValues] = loadNifti(typeNiiFile); 19 | tissueTypes = loadTableFile(typeTextFile); 20 | 21 | [~, baValues] = loadNifti(baNiiFile); 22 | baTypes = loadTableFile(baTextFile); 23 | 24 | [~, labelValues] = loadNifti(labelNiiFile); 25 | labelTypes = loadTableFile(labelTextFile); 26 | 27 | if isa(input, 'string') 28 | clusterImage = input; 29 | [xyz, maskMatrix] = roiFindIndex(clusterImage, 0); 30 | clusterXyz = adjustXyz(xyz, maskMatrix, typeHeader); 31 | clusterIndex = sub2ind(size(typeValues), clusterXyz{1}(1, :), clusterXyz{1}(2, :), clusterXyz{1}(3, :))'; 32 | 33 | clusterTypes = typeValues(clusterIndex); 34 | clusterBas = baValues(clusterIndex); 35 | clusterLabels = labelValues(clusterIndex); 36 | elseif isa(input, 'double') 37 | coordinates = input; 38 | [xMm, yMm, zMm] = vox2mm(coordinates, typeNiiFile); 39 | 40 | clusterTypes = typeValues(xMm, yMm, zMm); 41 | clusterBas = baValues(xMm, yMm, zMm); 42 | clusterLabels = labelValues(xMm, yMm, zMm); 43 | end 44 | 45 | % Tissue Types 46 | result = setdiff(unique(clusterTypes), 0); 47 | instances = histc(clusterTypes, result); 48 | [~, index] = sort(instances, 'descend'); 49 | sortedValues = result(index); 50 | typeCells = cell(size(sortedValues))'; 51 | for iType = 1:length(sortedValues) 52 | ttIndex = strcmp(tissueTypes(:, 1), num2str(sortedValues(iType))); 53 | tissueType = tissueTypes{ttIndex, 2}; 54 | typeCells{iType} = tissueType; 55 | end 56 | if ~isempty(typeCells) 57 | typeString = strjoin(typeCells, '| '); 58 | else 59 | typeString = 'Undefined'; 60 | end 61 | 62 | % Labels 63 | values = setdiff(unique(clusterLabels), 0); 64 | instances = histc(clusterLabels, values); 65 | [~, index] = sort(instances, 'descend'); 66 | sortedValues = values(index); 67 | labelCells = cell(size(sortedValues))'; 68 | for iLabel = 1:length(sortedValues) 69 | labelIndex = strcmp(labelTypes(:, 1), num2str(sortedValues(iLabel))); 70 | labelType = labelTypes{labelIndex, 2}; 71 | labelCells{iLabel} = labelType; 72 | end 73 | if ~isempty(labelCells) 74 | labelString = strjoin(labelCells, '| '); 75 | else 76 | labelString = 'Undefined'; 77 | end 78 | 79 | % BAs 80 | values = setdiff(unique(clusterBas), 0); 81 | instances = histc(clusterBas, values); 82 | [~, index] = sort(instances, 'descend'); 83 | sortedValues = values(index); 84 | baCells = cell(size(sortedValues))'; 85 | for iBa = 1:length(sortedValues) 86 | baIndex = strcmp(baTypes(:, 1), num2str(sortedValues(iBa))); 87 | baType = strrep(baTypes{baIndex, 2}, 'brodmann area ', ''); 88 | baCells{iBa} = baType; 89 | end 90 | if ~isempty(baCells) 91 | baString = strjoin(baCells, '| '); 92 | else 93 | baString = 'Undefined'; 94 | end 95 | 96 | if ~strfind(typeString, 'Gray') 97 | outString = typeString; 98 | else 99 | outString = labelString; 100 | end 101 | end 102 | 103 | function [xSub, ySub, zSub] = vox2mm(varargin) 104 | % FORMAT [xSub, ySub, zSub] = vox2mm(x, y, z, niiFile) 105 | % or 106 | % FORMAT [xSub, ySub, zSub] = vox2mm([x, y, z], niiFile) 107 | % Converts voxel coordinates to subscript values denoting position in data 108 | % matrix. 109 | % 110 | % Inputs: 111 | % x: X-coordinate. 112 | % y: Y-coordinate. 113 | % z: Z-coordinate. 114 | % niiFile: Nifti file from which alignment matrix will be used. 115 | % 116 | % Outputs: 117 | % xSub: X subscript. 118 | % ySub: Y subscript. 119 | % zSub: Z subscript. 120 | % 121 | % 150305 Created by Taylor Salo 122 | 123 | if nargin == 2 124 | loc = varargin{1}; 125 | niiFile = varargin{2}; 126 | elseif nargin == 4 127 | x = varargin{1}; 128 | y = varargin{2}; 129 | z = varargin{3}; 130 | loc = [x, y, z]; 131 | niiFile = varargin{4}; 132 | else 133 | error('You gave bad inputs.'); 134 | end 135 | 136 | v2m = spm_get_space(niiFile); 137 | mm = (loc - v2m(1:3, 4)') / v2m(1:3, 1:3); 138 | xSub = mm(1); 139 | ySub = mm(2); 140 | zSub = mm(3); 141 | end 142 | -------------------------------------------------------------------------------- /univariate-tables/loadTableFile.m: -------------------------------------------------------------------------------- 1 | function data3 = loadTableFile(textFile) 2 | % FORMAT data3 = loadTableFile(textFile) 3 | % Loads the Pickatlas text files. 4 | % Dependencies: readCsv.m, removeEmptyCells.m 5 | TextStruct = readCsv(textFile); 6 | data = TextStruct{1}.col; 7 | data2 = cellfun(@strsplit, data, repmat({'\t'}, size(data)), 'UniformOutput', 0); 8 | data3 = cell(length(data2), length(data2{1})); 9 | 10 | for iRow = 1:length(data2) 11 | data2{iRow} = removeEmptyCells(data2{iRow}); 12 | data3(iRow, :) = data2{iRow}; 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /univariate-tables/saveClustersAndEffectSize.m: -------------------------------------------------------------------------------- 1 | function saveClustersAndEffectSize(spmFile, pThr, correction, k, maskFile) 2 | % FORMAT saveClustersAndEffectSize(spmFile, pThr, correction, k, maskFile) 3 | % Loops through T contrasts in SPM second-level analysis, thresholding spmT 4 | % image based on pThr, correction, and k, and saving a mask of each cluster 5 | % in subdirectory. Also creates a Cohen's d image for each T contrast and 6 | % extracts mean Cohen's d for each significant cluster, summarizing in 7 | % outputted csv. 8 | % 9 | % 10 | % spmFile: Path to SPM.mat file (including SPM.mat). String. 11 | % pThr: p threshold. Cell array (1x1 or 1x2) of doubles. 12 | % correction: Correction applied to p threshold (FWE, FDR, or unc). 13 | % Cell array (same size as pThr) of strings. 14 | % k: Minimum acceptable cluster size. Double. 15 | % 16 | % 17 | % 141019-150320 Created by Taylor Salo 18 | 19 | %% Check inputs 20 | if exist(spmFile, 'file') 21 | [path, ~] = fileparts(spmFile); 22 | LoadedVariables = load(spmFile); 23 | SPM = LoadedVariables.SPM; 24 | if isempty(path) 25 | path = pwd; 26 | end 27 | else 28 | error('spmFile does not exist. Quitting.'); 29 | end 30 | 31 | [pThr, correction, k] = checkInputs(pThr, correction, k); 32 | 33 | %% Do everything else. 34 | origDir = pwd; 35 | cd(path); 36 | try 37 | fprintf('Evaluating second-level results of design: %s.\n', SPM.xsDes.Design); 38 | catch err 39 | fprintf('Design could not be determined.\n'); 40 | end 41 | if length(pThr) == 1 42 | fprintf(['Evaluating at p < ' num2str(pThr{1}) ' ' correction{1} ' and k > ' num2str(k) '.\n']); 43 | clusterExtentThresholdingDetected = false; 44 | else 45 | fprintf('Evaluating at voxel-level p < %g %s and k > %d and cluster-level p < %g %s.\n', pThr{1}, correction{1}, k, pThr{2}, correction{2}); 46 | clusterExtentThresholdingDetected = true; 47 | fprintf('Cluster extent thresholding detected: Only cluster-level statistics will be reported.\n'); 48 | switch correction{2} 49 | case 'unc' 50 | clusterLevelSigCol = 4; 51 | case 'FWE' 52 | clusterLevelSigCol = 1; 53 | case 'FDR' 54 | clusterLevelSigCol = 2; 55 | end 56 | end 57 | resmsFile = SPM.VResMS.fname; 58 | [~, ~, fileSuffix] = fileparts(resmsFile); 59 | rpvFile = fullfile(path, ['RPV' fileSuffix]); 60 | 61 | for iCon = 1:length(SPM.xCon) 62 | xSPM.STAT = SPM.xCon(iCon).STAT; 63 | xSPM.df = [SPM.xCon(iCon).eidf SPM.xX.erdf]; 64 | xSPM.k = k; 65 | xSPM.VRpv = spm_vol(rpvFile); 66 | xSPM.M = xSPM.VRpv.mat; 67 | xSPM.n = 1; 68 | xSPM.S = SPM.xVol.S; 69 | xSPM.Ic = iCon; 70 | xSPM.R = SPM.xVol.R; 71 | xSPM.FWHM = SPM.xVol.FWHM; 72 | xSPM.DIM = SPM.xVol.DIM; 73 | 74 | spmTFile = fullfile(path, SPM.xCon(iCon).Vspm.fname); 75 | conName = sprintf('Contrast_%03d-%s', iCon, strrep(SPM.xCon(iCon).name, ' ', '_')); 76 | fprintf('\tEvaluating contrast %d, %s\n', iCon, conName); 77 | 78 | if ~isempty(maskFile) 79 | [~, maskName, ~] = fileparts(maskFile); 80 | addMask = ['_' maskName]; 81 | else 82 | addMask = ''; 83 | end 84 | 85 | if clusterExtentThresholdingDetected 86 | outDir = fullfile(path, sprintf('v%g_%s_k%d_c%g_%s%s_clusters/%s/',... 87 | pThr{1}, correction{1}, k, pThr{2},... 88 | correction{2}, addMask, conName)); 89 | else 90 | outDir = fullfile(path, sprintf('v%g_%s_k%d%s_clusters/%s/', pThr{1},... 91 | correction{1}, k, addMask, conName)); 92 | end 93 | 94 | if ~exist(outDir, 'dir') 95 | mkdir(outDir); 96 | end 97 | 98 | % Create Cohen's D image. 99 | VspmT = spm_vol(spmTFile); 100 | [contrastTValues, ~] = spm_read_vols(VspmT); 101 | 102 | if ~isempty(maskFile) 103 | [xyz, maskMatrix] = maskFindIndex(maskFile, 0); 104 | roiXyz = adjustXyz(xyz, maskMatrix, VspmT); 105 | ind = sub2ind(size(contrastTValues), roiXyz{1}(1, :), roiXyz{1}(2, :), roiXyz{1}(3, :))'; 106 | maskedValues = zeros(size(contrastTValues)); 107 | maskedValues(ind) = contrastTValues(ind); 108 | contrastTValues = maskedValues; 109 | end 110 | 111 | if strcmp(xSPM.STAT, 'T') 112 | dValues = (2 .* contrastTValues) ./ sqrt(xSPM.df(2)); 113 | dFileHeader = VspmT; 114 | dFileHeader.fname = fullfile(outDir, ['D_' conName '.nii']); 115 | outDFileHeader = spm_create_vol(dFileHeader); 116 | spm_write_vol(outDFileHeader, dValues); 117 | end 118 | 119 | % Determine voxel size of T image. 120 | [sizeX, sizeY, sizeZ] = getVoxelSize(spmTFile); 121 | voxelScalar = sizeX * sizeY * sizeZ; 122 | xSPM.VOX = [sizeX sizeY sizeZ]; 123 | xSPM.units = {'mm' 'mm' 'mm'}; 124 | 125 | % Set up output csv. 126 | clear outStruct 127 | outStruct{1}.header{1} = 'Region of Activation'; outStruct{2}.header{1} = 'BA'; 128 | outStruct{3}.header{1} = 'L/R'; outStruct{4}.header{1} = 'k (mm3)'; 129 | outStruct{7}.header{1} = 'x'; outStruct{8}.header{1} = 'y'; outStruct{9}.header{1} = 'z'; 130 | 131 | if clusterExtentThresholdingDetected 132 | outStruct{5}.header{1} = 'Mean T'; 133 | outStruct{6}.header{1} = 'Mean D'; 134 | else 135 | outStruct{5}.header{1} = 'T'; 136 | outStruct{6}.header{1} = 'D'; 137 | end 138 | 139 | roaCol = 1; baCol = 2; lrCol = 3; kCol = 4; tCol = 5; dCol = 6; xCol = 7; yCol = 8; zCol = 9; 140 | outStruct{1}.col{1} = ''; outStruct{2}.col{1} = ''; outStruct{3}.col{1} = ''; 141 | outStruct{4}.col{1} = ''; outStruct{5}.col{1} = ''; outStruct{6}.col{1} = ''; 142 | outStruct{7}.col{1} = ''; outStruct{8}.col{1} = ''; outStruct{9}.col{1} = ''; 143 | 144 | % Create masks of all significant clusters and determine mean 145 | % Cohen's D of each cluster. 146 | switch correction{1} 147 | case 'unc' 148 | xSPM.u = spm_u(pThr{1}, xSPM.df, xSPM.STAT); 149 | case 'FWE' 150 | xSPM.u = spm_uc(pThr{1}, xSPM.df, xSPM.STAT, xSPM.R, 1, xSPM.S); 151 | case 'FDR' 152 | xSPM.u = spm_uc_FDR(pThr{1}, xSPM.df, xSPM.STAT, 1, VspmT, 0); 153 | otherwise 154 | error('Variable corr must be either ''unc'', ''FWE'', or ''FDR''.'); 155 | end 156 | 157 | clusterFileHeader = VspmT; 158 | allClustersFileHeader = VspmT; 159 | [allClustVals, nClusters] = spm_bwlabel(double(contrastTValues > xSPM.u), 18); 160 | [clustSize, clustNum] = sort(histc(allClustVals(:), 0:nClusters), 1, 'descend'); 161 | clustSize = clustSize(2:end); clustNum = clustNum(2:end) - 1; 162 | clustNum = clustNum(clustSize >= k); clustSize = clustSize(clustSize >= k); 163 | 164 | [x, y, z] = ind2sub(size(contrastTValues), find(contrastTValues > xSPM.u)); 165 | XYZ = [x'; y'; z']; 166 | Z = min(Inf, spm_get_data(SPM.xCon(iCon).Vspm, XYZ)); 167 | 168 | V2R = 1/prod(SPM.xVol.FWHM(SPM.xVol.DIM > 1)); 169 | 170 | if strcmp(xSPM.STAT, 'T') || ~isempty(strfind(which('spm'), 'spm12')) 171 | continueClusterExtentThresholding = true; 172 | [uc, xSPM.Pc, ue] = spm_uc_clusterFDR(0.05, xSPM.df, xSPM.STAT, xSPM.R, xSPM.n, Z, XYZ, V2R, xSPM.u); 173 | else 174 | continueClusterExtentThresholding = false; 175 | uc = NaN; ue = NaN; xSPM.Pc = ones(1, nClusters) * .05; 176 | end 177 | 178 | if clusterExtentThresholdingDetected && ~continueClusterExtentThresholding 179 | fprintf(['\tAttempting to use cluster-extent thresholding with F-contrast.\n'... 180 | '\t\tThis is possible in SPM12, but not the currently loaded version of SPM.\n']); 181 | continue 182 | end 183 | 184 | [up, xSPM.Pp] = spm_uc_peakFDR(0.05, xSPM.df, xSPM.STAT, xSPM.R, xSPM.n, Z, XYZ, xSPM.u); 185 | 186 | A = spm_clusters(XYZ); 187 | Q = []; 188 | for i = 1:max(A) 189 | j = find(A == i); 190 | if length(j) >= xSPM.k 191 | Q = [Q j]; 192 | end 193 | end 194 | 195 | xSPM.Z = Z(:, Q); 196 | xSPM.XYZ = XYZ(:, Q); 197 | xSPM.XYZmm = xSPM.XYZ; 198 | 199 | for jVox = 1:size(xSPM.XYZ, 2) 200 | xSPM.XYZmm(:, jVox) = (xSPM.XYZ(:, jVox).' * xSPM.M(1:3, 1:3) + xSPM.M(1:3, 4)').'; 201 | end 202 | uu = spm_uc(0.05, xSPM.df, xSPM.STAT, xSPM.R, xSPM.n, xSPM.S); 203 | 204 | xSPM.uc = [uu up ue uc]; 205 | 206 | table = spm_list_edited(xSPM, 5, 8); 207 | 208 | % Cluster row index. 209 | clustLevelCol = table(:, 3); 210 | clustIdx = find(~cellfun(@isempty, clustLevelCol)); 211 | 212 | % Pare down table to cluster-level significant clusters if cluster- 213 | % extent based thresholding is being applied. Else, keep as is. 214 | if clusterExtentThresholdingDetected 215 | for iClust = length(clustIdx):-1:1 216 | if iClust == length(clustIdx) 217 | if table{clustIdx(iClust), clusterLevelSigCol} > pThr{2} 218 | table(clustIdx(iClust):end, :) = []; 219 | end 220 | else 221 | if table{clustIdx(iClust), clusterLevelSigCol} > pThr{2} 222 | table(clustIdx(iClust):clustIdx(iClust + 1) - 1, :) = []; 223 | end 224 | end 225 | end 226 | end 227 | 228 | clustLevelCol = table(:, 3); 229 | clustIdx = find(~cellfun(@isempty, clustLevelCol)); 230 | clear x y z XYZ 231 | 232 | for jClust = 1:length(clustNum) 233 | oneClustVals = zeros(size(allClustVals)); 234 | oneClustVals(allClustVals == clustNum(jClust)) = 1; 235 | [x, y, z] = ind2sub(size(oneClustVals), find(oneClustVals == 1)); 236 | clusterCoordinates = [x'; y'; z']; 237 | clustNumber = 0; 238 | 239 | % Determine which cluster you're looking at and fill in that 240 | % position in the output table. Basically we're merging the 241 | % information about the clusters from the table with the 242 | % information found through more direct means, including effect 243 | % size. 244 | for kClust = 1:length(clustIdx) 245 | peakMM(1:3) = (table{clustIdx(kClust), 10}.' - VspmT.mat(1:3, 4)') / VspmT.mat(1:3, 1:3); 246 | if any(ismember(clusterCoordinates.', peakMM, 'rows')) 247 | clustNumber = kClust; 248 | clustPeakMM = peakMM; 249 | end 250 | end 251 | 252 | % Skip clusters not found in the table (because they're not 253 | % significant). Only skips clusters if cluster-extent 254 | % thresholding was used. 255 | if clustNumber ~= 0 256 | if ~exist('allClustMask', 'var') 257 | allClustMask = oneClustVals; 258 | else 259 | allClustMask = allClustMask + oneClustVals; 260 | end 261 | peakCoord = table{clustIdx(clustNumber), 10}.'; 262 | clusterFile = fullfile(outDir, sprintf('Cluster_%03d_%d_%d_%d.nii', clustNumber, peakCoord(1), peakCoord(2), peakCoord(3))); 263 | clusterFileHeader.fname = clusterFile; 264 | spm_write_vol(clusterFileHeader, oneClustVals); 265 | 266 | % Fill in output csv. 267 | if peakCoord(1) < 0 268 | outStruct{lrCol}.col{clustIdx(clustNumber), 1} = 'L'; 269 | elseif peakCoord(1) > 0 270 | outStruct{lrCol}.col{clustIdx(clustNumber), 1} = 'R'; 271 | else 272 | outStruct{lrCol}.col{clustIdx(clustNumber), 1} = 'I'; 273 | end 274 | 275 | outStruct{kCol}.col{clustIdx(clustNumber), 1} = clustSize(jClust) * voxelScalar; 276 | 277 | outStruct{xCol}.col{clustIdx(clustNumber), 1} = peakCoord(1); 278 | outStruct{yCol}.col{clustIdx(clustNumber), 1} = peakCoord(2); 279 | outStruct{zCol}.col{clustIdx(clustNumber), 1} = peakCoord(3); 280 | 281 | if clusterExtentThresholdingDetected 282 | if strcmp(xSPM.STAT, 'T') 283 | outStruct{dCol}.col{clustIdx(clustNumber), 1} = mean(dValues(find(oneClustVals == 1))); 284 | else 285 | outStruct{dCol}.col{clustIdx(clustNumber), 1} = ''; 286 | end 287 | [outString, baString] = getBaAndRegion(clusterFile); 288 | outStruct{roaCol}.col{clustIdx(clustNumber), 1} = outString; 289 | outStruct{baCol}.col{clustIdx(clustNumber), 1} = baString; 290 | outStruct{tCol}.col{clustIdx(clustNumber), 1} = mean(contrastTValues(find(oneClustVals == 1))); 291 | else 292 | if strcmp(xSPM.STAT, 'T') 293 | outStruct{dCol}.col{clustIdx(clustNumber), 1} = dValues(clustPeakMM(1), clustPeakMM(2), clustPeakMM(3)); 294 | else 295 | outStruct{dCol}.col{clustIdx(clustNumber), 1} = ''; 296 | end 297 | [outString, baString] = getBaAndRegion(peakCoord); 298 | outStruct{roaCol}.col{clustIdx(clustNumber), 1} = outString; 299 | outStruct{baCol}.col{clustIdx(clustNumber), 1} = baString; 300 | outStruct{tCol}.col{clustIdx(clustNumber), 1} = num2str(table{clustIdx(clustNumber), 7}); 301 | end 302 | 303 | if clustNumber == length(clustIdx) 304 | [nRows, ~] = size(table); 305 | else 306 | nRows = clustIdx(clustNumber + 1) - 1; 307 | end 308 | 309 | for mSubClust = clustIdx(clustNumber) + 1:nRows 310 | if table{mSubClust, 10}(1) < 0 311 | outStruct{lrCol}.col{mSubClust, 1} = 'L'; 312 | elseif table{mSubClust, 10}(1) > 0 313 | outStruct{lrCol}.col{mSubClust, 1} = 'R'; 314 | else 315 | outStruct{lrCol}.col{mSubClust, 1} = 'I'; 316 | end 317 | 318 | outStruct{kCol}.col{mSubClust, 1} = ''; 319 | subPeakMm = (table{mSubClust, 10}.' - VspmT.mat(1:3, 4)') / VspmT.mat(1:3, 1:3); 320 | 321 | if clusterExtentThresholdingDetected 322 | outStruct{dCol}.col{mSubClust, 1} = ''; 323 | outStruct{tCol}.col{mSubClust, 1} = ''; 324 | outStruct{roaCol}.col{mSubClust, 1} = ''; 325 | outStruct{baCol}.col{mSubClust, 1} = ''; 326 | else 327 | if strcmp(xSPM.STAT, 'T') 328 | outStruct{dCol}.col{mSubClust, 1} = dValues(subPeakMm(1), subPeakMm(2), subPeakMm(3)); 329 | else 330 | outStruct{dCol}.col{mSubClust, 1} = ''; 331 | end 332 | [outString, baString] = getBaAndRegion(table{mSubClust, 10}'); 333 | outStruct{roaCol}.col{mSubClust, 1} = ['\t' outString]; 334 | outStruct{baCol}.col{mSubClust, 1} = baString; 335 | outStruct{tCol}.col{mSubClust, 1} = num2str(table{mSubClust, 7}); 336 | end 337 | 338 | outStruct{xCol}.col{mSubClust, 1} = table{mSubClust, 10}(1); 339 | outStruct{yCol}.col{mSubClust, 1} = table{mSubClust, 10}(2); 340 | outStruct{zCol}.col{mSubClust, 1} = table{mSubClust, 10}(3); 341 | end 342 | end 343 | end 344 | if exist('allClustMask', 'var') 345 | allSignificantClusterValues = contrastTValues .* allClustMask; 346 | allClustersFileHeader.fname = fullfile(outDir, 'allClusterMask.nii'); 347 | spm_write_vol(allClustersFileHeader, allClustMask); 348 | 349 | allClustersFileHeader.fname = fullfile(outDir, 'allClusterVals.nii'); 350 | spm_write_vol(allClustersFileHeader, allSignificantClusterValues); 351 | clear allClustMask 352 | end 353 | 354 | fprintf('\t\t%d out of %d clusters are larger than %d voxels.\n', length(clustIdx), nClusters, k); 355 | writeCsv(outStruct, fullfile(outDir, sprintf('ClusterReport_%03d.csv', length(clustIdx)))); 356 | end 357 | cd(origDir); 358 | fprintf('Done.\n\n'); 359 | end 360 | 361 | %% Check Inputs 362 | function [pThr, correction, k] = checkInputs(pThr, correction, k) 363 | % FORMAT [pThr, correction, k] = checkInputs(pThr, correction, k) 364 | % By Taylor Salo. 365 | if iscell(pThr) 366 | [m, n] = size(pThr); 367 | if m * n > 2 368 | fprintf('pThr is %dx%d.\n\tSetting pThr to {0.01 0.05} and corr to {''unc'' ''FWE''}.\n', m, n); 369 | pThr = {0.01 0.05}; 370 | correction = {'unc' 'FWE'}; 371 | elseif m * n == 2 372 | for iP = 1:length(pThr) 373 | if ~isa(pThr{iP}, 'double') 374 | fprintf('pThr{%d} is not double.\n\tSetting pThr to {0.01 0.05} and corr to {''unc'' ''FWE''}.\n', iP); 375 | pThr = {0.01 0.05}; 376 | correction = {'unc' 'FWE'}; 377 | break 378 | end 379 | end 380 | else 381 | if ~isa(pThr{1}, 'double') 382 | fprintf('pThr{1} is not double.\n\tSetting pThr to {0.001} and corr to {''unc''}.\n'); 383 | pThr = {0.001}; 384 | correction = {'unc'}; 385 | end 386 | end 387 | else 388 | fprintf('pThr is not a 1x2 or 2x1 cell array of doubles.\n\tSetting pThr to {0.01 0.05} and corr to {''unc'' ''FWE''}.\n'); 389 | pThr = {0.01 0.05}; 390 | correction = {'unc' 'FWE'}; 391 | end 392 | if iscellstr(correction) 393 | if length(correction) ~= length(pThr) 394 | fprintf('pThr and corr are different lengths.\n'); 395 | if length(pThr) == 2 396 | fprintf('\tSetting corr to {''unc'' ''FWE''}.\n'); 397 | correction = {'unc' 'FWE'}; 398 | else 399 | fprintf('\tSetting corr to {''unc''}.\n'); 400 | correction = {'unc'}; 401 | end 402 | else 403 | if length(correction) == 1 404 | if ~cellstrfind(correction{1}, {'unc' 'FWE' 'FDR'}, 'exact') 405 | fprintf('corr must be composed of ''unc'', ''FWE'', or ''FDR''.\n\tSetting corr to {''unc''}.\n'); 406 | correction = {'unc'}; 407 | end 408 | else 409 | if ~cellstrfind(correction{1}, {'unc' 'FWE' 'FDR'}, 'exact') 410 | fprintf('corr must be composed of ''unc'', ''FWE'', or ''FDR''.\n\tSetting corr{1} to ''unc''.\n'); 411 | correction{1} = 'unc'; 412 | end 413 | if ~cellstrfind(correction{2}, {'unc' 'FWE' 'FDR'}, 'exact') 414 | fprintf('corr must be composed of ''unc'', ''FWE'', or ''FDR''.\n\tSetting corr{2} to ''FWE''.\n'); 415 | correction{2} = 'FWE'; 416 | end 417 | end 418 | end 419 | else 420 | fprintf('corr is not a cellstr.\n'); 421 | if length(pThr) == 2 422 | fprintf('\tSetting corr to {''unc'' ''FWE''}.\n'); 423 | correction = {'unc' 'FWE'}; 424 | else 425 | fprintf('\tSetting corr to {''unc''}.\n'); 426 | correction = {'unc'}; 427 | end 428 | end 429 | if ~isa(k, 'double') 430 | fprintf('Warning, your set k is not a double. Setting to default 5.\n'); 431 | k = 5; 432 | end 433 | end 434 | 435 | %% Determine Voxel Size in Nifti File 436 | function [sizeX, sizeY, sizeZ] = getVoxelSize(niiFile) 437 | % FORMAT [sizeX, sizeY, sizeZ] = getVoxelSize(niiFile) 438 | % Determines size of voxel in X, Y, and Z dimensions. This assumes that the 439 | % matrix is not diagonal, but works fine if it is. 440 | % By Taylor Salo. 441 | % 442 | % 443 | % niiFile: Nifti file from which voxel size will be determined. 444 | % String. 445 | % 446 | % sizeX (output): Size in mm of voxel in X dimension. Double. 447 | % sizeY (output): Size in mm of voxel in Y dimension. Double. 448 | % sizeZ (output): Size in mm of voxel in Z dimension. Double. 449 | V = spm_vol(niiFile); 450 | mniO = (V.mat * [1 1 1 1]')'; 451 | mniX = (V.mat * [2 1 1 1]')'; 452 | mniY = (V.mat * [1 2 1 1]')'; 453 | mniZ = (V.mat * [1 1 2 1]')'; 454 | sizeX = pdist([mniO; mniX], 'euclidean'); 455 | sizeY = pdist([mniO; mniY], 'euclidean'); 456 | sizeZ = pdist([mniO; mniZ], 'euclidean'); 457 | end 458 | 459 | %% Extract Coordinates of ROI 460 | function [index, mat] = maskFindIndex(roiLoc, thresh) 461 | % FORMAT [index, mat] = maskFindIndex(roiLoc, thresh) 462 | % Returns the XYZ address of voxels with values greater than threshold. 463 | % By Dennis Thompson. 464 | % 465 | % Inputs: 466 | % roiLoc: String pointing to nifti image. 467 | % thresh: Threshold value, defaults to zero. Double. 468 | % 469 | % Outputs: 470 | % index: Index of ROI XYZ coordinates. 471 | % mat: Mask affine transformation matrix. 472 | % 473 | % 474 | % 080101 Created by Dennis Thompson 475 | 476 | if ~exist('thresh','var'), 477 | thresh = 0; 478 | end 479 | 480 | data = nifti(roiLoc); 481 | Y = double(data.dat); 482 | Y(isnan(Y)) = 0; 483 | index = []; 484 | for iSheet = 1:size(Y, 3) 485 | % find values greater > thresh 486 | [xx, yy] = find(squeeze(Y(:, :, iSheet)) > thresh); 487 | if ~isempty(xx) 488 | zz = ones(size(xx)) * iSheet; 489 | index = [index [xx'; yy'; zz']]; 490 | end 491 | end 492 | 493 | mat = data.mat; 494 | end 495 | 496 | %% Adjust Coordinates of ROI 497 | function funcXYZ = adjustXyz(XYZ, ROImat, V) 498 | % FORMAT funcXYZ = adjustXyz(XYZ, ROImat, V) 499 | % By Dennis Thompson. 500 | % 501 | % 502 | % XYZ: XYZ coordinates of ones in binary matrix. 503 | % ROImat: Transformation matrix from ROI file. 504 | % V: Header information of nifti file from spm_vol. 505 | XYZ(4, :) = 1; 506 | funcXYZ = cell(length(V)); 507 | for n = 1:length(V) 508 | if iscell(V) 509 | tmp = inv(V{n}.mat) * (ROImat * XYZ); 510 | else 511 | tmp = inv(V(n).mat) * (ROImat * XYZ); 512 | end 513 | funcXYZ{n} = tmp(1:3, :); 514 | end 515 | end 516 | -------------------------------------------------------------------------------- /univariate-tables/spm_list_edited.m: -------------------------------------------------------------------------------- 1 | function outTable = spm_list_edited(xSPM, Num, Dis) 2 | % FORMAT outTable = spm_list_edited(xSPM, Num, Dis) 3 | % Summary list of local maxima for entire volume of interest 4 | % 5 | % Inputs: 6 | % xSPM - structure containing SPM, distribution & filtering details 7 | % - required fields are: 8 | % .Z - minimum of n Statistics {filtered on u and k} 9 | % .n - number of conjoint tests CAN DO IGNORE CONJUNCTIONS AND SET TO 1 10 | % .STAT - distribution {Z, T, X or F} CAN DO 11 | % .df - degrees of freedom [df{interest}, df{residual}] CAN DO 12 | % .u - height threshold {Z} CAN DO 13 | % .k - extent threshold {voxels} CAN DO (preset) 14 | % .XYZ - location of voxels {voxel coords} CAN DO (same as ROIs) 15 | % .XYZmm - location of voxels {mm coords} CAN DO (convert XYZ to mm using matrix) 16 | % .S - search Volume {voxels} 17 | % .R - search Volume {resels} 18 | % .FWHM - smoothness {voxels} 19 | % .M - voxels - > mm matrix CAN DO 20 | % .VOX - voxel dimensions {mm} CAN DO 21 | % .DIM - image dimensions {voxels} CAN DO 22 | % .units - space units CAN DO SET TO {'mm' 'mm' 'mm'} 23 | % .VRpv - filehandle - Resels per voxel CAN DO (load RPV.img header) 24 | % .Ps - uncorrected P values in searched volume (for voxel FDR) 25 | % .Pp - uncorrected P values of peaks (for peak FDR) 26 | % .Pc - uncorrected P values of cluster extents (for cluster FDR) 27 | % .uc - 0.05 critical thresholds for FWEp, FDRp, FWEc, FDRc MAY NOT NEED 28 | % .thresDesc - description of height threshold (string) DO NOT NEED 29 | % 30 | % Num - number of maxima per cluster [3] 31 | % Dis - distance among clusters {mm} [8] 32 | % 33 | % Output: 34 | % outTable - Nx9 cell array (same as whole brain results from the GUI 35 | % without the set level stats). 36 | %__________________________________________________________________________ 37 | % 38 | % spm_list_edited characterizes SPMs (thresholded at u and k) in terms of 39 | % excursion sets (a collection of face, edge and vertex connected subsets 40 | % or clusters). The corrected significance of the results are based on 41 | % set, cluster and voxel-level inferences using distributional 42 | % approximations from the Theory of Gaussian Fields. These distributions 43 | % assume that the SPM is a reasonable lattice approximation of a 44 | % continuous random field with known component field smoothness. 45 | % 46 | % The p values are based on the probability of obtaining c, or more, 47 | % clusters of k, or more, resels above u, in the volume S analysed = 48 | % P(u,k,c). For specified thresholds u, k, the set-level inference is 49 | % based on the observed number of clusters C, = P(u,k,C). For each 50 | % cluster of size K the cluster-level inference is based on P(u,K,1) 51 | % and for each voxel (or selected maxima) of height U, in that cluster, 52 | % the voxel-level inference is based on P(U,0,1). All three levels of 53 | % inference are supported with a tabular presentation of the p values 54 | % and the underlying statistic: 55 | % 56 | % Set-level - c = number of suprathreshold clusters 57 | % - P = prob(c or more clusters in the search volume) 58 | % 59 | % Cluster-level - k = number of voxels in this cluster 60 | % - Pc = prob(k or more voxels in the search volume) 61 | % - Pu = prob(k or more voxels in a cluster) 62 | % - Qc = lowest FDR bound for which this cluster would be 63 | % declared positive 64 | % 65 | % Peak-level - T/F = Statistic upon which the SPM is based 66 | % - Ze = The equivalent Z score - prob(Z > Ze) = prob(t > T) 67 | % - Pc = prob(Ze or higher in the search volume) 68 | % - Qp = lowest FDR bound for which this peak would be 69 | % declared positive 70 | % - Pu = prob(Ze or higher at that voxel) 71 | % 72 | % Voxel-level - Qu = Expd(Prop of false positives among voxels >= Ze) 73 | % 74 | % x,y,z (mm) - Coordinates of the voxel 75 | % 76 | % The table is grouped by regions and sorted on the Ze-variate of the 77 | % primary maxima. Ze-variates (based on the uncorrected p value) are the 78 | % Z score equivalent of the statistic. Volumes are expressed in voxels. 79 | % 80 | % Clicking on values in the table returns the value to the MATLAB 81 | % workspace. In addition, clicking on the co-ordinates jumps the 82 | % results section cursor to that location. The table has a context menu 83 | % (obtained by right-clicking in the background of the table), 84 | % providing options to print the current table as a text table, or to 85 | % extract the table data to the MATLAB workspace. 86 | % 87 | %__________________________________________________________________________ 88 | % Copyright (C) 1999-2011 Wellcome Trust Centre for Neuroimaging 89 | 90 | % Karl Friston, Andrew Holmes, Guillaume Flandin 91 | % $Id: spm_list.m 4617 2012-01-11 15:46:16Z will $ 92 | % Edited and reduced by Taylor Salo 140912 for compatibility with 93 | % save_clusters_and_effect_size. 94 | 95 | %-Get number of maxima per cluster to be reported 96 | %---------------------------------------------------------------------- 97 | if ~exist('Num', 'var') 98 | Num = spm_get_defaults('stats.results.volume.nbmax'); % 3 99 | end 100 | 101 | %-Get minimum distance among clusters (mm) to be reported 102 | %---------------------------------------------------------------------- 103 | if ~exist('Dis', 'var') 104 | Dis = spm_get_defaults('stats.results.volume.nbmax'); % 8 105 | end 106 | 107 | %-Get header string 108 | %---------------------------------------------------------------------- 109 | if xSPM.STAT ~= 'P' 110 | Title = 'p-values adjusted for search volume'; 111 | else 112 | Title = 'Posterior Probabilities'; 113 | end 114 | 115 | %-Extract data from xSPM 116 | %---------------------------------------------------------------------- 117 | S = xSPM.S; 118 | VOX = xSPM.VOX; 119 | DIM = xSPM.DIM; 120 | M = xSPM.M; 121 | XYZ = xSPM.XYZ; 122 | Z = xSPM.Z; 123 | VRpv = xSPM.VRpv; 124 | n = xSPM.n; 125 | STAT = xSPM.STAT; 126 | df = xSPM.df; 127 | u = xSPM.u; 128 | k = xSPM.k; 129 | try, uc = xSPM.uc; end 130 | try, QPs = xSPM.Ps; end 131 | try, QPp = xSPM.Pp; end 132 | try, QPc = xSPM.Pc; end 133 | 134 | if STAT~='P' 135 | R = full(xSPM.R); 136 | FWHM = full(xSPM.FWHM); 137 | end 138 | try 139 | units = xSPM.units; 140 | catch 141 | units = {'mm' 'mm' 'mm'}; 142 | end 143 | units{1} = [units{1} ' ']; 144 | units{2} = [units{2} ' ']; 145 | 146 | DIM = DIM > 1; % non-empty dimensions 147 | D = sum(DIM); % highest dimension 148 | VOX = VOX(DIM); % scaling 149 | 150 | if STAT ~= 'P' 151 | FWHM = FWHM(DIM); % Full width at half max 152 | FWmm = FWHM.*VOX; % FWHM {units} 153 | V2R = 1/prod(FWHM); % voxels to resels 154 | k = k*V2R; % extent threshold in resels 155 | R = R(1:(D + 1)); % eliminate null resel counts 156 | try, QPs = sort(QPs(:)); end % Needed for voxel FDR 157 | try, QPp = sort(QPp(:)); end % Needed for peak FDR 158 | try, QPc = sort(QPc(:)); end % Needed for cluster FDR 159 | end 160 | 161 | % Choose between voxel-wise and topological FDR 162 | %---------------------------------------------------------------------- 163 | topoFDR = spm_get_defaults('stats.topoFDR'); 164 | 165 | %-Tolerance for p-value underflow, when computing equivalent Z's 166 | %---------------------------------------------------------------------- 167 | tol = eps*10; 168 | 169 | %-Table Headers 170 | %---------------------------------------------------------------------- 171 | TabDat.tit = Title; 172 | 173 | TabDat.hdr = {... 174 | 'set', 'p', '\itp';... 175 | 'set', 'c', '\itc';... 176 | 'cluster', 'p(FWE-corr)', '\itp\rm_{FWE-corr}';... 177 | 'cluster', 'p(FDR-corr)', '\itq\rm_{FDR-corr}';... 178 | 'cluster', 'equivk', '\itk\rm_E';... 179 | 'cluster', 'p(unc)', '\itp\rm_{uncorr}';... 180 | 'peak', 'p(FWE-corr)', '\itp\rm_{FWE-corr}';... 181 | 'peak', 'p(FDR-corr)', '\itq\rm_{FDR-corr}';... 182 | 'peak', STAT, sprintf('\\it%c',STAT);... 183 | 'peak', 'equivZ', '(\itZ\rm_\equiv)';... 184 | 'peak', 'p(unc)', '\itp\rm_{uncorr}';... 185 | '', 'x,y,z {mm}', [units{:}]}';... 186 | 187 | %-Coordinate Precisions 188 | %---------------------------------------------------------------------- 189 | if isempty(xSPM.XYZmm) % empty results 190 | xyzfmt = '%3.0f %3.0f %3.0f'; 191 | voxfmt = repmat('%0.1f ',1,numel(FWmm)); 192 | elseif ~any(strcmp(units{3},{'mm',''})) % 2D data 193 | xyzfmt = '%3.0f %3.0f %3.0f'; 194 | voxfmt = repmat('%0.1f ',1,numel(FWmm)); 195 | else % 3D data, work out best precision based on voxel sizes and FOV 196 | xyzsgn = min(xSPM.XYZmm(DIM,:),[],2) < 0; 197 | xyzexp = max(floor(log10(max(abs(xSPM.XYZmm(DIM,:)),[],2)))+(max(abs(xSPM.XYZmm(DIM,:)),[],2) >= 1),0); 198 | voxexp = floor(log10(abs(VOX')))+(abs(VOX') >= 1); 199 | xyzdec = max(-voxexp,0); 200 | voxdec = max(-voxexp,1); 201 | xyzwdt = xyzsgn+xyzexp+(xyzdec>0)+xyzdec; 202 | voxwdt = max(voxexp,0)+(voxdec>0)+voxdec; 203 | tmpfmt = cell(size(xyzwdt)); 204 | for i = 1:numel(xyzwdt) 205 | tmpfmt{i} = sprintf('%%%d.%df ', xyzwdt(i), xyzdec(i)); 206 | end 207 | xyzfmt = [tmpfmt{:}]; 208 | tmpfmt = cell(size(voxwdt)); 209 | for i = 1:numel(voxwdt) 210 | tmpfmt{i} = sprintf('%%%d.%df ', voxwdt(i), voxdec(i)); 211 | end 212 | voxfmt = [tmpfmt{:}]; 213 | end 214 | TabDat.fmt = { '%-0.3f','%g',... %-Set 215 | '%0.3f', '%0.3f','%0.0f', '%0.3f',... %-Cluster 216 | '%0.3f', '%0.3f', '%6.2f', '%5.2f', '%0.3f',... %-Peak 217 | xyzfmt}; %-XYZ 218 | 219 | %-Table filtering note 220 | %---------------------------------------------------------------------- 221 | if isinf(Num) 222 | TabDat.str = sprintf('table shows all local maxima > %.1fmm apart',Dis); 223 | else 224 | TabDat.str = sprintf(['table shows %d local maxima ',... 225 | 'more than %.1fmm apart'],Num,Dis); 226 | end 227 | 228 | %-Footnote with SPM parameters 229 | %---------------------------------------------------------------------- 230 | if STAT ~= 'P' 231 | Pz = spm_P(1,0,u,df,STAT,1,n,S); 232 | Pu = spm_P(1,0,u,df,STAT,R,n,S); 233 | [P Pn Ec Ek] = spm_P(1,k,u,df,STAT,R,n,S); 234 | 235 | TabDat.ftr = cell(9,2); 236 | TabDat.ftr{1,1} = ... 237 | ['Height threshold: ' STAT ' = %0.2f, p = %0.3f (%0.3f)']; 238 | TabDat.ftr{1,2} = [u,Pz,Pu]; 239 | TabDat.ftr{2,1} = ... 240 | 'Extent threshold: k = %0.0f voxels, p = %0.3f (%0.3f)'; 241 | TabDat.ftr{2,2} = [k/V2R,Pn,P]; 242 | TabDat.ftr{3,1} = ... 243 | 'Expected voxels per cluster, = %0.3f'; 244 | TabDat.ftr{3,2} = Ek/V2R; 245 | TabDat.ftr{4,1} = ... 246 | 'Expected number of clusters, = %0.2f'; 247 | TabDat.ftr{4,2} = Ec*Pn; 248 | if any(isnan(uc)) 249 | TabDat.ftr{5,1} = 'FWEp: %0.3f, FDRp: %0.3f'; 250 | TabDat.ftr{5,2} = uc(1:2); 251 | else 252 | TabDat.ftr{5,1} = ... 253 | 'FWEp: %0.3f, FDRp: %0.3f, FWEc: %0.0f, FDRc: %0.0f'; 254 | TabDat.ftr{5,2} = uc; 255 | end 256 | TabDat.ftr{6,1} = 'Degrees of freedom = [%0.1f, %0.1f]'; 257 | TabDat.ftr{6,2} = df; 258 | TabDat.ftr{7,1} = ... 259 | ['FWHM = ' voxfmt units{:} '; ' voxfmt '{voxels}']; 260 | TabDat.ftr{7,2} = [FWmm FWHM]; 261 | TabDat.ftr{8,1} = ... 262 | 'Volume: %0.0f = %0.0f voxels = %0.1f resels'; 263 | TabDat.ftr{8,2} = [S*prod(VOX),S,R(end)]; 264 | TabDat.ftr{9,1} = ... 265 | ['Voxel size: ' voxfmt units{:} '; (resel = %0.2f voxels)']; 266 | TabDat.ftr{9,2} = [VOX,prod(FWHM)]; 267 | else 268 | TabDat.ftr = {}; 269 | end 270 | 271 | %-Characterize excursion set in terms of maxima 272 | % (sorted on Z values and grouped by regions) 273 | %---------------------------------------------------------------------- 274 | if isempty(Z) 275 | TabDat.dat = cell(0,12); 276 | outTable = TabDat.dat(:, 3:end); 277 | return 278 | end 279 | 280 | %-Workaround in spm_max for conjunctions with negative thresholds 281 | %---------------------------------------------------------------------- 282 | minz = abs(min(min(Z))); 283 | Z = 1 + minz + Z; 284 | [N Z XYZ A L] = spm_max(Z,XYZ); 285 | Z = Z - minz - 1; 286 | 287 | %-Convert cluster sizes from voxels (N) to resels (K) 288 | %---------------------------------------------------------------------- 289 | c = max(A); %-Number of clusters 290 | NONSTAT = spm_get_defaults('stats.rft.nonstat'); 291 | 292 | if STAT ~= 'P' 293 | if NONSTAT 294 | K = zeros(c,1); 295 | for i = 1:c 296 | 297 | %-Get LKC for voxels in i-th region 298 | %---------------------------------------------------------- 299 | LKC = spm_get_data(VRpv,L{i}); 300 | 301 | %-Compute average of valid LKC measures for i-th region 302 | %---------------------------------------------------------- 303 | valid = ~isnan(LKC); 304 | if any(valid) 305 | LKC = sum(LKC(valid)) / sum(valid); 306 | else 307 | LKC = V2R; % fall back to whole-brain resel density 308 | end 309 | 310 | %-Intrinsic volume (with surface correction) 311 | %---------------------------------------------------------- 312 | IV = spm_resels([1 1 1],L{i},'V'); 313 | IV = IV*[1/2 2/3 2/3 1]'; 314 | K(i) = IV*LKC; 315 | 316 | end 317 | K = K(A); 318 | else 319 | K = N*V2R; 320 | end 321 | end 322 | 323 | %-Convert maxima locations from voxels to mm 324 | %---------------------------------------------------------------------- 325 | XYZmm = M(1:3,:)*[XYZ; ones(1,size(XYZ,2))]; 326 | 327 | %-Set-level p values {c} - do not display if reporting a single cluster 328 | %---------------------------------------------------------------------- 329 | if STAT ~= 'P' 330 | Pc = spm_P(c,k,u,df,STAT,R,n,S); %-Set-level p-value 331 | else 332 | Pc = []; 333 | end 334 | 335 | TabDat.dat = {Pc,c}; 336 | TabLin = 1; 337 | 338 | %-Cluster and local maxima p-values & statistics 339 | %---------------------------------------------------------------------- 340 | while numel(find(isfinite(Z))) 341 | 342 | %-Find largest remaining local maximum 343 | %------------------------------------------------------------------ 344 | [U,i] = max(Z); %-largest maxima 345 | j = find(A == A(i)); %-maxima in cluster 346 | 347 | 348 | %-Compute cluster {k} and peak-level {u} p values for this cluster 349 | %------------------------------------------------------------------ 350 | if STAT ~= 'P' 351 | 352 | % p-values (FWE) 353 | %-------------------------------------------------------------- 354 | Pz = spm_P(1,0, U,df,STAT,1,n,S); % uncorrected p value 355 | Pu = spm_P(1,0, U,df,STAT,R,n,S); % FWE-corrected {based on Z} 356 | [Pk Pn] = spm_P(1,K(i),u,df,STAT,R,n,S); % [un]corrected {based on K} 357 | 358 | % q-values (FDR) 359 | %-------------------------------------------------------------- 360 | if topoFDR 361 | Qc = spm_P_clusterFDR(K(i),df,STAT,R,n,u,QPc); % based on K 362 | Qp = spm_P_peakFDR(U,df,STAT,R,n,u,QPp); % based on Z 363 | Qu = []; 364 | else 365 | Qu = spm_P_FDR(U,df,STAT,n,QPs); % voxel FDR-corrected 366 | Qc = []; 367 | Qp = []; 368 | end 369 | 370 | % Equivalent Z-variate 371 | %-------------------------------------------------------------- 372 | if Pz < tol 373 | Ze = Inf; 374 | else 375 | Ze = spm_invNcdf(1 - Pz); 376 | end 377 | else 378 | Pz = []; 379 | Pu = []; 380 | Qu = []; 381 | Pk = []; 382 | Pn = []; 383 | Qc = []; 384 | Qp = []; 385 | ws = warning('off','SPM:outOfRangeNormal'); 386 | Ze = spm_invNcdf(U); 387 | warning(ws); 388 | end 389 | 390 | if topoFDR 391 | [TabDat.dat{TabLin,3:12}] = deal(Pk,Qc,N(i),Pn,Pu,Qp,U,Ze,Pz,XYZmm(:,i)); 392 | else 393 | [TabDat.dat{TabLin,3:12}] = deal(Pk,Qc,N(i),Pn,Pu,Qu,U,Ze,Pz,XYZmm(:,i)); 394 | end 395 | TabLin = TabLin + 1; 396 | 397 | %-Print Num secondary maxima (> Dis mm apart) 398 | %------------------------------------------------------------------ 399 | [~,q] = sort(-Z(j)); % sort on Z value 400 | D = i; 401 | for i = 1:length(q) 402 | d = j(q(i)); 403 | if min(sqrt(sum((XYZmm(:,D)-repmat(XYZmm(:,d),1,size(D,2))).^2)))>Dis 404 | if length(D) < Num 405 | % voxel-level p values {Z} 406 | %------------------------------------------------------ 407 | if STAT ~= 'P' 408 | Pz = spm_P(1,0,Z(d),df,STAT,1,n,S); 409 | Pu = spm_P(1,0,Z(d),df,STAT,R,n,S); 410 | if topoFDR 411 | Qp = spm_P_peakFDR(Z(d),df,STAT,R,n,u,QPp); 412 | Qu = []; 413 | else 414 | Qu = spm_P_FDR(Z(d),df,STAT,n,QPs); 415 | Qp = []; 416 | end 417 | if Pz < tol 418 | Ze = Inf; 419 | else 420 | Ze = spm_invNcdf(1 - Pz); 421 | end 422 | else 423 | Pz = []; 424 | Pu = []; 425 | Qu = []; 426 | Qp = []; 427 | ws = warning('off','SPM:outOfRangeNormal'); 428 | Ze = spm_invNcdf(Z(d)); 429 | warning(ws); 430 | end 431 | D = [D d]; 432 | if topoFDR 433 | [TabDat.dat{TabLin,7:12}] = ... 434 | deal(Pu,Qp,Z(d),Ze,Pz,XYZmm(:,d)); 435 | else 436 | [TabDat.dat{TabLin,7:12}] = ... 437 | deal(Pu,Qu,Z(d),Ze,Pz,XYZmm(:,d)); 438 | end 439 | TabLin = TabLin+1; 440 | end 441 | end 442 | end 443 | Z(j) = NaN; % Set local maxima to NaN 444 | end 445 | outTable = TabDat.dat(:, 3:end); 446 | end 447 | -------------------------------------------------------------------------------- /visualization/batchQuickViewGif.m: -------------------------------------------------------------------------------- 1 | % Make images! 2 | inputFile = '/home/tsalo/fmri-images-rise/tcan_final.nii'; 3 | outFolder = '/home/tsalo/fmri-images-rise/'; 4 | view = 'sag'; 5 | 6 | switch view 7 | case 'ax' 8 | % zMin = -50 9 | % zMax = 84 10 | zCoords = [-50:2:84 82:-2:-48]'; 11 | yCoords = zeros(size(zCoords)); 12 | xCoords = zeros(size(zCoords)); 13 | coordsVary = zCoords; 14 | case 'cor' 15 | % yMin = -112 16 | % yMax = 76 17 | yCoords = [-112:2:76 74:-2:-110]'; 18 | xCoords = zeros(size(yCoords)); 19 | zCoords = zeros(size(yCoords)); 20 | coordsVary = yCoords; 21 | case 'sag' 22 | % xMin = -78 23 | % xMax = 78 24 | xCoords = [-78:2:78 76:-2:-76]'; 25 | yCoords = zeros(size(xCoords)); 26 | zCoords = zeros(size(xCoords)); 27 | coordsVary = xCoords; 28 | end 29 | 30 | coordinates = [xCoords yCoords zCoords]; 31 | 32 | for iSlice = 1:length(coordsVary) 33 | saveFile = [outFolder sprintf('%03d', iSlice) '_' num2str(coordsVary(iSlice))]; 34 | quickViewGif(inputFile, saveFile, coordinates(iSlice, :), view) 35 | end -------------------------------------------------------------------------------- /visualization/quickViewGif.m: -------------------------------------------------------------------------------- 1 | function quickViewGif(inputFile, saveFile, coordinates, view) 2 | % FORMAT quickViewGif(inputFile, saveFile, coordinates, view) 3 | % Saves xjview screenshots cropped to fit specific views. 4 | % 5 | % inputFile: Nifti file (con, spmT, etc) for which image is required. 6 | % saveFile: output path and filename (without extension) where image 7 | % will be saved. 8 | % coordinates: Coordinates. 1x3 double. 9 | % view: View. String. Options: 'sag' (sagittal), 'ax' (axial), 10 | % 'cor' (coronal). 11 | % 12 | % 13 | % 141222 Created by Taylor Salo 14 | 15 | p = 0.001; % P value. 16 | k = 100; % Minimum cluster size. 17 | 18 | % Save image 19 | if exist(inputFile,'file') 20 | fprintf(['\tFile Found: ' inputFile ': \n']); 21 | else 22 | fprintf(['\tFile: ' inputFile ' DNE\n']); 23 | return 24 | end 25 | 26 | xjviewModified(inputFile, p, k, coordinates); 27 | fg = spm_figure('FindWin','Graphics'); 28 | % Where to save screenshots 29 | set(gcf, 'PaperUnits', 'inches', 'PaperPosition', [.5 2.5 8.5 11.0]); 30 | print(fg, '-noui', '-djpeg', saveFile); 31 | gf = imread([saveFile '.jpg']); 32 | switch view 33 | case 'sag' 34 | gf_crop = imcrop(gf,[637, 72, 306, 259]); 35 | imwrite(gf_crop,[saveFile '.jpg']); 36 | case 'ax' 37 | gf_crop = imcrop(gf,[637, 341, 306, 259]); 38 | imwrite(gf_crop,[saveFile '.jpg']); 39 | case 'cor' 40 | gf_crop = imcrop(gf,[955, 72, 255, 259]); 41 | imwrite(gf_crop,[saveFile '.jpg']); 42 | end 43 | fprintf(['Saved ' saveFile '.jpg\n']); 44 | close gcf 45 | end 46 | -------------------------------------------------------------------------------- /writeSummaryImage.m: -------------------------------------------------------------------------------- 1 | function writeSummaryImage(cellVols, outName, expr) 2 | % FORMAT writeSummaryImage(cellVols, outName, expr) 3 | % Calls spm_imcalc to perform calculations (e.g. sum, mean, std) on a cell 4 | % array of nifti images to output one summary image. 5 | % 6 | % cellVols: Cell array of images upon which calculations will be performed. 7 | % outName: The name (with full path) of the image to be written. String. 8 | % expr: The calculation to be performed upon the images in cellVols. 9 | % String. Possible values: sum(X), mean(X), std(X), maybe others. 10 | 11 | for iVol = 1:length(cellVols) 12 | Vi(iVol) = spm_vol(cellVols{iVol}); 13 | end 14 | Vo = Vi(1); 15 | Vo.fname = outName; 16 | 17 | spm_imcalc(Vi, Vo, expr, {1, 0, 0}); 18 | end 19 | --------------------------------------------------------------------------------