Skip to content

Commit

Permalink
Merge pull request #123 from brainlife/fix/dwi_b0_fmap
Browse files Browse the repository at this point in the history
[FIX] Alert users that DWI B0map sequences should be fmap/epi, not dwi/dwi
  • Loading branch information
dlevitas authored Apr 12, 2024
2 parents 3be50cd + 0174414 commit f737f38
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 17 deletions.
28 changes: 28 additions & 0 deletions handler/ezBIDS_core/ezBIDS_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -2954,6 +2954,7 @@ def modify_objects_info(dataset_list):
"subject_idx": protocol["subject_idx"],
"session_idx": protocol["session_idx"],
"series_idx": protocol["series_idx"],
"message": protocol["message"],
"AcquisitionDate": protocol["AcquisitionDate"],
"AcquisitionTime": protocol["AcquisitionTime"],
"SeriesNumber": protocol["SeriesNumber"],
Expand Down Expand Up @@ -3027,6 +3028,30 @@ def extract_series_info(dataset_list_unique_series):
return ui_series_info_list


def check_dwi_b0maps(dataset_list_unique_series):
for unique_dic in dataset_list_unique_series:
if (unique_dic['type'] == 'dwi/dwi'
and unique_dic['NumVolumes'] < 10
and (not any(x.endswith('.bval') for x in unique_dic['paths']) and not unique_dic['exclude'])
or ('b0map' in unique_dic['SeriesDescription'] or '_b0_' in unique_dic['SeriesDescription'])):

# What we (likely have are DWI b0map sequences, which should be mapped as fmap/epi according to BIDS)
unique_dic['datatype'] = 'fmap'
unique_dic['suffix'] = 'epi'
unique_dic['type'] = 'fmap/epi'
if 'b0map' in unique_dic['SeriesDescription'] or '_b0_' in unique_dic['SeriesDescription']:
more_message = ', and b0map or _b0_ is in the sequence description'
else:
more_message = ''
unique_dic["message"] = "Acquisition was determined to be fmap/epi because there are no " \
f"corresponding bval/bvec files, and the number of volumes is < 10 {more_message}. In BIDS parlance, " \
"this DWI b0map should be fmap/epi rather than dwi/dwi " \
"(for reference, see https://neurostars.org/t/bids-b0-correction-for-dwi/3802). " \
"Please modify if incorrect."

return dataset_list_unique_series


# Begin (Apply functions)
print("########################################")
print("Beginning conversion process of uploaded dataset")
Expand Down Expand Up @@ -3115,6 +3140,9 @@ def extract_series_info(dataset_list_unique_series):
# Identify datatype and suffix information
dataset_list_unique_series = datatype_suffix_identification(dataset_list_unique_series, lookup_dic, config)

# Look for DWI b0maps, which are actually fmap/epi in BIDS parlance
dataset_list_unique_series = check_dwi_b0maps(dataset_list_unique_series)

# Identify entity label information
dataset_list_unique_series = entity_labels_identification(dataset_list_unique_series, lookup_dic)

