Skip to content

ENH: Adds populate_intended_for for fmaps #482

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 94 commits into from
Feb 24, 2022
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
94 commits
Select commit Hold shift + click to select a range
6dbd28a
Adds `add_field_to_json` to utils.py
pvelasco Dec 9, 2020
d5f01b9
ENH: Adds `populate_intended_for`
pvelasco Dec 11, 2020
7117659
Adds `populate_intended_for` to `process_extra_commands`
pvelasco Dec 14, 2020
5c4ff83
BF: acq_str/run_str will not break if no acq/run label present
pvelasco Dec 17, 2020
77d63f1
RF: `populate-intended-for` command uses dashes (`-`)
pvelasco Dec 17, 2020
42d3c20
RF: Prepares way for further `populate_intended_for` unit tests
pvelasco Dec 17, 2020
0d821d5
Corrects usage of `populate-intended-for` in allowed commands
pvelasco Dec 17, 2020
7edcf36
Merge pull request #4 from nipy/master
pvelasco Dec 18, 2020
34b2acc
BF: Fixes broken test for `populate_intended_for` in Python3.5
pvelasco Dec 18, 2020
7a01b5f
RF: make `get_shim_setting` a separate function
pvelasco Dec 18, 2020
36643ae
RF: Simplify a little bit `create_dummy_pepolar_bids_session`
pvelasco Dec 19, 2020
1be873c
RF: `test_bids.py: create_dummy_pepolar_bids_session` now also return…
pvelasco Dec 21, 2020
7815c92
Adds tests for the case in which there are no ShimSettings
pvelasco Dec 21, 2020
4b21d45
BF: Fixes bug in `populate_intended_for`
pvelasco Dec 22, 2020
03d4005
Adds a new test case for `populate_intended_for`
pvelasco Dec 22, 2020
d3c9962
RF: `test_convert.py` imports full module `convert.py`
pvelasco Dec 23, 2020
1bd783a
ENH: Adds a call to `populate_intended_for` in `convert.py:convert`
pvelasco Dec 23, 2020
d9dabe7
BF: Fixes unit tests for test_convert.py
pvelasco Jan 4, 2021
fb7dd70
BF: Fixes unit tests for test_convert.py
pvelasco Jan 4, 2021
015f79c
BF: skips populate_intended_for for non-BIDS-compliant datasets
pvelasco Jan 5, 2021
90ccae5
Merge 'nipy_heudiconv/master' into adds_populate_intended_for
pvelasco Feb 4, 2021
0690c1a
Update heudiconv/bids.py
pvelasco Feb 18, 2021
9b397de
Improve lgr in `bids.py`
pvelasco Feb 18, 2021
fefa521
Merge remote-tracking branch 'nipy_heudiconv/master' into adds_popula…
pvelasco Feb 18, 2021
d801fee
Simplify sorting of fmaps in heudiconv/bids.py
pvelasco Feb 19, 2021
8f25fb6
Simplify expression to find session field maps
pvelasco Mar 2, 2021
b9a1d7a
Minor: Use preferred str formatting in lgr call
pvelasco Mar 2, 2021
37c9b7a
RF: Add find_fmap_groups for populating IntendedFor
pvelasco Mar 10, 2021
cfcb118
Add new test case for find_fmap_groups
pvelasco Mar 10, 2021
6027ab2
ENH: New function to extract info relevant for IntendedFor
pvelasco Mar 15, 2021
4672145
RF: populate_intended_for uses now get_key_info_for_fmap_assignment
pvelasco Mar 15, 2021
1aa5f2d
Add functions to find fmaps compatible with runs/sessions
pvelasco Mar 18, 2021
ec38175
RF: rename variables
pvelasco Mar 22, 2021
c05b8cf
RF: `find_compatible_fmaps_*` now return compatible_fmap_groups
pvelasco Mar 22, 2021
af80739
Add `select_fmap_from_compatible_groups`, allowing the user to select…
pvelasco Mar 31, 2021
2f9055b
Fix test_get_key_info_for_fmap_assignment
pvelasco Apr 1, 2021
3d85e2b
RF: improve `populate_intended_for`
pvelasco Apr 1, 2021
394d8f9
RF: Add `remove_prefix`/`remove_suffix` for path strings
pvelasco Apr 1, 2021
af65665
Merge nipy/heudiconv:master into this branch
pvelasco Apr 2, 2021
bf8f0a9
$BF: Fix test_convert.test_convert()
pvelasco Apr 2, 2021
b5b5051
Merge branch 'adds_populate_intended_for' into adds_populate_intended…
pvelasco Apr 2, 2021
f947173
RF, ENH: Expand choices for `populate_intended_for`
pvelasco Apr 2, 2021
5615f99
Merge branch 'master' into adds_populate_intended_for
pvelasco Apr 6, 2021
8811491
BF: Fix op.join(tmpdir,...) in test_bids for Python3.5 compatibility
pvelasco Apr 6, 2021
e6ba946
Merge remote-tracking branch 'origin/adds_populate_intended_for' into…
pvelasco Apr 6, 2021
1e1ba4d
BF: Make sure the order of the test files is the intended one
pvelasco Apr 8, 2021
a0ee48e
BF: Make sure we remove the trailing op.sep in path
pvelasco Apr 8, 2021
09e0d1d
BF: Fixes test_convert.test_convert
pvelasco Apr 16, 2021
c38253b
ENH: `convert` now takes `populate_intended_for` params, from heuristic
pvelasco Apr 16, 2021
fa7b809
RF: test_convert -> test_populate_intended_for
pvelasco Apr 20, 2021
47bab3c
RF: Get rid of sys_modules call
pvelasco Apr 20, 2021
5b0595a
Change format of debug logger.
pvelasco Apr 20, 2021
0650b68
RF: utils.add_field_to_json -> utils.update_json
pvelasco Apr 20, 2021
ce1f0d2
Don't specify intent in `save_json`
pvelasco Apr 20, 2021
c4a2224
RF: Allow user to specify `pretty` argument for update_json
pvelasco Apr 20, 2021
267bf62
BF: Delete duplicated functions in bids.py
pvelasco Apr 20, 2021
c56cba4
Add documentation for `populate_intended_for`
pvelasco Apr 29, 2021
c4c7587
ENH: do use update_json for IntendedFor addition with pretty=True
yarikoptic Apr 30, 2021
467a261
Merge remote-tracking branch 'origin/master' into adds_populate_inten…
yarikoptic May 3, 2021
ae61382
Revert to importing specific functions in test_convert.
pvelasco May 3, 2021
f3dd790
Merge remote into local for branch 'adds_populate_intended_for'
pvelasco May 3, 2021
723fa67
BF: call `populate_intended_for` using the full session path
pvelasco May 3, 2021
5a06034
RF: only call `populate_intended_for` if heuristic specifies the options
pvelasco May 6, 2021
1bd151b
Add test for populate_intended_for using heuristic without POPULATE_I…
pvelasco May 10, 2021
ccb96df
RF: allow multiple parameters that need to be matched for populate_in…
pvelasco May 10, 2021
b72ecd8
Make `'ImagingVolume'` the default matching_parameters
pvelasco May 13, 2021
34c1b41
Add `'Force'` as `matching_parameter` option for fmap matching
pvelasco May 14, 2021
bdbc91e
RF: Add `'AcquisitionLabel'` as `matching_parameter` option for fmap …
pvelasco May 14, 2021
156ed9b
ENH: pass POPULATE_INTENDED_FOR_OPTS from heuristic upon command styl…
Jun 11, 2021
bf9a3e1
BF: Various minor fixes while trying to use with nibabel 3.2.1
Jun 11, 2021
bda7cc5
Cover the case key_info is a string; modify unit tests
pvelasco Jun 21, 2021
606a727
Remove import namedtuple (no longer needed)
pvelasco Jun 21, 2021
2886525
ENH: Add BIDSFile class
pvelasco Jun 21, 2021
d3b8210
Merge pull request #11 from cbinyu/BIDSFile_helper
pvelasco Jun 21, 2021
141f6e9
Merge pull request #34 from cbinyu/patch_dbic
yarikoptic Jun 21, 2021
0ecd867
Merge pull request #10 from dbic/adds_populate_intended_for
pvelasco Jun 22, 2021
1867898
Remove default arguments for populate-intended-for functions
pvelasco Jul 19, 2021
f8c5528
Update docs/heuristics.rst for RF matching_parameters key
pvelasco Jul 19, 2021
312e139
Update heudiconv/bids.py
pvelasco Sep 7, 2021
961fd83
Replace type check with isinstance
pvelasco Sep 8, 2021
51d1a96
ENH: Autopopulate subjects and sessions for populate-intended-for\n\n…
pvelasco Sep 8, 2021
d53e50b
Check arguments for populate_intended_for functions only at top level
pvelasco Sep 8, 2021
0f38e5e
Merge commit 'v0.10.0-10-g80a6538' (origin/master) into adds_populate…
yarikoptic Oct 28, 2021
88e93eb
When json field is list of numbers, change evaluation to proper block
Feb 11, 2022
717f500
Merge pull request #12 from neurorepro/adds_populate_intended_for
pvelasco Feb 11, 2022
784e946
Add matching by custom label
Feb 14, 2022
45af83a
Remove unused function parameters
Feb 14, 2022
4848977
Update new function docstring to include new parameter description
Feb 14, 2022
2f6a4e9
Correct docstrings for new parameter descriptions
Feb 14, 2022
457bf40
Correct random seeding by using random library instead of numpy
Feb 15, 2022
547dafd
Set label seed for the whole test suite and print it to stdout
Feb 15, 2022
4b23544
Merge pull request #13 from neurorepro/adds_populate_intended_for
pvelasco Feb 15, 2022
97b76f3
Merge remote-tracking branch 'origin/master' into adds_populate_inten…
yarikoptic Feb 23, 2022
1c5ca68
RF+typo fix: no need for explicit list in sorted()
yarikoptic Feb 23, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 146 additions & 0 deletions heudiconv/bids.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
save_json,
create_file_if_missing,
json_dumps_pretty,
add_field_to_json,
set_readonly,
is_readonly,
get_datetime,
Expand Down Expand Up @@ -46,6 +47,9 @@ class BIDSError(Exception):

