fr/fr_env/lib/python3.8/site-packages/imageio/plugins/dicom.py

328 lines
12 KiB
Python

# -*- 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?
import os
import sys
import logging
import subprocess
from .. import formats
from ..core import Format, BaseProgressIndicator, StdoutProgressIndicator
from ..core import read_n_bytes
_dicom = None # lazily loaded in load_lib()
logger = logging.getLogger(__name__)
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"])
return [fname]
except Exception:
return None
def get_gdcmconv_exe():
fname = "gdcmconv" + ".exe" * sys.platform.startswith("win")
# Maybe it's on the path
try:
subprocess.check_call([fname, "--version"])
return [fname, "--raw"]
except Exception:
pass
# Select directories where it could be
candidates = []
base_dirs = [r"c:\Program Files"]
for base_dir in base_dirs:
if os.path.isdir(base_dir):
for dname in os.listdir(base_dir):
if dname.lower().startswith("gdcm"):
suffix = dname[4:].strip()
candidates.append((suffix, os.path.join(base_dir, dname)))
# Sort, so higher versions are tried earlier
candidates.sort(reverse=True)
# Select executable
filename = None
for _, dirname in candidates:
exe1 = os.path.join(dirname, "gdcmconv.exe")
exe2 = os.path.join(dirname, "bin", "gdcmconv.exe")
if os.path.isfile(exe1):
filename = exe1
break
if os.path.isfile(exe2):
filename = exe2
break
else:
return None
return [filename, "--raw"]
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. 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.
By default, only uncompressed and deflated transfer syntaxes are supported.
If gdcm or dcmtk is installed, these will be used to automatically convert
the data. See https://github.com/malaterre/GDCM/releases for installing GDCM.
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):
_compressed_warning_dirs = set()
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:
# We cannot do this on our own. Perhaps with some help ...
cmd = get_gdcmconv_exe()
if not cmd and "JPEG" in str(err):
cmd = get_dcmdjpeg_exe()
if not cmd:
msg = err.args[0].replace("using", "installing")
msg = msg.replace("convert", "auto-convert")
err.args = (msg,)
raise
else:
fname1 = self.request.get_local_filename()
fname2 = fname1 + ".raw"
try:
subprocess.check_call(cmd + [fname1, fname2])
except Exception:
raise err
d = os.path.dirname(fname1)
if d not in self._compressed_warning_dirs:
self._compressed_warning_dirs.add(d)
logger.warning(
"DICOM file contained compressed data. "
+ "Autoconverting with "
+ cmd[0]
+ " (this warning is shown once for each directory)"
)
dcm = _dicom.SimpleDicomReader(fname2)
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