Expand Down
14 changes: 13 additions & 1 deletion ui/src/SeriesPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,7 @@ import AsyncImageLink from './components/AsyncImageLink.vue';
import { Splitpanes, Pane } from 'splitpanes';
import 'splitpanes/dist/splitpanes.css';
import { setMaxListeners } from 'process';
export default defineComponent({
components: {
Expand Down Expand Up @@ -483,7 +484,7 @@ export default defineComponent({
);
}
}
//Ensure other fmap or perf/m0scan series aren't included in the IntendedFor mapping
// Ensure other fmap or perf/m0scan series aren't included in the IntendedFor mapping
if (s.IntendedFor.length > 0) {
s.IntendedFor.forEach((i) => {
if (
Expand All @@ -499,6 +500,17 @@ export default defineComponent({
});
}
}
/*
If user tries modifying a DWI b0map (fmap/epi) to dwi/dwi, warn them that it could be improper. At the
end of the day though, user has final say.
*/
if (s.type === 'dwi/dwi') {
if (s.message.includes('fmap/epi')) {
s.validationWarnings.push(
'This sequence is believed to be a DWI b0map, which in BIDS corresponds to fmap/epi. If this sequence is not a DWI b0map, please proceed. Otherwise, please reconsider.'
);
}
}
},
isValid(cb: (v?: string) => void) {
Expand Down
87 changes: 71 additions & 16 deletions ui/src/libUnsafe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ export function fmapQA($root: IEzbids) {

// https://bids-specification.readthedocs.io/en/stable/04-modality-specific-files/01-magnetic-resonance-imaging-data.html#types-of-fieldmaps

// case #1: Phase-difference map and at least one magnitude image
// case #1: Phase-difference map and at least one magnitude sequence
let fmapMagPhasediffObjs = section.filter(function (o) {
return (
o._type === 'fmap/magnitude1' || o._type === 'fmap/magnitude2' || o._type === 'fmap/phasediff'
Expand Down Expand Up @@ -548,6 +548,15 @@ export function setRun($root: IEzbids) {
export function setIntendedFor($root: IEzbids) {
// Apply fmap intendedFor mapping, based on user specifications on Series page.

function isNumberInObject(obj: number[], number: number) {
for (const key in obj) {
if (obj.hasOwnProperty(key) && obj[key] === number) {
return true;
}
}
return false;
}

// Loop through subjects
$root._organized.forEach((subGroup: OrganizedSubject) => {
subGroup.sess.forEach((sesGroup: OrganizedSession) => {
Expand All @@ -561,6 +570,16 @@ export function setIntendedFor($root: IEzbids) {
(e) => e.analysisResults.section_id === s && !e._exclude && e._type !== 'exclude'
);

let allDWIs = section.filter((d) => d._type === 'dwi/dwi');
let allDWIfmaps = section.filter(
(d) => d._type === 'fmap/epi' && d.message.includes('corresponding bval/bvec files')
);

let DWIfmapWorflow = false;
if (allDWIs.length === allDWIfmaps.length) {
DWIfmapWorflow = true;
}

section.forEach((obj: IObject) => {
//add IntendedFor information
if (obj._type.startsWith('fmap/') || obj._type === 'perf/m0scan') {
Expand All @@ -573,17 +592,53 @@ export function setIntendedFor($root: IEzbids) {
}
}

let correspindingSeriesIntendedFor = $root.series[obj.series_idx].IntendedFor;
if (correspindingSeriesIntendedFor !== undefined && correspindingSeriesIntendedFor !== null) {
correspindingSeriesIntendedFor.forEach((i: number) => {
let IntendedForIDs = section
.filter((o) => o.series_idx === i && o._type !== 'func/events')
.map((o) => o.idx);
if (obj.IntendedFor !== undefined) {
obj.IntendedFor = obj.IntendedFor.concat(IntendedForIDs);
/*
Could have an issue where the fmap/epi pertains to a DWI b0map sequence. In certain cases,
there may be a one-to-one correspondence, so don't let this fmap pertain to all if that's
the case.
*/
if (obj.message.includes('corresponding bval/bvec files')) {
if (DWIfmapWorflow) {
// one-to-one correspondence between DWI and a DWI b0map (mapped as fmap/epi)
let correspondingDWI = section.filter(
(c) =>
c._type === 'dwi/dwi' &&
c._entities.direction.split('').reverse().join('') ===
obj._entities.direction &&
c._entities.run === obj._entities.run
);
if (correspondingDWI.length === 1) {
let IntendedForID = correspondingDWI[0].idx;
if (obj.IntendedFor !== undefined) {
if (!isNumberInObject(obj.IntendedFor, IntendedForID)) {
obj.IntendedFor = obj.IntendedFor.concat(IntendedForID);
}
}
}
});
}
} else {
// Otherwise, proceed as usual
let correspindingSeriesIntendedFor = $root.series[obj.series_idx].IntendedFor;

if (
correspindingSeriesIntendedFor !== undefined &&
correspindingSeriesIntendedFor !== null
) {
correspindingSeriesIntendedFor.forEach((i: number) => {
let IntendedForIDs = section
.filter((o) => o.series_idx === i && o._type !== 'func/events')
.map((o) => o.idx);
if (obj.IntendedFor !== undefined) {
for (const IntendedForID of IntendedForIDs) {
if (!isNumberInObject(obj.IntendedFor, IntendedForID)) {
obj.IntendedFor = obj.IntendedFor.concat(IntendedForID);
}
}
}
});
}
}

if (Object.keys(obj.IntendedFor).length !== 0) {
obj.IntendedFor?.forEach((e) => {
let IntendedForObj = $root.objects.filter((o: IObject) => o.idx === e)[0];
Expand All @@ -596,7 +651,7 @@ export function setIntendedFor($root: IEzbids) {
if (obj._type.startsWith('fmap/')) {
if (Object.keys(obj.IntendedFor).length === 0) {
obj.validationWarnings = [
'It is recommended that field map (fmap) images have IntendedFor set to at least 1 series ID. This is necessary if you plan on using processing BIDS-apps such as fMRIPrep',
'It is recommended that field map (fmap) sequences have IntendedFor set to at least 1 series ID. This is necessary if you plan on using processing BIDS-apps such as fMRIPrep',
];
obj.analysisResults.warnings = obj.validationWarnings;
} else {
Expand All @@ -606,7 +661,7 @@ export function setIntendedFor($root: IEzbids) {
} else if (obj._type === 'perf/m0scan') {
if (Object.keys(obj.IntendedFor).length === 0) {
obj.validationErrors = [
'It is required that perfusion m0scan images have IntendedFor set to at least 1 series ID.',
'It is required that perfusion m0scan sequences have IntendedFor set to at least 1 series ID.',
];
} else {
obj.validationErrors = [];
Expand Down Expand Up @@ -740,7 +795,7 @@ export function dwiQA($root: IEzbids) {
}
});
}
let fmapIntendedFor = protocolObjects.filter((t) => t._type.startsWith('fmap/'));
let fmapIntendedFor = protocolObjects.filter((t) => t._type.startsWith('fmap/') && !t._exclude);
fmapIntendedFor.forEach((f) => {
if (f.IntendedFor === null) {
f.IntendedFor = [];
Expand Down Expand Up @@ -774,7 +829,7 @@ export function dwiQA($root: IEzbids) {
let corrProtocolObj = protocolObjects.filter((e) => e.idx == d.idx)[0]; //will always be an index of 1, so just grab the first (i.e. only) index
if (!d.fmap && !d.oppDWI) {
corrProtocolObj.analysisResults.warnings = [
"This dwi/dwi acquisition doesn't appear to have a corresponding dwi/dwi or field map acquisition with a 180 degree flipped phase encoding direction. You may wish to exclude this from BIDS conversion, unless there is a reason for keeping it.",
"This dwi/dwi acquisition doesn't appear to have a corresponding dwi/dwi or fmap sequence with a 180 degree flipped phase encoding direction. You may wish to exclude this from BIDS conversion, unless there is a reason for keeping it.",
];
} else {
corrProtocolObj.analysisResults.warnings = [];
Expand Down Expand Up @@ -876,14 +931,14 @@ export function validateEntities(level: string, info: any) {
!info.ImageType.includes(i.toUpperCase())
) {
info.validationWarnings.push(
`ezBIDS detects that this image is not part-${i}. Please verify before continuing`
`ezBIDS detects that this sequence is not part-${i}. Please verify before continuing`
);
}
}
} else {
if (entities[k] === i && level === 'Series' && !info.ImageType.includes('IMAGINARY')) {
info.validationWarnings.push(
'ezBIDS detects that this image is not part-imag. Please verify before continuing'
'ezBIDS detects that this sequence is not part-imag. Please verify before continuing'
);
}
}
Expand Down
2 changes: 2 additions & 0 deletions ui/src/store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ export interface IObject {
subject_idx: number;
session_idx: number;

message: string;

_SeriesDescription: string; //copied from series for quick ref
type: string; //override
_type: string;
Expand Down

0 comments on commit f737f38

Please sign in to comment.