File: dicom.py

package info (click to toggle)
python-imageio 2.4.1-3
  • links: PTS, VCS
  • area: main
  • in suites: bullseye
  • size: 4,824 kB
  • sloc: python: 18,306; makefile: 145
file content (278 lines) | stat: -rw-r--r-- 10,408 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
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
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
# -*- coding: utf-8 -*-
# imageio is distributed under the terms of the (new) BSD License.

""" Plugin for reading DICOM files.
"""

# todo: Use pydicom:
# * Note: is not py3k ready yet
# * Allow reading the full meta info
# I think we can more or less replace the SimpleDicomReader with a
# pydicom.Dataset For series, only ned to read the full info from one
# file: speed still high
# * Perhaps allow writing?

from __future__ import absolute_import, print_function, division

import os
import sys
import subprocess

from .. import formats
from ..core import Format, BaseProgressIndicator, StdoutProgressIndicator
from ..core import read_n_bytes

_dicom = None  # lazily loaded in load_lib()


def load_lib():
    global _dicom
    from . import _dicom

    return _dicom


# Determine endianity of system
sys_is_little_endian = sys.byteorder == "little"


def get_dcmdjpeg_exe():
    fname = "dcmdjpeg" + ".exe" * sys.platform.startswith("win")
    for dir in (
        "c:\\dcmtk",
        "c:\\Program Files",
        "c:\\Program Files\\dcmtk",
        "c:\\Program Files (x86)\\dcmtk",
    ):
        filename = os.path.join(dir, fname)
        if os.path.isfile(filename):
            return filename

    try:
        subprocess.check_call([fname, "--version"], shell=True)
        return fname
    except Exception:
        return None


