Skip to content

Commit

Permalink
Merge pull request #19 from neuro-ml/dev
Browse files Browse the repository at this point in the history
refactor spatial, add error raising to get_common_tag
  • Loading branch information
mishgon authored Jun 27, 2022
2 parents 339c768 + 99bce34 commit 4188d9e
Show file tree
Hide file tree
Showing 3 changed files with 41 additions and 30 deletions.
2 changes: 1 addition & 1 deletion dicom_csv/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '0.2.3'
__version__ = '0.2.4'
62 changes: 35 additions & 27 deletions dicom_csv/spatial.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,13 @@ class Plane(Enum):
Sagittal, Coronal, Axial = 0, 1, 2


# TODO: deprecate
class SlicesOrientation(NamedTuple):
"""
Defines how slices should be transformed in order to be canonically oriented:
First transpose slices if ``transpose == True``.
Then flip slices along ``flip_axes`` (they already account for transposition).
"""
transpose: bool
flip_axes: tuple

Expand All @@ -36,7 +42,7 @@ def _get_image_position_patient(instance: Instance):


@csv_series
def get_image_position_patient(series: Series):
def get_image_position_patient(series: Series) -> np.ndarray:
"""Returns ImagePositionPatient stacked into array."""
return np.stack(list(map(_get_image_position_patient, series)))

Expand All @@ -54,8 +60,10 @@ def _get_orientation_matrix(instance: Instance):
def get_orientation_matrix(series: Series) -> np.ndarray:
"""
Returns a 3 x 3 orthogonal transition matrix from the image-based basis to the patient-based basis.
Rows are coordinates of image-based basis vectors in the patient-based basis, while columns are
coordinates of patient-based basis vectors in the image-based basis vectors.
Rows are coordinates of image-based basis vectors in the patient-based basis.
Columns are coordinates of patient-based basis vectors in the image-based basis vectors.
See https://dicom.innolitics.com/ciods/rt-dose/image-plane/00200037 for details.
"""
om = _get_orientation_matrix(series[0])
if not np.all([np.allclose(om, _get_orientation_matrix(i)) for i in series]):
Expand All @@ -64,27 +72,26 @@ def get_orientation_matrix(series: Series) -> np.ndarray:
return om


def _get_image_planes(orientation_matrix):
return tuple(Plane(i) for i in np.argmax(np.abs(orientation_matrix), axis=1))
def _orientation_matrix_to_image_planes(om):
return tuple(Plane(i) for i in np.argmax(np.abs(om), axis=1))


def orientation_matrix_to_slices_plane(om: np.ndarray) -> Plane:
return _orientation_matrix_to_image_planes(om)[2]


def get_slice_plane(instance: Instance) -> Plane:
return _get_image_planes(_get_orientation_matrix(instance))[2]
return orientation_matrix_to_slices_plane(_get_orientation_matrix(instance))


@csv_series
def get_slices_plane(series: Series) -> Plane:
unique_planes = set(map(get_slice_plane, series))
if len(unique_planes) > 1:
raise ConsistencyError('Slice plane varies across slices.')

plane, = unique_planes
return plane
return orientation_matrix_to_slices_plane(get_orientation_matrix(series))


def get_slice_orientation(instance: Instance) -> SlicesOrientation:
om = _get_orientation_matrix(instance)
planes = _get_image_planes(om)
@np.deprecate
def orientation_matrix_to_slices_orientation(om: np.ndarray) -> SlicesOrientation:
planes = _orientation_matrix_to_image_planes(om)

if set(planes) != {Plane.Sagittal, Plane.Coronal, Plane.Axial}:
raise ValueError('Main image planes cannot be treated as saggital, coronal and axial.')
Expand All @@ -105,18 +112,19 @@ def get_slice_orientation(instance: Instance) -> SlicesOrientation:
return SlicesOrientation(transpose=transpose, flip_axes=tuple(flip_axes))


@np.deprecate
def get_slice_orientation(instance: Instance) -> SlicesOrientation:
return orientation_matrix_to_slices_orientation(_get_orientation_matrix(instance))


@np.deprecate
@csv_series
def get_slices_orientation(series: Series) -> SlicesOrientation:
orientations = set(map(get_slice_orientation, series))
if len(orientations) > 1:
raise ConsistencyError('Slice orientation varies across slices.')

orientation, = orientations
return orientation
return orientation_matrix_to_slices_orientation(get_orientation_matrix(series))


@csv_series
def order_series(series: Series, decreasing=True) -> Series:
def order_series(series: Series, decreasing: bool = True) -> Series:
index = get_slices_plane(series).value
return sorted(series, key=lambda s: _get_image_position_patient(s)[index], reverse=decreasing)

Expand All @@ -125,7 +133,7 @@ def order_series(series: Series, decreasing=True) -> Series:
def get_slice_locations(series: Series) -> np.ndarray:
"""
Computes slices location from ImagePositionPatient.
WARNING: the order of slice locations can be both increasing or decreasing for ordered series
NOTE: the order of slice locations can be both increasing or decreasing for ordered series
(see order_series).
"""
om = get_orientation_matrix(series)
Expand Down Expand Up @@ -157,7 +165,7 @@ def get_slice_spacing(series: Series, max_delta: float = 0.1, errors: bool = Tru
return locations_to_spacing(sorted(locations), max_delta, errors)


def locations_to_spacing(locations: Sequence[float], max_delta: float = 0.1, errors: bool = True):
def locations_to_spacing(locations: Sequence[float], max_delta: float = 0.1, errors: bool = True) -> float:
def throw(err):
if errors:
raise err
Expand Down Expand Up @@ -189,15 +197,15 @@ def get_pixel_spacing(series: Series) -> Tuple[float, float]:


@csv_series
def get_voxel_spacing(series: Series):
def get_voxel_spacing(series: Series) -> Tuple[float, float, float]:
"""Returns voxel spacing: pixel spacing and distance between slices' centers."""
dx, dy = get_pixel_spacing(series)
dz = get_slice_spacing(series)
return dx, dy, dz


@csv_series
def get_image_size(series: Series):
def get_image_size(series: Series) -> Tuple[int, int, int]:
rows = get_common_tag(series, 'Rows')
columns = get_common_tag(series, 'Columns')
slices = len(series)
Expand All @@ -206,7 +214,7 @@ def get_image_size(series: Series):

def drop_duplicated_slices(series: Series, tolerance_hu=1) -> Series:
series = drop_duplicated_instances(series)

indices = list(range(len(series)))
slice_locations = get_slice_locations(series)
try:
Expand Down
7 changes: 5 additions & 2 deletions dicom_csv/tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ def get_tag(instance: Instance, tag, default=inspect.Parameter.empty):


def get_common_tag(series: Series, tag, default=inspect.Parameter.empty):
if len(series) == 0:
raise ValueError('Empty series.')

try:
try:
unique_values = {get_tag(i, tag) for i in series}
Expand All @@ -34,8 +37,8 @@ def get_common_tag(series: Series, tag, default=inspect.Parameter.empty):
raise
else:
return default


def _get_sop_uid(instance):
return str(get_tag(instance, 'SOPInstanceUID'))

Expand Down

0 comments on commit 4188d9e

Please sign in to comment.