Skip to content

Commit

Permalink
Merge pull request #127 from ExploreASL/feature-#ASL_BIDS
Browse files Browse the repository at this point in the history
Feature #asl bids
  • Loading branch information
Remi-Gau authored Feb 6, 2021
2 parents b0d9bda + 6cba2ee commit c5167eb
Showing 1 changed file with 254 additions and 3 deletions.
257 changes: 254 additions & 3 deletions +bids/layout.m
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ function tolerant_message(tolerant, msg)
% -Parse a subject's directory
% ==========================================================================
function subject = parse_subject(pth, subjname, sesname)

% For each modality (anat, func, eeg...) all the files from the
% corresponding directory are listed and their filenames parsed with extra
% BIDS valid entities listed (e.g. 'acq','ce','rec','fa'...).
Expand All @@ -181,6 +182,7 @@ function tolerant_message(tolerant, msg)
subject.fmap = struct([]); % fieldmap data
subject.beh = struct([]); % behavioral experiment data
subject.dwi = struct([]); % diffusion imaging data
subject.perf = struct([]); % ASL perfusion imaging data
subject.eeg = struct([]); % EEG data
subject.meg = struct([]); % MEG data
subject.ieeg = struct([]); % iEEG data
Expand All @@ -193,6 +195,7 @@ function tolerant_message(tolerant, msg)
subject = parse_meg(subject);
subject = parse_beh(subject);
subject = parse_dwi(subject);
subject = parse_perf(subject);
subject = parse_pet(subject);
subject = parse_ieeg(subject);

Expand Down Expand Up @@ -263,6 +266,250 @@ function tolerant_message(tolerant, msg)
end
end


function subject = parse_perf(subject)

% --------------------------------------------------------------------------
% -ASL perfusion imaging data
% --------------------------------------------------------------------------
pth = fullfile(subject.path, 'perf');
if exist(pth, 'dir')
fileList = bids.internal.file_utils('List', pth, ...
sprintf('^%s.*asl\\.nii(\\.gz)?$', subject.name));
fileList = convert_to_cell(fileList);
j = 1;

% ASL timeseries NIfTI file
% ----------------------------------------------------------------------
labels = regexp(fileList, [ ...
'^sub-[a-zA-Z0-9]+' ... % sub-<participant_label>
'(?<ses>_ses-[a-zA-Z0-9]+)?' ... % ses-<label>
'(?<acq>_acq-[a-zA-Z0-9]+)?' ... % acq-<label>
'(?<rec>_rec-[a-zA-Z0-9]+)?' ... % rec-<label>
'(?<run>_run-[a-zA-Z0-9]+)?' ... % run-<index>
'_asl\.nii(\.gz)?$'], 'names'); % NIfTI file suffix/extension

if any(~cellfun(@isempty, labels))
idx = find(~cellfun(@isempty, labels));
for i = 1:numel(idx)
% Parse filename
% ---------------------------
fb = bids.internal.file_utils(bids.internal.file_utils(fileList{idx(i)}, 'basename'), 'basename');
p = bids.internal.parse_filename(fileList{i}, {'sub', 'ses', 'acq', 'dir', 'rec', 'run'});

if j==1
subject.perf = p;
else
fields_p = fields(p);
for iField=1:length(fields_p)
subject.perf(j).(fields_p{iField}) = p.(fields_p{iField});
end
end

% add type
subject.perf(j).type = 'asl';

% default to run 1 ((!) TODO: but could be that we need to check this
% at the end!)
if isempty(subject.perf(j).run)
subject.perf(j).run = '1';
end

% Manage JSON-sidecar metadata (REQUIRED)
% ---------------------------
metafile = fullfile(pth, bids.internal.file_utils(fb, 'ext', 'json'));

if exist(metafile, 'file')
[~, Ffile] = fileparts(metafile);
subject.perf(j).json_sidecar_filename = [Ffile '.json'];
subject.perf(j).meta = bids.util.jsondecode(metafile);
else
warning(['Missing: ' metafile]);
end

% Manage ASLCONTEXT-sidecar metadata (REQUIRED)
% ---------------------------
metafile = fullfile(pth, bids.internal.file_utils([fb(1:end-4) '_aslcontext'], 'ext', 'tsv'));

