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
|