From 5d2dd7cba1f506313514a8bbac99884b6a16c3b5 Mon Sep 17 00:00:00 2001
From: Paul McCarthy <pauldmccarthy@gmail.com>
Date: Sun, 10 Dec 2017 11:11:39 +1030
Subject: [PATCH] Wrappers around dcm2niix.

---
 fsl/data/dicom.py | 194 ++++++++++++++++++++++------------------------
 1 file changed, 91 insertions(+), 103 deletions(-)

diff --git a/fsl/data/dicom.py b/fsl/data/dicom.py
index 8fb388e6e..fe3795fe8 100644
--- a/fsl/data/dicom.py
+++ b/fsl/data/dicom.py
@@ -9,28 +9,32 @@ volumetric DICOM data series. The ``DicomImage`` is simply an :class:`.`Image`
 which provides accessors for additional DICOM meta data.
 
 The following other functions are provided in this module, which are thin
-wrappers around functionality provided by ``pydicom`` and ``dcmstack``:
+wrappers around functionality provided by Chris Rorden's ``dcm2niix`` program:
 
 .. autosummary::
    :nosignatures:
 
    scanDir
-   stack
-"""
+   loadNifti
 
+.. note:: These functions will not work if an executable called ``dcm2niix``
+          cannot be found.
 
-import os
-import fnmatch
+.. see:: https://github.com/rordenlab/dcm2niix/
+"""
 
-import pydicom as dicom
 
-from . import dcmstack
+import os.path    as op
+import subprocess as sp
+import               glob
+import               json
 
-from . import image as fslimage
+import fsl.utils.tempdir as tempdir
+import fsl.data.image    as fslimage
 
 
 class DicomImage(fslimage.Image):
-    """The ``DicomImage`` is a volumetric :class:`.Image` with associated
+    """The ``DicomImage`` is a volumetric :class:`.Image` with some associated
     DICOM metadata.
 
     The ``Image`` class is used to manage the data and the voxel-to-world
@@ -39,114 +43,98 @@ class DicomImage(fslimage.Image):
 
     def __init__(self, image, meta):
         """Create a ``DicomImage``.
+
+        :arg image: Passed through to :meth:`.Image.__init__`.
+        :arg meta:  Dictionary containing DICOM meta-data.
         """
         fslimage.Image.__init__(self, image)
+        self.__meta = meta
+
+
+    def keys(self):
+        """Returns the keys contained in the DICOM metadata dictionary
+        (``dict.keys``).
+        """
+        return self.__meta.keys()
 
 
-def scanDir(dcmdir, filePattern='*.dcm', callback=None):
-    """Recursively scans the given DICOM directory, and returns a dictionary
-    which contains all of the data series that were found.
+    def values(self):
+        """Returns the values contained in the DICOM metadata dictionary
+        (``dict.values``).
+        """
+        return self.__meta.values()
+
+
+    def items(self):
+        """Returns the items contained in the DICOM metadata dictionary
+        (``dict.items``).
+        """
+        return self.__meta.items()
 
-   :arg dcmdir:       Directory containing DICOM files.
 
-   :arg filePattern:  Glob-like pattern with which to identify DICOM files.
-                      Defaults to ``'*.dcm'``.
+    def get(self, *args, **kwargs):
+        """Returns the metadata value with the specified key (``dict.get``).
+        """
+        return self.__meta.get(*args, **kwargs)
 
-   :arg callback:     Function which will get called every time a file is
-                      loaded, and can be used for e.g. updating progress.
-                      Must accept three positional parameters:
-                        - ``path``: Path
-                        - ``n``:    Index of current path
-                        - ``ttl``:  Total number of paths
 
-                      After all files have been loaded, this function is called
-                      once more before the files are grouped into data series.
-                      For this final call, ``path is None``, and ``n == ttl``.
+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.
 
-    :returns:         A list containing one element for each identified data
-                      series. Each element itself is a list with one element
-                      for each file, where each element is a tuple containing
-                      the ``pydicom.dataset.FileDataset``, and a ``dict``
-                      containing some basic metadata extracted from the file.
+    :arg dcmdir: Directory containing DICOM files.
 
-    .. see:: ``dcmstack.parse_and_group`` and ``pydicom.dicomio.dcmread``.
+    :returns:    A list of dictionaries, each containing metadata about
+                 one DICOM data series.
     """
 
-    def default_callback(path, n, ttl):
-        pass
-
-    if callback is None:
-        callback = default_callback
-
-    # Find all the DICOM files in the directory.
-    # If/when we drop python < 3.5, we can use:
-    #
-    #   glob.glob(op.join(dcmdir, '**', filePattern), recursive=True)
-    dcmfiles = []
-    for root, dirnames, filenames in os.walk(dcmdir):
-        for filename in fnmatch.filter(filenames, filePattern):
-            dcmfiles.append(os.path.join(root, filename))
-
-    # No files found
-    if len(dcmfiles) == 0:
-        return {}
-
-    # Tell pydicom to only load the tags that
-    # are necessary to group files into series,
-    # and to give us basic metadata.
-    tags = [
-        'SeriesInstanceUID',
-        'SeriesNumber',
-        'SeriesDescription',
-        'ProtocolName',
-        'ImageOrientationPatient',
-        'Rows',
-        'Columns',
-        'PixelSpacing']
-
-    # Load the files one by one
-    dcms = []
-    for i, path in enumerate(dcmfiles):
-        callback(path, i, len(dcmfiles))
-        dcms.append(dicom.dcmread(path, defer_size=64, specific_tags=tags))
-
-    callback(None, len(dcmfiles), len(dcmfiles))
-
-    # Group the files into data series
-    series = dcmstack.parse_and_group(dcms)
-
-    # parse_and_group returns a dict, with
-    # one entry for each data series, where
-    # each entry is a list containing
-    #   (pydicom file, metadata, filepath)
-    #
-    # We don't care about the dict keys,
-    series = list(series.values())
-
-    return series
-
-
-def stack(series, callback=None):
-    """Takes a DICOM data series, as returned by :func:`scanDir`, and converts
-    it to a ``dcmstack.DicomStack``.
-
-    :arg series:
-
-    :arg callback:
-
-    :returns:
-    """
+    dcmdir = op.abspath(dcmdir)
+    cmd    = 'dcm2niix -b o -ba n -f %s -o . {}'.format(dcmdir)
 
-    def default_callback(path, n, ttl):
-        pass
+    with tempdir.tempdir() as td:
+
+        sp.call(cmd.split())
+
+        files = glob.glob(op.join(td, '*.json'))
+
+        if len(files) == 0:
+            return []
+
+        # sort numerically by series number
+        def sortkey(f):
+            return int(op.splitext(op.basename(f))[0])
+
+        files = sorted(files, key=sortkey)
+
+        series = []
+        for fn in files:
+            with open(fn, 'rt') as f:
+                meta = json.load(f)
+                meta['DicomDir'] = dcmdir
+                series.append(meta)
+
+        return series
+
+
+def loadNifti(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.
+    """
 
-    if callback is None:
-        callback = default_callback
+    dcmdir = series['DicomDir']
+    snum   = series['SeriesNumber']
+    cmd    = 'dcm2niix -b n -f %s -z n -o n {}'.format(dcmdir)
 
-    ds = dcmstack.DicomStack()
+    with tempdir.tempdir() as td:
+        sp.call(cmd.split())
 
-    for i, (_, meta, filename) in enumerate(series):
-        callback(filename, i, len(series))
-        ds.add_dcm(dicom.dcmread(filename), meta)
+        files = glob.glob(op.join(td, '{}.nii'.format(snum)))
 
-    return ds
+        return [DicomImage(f, series) for f in files]
-- 
GitLab