Skip to content
Closed
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
28 changes: 19 additions & 9 deletions src/highdicom/volume.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,9 +272,10 @@ def __init__(
affine: numpy.ndarray
4 x 4 affine matrix representing the transformation from voxel
indices to the frame-of-reference coordinate system. The top left 3
x 3 matrix should be a scaled orthogonal matrix representing the
rotation and scaling. The top right 3 x 1 vector represents the
translation component. The last row should have value [0, 0, 0, 1].
x 3 matrix must be an invertible, scaled orthogonal matrix
representing the rotation and scaling. The top right 3 x 1 vector
represents the translation component. The last row should have
value [0, 0, 0, 1].
coordinate_system: highdicom.CoordinateSystemNames | str
Coordinate system (``"PATIENT"`` or ``"SLIDE"``) in which the volume
is defined.
Expand Down Expand Up @@ -330,6 +331,13 @@ def __init__(
to_reference_convention=LPS_PATIENT_REFERENCE_CONVENTION,
)

try:
np.linalg.inv(affine)
except np.linalg.LinAlgError as error:
raise ValueError(
"Argument 'affine' must be invertible."
) from error

self._affine = affine
self._frame_of_reference_uid = frame_of_reference_uid

Expand Down Expand Up @@ -1990,9 +1998,10 @@ def __init__(
4 x 4 affine matrix representing the transformation from pixel
indices (slice index, row index, column index) to the
frame-of-reference coordinate system. The top left 3 x 3 matrix
should be a scaled orthogonal matrix representing the rotation and
scaling. The top right 3 x 1 vector represents the translation
component. The last row should have value [0, 0, 0, 1].
must be an invertible, scaled orthogonal matrix representing the
rotation and scaling. The top right 3 x 1 vector represents the
translation component. The last row should have value
[0, 0, 0, 1].
spatial_shape: Sequence[int]
Number of voxels in the (implied) volume along the three spatial
dimensions.
Expand Down Expand Up @@ -2477,9 +2486,10 @@ def __init__(
4 x 4 affine matrix representing the transformation from pixel
indices (slice index, row index, column index) to the
frame-of-reference coordinate system. The top left 3 x 3 matrix
should be a scaled orthogonal matrix representing the rotation and
scaling. The top right 3 x 1 vector represents the translation
component. The last row should have value [0, 0, 0, 1].
must be an invertible, scaled orthogonal matrix representing the
rotation and scaling. The top right 3 x 1 vector represents the
translation component. The last row should have value
[0, 0, 0, 1].
coordinate_system: highdicom.CoordinateSystemNames | str
Coordinate system (``"PATIENT"`` or ``"SLIDE"``) in which the volume
is defined.
Expand Down
20 changes: 20 additions & 0 deletions tests/test_volume.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,26 @@ def read_ct_series_volume():
return get_volume_from_series(ct_series), ct_series


@pytest.mark.parametrize('volume_type', [Volume, VolumeGeometry])
def test_reject_singular_affine(volume_type):
affine = np.eye(4)
# Keep the dependent column non-zero so that the approximate
# orthogonality check alone does not reject the affine.
affine[:3, 2] = affine[:3, 0] * 1e-6

kwargs = {
'affine': affine,
'coordinate_system': "PATIENT",
}
if volume_type is Volume:
kwargs['array'] = np.zeros((2, 2, 2))
else:
kwargs['spatial_shape'] = (2, 2, 2)

with pytest.raises(ValueError, match="affine.*invertible"):
volume_type(**kwargs)


def test_transforms():
array = np.zeros((25, 50, 50))
orientation = [1.0, 0.0, 0.0, 0.0, 1.0, 0.0]
Expand Down