Newer
Older
#!/usr/bin/env python
#
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#

Paul McCarthy
committed
"""This module provides the :class:`.DicomImage` class, which represents a
volumetric DICOM data series. The ``DicomImage`` is simply an :class:`.`Image`
which provides accessors for additional DICOM meta data.

Paul McCarthy
committed
The following other functions are provided in this module, which are thin
wrappers around functionality provided by Chris Rorden's ``dcm2niix`` program:

Paul McCarthy
committed
.. autosummary::
:nosignatures:

Paul McCarthy
committed
scanDir

Paul McCarthy
committed
.. note:: These functions will not work if an executable called ``dcm2niix``
cannot be found.

Paul McCarthy
committed
.. see:: https://github.com/rordenlab/dcm2niix/
"""

Paul McCarthy
committed
import os.path as op
import subprocess as sp
import logging
import deprecation

Paul McCarthy
committed
import fsl.utils.memoize as memoize

Paul McCarthy
committed
log = logging.getLogger(__name__)

Paul McCarthy
committed
class DicomImage(fslimage.Image):
"""The ``DicomImage`` is a volumetric :class:`.Image` with some associated

Paul McCarthy
committed
DICOM metadata.
The ``Image`` class is used to manage the data and the voxel-to-world
transformation. Additional DICOM metadata may be accessed via the
:class:`.Image` metadata access methods.

Paul McCarthy
committed
"""
def __init__(self, image, metadata, dicomDir, *args, **kwargs):

Paul McCarthy
committed
"""Create a ``DicomImage``.
:arg image: Passed through to :meth:`.Image.__init__`.
:arg metadata: Dictionary containing DICOM meta-data.
:arg dicomDir: Directory that the dicom image was loaded from.

Paul McCarthy
committed
"""
self.__dicomDir = dicomDir
if metadata is not None:
for k, v in metadata.items():
self.setMeta(k, v)
@property
def dicomDir(self):
"""Returns the directory that the DICOM image data was loaded from. """
return self.__dicomDir
@deprecation.deprecated(deprecated_in='1.6.0',
removed_in='2.0.0',
details='Use metaKeys instead')
"""Deprecated - use :meth:`.Image.metaKeys`. """
return self.metaKeys()

Paul McCarthy
committed
@deprecation.deprecated(deprecated_in='1.6.0',
removed_in='2.0.0',
details='Use metaValues instead')
"""Deprecated - use :meth:`.Image.metaValues`. """
return self.metaValues()
@deprecation.deprecated(deprecated_in='1.6.0',
removed_in='2.0.0',
details='Use metaItems instead')
"""Deprecated - use :meth:`.Image.metaItems`. """
return self.metaItems()

Paul McCarthy
committed
@deprecation.deprecated(deprecated_in='1.6.0',
removed_in='2.0.0',
details='Use getMeta instead')
"""Deprecated - use :meth:`.Image.getMeta`. """
return self.getMeta(*args, **kwargs)

Paul McCarthy
committed
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
@memoize.memoize
def enabled():
"""Returns ``True`` if ``dcm2niix`` is present, and recent enough,
``False`` otherwise.
"""
cmd = 'dcm2niix -h'
minimumVersion = (1, 0, 2016, 9, 30)
versionPattern = re.compile('v'
'(?P<major>[0-9]+)\.'
'(?P<minor>[0-9]+)\.'
'(?P<year>[0-9]{4})'
'(?P<month>[0-9]{2})'
'(?P<day>[0-9]{2})')
try:
output = sp.check_output(cmd.split()).decode()
output = [l for l in output.split('\n') if 'version' in l.lower()]
output = '\n'.join(output).split()
for word in output:
match = re.match(versionPattern, word)
if match is None:
continue
installedVersion = (
int(match.group('major')),
int(match.group('minor')),
int(match.group('year')),
int(match.group('month')),
int(match.group('day')))
# make sure installed version
# is equal to or newer than
# minimum required version
for iv, mv in zip(installedVersion, minimumVersion):
if iv > mv: return True
elif iv < mv: return False
# if we get here, versions are equal
return True
except Exception as e:
log.debug('Error parsing dcm2niix version string: {}'.format(e))
return False
def scanDir(dcmdir):
"""Uses ``dcm2niix`` to scans the given DICOM directory, and returns a
list of dictionaries, one for each data series that was identified.
Each dictionary is populated with some basic metadata about the series.

Paul McCarthy
committed
:arg dcmdir: Directory containing DICOM files.

Paul McCarthy
committed
:returns: A list of dictionaries, each containing metadata about
one DICOM data series.

Paul McCarthy
committed
"""
if not enabled():
raise RuntimeError('dcm2niix is not available or is too old')

Paul McCarthy
committed
dcmdir = op.abspath(dcmdir)
cmd = 'dcm2niix -b o -ba n -f %s -o . {}'.format(dcmdir)
snumPattern = re.compile('^[0-9]+')

Paul McCarthy
committed
with open(os.devnull, 'wb') as devnull:
sp.call(cmd.split(), stdout=devnull, stderr=devnull)
files = glob.glob(op.join(td, '*.json'))
if len(files) == 0:
return []

Paul McCarthy
committed
# sort numerically by series number if possible
try:
def sortkey(f):
match = re.match(snumPattern, f)
snum = int(match.group(0))
return snum

Paul McCarthy
committed
files = sorted(files, key=sortkey)
except Exception:
files = sorted(files)
series = []
for fn in files:
with open(fn, 'rt') as f:
meta = json.load(f)
meta['DicomDir'] = dcmdir
series.append(meta)
return series
"""Takes a DICOM series meta data dictionary, as returned by
:func:`scanDir`, and loads the associated data as one or more NIFTI
images.
:arg series: Dictionary as returned by :func:`scanDir`, containing
meta data about one DICOM data series.
:returns: List containing one or more :class:`.DicomImage` objects.
"""

Paul McCarthy
committed
if not enabled():
raise RuntimeError('dcm2niix is not available or is too old')
dcmdir = series['DicomDir']
snum = series['SeriesNumber']
desc = series['SeriesDescription']
cmd = 'dcm2niix -b n -f %s -z n -o . {}'.format(dcmdir)

Paul McCarthy
committed

Paul McCarthy
committed
with open(os.devnull, 'wb') as devnull:
sp.call(cmd.split(), stdout=devnull, stderr=devnull)
files = glob.glob(op.join(td, '{}*.nii'.format(snum)))
images = [nib.load(f) for f in files]
# Force-load images into memory
[i.get_data() for i in images]

Paul McCarthy
committed
return [DicomImage(i, series, dcmdir, name=desc) for i in images]