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
25 changes: 21 additions & 4 deletions docs/ann.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ contains. The required metadata elements include:
* A ``uid`` (``str`` or :class:`highdicom.UID`) uniquely identifying the group.
Usually, you will want to generate a UID for this.
* An ``annotated_property_category`` and ``annotated_property_type``
(:class:`highdicom.sr.CodedConcept`) coded values (see :ref:`coding`)
(:class:`highdicom.sr.CodedConcept`), coded values (see :ref:`coding`)
describing the category and specific structure that has been annotated.
* A ``graphic_type`` (:class:`highdicom.ann.GraphicTypeValues`) indicating the
"form" of the annotations. Permissible values are ``"ELLIPSE"``, ``"POINT"``,
Expand All @@ -45,8 +45,23 @@ contains. The required metadata elements include:
algorithm used to generate the annotations (``"MANUAL"``,
``"SEMIAUTOMATIC"``, or ``"AUTOMATIC"``).

Further optional metadata may optionally be provided, see the API documentation
for more information.
Further optional metadata may optionally be provided, including:

* An ``algorithm_identification``
(:class:`highdicom.AlgorithmIdentificationSequence`), specifying information
about an algorithm that generated the annotations.
* A list of ``anatomic_regions`` (a sequence of
:class:`highdicom.sr.CodedConcept` objects), giving coded values (see
:ref:`coding`) describing regions containing the annotations.
* A list of ``primary_anatomic_structures`` (a sequence of
:class:`highdicom.sr.CodedConcept` objects) giving coded values (see
:ref:`coding`) describing anatomic structures of interest.
* A free-text ``description`` (``str``) of the annotation group.
* A ``display_color`` (:class:`highdicom.color.CIELabColor`) giving a
recommended value for viewers to use to render these annotations. This is in
CIE-Lab color space, but alternative constructors of the
:class:`highdicom.color.CIELabColor` class allow conversion from RGB values
or well-known color names.

The actual annotation data is passed to the group as a list of
``numpy.ndarray`` objects, each of shape (*N* x *D*). *N* is the number of
Expand Down Expand Up @@ -88,6 +103,7 @@ Here is a simple example of constructing an annotation group:
algorithm_type=hd.ann.AnnotationGroupGenerationTypeValues.MANUAL,
graphic_type=hd.ann.GraphicTypeValues.POINT,
graphic_data=graphic_data,
display_color=hd.color.CIELabColor.from_string('turquoise'),
)

Note that including two nuclei would be very unusual in practice: annotations
Expand Down Expand Up @@ -141,6 +157,7 @@ Here is the above example with an area measurement included:
graphic_type=hd.ann.GraphicTypeValues.POINT,
graphic_data=graphic_data,
measurements=[area_measurement],
display_color=hd.color.CIELabColor.from_string('lawngreen'),
)

Constructing MicroscopyBulkSimpleAnnotation Objects
Expand Down Expand Up @@ -256,7 +273,7 @@ of matching groups, since the filters may match multiple groups.

# If there are no matches, an empty list is returned
groups = ann.get_annotation_groups(
annotated_property_type=Code('53982002', "SCT", "Cell membrane"),
annotated_property_type=Code('53982002', 'SCT', 'Cell membrane'),
)
assert len(groups) == 0

Expand Down
9 changes: 9 additions & 0 deletions docs/seg.rst
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,13 @@ description includes the following information:
a human readable ID and unique ID to a specific segment. This can be used,
for example, to uniquely identify particular lesions over multiple imaging
studies. These are passed as strings.
- **Display Color**: (Optional) You can provide a recommended color as a
:class:`highdicom.color.CIELabColor` to use when displaying this segment.
Some viewers will use this information to decide what color to render the
segment by default. This color should be provided in CIE-Lab color space, but
alternative constructors of the :class:`highdicom.color.CIELabColor` class
allow conversion from RGB values or well-known color names.


Notice that the segment description makes use of coded concepts to ensure that
the way a particular anatomical structure is described is standardized and
Expand All @@ -108,6 +115,7 @@ representing a liver that has been manually segmented.
segmented_property_category=codes.SCT.Organ,
segmented_property_type=codes.SCT.Liver,
algorithm_type=hd.seg.SegmentAlgorithmTypeValues.MANUAL,
display_color=hd.color.CIELabColor.from_string('red'),
)

In this second example, we describe a segment representing a tumor that has
Expand All @@ -134,6 +142,7 @@ we must first provide more information about the algorithm used in an
algorithm_type=hd.seg.SegmentAlgorithmTypeValues.AUTOMATIC,
algorithm_identification=algorithm_identification,
anatomic_regions=[codes.SCT.Kidney]
display_color=hd.color.CIELabColor.from_rgb(0, 0, 255),
)

