dicom.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. # -*- coding: utf-8 -*-
  2. # imageio is distributed under the terms of the (new) BSD License.
  3. """Read DICOM files.
  4. Backend Library: internal
  5. A format for reading DICOM images: a common format used to store
  6. medical image data, such as X-ray, CT and MRI.
  7. This format borrows some code (and ideas) from the pydicom project. However,
  8. only a predefined subset of tags are extracted from the file. This allows
  9. for great simplifications allowing us to make a stand-alone reader, and
  10. also results in a much faster read time.
  11. By default, only uncompressed and deflated transfer syntaxes are supported.
  12. If gdcm or dcmtk is installed, these will be used to automatically convert
  13. the data. See https://github.com/malaterre/GDCM/releases for installing GDCM.
  14. This format provides functionality to group images of the same
  15. series together, thus extracting volumes (and multiple volumes).
  16. Using volread will attempt to yield a volume. If multiple volumes
  17. are present, the first one is given. Using mimread will simply yield
  18. all images in the given directory (not taking series into account).
  19. Parameters
  20. ----------
  21. progress : {True, False, BaseProgressIndicator}
  22. Whether to show progress when reading from multiple files.
  23. Default True. By passing an object that inherits from
  24. BaseProgressIndicator, the way in which progress is reported
  25. can be costumized.
  26. """
  27. # todo: Use pydicom:
  28. # * Note: is not py3k ready yet
  29. # * Allow reading the full meta info
  30. # I think we can more or less replace the SimpleDicomReader with a
  31. # pydicom.Dataset For series, only ned to read the full info from one
  32. # file: speed still high
  33. # * Perhaps allow writing?
  34. import os
  35. import sys
  36. import logging
  37. import subprocess
  38. from ..core import Format, BaseProgressIndicator, StdoutProgressIndicator
  39. from ..core import read_n_bytes
  40. _dicom = None # lazily loaded in load_lib()
  41. logger = logging.getLogger(__name__)
  42. def load_lib():
  43. global _dicom
  44. from . import _dicom
  45. return _dicom
  46. # Determine endianity of system
  47. sys_is_little_endian = sys.byteorder == "little"
  48. def get_dcmdjpeg_exe():
  49. fname = "dcmdjpeg" + ".exe" * sys.platform.startswith("win")
  50. for dir in (
  51. "c:\\dcmtk",
  52. "c:\\Program Files",
  53. "c:\\Program Files\\dcmtk",
  54. "c:\\Program Files (x86)\\dcmtk",
  55. ):
  56. filename = os.path.join(dir, fname)
  57. if os.path.isfile(filename):
  58. return [filename]
  59. try:
  60. subprocess.check_call([fname, "--version"])
  61. return [fname]
  62. except Exception:
  63. return None
  64. def get_gdcmconv_exe():
  65. fname = "gdcmconv" + ".exe" * sys.platform.startswith("win")
  66. # Maybe it's on the path
  67. try:
  68. subprocess.check_call([fname, "--version"])
  69. return [fname, "--raw"]
  70. except Exception:
  71. pass
  72. # Select directories where it could be
  73. candidates = []
  74. base_dirs = [r"c:\Program Files"]
  75. for base_dir in base_dirs:
  76. if os.path.isdir(base_dir):
  77. for dname in os.listdir(base_dir):
  78. if dname.lower().startswith("gdcm"):
  79. suffix = dname[4:].strip()
  80. candidates.append((suffix, os.path.join(base_dir, dname)))
  81. # Sort, so higher versions are tried earlier
  82. candidates.sort(reverse=True)
  83. # Select executable
  84. filename = None
  85. for _, dirname in candidates:
  86. exe1 = os.path.join(dirname, "gdcmconv.exe")
  87. exe2 = os.path.join(dirname, "bin", "gdcmconv.exe")
  88. if os.path.isfile(exe1):
  89. filename = exe1
  90. break
  91. if os.path.isfile(exe2):
  92. filename = exe2
  93. break
  94. else:
  95. return None
  96. return [filename, "--raw"]
  97. class DicomFormat(Format):
  98. """See :mod:`imageio.plugins.dicom`"""
  99. def _can_read(self, request):
  100. # If user URI was a directory, we check whether it has a DICOM file
  101. if os.path.isdir(request.filename):
  102. files = os.listdir(request.filename)
  103. for fname in sorted(files): # Sorting make it consistent
  104. filename = os.path.join(request.filename, fname)
  105. if os.path.isfile(filename) and "DICOMDIR" not in fname:
  106. with open(filename, "rb") as f:
  107. first_bytes = read_n_bytes(f, 140)
  108. return first_bytes[128:132] == b"DICM"
  109. else:
  110. return False
  111. # Check
  112. return request.firstbytes[128:132] == b"DICM"
  113. def _can_write(self, request):
  114. # We cannot save yet. May be possible if we will used pydicom as
  115. # a backend.
  116. return False
  117. # --
  118. class Reader(Format.Reader):
  119. _compressed_warning_dirs = set()
  120. def _open(self, progress=True):
  121. if not _dicom:
  122. load_lib()
  123. if os.path.isdir(self.request.filename):
  124. # A dir can be given if the user used the format explicitly
  125. self._info = {}
  126. self._data = None
  127. else:
  128. # Read the given dataset now ...
  129. try:
  130. dcm = _dicom.SimpleDicomReader(self.request.get_file())
  131. except _dicom.CompressedDicom as err:
  132. # We cannot do this on our own. Perhaps with some help ...
  133. cmd = get_gdcmconv_exe()
  134. if not cmd and "JPEG" in str(err):
  135. cmd = get_dcmdjpeg_exe()
  136. if not cmd:
  137. msg = err.args[0].replace("using", "installing")
  138. msg = msg.replace("convert", "auto-convert")
  139. err.args = (msg,)
  140. raise
  141. else:
  142. fname1 = self.request.get_local_filename()
  143. fname2 = fname1 + ".raw"
  144. try:
  145. subprocess.check_call(cmd + [fname1, fname2])
  146. except Exception:
  147. raise err
  148. d = os.path.dirname(fname1)
  149. if d not in self._compressed_warning_dirs:
  150. self._compressed_warning_dirs.add(d)
  151. logger.warning(
  152. "DICOM file contained compressed data. "
  153. + "Autoconverting with "
  154. + cmd[0]
  155. + " (this warning is shown once for each directory)"
  156. )
  157. dcm = _dicom.SimpleDicomReader(fname2)
  158. self._info = dcm._info
  159. self._data = dcm.get_numpy_array()
  160. # Initialize series, list of DicomSeries objects
  161. self._series = None # only created if needed
  162. # Set progress indicator
  163. if isinstance(progress, BaseProgressIndicator):
  164. self._progressIndicator = progress
  165. elif progress is True:
  166. p = StdoutProgressIndicator("Reading DICOM")
  167. self._progressIndicator = p
  168. elif progress in (None, False):
  169. self._progressIndicator = BaseProgressIndicator("Dummy")
  170. else:
  171. raise ValueError("Invalid value for progress.")
  172. def _close(self):
  173. # Clean up
  174. self._info = None
  175. self._data = None
  176. self._series = None
  177. @property
  178. def series(self):
  179. if self._series is None:
  180. pi = self._progressIndicator
  181. self._series = _dicom.process_directory(self.request, pi)
  182. return self._series
  183. def _get_length(self):
  184. if self._data is None:
  185. dcm = self.series[0][0]
  186. self._info = dcm._info
  187. self._data = dcm.get_numpy_array()
  188. nslices = self._data.shape[0] if (self._data.ndim == 3) else 1
  189. if self.request.mode[1] == "i":
  190. # User expects one, but lets be honest about this file
  191. return nslices
  192. elif self.request.mode[1] == "I":
  193. # User expects multiple, if this file has multiple slices, ok.
  194. # Otherwise we have to check the series.
  195. if nslices > 1:
  196. return nslices
  197. else:
  198. return sum([len(serie) for serie in self.series])
  199. elif self.request.mode[1] == "v":
  200. # User expects a volume, if this file has one, ok.
  201. # Otherwise we have to check the series
  202. if nslices > 1:
  203. return 1
  204. else:
  205. return len(self.series) # We assume one volume per series
  206. elif self.request.mode[1] == "V":
  207. # User expects multiple volumes. We have to check the series
  208. return len(self.series) # We assume one volume per series
  209. else:
  210. raise RuntimeError("DICOM plugin should know what to expect.")
  211. def _get_slice_data(self, index):
  212. nslices = self._data.shape[0] if (self._data.ndim == 3) else 1
  213. # Allow index >1 only if this file contains >1
  214. if nslices > 1:
  215. return self._data[index], self._info
  216. elif index == 0:
  217. return self._data, self._info
  218. else:
  219. raise IndexError("Dicom file contains only one slice.")
  220. def _get_data(self, index):
  221. if self._data is None:
  222. dcm = self.series[0][0]
  223. self._info = dcm._info
  224. self._data = dcm.get_numpy_array()
  225. nslices = self._data.shape[0] if (self._data.ndim == 3) else 1
  226. if self.request.mode[1] == "i":
  227. return self._get_slice_data(index)
  228. elif self.request.mode[1] == "I":
  229. # Return slice from volume, or return item from series
  230. if index == 0 and nslices > 1:
  231. return self._data[index], self._info
  232. else:
  233. L = []
  234. for serie in self.series:
  235. L.extend([dcm_ for dcm_ in serie])
  236. return L[index].get_numpy_array(), L[index].info
  237. elif self.request.mode[1] in "vV":
  238. # Return volume or series
  239. if index == 0 and nslices > 1:
  240. return self._data, self._info
  241. else:
  242. return (
  243. self.series[index].get_numpy_array(),
  244. self.series[index].info,
  245. )
  246. # mode is `?` (typically because we are using V3). If there is a
  247. # series (multiple files), index referrs to the element of the
  248. # series and we read volumes. If there is no series, index
  249. # referrs to the slice in the volume we read "flat" images.
  250. elif len(self.series) > 1:
  251. # mode is `?` and there are multiple series. Each series is a ndimage.
  252. return (
  253. self.series[index].get_numpy_array(),
  254. self.series[index].info,
  255. )
  256. else:
  257. # mode is `?` and there is only one series. Each slice is an ndimage.
  258. return self._get_slice_data(index)
  259. def _get_meta_data(self, index):
  260. if self._data is None:
  261. dcm = self.series[0][0]
  262. self._info = dcm._info
  263. self._data = dcm.get_numpy_array()
  264. nslices = self._data.shape[0] if (self._data.ndim == 3) else 1
  265. # Default is the meta data of the given file, or the "first" file.
  266. if index is None:
  267. return self._info
  268. if self.request.mode[1] == "i":
  269. return self._info
  270. elif self.request.mode[1] == "I":
  271. # Return slice from volume, or return item from series
  272. if index == 0 and nslices > 1:
  273. return self._info
  274. else:
  275. L = []
  276. for serie in self.series:
  277. L.extend([dcm_ for dcm_ in serie])
  278. return L[index].info
  279. elif self.request.mode[1] in "vV":
  280. # Return volume or series
  281. if index == 0 and nslices > 1:
  282. return self._info
  283. else:
  284. return self.series[index].info
  285. else: # pragma: no cover
  286. raise ValueError("DICOM plugin should know what to expect.")