Skip to content

Commit 7e40043

Browse files
CPBridgeChris Bridge
andauthored
Referenced Images in MeasurementsAndQualitativeEvaluations (TID 1501) (#169)
* Add source image to MeasurementsAndQualitativeEvaluations * Add source image options to get_image_measurement_groups * Implement SourceImageForMeasurementGroup Co-authored-by: Chris Bridge <cbridge@partners.org>
1 parent d34af11 commit 7e40043

File tree

4 files changed

+528
-11
lines changed

4 files changed

+528
-11
lines changed

src/highdicom/sr/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
ImageRegion3D,
77
LongitudinalTemporalOffsetFromEvent,
88
SourceImageForMeasurement,
9+
SourceImageForMeasurementGroup,
910
SourceImageForSegmentation,
1011
SourceImageForRegion,
1112
SourceSeriesForSegmentation,
@@ -136,6 +137,7 @@
136137
'ScoordContentItem',
137138
'Scoord3DContentItem',
138139
'SourceImageForMeasurement',
140+
'SourceImageForMeasurementGroup',
139141
'SourceImageForSegmentation',
140142
'SourceImageForRegion',
141143
'SourceSeriesForSegmentation',

src/highdicom/sr/content.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,114 @@ def _from_dataset(
182182
return cast(LongitudinalTemporalOffsetFromEvent, item)
183183

184184

185+
class SourceImageForMeasurementGroup(ImageContentItem):
186+
187+
"""Content item representing a reference to an image that was used as a
188+
source.
189+
"""
190+
191+
def __init__(
192+
self,
193+
referenced_sop_class_uid: str,
194+
referenced_sop_instance_uid: str,
195+
referenced_frame_numbers: Optional[Sequence[int]] = None
196+
):
197+
"""
198+
Parameters
199+
----------
200+
referenced_sop_class_uid: str
201+
SOP Class UID of the referenced image object
202+
referenced_sop_instance_uid: str
203+
SOP Instance UID of the referenced image object
204+
referenced_frame_numbers: Union[Sequence[int], None], optional
205+
numbers of the frames to which the reference applies in case the
206+
referenced image is a multi-frame image
207+
208+
Raises
209+
------
210+
ValueError
211+
If any referenced frame number is not a positive integer
212+
213+
"""
214+
if referenced_frame_numbers is not None:
215+
if any(f < 1 for f in referenced_frame_numbers):
216+
raise ValueError(
217+
'Referenced frame numbers must be >= 1. Frame indexing is '
218+
'1-based.'
219+
)
220+
super().__init__(
221+
name=CodedConcept(
222+
value='260753009',
223+
scheme_designator='SCT',
224+
meaning='Source',
225+
),
226+
referenced_sop_class_uid=referenced_sop_class_uid,
227+
referenced_sop_instance_uid=referenced_sop_instance_uid,
228+
referenced_frame_numbers=referenced_frame_numbers,
229+
relationship_type=RelationshipTypeValues.CONTAINS
230+
)
231+
232+
@classmethod
233+
def from_source_image(
234+
cls,
235+
image: Dataset,
236+
referenced_frame_numbers: Optional[Sequence[int]] = None
237+
) -> 'SourceImageForMeasurementGroup':
238+
"""Construct the content item directly from an image dataset
239+
240+
Parameters
241+
----------
242+
image: pydicom.dataset.Dataset
243+
Dataset representing the image to be referenced
244+
referenced_frame_numbers: Union[Sequence[int], None], optional
245+
numbers of the frames to which the reference applies in case the
246+
referenced image is a multi-frame image
247+
248+
Returns
249+
-------
250+
highdicom.sr.SourceImageForMeasurementGroup
251+
Content item representing a reference to the image dataset
252+
253+
"""
254+
# Check the dataset and referenced frames are valid
255+
_check_valid_source_image_dataset(image)
256+
_check_frame_numbers_valid_for_dataset(
257+
image,
258+
referenced_frame_numbers
259+
)
260+
return cls(
261+
referenced_sop_class_uid=image.SOPClassUID,
262+
referenced_sop_instance_uid=image.SOPInstanceUID,
263+
referenced_frame_numbers=referenced_frame_numbers
264+
)
265+
266+
@classmethod
267+
def from_dataset(cls, dataset: Dataset) -> 'SourceImageForMeasurementGroup':
268+
"""Construct object from an existing dataset.
269+
270+
Parameters
271+
----------
272+
dataset: pydicom.dataset.Dataset
273+
Dataset representing an SR Content Item with value type IMAGE
274+
275+
Returns
276+
-------
277+
highdicom.sr.SourceImageForMeasurementGroup
278+
Constructed object
279+
280+
"""
281+
dataset_copy = deepcopy(dataset)
282+
return cls._from_dataset(dataset_copy)
283+
284+
@classmethod
285+
def _from_dataset(
286+
cls,
287+
dataset: Dataset
288+
) -> 'SourceImageForMeasurementGroup':
289+
item = super()._from_dataset_base(dataset)
290+
return cast(SourceImageForMeasurementGroup, item)
291+
292+
185293
class SourceImageForMeasurement(ImageContentItem):
186294

187295
"""Content item representing a reference to an image that was used as a

src/highdicom/sr/templates.py

Lines changed: 139 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
RealWorldValueMap,
1818
ReferencedSegment,
1919
ReferencedSegmentationFrame,
20+
SourceImageForMeasurementGroup,
2021
SourceImageForMeasurement,
2122
SourceImageForSegmentation,
2223
SourceSeriesForSegmentation
@@ -42,12 +43,18 @@
4243
)
4344

4445

46+
# Codes missing from pydicom
4547
DEFAULT_LANGUAGE = CodedConcept(
4648
value='en-US',
4749
scheme_designator='RFC5646',
4850
meaning='English (United States)'
4951
)
5052
_REGION_IN_SPACE = Code('130488', 'DCM', 'Region in Space')
53+
_SOURCE = CodedConcept(
54+
value='260753009',
55+
scheme_designator='SCT',
56+
meaning='Source',
57+
)
5158

5259

5360
logger = logging.getLogger(__name__)
@@ -2417,10 +2424,10 @@ def finding_sites(self) -> List[FindingSite]:
24172424
return []
24182425

24192426

2420-
class MeasurementsAndQualitativeEvaluations(Template):
2427+
class _MeasurementsAndQualitativeEvaluations(Template):
24212428

2422-
""":dcm:`TID 1501 <part16/chapter_A.html#sect_TID_1501>`
2423-
Measurement and Qualitative Evaluation Group"""
2429+
"""Abstract base class for Measurements and Qualitative Evaluation
2430+
templates."""
24242431

24252432
def __init__(
24262433
self,
@@ -2436,7 +2443,7 @@ def __init__(
24362443
qualitative_evaluations: Optional[
24372444
Sequence[QualitativeEvaluation]
24382445
] = None,
2439-
finding_category: Optional[Union[CodedConcept, Code]] = None
2446+
finding_category: Optional[Union[CodedConcept, Code]] = None,
24402447
):
24412448
"""
24422449
@@ -2597,7 +2604,7 @@ def from_sequence(
25972604
cls,
25982605
sequence: Sequence[Dataset],
25992606
is_root: bool = False
2600-
) -> 'MeasurementsAndQualitativeEvaluations':
2607+
) -> '_MeasurementsAndQualitativeEvaluations':
26012608
"""Construct object from a sequence of datasets.
26022609
26032610
Parameters
@@ -2612,7 +2619,7 @@ def from_sequence(
26122619
26132620
Returns
26142621
-------
2615-
highdicom.sr.MeasurementsAndQualitativeEvaluations
2622+
highdicom.sr._MeasurementsAndQualitativeEvaluations
26162623
Content Sequence containing root CONTAINER SR Content Item
26172624
26182625
"""
@@ -2632,8 +2639,8 @@ def from_sequence(
26322639
'because it does not have name "Measurement Group".'
26332640
)
26342641
instance = ContentSequence.from_sequence(sequence)
2635-
instance.__class__ = MeasurementsAndQualitativeEvaluations
2636-
return cast(MeasurementsAndQualitativeEvaluations, instance)
2642+
instance.__class__ = cls
2643+
return cast(cls, instance)
26372644

26382645
@property
26392646
def method(self) -> Union[CodedConcept, None]:
@@ -2812,8 +2819,112 @@ def get_qualitative_evaluations(
28122819
]
28132820

28142821

2822+
class MeasurementsAndQualitativeEvaluations(
2823+
_MeasurementsAndQualitativeEvaluations
2824+
):
2825+
2826+
""":dcm:`TID 1501 <part16/chapter_A.html#sect_TID_1501>`
2827+
Measurement and Qualitative Evaluation Group"""
2828+
2829+
def __init__(
2830+
self,
2831+
tracking_identifier: TrackingIdentifier,
2832+
referenced_real_world_value_map: Optional[RealWorldValueMap] = None,
2833+
time_point_context: Optional[TimePointContext] = None,
2834+
finding_type: Optional[Union[CodedConcept, Code]] = None,
2835+
method: Optional[Union[CodedConcept, Code]] = None,
2836+
algorithm_id: Optional[AlgorithmIdentification] = None,
2837+
finding_sites: Optional[Sequence[FindingSite]] = None,
2838+
session: Optional[str] = None,
2839+
measurements: Sequence[Measurement] = None,
2840+
qualitative_evaluations: Optional[
2841+
Sequence[QualitativeEvaluation]
2842+
] = None,
2843+
finding_category: Optional[Union[CodedConcept, Code]] = None,
2844+
source_images: Optional[
2845+
Sequence[SourceImageForMeasurementGroup]
2846+
] = None,
2847+
):
2848+
"""
2849+
2850+
Parameters
2851+
----------
2852+
tracking_identifier: highdicom.sr.TrackingIdentifier
2853+
Identifier for tracking measurements
2854+
referenced_real_world_value_map: Union[highdicom.sr.RealWorldValueMap, None], optional
2855+
Referenced real world value map for region of interest
2856+
time_point_context: Union[highdicom.sr.TimePointContext, None], optional
2857+
Description of the time point context
2858+
finding_type: Union[highdicom.sr.CodedConcept, pydicom.sr.coding.Code, None], optional
2859+
Type of observed finding
2860+
method: Union[highdicom.sr.CodedConcept, pydicom.sr.coding.Code, None], optional
2861+
Coded measurement method (see
2862+
:dcm:`CID 6147 <part16/sect_CID_6147.html>`
2863+
"Response Criteria" for options)
2864+
algorithm_id: Union[highdicom.sr.AlgorithmIdentification, None], optional
2865+
Identification of algorithm used for making measurements
2866+
finding_sites: Sequence[highdicom.sr.FindingSite, None], optional
2867+
Coded description of one or more anatomic locations at which
2868+
finding was observed
2869+
session: Union[str, None], optional
2870+
Description of the session
2871+
measurements: Union[Sequence[highdicom.sr.Measurement], None], optional
2872+
Numeric measurements
2873+
qualitative_evaluations: Union[Sequence[highdicom.sr.QualitativeEvaluation], None], optional
2874+
Coded name-value pairs that describe qualitative evaluations
2875+
finding_category: Union[highdicom.sr.CodedConcept, pydicom.sr.coding.Code, None], optional
2876+
Category of observed finding, e.g., anatomic structure or
2877+
morphologically abnormal structure
2878+
source_images: Optional[Sequence[highdicom.sr.SourceImageForMeasurementGroup]], optional
2879+
Images to that were the source of the measurements. If not provided,
2880+
all images that listed in the document tree of the containing SR
2881+
document are assumed to be source images.
2882+
2883+
""" # noqa: E501
2884+
super().__init__(
2885+
tracking_identifier=tracking_identifier,
2886+
referenced_real_world_value_map=referenced_real_world_value_map,
2887+
time_point_context=time_point_context,
2888+
finding_type=finding_type,
2889+
method=method,
2890+
algorithm_id=algorithm_id,
2891+
finding_sites=finding_sites,
2892+
session=session,
2893+
measurements=measurements,
2894+
qualitative_evaluations=qualitative_evaluations,
2895+
finding_category=finding_category,
2896+
)
2897+
group_item = self[0]
2898+
2899+
if source_images is not None:
2900+
for img in source_images:
2901+
if not isinstance(img, SourceImageForMeasurementGroup):
2902+
raise TypeError(
2903+
'Items of argument "source_images" must be of type '
2904+
'highdicom.sr.SourceImageForMeasurementGroup.'
2905+
)
2906+
group_item.ContentSequence.extend(source_images)
2907+
2908+
@property
2909+
def source_images(self) -> List[SourceImageForMeasurementGroup]:
2910+
"""List[highdicom.sr.SourceImageForMeasurementGroup]: source images"""
2911+
root_item = self[0]
2912+
matches = find_content_items(
2913+
root_item,
2914+
name=_SOURCE,
2915+
value_type=ValueTypeValues.IMAGE,
2916+
relationship_type=RelationshipTypeValues.CONTAINS
2917+
)
2918+
if len(matches) > 0:
2919+
return [
2920+
SourceImageForMeasurementGroup.from_dataset(m) for m in matches
2921+
]
2922+
return []
2923+
2924+
28152925
class _ROIMeasurementsAndQualitativeEvaluations(
2816-
MeasurementsAndQualitativeEvaluations):
2926+
_MeasurementsAndQualitativeEvaluations
2927+
):
28172928

