Skip to content

Commit

Permalink
Merge pull request #9 from jond01/chore/modernize/black
Browse files Browse the repository at this point in the history
Format all the code base with `black`
  • Loading branch information
elazarcoh committed Mar 6, 2022
2 parents b865ed8 + 49f4ad1 commit d7dac3c
Show file tree
Hide file tree
Showing 19 changed files with 705 additions and 242 deletions.
4 changes: 2 additions & 2 deletions medio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
from medio.read_save import read_img, save_img, save_dir
from medio import backends, metadata, medimg, utils

__version__ = '0.4.1'
__version__ = "0.4.1"

__all__ = ['read_img', 'save_img', 'save_dir', 'MetaData', 'Affine', '__version__']
__all__ = ["read_img", "save_img", "save_dir", "MetaData", "Affine", "__version__"]
195 changes: 136 additions & 59 deletions medio/backends/itk_io.py

Large diffs are not rendered by default.

47 changes: 26 additions & 21 deletions medio/backends/nib_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,11 @@


class NibIO:
coord_sys = 'nib'
RGB_DTYPE = np.dtype([('R', np.uint8),
('G', np.uint8),
('B', np.uint8)])
RGBA_DTYPE = np.dtype([('R', np.uint8),
('G', np.uint8),
('B', np.uint8),
('A', np.uint8)])
coord_sys = "nib"
RGB_DTYPE = np.dtype([("R", np.uint8), ("G", np.uint8), ("B", np.uint8)])
RGBA_DTYPE = np.dtype(
[("R", np.uint8), ("G", np.uint8), ("B", np.uint8), ("A", np.uint8)]
)

@staticmethod
def read_img(input_path, desired_axcodes=None, header=False, channels_axis=None):
Expand All @@ -28,16 +25,20 @@ def read_img(input_path, desired_axcodes=None, header=False, channels_axis=None)
:return: image array and corresponding metadata
"""
img_struct = nib.load(input_path)
orig_ornt_str = ''.join(nib.aff2axcodes(img_struct.affine))
orig_ornt_str = "".join(nib.aff2axcodes(img_struct.affine))
if desired_axcodes is not None:
img_struct = NibIO.reorient(img_struct, desired_axcodes)
img = np.asanyarray(img_struct.dataobj)
if channels_axis is not None:
img = NibIO.unravel_array(img, channels_axis)
affine = Affine(img_struct.affine)
metadata = MetaData(affine=affine, orig_ornt=orig_ornt_str, coord_sys=NibIO.coord_sys)
metadata = MetaData(
affine=affine, orig_ornt=orig_ornt_str, coord_sys=NibIO.coord_sys
)
if header:
metadata.header = {key: img_struct.header[key] for key in img_struct.header.keys()}
metadata.header = {
key: img_struct.header[key] for key in img_struct.header.keys()
}
return img, metadata

@staticmethod
Expand Down Expand Up @@ -79,30 +80,34 @@ def unravel_array(array, channels_axis=-1):
np.dtype([('R', 'uint8'), ('G', 'uint8'), ('B', 'uint8')])
Convert it into an array with dtype 'uint8' and 3 channels for RGB in an additional last dimension"""
dtype = array.dtype
if not (hasattr(dtype, '__len__') and len(dtype) > 1):
if not (hasattr(dtype, "__len__") and len(dtype) > 1):
return array
return np.stack([array[field] for field in dtype.fields], axis=channels_axis)

