diff --git a/docs/quickstart.rst b/docs/quickstart.rst index fa0cd04d..c65e2fe3 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -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( diff --git a/docs/tid1500.rst b/docs/tid1500.rst index 5c0e4fe2..a03aa4fb 100644 --- a/docs/tid1500.rst +++ b/docs/tid1500.rst @@ -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, @@ -677,7 +677,7 @@ 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 @@ -685,7 +685,7 @@ highdicom test data within the highdicom repository at 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], ) @@ -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], ) diff --git a/docs/tid1500parsing.rst b/docs/tid1500parsing.rst index 2273c770..60f5e195 100644 --- a/docs/tid1500parsing.rst +++ b/docs/tid1500parsing.rst @@ -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 diff --git a/src/highdicom/ko/sop.py b/src/highdicom/ko/sop.py index 6c178b11..effaad32 100644 --- a/src/highdicom/ko/sop.py +++ b/src/highdicom/ko/sop.py @@ -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.' diff --git a/src/highdicom/sr/content.py b/src/highdicom/sr/content.py index 2914a2a5..0aec3aa5 100644 --- a/src/highdicom/sr/content.py +++ b/src/highdicom/sr/content.py @@ -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. @@ -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, @@ -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 @@ -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 diff --git a/src/highdicom/sr/sop.py b/src/highdicom/sr/sop.py index 305d3195..8e3b0ba4 100644 --- a/src/highdicom/sr/sop.py +++ b/src/highdicom/sr/sop.py @@ -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 diff --git a/src/highdicom/sr/utils.py b/src/highdicom/sr/utils.py index 9b6ce34d..f3bb6734 100644 --- a/src/highdicom/sr/utils.py +++ b/src/highdicom/sr/utils.py @@ -235,15 +235,15 @@ 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 ---------- @@ -251,6 +251,12 @@ def collect_evidence( 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 ------- @@ -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, @@ -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 @@ -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) @@ -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) diff --git a/tests/test_sr.py b/tests/test_sr.py index ce324999..6c570449 100644 --- a/tests/test_sr.py +++ b/tests/test_sr.py @@ -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 @@ -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( @@ -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 @@ -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( @@ -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) @@ -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):