Skip to content

Commit 4121bc1

Browse files
authored
Merge pull request #319 from ImagingDataCommons/bug/nonaligned_volume
0.24.0 bugfixes
2 parents 16cd061 + 7e2827e commit 4121bc1

File tree

5 files changed

+543
-20
lines changed

5 files changed

+543
-20
lines changed

docs/volume.rst

+4-4
Original file line numberDiff line numberDiff line change
@@ -394,8 +394,8 @@ spatial metadata in the output object is correct.
394394
"""This is a stand-in for a generic segmentation tool.
395395
396396
We assume that the tool has certain requirements on the input array, in
397-
this case that it has patient orientation "PRF" and a shape of (400, 400,
398-
2).
397+
this case that it has patient orientation "FLP" and a shape of (2, 400,
398+
400).
399399
400400
Further, we assume that the tool takes in a numpy array and returns a
401401
binary segmentation that is pixel-for-pixel aligned with its input array
@@ -414,8 +414,8 @@ spatial metadata in the output object is correct.
414414
# Manipulate the original volume to give a suitable input for the tool
415415
input_volume = (
416416
original_volume
417-
.to_patient_orientation("PRF")
418-
.crop_to_spatial_shape((400, 400, 2))
417+
.to_patient_orientation("FLP")
418+
.crop_to_spatial_shape((2, 400, 400))
419419
)
420420
421421
# Run the "complex segmentation tool"

src/highdicom/seg/sop.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -974,8 +974,10 @@ def __init__(
974974
'orientation.'
975975
)
976976

977-
source_plane_orientation = deepcopy(
978-
src_sfg.PlaneOrientationSequence
977+
source_plane_orientation = (
978+
PlaneOrientationSequence.from_sequence(
979+
src_sfg.PlaneOrientationSequence
980+
)
979981
)
980982
else:
981983
iop = src_img.ImageOrientationPatient

src/highdicom/volume.py

+13-11
Original file line numberDiff line numberDiff line change
@@ -1097,14 +1097,14 @@ def random_permute_spatial_axes(
10971097
"Argument 'axes' should contain unique values."
10981098
)
10991099

1100-
if set(axes) <= {0, 1, 2}:
1100+
if not set(axes) <= {0, 1, 2}:
11011101
raise ValueError(
11021102
"Argument 'axes' should contain only 0, 1, and 2."
11031103
)
11041104

11051105
indices = np.random.permutation(axes).tolist()
11061106
if len(indices) == 2:
1107-
missing_index = {0, 1, 2} - set(indices)
1107+
missing_index = list({0, 1, 2} - set(indices))[0]
11081108
indices.insert(missing_index, missing_index)
11091109

11101110
return self.permute_spatial_axes(indices)
@@ -1289,7 +1289,7 @@ def random_flip_spatial(self, axes: Sequence[int] = (0, 1, 2)) -> Self:
12891289
"Argument 'axes' should contain unique values."
12901290
)
12911291

1292-
if set(axes) <= {0, 1, 2}:
1292+
if not set(axes) <= {0, 1, 2}:
12931293
raise ValueError(
12941294
"Argument 'axes' should contain only 0, 1, and 2."
12951295
)
@@ -1692,9 +1692,9 @@ def match_geometry(
16921692

16931693
permute_indices = []
16941694
step_sizes = []
1695-
for u, s in zip(self.unit_vectors(), self.spacing):
1695+
for u, s in zip(other.unit_vectors(), other.spacing):
16961696
for j, (v, t) in enumerate(
1697-
zip(other.unit_vectors(), other.spacing)
1697+
zip(self.unit_vectors(), self.spacing)
16981698
):
16991699
dot_product = u @ v
17001700
if (
@@ -1703,7 +1703,7 @@ def match_geometry(
17031703
):
17041704
permute_indices.append(j)
17051705

1706-
scale_factor = t / s
1706+
scale_factor = s / t
17071707
step = int(np.round(scale_factor))
17081708
if abs(scale_factor - step) > tol:
17091709
raise RuntimeError(
@@ -1724,7 +1724,6 @@ def match_geometry(
17241724
requires_permute = permute_indices != [0, 1, 2]
17251725
if requires_permute:
17261726
new_volume = self.permute_spatial_axes(permute_indices)
1727-
step_sizes = [step_sizes[i] for i in permute_indices]
17281727
else:
17291728
new_volume = self
17301729

@@ -2789,10 +2788,13 @@ def permute_spatial_axes(self, indices: Sequence[int]) -> Self:
27892788
"""
27902789
new_affine = self._permute_affine(indices)
27912790