For a description of how to access segment metadata in existing segmentations,
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "highdicom"
version = "0.26.1"
version = "0.27.0"
description = "High-level DICOM abstractions."
readme = "README.md"
requires-python = ">=3.10"
Expand Down
16 changes: 15 additions & 1 deletion src/highdicom/ann/content.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
AnnotationGroupGenerationTypeValues,
GraphicTypeValues,
)
from highdicom.color import CIELabColor
from highdicom.content import AlgorithmIdentificationSequence
from highdicom.sr.coding import CodedConcept
from highdicom.uid import UID
Expand Down Expand Up @@ -194,7 +195,8 @@ def __init__(
) = None,
primary_anatomic_structures: None | (
Sequence[Code | CodedConcept]
) = None
) = None,
display_color: CIELabColor | None = None,
):
"""
Parameters
Expand Down Expand Up @@ -238,6 +240,8 @@ def __init__(
primary_anatomic_structures: Union[Sequence[Union[highdicom.sr.Code, highdicom.sr.CodedConcept]], None], optional
Anatomic structure(s) the annotations represent
(see CIDs for domain-specific primary anatomic structures)
display_color: Union[highdicom.color.CIELabColor, None], optional
A recommended color to render this annotation group.

""" # noqa: E501
super().__init__()
Expand Down Expand Up @@ -293,6 +297,16 @@ def __init__(
graphic_type = GraphicTypeValues(graphic_type)
self.GraphicType = graphic_type.value

if display_color is not None:
if not isinstance(display_color, CIELabColor):
raise TypeError(
'"display_color" must be of type '
'highdicom.color.CIELabColor.'
)
self.RecommendedDisplayCIELabValue = list(
display_color.value
)

for i in range(len(graphic_data)):
num_coords = graphic_data[i].shape[0]
if graphic_type == GraphicTypeValues.POINT:
Expand Down
74 changes: 71 additions & 3 deletions src/highdicom/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ def __init__(
device_serial_number: str | None = None,
institution_name: str | None = None,
institutional_department_name: str | None = None,
content_date: str | datetime.date | None = None,
content_time: str | datetime.time | None = None,
series_date: str | datetime.date | None = None,
series_time: str | datetime.time | None = None,
):
"""
Parameters
Expand Down Expand Up @@ -121,6 +125,18 @@ def __init__(
institutional_department_name: Union[str, None], optional
Name of the department of the person or device that creates the
SR document instance.
content_date: str | datetime.date | None, optional
Date the content of this instance was created. If not specified,
the current date will be used.
content_time: str | datetime.time | None, optional
Time the content of this instance was created. If not specified,
the current time will be used.
series_date: str | datetime.date | None, optional
Date the series was started. This should be the same for all
instances in a series.
series_time: str | datetime.time | None, optional
Time the series was started. This should be the same for all
instances in a series.

Note
----
Expand Down Expand Up @@ -207,7 +223,11 @@ def __init__(
_check_long_string(device_serial_number)
self.DeviceSerialNumber = device_serial_number
if software_versions is not None:
_check_long_string(software_versions)
if not isinstance(software_versions, str):
for v in software_versions:
_check_long_string(v)
else:
_check_long_string(software_versions)
self.SoftwareVersions = software_versions
if institution_name is not None:
_check_long_string(institution_name)
Expand All @@ -227,11 +247,59 @@ def __init__(
)
self.InstanceNumber = instance_number

now = datetime.datetime.now()
self.InstanceCreationDate = DA(now.date())
self.InstanceCreationTime = TM(now.time())

# Content Date and Content Time are not present in all IODs
if is_attribute_in_iod('ContentDate', sop_class_uid):
self.ContentDate = DA(datetime.datetime.now().date())
if content_date is None:
if content_time is not None:
raise TypeError(
"'content_time' may not be specified without "
"'content_date'."
)
content_date = now.date()
content_date = DA(content_date)
self.ContentDate = content_date
elif content_date is not None:
raise TypeError(
f"'content_date' should not be specified for SOP Class UID: "
f"{sop_class_uid}"
)
if is_attribute_in_iod('ContentTime', sop_class_uid):
self.ContentTime = TM(datetime.datetime.now().time())
if content_time is None:
content_time = now.time()
content_time = TM(content_time)
self.ContentTime = content_time
elif content_time is not None:
raise TypeError(
f"'content_time' should not be specified for SOP Class UID: "
f"{sop_class_uid}"
)

if series_date is not None:
series_date = DA(series_date)
if content_date is not None:
if series_date > content_date:
raise ValueError(
"'series_date' must not be later than 'content_date'."
)
self.SeriesDate = series_date
if series_time is not None:
series_time = TM(series_time)
if series_date is None:
raise TypeError(
"'series_time' may not be specified without "
"'series_date'."
)
if content_time is not None:
if series_time > content_time:
raise ValueError(
"'series_time' must not be later than content time."
)
self.SeriesTime = series_time

if content_qualification is not None:
content_qualification = ContentQualificationValues(
content_qualification
Expand Down
Loading