if exist(metafile, 'file')
[~, Ffile] = fileparts(metafile);
subject.perf(j).context_sidecar_filename = [Ffile '.tsv'];
subject.perf(j).context = bids.util.tsvread(metafile);
else
warning(['Missing: ' metafile]);
end

% Manage M0 (REQUIRED)
% ---------------------------
% M0 field is flexible:

if ~isfield(subject.perf(j).meta, 'M0Type')
warning(['M0Type field missing in ' subject.perf(j).json_sidecar_filename]);

else
switch subject.perf(j).meta.M0Type
case 'Separate'
% the M0 was obtained as a separate scan
subject.perf(j).m0type = 'separate_scan';
subject.perf(j).m0explanation = 'M0 was obtained as a separate scan';

% M0scan.nii filename
m0_filename = [subject.perf(j).filename(1:end-10) 'm0scan' subject.perf(j).ext]; % assuming the (.nii|.nii.gz) extension choice is the same throughout
if ~exist(fullfile(pth, m0_filename), 'file')
warning(['Missing: ' m0_filename]);
else
% subject.perf(j).m0_filename = m0_filename;
% -> this is included in the same structure for the m0scan.nii
end

% M0 sidecar filename
m0_json_sidecar_filename = [subject.perf(j).filename(1:end-10) 'm0scan.json'];
if ~exist(fullfile(pth, m0_json_sidecar_filename), 'file')
warning(['Missing: ' m0_json_sidecar_filename]);
else
% subject.perf(j).m0_json_sidecar_filename = m0_json_sidecar_filename;
% -> this is included in the same structure for the m0scan.nii
end

case 'Included'
% M0 is one or more image(s) in the *asl.nii[.gz] timeseries
if ~isfield(subject.perf(j), 'context') || ~isfield(subject.perf(j).context, 'volume_type')
warning('Cannot find M0 volume in aslcontext, context-information missing');
else
m0indices = find(cellfun(@(x) strcmp(x, 'm0scan'), subject.perf(j).context.volume_type)==true);
if isempty(m0indices)
warning('No M0 volume found in aslcontext');
else
subject.perf(j).m0type = 'within_timeseries';
subject.perf(j).m0explanation = 'M0 is one or more image(s) in the *asl.nii[.gz] timeseries';
subject.perf(j).m0_volume_index = m0indices;
end
end

case 'Estimate'
% this is a single estimated M0 value, e.g. when the M0 is
% obtained from an external scan and/or study
subject.perf(j).m0type = 'single_value';
subject.perf(j).m0explanation = 'this is a single estimated M0 value, e.g. when the M0 is obtained from an external scan and/or study';
subject.perf(j).m0_value = subject.perf(j).meta.M0;

case 'Absent'
% this shows that there is no M0, so this
% leaves us the only option to use the (average) control volume as pseudo-M0 volume
% which is a safe option if no background suppression was used
subject.perf(j).m0type = 'use_control_as_m0';
subject.perf(j).m0explanation = 'M0 is absent, so we can use the (average) control volume as pseudo-M0 (if no background suppression was used)';
% which is a safe option if no background suppression was used
if subject.perf(j).meta.BackgroundSuppression==true
warning('Caution when using control as M0, background suppression was applied');
end

otherwise
warning(['Unknown M0Type:' subject.perf(j).meta.M0Type ' in ' subject.perf(j).json_sidecar_filename]);
end
end


% Manage labeling image metadata (OPTIONAL)
% ---------------------------
metafile = fullfile(pth, bids.internal.file_utils([fb(1:end-4) '_labeling'], 'ext', 'jpg'));

if exist(metafile, 'file')
[~, Ffile] = fileparts(metafile);
subject.perf(j).labeling_image_filename = [Ffile '.jpg'];
end

j = j + 1;
end % for i = 1:numel(idx)
end % if any(~cellfun(@isempty, labels))

% -M0scan NIfTI file
% ----------------------------------------------------------------------
fileList = bids.internal.file_utils('List', pth, ...
sprintf('^%s.*_m0scan\\.nii(\\.gz)?$', subject.name));
fileList = convert_to_cell(fileList);

