Skip to content

Commit e3beae5

Browse files
authored
v0.27.0 release (#377)
* Add CIELabColor to AnnotationGroup (#374) * Add CIELabColor to AnnotationGroup * Add further description of annotation group parameters * Add colorspace conversions * Fix rounding * Bug fix * Add checks and docs to new methods * Docs and tests for color conversion * Add cie-lab examples to docstring * Tweak examples * Add CIELabColor.lab property * Version bump * Add content date/time, series date/time, instance creation date/time to base class (#376)
1 parent 5a1f14b commit e3beae5

File tree

10 files changed

+783
-18
lines changed

10 files changed

+783
-18
lines changed

docs/ann.rst

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ contains. The required metadata elements include:
3535
* A ``uid`` (``str`` or :class:`highdicom.UID`) uniquely identifying the group.
3636
Usually, you will want to generate a UID for this.
3737
* An ``annotated_property_category`` and ``annotated_property_type``
38-
(:class:`highdicom.sr.CodedConcept`) coded values (see :ref:`coding`)
38+
(:class:`highdicom.sr.CodedConcept`), coded values (see :ref:`coding`)
3939
describing the category and specific structure that has been annotated.
4040
* A ``graphic_type`` (:class:`highdicom.ann.GraphicTypeValues`) indicating the
4141
"form" of the annotations. Permissible values are ``"ELLIPSE"``, ``"POINT"``,
@@ -45,8 +45,23 @@ contains. The required metadata elements include:
4545
algorithm used to generate the annotations (``"MANUAL"``,
4646
``"SEMIAUTOMATIC"``, or ``"AUTOMATIC"``).
4747

48-
Further optional metadata may optionally be provided, see the API documentation
49-
for more information.
48+
Further optional metadata may optionally be provided, including:
49+
50+
* An ``algorithm_identification``
51+
(:class:`highdicom.AlgorithmIdentificationSequence`), specifying information
52+
about an algorithm that generated the annotations.
53+
* A list of ``anatomic_regions`` (a sequence of
54+
:class:`highdicom.sr.CodedConcept` objects), giving coded values (see
55+
:ref:`coding`) describing regions containing the annotations.
56+
* A list of ``primary_anatomic_structures`` (a sequence of
57+
:class:`highdicom.sr.CodedConcept` objects) giving coded values (see
58+
:ref:`coding`) describing anatomic structures of interest.
59+
* A free-text ``description`` (``str``) of the annotation group.
60+
* A ``display_color`` (:class:`highdicom.color.CIELabColor`) giving a
61+
recommended value for viewers to use to render these annotations. This is in
62+
CIE-Lab color space, but alternative constructors of the
63+
:class:`highdicom.color.CIELabColor` class allow conversion from RGB values
64+
or well-known color names.
5065

5166
The actual annotation data is passed to the group as a list of
5267
``numpy.ndarray`` objects, each of shape (*N* x *D*). *N* is the number of
@@ -88,6 +103,7 @@ Here is a simple example of constructing an annotation group:
88103
algorithm_type=hd.ann.AnnotationGroupGenerationTypeValues.MANUAL,
89104
graphic_type=hd.ann.GraphicTypeValues.POINT,
90105
graphic_data=graphic_data,
106+
display_color=hd.color.CIELabColor.from_string('turquoise'),
91107
)
92108
93109
Note that including two nuclei would be very unusual in practice: annotations
@@ -141,6 +157,7 @@ Here is the above example with an area measurement included:
141157
graphic_type=hd.ann.GraphicTypeValues.POINT,
142158
graphic_data=graphic_data,
143159
measurements=[area_measurement],
160+
display_color=hd.color.CIELabColor.from_string('lawngreen'),
144161
)
145162
146163
Constructing MicroscopyBulkSimpleAnnotation Objects
@@ -256,7 +273,7 @@ of matching groups, since the filters may match multiple groups.
256273
257274
# If there are no matches, an empty list is returned
258275
groups = ann.get_annotation_groups(
259-
annotated_property_type=Code('53982002', "SCT", "Cell membrane"),
276+
annotated_property_type=Code('53982002', 'SCT', 'Cell membrane'),
260277
)
261278
assert len(groups) == 0
262279

docs/seg.rst

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

8996
Notice that the segment description makes use of coded concepts to ensure that
9097
the way a particular anatomical structure is described is standardized and
@@ -108,6 +115,7 @@ representing a liver that has been manually segmented.
108115
segmented_property_category=codes.SCT.Organ,
109116
segmented_property_type=codes.SCT.Liver,
110117
algorithm_type=hd.seg.SegmentAlgorithmTypeValues.MANUAL,
118+
display_color=hd.color.CIELabColor.from_string('red'),
111119
)
112120
113121
In this second example, we describe a segment representing a tumor that has
@@ -134,6 +142,7 @@ we must first provide more information about the algorithm used in an
134142
algorithm_type=hd.seg.SegmentAlgorithmTypeValues.AUTOMATIC,
135143
algorithm_identification=algorithm_identification,
136144
anatomic_regions=[codes.SCT.Kidney]
145+
display_color=hd.color.CIELabColor.from_rgb(0, 0, 255),
137146
)
138147
139148
For a description of how to access segment metadata in existing segmentations,

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "highdicom"
7-
version = "0.26.1"
7+
version = "0.27.0"
88
description = "High-level DICOM abstractions."
99
readme = "README.md"
1010
requires-python = ">=3.10"

