Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions src/highdicom/volume.py
Original file line number Diff line number Diff line change
Expand Up @@ -2504,6 +2504,15 @@ def __init__(
"Argument 'array' must be at least three dimensional."
)

if not any([
np.issubdtype(array.dtype, dtype) for dtype in
[np.floating, np.integer, np.bool_]
]):
raise ValueError(
"Argument 'array' must have a dtype of float, integer,"
f" or bool, received '{array.dtype}'."
Comment on lines +2512 to +2513
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This suggests that the dtype must be one of these options exactly. How about:

Suggested change
"Argument 'array' must have a dtype of float, integer,"
f" or bool, received '{array.dtype}'."
"Argument 'array' must have an integer, floating point, "
f" or boolean dtype, received '{array.dtype}'."

)

if channels is None:
channels = {}

Expand Down Expand Up @@ -2856,6 +2865,16 @@ def array(self, value: np.ndarray) -> None:
raise ValueError(
"Array must match the shape of the existing array."
)

if not any([
np.issubdtype(value.dtype, dtype) for dtype in
[np.floating, np.integer, np.bool_]
]):
raise ValueError(
"Argument 'array' must have an integer, floating point, "
f" or boolean dtype, received '{array.dtype}'."
)

self._array = value

def astype(self, dtype: type) -> Self:
Expand Down Expand Up @@ -3588,6 +3607,9 @@ def pad_array(array: np.ndarray, cval: float) -> float:
def to_sitk(self) -> 'SimpleITK.Image': # noqa: F821
"""Convert the Volume to ``SimpleITK.Image`` format.

This method requires an optional dependency to be installed
separately from highdicom, specifically ``SimpleITK``.

The Volume is converted to a 3D ``SimpleITK.Image``. If
its array's current datatype is not supported by SimpleITK,
it is safely cast to a compatible type where possible. If
Expand Down Expand Up @@ -3674,6 +3696,9 @@ def from_sitk(
) -> Self:
"""Construct a Volume from a `SimpleITK.Image`.

This method requires an optional dependency to be installed
separately from highdicom, specifically ``SimpleITK``.

The ``SimpleITK.Image`` is converted to a 3D Volume.
Spatial metadata (spacing, direction, origin) is preserved
with both highdicom and SimpletITK using "LPS" convention.
Expand Down Expand Up @@ -3730,6 +3755,9 @@ def from_sitk(
def to_itk(self) -> 'itk.Image': # noqa: F821
"""Convert the volume to `itk.Image` format.

This method requires an optional dependency to be installed
separately from highdicom, specifically ``itk``.

The Volume is converted to a 3D ``itk.image``. If its array's
current datatype is not supported by ITK, it is safely cast to
a compatible type where possible. If impossible to cast safely,
Expand Down Expand Up @@ -3840,6 +3868,9 @@ def from_itk(
) -> Self:
"""Construct a Volume from an `itk.Image`.

This method requires an optional dependency to be installed
separately from highdicom, specifically ``itk``.

The ``itk.Image`` is converted to a 3D Volume.
Spatial metadata (spacing, direction, origin) is preserved
with both highdicom and ITK using "LPS" convention. As ITK uses
Expand Down
74 changes: 74 additions & 0 deletions tests/test_volume.py
Original file line number Diff line number Diff line change
Expand Up @@ -1341,3 +1341,77 @@ def test_match_geometry_segmentation():
seg_vol_from_matched_dataset.array,
seg_volume_matched.array
)


@pytest.mark.parametrize(
'dtype',
[
np.uint8,
np.uint16,
np.uint32,
np.uint64,
np.int8,
np.int16,
np.int32,
np.int64,
int,
np.float16,
np.float32,
np.float64,
float,
np.float128,
np.complex64,
complex,
np.complex128,
np.complex256,
np.bool_,
bool,
str
]
)
def test_dtype_restriction(dtype):
if any([
np.issubdtype(dtype, t) for t in
[np.floating, np.integer, np.bool_]
]):
volume = Volume.from_attributes(
array=np.zeros((10, 10, 10), dtype=dtype),
image_position=(0.0, 0.0, 0.0),
image_orientation=(1.0, 0.0, 0.0, 0.0, 1.0, 0.0),
pixel_spacing=(1.0, 1.0),
spacing_between_slices=1.0,
coordinate_system='PATIENT',
)

for disallowed_dtype in [
np.complex64,
complex,
np.complex128,
np.complex256,
str
]:
with pytest.raises(
ValueError,
match=(
"Array must have a dtype of float, integer,"
" or bool, received"
)
):
volume.array = np.zeros((10, 10, 10), dtype=disallowed_dtype)
Comment on lines +1386 to +1400
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this test? Isn't it the same regardless of dtype, and therefore run loads of times unnecessarily?

Would it be cleaner to have two tests: one for valid dtypes and the other for invalid dtypes?


else:
with pytest.raises(
ValueError,
match=(
"Argument 'array' must have a dtype of float, integer,"
" or bool, received"
)
):
volume = Volume.from_attributes(
array=np.zeros((10, 10, 10), dtype=dtype),
image_position=(0.0, 0.0, 0.0),
image_orientation=(1.0, 0.0, 0.0, 0.0, 1.0, 0.0),
pixel_spacing=(1.0, 1.0),
spacing_between_slices=1.0,
coordinate_system='PATIENT',
)
Loading