-
Notifications
You must be signed in to change notification settings - Fork 126
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
[ENH] Adds extraction of physio signals from DICOMs #446
base: master
Are you sure you want to change the base?
Changes from 23 commits
f4e8cfd
e832ae6
aff779f
9de4bd6
e935490
9de5114
892bc6a
e2774f5
cca6c7c
532c630
4ac551e
5b33646
74d55d5
0fb0c2f
46c0bdb
371d006
81ec2a2
2768e7a
fe3fc34
38d6360
593062f
62841a7
5c78593
0745845
15e4a49
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -461,6 +461,8 @@ def convert(items, converter, scaninfo_suffix, custom_callable, with_prov, | |
if outtype == 'dicom': | ||
convert_dicom(item_dicoms, bids_options, prefix, | ||
outdir, tempdirs, symlink, overwrite) | ||
elif outtype == 'physio': | ||
convert_physio(item_dicoms, bids_options, prefix) | ||
elif outtype in ['nii', 'nii.gz']: | ||
assert converter == 'dcm2niix', ('Invalid converter ' | ||
'{}'.format(converter)) | ||
|
@@ -580,6 +582,49 @@ def convert_dicom(item_dicoms, bids_options, prefix, | |
shutil.copyfile(filename, outfile) | ||
|
||
|
||
def convert_physio(item_dicoms, bids_options, prefix): | ||
"""Save DICOM physiology as BIDS physio files | ||
|
||
Parameters | ||
pvelasco marked this conversation as resolved.
Show resolved
Hide resolved
|
||
---------- | ||
item_dicoms : list of filenames | ||
DICOMs to save | ||
bids_options : list or None | ||
If not None then save to BIDS format. List may be empty | ||
or contain bids specific options | ||
prefix : string | ||
Conversion outname | ||
|
||
Returns | ||
pvelasco marked this conversation as resolved.
Show resolved
Hide resolved
|
||
------- | ||
None | ||
""" | ||
if bids_options is None: | ||
return | ||
|
||
try: | ||
from bidsphysio.dcm2bids.dcm2bidsphysio import dcm2bids | ||
except ImportError: | ||
lgr.warning( | ||
"bidsphysio.dcm2bids not found. " | ||
"Not extracting physiological recordings." | ||
) | ||
return | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would be ok with erroring out here since if heuristic did instruct to get those |
||
|
||
item_dicoms = list(map(op.abspath, item_dicoms)) # absolute paths | ||
if len(item_dicoms) > 1: | ||
lgr.warning( | ||
"More than one PHYSIO file has been found for this series. " | ||
"If each file corresponds to a different signal, all is OK. " | ||
"If multiple files have the same signal, only the signal " | ||
"from the last file will be saved." | ||
) | ||
for dicom_file in item_dicoms: | ||
physio_data = dcm2bids(dicom_file) | ||
if physio_data.labels(): | ||
physio_data.save_to_bids_with_trigger(prefix) | ||
|
||
|
||
def nipype_convert(item_dicoms, prefix, with_prov, bids_options, tmpdir, dcmconfig=None): | ||
""" | ||
Converts DICOMs grouped from heuristic using Nipype's Dcm2niix interface. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
"""Heuristic demonstrating conversion of the PhoenixZIPReport from Siemens. | ||
|
||
It only cares about converting a series with have PhoenixZIPReport in their | ||
series_description and outputs **only to sourcedata**. | ||
""" | ||
|
||
|
||
def create_key(template, outtype=('nii.gz',), annotation_classes=None): | ||
if template is None or not template: | ||
raise ValueError('Template must be a valid format string') | ||
return template, outtype, annotation_classes | ||
|
||
|
||
def infotodict(seqinfo): | ||
"""Heuristic evaluator for determining which runs belong where | ||
|
||
allowed template fields - follow python string module: | ||
|
||
item: index within category | ||
subject: participant id | ||
seqitem: run number during scanning | ||
subindex: sub index within group | ||
""" | ||
sbref = create_key('sub-{subject}/func/sub-{subject}_task-QA_sbref', outtype=('nii.gz', 'dicom',)) | ||
scout = create_key('sub-{subject}/anat/sub-{subject}_T1w', outtype=('nii.gz', 'dicom',)) | ||
phoenix_doc = create_key('sub-{subject}/misc/sub-{subject}_phoenix', outtype=('dicom',)) | ||
|
||
info = {sbref: [], scout: [], phoenix_doc: []} | ||
for s in seqinfo: | ||
if ( | ||
'PhoenixZIPReport' in s.series_description | ||
and s.image_type[3] == 'CSA REPORT' | ||
): | ||
info[phoenix_doc].append({'item': s.series_id}) | ||
if 'scout' in s.series_description.lower(): | ||
info[scout].append({'item': s.series_id}) | ||
|
||
return info |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
""" | ||
Heuristic demonstrating extraction of physiological data from CMRR | ||
fMRI DICOMs | ||
|
||
We want to make sure the run number for the _sbref, _phase and | ||
_physio matches that of the corresponding _bold. For "normal" | ||
scanning, you can just rely on the {item} value, but if you have a | ||
functional run with just saving the magnitude and then one saving | ||
both magnitude and phase, you would have _run-01_bold, _run-02_bold | ||
and _run-01_phase, but the phase image corresponds to _run-02_bold, | ||
so the run number in the filename will not match | ||
""" | ||
|
||
|
||
def create_key(template, outtype=('nii.gz',), annotation_classes=None): | ||
if template is None or not template: | ||
raise ValueError('Template must be a valid format string') | ||
return template, outtype, annotation_classes | ||
|
||
def infotodict(seqinfo): | ||
"""Heuristic evaluator for determining which runs belong where | ||
|
||
allowed template fields - follow python string module: | ||
|
||
item: index within category | ||
subject: participant id | ||
seqitem: run number during scanning | ||
subindex: sub index within group | ||
""" | ||
|
||
info = {} | ||
run_no = 0 | ||
for idx, s in enumerate(seqinfo): | ||
# We want to make sure the _SBRef, PhysioLog and phase series | ||
# (if present) are labeled the same as the main (magnitude) | ||
# image. So we only focus on the magnitude series (to exclude | ||
# phase images) without _SBRef at the end of the series_ | ||
# description and then we search if the phase and/or _SBRef | ||
# are present. | ||
if ( | ||
'epfid2d' in s.sequence_name | ||
and ( | ||
'M' in s.image_type | ||
or 'FMRI' in s.image_type | ||
) | ||
and not s.series_description.lower().endswith('_sbref') | ||
and not 'DERIVED' in s.image_type | ||
): | ||
run_no += 1 | ||
bold = create_key( | ||
'sub-{subject}/func/sub-{subject}_task-test_run-%02d_bold' % run_no | ||
) | ||
info[bold] = [{'item': s.series_id}] | ||
next_series = idx+1 # used for physio log below | ||
|
||
### is phase image present? ### | ||
# At least for Siemens systems, if magnitude/phase was | ||
# selected, the phase images come as a separate series | ||
# immediatelly following the magnitude series. | ||
# (note: make sure you don't check beyond the number of | ||
# elements in seqinfo...) | ||
if ( | ||
idx+1 < len(seqinfo) | ||
and 'P' in seqinfo[idx+1].image_type | ||
): | ||
phase = create_key( | ||
'sub-{subject}/func/sub-{subject}_task-test_run-%02d_phase' % run_no | ||
) | ||
info[phase] = [{'item': seqinfo[idx+1].series_id}] | ||
next_series = idx+2 # used for physio log below | ||
|
||
### SBREF ### | ||
# here, within the functional run code, check to see if | ||
# the previous run's series_description ended in _sbref, | ||
# to assign the same run number. | ||
if ( | ||
idx > 0 | ||
and seqinfo[idx-1].series_description.lower().endswith('_sbref') | ||
): | ||
sbref = create_key( | ||
'sub-{subject}/func/sub-{subject}_task-test_run-%02d_sbref' % run_no | ||
) | ||
info[sbref] = [{'item': seqinfo[idx-1].series_id}] | ||
|
||
### PHYSIO LOG ### | ||
# here, within the functional run code, check to see if | ||
# the next run image_type lists "PHYSIO", to assign the | ||
# same run number. | ||
if ( | ||
next_series < len(seqinfo) | ||
and 'PHYSIO' in seqinfo[next_series].image_type | ||
): | ||
physio = create_key( | ||
'sub-{subject}/func/sub-{subject}_task-test_run-%02d_physio' % run_no, | ||
outtype = ('physio',) | ||
) | ||
info[physio] = [{'item': seqinfo[next_series].series_id}] | ||
|
||
return info |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -43,7 +43,10 @@ | |||||
'extras': [ | ||||||
'duecredit', # optional dependency | ||||||
], # Requires patched version ATM ['dcmstack'], | ||||||
'datalad': ['datalad >=%s' % MIN_DATALAD_VERSION] | ||||||
'datalad': ['datalad >=%s' % MIN_DATALAD_VERSION], | ||||||
'dcm2bids': [ | ||||||
pvelasco marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
'bidsphysio.dcm2bids >=1.4.3; python_version>"3.5"', # if dicoms with physio need to be converted | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
as 3.5 is already below what we support |
||||||
] | ||||||
} | ||||||
|
||||||
# Flatten the lists | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. all those binary files below adding up to over 1MB -- I think should be moved to some external dataset and used via Later we should improve this |
||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
samplePhysio dataset | ||
|
||
It contains phantom functional images and physiological recordings | ||
using CMRR Multi-Band EPI saved as DICOMs. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -46,7 +46,8 @@ | |
'patient_sex', # 23 | ||
'date', # 24 | ||
'series_uid', # 25 | ||
] | ||
'time', # 26 | ||
] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
||
SeqInfo = namedtuple('SeqInfo', seqinfo_fields) | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we would need to document that, e.g. at https://github.com/nipy/heudiconv/blob/HEAD/docs/heuristics.rst#infotodictseqinfos (make it all into a nice itemized list there for
nii
,dicom
and nowphysio
)