Permalink
Cannot retrieve contributors at this time
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
hMRI-toolbox-public/hmri_create_MTProt.m
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
1659 lines (1481 sloc)
71.4 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function [fR1, fR2s, fMT, fA, PPDw, PT1w, PMTw] = hmri_create_MTProt(jobsubj) %#ok<*STOUT> | |
%========================================================================== | |
% This is hmri_create_MTProt, part of the hMRI-Toolbox. | |
% | |
% PURPOSE | |
% Estimation of quantitative parameters (R1, R2*, PD, MT) from | |
% multi-contrast multi-echo FLASH protocol | |
% | |
% FORMAT | |
% [fR1, fR2s, fMT, fA, PPDw, PT1w, PMTw] = hmri_create_MTProt(jobsubj) | |
% | |
% INPUTS | |
% jobsubj parameters for one subject out of the job list. | |
% NB: ONE SINGLE DATA SET FROM ONE SINGLE SUBJECT IS | |
% PROCESSED HERE, LOOP OVER SUBJECTS DONE AT HIGHER LEVEL. | |
% P_trans filenames of a pair of magnitude image + transmit bias map | |
% [p.u.] of B1+ field (not mandatory), saved as | |
% "b1_trans_input" field in jobsubj. | |
% | |
% OUTPUTS | |
% fR1 R1 map output filename (only quantitative if B1+ map provided) | |
% fR2s R2* map output filename (simple fit on PD data or OLS across | |
% all contrasts, according to defaults settings) | |
% fMT Magnetization transfer (MT) map output filename | |
% fA Proton density map output filename (free water concentration | |
% (PD) [%] or signal amplitude (A) [a.u.] depending on job and | |
% defaults settings (PDproc & RF sensitivity bias correction)). | |
% PPDw average PD-weighted image filename (or OLS fit at TE=0 if fullOLS = true) | |
% PT1w average T1-weighted image filename (or OLS fit at TE=0 if fullOLS = true) | |
% PMTw average MT-weighted image filename (or OLS fit at TE=0 if fullOLS = true) | |
% | |
% OTHER USEFUL VARIABLES EXPLAINED | |
% P_mtw, P_pdw, P_t1w (from jobsubj.raw_mpm) are MTw, PDw, T1w series of | |
% images respectively (multiple echoes) | |
% TE_mtw, TE_pdw, TE_t1w: echo times of the first echo (volume) of each | |
% contrast, respectively. | |
% TR_mtw, TR_pdw, TR_t1w: repetition time for each contrast, | |
% respectively. | |
% fa_mtw, fa_pdw, fa_t1w: excitation flip angles for each contrast, | |
% respectively. | |
% | |
%========================================================================== | |
% FEATURES AND REFERENCES | |
% Estimation of R1 and MT maps | |
% Helms et al., Magnetic Resonance in Medicine 60:1396-1407 (2008) | |
% Helms et al., Magnetic Resonance in Medicine 59:667-672 (2008) | |
% | |
% B1 correction of MT maps | |
% Weiskopf et al., Front. Neurosci. 2013, doi: | |
% 10.3389/fnins.2013.00095 | |
% | |
% Imperfect spoiling correction | |
% Preibisch and Deichmann, MRM 61:125-135 (2009) | |
% Corrects for residual transverse magnetization coherences that | |
% remain despite RF & gradient spoiling and result in deviations from | |
% the Ernst equation (ideal expected signal in a FLASH acquisition). | |
% Modelling data and P2_a, P2_b correction factors calculation based | |
% on code supplied by Ralf Deichmann. Only a small subset of | |
% experimental parameters have been used and imperfect spoiling | |
% correction can only be applied if these correction factors are | |
% defined specifically for the acquisition protocol used. | |
% | |
% OLS R2* map calculation | |
% Ordinary least square fit of R2* estimated from all echoes from all | |
% three contrasts instead of the PD-weighted image only. This | |
% increases the SNR in R2* maps. It is noted that it potentially | |
% mixes in additional MT and T1 contrast, as discussed in: | |
% Weiskopf et al. Front. Neurosci. 2014 DOI: 10.3389/fnins.2014.00278 | |
% This reference should be cited if you use this output. | |
% | |
%========================================================================== | |
% Written by | |
% Gunther Helms, MR-Research in Neurology and Psychiatry, University | |
% of Goettingen | |
% Martina Callaghan, John Ashburner, Wellcome Trust Centre for | |
% Neuroimaging at UCL, London | |
% Antoine Lutti, CHUV, Lausanne, Switzerland | |
% Nikolaus Weiskopf, Department of Neurophysics, Max Planck Institute | |
% for Human Cognitive and Brain Sciences, Leipzig, Germany | |
% | |
%========================================================================== | |
%% =======================================================================% | |
% Initiate processing | |
%=========================================================================% | |
flags = jobsubj.log.flags; | |
flags.PopUp = false; | |
hmri_log(sprintf('\t============ MPM PROCESSING - %s.m (%s) ============', mfilename, datestr(now)),flags); | |
% retrieves all required parameters for MPM processing | |
mpm_params = get_mpm_params(jobsubj); | |
% for convenience, define a few parameters to make formulae more readable | |
% and avoid number of repetitions: | |
% B1 transmit input: | |
% P_trans(1,:) = magnitude image (anatomical reference for coregistration) | |
% P_trans(2,:) = B1 map (p.u.) | |
P_trans = jobsubj.b1_trans_input; | |
% index number for each contrast - zero index means no images available | |
PDwidx = mpm_params.PDwidx; | |
T1widx = mpm_params.T1widx; | |
MTwidx = mpm_params.MTwidx; | |
% TE/TR/FA for each contrast | |
% T1w and MTw contrasts are optional. | |
% PDw must be present or nothing can be calculated. The script will abort | |
% at the next line if no PDw echoes available. In that case, a warning has | |
% been thrown earlier (in get_mpm_params). | |
TE_pdw = mpm_params.input(PDwidx).TE; | |
TR_pdw = mpm_params.input(PDwidx).TR; | |
fa_pdw = mpm_params.input(PDwidx).fa; | |
if T1widx | |
TE_t1w = mpm_params.input(T1widx).TE; %#ok<NASGU> | |
TR_t1w = mpm_params.input(T1widx).TR; | |
fa_t1w = mpm_params.input(T1widx).fa; | |
end | |
if MTwidx | |
TE_mtw = mpm_params.input(MTwidx).TE; %#ok<NASGU> | |
TR_mtw = mpm_params.input(MTwidx).TR; | |
fa_mtw = mpm_params.input(MTwidx).fa; | |
end | |
% other parameters | |
threshall = mpm_params.proc.threshall; | |
PDproc = mpm_params.proc.PD; | |
ISC = mpm_params.proc.ISC; | |
dt = [spm_type('float32'),spm_platform('bigend')]; % for nifti output | |
outbasename = spm_file(mpm_params.input(PDwidx).fnam(1,:),'basename'); % for all output files | |
calcpath = mpm_params.calcpath; | |
mpm_params.outbasename = outbasename; | |
respath = mpm_params.respath; | |
supplpath = mpm_params.supplpath; | |
% Number of echoes averaged (maximum number or echoes available for ALL | |
% contrasts AND under TE_limit (+1) - see get_mpm_params) | |
avg_nr = mpm_params.nr_echoes4avg; | |
% load PDw images | |
% PDw images are the reference space for all results. Therefore, the matrix | |
% dimension defined below (dm) is used across the whole script. It must not | |
% been redefined. | |
V_pdw = spm_vol(mpm_params.input(PDwidx).fnam); | |
dm = V_pdw(1).dim; | |
%% =======================================================================% | |
% Calculate R2* map from PDw echoes | |
%=========================================================================% | |
fR2s = ''; | |
hmri_log(sprintf('\t-------- R2* map calculation (basic exponential decay) --------'),mpm_params.nopuflags); | |
hmri_log(sprintf(['INFO: minimum number of echoes required for R2* map calculation is %d.' ... | |
'\nNumber of PDw echoes available: %d'], mpm_params.neco4R2sfit, length(mpm_params.input(PDwidx).TE)), mpm_params.nopuflags); | |
if mpm_params.basicR2s | |
if length(mpm_params.input(PDwidx).TE)<6 | |
hmri_log(sprintf(['GENERAL WARNING: deriving R2* map from an echo train including ' ... | |
'\nfewer than 6 echoes has not been validated nor investigated. ' ... | |
'\nFor a robust estimation, the minimum number of echoes required ' ... | |
'\ndepends on many factors, amongst which: ' ... | |
'\n\t- SNR/resolution' ... | |
'\n\t- distribution/spacing between TEs: note that early echoes are more' ... | |
'\n\t\taffected by the PDw contrast.' ... | |
'\nInterpret results carefully, with in mind a possible lack of robustness ' ... | |
'\nand reliability in the R2* estimation.']),mpm_params.defflags); | |
end | |
spm_progress_bar('Init',dm(3),'R2* fit','planes completed'); | |
% create nifti object for output R2* map | |
fR2s = fullfile(calcpath,[outbasename '_' mpm_params.output(mpm_params.qR2s).suffix '.nii']); | |
Ni = nifti; | |
Ni.mat = V_pdw(1).mat; | |
Ni.mat0 = V_pdw(1).mat; | |
Ni.descrip = mpm_params.output(mpm_params.qR2s).descrip{1}; | |
Ni.dat = file_array(fR2s,dm,dt, 0,1,0); | |
create(Ni); | |
% Fit R2* decay | |
reg = [ones(numel(TE_pdw),1) TE_pdw(:)]; | |
W = (reg'*reg)\reg'; | |
W = -W(2,:)'; | |
for p = 1:dm(3) | |
M = spm_matrix([0 0 p 0 0 0 1 1 1]); | |
Y = zeros(dm(1:2)); | |
for i = 1:numel(V_pdw) | |
M1 = V_pdw(i).mat\V_pdw(1).mat*M; | |
Y = Y + W(i)*log(max(spm_slice_vol(V_pdw(i),M1,dm(1:2),mpm_params.interp),1)); | |
end | |
Ni.dat(:,:,p) = max(min(Y,threshall.R2s),-threshall.R2s)*1000; % threshold T2* at +/- 0.1ms or R2* at +/- 10000 *(1/sec), negative values are allowed to preserve Gaussian distribution | |
spm_progress_bar('Set',p); | |
end | |
spm_progress_bar('Clear'); | |
% Set and write metadata | |
input_files = mpm_params.input(PDwidx).fnam; | |
Output_hdr = init_mpm_output_metadata(input_files, mpm_params); | |
Output_hdr.history.output.imtype = 'Basic R2* map'; | |
Output_hdr.history.output.units = 's-1'; | |
set_metadata(fR2s,Output_hdr,mpm_params.json); | |
else | |
hmri_log(sprintf('INFO: No (basic) R2* map will be calculated.\nInsufficient number of PDw echoes.'), mpm_params.defflags); | |
end | |
%% =======================================================================% | |
% Reading and averaging the images | |
%=========================================================================% | |
hmri_log(sprintf('\t-------- Reading and averaging the images --------'),mpm_params.nopuflags); | |
% Average only first few echoes for increased SNR and fit T2* | |
Pavg = cell(1,mpm_params.ncon); | |
for ccon=1:mpm_params.ncon % loop over available contrasts | |
Pavg{ccon} = fullfile(calcpath,[outbasename '_' mpm_params.input(ccon).tag 'w.nii']); | |
V = spm_vol(mpm_params.input(ccon).fnam); | |
Ni = nifti; | |
Ni.mat = V(1).mat; | |
Ni.mat0 = V(1).mat; | |
Ni.descrip = sprintf('Averaged %sw images - %d echoes', mpm_params.input(ccon).tag, avg_nr); | |
Ni.dat = file_array(Pavg{ccon},dm,dt,0,1,0); | |
create(Ni); | |
spm_progress_bar('Init',dm(3),Ni.descrip,'planes completed'); | |
% sm = 0; | |
for p = 1:dm(3) | |
M = spm_matrix([0 0 p]); | |
Y = zeros(dm(1:2)); | |
for nr = 1:avg_nr | |
M1 = V(nr).mat\V(1).mat*M; | |
Y = Y + spm_slice_vol(V(nr),M1,dm(1:2),mpm_params.interp); | |
end | |
Ni.dat(:,:,p) = Y/avg_nr; | |
% sm = sm + sum(Y(:))/avg_nr; | |
spm_progress_bar('Set',p); | |
end | |
% avg = sm/prod(dm); | |
spm_progress_bar('Clear'); | |
input_files = mpm_params.input(ccon).fnam; | |
Output_hdr = init_mpm_output_metadata(input_files, mpm_params); | |
Output_hdr.history.output.imtype = Ni.descrip; | |
Output_hdr.history.output.units = 'a.u.'; | |
set_metadata(Pavg{ccon},Output_hdr,mpm_params.json); | |
end | |
% Average T1w image for PD calculation | |
% (average over PDproc.nr_echoes_forA echoes, see hmri_defaults): | |
if (PDwidx && T1widx) | |
PT1w_forA = fullfile(calcpath,[outbasename '_T1w_forA.nii']); | |
V = spm_vol(mpm_params.input(T1widx).fnam); | |
Ni = nifti; | |
Ni.mat = V(1).mat; | |
Ni.mat0 = V(1).mat; | |
Ni.descrip = sprintf('Averaged T1w images for PD calculation - %d echoes',PDproc.nr_echoes_forA); | |
Ni.dat = file_array(PT1w_forA,dm,dt, 0,1,0); | |
create(Ni); | |
spm_progress_bar('Init',dm(3),Ni.descrip,'planes completed'); | |
for p = 1:dm(3), | |
M = spm_matrix([0 0 p]); | |
Y = zeros(dm(1:2)); | |
for nr = 1:PDproc.nr_echoes_forA, | |
M1 = V(nr).mat\V(1).mat*M; | |
Y = Y + spm_slice_vol(V(nr),M1,dm(1:2),mpm_params.interp); | |
end | |
Ni.dat(:,:,p) = Y/PDproc.nr_echoes_forA; | |
spm_progress_bar('Set',p); | |
end | |
spm_progress_bar('Clear'); | |
end | |
%% =======================================================================% | |
% Coregistering the images | |
%=========================================================================% | |
hmri_log(sprintf('\t-------- Coregistering the images --------'),mpm_params.nopuflags); | |
% NOTE: coregistration can be disabled using the hmri_def.coreg2PDw flag | |
x_MT2PD = [0 0 0 0 0 0]; | |
if MTwidx; | |
if mpm_params.coreg | |
x_MT2PD = coreg_mt(Pavg{PDwidx}, Pavg{MTwidx}); | |
end | |
end | |
x_T12PD = [0 0 0 0 0 0]; | |
if T1widx; | |
if mpm_params.coreg | |
x_T12PD = coreg_mt(Pavg{PDwidx}, Pavg{T1widx}); | |
coreg_mt(Pavg{PDwidx}, PT1w_forA); | |
end | |
end | |
V_trans = []; | |
if ~isempty(P_trans) | |
% Load B1 mapping data if available and coregister to PDw | |
% P_trans(1,:) = magnitude image (anatomical reference for coregistration) | |
% P_trans(2,:) = B1 map (p.u.) | |
if mpm_params.coreg | |
coreg_bias_map(Pavg{PDwidx}, P_trans); | |
end | |
V_trans = spm_vol(P_trans); | |
end | |
% parameters saved for quality assessment | |
if mpm_params.QA.enable | |
if exist(mpm_params.QA.fnam,'file') | |
mpm_params.QA = spm_jsonread(mpm_params.QA.fnam); | |
end | |
mpm_params.QA.ContrastCoreg.MT2PD = x_MT2PD; | |
mpm_params.QA.ContrastCoreg.T12PD = x_T12PD; | |
spm_jsonwrite(mpm_params.QA.fnam, mpm_params.QA, struct('indent','\t')); | |
end | |
% load averaged images | |
Vavg(PDwidx) = spm_vol(Pavg{PDwidx}); | |
if MTwidx; Vavg(MTwidx) = spm_vol(Pavg{MTwidx}); end | |
if T1widx | |
Vavg(T1widx) = spm_vol(Pavg{T1widx}); | |
VT1w_forA = spm_vol(PT1w_forA); | |
end | |
%% =======================================================================% | |
% multi-contrast R2* map calculation for quality assessment by AL | |
% METHOD: R2s map calculated for each contrast separately (PDw, MTw, T1w) | |
% to evaluate the SD of each R2* map in white matter (i.e. intra-run motion | |
% measurement). | |
%=========================================================================% | |
if mpm_params.QA.enable | |
hmri_log(sprintf('\t-------- Multi-contrast R2* map calculation for QA --------'),mpm_params.nopuflags); | |
fR2sQA = cell(1,mpm_params.ncon); | |
for ccon = 1:mpm_params.ncon | |
% only if enough echoes | |
if mpm_params.estaticsR2s(ccon) | |
fR2sQA{ccon} = fullfile(calcpath,[outbasename '_R2s_' mpm_params.input(ccon).tag 'w.nii']); | |
dt = [spm_type('float32'),spm_platform('bigend')]; | |
Ni = nifti; | |
Ni.mat = V_pdw(1).mat; | |
Ni.mat0 = V_pdw(1).mat; | |
Ni.descrip='OLS R2* map [s-1]'; | |
Ni.dat = file_array(fR2sQA{ccon},dm,dt, 0,1,0); | |
create(Ni); | |
TE = mpm_params.input(ccon).TE; | |
Vcon = spm_vol(mpm_params.input(ccon).fnam); | |
% The assumption is that the result of co-registering the average | |
% weighted volumes is applicable for each of the echoes of that | |
% contrast => Replicate the mat field across contrasts for all echoes. | |
% % matField = cat(3, repmat(VPDw.mat, [1, 1, nPD]), ... | |
% % repmat(VMTw.mat, [1, 1, nMT]), repmat(VT1w.mat, [1, 1, nT1])); | |
reg = [ones(numel(TE),1) TE(:)]; | |
W = (reg'*reg)\reg'; | |
spm_progress_bar('Init',dm(3),'multi-contrast R2* fit','planes completed'); | |
for p = 1:dm(3), | |
M = spm_matrix([0 0 p 0 0 0 1 1 1]); | |
data = zeros([numel(TE) dm(1:2)]); | |
for cecho = 1:numel(TE) | |
% Take slice p (defined in M) and map to a location in the | |
% appropriate contrast using the matField entry for that | |
% contrast, which has been co-registered to the PD-weighted | |
% data: | |
M1 = Vavg(ccon).mat\V_pdw(1).mat*M; | |
% Third order B-spline interpolation for OLS R2* estimation | |
% since we no longer assume that the echoes are perfectly | |
% aligned as we do for the standard PDw derived R2* estimate. | |
data(cecho,:,:) = log(max(spm_slice_vol(Vcon(cecho),M1,dm(1:2),mpm_params.interp),1)); | |
end | |
Y = W*reshape(data, [numel(TE) prod(dm(1:2))]); | |
Y = -reshape(Y(end,:), dm(1:2)); | |
% NB: mat field defined by V_pdw => first PDw echo | |
% threshold T2* at +/- 0.1ms or R2* at +/- 10000 *(1/sec), | |
% negative values are allowed to preserve Gaussian distribution | |
Ni.dat(:,:,p) = max(min(Y,threshall.R2s),-threshall.R2s)*1000; | |
spm_progress_bar('Set',p); | |
end | |
spm_progress_bar('Clear'); | |
end | |
end | |
end | |
%% =======================================================================% | |
% OLS R2* map calculation by MFC | |
% [Reference: ESTATICS, Weiskopf et al. 2014] | |
%=========================================================================% | |
fR2s_OLS = ''; | |
hmri_log(sprintf('\t-------- OLS R2* map calculation (ESTATICS) --------'),mpm_params.nopuflags); | |
LogMsg = sprintf(['INFO: minimum number of echoes required for R2* map calculation is %d.' ... | |
'\nNumber of echoes available: '], mpm_params.neco4R2sfit); | |
for ccon = 1:mpm_params.ncon | |
LogMsg = sprintf('%s %sw = %d.',LogMsg, mpm_params.input(ccon).tag, length(mpm_params.input(ccon).TE)); | |
end | |
hmri_log(LogMsg, mpm_params.nopuflags); | |
if mpm_params.proc.R2sOLS && any(mpm_params.estaticsR2s) | |
if length(mpm_params.input(PDwidx).TE)<6 | |
hmri_log(sprintf(['GENERAL WARNING: deriving R2* map from echo trains including ' ... | |
'\nfewer than 6 echoes has not been validated nor investigated. ' ... | |
'\nFor a robust estimation, the minimum number of echoes required ' ... | |
'\ndepends on many factors, amongst which: ' ... | |
'\n\t- SNR/resolution' ... | |
'\n\t- distribution/spacing between TEs: note that early echoes are more' ... | |
'\n\t\taffected by the specific contrast, violating the ESTATICS assumption ' ... | |
'\n\t\tof a common decay between contrasts.' ... | |
'\n\t- number of contrasts available (fewer echoes per contrast required ' ... | |
'\n\t\tfor 3 (PDw, T1w, MTw) contrasts as compared to 2 or even 1) ' ... | |
'\nInterpret results carefully, with in mind a possible lack of robustness ' ... | |
'\nand reliability in the R2* estimation.']),mpm_params.defflags); | |
end | |
% OLS fit at TE=0: to be used instead of averaged echoes of each | |
% contrast if "fullOLS" option is enabled | |
if mpm_params.fullOLS | |
hmri_log(sprintf('\t-------- and fit to TE=0 for each contrast --------'),mpm_params.nopuflags); | |
Nmap = nifti; | |
Pte0 = cell(1,mpm_params.ncon); | |
for ccon = 1:mpm_params.ncon | |
% requires a minimum of neco4R2sfit echoes for a robust fit | |
if mpm_params.estaticsR2s(ccon) | |
Pte0{ccon} = fullfile(calcpath,[outbasename '_' mpm_params.input(ccon).tag 'w_OLSfit_TEzero.nii']); | |
Ni = nifti; | |
Ni.mat = V_pdw(1).mat; | |
Ni.mat0 = V_pdw(1).mat; | |
Ni.descrip = sprintf('OLS fit to TE=0 for %sw images - %d echoes', mpm_params.input(ccon).tag, length(mpm_params.input(ccon).TE)); | |
Ni.dat = file_array(Pte0{ccon},dm,dt,0,1,0); | |
create(Ni); | |
% set metadata | |
input_files = mpm_params.input(ccon).fnam; | |
Output_hdr = init_mpm_output_metadata(input_files, mpm_params); | |
Output_hdr.history.output.imtype = Ni.descrip; | |
Output_hdr.history.output.units = 'a.u.'; | |
set_metadata(Pte0{ccon},Output_hdr,mpm_params.json); | |
% re-load the updated NIFTI file (in case extended header has | |
% been added, the offset has changed and must be updated before | |
% writing the data to the file!) | |
Nmap(ccon) = nifti(Pte0{ccon}); | |
else | |
Pte0{ccon} = ''; | |
end | |
end | |
end % init nifti objects for fullOLS case | |
fR2s_OLS = fullfile(calcpath,[outbasename '_R2s_OLS' '.nii']); | |
Ni = nifti; | |
Ni.mat = V_pdw(1).mat; | |
Ni.mat0 = V_pdw(1).mat; | |
Ni.descrip = 'OLS R2* map [s-1]'; | |
Ni.dat = file_array(fR2s_OLS,dm,dt,0,1,0); | |
create(Ni); | |
% Combine the data and echo times: | |
nechoes = zeros(1,mpm_params.ncon); | |
for ccon = 1:mpm_params.ncon | |
% only if enough echoes | |
if mpm_params.estaticsR2s(ccon) | |
nechoes(ccon) = size(mpm_params.input(ccon).fnam,1); | |
end | |
end | |
% Same formalism as for PDw fit but now extra colums for the "S(0)" | |
% amplitudes of the different contrasts: | |
reg = zeros(sum(nechoes),mpm_params.ncon+1); | |
for ccon = 1:mpm_params.ncon | |
% only if enough echoes | |
if mpm_params.estaticsR2s(ccon) | |
reg(sum(nechoes(1:ccon-1))+(1:nechoes(ccon)),ccon) = 1; | |
reg(sum(nechoes(1:ccon-1))+(1:nechoes(ccon)),end) = mpm_params.input(ccon).TE; | |
end | |
end | |
W = (reg'*reg)\reg'; | |
spm_progress_bar('Init',dm(3),'OLS R2* fit','planes completed'); | |
for p = 1:dm(3), | |
M = spm_matrix([0 0 p 0 0 0 1 1 1]); | |
data = zeros([sum(nechoes) dm(1:2)]); | |
for ccon = 1:mpm_params.ncon | |
% only if enough echoes | |
if mpm_params.estaticsR2s(ccon) | |
Vcon = spm_vol(mpm_params.input(ccon).fnam); | |
% Take slice p (defined in M) and map to a location in the | |
% appropriate contrast using the V.mat field entry for that | |
% contrast, which has been co-registered to the PD-weighted | |
% data: | |
M1 = Vavg(ccon).mat\V_pdw(1).mat*M; | |
for cecho = 1:nechoes(ccon) | |
% Third order B-spline interpolation for OLS R2* estimation | |
% since we no longer assume that the echoes are perfectly | |
% aligned as we do for the standard PDw derived R2* | |
% estimate. | |
data(sum(nechoes(1:ccon-1))+cecho,:,:) = log(max(spm_slice_vol(Vcon(cecho),M1,dm(1:2),mpm_params.interp),1)); | |
end | |
end | |
end | |
Y = W*reshape(data, [sum(nechoes) prod(dm(1:2))]); | |
% Writes "fullOLS" images (OLS fit to TE=0 for each contrast) | |
if mpm_params.fullOLS | |
for ccon = 1:mpm_params.ncon | |
% only if enough echoes | |
if mpm_params.estaticsR2s(ccon) | |
Nmap(ccon).dat(:,:,p) = reshape(exp(Y(ccon,:)), dm(1:2)); | |
end | |
end | |
end | |
Y = -reshape(Y(end,:), dm(1:2)); | |
% NB: mat field defined by V_pdw => first PDw echo | |
% threshold T2* at +/- 0.1ms or R2* at +/- 10000 *(1/sec), | |
% negative values are allowed to preserve Gaussian distribution. | |
Ni.dat(:,:,p) = max(min(Y,threshall.R2s),-threshall.R2s)*1000; | |
spm_progress_bar('Set',p); | |
end | |
spm_progress_bar('Clear'); | |
% Set metadata (R2S_OLS) | |
input_files = mpm_params.input(PDwidx).fnam; | |
if (T1widx); input_files = char(input_files, mpm_params.input(T1widx).fnam); end | |
if (MTwidx); input_files = char(input_files, mpm_params.input(MTwidx).fnam); end | |
Output_hdr = init_mpm_output_metadata(input_files, mpm_params); | |
Output_hdr.history.output.imtype = mpm_params.output(mpm_params.qR2s).descrip; | |
Output_hdr.history.output.units = mpm_params.output(mpm_params.qR2s).units; | |
set_metadata(fullfile(calcpath,[outbasename '_' mpm_params.output(mpm_params.qR2s).suffix '_OLS.nii']),Output_hdr,mpm_params.json); | |
else | |
hmri_log(sprintf('INFO: No R2* map will be calculated using the ESTATICS model. \nInsufficient number of echoes.'), mpm_params.defflags); | |
end % OLS code | |
% if "fullOLS" option enabled AND could be applied to every contrast | |
% (enough echoes for every contrast), Pte0 images replace Pavg images in | |
% the rest of the code. We need to apply the substitution and reload the | |
% images... | |
if mpm_params.fullOLS && all(mpm_params.estaticsR2s) | |
for ccon = 1:mpm_params.ncon | |
Pavg{ccon} = Pte0{ccon}; | |
Vavg(ccon) = spm_vol(Pavg{ccon}); | |
end | |
end | |
%% =======================================================================% | |
% Prepare output for R1, PD and MT maps | |
%=========================================================================% | |
% define NIFTI objects for output images | |
Nmap = nifti; | |
for ii=1:length(mpm_params.output)-1*(~isempty(fR2s)||~isempty(fR2s_OLS)) % R2s output already done | |
%dm = V_pdw(1).dim; | |
Ni = nifti; | |
Ni.mat = V_pdw(1).mat; | |
Ni.mat0 = V_pdw(1).mat; | |
Ni.descrip = mpm_params.output(ii).descrip{1}; | |
Ni.dat = file_array(fullfile(calcpath,[outbasename '_' mpm_params.output(ii).suffix '.nii']),dm,dt, 0,1,0); | |
create(Ni); | |
Nmap(ii) = Ni; | |
end | |
fR1 = ''; | |
fA = ''; | |
fMT = ''; | |
if (PDwidx && T1widx) | |
fR1 = fullfile(calcpath,[outbasename '_' mpm_params.output(mpm_params.qR1).suffix '.nii']); | |
fA = fullfile(calcpath,[outbasename '_' mpm_params.output(mpm_params.qPD).suffix '.nii']); | |
if MTwidx | |
fMT = fullfile(calcpath,[outbasename '_' mpm_params.output(mpm_params.qMT).suffix '.nii']); | |
end | |
end | |
% mpm_params.output(mpm_params.qR1).fnam = ''; | |
% mpm_params.output(mpm_params.qPD).fnam = ''; | |
% mpm_params.output(mpm_params.qMT).fnam = ''; | |
% if (PDwidx && T1widx) | |
% mpm_params.output(mpm_params.qR1).fnam = fullfile(calcpath,[outbasename '_' mpm_params.output(mpm_params.qR1).suffix '.nii']); | |
% mpm_params.output(mpm_params.qPD).fnam = fullfile(calcpath,[outbasename '_' mpm_params.output(mpm_params.qPD).suffix '.nii']); | |
% if MTwidx | |
% mpm_params.output(mpm_params.qMT).fnam = fullfile(calcpath,[outbasename '_' mpm_params.output(mpm_params.qMT).suffix '.nii']); | |
% end | |
% end | |
%% =======================================================================% | |
% Map calculation continued (R1, PD, MT) | |
%=========================================================================% | |
hmri_log(sprintf('\t-------- Map calculation continued (R1, MTR) --------'), mpm_params.nopuflags); | |
M0 = Ni.mat; | |
fa_pdw_rad = fa_pdw * pi / 180; | |
if MTwidx; fa_mtw_rad = fa_mtw * pi / 180; end | |
if T1widx; fa_t1w_rad = fa_t1w * pi / 180; end | |
spm_progress_bar('Init',dm(3),'Calculating maps','planes completed'); | |
% First calculate R1 & MTR | |
for p = 1:dm(3) | |
M = M0*spm_matrix([0 0 p]); | |
% PDw images are always available, so this bit is always loaded: | |
PDw = spm_slice_vol(Vavg(PDwidx),Vavg(PDwidx).mat\M,dm(1:2),mpm_params.interp); | |
if ~isempty(V_trans) | |
f_T = spm_slice_vol(V_trans(2,:),V_trans(2,:).mat\M,dm(1:2),mpm_params.interp)/100; % divide by 100, since p.u. maps | |
else | |
f_T = []; | |
end | |
% Standard magnetization transfer ratio (MTR) in percent units [p.u.] | |
% only if trpd = trmt and fapd = famt and if PDw and MTw images are | |
% available | |
if (MTwidx && PDwidx) | |
MTw = spm_slice_vol(Vavg(MTwidx),Vavg(MTwidx).mat\M,dm(1:2),mpm_params.interp); | |
if (TR_mtw == TR_pdw) && (fa_mtw == fa_pdw) % additional MTR image... | |
MTR = (PDw-MTw)./(PDw+eps) * 100; | |
% write MTR image | |
Nmap(mpm_params.qMTR).dat(:,:,p) = max(min(MTR,threshall.MTR),-threshall.MTR); | |
end | |
end | |
% T1 map and A/PD maps can only be calculated if T1w images are | |
% available: | |
if T1widx | |
T1w = spm_slice_vol(Vavg(T1widx),Vavg(T1widx).mat\M,dm(1:2),mpm_params.interp); | |
if isempty(f_T) | |
% semi-quantitative T1 | |
R1 = (((T1w * (fa_t1w_rad / 2 / TR_t1w)) - (PDw * fa_pdw_rad / 2 / TR_pdw)) ./ ... | |
max(((PDw / fa_pdw_rad) - (T1w / fa_t1w_rad)),eps))*10^6; | |
else | |
% Transmit bias corrected quantitative T1 values | |
% correct T1 for transmit bias f_T with fa_true = f_T * fa_nom | |
% T1corr = T1 / f_T / f_T | |
if ISC.enabled | |
% MFC: We do have P2_a and P2_b parameters for this sequence | |
% => T1 = A(B1) + B(B1)*T1app (see Preibisch 2009) | |
T1 = ISC.P2_a(1)*f_T.^2 + ... | |
ISC.P2_a(2)*f_T + ... | |
ISC.P2_a(3) + ... | |
(ISC.P2_b(1)*f_T.^2+ISC.P2_b(2)*f_T+ISC.P2_b(3)) .* ... | |
((((PDw / fa_pdw_rad) - (T1w / fa_t1w_rad)+eps) ./ ... | |
max((T1w * fa_t1w_rad / 2 / TR_t1w) - (PDw * fa_pdw_rad / 2 / TR_pdw),eps))./f_T.^2); | |
else | |
% MFC: We do not have P2_a or P2_b parameters for this sequence | |
% => T1 = T1app | |
T1 = ((((PDw / fa_pdw_rad) - (T1w / fa_t1w_rad)+eps) ./ ... | |
max((T1w * fa_t1w_rad / 2 / TR_t1w) - (PDw * fa_pdw_rad / 2 / TR_pdw),eps))./f_T.^2); | |
end | |
R1 = 1./T1*10^6; | |
end | |
R1(R1<0) = 0; | |
tmp = R1; | |
Nmap(mpm_params.qR1).dat(:,:,p) = min(max(tmp,-threshall.R1),threshall.R1)*0.001; % truncating images | |
end | |
spm_progress_bar('Set',p); | |
end | |
spm_progress_bar('Clear'); | |
% apply UNICORT if required: | |
% NOTE: the (unicort) B1 map will then be used to calculate PD, but | |
% not to correct R1 map from RF spoiling effects (unsuitable | |
% interfering and higher order corrections!) nor to correct MT map | |
% from higher order transmit bias effects. | |
V_trans_unicort = []; | |
if (mpm_params.UNICORT.R1) | |
out_unicort = hmri_create_unicort(Pavg{PDwidx}, fR1, jobsubj); | |
fR1 = out_unicort.R1u{1}; | |
P_trans_unicort = out_unicort.B1u{1}; | |
V_trans_unicort = spm_vol(P_trans_unicort); | |
Output_hdr = get_metadata(fR1); | |
Output_hdr{1}.history.output.imtype = mpm_params.output(mpm_params.qR1).descrip; | |
set_metadata(fR1,Output_hdr{1},mpm_params.json); | |
end | |
V_R1 = spm_vol(fR1); | |
hmri_log(sprintf('\t-------- Map calculation continued (MT, A/PD) --------'), mpm_params.nopuflags); | |
spm_progress_bar('Init',dm(3),'Calculating maps (continued)','planes completed'); | |
% Then calculate MT & PD | |
for p = 1:dm(3) | |
M = M0*spm_matrix([0 0 p]); | |
if ~isempty(V_trans) | |
f_T = spm_slice_vol(V_trans(2,:),V_trans(2,:).mat\M,dm(1:2),mpm_params.interp)/100; % divide by 100, since p.u. maps | |
elseif ~isempty(V_trans_unicort) | |
f_T = spm_slice_vol(V_trans_unicort(1,:),V_trans_unicort(1,:).mat\M,dm(1:2),mpm_params.interp)/100; % divide by 100, since p.u. maps | |
else | |
f_T = []; | |
end | |
% PDw images are always available, so this bit is always loaded: | |
PDw = spm_slice_vol(Vavg(PDwidx),Vavg(PDwidx).mat\M,dm(1:2),mpm_params.interp); | |
% T1 map and A/PD maps can only be calculated if T1w images are | |
% available: | |
if T1widx | |
T1w = spm_slice_vol(Vavg(T1widx),Vavg(T1widx).mat\M,dm(1:2),mpm_params.interp); | |
T1 = 10^3./spm_slice_vol(V_R1,V_R1.mat\M,dm(1:2),mpm_params.interp); | |
% if "fullOLS" option enabled, use the OLS fit at TE=0 as | |
% "T1w_forA"; otherwise use the average calculated earlier (by | |
% default, corresponds to the first echo to reduce R2* bias) | |
if mpm_params.fullOLS | |
T1w_forA = T1w; | |
else | |
T1w_forA = spm_slice_vol(VT1w_forA,VT1w_forA.mat\M,dm(1:2),mpm_params.interp); | |
end | |
% A values proportional to PD | |
% f_T correction is applied either if: | |
% - f_T has been provided as separate B1 mapping measurement (not | |
% UNICORT!) or | |
% - f_T has been calculated using UNICORT *AND* the UNICORT.PD flag | |
% is enabled (advanced user only! method not validated yet!) | |
if(~isempty(f_T))&&(~mpm_params.UNICORT.R1 || mpm_params.UNICORT.PD) | |
A = T1 .* (T1w_forA .*(fa_t1w_rad*f_T) / 2 / TR_t1w) + (T1w_forA ./ (fa_t1w_rad*f_T)); | |
else | |
% semi-quantitative A | |
A = T1 .* (T1w_forA * fa_t1w_rad / 2 / TR_t1w) + (T1w_forA / fa_t1w_rad); | |
end | |
tmp = A; | |
tmp(isinf(tmp)) = 0; | |
Nmap(mpm_params.qPD).dat(:,:,p) = max(min(tmp,threshall.A),-threshall.A); | |
% dynamic range increased to 10^5 to accommodate phased-array coils and symmetrical for noise distribution | |
% for MT maps calculation, one needs MTw images on top of the T1w | |
% and PDw ones... | |
if MTwidx | |
MTw = spm_slice_vol(Vavg(MTwidx),Vavg(MTwidx).mat\M,dm(1:2),3); | |
T1_forMT = ((PDw / fa_pdw_rad) - (T1w / fa_t1w_rad)) ./ ... | |
max((T1w * (fa_t1w_rad / 2 / TR_t1w)) - (PDw * fa_pdw_rad / 2 / TR_pdw),eps); | |
A_forMT = T1_forMT .* (T1w * fa_t1w_rad / 2 / TR_t1w) + (T1w / fa_t1w_rad); | |
% MT in [p.u.]; offset by - famt * famt / 2 * 100 where MT_w = 0 (outside mask) | |
MT = ( (A_forMT * fa_mtw_rad - MTw) ./ (MTw+eps) ./ (T1_forMT + eps) * TR_mtw - fa_mtw_rad^2 / 2 ) * 100; | |
% f_T correction is applied either if: | |
% - f_T has been provided as separate B1 mapping measurement (not | |
% UNICORT!) or | |
% - f_T has been calculated using UNICORT *AND* the UNICORT.MT flag | |
% is enabled (advanced user only! method not validated yet!) | |
if (~isempty(f_T))&&(~mpm_params.UNICORT.R1 || mpm_params.UNICORT.MT) | |
MT = MT .* (1 - 0.4) ./ (1 - 0.4 * f_T); | |
end | |
tmp = MT; | |
Nmap(mpm_params.qMT).dat(:,:,p) = max(min(tmp,threshall.MT),-threshall.MT); | |
end | |
end | |
spm_progress_bar('Set',p); | |
end | |
spm_progress_bar('Clear'); | |
% set metadata for all output images | |
input_files = mpm_params.input(PDwidx).fnam; | |
if (T1widx); input_files = char(input_files, mpm_params.input(T1widx).fnam); end | |
if (MTwidx); input_files = char(input_files, mpm_params.input(MTwidx).fnam); end | |
Output_hdr = init_mpm_output_metadata(input_files, mpm_params); | |
for ctr = 1:length(mpm_params.output)-1 | |
Output_hdr.history.output.imtype = mpm_params.output(ctr).descrip; | |
Output_hdr.history.output.units = mpm_params.output(ctr).units; | |
set_metadata(fullfile(calcpath,[outbasename '_' mpm_params.output(ctr).suffix '.nii']),Output_hdr,mpm_params.json); | |
end | |
%% =======================================================================% | |
% ACPC Realign all images - only if MT map created | |
%=========================================================================% | |
if mpm_params.ACPCrealign | |
if (MTwidx && PDwidx && T1widx) | |
hmri_log(sprintf('\t-------- ACPC Realign all images --------'), mpm_params.nopuflags); | |
% Define and calculate masked MT image | |
% Load MT image | |
V_MT = spm_vol(fMT); | |
MTimage = spm_read_vols(V_MT); | |
% Define new file name for masked MT image | |
V_MT.fname = fullfile(calcpath,['masked_' spm_str_manip(fMT,'t')]); | |
% Load average PDw image (mask based on averaged PDw image) | |
PDWimage = spm_read_vols(Vavg(PDwidx)); | |
% Mask MT image and save the masked MT image ('masked_..._MT.nii') | |
MTimage(PDWimage<0.6*mean(PDWimage(:)))=0; | |
spm_write_vol(V_MT,MTimage); | |
% Use masked MT image to calculate transformation for ACPC realignment | |
% (to increase robustness in segmentation): | |
[~,R] = hmri_create_comm_adjust(1,V_MT.fname,V_MT.fname,8,0,fullfile(spm('Dir'),'canonical','avg152T1.nii')); | |
% Collect all images from all defined output directories: | |
allpaths = jobsubj.path; | |
ACPC_images = []; | |
f = fieldnames(allpaths); | |
for cfield = 1:length(f) | |
tmpfiles = spm_select('FPList',allpaths.(f{cfield}),'.*.(img|nii)$'); | |
if ~isempty(tmpfiles) | |
if ~isempty(ACPC_images) | |
ACPC_images = char(ACPC_images,spm_select('FPList',allpaths.(f{cfield}),'.*.(img|nii)$')); | |
else | |
ACPC_images = tmpfiles; | |
end | |
end | |
end | |
% Apply transform to ALL images | |
for i=1:size(ACPC_images,1) | |
spm_get_space(deblank(ACPC_images(i,:)),... | |
R*spm_get_space(deblank(ACPC_images(i,:)))); | |
Vsave = spm_vol(ACPC_images(i,:)); | |
Vsave.descrip = [Vsave.descrip ' - AC-PC realigned']; | |
spm_write_vol(Vsave,spm_read_vols(spm_vol(ACPC_images(i,:)))); | |
end; | |
% Save transformation matrix | |
spm_jsonwrite(fullfile(supplpath,'hMRI_map_creation_ACPCrealign_transformation_matrix.json'),R,struct('indent','\t')); | |
else | |
hmri_log(sprintf(['WARNING: ACPC Realign was enabled, but no MT map was available \n' ... | |
'to proceed. ACPC realignment must be done separately, e.g. you can \n'... | |
'run [hMRI tools > Auto-reorient] before calculating the maps.\n' ... | |
'NOTE: segmentation might crash if no initial reorientation.']), mpm_params.defflags); | |
end | |
end | |
% for quality assessment and/or PD map calculation | |
% segmentation preferentially performed on MT map but can be done on R1 map | |
% if no MT map available. Therefore, we must at least have R1 available, | |
% i.e. both PDw and T1w inputs... | |
if (mpm_params.QA.enable||(PDproc.calibr)) && (PDwidx && T1widx) | |
if ~isempty(fMT); | |
Vsave = spm_vol(fMT); | |
else % ~isempty(fR1); | |
Vsave = spm_vol(fR1); | |
end | |
MTtemp = spm_read_vols(Vsave); | |
% The 5 outer voxels in all directions are nulled in order to remove | |
% artefactual effects from the MT map on segmentation: | |
MTtemp(1:5,:,:)=0; MTtemp(end-5:end,:,:)=0; | |
MTtemp(:,1:5,:)=0; MTtemp(:,end-5:end,:)=0; | |
MTtemp(:,:,1:5)=0; MTtemp(:,:,end-5:end)=0; | |
Vsave.fname = spm_file(Vsave.fname,'suffix','_outer_suppressed'); | |
spm_write_vol(Vsave,MTtemp); | |
% use unified segmentation with uniform defaults across the toobox: | |
job_brainmask = hmri_get_defaults('segment'); | |
job_brainmask.channel.vols = {Vsave.fname}; | |
job_brainmask.channel.write = [0 0]; % no need to write BiasField nor BiasCorrected image | |
output_list = spm_preproc_run(job_brainmask); | |
fTPM = char(cat(1,output_list.tiss.c)); | |
end | |
% for quality assessment - the above segmentation must have run | |
if mpm_params.QA.enable && exist('fTPM','var') && any(mpm_params.estaticsR2s) | |
% Load existing QA results | |
if exist(mpm_params.QA.fnam,'file') | |
mpm_params.QA = spm_jsonread(mpm_params.QA.fnam); | |
end | |
% Calculate WM mask | |
TPMs = spm_read_vols(spm_vol(fTPM)); | |
WMmask = zeros(size(squeeze(TPMs(:,:,:,2)))); | |
WMmask(squeeze(TPMs(:,:,:,2))>=PDproc.WMMaskTh) = 1; | |
WMmask = spm_erode(spm_erode(double(WMmask))); | |
% Load OLS R2s maps calculated for each contrast, mask them and | |
% calculate SD within the WM mask (measure of the intra-run motion for | |
% each contrast) | |
for ccon = 1:mpm_params.ncon | |
R2s = spm_read_vols(spm_vol(fR2sQA{ccon})); | |
MaskedR2s = squeeze(R2s.*WMmask); | |
SDR2s = std(MaskedR2s(MaskedR2s~=0),[],1); | |
mpm_params.QA.SDR2s.([mpm_params.input(ccon).tag 'w']) = SDR2s; | |
end | |
spm_jsonwrite(mpm_params.QA.fnam, mpm_params.QA, struct('indent','\t')); | |
end | |
% PD map calculation | |
% for correction of the R2s bias in the A map if that option is enabled... | |
if PDproc.T2scorr && (~isempty(fR2s)||~isempty(fR2s_OLS)) | |
% uses OLS it if available - less noisy | |
if ~isempty(fR2s_OLS) | |
PR2s = fR2s_OLS; | |
else | |
PR2s = fR2s; | |
end | |
% calculate correction (expected to be between 1 and 1.5 approx) | |
R2s = spm_read_vols(spm_vol(PR2s)); | |
R2scorr4A = zeros(size(R2s)); | |
for cecho=1:mpm_params.proc.PD.nr_echoes_forA | |
TE = mpm_params.input(PDwidx).TE(cecho)*0.001; % in seconds | |
R2scorr4A = R2scorr4A + exp(-TE.*R2s); | |
end | |
R2scorr4A = R2scorr4A/mpm_params.proc.PD.nr_echoes_forA; | |
% save correction for inspection | |
NiR2scorr4A = nifti; | |
NiR2scorr4A.mat = V_pdw(1).mat; | |
NiR2scorr4A.mat0 = V_pdw(1).mat; | |
NiR2scorr4A.descrip = 'R2* bias correction factor for A map (T2scorr option)'; | |
fR2scorr4A = spm_file(PR2s,'suffix','_corr4A'); | |
NiR2scorr4A.dat = file_array(fR2scorr4A,dm,dt, 0,1,0); | |
create(NiR2scorr4A); | |
NiR2scorr4A.dat(:,:,:) = R2scorr4A; | |
% apply correction | |
NiAcorr = nifti; | |
NiAcorr.mat = V_pdw(1).mat; | |
NiAcorr.mat0 = V_pdw(1).mat; | |
NiAcorr.descrip = 'R2* bias corrected A map (T2scorr option)'; | |
fAcorr = spm_file(fA,'suffix','_R2scorr'); | |
NiAcorr.dat = file_array(fAcorr,dm,dt, 0,1,0); | |
create(NiAcorr); | |
tmp = spm_read_vols(spm_vol(fA))./(R2scorr4A+eps); | |
tmp(isnan(tmp)|isinf(tmp)) = 0; | |
tmp = max(min(tmp,threshall.A),-threshall.A); | |
NiAcorr.dat(:,:,:) = tmp; | |
fA = fAcorr; | |
end | |
% PD map calculation continued | |
if ~isempty(f_T) && (mpm_params.UNICORT.PD || ~mpm_params.UNICORT.R1) | |
% if calibration enabled, do the Unified Segmentation bias correction | |
% if required and calibrate the PD map | |
if PDproc.calibr | |
PDcalculation(fA,fTPM,mpm_params); | |
end | |
end | |
% copy final result files into Results directory | |
% NB: to avoid ambiguity for users, only the 4 final maps to be used in | |
% further analysis are in Results, all other files (additional maps, | |
% processing parameters, etc) are in Results/Supplementary. | |
if ~isempty(fR1) | |
fR1_final = fullfile(respath, spm_file(fR1,'filename')); | |
try copyfile(fR1,fR1_final); end | |
try copyfile([spm_str_manip(fR1,'r') '.json'],[spm_str_manip(fR1_final,'r') '.json']); end %#ok<*TRYNC> | |
fR1 = fR1_final; | |
end | |
if ~isempty(fR2s) | |
fR2s_final = fullfile(respath, spm_file(fR2s,'filename')); | |
copyfile(fR2s,fR2s_final); | |
try copyfile([spm_str_manip(fR2s,'r') '.json'],[spm_str_manip(fR2s_final,'r') '.json']); end | |
fR2s = fR2s_final; | |
end | |
% NB: if OLS calculation of R2s map has been done, the output file for R2s | |
% map is the OLS result. In that case, the basic R2s map is moved to | |
% Results/Supplementary while the R2s_OLS is copied into results directory: | |
if mpm_params.proc.R2sOLS && ~isempty(fR2s_OLS) | |
% move basic R2s map to Results/Supplementary | |
movefile(fR2s_final, fullfile(supplpath, spm_file(fR2s_final,'filename'))); | |
try movefile([spm_str_manip(fR2s_final,'r') '.json'],fullfile(supplpath, [spm_file(fR2s_final,'basename') '.json'])); end | |
% copy OLS_R2s map to Results | |
fR2s_OLS_final = fullfile(respath, spm_file(fR2s_OLS,'filename')); | |
copyfile(fR2s_OLS,fR2s_OLS_final); | |
try copyfile([spm_str_manip(fR2s_OLS,'r') '.json'],[spm_str_manip(fR2s_OLS_final,'r') '.json']); end | |
% the hmri_create_MTProt fR2s output in now the R2s_OLS map | |
fR2s = fR2s_OLS_final; | |
end | |
if ~isempty(fMT) | |
fMT_final = fullfile(respath, spm_file(fMT,'filename')); | |
copyfile(fMT,fMT_final); | |
try copyfile([spm_str_manip(fMT,'r') '.json'],[spm_str_manip(fMT_final,'r') '.json']); end | |
fMT = fMT_final; | |
end | |
if ~isempty(fA) | |
fA_final = fullfile(respath, spm_file(fA,'filename')); | |
copyfile(fA,fA_final); | |
try copyfile([spm_str_manip(fA,'r') '.json'],[spm_str_manip(fA_final,'r') '.json']); end | |
fA = fA_final; | |
end | |
PPDw_final = fullfile(supplpath, spm_file(Pavg{PDwidx},'filename')); | |
copyfile(Pavg{PDwidx},PPDw_final); | |
try copyfile([spm_str_manip(Pavg{PDwidx},'r') '.json'],[spm_str_manip(PPDw_final,'r') '.json']); end | |
PPDw = PPDw_final; | |
if T1widx | |
PT1w_final = fullfile(supplpath, spm_file(Pavg{T1widx},'filename')); | |
copyfile(Pavg{T1widx},PT1w_final); | |
try copyfile([spm_str_manip(Pavg{T1widx},'r') '.json'],[spm_str_manip(PT1w_final,'r') '.json']); end | |
PT1w = PT1w_final; | |
else | |
PT1w = ''; | |
end | |
if MTwidx | |
PMTw_final = fullfile(supplpath, spm_file(Pavg{MTwidx},'filename')); | |
copyfile(Pavg{MTwidx},PMTw_final); | |
try copyfile([spm_str_manip(Pavg{MTwidx},'r') '.json'],[spm_str_manip(PMTw_final,'r') '.json']); end | |
PMTw = PMTw_final; | |
else | |
PMTw = ''; | |
end | |
% save processing params (mpm_params) | |
spm_jsonwrite(fullfile(supplpath,'hMRI_map_creation_mpm_params.json'),mpm_params,struct('indent','\t')); | |
spm_progress_bar('Clear'); | |
hmri_log(sprintf('\t============ MPM PROCESSING: completed (%s) ============', datestr(now)),mpm_params.nopuflags); | |
end | |
%% =======================================================================% | |
% To coregister the structural images for MT protocol | |
%=========================================================================% | |
function [x] = coreg_mt(P_ref, P_src) | |
for src_nr = 1:size(P_src,1) | |
VG = spm_vol(P_ref); | |
VF = spm_vol(P_src(src_nr,:)); | |
coregflags.sep = [4 2]; | |
x = spm_coreg(VG,VF, coregflags); | |
M = inv(spm_matrix(x)); | |
MM = spm_get_space(deblank(VF.fname)); | |
spm_get_space(deblank(deblank(VF.fname)), M*MM); %#ok<*MINV> | |
end | |
end | |
%% =======================================================================% | |
% To coregister the B1 or receive maps with the anatomical images in the | |
% MT protocol. | |
%=========================================================================% | |
function [] = coreg_bias_map(P_ref, P_src) | |
P_src(1,:); | |
VG = spm_vol(P_ref); | |
VF = spm_vol(P_src(1,:)); | |
%coregflags.sep = [2 1]; | |
coregflags.sep = [4 2]; | |
x = spm_coreg(VG,VF, coregflags); | |
%x = spm_coreg(mireg(i).VG, mireg(i).VF,flags.estimate); | |
M = inv(spm_matrix(x)); | |
MM = spm_get_space(deblank(VF.fname)); | |
spm_get_space(deblank(deblank(VF.fname)), M*MM); | |
VF2 = spm_vol(P_src(2,:)); % now also apply transform to the map | |
M = inv(spm_matrix(x)); | |
MM = spm_get_space(deblank(VF2.fname)); | |
spm_get_space(deblank(deblank(VF2.fname)), M*MM); | |
end | |
%% =======================================================================% | |
% Proton density map calculation | |
%=========================================================================% | |
function PDcalculation(fA, fTPM, mpm_params) | |
% fA is the filename of the output A image | |
% (not yet properly quantitative PD map when it enters PDcalculation) | |
% fTMP is the list of TPMs generated from MT map | |
hmri_log(sprintf('\t-------- Proton density map calculation --------'), mpm_params.nopuflags); | |
PDproc = mpm_params.proc.PD; | |
threshA = mpm_params.proc.threshall.A; | |
calcpath = mpm_params.calcpath; | |
TPMs = spm_read_vols(spm_vol(fTPM)); | |
WBmask = zeros(size(squeeze(TPMs(:,:,:,1)))); | |
WBmask(sum(cat(4,TPMs(:,:,:,1:2),TPMs(:,:,:,end)),4)>=PDproc.WBMaskTh) = 1; | |
WMmask=zeros(size(squeeze(TPMs(:,:,:,1)))); | |
WMmask(squeeze(TPMs(:,:,:,2))>=PDproc.WMMaskTh) = 1; | |
% Save masked A map for bias-field correction later | |
V_maskedA = spm_vol(fA); | |
V_maskedA.fname = fullfile(calcpath,['masked_' spm_str_manip(V_maskedA.fname,'t')]); | |
maskedA = spm_read_vols(spm_vol(fA)).*WBmask; | |
maskedA(maskedA==Inf) = 0; | |
maskedA(isnan(maskedA)) = 0; | |
maskedA(maskedA==threshA) = 0; | |
spm_write_vol(V_maskedA,maskedA); | |
% Bias-field correction of masked A map | |
% use unified segmentation with uniform defaults across the toolbox: | |
if isfield(mpm_params.proc.RFsenscorr,'RF_us') | |
job_bfcorr = hmri_get_defaults('segment'); | |
job_bfcorr.channel.vols = {V_maskedA.fname}; | |
job_bfcorr.channel.biasreg = PDproc.biasreg; | |
job_bfcorr.channel.biasfwhm = PDproc.biasfwhm; | |
job_bfcorr.channel.write = [1 0]; % need the BiasField, obviously! | |
for ctis=1:length(job_bfcorr.tissue) | |
job_bfcorr.tissue(ctis).native = [0 0]; % no need to write c* volumes | |
end | |
output_list = spm_preproc_run(job_bfcorr); | |
% Bias field correction of A map. | |
% Bias field calculation is based on the masked A map, while correction | |
% must be applied to the unmasked A map. The BiasField is therefore | |
% retrieved from previous step and applied onto the original A map. | |
BFfnam = output_list.channel.biasfield{1}; | |
BF = double(spm_read_vols(spm_vol(BFfnam))); | |
Y = BF.*spm_read_vols(spm_vol(fA)); | |
else | |
Y = spm_read_vols(spm_vol(fA)); | |
end | |
% Calibration of flattened A map to % water content using typical white | |
% matter value from the litterature (69%) | |
A_WM = WMmask.*Y; | |
Y = Y/mean(A_WM(A_WM~=0))*69; | |
hmri_log(sprintf(['INFO (PD calculation):\n\tmean White Matter intensity: %.1f\n' ... | |
'\tSD White Matter intensity %.1f\n'],mean(A_WM(A_WM~=0)),std(A_WM(A_WM~=0))), mpm_params.defflags); | |
Y(Y>200) = 0; | |
% MFC: Estimating Error for data set to catch bias field issues: | |
errorEstimate = std(A_WM(A_WM > 0))./mean(A_WM(A_WM > 0)); | |
Vsave = spm_vol(fA); | |
Vsave.descrip = [Vsave.descrip '. Error Estimate: ', num2str(errorEstimate)]; | |
if errorEstimate > 0.06 %#ok<BDSCI> | |
% MFC: Testing on 15 subjects showed 6% is a good cut-off: | |
hmri_log(sprintf(['WARNING: Error estimate is high (%.1f%%) for calculated PD map:\n%s' ... | |
'\nError higher than 6%% may indicate motion.\n'], errorEstimate*100, Vsave.fname), mpm_params.defflags); | |
end | |
if mpm_params.QA.enable | |
if exist(mpm_params.QA.fnam,'file') | |
mpm_params.QA = spm_jsonread(mpm_params.QA.fnam); | |
end | |
mpm_params.QA.PD.mean = mean(A_WM(A_WM > 0)); | |
mpm_params.QA.PD.SD = std(A_WM(A_WM > 0)); | |
spm_jsonwrite(mpm_params.QA.fnam,mpm_params.QA,struct('indent','\t')); | |
end | |
spm_write_vol(Vsave,Y); | |
end | |
%% =======================================================================% | |
% Sort out all parameters required for the MPM calculation. Check whether | |
% input data are coherent. Retrieves default parameters from the | |
% hmri_get_defaults. | |
%=========================================================================% | |
function mpm_params = get_mpm_params(jobsubj) | |
% save SPM version (slight differences may appear in the results depending | |
% on the SPM version!) | |
[v,r] = spm('Ver'); | |
mpm_params.SPMver = sprintf('%s (%s)', v, r); | |
% flags for logging information and warnings | |
mpm_params.defflags = jobsubj.log.flags; % default flags | |
mpm_params.nopuflags = jobsubj.log.flags; % force no Pop-Up | |
mpm_params.nopuflags.PopUp = false; | |
hmri_log(sprintf('\t------------ PROCESSING PARAMETERS: SETTING UP AND CONSISTENCY CHECK ------------'),mpm_params.nopuflags); | |
% global parameters | |
mpm_params.json = hmri_get_defaults('json'); | |
mpm_params.centre = hmri_get_defaults('centre'); | |
mpm_params.calcpath = jobsubj.path.mpmpath; | |
mpm_params.respath = jobsubj.path.respath; | |
mpm_params.supplpath = jobsubj.path.supplpath; | |
mpm_params.QA.enable = hmri_get_defaults('qMRI_maps.QA'); % quality assurance | |
if mpm_params.QA.enable | |
mpm_params.QA.fnam = fullfile(mpm_params.supplpath,'hMRI_map_creation_quality_assessment.json'); | |
spm_jsonwrite(mpm_params.QA.fnam,mpm_params.QA,struct('indent','\t')); | |
end | |
mpm_params.ACPCrealign = hmri_get_defaults('qMRI_maps.ACPCrealign'); % realigns qMRI maps to MNI | |
mpm_params.interp = hmri_get_defaults('interp'); | |
mpm_params.fullOLS = hmri_get_defaults('fullOLS'); % uses all echoes for OLS fit at TE=0 | |
mpm_params.neco4R2sfit = hmri_get_defaults('neco4R2sfit'); % minimum number of echoes for R2* calculation | |
if mpm_params.neco4R2sfit<2 | |
hmri_log(sprintf(['WARNING: the (strict) minimum number of echoes required to ' ... | |
'\ncalculate R2* map is 2. The default value (%d) has been modified ' ... | |
'\nand is now neco4R2sfit = 2.']),mpm_params.defflags); | |
mpm_params.neco4R2sfit = 2; | |
end | |
% UNICORT settings: | |
mpm_params.UNICORT.R1 = isfield(jobsubj.b1_type,'UNICORT'); % uses UNICORT to estimate B1 transmit bias | |
tmp = hmri_get_defaults('UNICORT'); | |
mpm_params.UNICORT.PD = tmp.PD; % uses B1map estimated as biasfield for R1 to correct for B1 transmit bias in PD | |
mpm_params.UNICORT.MT = tmp.MT; % uses B1map estimated as biasfield for R1 to correct for B1 transmit bias in MT | |
LogMsg = ''; | |
if mpm_params.UNICORT.R1 | |
LogMsg = sprintf(['%s\nINFO: B1 transmit field estimated using UNICORT ' ... | |
'\nand applied to correct R1 maps for B1 transmit bias.'], LogMsg); | |
flags = mpm_params.nopuflags; | |
end | |
if mpm_params.UNICORT.PD | |
LogMsg = sprintf(['%s\nWARNING: UNICORT B1+ estimate (if available) will be ' ... | |
'\nused to correct the PD map for B1 transmit bias. This method has ' ... | |
'\nnot been validated. Use with care!'], LogMsg); | |
flags = mpm_params.defflags; | |
end | |
if mpm_params.UNICORT.MT | |
LogMsg = sprintf(['%s\nWARNING: UNICORT B1+ estimate (if available) will be ' ... | |
'\nused to correct the MT map for high-order B1 transmit bias. This ' ... | |
'\nmethod has not been validated. Use with care!'], LogMsg); | |
flags = mpm_params.defflags; | |
end | |
if ~isempty(LogMsg) | |
hmri_log(LogMsg,flags); | |
end | |
% check consistency in UNICORT settings: | |
if ~mpm_params.UNICORT.R1 | |
if mpm_params.UNICORT.PD | |
hmri_log(sprintf(['WARNING: no UNICORT B1 estimate available for PD calculation.' ... | |
'\nB1 bias correction must be defined as "UNICORT" (not the case).' ... | |
'\nUNICORT.PD is disabled!']),mpm_params.defflags); | |
mpm_params.UNICORT.PD = false; | |
end | |
if mpm_params.UNICORT.MT | |
hmri_log(sprintf(['WARNING: no UNICORT B1 estimate available for MT calculation.' ... | |
'\nB1 bias correction must be defined as "UNICORT" (not the case).' ... | |
'\nUNICORT.MT is disabled!']),mpm_params.defflags); | |
mpm_params.UNICORT.MT = false; | |
end | |
end | |
% retrieve input file names for map creation. | |
% the "mpm_params.input" field is an array, each element corresponds to a | |
% contrast. | |
% if no input files for a given contrast, no input entry created and | |
% warning is thrown. | |
ccon = 0; | |
LogMsg = 'INFO: FLASH echoes loaded for each contrast are: '; | |
% 1) try MTw contrast: | |
tmpfnam = char(jobsubj.raw_mpm.MT); % P_mtw | |
if isempty(tmpfnam) | |
LogMsg = sprintf('%s\n\t- WARNING: no MT-weighted FLASH echoes available!',LogMsg); | |
mpm_params.MTwidx = 0; % zero index means no contrast available | |
else | |
ccon = ccon+1; | |
LogMsg = sprintf('%s\n\t- MT-weighted: %d echoes',LogMsg,size(tmpfnam,1)); | |
mpm_params.input(ccon).fnam = tmpfnam; | |
mpm_params.input(ccon).tag = 'MT'; | |
mpm_params.MTwidx = ccon; | |
end | |
% 2) try PDw contrast: | |
tmpfnam = char(jobsubj.raw_mpm.PD); % P_pdw | |
if isempty(tmpfnam) | |
LogMsg = sprintf('%s\n\t- WARNING: no PD-weighted FLASH echoes available! \n\t\tThe map creation won''t proceed!',LogMsg); | |
mpm_params.PDwidx = 0; % zero index means no contrast available | |
else | |
ccon = ccon+1; | |
LogMsg = sprintf('%s\n\t- PD-weighted: %d echoes',LogMsg,size(tmpfnam,1)); | |
mpm_params.input(ccon).fnam = tmpfnam; | |
mpm_params.input(ccon).tag = 'PD'; | |
mpm_params.PDwidx = ccon; | |
end | |
% 3) try T1w contrast: | |
tmpfnam = char(jobsubj.raw_mpm.T1); % P_t1w | |
if isempty(tmpfnam) | |
LogMsg = sprintf('%s\n\t- WARNING: no T1-weighted FLASH echoes available!',LogMsg); | |
mpm_params.T1widx = 0; % zero index means no contrast available | |
else | |
ccon = ccon+1; | |
LogMsg = sprintf('%s\n\t- T1-weighted: %d echoes',LogMsg,size(tmpfnam,1)); | |
mpm_params.input(ccon).fnam = tmpfnam; | |
mpm_params.input(ccon).tag = 'T1'; | |
mpm_params.T1widx = ccon; % zero index means no contrast available | |
end | |
mpm_params.ncon = ccon; % number of contrasts available | |
% Message displayed as pop-up if enabled since it is an important | |
% information | |
hmri_log(LogMsg, mpm_params.defflags); | |
% collect TE, TR and FA for each available contrast | |
for ccon = 1:mpm_params.ncon | |
p = get_trtefa(mpm_params.input(ccon).fnam); | |
if ~all(cellfun('isempty',{p.tr p.te p.fa})) | |
mpm_params.input(ccon).TE = cat(1,p.te); | |
mpm_params.input(ccon).TR = p(1).tr; | |
mpm_params.input(ccon).fa = p(1).fa; | |
else | |
hmri_log(sprintf('WARNING: No TE/TR/FA values found for %sw images. Fallback to defaults.',mpm_params.input(ccon).tag),mpm_params.defflags); | |
MPMacq = hmri_get_defaults('MPMacq'); | |
mpm_params.input(ccon).TE = MPMacq.(['TE_' lower(mpm_params.input(ccon).tag) 'w']); | |
mpm_params.input(ccon).TR = MPMacq.(['TR_' lower(mpm_params.input(ccon).tag) 'w']); | |
mpm_params.input(ccon).fa = MPMacq.(['fa_' lower(mpm_params.input(ccon).tag) 'w']); | |
end | |
end | |
% check that echo times are identical (common echoes only) | |
% NOTE: only necessary when not using the TE=0 extrapolation | |
if ~mpm_params.fullOLS | |
for ccon = 1:mpm_params.ncon-1 | |
TEcon1 = mpm_params.input(ccon).TE; | |
TEcon2 = mpm_params.input(ccon+1).TE; | |
for necho = 1:min(length(TEcon1),length(TEcon2)) | |
if ~(TEcon1(necho)==TEcon2(necho)) | |
hmri_log('ERROR: Echo times do not match between contrasts! Aborting.',mpm_params.defflags); | |
error('Echo times do not match between contrasts! Aborting.'); | |
end | |
end | |
end | |
end | |
% maximum TE for averaging (ms) | |
maxTEval4avg = 30; | |
% find maximum number of echoes that are common to all available contrasts | |
ncommonTEvals = 1000; | |
for ccon = 1:mpm_params.ncon | |
ncommonTEvals = min(length(mpm_params.input(ccon).TE),ncommonTEvals); | |
end | |
% find maximum number of echoes that are common to all available contrasts | |
% AND one more than the maxTEval4avg: | |
mpm_params.nr_echoes4avg = min(length(find(mpm_params.input(1).TE<maxTEval4avg))+1,ncommonTEvals); | |
hmri_log(sprintf('INFO: averaged PDw/T1w/MTw will be calculated based on the first %d echoes.',mpm_params.nr_echoes4avg),mpm_params.nopuflags); | |
% if T1w and PDw data available, identify the protocol to define imperfect | |
% spoiling correction parameters (for T1 map calculation) | |
ISC = hmri_get_defaults('imperfectSpoilCorr.enabled'); | |
if ~ISC | |
hmri_log(sprintf(['INFO: Imperfect spoiling correction is disabled.' ... | |
'\nIf your data were acquired with one of the standard MPM ' ... | |
'\nprotocols (customized MT-FLASH sequence) for which the correction ' ... | |
'\ncoefficients are available, it is recommended to enable that option.']),mpm_params.defflags); | |
end | |
if mpm_params.PDwidx && mpm_params.T1widx && ISC | |
% retrieve all available protocols: | |
MPMacq_sets = hmri_get_defaults('MPMacq_set'); | |
% current protocol is defined by [TR_pdw TR_t1w fa_pdw fa_t1w]: | |
MPMacq_prot = [mpm_params.input(mpm_params.PDwidx).TR; | |
mpm_params.input(mpm_params.T1widx).TR; | |
mpm_params.input(mpm_params.PDwidx).fa; | |
mpm_params.input(mpm_params.T1widx).fa]'; | |
% then match the values and find protocol tag | |
nsets = numel(MPMacq_sets.vals); | |
ii = 0; mtch = false; | |
while ~mtch && ii < nsets | |
ii = ii+1; | |
if all(MPMacq_prot == MPMacq_sets.vals{ii}) | |
mtch = true; | |
prot_tag = MPMacq_sets.tags{ii}; | |
hmri_log(sprintf(['INFO: MPM acquisition protocol = %s.' ... | |
'\n\tThe coefficients corresponding to this protocol will be applied' ... | |
'\n\tto correct for imperfect spoiling. Please check carefully that' ... | |
'\n\tthe protocol used is definitely the one for which the ' ... | |
'\n\tcoefficients have been calculated.'],prot_tag),mpm_params.defflags); | |
end | |
end | |
if ~mtch | |
prot_tag = 'Unknown'; | |
hmri_log(sprintf(['WARNING: MPM protocol unknown. ' ... | |
'\n\tCorrection for imperfect spoiling will not be applied.']),mpm_params.defflags); | |
end | |
else | |
prot_tag = 'Unknown'; | |
end | |
% now retrieve imperfect spoiling correction coefficients | |
mpm_params.proc.ISC = hmri_get_defaults(['imperfectSpoilCorr.',prot_tag]); | |
% RF sensitivity bias correction | |
mpm_params.proc.RFsenscorr = jobsubj.sensitivity; | |
% other processing parameters from defaults | |
% load threshold to save qMRI maps | |
mpm_params.proc.threshall = hmri_get_defaults('qMRI_maps_thresh'); | |
% load PD maps processing parameters (including calibr (calibration | |
% parameter) and T2scorr (T2s correction) fields) | |
mpm_params.proc.PD = hmri_get_defaults('PDproc'); | |
% if no RF sensitivity bias correction or no B1 transmit bias correction | |
% applied, not worth trying any calibration: | |
if (isfield(mpm_params.proc.RFsenscorr,'RF_none')||(isempty(jobsubj.b1_trans_input)&&~mpm_params.UNICORT.PD)) && mpm_params.proc.PD.calibr | |
hmri_log(sprintf(['WARNING: both RF sensitivity and B1 transmit bias corrections '... | |
'\nare required to generate a quantitative (calibrated) PD map.' ... | |
'\nEither or both of these is missing. Therefore an amplitude ' ... | |
'\n"A" map will be output instead of a quantitative ' ... | |
'\nPD map. PD map calibration has been disabled.']),mpm_params.defflags); | |
mpm_params.proc.PD.calibr = 0; | |
end | |
% if fullOLS, T2*-weighting bias correction must not be applied | |
if mpm_params.fullOLS && mpm_params.proc.PD.T2scorr | |
hmri_log(sprintf(['WARNING: if TE=0 fit is enabled (fullOLS option), ' ... | |
'\nno T2*-weighting bias correction is required. \nT2scorr option disabled.']),mpm_params.defflags); | |
mpm_params.proc.PD.T2scorr = 0; | |
end | |
% T2*-weighting bias correction must always be applied somehow. If neither | |
% the fullOLS nor the T2scorr options are enabled, we don't force it to be | |
% applied (could still be usefull for comparison purposes) but throw a | |
% warning: | |
if ~mpm_params.fullOLS && ~mpm_params.proc.PD.T2scorr | |
hmri_log(sprintf(['WARNING: both TE=0 fit (fullOLS option) and T2*-weighting ' ... | |
'\nbias correction (T2scorr option) are disabled. The T2* bias won''t ' ... | |
'\nbe corrected for and impact the resulting PD map. It is strongly ' ... | |
'\nrecommended to have either of these options enabled!' ... | |
'\nNOTE: this recommendation does not apply to single-echo datasets.']),mpm_params.defflags); | |
end | |
% whether OLS R2* is calculated | |
mpm_params.proc.R2sOLS = hmri_get_defaults('R2sOLS'); | |
% check whether there are enough echoes (neco4R2sfit) to estimate R2* | |
% (1) for basic R2* estimation, check only PDw images | |
mpm_params.basicR2s = false; | |
if mpm_params.PDwidx | |
if length(mpm_params.input(mpm_params.PDwidx).TE) > (mpm_params.neco4R2sfit-1) | |
mpm_params.basicR2s = true; | |
end | |
end | |
% (2) for ESTATICS R2* estimation, check which (if any) contrast is usable | |
mpm_params.estaticsR2s = false*ones(1,mpm_params.ncon); | |
if mpm_params.proc.R2sOLS | |
for ccon = 1:mpm_params.ncon | |
mpm_params.estaticsR2s(ccon) = length(mpm_params.input(ccon).TE)>(mpm_params.neco4R2sfit-1); | |
end | |
end | |
% if T2scorr enabled, must check there will be a R2s map generated! | |
if mpm_params.proc.PD.T2scorr && ~(any(mpm_params.estaticsR2s)||mpm_params.basicR2s) | |
hmri_log(sprintf(['WARNING: not enough echoes available (minimum is %d) to ' ... | |
'\ncalculate R2* map. No T2*-weighting bias correction can be ' ... | |
'\napplied (T2scorr option). T2scorr disabled.']),mpm_params.defflags); | |
mpm_params.proc.PD.T2scorr = 0; | |
end | |
% consistency check for number of echoes averaged for A calculation: | |
if mpm_params.PDwidx && mpm_params.T1widx | |
if mpm_params.proc.PD.nr_echoes_forA > size(mpm_params.input(mpm_params.T1widx).fnam,1) | |
hmri_log(sprintf(['WARNING: number of T1w echoes to be averaged for PD calculation (%d)' ... | |
'\nis bigger than the available number of echoes (%d). Setting nr_echoes_forA' ... | |
'\nto %d, the maximum number of echoes available.'], mpm_params.proc.PD.nr_echoes_forA, ... | |
size(mpm_params.input(mpm_params.T1widx).fnam,1), ... | |
size(mpm_params.input(mpm_params.T1widx).fnam,1)),mpm_params.defflags); | |
mpm_params.proc.PD.nr_echoes_forA = size(mpm_params.input(mpm_params.T1widx).fnam,1); | |
end | |
end | |
% coregistration of all images to the PDw average (or TE=0 fit): | |
mpm_params.coreg = hmri_get_defaults('coreg2PDw'); | |
% Prepare output for R1, PD, MT and R2* maps | |
RFsenscorr = fieldnames(mpm_params.proc.RFsenscorr); | |
B1transcorr = fieldnames(jobsubj.b1_type); | |
coutput = 0; | |
% R1 output: requires PDw and T1w input and optional B1 transmit (or | |
% UNICORT) and RF sensitivity maps for bias correction: | |
if (mpm_params.T1widx && mpm_params.PDwidx) | |
coutput = coutput+1; | |
mpm_params.qR1 = coutput; | |
mpm_params.output(coutput).suffix = 'R1'; | |
mpm_params.output(coutput).units = 's-1'; | |
mpm_params.output(coutput).descrip{1} = 'R1 map [s-1]'; | |
if mpm_params.proc.ISC.enabled | |
mpm_params.output(coutput).descrip{end+1} = '- Imperfect Spoiling Correction applied'; | |
else | |
mpm_params.output(coutput).descrip{end+1} = '- no Imperfect Spoiling Correction applied'; | |
end | |
switch B1transcorr{1} | |
case 'no_B1_correction' | |
mpm_params.output(coutput).descrip{end+1} = '- no B1+ bias correction applied'; | |
case 'UNICORT' | |
mpm_params.output(coutput).descrip{end+1} = '- B1+ bias correction using UNICORT'; | |
otherwise | |
mpm_params.output(coutput).descrip{end+1} = sprintf('- B1+ bias correction using provided B1 map (%s)',B1transcorr{1}); | |
end | |
switch RFsenscorr{1} | |
case 'RF_none' | |
mpm_params.output(coutput).descrip{end+1} = '- no RF sensitivity bias correction'; | |
case 'RF_us' % Unified Segmentation only applies to PD maps | |
mpm_params.output(coutput).descrip{end+1} = '- no RF sensitivity bias correction'; | |
case 'RF_once' | |
mpm_params.output(coutput).descrip{end+1} = '- RF sensitivity bias correction based on a single sensitivity measurement'; | |
case 'RF_per_contrast' | |
mpm_params.output(coutput).descrip{end+1} = '- RF sensitivity bias correction based on a per-contrast sensitivity measurement'; | |
end | |
else | |
mpm_params.qR1 = 0; | |
end | |
% PD output: requires PDw and T1w input: | |
if (mpm_params.T1widx && mpm_params.PDwidx) | |
coutput = coutput+1; | |
mpm_params.qPD = coutput; | |
if mpm_params.proc.PD.calibr && ... | |
~isfield(mpm_params.proc.RFsenscorr,'RF_none') && ... | |
(~isempty(jobsubj.b1_trans_input)|| mpm_params.UNICORT.PD) | |
mpm_params.output(coutput).suffix = 'PD'; | |
mpm_params.output(coutput).descrip{1} = 'PD map (water concentration) [p.u.]'; | |
mpm_params.output(coutput).descrip{end+1} = '- WM calibration (69%)'; | |
mpm_params.output(coutput).units = 'p.u.'; | |
else | |
mpm_params.output(coutput).suffix = 'A'; | |
mpm_params.output(coutput).descrip{1} = 'A map (signal amplitude) [a.u.]'; | |
mpm_params.output(coutput).descrip{end+1} = '- no WM calibration (69%)'; | |
mpm_params.output(coutput).units = 'a.u.'; | |
end | |
switch B1transcorr{1} | |
case 'no_B1_correction' | |
mpm_params.output(coutput).descrip{end+1} = '- no B1+ bias correction applied'; | |
case 'UNICORT' | |
if mpm_params.UNICORT.PD | |
mpm_params.output(coutput).descrip{end+1} = '- B1+ bias correction: UNICORT-estimated R1 map + UNICORT(R1)-estimated B1 map'; | |
else | |
mpm_params.output(coutput).descrip{end+1} = '- B1+ bias correction: UNICORT-estimated R1 map used for A map calculation'; | |
end | |
otherwise | |
mpm_params.output(coutput).descrip{end+1} = sprintf('- B1+ bias correction using provided B1 map (%s)',B1transcorr{1}); | |
end | |
switch RFsenscorr{1} | |
case 'RF_none' | |
mpm_params.output(coutput).descrip{end+1} = '- no RF sensitivity bias correction'; | |
case 'RF_us' % Unified Segmentation only applies to PD maps | |
if mpm_params.proc.PD.calibr | |
mpm_params.output(coutput).descrip{end+1} = '- RF sensitivity bias correction based on Unified Segmentation'; | |
else | |
mpm_params.output(coutput).descrip{end+1} = '- no RF sensitivity bias correction'; | |
end | |
case 'RF_once' | |
mpm_params.output(coutput).descrip{end+1} = '- RF sensitivity bias correction based on a single sensitivity measurement'; | |
case 'RF_per_contrast' | |
mpm_params.output(coutput).descrip{end+1} = '- RF sensitivity bias correction based on a per-contrast sensitivity measurement'; | |
end | |
if mpm_params.fullOLS | |
mpm_params.output(coutput).descrip{end+1} = '- R2* bias accounted for by TE=0 extrapolation (fullOLS option)'; | |
elseif mpm_params.proc.PD.T2scorr | |
mpm_params.output(coutput).descrip{end+1} = '- R2* bias corrected for (T2scorr option)'; | |
else | |
mpm_params.output(coutput).descrip{end+1} = '- R2* bias not corrected for'; | |
end | |
else | |
mpm_params.qPD = 0; | |
end | |
% MT output: requires all three contrasts | |
if (mpm_params.T1widx && mpm_params.PDwidx && mpm_params.MTwidx) | |
coutput = coutput+1; | |
mpm_params.qMT = coutput; | |
mpm_params.output(coutput).suffix = 'MTsat'; | |
mpm_params.output(coutput).descrip{1} = 'MT saturation map [p.u.]'; | |
mpm_params.output(coutput).units = 'p.u.'; | |
switch B1transcorr{1} | |
case 'no_B1_correction' | |
mpm_params.output(coutput).descrip{end+1} = '- no B1+ bias correction applied'; | |
case 'UNICORT' | |
if mpm_params.UNICORT.MT | |
mpm_params.output(coutput).descrip{end+1} = '- B1+ bias correction: UNICORT(R1)-estimated B1 map'; | |
else | |
mpm_params.output(coutput).descrip{end+1} = '- no B1+ bias correction applied'; | |
end | |
otherwise | |
mpm_params.output(coutput).descrip{end+1} = sprintf('- B1+ bias correction using provided B1 map (%s)',B1transcorr{1}); | |
end | |
switch RFsenscorr{1} | |
case 'RF_none' | |
mpm_params.output(coutput).descrip{end+1} = '- no RF sensitivity bias correction'; | |
case 'RF_us' % Unified Segmentation only applies to PD maps | |
mpm_params.output(coutput).descrip{end+1} = '- no RF sensitivity bias correction'; | |
case 'RF_once' | |
mpm_params.output(coutput).descrip{end+1} = '- RF sensitivity bias correction based on a single sensitivity measurement'; | |
case 'RF_per_contrast' | |
mpm_params.output(coutput).descrip{end+1} = '- RF sensitivity bias correction based on a per-contrast sensitivity measurement'; | |
end | |
else | |
mpm_params.qMT = 0; | |
end | |
if (mpm_params.PDwidx && mpm_params.MTwidx) | |
if (mpm_params.input(mpm_params.MTwidx).TR == mpm_params.input(mpm_params.PDwidx).TR) ... | |
&& (mpm_params.input(mpm_params.MTwidx).fa == mpm_params.input(mpm_params.PDwidx).fa) % additional MTR image... | |
coutput = coutput+1; | |
mpm_params.qMTR = coutput; | |
mpm_params.output(coutput).suffix = 'MTR'; | |
mpm_params.output(coutput).descrip{1} = 'Classic MTR image [a.u.] - not currently saved in Results'; | |
mpm_params.output(coutput).units = 'a.u.'; | |
else | |
mpm_params.qMTR = 0; | |
end | |
else | |
mpm_params.qMTR = 0; | |
end | |
% R2* map: requires a minimum of 4 echoes with PDw contrast | |
if size(mpm_params.input(mpm_params.PDwidx).fnam,1) > (mpm_params.neco4R2sfit-1) | |
coutput = coutput+1; | |
mpm_params.qR2s = coutput; | |
mpm_params.output(coutput).suffix = 'R2s'; | |
mpm_params.output(coutput).descrip{1} = 'R2* map [s-1]'; | |
mpm_params.output(coutput).units = '[s-1]'; | |
if mpm_params.proc.R2sOLS | |
mpm_params.output(coutput).descrip{end+1} = '- ESTATICS model (OLS R2* map calculation)'; | |
end | |
mpm_params.output(coutput).descrip{end+1} = '- B1+ bias correction does not apply'; | |
switch RFsenscorr{1} | |
case 'RF_none' | |
mpm_params.output(coutput).descrip{end+1} = '- no RF sensitivity bias correction'; | |
case 'RF_us' % Unified Segmentation only applies to PD maps | |
mpm_params.output(coutput).descrip{end+1} = '- no RF sensitivity bias correction'; | |
case 'RF_once' | |
mpm_params.output(coutput).descrip{end+1} = '- RF sensitivity bias correction based on a single sensitivity measurement'; | |
case 'RF_per_contrast' | |
mpm_params.output(coutput).descrip{end+1} = '- RF sensitivity bias correction based on a per-contrast sensitivity measurement'; | |
end | |
else | |
mpm_params.qR2s = 0; | |
end | |
% Summary of the output generated: | |
LogMsg = 'SUMMARY OF THE MAPS CALCULATED'; | |
for coutput = 1:length(mpm_params.output) | |
LogMsg = sprintf('%s\n\n(%d) %s (%s)',LogMsg, coutput, mpm_params.output(coutput).descrip{1} , mpm_params.output(coutput).suffix); | |
for cdescrip = 2:length(mpm_params.output(coutput).descrip) | |
LogMsg = sprintf('%s\n %s',LogMsg, mpm_params.output(coutput).descrip{cdescrip}); | |
end | |
end | |
hmri_log(LogMsg, mpm_params.nopuflags); | |
end | |
%% =======================================================================% | |
% To arrange the metadata structure for MPM calculation output. | |
%=========================================================================% | |
function metastruc = init_mpm_output_metadata(input_files, mpm_params) | |
proc.descrip = ['hMRI toolbox - ' mfilename '.m - map calculation']; | |
proc.version = hmri_get_version; | |
proc.params = mpm_params; | |
% must be defined on the spot, default values here | |
output.imtype = 'MPM'; | |
output.units = 'a.u.'; | |
metastruc = init_output_metadata_structure(input_files, proc, output); | |
end | |
%% =======================================================================% | |
% To extract the TR/TE/FA values out of the descriptor field of | |
% the nifti header or the extended header/JSON metadata. | |
%=========================================================================% | |
function p = get_trtefa(P) | |
N = nifti(P); | |
nN = numel(N); | |
p(nN) = struct('tr',[],'te',[],'fa',[]); | |
for ii = 1:numel(N) | |
p(ii).tr = get_metadata_val(P(ii,:),'RepetitionTime'); | |
p(ii).te = get_metadata_val(P(ii,:),'EchoTime'); | |
p(ii).fa = get_metadata_val(P(ii,:),'FlipAngle'); | |
end | |
end |