Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow reference of optical path for annotations measurements #194

Open
wants to merge 30 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
4cd7fad
Add warning when empty segmentation is passed with omit_empty_frames
Jul 6, 2022
2c7a629
Change UserWarning to logger.warning
CPBridge Jul 7, 2022
1e4334a
Allow reference of optical path for measurements
hackermd Aug 5, 2022
2609498
Add property to access referenced images
hackermd Aug 5, 2022
da2f5b1
Provide referenced images for measurements
hackermd Aug 5, 2022
366c58a
Remove module level imports of module data (#196)
CPBridge Aug 15, 2022
ed5901f
Minor fixes to segmentation (#195)
CPBridge Aug 15, 2022
bfbd807
Implement TID 1601 Image Library Entry (#83)
seandoyle Aug 19, 2022
d03f4c8
Increase package version
hackermd Aug 19, 2022
6d80f5a
Move pylibjpeg-libjpeg to optional dependency
Oct 10, 2022
3a04c75
Add skips for tests that need libjpeg, restructure segmentation tests
Oct 10, 2022
d1e632d
Remove unnecessary import error
Oct 10, 2022
89d1343
Update installation docs
Oct 10, 2022
2656162
Bump python version in installation docs to match setup.py
Oct 10, 2022
0ca5784
Add libjpeg to CI workflow
Oct 10, 2022
d1a904e
Add workflow with and without libjpeg
Oct 10, 2022
320b4f4
Apply suggestions from code review
CPBridge Oct 11, 2022
1422a25
Update docs/installation.rst
CPBridge Oct 11, 2022
7e08517
remove openjpeg from deps, fix installation guide
Oct 11, 2022
c03598d
Add citation file (#204)
hackermd Oct 28, 2022
2152c19
Use deepcopy for CodedConcept.from_dataset()
Oct 28, 2022
14365c3
Merge pull request #205 from herrmannlab/fix_coded_concept_copy
CPBridge Nov 9, 2022
9026587
Merge pull request #201 from herrmannlab/make_pylibjpeg-libjpeg_optional
CPBridge Nov 9, 2022
4b13338
Merge pull request #181 from herrmannlab/empty_seg_error_message
CPBridge Nov 9, 2022
d36e2a5
Increase package version for release (#206)
CPBridge Nov 9, 2022
91e616d
Allow reference of optical path for measurements
hackermd Aug 5, 2022
615d38c
Add property to access referenced images
hackermd Aug 5, 2022
d32c006
Provide referenced images for measurements
hackermd Aug 5, 2022
bbd1a01
Merge branch 'enhancement/annotations-intensity-measurements' of gith…
hackermd Nov 18, 2022
1ff6985
Update src/highdicom/ann/content.py
hackermd Apr 1, 2023
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
60 changes: 56 additions & 4 deletions src/highdicom/ann/content.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@
AnnotationGroupGenerationTypeValues,
GraphicTypeValues,
)
from highdicom.content import AlgorithmIdentificationSequence
from highdicom.content import (
AlgorithmIdentificationSequence,
ReferencedImageSequence,
)
from highdicom.sr.coding import CodedConcept
from highdicom.uid import UID
from highdicom._module_utils import check_required_attributes
Expand All @@ -25,7 +28,8 @@ def __init__(
self,
name: Union[Code, CodedConcept],
values: np.ndarray,
unit: Union[Code, CodedConcept]
unit: Union[Code, CodedConcept],
referenced_images: Optional[ReferencedImageSequence] = None
) -> None:
"""
Parameters
Expand All @@ -40,6 +44,9 @@ def __init__(
unit: Union[highdicom.sr.CodedConcept, pydicom.sr.coding.Code], optional
Coded units of measurement (see :dcm:`CID 7181 <part16/sect_CID_7181.html>`
"Abstract Multi-dimensional Image Model Component Units")
referenced_images: Union[highdicom.ReferencedImageSequence, None], optional
Referenced image to which the measurement applies. Should only be
provided for intensity measurements.

""" # noqa: E501
super().__init__()
Expand All @@ -61,6 +68,22 @@ def __init__(
item.AnnotationIndexList = stored_indices.tobytes()
self.MeasurementValuesSequence = [item]

if referenced_images is not None:
if len(referenced_images) == 0:
raise ValueError(
'Argument "referenced_images" must contain one item.'
)
elif len(referenced_images) > 1:
raise ValueError(
'Argument "referenced_images" must contain only one item.'
)
if not isinstance(referenced_images, ReferencedImageSequence):
raise TypeError(
'Argument "referenced_images" must have type '
'ReferencedImageSequence.'
)
self.ReferencedImageSequence = referenced_images

@property
def name(self) -> CodedConcept:
"""highdicom.sr.CodedConcept: coded name"""
Expand All @@ -71,6 +94,14 @@ def unit(self) -> CodedConcept:
"""highdicom.sr.CodedConcept: coded unit"""
return self.MeasurementUnitsCodeSequence[0]

@property
def referenced_images(self) -> Union[ReferencedImageSequence, None]:
"""Union[highdicom.ReferencedImageSequence, None]: referenced images"""
if hasattr(self, 'ReferencedImageSequence'):
return self.ReferencedImageSequence
hackermd marked this conversation as resolved.
Show resolved Hide resolved
else:
return None

def get_values(self, number_of_annotations: int) -> np.ndarray:
"""Get measured values for annotations.

Expand Down Expand Up @@ -151,6 +182,11 @@ def from_dataset(cls, dataset: Dataset) -> 'Measurements':
measurements.MeasurementUnitsCodeSequence[0]
)
]
if hasattr(measurements, 'ReferencedImageSequence'):
measurements.ReferencedImageSequence = \
ReferencedImageSequence.from_sequence(
measurements.ReferencedImageSequence
)

return cast(Measurements, measurements)

Expand Down Expand Up @@ -520,6 +556,12 @@ def get_graphic_data(
)
else:
if coordinate_type == AnnotationCoordinateTypeValues.SCOORD:
if hasattr(self, 'CommonZCoordinateValue'):
raise ValueError(
'The annotation group contains the '
'"Common Z Coordinate Value" element and therefore '
'cannot have Annotation Coordinate Type "2D".'
)
coordinate_dimensionality = 2
else:
coordinate_dimensionality = 3
Expand Down Expand Up @@ -633,7 +675,10 @@ def get_measurements(
self,
name: Optional[Union[Code, CodedConcept]] = None
) -> Tuple[
List[CodedConcept], np.ndarray, List[CodedConcept]
List[CodedConcept],
np.ndarray,
List[CodedConcept],
List[Union[ReferencedImageSequence, None]]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NB backwards incompatible change

]:
"""Get measurements.

Expand All @@ -654,6 +699,8 @@ def get_measurements(
given annotation.
units: List[highdicom.sr.CodedConcept]
Units of measurements
referenced_images: List[highdicom.ReferencedImageSequence, None]
Referenced images

""" # noqa: E501
number_of_annotations = self.number_of_annotations
Expand All @@ -675,11 +722,16 @@ def get_measurements(
item.unit for item in self.MeasurementsSequence
if name is None or item.name == name
]
referenced_images = [
item.referenced_images for item in self.MeasurementsSequence
if name is None or item.name == name
]
else:
value_array = np.empty((number_of_annotations, 0), np.float32)
names = []
units = []
return (names, value_array, units)
referenced_images = []
return (names, value_array, units, referenced_images)

def _get_coordinate_index(
self,
Expand Down
94 changes: 81 additions & 13 deletions src/highdicom/content.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
from pydicom.sr.coding import Code
from pydicom.sr.codedict import codes
from pydicom.valuerep import DS, format_number_as_ds
from pydicom._storage_sopclass_uids import SegmentationStorage
from pydicom.uid import (
SegmentationStorage,
VLWholeSlideMicroscopyImageStorage,
)

from highdicom.enum import (
CoordinateSystemNames,
Expand Down Expand Up @@ -105,7 +108,7 @@ def from_sequence(

Returns
-------
highdicom.seg.content.AlgorithmIdentificationSequence
highdicom.AlgorithmIdentificationSequence
Algorithm Identification Sequence

"""
Expand Down Expand Up @@ -1406,21 +1409,20 @@ def __init__(
referenced_images: Optional[Sequence[Dataset]] = None,
referenced_frame_number: Union[int, Sequence[int], None] = None,
referenced_segment_number: Union[int, Sequence[int], None] = None,
referenced_optical_path_identifier: Union[int, None] = None,
):
"""

Parameters
----------
referenced_images: Union[Sequence[pydicom.Dataset], None], optional
Images to which the VOI LUT described in this dataset applies. Note
that if unspecified, the VOI LUT applies to every image referenced
in the presentation state object that this dataset is included in.
Images that should be referenced
referenced_frame_number: Union[int, Sequence[int], None], optional
Frame number(s) within a referenced multiframe image to which this
VOI LUT applies.
Frame number(s) within a referenced multiframe image
referenced_segment_number: Union[int, Sequence[int], None], optional
Segment number(s) within a referenced segmentation image to which
this VOI LUT applies.
Segment number(s) within a referenced segmentation image
referenced_optical_path_identifier: Union[int, None], optional
Identifier of the optical path within a referenced microscopy image

"""
super().__init__()
Expand All @@ -1445,6 +1447,7 @@ def __init__(
raise ValueError("Found duplicate instances in referenced images.")

multiple_images = len(referenced_images) > 1
sop_class_uid = referenced_images[0].SOPClassUID
if referenced_frame_number is not None:
if multiple_images:
raise ValueError(
Expand All @@ -1466,16 +1469,17 @@ def __init__(
f'Frame number {f} is invalid for referenced '
'image.'
)

if referenced_segment_number is not None:
if multiple_images:
raise ValueError(
'Specifying "referenced_segment_number" is not '
'supported with multiple referenced images.'
)
if referenced_images[0].SOPClassUID != SegmentationStorage:
if sop_class_uid != SegmentationStorage:
raise TypeError(
'"referenced_segment_number" is only valid when the '
'referenced image is a segmentation image.'
'referenced image is a Segmentation image.'
)
number_of_segments = len(referenced_images[0].SegmentSequence)
if isinstance(referenced_segment_number, Sequence):
Expand All @@ -1485,8 +1489,7 @@ def __init__(
for s in _referenced_segment_numbers:
if s < 1 or s > number_of_segments:
raise ValueError(
f'Segment number {s} is invalid for referenced '
'image.'
f'Segment number {s} is invalid for referenced image.'
)
if referenced_frame_number is not None:
# Check that the one of the specified segments exists
Expand All @@ -1504,6 +1507,31 @@ def __init__(
f'Referenced frame {f} does not contain any of '
'the referenced segments.'
)

if referenced_optical_path_identifier is not None:
if multiple_images:
raise ValueError(
'Specifying "referenced_optical_path_identifier" is not '
'supported with multiple referenced images.'
)
if sop_class_uid != VLWholeSlideMicroscopyImageStorage:
raise TypeError(
'"referenced_optical_path_identifier" is only valid when '
'referenced image is a VL Whole Slide Microscopy image.'
)
has_optical_path = False
for ref_img in referenced_images:
for optical_path_item in ref_img.OpticalPathSequence:
has_optical_path |= (
optical_path_item.OpticalPathIdentifier ==
referenced_optical_path_identifier
)
if not has_optical_path:
raise ValueError(
'None of the reference images contains the specified '
'"referenced_optical_path_identifier".'
)

for im in referenced_images:
if not does_iod_have_pixel_data(im.SOPClassUID):
raise ValueError(
Expand All @@ -1515,10 +1543,50 @@ def __init__(
ref_im.ReferencedSOPClassUID = im.SOPClassUID
if referenced_segment_number is not None:
ref_im.ReferencedSegmentNumber = referenced_segment_number
elif referenced_optical_path_identifier is not None:
ref_im.ReferencedOpticalPathIdentifier = \
str(referenced_optical_path_identifier)
if referenced_frame_number is not None:
ref_im.ReferencedFrameNumber = referenced_frame_number
self.append(ref_im)

@classmethod
def from_sequence(
cls,
sequence: DataElementSequence
) -> 'ReferencedImageSequence':
"""Construct instance from an existing data element sequence.

Parameters
----------
sequence: pydicom.sequence.Sequence
Data element sequence representing the
Algorithm Identification Sequence

Returns
-------
highdicom.ReferencedImageSequence
Referenced Image Sequence

"""
if not isinstance(sequence, DataElementSequence):
raise TypeError(
'Sequence should be of type pydicom.sequence.Sequence.'
)
if len(sequence) != 1:
raise ValueError('Sequence should contain a single item.')
check_required_attributes(
sequence[0],
module='advanced-blending-presentation-state',
base_path=[
'AdvancedBlendingSequence',
'ReferencedImageSequence',
]
)
ref_img_sequence = deepcopy(sequence)
ref_img_sequence.__class__ = ReferencedImageSequence
return cast(ReferencedImageSequence, ref_img_sequence)


class LUT(Dataset):

Expand Down
Loading