JointR1R2star
Joint R2-R2* single compartment relaxometry using variable flip angle and multi-echo GRE data
gpuJointR1R2star
With askadam solver
Usage
obj = gpuJointR1R2starMapping(te,tr,fa);
[out] = obj.estimate( data, mask, extraData, fitting);
Model parameters
% M0 : Proton density weighted signal
% R1 : (=1/T1) in s^-1
% R2star: R2* in s^-1
model_params = {'M0';'R1';'R2star'};
ub = [ 2; 10; 200];
lb = [ 0; 0.1; 0.1];
startpoint = [ 1; 1; 30];
I/O overview
obj = gpuJointR1R2starMapping(te,tr,fa);
Input |
Description |
|---|---|
te |
1xNte echo time [s] |
tr |
repetition time [s] |
fa |
1xNfa flip angle vector [degree] |
[out] = obj.estimate( data, mask, extraData, fitting);
Input |
Description |
|---|---|
data |
5D MRI data, [x,y,z,t,fa] |
mask |
3D mask, [x,y,z] |
extraData |
Structure array with additional data |
extraData.b1 |
3D B1+ map [ratio], [x,y,z] |
fitting |
Structure array for model parameter estimation |
fitting.optimiser |
Algorithm for parameter update, ‘adam’ (default) | ‘sgdm’ | ‘rmsprop’ |
fitting.isdisplay |
boolean, display optimisation process in graphic plot |
fitting.convergenceValue |
tolerance in loss gradient to stop the optimisation |
fitting.convergenceWindow |
# of elements in which ‘convergenceValue’ is computed |
fitting.iteration |
maximum # of optimisation iterations |
fitting.initialLearnRate |
initial learn rate of Adam optimiser |
fitting.tol |
tolerance in loss |
fitting.lambda |
regularisation parameter(s) |
fitting.regmap |
model parameter(s) in which regularisation is applied, |
fitting.TVmode |
Mode for total variation (TV) regularisation, ‘2D’ | ‘3D’ |
fitting.lossFunction |
loss function, ‘L1’ | ‘L2’ | ‘huber’ | ‘mse’ |
fitting.isWeighted |
is cost weighted, true|false, default = true |
fitting.weightMethod |
Weighting method, ‘1stecho’ (default) | ‘norm’ |
fitting.weightPower |
power order of the weight, default = 2 |
Example
Example script for noise propagation:
addpath(genpath('../../gacelle/'));
clear;
%% Simulate data
% for reproducibility
seed = 23439; rng(seed);
Nsample = 1e3; % #voxel
SNR = 100;
Nt = 15;
TE1 = 1.5e-3;
tr = 45e-3;
t = linspace(TE1, tr-3e-3, Nt);
fa = [5, 10, 20, 30, 40, 50, 70];
Nfa = numel(fa);
% Parameter raneg for forward simulation
M0_range = [0.5, 2];
R1_range = [0.2, 2.5];
R2star_range = [5, 60];
% generate ground truth
M0_GT = single(rand(1,Nsample) * diff(M0_range) + min(M0_range) );
R1_GT = single(rand(1,Nsample) * diff(R1_range) + min(R1_range));
R2star_GT = single(rand(1,Nsample) * diff(R2star_range) + min(R2star_range));
% forward signal simulation
pars.M0 = M0_GT;
pars.R1 = R1_GT;
pars.R2star = R2star_GT;
b1 = ones(size(M0_GT)); extraData.b1 = b1;
objGPU = gpuJointR1R2starMapping(t,tr,fa);
s = gather(extractdata(objGPU.FWD(pars, extraData)));
s = permute(reshape(s,[Nt, Nfa, Nsample]),[3 4 5 1 2]);
mask = ones(size(s,1:3),'logical');
% Let's assume Gaussian noise for simplicity
noiseLv = (M0_GT.')./SNR;
s = s + randn(size(s)) .* noiseLv;
%% askadam estimation
fitting = [];
fitting.optimiser = 'adam';
fitting.iteration = 4000;
fitting.initialLearnRate = 0.001;
fitting.convergenceValue = 1e-8;
fitting.lossFunction = 'l1';
fitting.tol = 1e-8;
fitting.isdisplay = false;
fitting.start = 'prior';
extraData = [];
extraData.b1 = b1.';
objGPU = gpuJointR1R2starMapping(t,tr,fa);
out = objGPU.estimate(s, mask, extraData, fitting);
%% askadam estimation
fitting = [];
fitting.optimiser = 'adam';
fitting.iteration = 4000;
fitting.initialLearnRate = 0.001;
fitting.convergenceValue = 1e-8;
fitting.lossFunction = 'l1';
fitting.tol = 1e-8;
fitting.isdisplay = false;
fitting.start = 'prior';
fitting.isWeighted = true;
fitting.weightMethod = '1stecho';
fitting.weightPower = 2;
extraData = [];
extraData.b1 = b1.';
objGPU = gpuJointR1R2starMapping(t,tr,fa);
out_weighted = objGPU.estimate(s, mask, extraData, fitting);
%% starting position
objGPU = gpuJointR1R2starMapping(t,tr,fa);
pars0 = objGPU.estimate_prior(s, mask, extraData);
%% plot result
figure(99);
field = fieldnames(pars);
tiledlayout(2,numel(field));
for k = 1:numel(field)
nexttile;
scatter(pars.(field{k}),pars0.(field{k}),5,'filled','MarkerFaceAlpha',.4);hold on
scatter(pars.(field{k}),out.final.(field{k}),5,'filled','MarkerFaceAlpha',.4);
h = refline(1);
h.Color = 'k';
title(field{k});
xlabel('GT');ylabel('Fitted');
end
for k = 1:numel(field)
nexttile;
scatter(pars.(field{k}),pars0.(field{k}),5,'filled','MarkerFaceAlpha',.4);hold on
scatter(pars.(field{k}),out_weighted.final.(field{k}),5,'filled','MarkerFaceAlpha',.4);
h = refline(1);
h.Color = 'k';
title([field{k} ' w-P2']);
xlabel('GT');ylabel('Fitted');
end
legend('Start','Fitted','Location','northwest');
Example script for real data:
addpath(genpath('/autofs/space/linen_001/users/kwokshing/tools/gacelle'))
addpath('/autofs/space/linen_001/users/kwokshing/tools/sepia/sepia_master');
sepia_addpath;
%% Subject info and directories
subj_label = 'sub-022';
bids_dir = '/autofs/cluster/connectome2/Bay8_C2/bids/';
derivatives_dir = fullfile(bids_dir,'derivatives/');
jointR1R2star_dir = fullfile(derivatives_dir,'jointR1R2star',subj_label);
SEPIA_dir = fullfile(derivatives_dir,'sepia',subj_label);
processed_vibe_dir = fullfile(derivatives_dir,'processed_vibe',subj_label);
file_list = dir(fullfile(processed_vibe_dir,'*rec-offline*space-MEGRE.mat'));
flip_angle = zeros(1,numel(file_list));
for kfile = 1:numel(file_list)
load(fullfile(file_list(kfile).folder,file_list(kfile).name),'FA');
flip_angle(kfile) = FA;
end
flip_angle = sort(flip_angle,'ascend');
%% load data
counter = 0;
img = [];
sepia_header = [];
unwrappedPhase = [];
totalField = [];
fa = zeros(1,length(flip_angle));
for kfa = 1:length(flip_angle)
counter = counter + 1;
% general GRE basename
acq_label = strcat('acq-',['FA' num2str(flip_angle(kfa))]);
prefix = strcat(subj_label,'_',acq_label,'_rec-offline');
% magnitude nifti image filename
magn_fn = dir(fullfile(processed_vibe_dir,strcat(prefix,'*part-mag*MEGRE_space-MEGRE.nii*')));
sepia_header_fn = dir(fullfile(processed_vibe_dir,strcat(prefix,'*space-MEGRE.mat')));
nii = load_untouch_nii(fullfile(magn_fn.folder, magn_fn.name));
img = cat(5,img,nii.img);
sepia_header{kfa} = load(fullfile(sepia_header_fn.folder, sepia_header_fn.name));
fa(kfa) = sepia_header{kfa}.FA;
tr = sepia_header{kfa}.TR;
end
te = sepia_header{end}.TE;
% B1 info
true_flip_angle_fn = dir(fullfile(processed_vibe_dir,'*acq-famp*TB1TFL*space-MEGRE.nii*'));
true_flip_angle_json = dir(fullfile(processed_vibe_dir,'*acq-famp*TB1TFL*.json'));
true_flip_angle = load_nii_img_only( fullfile( true_flip_angle_fn.folder, true_flip_angle_fn.name));
b1_header = jsondecode( fileread( fullfile( true_flip_angle_json.folder,true_flip_angle_json.name)));
b1 = true_flip_angle / 10 / b1_header.FlipAngle;
mask_fn = dir(fullfile(processed_vibe_dir,strcat(subj_label,'*acq-FA10*rec-offline*mask_brain*space-MEGRE.nii*')));
mask_filename = fullfile(mask_fn.folder, mask_fn.name);
mask = load_nii_img_only(mask_filename);
clear true_flip_angle
%% Prepare data fot batch processing
% Magnitude fitting
% setup algorithm parameters
fitting = [];
% fitting option
fitting.iteration = 4000;
fitting.convergenceWindow = 25; % at least 100 iterations and more robust convergance estimate
fitting.convergenceValue = 1e-8;
fitting.initialLearnRate = 0.001;
fitting.decayRate = 0;
fitting.tol = 1e-3;
% residual option
fitting.isWeighted = false;
extradata = [];
extradata.b1 = b1;
obj = gpuJointR1R2starMapping(te,tr,fa);
[out] = obj.estimate(img, mask, extradata, fitting);
%% Prepare data fot batch processing
% Magnitude fitting
% setup algorithm parameters
fitting = [];
% fitting option
fitting.iteration = 4000;
fitting.convergenceWindow = 25; % at least 100 iterations and more robust convergance estimate
fitting.convergenceValue = 1e-8;
fitting.initialLearnRate = 0.001;
fitting.decayRate = 0;
fitting.tol = 1e-4;
% residual option
fitting.isWeighted = true;
fitting.weightPower = 2;
fitting.weightMethod = '1stecho';
extradata = [];
extradata.b1 = b1;
obj = gpuJointR1R2starMapping(te,tr,fa);
[out_weighted] = obj.estimate(img, mask, extradata, fitting);
gpuJointR1R2starmcmc
With MCMC solver
Usage
obj = gpuJointR1R2starMappingmcmc(te,tr,fa);
[out] = obj.estimate( data, mask, extraData, fitting);
Model parameters
% M0 : Proton density weighted signal
% R1 : (=1/T1) in s^-1
% R2star: R2* in s^-1
model_params = {'M0';'R1';'R2star';'noise'};
ub = [ 2; 10; 200; 0.1];
lb = [ 0; 0.1; 0.1; 0.001];
startpoint = [ 1; 1; 30; 0.05];
step = [0.01;0.01; 1; 0.005];
I/O overview
obj = gpuJointR1R2starMappingmcmc(te,tr,fa);
Input |
Description |
|---|---|
te |
1xNte echo time [s] |
tr |
repetition time [s] |
fa |
1xNfa flip angle vector [degree] |
[out] = obj.estimate( data, mask, extraData, fitting);
Input |
Description |
|---|---|
data |
5D MRI data, [x,y,z,t,fa] |
mask |
3D mask, [x,y,z] |
extraData |
Structure array with additional data |
extraData.b1 |
3D B1+ map [ratio], [x,y,z] |
fitting |
Structure array for model parameter estimation |
fitting.algorithm |
MCMC algorithm, ‘MH’ (Metropolis-Hastings)|’GW’ (Affline-invariant ensemble) |
fitting.iteration |
# MCMC iterations |
fitting.repetition |
# repetition of MCMC proposal |
fitting.thinning |
sampling interval between iterations |
fitting.burnin |
iterations to be discarded at the beginning, if >1, the exact number will be used; else iteration*burnin |
fitting.xStepSize |
step size of model parameter in MCMC proposal, same size and order as ‘model_params’ (‘MH’ only) |
fitting.StepSize |
step size for ‘GW’ in MCMC proposal (‘GW’ only) |
fitting.Nwalker |
# random walkers (‘GW’ only) |
fitting.metric |
cell variable, metric(s) derived from posterior distribution, ‘mean’|’std’|’median’|’iqr’ (can be multiple) |
fitting.isWeighted |
is cost weighted, true|false, default = true |
fitting.weightMethod |
Weighting method, ‘1stecho’ (default) | ‘norm’ |
fitting.weightPower |
power order of the weight, default = 2 |
Output |
Description |
|---|---|
out |
structure contains optimisation result |
out.posterior |
structure contains MCMC posterior samples |
out.posterior.(model_params{k}) |
Model parameter MCMC posterior samples, masked and unshaped for memory preservation |
out.{metric}.(model_params{k}) |
Posterior statistics chosen in fitting.metric |
Example
Example script for noise propagation:
addpath(genpath('../../gacelle/'));
clear;
%% Simulate data
% for reproducibility
seed = 23439; rng(seed);
Nsample = 1e3; % #voxel
SNR = 100;
Nt = 15;
TE1 = 1.5e-3;
tr = 45e-3;
t = linspace(TE1, tr-3e-3, Nt);
fa = [5, 10, 20, 30, 40, 50, 70];
Nfa = numel(fa);
% Parameter raneg for forward simulation
M0_range = [0.5, 2];
R1_range = [0.2, 4];
R2star_range = [5, 100];
% generate ground truth
M0_GT = single(rand(1,Nsample) * diff(M0_range) + min(M0_range) );
R1_GT = single(rand(1,Nsample) * diff(R1_range) + min(R1_range));
R2star_GT = single(rand(1,Nsample) * diff(R2star_range) + min(R2star_range));
% forward signal simulation
pars.M0 = M0_GT;
pars.R1 = R1_GT;
pars.R2star = R2star_GT;
b1 = ones(size(M0_GT)); extraData.b1 = b1;
objGPU = gpuJointR1R2starMappingmcmc(t,tr,fa);
s = gather(objGPU.FWD(pars, [], extraData));
s = permute(reshape(s,[Nt, Nfa, Nsample]),[3 4 5 1 2]);
mask = ones(size(s,1:3),'logical');
% Let's assume Gaussian noise for simplicity
noiseLv = (M0_GT.')./SNR;
s = s + randn(size(s)) .* noiseLv;
%% askadam estimation
fitting = [];
fitting.algorithm = 'GW';
fitting.Nwalker = 50;
fitting.StepSize = 2;
fitting.iteration = 1e4;
fitting.thinning = 10; % Sample every 10 iteration
fitting.metric = {'mean','std'};
fitting.burnin = 0.1; % 10% burn-in
fitting.start = 'default';
extraData = [];
extraData.b1 = b1.';
objGPU = gpuJointR1R2starMappingmcmc(t,tr,fa);
out = objGPU.estimate(s, mask, extraData, fitting);
%% plot result
figure(2)
field = fieldnames(pars);
tiledlayout(1,numel(field));
for k = 1:numel(field)
nexttile;
scatter(pars.(field{k}),out.mean.(field{k}),5,'filled','MarkerFaceAlpha',.4);
h = refline(1);
h.Color = 'k';
title(field{k});
xlabel('GT');ylabel('Fitted');
end
Example script for real data:
addpath(genpath('/autofs/space/linen_001/users/kwokshing/tools/gacelle'))
addpath('/autofs/space/linen_001/users/kwokshing/tools/sepia/sepia_master');
sepia_addpath;
%% Subject info and directories
subj_label = 'sub-022';
bids_dir = '/autofs/cluster/connectome2/Bay8_C2/bids/';
derivatives_dir = fullfile(bids_dir,'derivatives/');
jointR1R2star_dir = fullfile(derivatives_dir,'jointR1R2star',subj_label);
SEPIA_dir = fullfile(derivatives_dir,'sepia',subj_label);
processed_vibe_dir = fullfile(derivatives_dir,'processed_vibe',subj_label);
file_list = dir(fullfile(processed_vibe_dir,'*rec-offline*space-MEGRE.mat'));
flip_angle = zeros(1,numel(file_list));
for kfile = 1:numel(file_list)
load(fullfile(file_list(kfile).folder,file_list(kfile).name),'FA');
flip_angle(kfile) = FA;
end
flip_angle = sort(flip_angle,'ascend');
%% load data
counter = 0;
img = [];
sepia_header = [];
unwrappedPhase = [];
totalField = [];
fa = zeros(1,length(flip_angle));
for kfa = 1:length(flip_angle)
counter = counter + 1;
% general GRE basename
acq_label = strcat('acq-',['FA' num2str(flip_angle(kfa))]);
prefix = strcat(subj_label,'_',acq_label,'_rec-offline');
% magnitude nifti image filename
magn_fn = dir(fullfile(processed_vibe_dir,strcat(prefix,'*part-mag*MEGRE_space-MEGRE.nii*')));
sepia_header_fn = dir(fullfile(processed_vibe_dir,strcat(prefix,'*space-MEGRE.mat')));
nii = load_untouch_nii(fullfile(magn_fn.folder, magn_fn.name));
img = cat(5,img,nii.img);
sepia_header{kfa} = load(fullfile(sepia_header_fn.folder, sepia_header_fn.name));
fa(kfa) = sepia_header{kfa}.FA;
tr = sepia_header{kfa}.TR;
end
te = sepia_header{end}.TE;
% B1 info
true_flip_angle_fn = dir(fullfile(processed_vibe_dir,'*acq-famp*TB1TFL*space-MEGRE.nii*'));
true_flip_angle_json = dir(fullfile(processed_vibe_dir,'*acq-famp*TB1TFL*.json'));
true_flip_angle = load_nii_img_only( fullfile( true_flip_angle_fn.folder, true_flip_angle_fn.name));
b1_header = jsondecode( fileread( fullfile( true_flip_angle_json.folder,true_flip_angle_json.name)));
b1 = true_flip_angle / 10 / b1_header.FlipAngle;
mask_fn = dir(fullfile(processed_vibe_dir,strcat(subj_label,'*acq-FA10*rec-offline*mask_brain*space-MEGRE.nii*')));
mask_filename = fullfile(mask_fn.folder, mask_fn.name);
mask = load_nii_img_only(mask_filename);
clear true_flip_angle
%% Prepare data fot batch processing
% Magnitude fitting
% setup algorithm parameters
fitting = [];
fitting.algorithm = 'GW';
fitting.Nwalker = 50;
fitting.StepSize = 2;
fitting.iteration = 1e4;
fitting.thinning = 10; % Sample every 20 iteration
fitting.metric = {'median','iqr'};
fitting.burnin = 0.1; % 10% burn-in
% residual option
fitting.isWeighted = true;
fitting.weightPower = 2;
fitting.weightMethod = '1stecho';
extradata = [];
extradata.b1 = b1;
obj = gpuJointR1R2starMappingmcmc(te,tr,fa);
[out] = obj.estimate(img, mask, extradata, fitting);