BIDS_VERSION = "1.4.1"

SHIM_KEY = 'ShimSetting'
lgr.debug('shim_key: {}'.format(SHIM_KEY))


def populate_bids_templates(path, defaults={}):
"""Premake BIDS text files with templates"""
Expand Down Expand Up @@ -457,3 +461,145 @@ def convert_sid_bids(subject_id):
lgr.warning('{0} contained nonalphanumeric character(s), subject '
'ID was cleaned to be {1}'.format(subject_id, sid))
return sid, subject_id


def get_shim_setting(json_file):
"""
Gets the "ShimSetting" field from a json_file.
If not present:
- for fmap files: use the <acq> entity from the file name
- for other files: use the folder name ('anat', 'func', 'dwi', ...)

Parameters:
----------
json_file : str

Returns:
-------
str with "ShimSetting" value or <acq> entity
"""
data = load_json(json_file)
if SHIM_KEY in data.keys():
shims = data[SHIM_KEY]
else:
modality = op.basename(op.dirname(json_file))
if modality == 'fmap':
# extract the <acq> entity:
shims = re.search('(?<=[/_]acq-)\w+',json_file).group(0).split('_')[0]
if shims.lower() in ['fmri', 'bold']:
shims = 'func'
else:
shims = modality
return shims


def populate_intended_for(path_to_bids_session):
"""
Adds the 'IntendedFor' field to the fmap .json files in a session folder.
It goes through the session folders and checks what runs have the same
'ShimSetting' as the fmaps. If there is no 'ShimSetting' field in the json
file, we'll use the folder name ('func', 'dwi', 'anat') and see which fmap
with a matching '_acq' entity.

If several fmap runs have the same 'ShimSetting' (or '_acq'), it will use
the first one. Because fmaps come in groups (with reversed PE polarity,
or magnitude/phase), it adds the same runs to the 'IntendedFor' of the
corresponding fmaps by checking the '_acq' and '_run' entities.

Note: the logic behind the way we decide how to populate the "IntendedFor"
is: we want all images in the session (except for the fmap images
themselves) to have AT MOST one fmap. (That is, a pair of SE EPI with
reversed polarities, or a magnitude a phase field map). If there are more
than one fmap (more than a fmap pair) with the same acquisition parameters
as, say, a functional run, we will just assign that run to the FIRST pair,
while leaving the other fmap pairs without any assigned images. If the
user's intentions were different, he/she will have to manually edit the
fmap json files.

Parameters:
----------
path_to_bids_session : str or os.path
path to the session folder (or to the subject folder, if there are no
sessions).
"""
lgr.info('')
lgr.info('Adding "IntendedFor" to the fieldmaps in {}.'.format(path_to_bids_session))