2792-
if self._array.ndim == 3:
2793-
new_array = np.transpose(self._array, indices)
2794-
else:
2795-
new_array = np.transpose(self._array, [*indices, 3])
2791+
new_array = np.transpose(
2792+
self._array,
2793+
[
2794+
*indices,
2795+
*[d + 3 for d in range(self.number_of_channel_dimensions)]
2796+
]
2797+
)
27962798

27972799
return self.__class__(
27982800
array=new_array,

tests/test_seg.py

+52
Original file line numberDiff line numberDiff line change
@@ -1952,6 +1952,58 @@ def test_construction_volume(self):
19521952
pp[0].ImagePositionPatient
19531953
)
19541954

1955+
def test_construction_volume_multiframe(self):
1956+
# Construction with a multiiframe source image and non-spatially
1957+
# aligned volume
1958+
arr = np.zeros((50, 50, 10), np.uint8)
1959+
arr[40:45, 34:39, 2:9] = 1
1960+
volume = Volume(
1961+
arr,
1962+
np.eye(4),
1963+
coordinate_system="PATIENT",
1964+
frame_of_reference_uid=self._ct_multiframe.FrameOfReferenceUID,
1965+
)
1966+
1967+
instance = Segmentation(
1968+
[self._ct_multiframe],
1969+
volume,
1970+
SegmentationTypeValues.BINARY.value,
1971+
self._segment_descriptions,
1972+
self._series_instance_uid,
1973+
self._series_number,
1974+
self._sop_instance_uid,
1975+
self._instance_number,
1976+
self._manufacturer,
1977+
self._manufacturer_model_name,
1978+
self._software_versions,
1979+
self._device_serial_number,
1980+
omit_empty_frames=False
1981+
)
1982+
assert np.array_equal(
1983+
instance.pixel_array,
1984+
arr,
1985+
)
1986+
1987+
self.check_dimension_index_vals(instance)
1988+
assert instance.DimensionOrganizationType == '3D'
1989+
shared_item = instance.SharedFunctionalGroupsSequence[0]
1990+
assert len(shared_item.PixelMeasuresSequence) == 1
1991+
pm_item = shared_item.PixelMeasuresSequence[0]
1992+
assert pm_item.PixelSpacing == [1.0, 1.0]
1993+
assert pm_item.SliceThickness == 1.0
1994+
assert len(shared_item.PlaneOrientationSequence) == 1
1995+
po_item = shared_item.PlaneOrientationSequence[0]
1996+
assert po_item.ImageOrientationPatient == \
1997+
[0.0, 0.0, 1.0, 0.0, 1.0, 0.0]
1998+
for plane_item, pp in zip(
1999+
instance.PerFrameFunctionalGroupsSequence,
2000+
volume.get_plane_positions(),
2001+
):
2002+
assert (
2003+
plane_item.PlanePositionSequence[0].ImagePositionPatient ==
2004+
pp[0].ImagePositionPatient
2005+
)
2006+
19552007
def test_construction_volume_channels(self):
19562008
# Segmentation instance from a series of single-frame CT images
19572009
# with empty frames kept in, as volume with channels

0 commit comments

Comments
 (0)