src/highdicom/ann/content.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
AnnotationGroupGenerationTypeValues,
1414
GraphicTypeValues,
1515
)
16+
from highdicom.color import CIELabColor
1617
from highdicom.content import AlgorithmIdentificationSequence
1718
from highdicom.sr.coding import CodedConcept
1819
from highdicom.uid import UID
@@ -194,7 +195,8 @@ def __init__(
194195
) = None,
195196
primary_anatomic_structures: None | (
196197
Sequence[Code | CodedConcept]
197-
) = None
198+
) = None,
199+
display_color: CIELabColor | None = None,
198200
):
199201
"""
200202
Parameters
@@ -238,6 +240,8 @@ def __init__(
238240
primary_anatomic_structures: Union[Sequence[Union[highdicom.sr.Code, highdicom.sr.CodedConcept]], None], optional
239241
Anatomic structure(s) the annotations represent
240242
(see CIDs for domain-specific primary anatomic structures)
243+
display_color: Union[highdicom.color.CIELabColor, None], optional
244+
A recommended color to render this annotation group.
241245
242246
""" # noqa: E501
243247
super().__init__()
@@ -293,6 +297,16 @@ def __init__(
293297
graphic_type = GraphicTypeValues(graphic_type)
294298
self.GraphicType = graphic_type.value
295299

300+
if display_color is not None:
301+
if not isinstance(display_color, CIELabColor):
302+
raise TypeError(
303+
'"display_color" must be of type '
304+
'highdicom.color.CIELabColor.'
305+
)
306+
self.RecommendedDisplayCIELabValue = list(
307+
display_color.value
308+
)
309+
296310
for i in range(len(graphic_data)):
297311
num_coords = graphic_data[i].shape[0]
298312
if graphic_type == GraphicTypeValues.POINT:

src/highdicom/base.py

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ def __init__(
6060
device_serial_number: str | None = None,
6161
institution_name: str | None = None,
6262
institutional_department_name: str | None = None,
63+
content_date: str | datetime.date | None = None,
64+
content_time: str | datetime.time | None = None,
65+
series_date: str | datetime.date | None = None,
66+
series_time: str | datetime.time | None = None,
6367
):
6468
"""
6569
Parameters
@@ -121,6 +125,18 @@ def __init__(
121125
institutional_department_name: Union[str, None], optional
122126
Name of the department of the person or device that creates the
123127
SR document instance.
128+
content_date: str | datetime.date | None, optional
129+
Date the content of this instance was created. If not specified,
130+
the current date will be used.
131+
content_time: str | datetime.time | None, optional
132+
Time the content of this instance was created. If not specified,
133+
the current time will be used.
134+
series_date: str | datetime.date | None, optional
135+
Date the series was started. This should be the same for all
136+
instances in a series.
137+
series_time: str | datetime.time | None, optional
138+
Time the series was started. This should be the same for all
139+
instances in a series.
124140
125141
Note
126142
----
@@ -207,7 +223,11 @@ def __init__(
207223
_check_long_string(device_serial_number)
208224
self.DeviceSerialNumber = device_serial_number
209225
if software_versions is not None:
210-
_check_long_string(software_versions)
226+
if not isinstance(software_versions, str):
227+
for v in software_versions:
228+
_check_long_string(v)
229+
else:
230+
_check_long_string(software_versions)
211231
self.SoftwareVersions = software_versions
212232
if institution_name is not None:
213233
_check_long_string(institution_name)
@@ -227,11 +247,59 @@ def __init__(
227247
)
228248
self.InstanceNumber = instance_number
229249

250+
now = datetime.datetime.now()
251+
self.InstanceCreationDate = DA(now.date())
252+
self.InstanceCreationTime = TM(now.time())
253+
230254
# Content Date and Content Time are not present in all IODs
231255
if is_attribute_in_iod('ContentDate', sop_class_uid):
232-
self.ContentDate = DA(datetime.datetime.now().date())
256+
if content_date is None:
257+
if content_time is not None:
258+
raise TypeError(
259+
"'content_time' may not be specified without "
260+
"'content_date'."
261+
)
262+
content_date = now.date()
263+
content_date = DA(content_date)
264+
self.ContentDate = content_date
265+
elif content_date is not None:
266+
raise TypeError(
267+
f"'content_date' should not be specified for SOP Class UID: "
268+
f"{sop_class_uid}"
269+
)
233270
if is_attribute_in_iod('ContentTime', sop_class_uid):
234-
self.ContentTime = TM(datetime.datetime.now().time())
271+
if content_time is None:
272+
content_time = now.time()
273+
content_time = TM(content_time)
274+
self.ContentTime = content_time
275+
elif content_time is not None:
276+
raise TypeError(
277+
f"'content_time' should not be specified for SOP Class UID: "
278+
f"{sop_class_uid}"
279+
)
280+
281+
if series_date is not None:
282+
series_date = DA(series_date)
283+
if content_date is not None:
284+
if series_date > content_date:
285+
raise ValueError(
286+
"'series_date' must not be later than 'content_date'."
287+
)
288+
self.SeriesDate = series_date
289+
if series_time is not None:
290+
series_time = TM(series_time)
291+
if series_date is None:
292+
raise TypeError(
293+
"'series_time' may not be specified without "
294+
"'series_date'."
295+
)
296+
if content_time is not None:
297+
if series_time > content_time:
298+
raise ValueError(
299+
"'series_time' must not be later than content time."
300+
)
301+
self.SeriesTime = series_time
302+
235303
if content_qualification is not None:
236304
content_qualification = ContentQualificationValues(
237305
content_qualification

0 commit comments

Comments
 (0)