@staticmethod
def pack_channeled_img(img, channels_axis):
dtype = img.dtype
if not np.issubdtype(dtype, np.uint8):
raise ValueError(f'RGB or RGBA images must have dtype "np.uint8", got: "{dtype}"')
raise ValueError(
f'RGB or RGBA images must have dtype "np.uint8", got: "{dtype}"'
)
n_channels = img.shape[channels_axis]
img = np.moveaxis(img, channels_axis, -1)
r_channel = img[..., 0]
if n_channels == 3:
img_rgb = np.empty_like(r_channel, dtype=NibIO.RGB_DTYPE)
img_rgb['R'] = r_channel
img_rgb['G'] = img[..., 1]
img_rgb['B'] = img[..., 2]
img_rgb["R"] = r_channel
img_rgb["G"] = img[..., 1]
img_rgb["B"] = img[..., 2]
return img_rgb
elif n_channels == 4:
img_rgba = np.empty_like(r_channel, dtype=NibIO.RGBA_DTYPE)
img_rgba['R'] = r_channel
img_rgba['G'] = img[..., 1]
img_rgba['B'] = img[..., 2]
img_rgba['A'] = img[..., 3]
img_rgba["R"] = r_channel
img_rgba["G"] = img[..., 1]
img_rgba["B"] = img[..., 2]
img_rgba["A"] = img[..., 3]
return img_rgba
else:
raise ValueError(f'Invalid number of channels: {n_channels}, should be 3 (RGB) or 4 (RGBA)')
raise ValueError(
f"Invalid number of channels: {n_channels}, should be 3 (RGB) or 4 (RGBA)"
)
94 changes: 69 additions & 25 deletions medio/backends/pdcm_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,22 @@


class PdcmIO:
coord_sys = 'itk'
coord_sys = "itk"
# channels axes in the transposed image for pydicom and dicom-numpy. The actual axis is the first or the second
# value of the tuple, according to the planar configuration (which is either 0 or 1)
DEFAULT_CHANNELS_AXES_PYDICOM = (0, -1)
DEFAULT_CHANNELS_AXES_DICOM_NUMPY = (0, 2)

@staticmethod
def read_img(input_path, desired_ornt=None, header=False, channels_axis=None, globber='*',
allow_default_affine=False, series=None):
def read_img(
input_path,
desired_ornt=None,
header=False,
channels_axis=None,
globber="*",
allow_default_affine=False,
series=None,
):
"""
Read a dicom file or folder (series) and return the numpy array and the corresponding metadata
:param input_path: path-like object (str or pathlib.Path) of the file or directory to read
Expand All @@ -37,21 +44,33 @@ def read_img(input_path, desired_ornt=None, header=False, channels_axis=None, gl
:return: numpy array and metadata
"""
input_path = Path(input_path)
temp_channels_axis = -1 # if there are channels, they must be in the last axis for the reorientation
# if there are channels, they must be in the last axis for the reorientation
temp_channels_axis = -1
if input_path.is_dir():
img, metadata, channeled = PdcmIO.read_dcm_dir(input_path, header, globber,
channels_axis=temp_channels_axis, series=series)
img, metadata, channeled = PdcmIO.read_dcm_dir(
input_path,
header,
globber,
channels_axis=temp_channels_axis,
series=series,
)
else:
img, metadata, channeled = PdcmIO.read_dcm_file(
input_path, header, allow_default_affine=allow_default_affine, channels_axis=temp_channels_axis)
input_path,
header,
allow_default_affine=allow_default_affine,
channels_axis=temp_channels_axis,
)
img, metadata = PdcmIO.reorient(img, metadata, desired_ornt)
# move the channels after the reorientation
if channeled and channels_axis != temp_channels_axis:
img = np.moveaxis(img, temp_channels_axis, channels_axis)
return img, metadata