# Resolve path (eliminate '..')
path_to_bids_session = op.abspath(path_to_bids_session)

# get the BIDS folder (if "data_folder" includes the session, remove it):
if op.basename(path_to_bids_session).startswith('ses-'):
bids_folder = op.dirname(path_to_bids_session)
else:
bids_folder = path_to_bids_session

fmap_dir = op.join(path_to_bids_session, 'fmap')
if not op.exists(fmap_dir):
lgr.warning('Fmap folder not found in {}.'.format(path_to_bids_session))
lgr.warning('We cannot add the IntendedFor field')
return

# Get a list of all fmap json files in the session:
# (we will remove elements later on, so don't just iterate)
fmap_jsons = sorted([j for j in glob(op.join(path_to_bids_session, 'fmap/*.json'))])

# Get a set with all non-fmap json files in the session (set is easier):
# We also exclude the SBRef files.
session_jsons = set(
j for j in glob(op.join(path_to_bids_session, '*/*.json')) if not (
j in fmap_jsons
# j[:-5] removes the '.json' from the end
or j[:-5].endswith('_sbref')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just thinking out loud: we so need "BIDS filename" parser -- our code is full of similar constructs. #452

)
)

# Loop through all the fmap json files and, for each one, find which other
# non-fmap images in the session have the same shim settings. Those that
# match are added to the intended_for list and removed from the list of
# non-fmap json files in the session (since they have already assigned to
# a fmap).
# After finishing with all the non-fmap images in the session, we go back
# to the fmap json file list, and find any other fmap json files of the
# same acquisition type and run number (because fmaps have several files:
# normal- and reversed-polarity, or magnitude and phase, etc.) We add the
# same IntendedFor list to those other corresponding fmap json files, and
# remove them from the list of available fmap json files.
# Once we have gone through all the fmap json files, we are done.
runs_accounted_for = set()
fmaps_accounted_for = set()
for fm_json in fmap_jsons:
if fm_json not in fmaps_accounted_for:
lgr.debug('Looking for runs for {}'.format(fm_json))
fm_shims = get_shim_setting(fm_json)

