Skip to content
Merged
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
40 changes: 38 additions & 2 deletions src/highdicom/seg/sop.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from concurrent.futures import Executor, Future, ProcessPoolExecutor
from contextlib import contextmanager
from copy import deepcopy
from itertools import chain
from os import PathLike
import sqlite3
from typing import (
Expand Down Expand Up @@ -44,7 +45,11 @@
from pydicom.sr.coding import Code
from pydicom.filereader import dcmread

from highdicom._module_utils import ModuleUsageValues, get_module_usage
from highdicom._module_utils import (
ModuleUsageValues,
get_module_usage,
does_iod_have_pixel_data,
)
from highdicom.base import SOPClass, _check_little_endian
from highdicom.content import (
ContentCreatorIdentificationCodeSequence,
Expand Down Expand Up @@ -1191,6 +1196,7 @@ def __init__(
tile_size: Union[Sequence[int], None] = None,
pyramid_uid: Optional[str] = None,
pyramid_label: Optional[str] = None,
further_source_images: Optional[Sequence[Dataset]] = None,
**kwargs: Any
) -> None:
"""
Expand Down Expand Up @@ -1400,6 +1406,13 @@ def __init__(
Human readable label for the pyramid containing this segmentation.
Should only be used if this segmentation is part of a
multi-resolution pyramid.
further_source_images: Optional[Sequence[pydicom.Dataset]], optional
Additional images to record as source images in the segmentation.
Unlike the main ``source_images`` parameter, these images will
*not* be used to infer the position and orientation of the
``pixel_array`` in the case that no plane positions are supplied.
Images from multiple series may be passed, however they must all
belong to the same study.
**kwargs: Any, optional
Additional keyword arguments that will be passed to the constructor
of `highdicom.base.SOPClass`
Expand Down Expand Up @@ -1570,12 +1583,35 @@ def __init__(

# General Reference

if further_source_images is not None:
# We make no requirement here that images should be from the same
# series etc, but they should belong to the same study and be image
# objects
for s_img in further_source_images:
if not isinstance(s_img, Dataset):
raise TypeError(
"All items in 'further_source_images' should be "
"of type 'pydicom.Dataset'."
)
if s_img.StudyInstanceUID != self.StudyInstanceUID:
raise ValueError(
"All items in 'further_source_images' should belong "
"to the same study as 'source_images'."
)
if not does_iod_have_pixel_data(s_img.SOPClassUID):
raise ValueError(
"All items in 'further_source_images' should be "
"image objects."
)
else:
further_source_images = []

# Note that appending directly to the SourceImageSequence is typically
# slow so it's more efficient to build as a Python list then convert
# later. We save conversion for after the main loop
source_image_seq: List[Dataset] = []
referenced_series: Dict[str, List[Dataset]] = defaultdict(list)
for s_img in source_images:
for s_img in chain(source_images, further_source_images):
ref = Dataset()
ref.ReferencedSOPClassUID = s_img.SOPClassUID
ref.ReferencedSOPInstanceUID = s_img.SOPInstanceUID
Expand Down
30 changes: 30 additions & 0 deletions tests/test_seg.py
Original file line number Diff line number Diff line change
Expand Up @@ -1529,6 +1529,36 @@ def test_construction_tiled_full(self):
assert instance.DimensionOrganizationType == "TILED_FULL"
assert not hasattr(instance, "PerFrameFunctionalGroupsSequence")

def test_construction_further_source_images(self):
further_source_image = deepcopy(self._ct_image)
series_uid = UID()
sop_uid = UID()
further_source_image.SeriesInstanceUID = series_uid
further_source_image.SOPInstanceUID = sop_uid
instance = Segmentation(
[self._ct_image],
self._ct_pixel_array,
SegmentationTypeValues.FRACTIONAL.value,
self._segment_descriptions,
self._series_instance_uid,
self._series_number,
self._sop_instance_uid,
self._instance_number,
self._manufacturer,
self._manufacturer_model_name,
self._software_versions,
self._device_serial_number,
content_label=self._content_label,
further_source_images=[further_source_image],
)
assert len(instance.SourceImageSequence) == 2
further_item = instance.SourceImageSequence[1]
assert further_item.ReferencedSOPInstanceUID == sop_uid

assert len(instance.ReferencedSeriesSequence) == 2
further_item = instance.ReferencedSeriesSequence[1]
assert further_item.SeriesInstanceUID == series_uid

@staticmethod
@pytest.fixture(
params=[
Expand Down