28182929
"""Abstract base class for ROI Measurements and Qualitative Evaluation
28192930
templates."""
@@ -4423,6 +4534,8 @@ def get_image_measurement_groups(
44234534
tracking_uid: Optional[str] = None,
44244535
finding_type: Optional[Union[CodedConcept, Code]] = None,
44254536
finding_site: Optional[Union[CodedConcept, Code]] = None,
4537+
referenced_sop_instance_uid: Optional[str] = None,
4538+
referenced_sop_class_uid: Optional[str] = None
44264539
) -> List[MeasurementsAndQualitativeEvaluations]:
44274540
"""Get imaging measurements of images.
44284541
@@ -4438,6 +4551,10 @@ def get_image_measurement_groups(
44384551
Finding
44394552
finding_site: Union[highdicom.sr.CodedConcept, pydicom.sr.coding.Code, None], optional
44404553
Finding site
4554+
referenced_sop_instance_uid: Union[str, None], optional
4555+
SOP Instance UID of the referenced instance.
4556+
referenced_sop_class_uid: Union[str, None], optional
4557+
SOP Class UID of the referenced instance.
44414558
44424559
Returns
44434560
-------
@@ -4483,6 +4600,19 @@ def get_image_measurement_groups(
44834600
)
44844601
matches.append(matches_tracking_uid)
44854602

4603+
if (
4604+
(referenced_sop_instance_uid is not None) or
4605+
(referenced_sop_class_uid is not None)
4606+
):
4607+
matches_uids = _contains_image_items(
4608+
group_item,
4609+
name=_SOURCE,
4610+
referenced_sop_class_uid=referenced_sop_class_uid,
4611+
referenced_sop_instance_uid=referenced_sop_instance_uid,
4612+
relationship_type=RelationshipTypeValues.CONTAINS
4613+
)
4614+
matches.append(matches_uids)
4615+
44864616
seq = MeasurementsAndQualitativeEvaluations.from_sequence(
44874617
[group_item]
44884618
)

0 commit comments

Comments
 (0)