labels = regexp(fileList, [ ...
'^sub-[a-zA-Z0-9]+' ... % sub-<participant_label>
'(?<ses>_ses-[a-zA-Z0-9]+)?' ... % ses-<label>
'(?<acq>_acq-[a-zA-Z0-9]+)?' ... % acq-<label>
'(?<rec>_rec-[a-zA-Z0-9]+)?' ... % rec-<label>
'(?<run>_run-[a-zA-Z0-9]+)?' ... % run-<index>
'_m0scan\.nii(\.gz)?$'], 'names'); % NIfTI file suffix/extension

if any(~cellfun(@isempty, labels))
idx = find(~cellfun(@isempty, labels));
for i = 1:numel(idx)
% Parse filename
% ---------------------------
fb = bids.internal.file_utils(bids.internal.file_utils(fileList{idx(i)}, 'basename'), 'basename');
p = bids.internal.parse_filename(fileList{i}, {'sub', 'ses', 'acq', 'dir', 'rec', 'run'});

if j==1
subject.perf = p;
else
fields_p = fields(p);
for iField=1:length(fields_p)
subject.perf(j).(fields_p{iField}) = p.(fields_p{iField});
end
end

% default to run 1 ((!) TODO: but could be that we need to check this
% at the end!)
if isempty(subject.perf(j).run)
subject.perf(j).run = '1';
end

% Manage JSON-sidecar metadata (REQUIRED)
% ---------------------------
metafile = fullfile(pth, bids.internal.file_utils(fb, 'ext', 'json'));

if ~exist(metafile, 'file')
warning(['Missing: ' metafile]);
else
[~, Ffile] = fileparts(metafile);
subject.perf(j).json_sidecar_filename = [Ffile '.json'];
subject.perf(j).meta = bids.util.jsondecode(metafile);

% Manage intended-for (REQUIRED)
% ---------------------------

% Get all NIfTIs that this m0scan is intended for
if ~isfield(subject.perf(j).meta, 'IntendedFor')
warning(['Missing field IntendedFor in ' metafile]);
elseif ischar(subject.perf(j).meta.IntendedFor)
path_intended_for{1} = subject.perf(j).meta.IntendedFor;
elseif isstruct(subject.perf(j).meta.IntendedFor)
for iPath=1:length(subject.perf(j).meta.IntendedFor)
path_intended_for{iPath} = subject.perf(j).meta.IntendedFor(iPath);
end
end

for iPath=1:length(path_intended_for)
% check if this NIfTI is not missing
if ~exist(fullfile(fileparts(pth), path_intended_for{iPath}), 'file')
warning(['Missing: ' fullfile(fileparts(pth), path_intended_for{iPath})]);
else
% also check that this NIfTI aims to the same m0scan
[~, path2check, ext2check] = fileparts(path_intended_for{iPath});
filename_found = max(arrayfun(@(x) strcmp(x.filename, [path2check ext2check]), subject.perf));
if ~filename_found
warning(['Did not find NIfTI for which is intended: ' subject.perf(j).filename]);
else
subject.perf(j).intended_for = path_intended_for{iPath};
end
end
end
end
end % for i = 1:numel(idx)
j = j + 1;
end % if any(~cellfun(@isempty, labels))

end % if exist(pth, 'dir')
end % function subject = parse_perf(subject)



function subject = parse_fmap(subject)
%
% TODO:
Expand Down Expand Up @@ -399,8 +646,12 @@ function tolerant_message(tolerant, msg)

for i = 1:numel(idx)

subject.fmap(j).type = 'epi';
subject.fmap(j).filename = file_list{idx(i)};
subject.fmap(j).filename = file_list{idx(i)};
if ~isempty(regexp(subject.fmap.filename, 'm0scan'))
subject.fmap(j).type = 'm0scan';
else
subject.fmap(j).type = 'epi';
end
subject.fmap(j).dir = labels{idx(i)}.dir;

subject = append_common_fmap_fields_to_structure(subject, labels{idx(i)}, j);
Expand Down Expand Up @@ -741,7 +992,7 @@ function tolerant_message(tolerant, msg)
suffix = 'fieldmap';

case 'phase_encoded_direction_image'
suffix = 'epi';
suffix = '(epi|m0scan)';

direction_pattern = '_dir-(?<dir>[a-zA-Z0-9]+)?';

Expand Down

0 comments on commit c5167eb

Please sign in to comment.