Skip to content

Commit d59fd66

Browse files
authored
Add further_source_images option to segmentation (#304)
1 parent ad498b2 commit d59fd66

2 files changed

Lines changed: 68 additions & 2 deletions

File tree

src/highdicom/seg/sop.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from concurrent.futures import Executor, Future, ProcessPoolExecutor
55
from contextlib import contextmanager
66
from copy import deepcopy
7+
from itertools import chain
78
from os import PathLike
89
import sqlite3
910
from typing import (
@@ -44,7 +45,11 @@
4445
from pydicom.sr.coding import Code
4546
from pydicom.filereader import dcmread
4647

47-
from highdicom._module_utils import ModuleUsageValues, get_module_usage
48+
from highdicom._module_utils import (
49+
ModuleUsageValues,
50+
get_module_usage,
51+
does_iod_have_pixel_data,
52+
)
4853
from highdicom.base import SOPClass, _check_little_endian
4954
from highdicom.content import (
5055
ContentCreatorIdentificationCodeSequence,
@@ -1191,6 +1196,7 @@ def __init__(
11911196
tile_size: Union[Sequence[int], None] = None,
11921197
pyramid_uid: Optional[str] = None,
11931198
pyramid_label: Optional[str] = None,
1199+
further_source_images: Optional[Sequence[Dataset]] = None,
11941200
**kwargs: Any
11951201
) -> None:
11961202
"""
@@ -1400,6 +1406,13 @@ def __init__(
14001406
Human readable label for the pyramid containing this segmentation.
14011407
Should only be used if this segmentation is part of a
14021408
multi-resolution pyramid.
1409+
further_source_images: Optional[Sequence[pydicom.Dataset]], optional
1410+
Additional images to record as source images in the segmentation.
1411+
Unlike the main ``source_images`` parameter, these images will
1412+
*not* be used to infer the position and orientation of the
1413+
``pixel_array`` in the case that no plane positions are supplied.
1414+
Images from multiple series may be passed, however they must all
1415+
belong to the same study.
14031416
**kwargs: Any, optional
14041417
Additional keyword arguments that will be passed to the constructor
14051418
of `highdicom.base.SOPClass`
@@ -1570,12 +1583,35 @@ def __init__(
15701583

15711584
# General Reference
15721585

1586+
if further_source_images is not None:
1587+
# We make no requirement here that images should be from the same
1588+
# series etc, but they should belong to the same study and be image
1589+
# objects
1590+
for s_img in further_source_images:
1591+
if not isinstance(s_img, Dataset):
1592+
raise TypeError(
1593+
"All items in 'further_source_images' should be "
1594+
"of type 'pydicom.Dataset'."
1595+
)
1596+
if s_img.StudyInstanceUID != self.StudyInstanceUID:
1597+
raise ValueError(
1598+
"All items in 'further_source_images' should belong "
1599+
"to the same study as 'source_images'."
1600+
)
1601+
if not does_iod_have_pixel_data(s_img.SOPClassUID):
1602+
raise ValueError(
1603+
"All items in 'further_source_images' should be "
1604+
"image objects."
1605+
)
1606+
else:
1607+
further_source_images = []
1608+
15731609
# Note that appending directly to the SourceImageSequence is typically
15741610
# slow so it's more efficient to build as a Python list then convert
15751611
# later. We save conversion for after the main loop
15761612
source_image_seq: List[Dataset] = []
15771613
referenced_series: Dict[str, List[Dataset]] = defaultdict(list)
1578-
for s_img in source_images:
1614+
for s_img in chain(source_images, further_source_images):
15791615
ref = Dataset()
15801616
ref.ReferencedSOPClassUID = s_img.SOPClassUID
15811617
ref.ReferencedSOPInstanceUID = s_img.SOPInstanceUID

tests/test_seg.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1529,6 +1529,36 @@ def test_construction_tiled_full(self):
15291529
assert instance.DimensionOrganizationType == "TILED_FULL"
15301530
assert not hasattr(instance, "PerFrameFunctionalGroupsSequence")
15311531

1532+
def test_construction_further_source_images(self):
1533+
further_source_image = deepcopy(self._ct_image)
1534+
series_uid = UID()
1535+
sop_uid = UID()
1536+
further_source_image.SeriesInstanceUID = series_uid
1537+
further_source_image.SOPInstanceUID = sop_uid
1538+
instance = Segmentation(
1539+
[self._ct_image],
1540+
self._ct_pixel_array,
1541+
SegmentationTypeValues.FRACTIONAL.value,
1542+
self._segment_descriptions,
1543+
self._series_instance_uid,
1544+
self._series_number,
1545+
self._sop_instance_uid,
1546+
self._instance_number,
1547+
self._manufacturer,
1548+
self._manufacturer_model_name,
1549+
self._software_versions,
1550+
self._device_serial_number,
1551+
content_label=self._content_label,
1552+
further_source_images=[further_source_image],
1553+
)
1554+
assert len(instance.SourceImageSequence) == 2
1555+
further_item = instance.SourceImageSequence[1]
1556+
assert further_item.ReferencedSOPInstanceUID == sop_uid
1557+
1558+
assert len(instance.ReferencedSeriesSequence) == 2
1559+
further_item = instance.ReferencedSeriesSequence[1]
1560+
assert further_item.SeriesInstanceUID == series_uid
1561+
15321562
@staticmethod
15331563
@pytest.fixture(
15341564
params=[

0 commit comments

Comments
 (0)