intended_for = []
for image_json in session_jsons:
image_shims = get_shim_setting(image_json)
if image_shims == fm_shims:
# BIDS specifies that the intended for are:
# - **image** files
# - path relative to the **subject level**
image_json_relative_path = op.relpath(image_json, start=bids_folder)
# image_json_relative_path[:-5] removes the '.json' extension:
intended_for.append(
image_json_relative_path[:-5] + '.nii.gz'
)
runs_accounted_for.add(image_json)
if len(intended_for) > 0:
intended_for = sorted([str(f) for f in intended_for])
# find all fmap json files with the same <acq> and <run> entities:
fm_json_name = op.basename(fm_json)
acq_match = re.findall('([/_]acq-([a-zA-Z0-9]*))', fm_json_name)
acq_str = acq_match[0][0] if acq_match else ''
run_match = re.findall('([/_]run-([a-zA-Z0-9]*))', fm_json_name)
run_str = run_match[0][0] if run_match else ''
# Loop through all the files that have the same "acq-" and "run-"
# Note: the following loop will also include 'fm_json'
for linked_fm_json in glob(op.join(path_to_bids_session, 'fmap/*' + acq_str + '*' + run_str + '*.json')):
# add the IntendedFor field to the json file:
add_field_to_json(linked_fm_json, {"IntendedFor": intended_for})
fmaps_accounted_for.update({linked_fm_json})
# Remove the runs accounted for from the session_jsons list, so that
# we don't assign another fmap to this image:
session_jsons -= runs_accounted_for
1 change: 1 addition & 0 deletions heudiconv/cli/run.py
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ def get_parser():
'heuristics', 'heuristic-info',
'ls', 'populate-templates',
'sanitize-jsons', 'treat-jsons',
'populate-intended-for'
),
help='Custom action to be performed on provided files instead of '
'regular operation.')
Expand Down
23 changes: 23 additions & 0 deletions heudiconv/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from .bids import (
convert_sid_bids,
populate_bids_templates,
populate_intended_for,
save_scans_key,
tuneup_bids_json_files,
add_participant_record,
Expand Down Expand Up @@ -521,6 +522,28 @@ def convert(items, converter, scaninfo_suffix, custom_callable, with_prov,
if custom_callable is not None:
custom_callable(*item)

# Populate "IntendedFor" for fmap files.
# Because fmap files can only be used to correct for distortions in images
# collected within the same scanning session, find unique subject/session
# combinations from the outname in each item:
outnames = [item[0] for item in items]
# - grab "sub-<sID>[/ses-<ses>]", and keep only unique ones:
try:
sessions = set(
re.search(
'sub-(?P<subj>[a-zA-Z0-9]*)([{0}_]ses-(?P<ses>[a-zA-Z0-9]*))?'.format(op.sep),
oname
).group(0)
for oname in outnames
)
except AttributeError:
# "sub-<sID>[/ses-<ses>]" is not present, so this is not BIDS compliant
# and it doesn't make sense to add "IntendedFor":
sessions = set()

for ses in sessions:
populate_intended_for(ses)


def convert_dicom(item_dicoms, bids_options, prefix,
outdir, tempdirs, symlink, overwrite):
Expand Down
12 changes: 9 additions & 3 deletions heudiconv/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import sys

from . import __version__, __packagename__
from .bids import populate_bids_templates, tuneup_bids_json_files
from .bids import populate_bids_templates, tuneup_bids_json_files, populate_intended_for
from .convert import prep_conversion
from .due import due, Doi
from .parser import get_study_sessions
Expand Down Expand Up @@ -47,13 +47,13 @@ def process_extra_commands(outdir, command, files, dicom_dir_template,
heuristic, session, subjs, grouping):
"""
Perform custom command instead of regular operations. Supported commands:
['treat-json', 'ls', 'populate-templates']
['treat-json', 'ls', 'populate-templates', 'populate-intended-for']

Parameters
----------
outdir : str
Output directory
command : {'treat-json', 'ls', 'populate-templates'}
command : {'treat-json', 'ls', 'populate-templates', 'populate-intended-for'}
Heudiconv command to run
files : list of str
List of files
Expand Down Expand Up @@ -108,6 +108,12 @@ def process_extra_commands(outdir, command, files, dicom_dir_template,
ensure_heuristic_arg(heuristic)
from .utils import get_heuristic_description
print(get_heuristic_description(heuristic, full=True))
elif command == 'populate-intended-for':
for subj in subjs:
session_path = op.join(outdir, 'sub-' + subj)
if session:
session_path = op.join(session_path, 'ses-' + session)
populate_intended_for(session_path)
else:
raise ValueError("Unknown command %s" % command)
return
Expand Down
Loading