class DicomFormat(Format):
    """ A format for reading DICOM images: a common format used to store
    medical image data, such as X-ray, CT and MRI.
    
    This format borrows some code (and ideas) from the pydicom project,
    and (to the best of our knowledge) has the same limitations as
    pydicom with regard to the type of files that it can handle. However,
    only a predefined subset of tags are extracted from the file. This allows
    for great simplifications allowing us to make a stand-alone reader, and
    also results in a much faster read time. We plan to allow reading all
    tags in the future (by using pydicom).
    
    This format provides functionality to group images of the same
    series together, thus extracting volumes (and multiple volumes).
    Using volread will attempt to yield a volume. If multiple volumes
    are present, the first one is given. Using mimread will simply yield
    all images in the given directory (not taking series into account).
    
    Parameters for reading
    ----------------------
    progress : {True, False, BaseProgressIndicator}
        Whether to show progress when reading from multiple files.
        Default True. By passing an object that inherits from
        BaseProgressIndicator, the way in which progress is reported
        can be costumized.
    
    """

    def _can_read(self, request):
        # If user URI was a directory, we check whether it has a DICOM file
        if os.path.isdir(request.filename):
            files = os.listdir(request.filename)
            for fname in sorted(files):  # Sorting make it consistent
                filename = os.path.join(request.filename, fname)
                if os.path.isfile(filename) and "DICOMDIR" not in fname:
                    with open(filename, "rb") as f:
                        first_bytes = read_n_bytes(f, 140)
                    return first_bytes[128:132] == b"DICM"
            else:
                return False
        # Check
        return request.firstbytes[128:132] == b"DICM"

    def _can_write(self, request):
        # We cannot save yet. May be possible if we will used pydicom as
        # a backend.
        return False

    # --

    class Reader(Format.Reader):
        def _open(self, progress=True):
            if not _dicom:
                load_lib()
            if os.path.isdir(self.request.filename):
                # A dir can be given if the user used the format explicitly
                self._info = {}
                self._data = None
            else:
                # Read the given dataset now ...
                try:
                    dcm = _dicom.SimpleDicomReader(self.request.get_file())
                except _dicom.CompressedDicom as err:
                    if "JPEG" in str(err):
                        exe = get_dcmdjpeg_exe()
                        if not exe:
                            raise
                        fname1 = self.request.get_local_filename()
                        fname2 = fname1 + ".raw"
                        try:
                            subprocess.check_call([exe, fname1, fname2], shell=True)
                        except Exception:
                            raise err
                        print(
                            "DICOM file contained compressed data. "
                            "Used dcmtk to convert it."
                        )
                        dcm = _dicom.SimpleDicomReader(fname2)
                    else:
                        raise

                self._info = dcm._info
                self._data = dcm.get_numpy_array()

            # Initialize series, list of DicomSeries objects
            self._series = None  # only created if needed

            # Set progress indicator
            if isinstance(progress, BaseProgressIndicator):
                self._progressIndicator = progress
            elif progress is True:
                p = StdoutProgressIndicator("Reading DICOM")
                self._progressIndicator = p
            elif progress in (None, False):
                self._progressIndicator = BaseProgressIndicator("Dummy")
            else:
                raise ValueError("Invalid value for progress.")

        def _close(self):
            # Clean up
            self._info = None
            self._data = None
            self._series = None

        @property
        def series(self):
            if self._series is None:
                pi = self._progressIndicator
                self._series = _dicom.process_directory(self.request, pi)
            return self._series

        def _get_length(self):
            if self._data is None:
                dcm = self.series[0][0]
                self._info = dcm._info
                self._data = dcm.get_numpy_array()

            nslices = self._data.shape[0] if (self._data.ndim == 3) else 1

            if self.request.mode[1] == "i":
                # User expects one, but lets be honest about this file
                return nslices
            elif self.request.mode[1] == "I":
                # User expects multiple, if this file has multiple slices, ok.
                # Otherwise we have to check the series.
                if nslices > 1:
                    return nslices
                else:
                    return sum([len(serie) for serie in self.series])
            elif self.request.mode[1] == "v":
                # User expects a volume, if this file has one, ok.
                # Otherwise we have to check the series
                if nslices > 1:
                    return 1
                else:
                    return len(self.series)  # We assume one volume per series
            elif self.request.mode[1] == "V":
                # User expects multiple volumes. We have to check the series
                return len(self.series)  # We assume one volume per series
            else:
                raise RuntimeError("DICOM plugin should know what to expect.")

        def _get_data(self, index):
            if self._data is None:
                dcm = self.series[0][0]
                self._info = dcm._info
                self._data = dcm.get_numpy_array()

            nslices = self._data.shape[0] if (self._data.ndim == 3) else 1

            if self.request.mode[1] == "i":
                # Allow index >1 only if this file contains >1
                if nslices > 1:
                    return self._data[index], self._info
                elif index == 0:
                    return self._data, self._info
                else:
                    raise IndexError("Dicom file contains only one slice.")
            elif self.request.mode[1] == "I":
                # Return slice from volume, or return item from series
                if index == 0 and nslices > 1:
                    return self._data[index], self._info
                else:
                    L = []
                    for serie in self.series:
                        L.extend([dcm_ for dcm_ in serie])
                    return L[index].get_numpy_array(), L[index].info
            elif self.request.mode[1] in "vV":
                # Return volume or series
                if index == 0 and nslices > 1:
                    return self._data, self._info
                else:
                    return (
                        self.series[index].get_numpy_array(),
                        self.series[index].info,
                    )
            else:  # pragma: no cover
                raise ValueError("DICOM plugin should know what to expect.")

        def _get_meta_data(self, index):
            if self._data is None:
                dcm = self.series[0][0]
                self._info = dcm._info
                self._data = dcm.get_numpy_array()

            nslices = self._data.shape[0] if (self._data.ndim == 3) else 1

            # Default is the meta data of the given file, or the "first" file.
            if index is None:
                return self._info

            if self.request.mode[1] == "i":
                return self._info
            elif self.request.mode[1] == "I":
                # Return slice from volume, or return item from series
                if index == 0 and nslices > 1:
                    return self._info
                else:
                    L = []
                    for serie in self.series:
                        L.extend([dcm_ for dcm_ in serie])
                    return L[index].info
            elif self.request.mode[1] in "vV":
                # Return volume or series
                if index == 0 and nslices > 1:
                    return self._info
                else:
                    return self.series[index].info
            else:  # pragma: no cover
                raise ValueError("DICOM plugin should know what to expect.")


# Add this format
formats.add_format(
    DicomFormat(
        "DICOM",
        "Digital Imaging and Communications in Medicine",
        ".dcm .ct .mri",
        "iIvV",
    )
)  # Often DICOM files have weird or no extensions