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
2 changes: 1 addition & 1 deletion docs/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -585,7 +585,7 @@ For more information see :doc:`tid1500parsing`.
assert measurement.value == 10.0

# Access the measurement's unit
assert measurement.unit == codes.UCUM.mm
assert measurement.unit == codes.UCUM.Millimeter

# Get the diameter measurement in this group
evaluation = group.get_qualitative_evaluations(
Expand Down
8 changes: 4 additions & 4 deletions docs/tid1500.rst
Original file line number Diff line number Diff line change
Expand Up @@ -641,7 +641,7 @@ highdicom test data within the highdicom repository at
nodule_measurement = hd.sr.Measurement(
name=codes.SCT.Diameter,
value=10.0,
unit=codes.UCUM.mm,
unit=codes.UCUM.Millimeter,
)
nodule_evaluation = hd.sr.QualitativeEvaluation(
name=codes.DCM.LevelOfSignificance,
Expand Down Expand Up @@ -677,15 +677,15 @@ highdicom test data within the highdicom repository at
aorta_measurement = hd.sr.Measurement(
name=codes.SCT.Diameter,
value=20.0,
unit=codes.UCUM.mm,
unit=codes.UCUM.Millimeter,
)

# Construct the measurement group
planar_group_2 = hd.sr.PlanarROIMeasurementsAndQualitativeEvaluations(
referenced_region=region,
tracking_identifier=aorta_roi_tracking_id,
finding_type=codes.SCT.Aorta,
finding_category=structure_code,
finding_category=codes.SCT.AnatomicalStructure,
measurements=[aorta_measurement],
)

Expand Down Expand Up @@ -715,7 +715,7 @@ highdicom test data within the highdicom repository at
vol_group = hd.sr.VolumetricROIMeasurementsAndQualitativeEvaluations(
referenced_volume_surface=volume_surface,
tracking_identifier=volumetric_roi_tracking_id,
finding_category=structure_code,
finding_category=codes.SCT.AnatomicalStructure,
finding_type=codes.SCT.Vertebra,
measurements=[vol_measurement],
)
Expand Down
2 changes: 1 addition & 1 deletion docs/tid1500parsing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ property (returns a ``float``), and the unit with the ``unit`` property.
assert measurement.value == 10.0

# Access the measurement's unit
assert measurement.unit == codes.UCUM.mm
assert measurement.unit == codes.UCUM.Millimeter

Additionally, the properties ``method``, ``finding_sites``, ``qualifier``,
``referenced_images``, and ``derivation`` allow you to access further optional
Expand Down
12 changes: 8 additions & 4 deletions src/highdicom/ko/sop.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,10 +138,14 @@ def __init__(
for tag, value in content[0].items():
self[tag] = value

ref_items, unref_items = collect_evidence(evidence, content[0])
if len(ref_items) > 0:
self.CurrentRequestedProcedureEvidenceSequence = ref_items
if len(ref_items) > 1:
same_study_items, other_study_items = collect_evidence(
evidence,
content[0],
study_instance_uid=self.StudyInstanceUID,
)
if len(same_study_items) > 0:
self.CurrentRequestedProcedureEvidenceSequence = same_study_items
if len(other_study_items) > 1:
raise ValueError(
'Key Object Selection Documents that reference instances '
'from multiple studies are not supported.'
Expand Down
10 changes: 7 additions & 3 deletions src/highdicom/sr/content.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@
logger = logging.getLogger(__name__)


# The pydicom codes.SCT.Source has an unnecessary "(attribute)" in the meaning
_SOURCE = Code(value='260753009', scheme_designator='SCT', meaning='Source')


def _check_valid_source_image_dataset(dataset: Dataset) -> None:
"""Raise an error if the image is not a valid source image reference.

Expand Down Expand Up @@ -449,7 +453,7 @@ def __init__(
'1-based.'
)
super().__init__(
name=codes.SCT.Source,
name=_SOURCE,
referenced_sop_class_uid=referenced_sop_class_uid,
referenced_sop_instance_uid=referenced_sop_instance_uid,
referenced_frame_numbers=referenced_frame_numbers,
Expand Down Expand Up @@ -718,7 +722,7 @@ def __init__(
graphic_type: GraphicTypeValues | str,
graphic_data: np.ndarray,
source_image: SourceImageForRegion,
purpose: CodedConcept | Code = codes.SCT.Source,
purpose: CodedConcept | Code = _SOURCE,
) -> None:
"""
Parameters
Expand Down Expand Up @@ -782,7 +786,7 @@ def __init__(
graphic_data: np.ndarray,
frame_of_reference_uid: str | UID,
fiducial_uid: str | UID | None = None,
purpose: CodedConcept | Code = codes.SCT.Source,
purpose: CodedConcept | Code = _SOURCE,
):
"""
Parameters
Expand Down
14 changes: 9 additions & 5 deletions src/highdicom/sr/sop.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,11 +242,15 @@ def __init__(
for tag, value in content.items():
self[tag] = value

ref_items, unref_items = collect_evidence(evidence, content)
if len(ref_items) > 0:
self.CurrentRequestedProcedureEvidenceSequence = ref_items
if len(unref_items) > 0 and record_evidence:
self.PertinentOtherEvidenceSequence = unref_items
same_study_items, other_study_items = collect_evidence(
evidence,
content,
study_instance_uid=self.StudyInstanceUID,
)
if len(same_study_items) > 0:
self.CurrentRequestedProcedureEvidenceSequence = same_study_items
if len(other_study_items) > 0 and record_evidence:
self.PertinentOtherEvidenceSequence = other_study_items

if requested_procedures is not None:
self.ReferencedRequestSequence = requested_procedures
Expand Down
45 changes: 29 additions & 16 deletions src/highdicom/sr/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,22 +235,28 @@ def _create_references(

def collect_evidence(
evidence: Sequence[Dataset],
content: Dataset
content: Dataset,
study_instance_uid: str | None = None,
) -> tuple[list[Dataset], list[Dataset]]:
"""Collect evidence for a SR document.

Any `evidence` that is referenced in `content` via IMAGE or
COMPOSITE content items will be grouped together for inclusion in the
Current Requested Procedure Evidence Sequence and all remaining
evidence will be grouped for potential inclusion in the Pertinent Other
Evidence Sequence.
Any ``evidence`` that belongs to the same study as the new SR document will
be grouped together for inclusion in the Current Requested Procedure
Evidence Sequence and all remaining evidence will be grouped for potential
inclusion in the Pertinent Other Evidence Sequence.

Parameters
----------
evidence: List[pydicom.dataset.Dataset]
Metadata of instances that serve as evidence for the SR document content
content: pydicom.dataset.Dataset
SR document content
study_instance_uid: str
Study instance UID of the SR being created. If not provided, the study
instance UID of the first ``evidence`` item is taken to the study
instance UID of the new SR. This is primarily for backwards
compatibility: it is recommended to always explicitly provide the study
instance UID.

Returns
-------
Expand All @@ -262,10 +268,13 @@ def collect_evidence(
Raises
------
ValueError
When a SOP instance is referenced in `content` but not provided as
`evidence`
When a SOP instance is referenced in ``content`` but not provided as
``evidence``

""" # noqa: E501
if study_instance_uid is None and len(evidence) > 1:
study_instance_uid = evidence[0].StudyInstanceUID

references = find_content_items(
content,
value_type=ValueTypeValues.IMAGE,
Expand All @@ -281,8 +290,12 @@ def collect_evidence(
for ref in references
}
evd_uids = set()
ref_group: Mapping[tuple[str, str], list[Dataset]] = defaultdict(list)
unref_group: Mapping[tuple[str, str], list[Dataset]] = defaultdict(list)
same_study_group: Mapping[
tuple[str, str], list[Dataset]
] = defaultdict(list)
other_study_group: Mapping[
tuple[str, str], list[Dataset]
] = defaultdict(list)
for evd in evidence:
if evd.SOPInstanceUID in evd_uids:
# Skip potential duplicates
Expand All @@ -291,10 +304,10 @@ def collect_evidence(
evd_item.ReferencedSOPClassUID = evd.SOPClassUID
evd_item.ReferencedSOPInstanceUID = evd.SOPInstanceUID
key = (evd.StudyInstanceUID, evd.SeriesInstanceUID)
if evd.SOPInstanceUID in ref_uids:
ref_group[key].append(evd_item)
if evd.StudyInstanceUID == study_instance_uid:
same_study_group[key].append(evd_item)
else:
unref_group[key].append(evd_item)
other_study_group[key].append(evd_item)
evd_uids.add(evd.SOPInstanceUID)
if not ref_uids.issubset(evd_uids):
missing_uids = ref_uids.difference(evd_uids)
Expand All @@ -305,6 +318,6 @@ def collect_evidence(
)
)

ref_items = _create_references(ref_group)
unref_items = _create_references(unref_group)
return (ref_items, unref_items)
same_study_items = _create_references(same_study_group)
other_study_items = _create_references(other_study_group)
return (same_study_items, other_study_items)
32 changes: 16 additions & 16 deletions tests/test_sr.py
Original file line number Diff line number Diff line change
Expand Up @@ -4268,9 +4268,8 @@ def test_evidence_multiple_studies(self):
manufacturer=self._manufacturer
)
ref_evd_items = report.CurrentRequestedProcedureEvidenceSequence
assert len(ref_evd_items) == 2
with pytest.raises(AttributeError):
assert report.PertinentOtherEvidenceSequence
assert len(ref_evd_items) == 1
assert len(report.PertinentOtherEvidenceSequence) == 1

evidence = report.get_evidence()
assert len(evidence) == 2
Expand Down Expand Up @@ -4299,7 +4298,8 @@ def test_evidence_multiple_studies(self):

def test_current_and_other_evidence(self):
ref_dataset2 = deepcopy(self._ref_dataset)
ref_dataset2.SeriesInstanceUID = '1.2.3'
ref_dataset2.StudyInstanceUID = '1.2.3.4.5'
ref_dataset2.SeriesInstanceUID = '1.2.3.4'
ref_dataset2.SOPInstanceUID = '1.2.3'

report = Comprehensive3DSR(
Expand All @@ -4313,10 +4313,10 @@ def test_current_and_other_evidence(self):
institutional_department_name=self._department_name,
manufacturer=self._manufacturer
)
ref_evd_items = report.CurrentRequestedProcedureEvidenceSequence
assert len(ref_evd_items) == 1
unref_evd_items = report.PertinentOtherEvidenceSequence
assert len(unref_evd_items) == 1
same_study_items = report.CurrentRequestedProcedureEvidenceSequence
assert len(same_study_items) == 1
other_study_items = report.PertinentOtherEvidenceSequence
assert len(other_study_items) == 1

evidence = report.get_evidence()
assert len(evidence) == 2
Expand Down Expand Up @@ -4522,10 +4522,10 @@ def test_construction(self):
assert report.Manufacturer == self._manufacturer
assert report.Modality == 'SR'

ref_evd_items = report.CurrentRequestedProcedureEvidenceSequence
assert len(ref_evd_items) == 1
with pytest.raises(AttributeError):
assert report.CurrentRequestedProcedureEvidenceSequence
unref_evd_items = report.PertinentOtherEvidenceSequence
assert len(unref_evd_items) == 1
assert report.PertinentOtherEvidenceSequence

def test_construction_content_is_sequence(self):
report = Comprehensive3DSR(
Expand Down Expand Up @@ -4554,10 +4554,10 @@ def test_construction_content_is_sequence(self):
assert report.Manufacturer == self._manufacturer
assert report.Modality == 'SR'

ref_evd_items = report.CurrentRequestedProcedureEvidenceSequence
assert len(ref_evd_items) == 1
with pytest.raises(AttributeError):
assert report.CurrentRequestedProcedureEvidenceSequence
unref_evd_items = report.PertinentOtherEvidenceSequence
assert len(unref_evd_items) == 1
assert report.PertinentOtherEvidenceSequence

def test_from_dataset(self):
report = Comprehensive3DSR.from_dataset(self._sr_document)
Expand Down Expand Up @@ -4586,8 +4586,8 @@ def test_evidence_duplication(self):
institutional_department_name=self._department_name,
manufacturer=self._manufacturer
)
unref_evd_items = report.PertinentOtherEvidenceSequence
assert len(unref_evd_items) == 1
ref_evd_items = report.CurrentRequestedProcedureEvidenceSequence
assert len(ref_evd_items) == 1


class TestSRUtilities(unittest.TestCase):
Expand Down