@staticmethod
def read_dcm_file(filename, header=False, allow_default_affine=False, channels_axis=None):
def read_dcm_file(
filename, header=False, allow_default_affine=False, channels_axis=None
):
"""
Read a single dicom file.
Return the image array, metadata, and whether it has channels
Expand All @@ -66,13 +85,19 @@ def read_dcm_file(filename, header=False, allow_default_affine=False, channels_a
if header:
metadata.header = {str(key): ds[key] for key in ds.keys()}
samples_per_pixel = ds.SamplesPerPixel
img = PdcmIO.move_channels_axis(img, samples_per_pixel=samples_per_pixel, channels_axis=channels_axis,
planar_configuration=ds.get('PlanarConfiguration', None),
default_axes=PdcmIO.DEFAULT_CHANNELS_AXES_PYDICOM)
img = PdcmIO.move_channels_axis(
img,
samples_per_pixel=samples_per_pixel,
channels_axis=channels_axis,
planar_configuration=ds.get("PlanarConfiguration", None),
default_axes=PdcmIO.DEFAULT_CHANNELS_AXES_PYDICOM,
)
return img, metadata, samples_per_pixel > 1

@staticmethod
def read_dcm_dir(input_dir, header=False, globber='*', channels_axis=None, series=None):
def read_dcm_dir(
input_dir, header=False, globber="*", channels_axis=None, series=None
):
"""
Reads a 3D dicom image: input path can be a file or directory (DICOM series).
Return the image array, metadata, and whether it has channels
Expand All @@ -84,15 +109,21 @@ def read_dcm_dir(input_dir, header=False, globber='*', channels_axis=None, serie
if header:
# TODO: add header support, something like
# metdata.header = [{str(key): ds[key] for key in ds.keys()} for ds in slices]
raise NotImplementedError("header=True is currently not supported for a series")
raise NotImplementedError(
"header=True is currently not supported for a series"
)
samples_per_pixel = slices[0].SamplesPerPixel
img = PdcmIO.move_channels_axis(img, samples_per_pixel=samples_per_pixel, channels_axis=channels_axis,
planar_configuration=slices[0].get('PlanarConfiguration', None),
default_axes=PdcmIO.DEFAULT_CHANNELS_AXES_DICOM_NUMPY)
img = PdcmIO.move_channels_axis(
img,
samples_per_pixel=samples_per_pixel,
channels_axis=channels_axis,
planar_configuration=slices[0].get("PlanarConfiguration", None),
default_axes=PdcmIO.DEFAULT_CHANNELS_AXES_DICOM_NUMPY,
)
return img, metadata, samples_per_pixel > 1

@staticmethod
def extract_slices(input_dir, globber='*', series=None):
def extract_slices(input_dir, globber="*", series=None):
"""Extract slices from input_dir and return them sorted"""
files = Path(input_dir).glob(globber)
slices = [pydicom.dcmread(filename) for filename in files]
Expand All @@ -106,24 +137,31 @@ def extract_slices(input_dir, globber='*', series=None):
series_uid = parse_series_uids(input_dir, datasets.keys(), series, globber)
slices = datasets[series_uid]

slices.sort(key=lambda ds: ds.get('InstanceNumber', 0))
slices.sort(key=lambda ds: ds.get("InstanceNumber", 0))
return slices

@staticmethod
def aff2meta(affine):
return MetaData(affine, coord_sys=PdcmIO.coord_sys)

@staticmethod
def move_channels_axis(array, samples_per_pixel, channels_axis=None, planar_configuration=None,
default_axes=DEFAULT_CHANNELS_AXES_PYDICOM):
def move_channels_axis(
array,
samples_per_pixel,
channels_axis=None,
planar_configuration=None,
default_axes=DEFAULT_CHANNELS_AXES_PYDICOM,
):
"""Move the channels axis from the original axis to the destined channels_axis"""
if (samples_per_pixel == 1) or (channels_axis is None):
# no rearrangement is needed
return array

# extract the original channels axis
if planar_configuration not in [0, 1]:
raise ValueError(f'Invalid Planar Configuration value: {planar_configuration}')
raise ValueError(
f"Invalid Planar Configuration value: {planar_configuration}"
)

orig_axis = default_axes[planar_configuration]
flag = True # original channels axis is assigned
Expand All @@ -138,7 +176,7 @@ def move_channels_axis(array, samples_per_pixel, channels_axis=None, planar_conf
break

if not flag:
raise ValueError('The original channels axis was not detected')
raise ValueError("The original channels axis was not detected")

return np.moveaxis(array, orig_axis, channels_axis)

Expand All @@ -160,14 +198,20 @@ def reorient(img, metadata, desired_ornt):
reoriented_img_struct = NibIO.reorient(img_struct, desired_ornt)

img = np.asanyarray(reoriented_img_struct.dataobj)
metadata = MetaData(reoriented_img_struct.affine, orig_ornt=orig_ornt, coord_sys=NibIO.coord_sys,
header=metadata.header)
metadata = MetaData(
reoriented_img_struct.affine,
orig_ornt=orig_ornt,
coord_sys=NibIO.coord_sys,
header=metadata.header,
)
# convert back to pydicom convention
metadata.convert(PdcmIO.coord_sys)
return img, metadata

@staticmethod
def save_arr2dcm_file(output_filename, template_filename, img_arr, dtype=None, keep_rescale=False):
def save_arr2dcm_file(
output_filename, template_filename, img_arr, dtype=None, keep_rescale=False
):
"""
Writes a dicom single file image using template file, without the intensity transformation from template dataset
unless keep_rescale is True
Expand Down
18 changes: 12 additions & 6 deletions medio/backends/pdcm_unpack_ds.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@
import logging

import numpy as np
from dicom_numpy.combine_slices import _validate_image_orientation, _extract_cosines, _requires_rescaling
from dicom_numpy.combine_slices import (
_validate_image_orientation,
_extract_cosines,
_requires_rescaling,
)

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -81,9 +85,10 @@ def _unpack_pixel_array(dataset, rescale=None):
rescale = _requires_rescaling(dataset)

if rescale:
voxels = voxels.astype('int16', copy=False) # TODO: it takes time! Consider view.
slope = getattr(dataset, 'RescaleSlope', 1)
intercept = getattr(dataset, 'RescaleIntercept', 0)
# TODO: it takes time! Consider view.
voxels = voxels.astype("int16", copy=False)
slope = getattr(dataset, "RescaleSlope", 1)
intercept = getattr(dataset, "RescaleIntercept", 0)
if int(slope) == slope and int(intercept) == intercept:
slope = int(slope)
intercept = int(intercept)
Expand All @@ -103,8 +108,9 @@ def _ijk_to_patient_xyz_transform_matrix(dataset):

transform[:3, 0] = row_cosine * column_spacing
transform[:3, 1] = column_cosine * row_spacing
transform[:3, 2] = (np.array(dataset.slice_position(-1)) - dataset.slice_position(0)
) / (dataset.NumberOfFrames - 1)
transform[:3, 2] = (
np.array(dataset.slice_position(-1)) - dataset.slice_position(0)
) / (dataset.NumberOfFrames - 1)
# transform[:3, 2] = slice_cosine * slice_spacing

transform[:3, 3] = dataset.ImagePositionPatient
Expand Down
11 changes: 8 additions & 3 deletions medio/metadata/affine.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def __new__(cls, affine=None, *, direction=None, spacing=None, origin=None):
affine = cls.construct_affine(direction, spacing, origin)
obj = np.asarray(affine).view(cls) # return array view of type Affine
return obj

def __init__(self, affine=None, *, direction=None, spacing=None, origin=None):
self.dim = self.shape[0] - 1
if affine is None:
Expand Down Expand Up @@ -74,7 +74,8 @@ def spacing(self):
def spacing(self, value):
value = np.asarray(value)
self._m_matrix = self._m_matrix @ np.diag(value / self._spacing)
self._spacing = np.abs(value) # the spacing must be positive (or at least nonnegative)
# the spacing must be positive (or at least nonnegative)
self._spacing = np.abs(value)

@property
def direction(self):
Expand Down Expand Up @@ -124,4 +125,8 @@ def affine2direction(affine, spacing=None):
def affine2comps(affine, spacing=None):
if spacing is None:
spacing = Affine.affine2spacing(affine)
return Affine.affine2direction(affine, spacing), spacing, Affine.affine2origin(affine)
return (
Affine.affine2direction(affine, spacing),
spacing,
Affine.affine2origin(affine),
)
8 changes: 4 additions & 4 deletions medio/metadata/convert_nib_itk.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,16 @@

# store compactly axis directions codes
axes_inv = TwoWayDict()
axes_inv['R'] = 'L'
axes_inv['A'] = 'P'
axes_inv['S'] = 'I'
axes_inv["R"] = "L"
axes_inv["A"] = "P"
axes_inv["S"] = "I"


def inv_axcodes(axcodes):
"""Inverse axes codes chars, for example: SPL -> IAR"""
if axcodes is None:
return None
new_axcodes = ''
new_axcodes = ""
for code in axcodes:
new_axcodes += axes_inv[code]
return new_axcodes
Expand Down
2 changes: 1 addition & 1 deletion medio/metadata/dcm_uid.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from pydicom.uid import generate_uid

# Given by Medical Connections (http:https://www.medicalconnections.co.uk/FreeUID.html)
MEDIO_ROOT_UID = '1.2.826.0.1.3680043.10.513.'
MEDIO_ROOT_UID = "1.2.826.0.1.3680043.10.513."


generate_uid = partial(generate_uid, prefix=MEDIO_ROOT_UID)

0 comments on commit d7dac3c

Please sign in to comment.