From e323f877c39d828ef8d65d9cb74ab93ebd1b66b4 Mon Sep 17 00:00:00 2001 From: Chris Bridge Date: Fri, 17 Jan 2025 14:14:39 -0500 Subject: [PATCH] Improvements to refrenced instance storage --- src/highdicom/image.py | 102 ++++++++++++++++++++++++++------------- src/highdicom/spatial.py | 6 +-- tests/utils.py | 27 ++++++++--- 3 files changed, 91 insertions(+), 44 deletions(-) diff --git a/src/highdicom/image.py b/src/highdicom/image.py index 817e6183..7381ffb0 100644 --- a/src/highdicom/image.py +++ b/src/highdicom/image.py @@ -1624,8 +1624,15 @@ def _build_luts_single_frame(self) -> None: self._is_tiled_full = False self._dim_ind_pointers = [] self._dim_ind_col_names = {} - self._single_source_frame_per_frame = True + self._single_source_frame_per_frame = False self._locations_preserved = None + self._missing_reference_instances = [] + referenced_uids = self._get_ref_instance_uids() + all_referenced_sops = {uids[2] for uids in referenced_uids} + + col_defs = [] + col_defs.append('FrameNumber INTEGER PRIMARY KEY') + col_data = [[1]] if 'SourceImageSequence' in self: self._single_source_frame_per_frame = ( @@ -1647,10 +1654,21 @@ def _build_luts_single_frame(self) -> None: self._locations_preserved = ( SpatialLocationsPreservedValues.NO ) - - col_defs = [] - col_defs.append('FrameNumber INTEGER PRIMARY KEY') - col_data = [[1]] + if self._single_source_frame_per_frame: + ref_frame = self.SourceImageSequence[0].get('ReferencedFrameNumber') + ref_uid = self.SourceImageSequence[0].ReferencedSOPInstanceUID + if ref_uid not in all_referenced_sops: + self._missing_reference_instances.append(ref_uid) + col_defs.append('ReferencedFrameNumber INTEGER') + col_defs.append('ReferencedSOPInstanceUID VARCHAR NOT NULL') + col_defs.append( + 'FOREIGN KEY(ReferencedSOPInstanceUID) ' + 'REFERENCES InstanceUIDs(SOPInstanceUID)' + ) + col_data += [ + [ref_frame], + [ref_uid], + ] if ( self._coordinate_system is not None and @@ -1791,6 +1809,7 @@ def _build_luts_multiframe(self) -> None: ) self._single_source_frame_per_frame = True + self._missing_reference_instances = [] if self._is_tiled_full: # With TILED_FULL, there is no PerFrameFunctionalGroupsSequence, @@ -1964,13 +1983,8 @@ def _build_luts_multiframe(self) -> None: else: ref_instance_uid = frame_source_instances[0] if ref_instance_uid not in all_referenced_sops: - raise AttributeError( - f'SOP instance {ref_instance_uid} referenced in ' - 'the source image sequence is not included in the ' - 'Referenced Series Sequence or Studies Containing ' - 'Other Referenced Instances Sequence. This is an ' - 'error with the integrity of the ' - 'object.' + self._missing_reference_instances.append( + ref_instance_uid ) referenced_instances.append(ref_instance_uid) referenced_frames.append(frame_source_frames[0]) @@ -2247,28 +2261,41 @@ def _get_ref_instance_uids(self) -> List[Tuple[str, str, str]]: """ instance_data = [] - if hasattr(self, 'ReferencedSeriesSequence'): - for ref_series in self.ReferencedSeriesSequence: - for ref_ins in ref_series.ReferencedInstanceSequence: - instance_data.append( - ( - self.StudyInstanceUID, - ref_series.SeriesInstanceUID, - ref_ins.ReferencedSOPInstanceUID - ) - ) - other_studies_kw = 'StudiesContainingOtherReferencedInstancesSequence' - if hasattr(self, other_studies_kw): - for ref_study in getattr(self, other_studies_kw): - for ref_series in ref_study.ReferencedSeriesSequence: - for ref_ins in ref_series.ReferencedInstanceSequence: - instance_data.append( - ( - ref_study.StudyInstanceUID, - ref_series.SeriesInstanceUID, - ref_ins.ReferencedSOPInstanceUID, + + def _include_sequence(seq): + for ds in seq: + if hasattr(ds, 'ReferencedSeriesSequence'): + for ref_series in ds.ReferencedSeriesSequence: + + # Two different sequences are used here, depending on + # which particular top sequence level sequence we are + # in + if 'ReferencedSOPSequence' in ref_series: + instance_sequence = ( + ref_series.ReferencedSOPSequence + ) + else: + instance_sequence = ( + ref_series.ReferencedInstanceSequence ) - ) + + for ref_ins in instance_sequence: + instance_data.append( + ( + ds.StudyInstanceUID, + ref_series.SeriesInstanceUID, + ref_ins.ReferencedSOPInstanceUID + ) + ) + + # Include the "main" referenced series sequence + _include_sequence([self]) + for kw in [ + 'StudiesContainingOtherReferencedInstancesSequence', + 'SourceImageEvidenceSequence' + ]: + if hasattr(self, kw): + _include_sequence(getattr(self, kw)) # There shouldn't be duplicates here, but there's no explicit rule # preventing it. @@ -2459,6 +2486,15 @@ def get_source_image_uids(self) -> List[Tuple[UID, UID, UID]]: for every image instance referenced in the image. """ + for ref_instance_uid in self._missing_reference_instances: + logger.warning( + f'SOP instances {ref_instance_uid} referenced in the source ' + 'image sequence is not included in the Referenced Series ' + 'Sequence, Source Image Evidence Sequence, or Studies Containing ' + 'Other Referenced Instances Sequence. This is an error with the ' + 'integrity of the object. This instance will be omitted from ' + 'the returned list. ' + ) cur = self._db_con.cursor() res = cur.execute( 'SELECT StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID ' diff --git a/src/highdicom/spatial.py b/src/highdicom/spatial.py index 69447605..50eda4b5 100644 --- a/src/highdicom/spatial.py +++ b/src/highdicom/spatial.py @@ -1193,15 +1193,15 @@ def create_affine_matrix_from_attributes( coordinate system. """ # noqa: E501 - if not isinstance(image_position, Sequence): + if not isinstance(image_position, (Sequence, np.ndarray)): raise TypeError('Argument "image_position" must be a sequence.') if len(image_position) != 3: raise ValueError('Argument "image_position" must have length 3.') - if not isinstance(image_orientation, Sequence): + if not isinstance(image_orientation, (Sequence, np.ndarray)): raise TypeError('Argument "image_orientation" must be a sequence.') if len(image_orientation) != 6: raise ValueError('Argument "image_orientation" must have length 6.') - if not isinstance(pixel_spacing, Sequence): + if not isinstance(pixel_spacing, (Sequence, np.ndarray)): raise TypeError('Argument "pixel_spacing" must be a sequence.') if len(pixel_spacing) != 2: raise ValueError('Argument "pixel_spacing" must have length 2.') diff --git a/tests/utils.py b/tests/utils.py index a0d92193..11283405 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -47,14 +47,25 @@ def find_readable_images() -> list[str]: # Various files are not expected to work and should be excluded exclusions = [ - "badVR.dcm", # cannot be read due to bad VFR - "MR_truncated.dcm", # pixel data is truncated - "liver_1frame.dcm", # missing number of frames - "JPEG2000-embedded-sequence-delimiter.dcm", # pydicom cannot decode pixels - "image_dfl.dcm", # deflated transfer syntax cannot be read lazily - "JPEG-lossy.dcm", # pydicom cannot decode pixels - "TINY_ALPHA", # no pixels - "SC_rgb_jpeg.dcm", # messed up transder syntax + # cannot be read due to bad VFR + "badVR.dcm", + # pixel data is truncated + "MR_truncated.dcm", + # missing number of frames + "liver_1frame.dcm", + # pydicom cannot decode pixels + "JPEG2000-embedded-sequence-delimiter.dcm", + # deflated transfer syntax cannot be read lazily + "image_dfl.dcm", + # pydicom cannot decode pixels + "JPEG-lossy.dcm", + # no pixels + "TINY_ALPHA", + # messed up transfer syntax + "SC_rgb_jpeg.dcm", + # Incorrect source image sequence. This can hopefully be added back + # after https://github.com/pydicom/pydicom/pull/2204 + "SC_rgb_small_odd.dcm", ] files_to_use = []