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?
Toolbox/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.
1257 lines (1109 sloc)
50 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, P_trans, P_receiv) %#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, P_trans, P_receiv) | |
% | |
% 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) | |
% P_receive filenames of a pair of magnitude image + sensitivity map of | |
% phased-array coil relative to BC [p.u.] (not mandatory) | |
% | |
% 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 defaults | |
% settings (PDproc.PDmap)). | |
% PPDw averate 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 RF spoiling correction | |
% Preibisch and Deichmann, MRM 61:125-135 (2009) | |
% Corrects for residual transverse magnetization coherences that | |
% remain despite RF 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 RF 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 | |
%=========================================================================% | |
fprintf(1, ['---------------------- MPM PROCESSING ----------------------\n' ... | |
'Create maps from multi-contrast multi-echo FLASH protocol.\n']); | |
% 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: | |
% index number for each contrast - zero index means no images available | |
PDidx = mpm_params.PDidx; | |
T1idx = mpm_params.T1idx; | |
MTidx = mpm_params.MTidx; | |
% 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(PDidx).TE; | |
TR_pdw = mpm_params.input(PDidx).TR; | |
fa_pdw = mpm_params.input(PDidx).fa; | |
if T1idx | |
TE_t1w = mpm_params.input(T1idx).TE; %#ok<NASGU> | |
TR_t1w = mpm_params.input(T1idx).TR; | |
fa_t1w = mpm_params.input(T1idx).fa; | |
end | |
if MTidx | |
TE_mtw = mpm_params.input(MTidx).TE; %#ok<NASGU> | |
TR_mtw = mpm_params.input(MTidx).TR; | |
fa_mtw = mpm_params.input(MTidx).fa; | |
end | |
% other parameters | |
threshall = mpm_params.proc.threshall; | |
PDproc = mpm_params.proc.PD; | |
RFC = mpm_params.proc.RFC; | |
dt = [spm_type('float32'),spm_platform('bigend')]; % for nifti output | |
outbasename = spm_file(mpm_params.input(end).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; | |
%% =======================================================================% | |
% Calculate R2* map from PDw echoes | |
%=========================================================================% | |
fprintf(1,'\n -------- R2* map calculation --------\n'); | |
% load PDw images | |
V_pdw = spm_vol(mpm_params.input(PDidx).fnam); | |
dm = V_pdw(1).dim; | |
spm_progress_bar('Init',dm(3),'R2* fit','planes completed'); | |
% create nifti object for output R2* map | |
Ni = nifti; | |
Ni.mat = V_pdw(1).mat; | |
Ni.mat0 = V_pdw(1).mat; | |
Ni.descrip = 'R2* map [s-1]'; | |
Ni.dat = file_array(fullfile(calcpath,[outbasename '_R2s' '.nii']),dm,dt, 0,1,0); | |
create(Ni); | |
fR2s = fullfile(calcpath,[outbasename '_R2s' '.nii']); | |
% 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(PDidx).fnam; | |
Output_hdr = init_mpm_output_metadata(input_files, mpm_params); | |
Output_hdr.history.output.imtype = 'R2* map'; | |
Output_hdr.history.output.units = 's-1'; | |
set_metadata(fR2s,Output_hdr,mpm_params.json); | |
%% =======================================================================% | |
% Reading and averaging the images | |
%=========================================================================% | |
fprintf(1,'\n -------- Reading and averaging the images --------\n'); | |
% 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); | |
dm = V(1).dim; | |
Ni = nifti; | |
Ni.mat = V(1).mat; | |
Ni.mat0 = V(1).mat; | |
Ni.descrip = sprintf('Averaged %sw images', mpm_params.input(ccon).tag); | |
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 (PDidx && T1idx) | |
PT1w_forA = fullfile(calcpath,[outbasename '_T1w_forA.nii']); | |
V = spm_vol(mpm_params.input(T1idx).fnam); | |
dm = V(1).dim; | |
Ni = nifti; | |
Ni.mat = V(1).mat; | |
Ni.mat0 = V(1).mat; | |
Ni.descrip = 'Averaged T1w images for PD calculation'; | |
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 | |
%=========================================================================% | |
fprintf(1,'\n -------- Coregistering the images --------\n'); | |
x_MT2PD = []; | |
if MTidx; x_MT2PD = coreg_mt(Pavg{PDidx}, Pavg{MTidx}); end | |
x_T12PD = []; | |
if T1idx; | |
x_T12PD = coreg_mt(Pavg{PDidx}, Pavg{T1idx}); | |
coreg_mt(Pavg{PDidx}, PT1w_forA); | |
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.) | |
coreg_bias_map(Pavg{PDidx}, P_trans); | |
V_trans = spm_vol(P_trans); | |
end | |
V_receiv = []; | |
if ~isempty(P_receiv) | |
% Load sensitivity map if available and coregister to PDw | |
% P_receiv(1,:) = magnitude image (anatomical reference for coregistration) | |
% P_receiv(2,:) = sensitivity map | |
coreg_bias_map(Pavg{PDidx}, P_receiv); | |
V_receiv = spm_vol(P_receiv); | |
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(PDidx) = spm_vol(Pavg{PDidx}); | |
if MTidx; Vavg(MTidx) = spm_vol(Pavg{MTidx}); end | |
if T1idx | |
Vavg(T1idx) = spm_vol(Pavg{T1idx}); | |
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 | |
fprintf(1,'\n -------- multi-contrast R2* map calculation for QA --------\n'); | |
for ccon = 1:mpm_params.ncon | |
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(fullfile(calcpath,[outbasename '_R2s_' mpm_params.input(ccon).tag 'w.nii']),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(size(TE)) 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([size(TE,1) dm(1:2)]); | |
for cecho = 1:size(TE,1) | |
% 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, [size(TE,1) 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 | |
%% =======================================================================% | |
% OLS R2* map calculation by MFC | |
% [Reference: ESTATICS, Weiskopf et al. 2014] | |
%=========================================================================% | |
if mpm_params.proc.R2sOLS | |
fprintf(1,'\n -------- OLS R2* map calculation --------\n'); | |
% OLS fit at TE=0: to be used instead of averaged echoes of each | |
% contrast if "fullOLS" option is enabled | |
if mpm_params.fullOLS | |
fprintf(1,'\n -------- and fit to TE=0 for all contrasts --------\n'); | |
Nmap = nifti; | |
Pte0 = cell(1,mpm_params.ncon); | |
for ccon = 1:mpm_params.ncon | |
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', mpm_params.input(ccon).tag); | |
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}); | |
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 | |
nechoes(ccon) = size(mpm_params.input(ccon).fnam,1); | |
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 | |
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 | |
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 | |
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 | |
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 | |
Nmap(ccon).dat(:,:,p) = reshape(exp(Y(ccon,:)), dm(1:2)); | |
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(PDidx).fnam; | |
if (T1idx); input_files = char(input_files, mpm_params.input(T1idx).fnam); end | |
if (MTidx); input_files = char(input_files, mpm_params.input(MTidx).fnam); end | |
Output_hdr = init_mpm_output_metadata(input_files, mpm_params); | |
Output_hdr.history.output.imtype = 'R2*-OLS map'; | |
Output_hdr.history.output.units = 's-1'; | |
set_metadata(fullfile(calcpath,[outbasename '_R2s_OLS' '.nii']),Output_hdr,mpm_params.json); | |
end % OLS code | |
% if "fullOLS" option enabled, 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 | |
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 | |
%=========================================================================% | |
% description fields and file names of output images | |
coutput = 0; | |
if T1idx | |
coutput = coutput+1; | |
output_suffix{coutput} = 'R1'; | |
units{coutput} = 's-1'; | |
if isempty(V_trans) | |
descrip{coutput} = 'R1 map (no B1+ bias correction applied)'; | |
else | |
descrip{coutput} = 'R1 map (with B1+ bias correction) [s-1]'; | |
end | |
R1map_idx = coutput; | |
end | |
if T1idx | |
coutput = coutput+1; | |
if PDproc.PDmap && ~isempty(V_trans) | |
output_suffix{coutput} = 'PD'; | |
descrip{coutput} = 'Water concentration [%]'; | |
units{coutput} = '%'; | |
else | |
output_suffix{coutput} = 'A'; | |
descrip{coutput} = 'Signal amplitude [a.u.]'; | |
units{coutput} = 'a.u.'; | |
end | |
Amap_idx = coutput; | |
end | |
if (MTidx && T1idx) | |
coutput = coutput+1; | |
output_suffix{coutput} = 'MT'; | |
descrip{coutput} = 'Delta MT map'; | |
units{coutput} = 'a.u.'; | |
MTmap_idx = coutput; | |
end | |
if (MTidx && PDidx) | |
if (TR_mtw == TR_pdw) && (fa_mtw == fa_pdw) % additional MTR image... | |
coutput = coutput+1; | |
output_suffix{coutput} = 'MTR'; | |
descrip{coutput} = 'Classic MTR image'; | |
units{coutput} = 'a.u.'; | |
MTRmap_idx = coutput; | |
end | |
end | |
noutput = coutput; | |
% define NIFTI objects for output images | |
Nmap = nifti; | |
for ii=1:noutput | |
dm = V_pdw(1).dim; | |
Ni = nifti; | |
Ni.mat = V_pdw(1).mat; | |
Ni.mat0 = V_pdw(1).mat; | |
Ni.descrip = descrip{ii}; | |
Ni.dat = file_array(fullfile(calcpath,[outbasename '_' output_suffix{ii} '.nii']),dm,dt, 0,1,0); | |
create(Ni); | |
Nmap(ii) = Ni; | |
end | |
fR1 = ''; | |
fA = ''; | |
fMT = ''; | |
if (PDidx && T1idx) | |
fR1 = fullfile(calcpath,[outbasename '_' output_suffix{R1map_idx} '.nii']); | |
fA = fullfile(calcpath,[outbasename '_' output_suffix{Amap_idx} '.nii']); | |
if MTidx | |
fMT = fullfile(calcpath,[outbasename '_' output_suffix{MTmap_idx} '.nii']); | |
end | |
end | |
%% =======================================================================% | |
% Map calculation continued (R1, PD, MT) | |
%=========================================================================% | |
fprintf(1,'\n -------- Map calculation continued (R1, PD, MT) --------\n'); | |
M0 = Ni.mat; | |
dm = size(Ni.dat); | |
fa_pdw_rad = fa_pdw * pi / 180; | |
if MTidx; fa_mtw_rad = fa_mtw * pi / 180; end | |
if T1idx; fa_t1w_rad = fa_t1w * pi / 180; end | |
spm_progress_bar('Init',dm(3),'Calculating maps','planes completed'); | |
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(PDidx),Vavg(PDidx).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 | |
if ~isempty(V_receiv) && ~isempty(V_trans) | |
f_R = spm_slice_vol(V_receiv(2,:),V_receiv(2,:).mat\M,dm(1:2),mpm_params.interp)/100; % divide by 100, since p.u. maps | |
f_R = f_R .* f_T; % f_R is only the sensitivity map and not the true receive bias map, therefore needs to be multiplied by transmit bias (B1+ approx. B1- map) | |
else | |
f_R = []; | |
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 (MTidx && PDidx) | |
MTw = spm_slice_vol(Vavg(MTidx),Vavg(MTidx).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(MTRmap_idx).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 T1idx | |
T1w = spm_slice_vol(Vavg(T1idx),Vavg(T1idx).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 | |
if isempty(f_T) | |
% semi-quantitative T1 | |
T1 = ((PDw / fa_pdw_rad) - (T1w / fa_t1w_rad)) ./ ... | |
max((T1w * (fa_t1w_rad / 2 / TR_t1w)) - (PDw * fa_pdw_rad / 2 / TR_pdw),eps); | |
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 RFC.RFCorr | |
% MFC: We do have P2_a and P2_b parameters for this sequence | |
% => T1 = A(B1) + B(B1)*T1app (see Preibisch 2009) | |
T1 = RFC.P2_a(1)*f_T.^2 + ... | |
RFC.P2_a(2)*f_T + ... | |
RFC.P2_a(3) + ... | |
(RFC.P2_b(1)*f_T.^2+RFC.P2_b(2)*f_T+RFC.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 | |
T1 = max(T1,0); | |
R1(R1<0) = 0; | |
tmp = R1; | |
Nmap(R1map_idx).dat(:,:,p) = min(max(tmp,-threshall.R1),threshall.R1)*0.001; % truncating images | |
% A values proportional to PD | |
if (~isempty(f_T)) && (~isempty(f_R)) | |
% Transmit and receive bias corrected quantitative A values | |
% again: correct A for transmit bias f_T and receive bias f_R | |
% Acorr = A / f_T / f_R , proportional PD | |
A = (T1 .* (T1w_forA * fa_t1w_rad / 2 / TR_t1w) + (T1w_forA / fa_t1w_rad))./f_T./f_R; | |
elseif(~isempty(f_T))&&(isempty(f_R))%&&(PDproc.PDmap) | |
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; | |
Nmap(Amap_idx).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 MTidx | |
MTw = spm_slice_vol(Vavg(MTidx),Vavg(MTidx).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; | |
if (~isempty(f_T)) | |
MT = MT .* (1 - 0.4) ./ (1 - 0.4 * f_T); | |
end | |
tmp = MT; | |
Nmap(MTmap_idx).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(PDidx).fnam; | |
if (T1idx); input_files = char(input_files, mpm_params.input(T1idx).fnam); end | |
if (MTidx); input_files = char(input_files, mpm_params.input(MTidx).fnam); end | |
Output_hdr = init_mpm_output_metadata(input_files, mpm_params); | |
for ctr = 1:noutput | |
Output_hdr.history.output.imtype = descrip(ctr); | |
Output_hdr.history.output.units = units(ctr); | |
set_metadata(fullfile(calcpath,[outbasename '_' output_suffix{ctr} '.nii']),Output_hdr,mpm_params.json); | |
end | |
%% =======================================================================% | |
% ACPC Realign all images - only if MT map created | |
%=========================================================================% | |
if mpm_params.ACPCrealign | |
if (MTidx && PDidx && T1idx) | |
fprintf(1,'\n -------- ACPC Realign all images --------\n'); | |
% 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(PDidx)); | |
% 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,'MPM_map_creation_ACPCrealign_transformation_matrix.json'),R,struct('indent','\t')); | |
else | |
fprintf(1,['\nWARNING: 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.\n']); | |
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.PDmap)) && (PDidx && T1idx) | |
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') | |
% 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(spm_select('FPList',calcpath,sprintf('^s.*_R2s_%sw.nii$',mpm_params.input(ccon).tag)))); | |
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 | |
if ~isempty(f_T) && isempty(f_R) && PDproc.PDmap | |
% for correction of the R2s bias in the A map if that option is enabled... | |
if PDproc.T2scorr | |
% uses OLS it if available - less noisy | |
if exist('fR2s_OLS','var') | |
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(PDidx).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 | |
dm = V_pdw(1).dim; | |
NiR2scorr4A = nifti; | |
NiR2scorr4A.mat = V_pdw(1).mat; | |
NiR2scorr4A.mat0 = V_pdw(1).mat; | |
NiR2scorr4A.descrip = descrip{ii}; | |
fR2scorr4A = spm_file(PR2s,'suffix','_corr4A'); | |
NiR2scorr4A.dat = file_array(fR2scorr4A,dm,dt, 0,1,0); | |
create(NiR2scorr4A); | |
NiR2scorr4A.dat(:,:,:) = R2scorr4A; | |
% apply correction | |
dm = V_pdw(1).dim; | |
NiAcorr = nifti; | |
NiAcorr.mat = V_pdw(1).mat; | |
NiAcorr.mat0 = V_pdw(1).mat; | |
NiAcorr.descrip = descrip{ii}; | |
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 | |
PDcalculation(fA,fTPM,mpm_params); | |
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')); | |
copyfile(fR1,fR1_final); | |
try copyfile([spm_str_manip(fR1,'r') '.json'],[spm_str_manip(fR1_final,'r') '.json']); end %#ok<*TRYNC> | |
fR1 = fR1_final; | |
end | |
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; | |
% 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 | |
% move basin 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{PDidx},'filename')); | |
copyfile(Pavg{PDidx},PPDw_final); | |
try copyfile([spm_str_manip(Pavg{PDidx},'r') '.json'],[spm_str_manip(PPDw_final,'r') '.json']); end | |
PPDw = PPDw_final; | |
if T1idx | |
PT1w_final = fullfile(supplpath, spm_file(Pavg{T1idx},'filename')); | |
copyfile(Pavg{T1idx},PT1w_final); | |
try copyfile([spm_str_manip(Pavg{T1idx},'r') '.json'],[spm_str_manip(PT1w_final,'r') '.json']); end | |
PT1w = PT1w_final; | |
else | |
PT1w = ''; | |
end | |
if MTidx | |
PMTw_final = fullfile(supplpath, spm_file(Pavg{MTidx},'filename')); | |
copyfile(Pavg{MTidx},PMTw_final); | |
try copyfile([spm_str_manip(Pavg{MTidx},'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,'MPM_map_creation_mpm_params.json'),mpm_params,struct('indent','\t')); | |
spm_progress_bar('Clear'); | |
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 | |
fprintf(1,'\n -------- Proton density map calculation --------\n'); | |
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 toobox: | |
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)); | |
% 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; | |
fprintf(1,'\nINFO (PD calculation):\n\tmean White Matter intensity: %.1f\n',mean(A_WM(A_WM~=0))); | |
fprintf(1,'\tSD White Matter intensity %.1f\n\n',std(A_WM(A_WM~=0),[],1)); | |
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: | |
fprintf(1,['\nWARNING: Error estimate is high for calculated PD map:\n%s' ... | |
'\nError higher than 6% may indicate motion.\n'], Vsave.fname); | |
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) | |
% 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,'MPM_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 | |
% 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; | |
fprintf(1,'\nINFO: FLASH echoes loaded for each contrast are: '); | |
% 1) try MTw contrast: | |
tmpfnam = char(jobsubj.raw_mpm.MT); % P_mtw | |
if isempty(tmpfnam) | |
fprintf(1,'\n\t- WARNING: no MT-weighted FLASH echoes available!'); | |
mpm_params.MTidx = 0; % zero index means no contrast available | |
else | |
ccon = ccon+1; | |
fprintf(1,'\n\t- MT-weighted: %d echoes', size(tmpfnam,1)); | |
mpm_params.input(ccon).fnam = tmpfnam; | |
mpm_params.input(ccon).tag = 'MT'; | |
mpm_params.MTidx = ccon; | |
end | |
% 2) try PDw contrast: | |
tmpfnam = char(jobsubj.raw_mpm.PD); % P_pdw | |
if isempty(tmpfnam) | |
fprintf(1,'\n\n\t- WARNING: no PD-weighted FLASH echoes available! \n\t\tThe map creation won''t be able to proceed!\n'); | |
mpm_params.PDidx = 0; % zero index means no contrast available | |
else | |
ccon = ccon+1; | |
fprintf(1,'\n\t- PD-weighted: %d echoes', size(tmpfnam,1)); | |
mpm_params.input(ccon).fnam = tmpfnam; | |
mpm_params.input(ccon).tag = 'PD'; | |
mpm_params.PDidx = ccon; | |
end | |
% 3) try T1w contrast: | |
tmpfnam = char(jobsubj.raw_mpm.T1); % P_t1w | |
if isempty(tmpfnam) | |
fprintf(1,'\n\t- WARNING: no T1-weighted FLASH echoes available!'); | |
mpm_params.T1idx = 0; % zero index means no contrast available | |
else | |
ccon = ccon+1; | |
fprintf(1,'\n\t- T1-weighted: %d echoes', size(tmpfnam,1)); | |
mpm_params.input(ccon).fnam = tmpfnam; | |
mpm_params.input(ccon).tag = 'T1'; | |
mpm_params.T1idx = ccon; % zero index means no contrast available | |
end | |
mpm_params.ncon = ccon; % number of contrasts available | |
fprintf(1,'\n'); | |
% collect TE, TR and FA for each available contrast | |
for ccon = 1:mpm_params.ncon | |
p = get_trtefa(mpm_params.input(ccon).fnam); | |
if ~isempty(p) | |
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 | |
fprintf(1,'\nWARNING: No TE/TR/FA values found for %sw images. Fallback to defaults.\n',mpm_params.input(ccon).tag); | |
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)) | |
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); | |
fprintf(1,'\nINFO: averaged PDw/T1w/MTw will be calculated based on the first %d echoes.\n',mpm_params.nr_echoes4avg); | |
% if T1w and PDw data available, identify the protocol to define RF | |
% spoiling correction parameters (for T1 map calculation) | |
if mpm_params.PDidx && mpm_params.T1idx | |
% 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.PDidx).TR; | |
mpm_params.input(mpm_params.T1idx).TR; | |
mpm_params.input(mpm_params.PDidx).fa; | |
mpm_params.input(mpm_params.T1idx).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}; | |
fprintf(1,'\nINFO: MPM acquisition protocol = %s.\n', prot_tag); | |
end | |
end | |
if ~mtch | |
prot_tag = 'Unknown'; | |
fprintf(1,'\nWARNING: MPM protocol unknown. No RF spoiling correction will be applied.\n'); | |
end | |
else | |
prot_tag = 'Unknown'; | |
end | |
% now retrieve RF spoiling correction parameters | |
mpm_params.proc.RFC = hmri_get_defaults(['rfcorr.',prot_tag]); | |
% 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 | |
mpm_params.proc.PD = hmri_get_defaults('PDproc'); | |
if ~mpm_params.T1idx && mpm_params.proc.PD.PDmap | |
fprintf(1,'\nWARNING: PD map calculation enabled but T1w images not available.\n\tPD map won''t be calculated.\n'); | |
mpm_params.proc.PD.PDmap = 0; | |
end | |
% if fullOLS, T2*-weighting bias correction must not be applied | |
if mpm_params.fullOLS && mpm_params.proc.PD.T2scorr | |
fprintf(1,'\nWARNING: if TE=0 fit is enabled (fullOLS option), no T2*-weighting \nbias correction is required. T2scorr disabled.\n'); | |
mpm_params.proc.PD.T2scorr = 0; | |
end | |
% whether OLS R2* is calculated | |
mpm_params.proc.R2sOLS = hmri_get_defaults('R2sOLS'); | |
% consistency check for number of echoes averaged for A calculation: | |
if mpm_params.PDidx && mpm_params.T1idx | |
if mpm_params.proc.PD.nr_echoes_forA > size(mpm_params.input(mpm_params.T1idx).fnam,1) | |
fprintf(1,['\nWARNING: 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 the maximum number of echoes.\n'],mpm_params.proc.PD.nr_echoes_forA, ... | |
size(mpm_params.input(mpm_params.T1idx).fnam,1)); | |
mpm_params.proc.PD.nr_echoes_forA = size(mpm_params.input(T1idx).fnam,1); | |
end | |
end | |
end | |
%% =======================================================================% | |
% To arrange the metadata structure for MPM calculation output. | |
%=========================================================================% | |
function metastruc = init_mpm_output_metadata(input_files, mpm_params) | |
proc.descrip = 'MPM 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 |