Skip to content

Commit a4dfcb5

Browse files
authored
Merge pull request #323 from ImagingDataCommons/v0.25.0dev
v0.25.0dev
2 parents 4121bc1 + e2bce7c commit a4dfcb5

9 files changed

Lines changed: 565 additions & 21 deletions

File tree

docs/image.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,17 @@ processing unnecessary frames. If you know that you are likely to access frames
124124
multiple times, you can force caching of the stored values by accessing the
125125
``.pixel_array`` property (inherited from ``pydicom.Dataset``).
126126

127+
Additionally, there are two methods for accessing multiple frames at a time:
128+
129+
* :meth:`highdicom.Image.get_stored_frames()`: Returns a stack of multiple
130+
stored frames. The first parameter is a list (or other iterable) of frame
131+
numbers. If omitted, all frames are returned in the order they are stored in
132+
the image.
133+
* :meth:`highdicom.Image.get_frames()`: Returns a stack of multiple
134+
frames with pixel transforms applied. The first parameter is a list (or other
135+
iterable) of frame numbers. If omitted, all frames are returned in the order
136+
they are stored in the image.
137+
127138
Accessing Total Pixel Matrices
128139
------------------------------
129140

docs/pixel_transforms.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ The :class:`highdicom.Image` class has several methods that return frames or
5050
arrangements of frames from a DICOM image:
5151

5252
* :meth:`highdicom.Image.get_frame()`
53+
* :meth:`highdicom.Image.get_frames()`
5354
* :meth:`highdicom.Image.get_volume()`
5455
* :meth:`highdicom.Image.get_total_pixel_matrix()`
5556

docs/seg.rst

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -345,8 +345,6 @@ segmentation type.
345345

346346
.. code-block:: python
347347
348-
import numpy as np
349-
350348
from pydicom.sr.codedict import codes
351349
from pydicom.data import get_testdata_file
352350
@@ -355,13 +353,8 @@ segmentation type.
355353
# Load an enhanced (multiframe) CT image
356354
source_image = hd.imread(get_testdata_file('eCT_Supplemental.dcm'))
357355
358-
# Stack all the frames of the image
359-
image_array = np.stack(
360-
[
361-
source_image.get_frame(i + 1)
362-
for i in range(source_image.number_of_frames)
363-
]
364-
)
356+
# Get a stack of all the frames of the image
357+
image_array = source_image.get_frames()
365358
366359
# Create a segmentation by thresholding the CT image at 0 HU
367360
mask = image_array > 0

docs/volume.rst

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,189 @@ Patient orientations may be represented as strings or as tuples of the
368368
Channels
369369
--------
370370

371+
In addition to the three spatial dimensions, a volume may have further
372+
non-spatial dimensions that are referred to as "channels". Channel dimensions
373+
are stacked after the spatial dimensions in the volume's pixel array. The
374+
meaning of each channel is explicitly described in the volume. Common uses for
375+
channels include RGB channels in color images, optical paths in microscopy
376+
images, or contrast phases in radiology images.
377+
378+
The :class:`highdicom.ChannelDescriptor` class is used to describe the meaning
379+
of a single channel dimension. Where possible, it is recommended to use DICOM
380+
attributes to describe channels. A DICOM keyword or the corresponding tag value
381+
may be passed to the :class:`highdicom.ChannelDescriptor` constructor.
382+
383+
When using a DICOM attribute, each channel of the volume is associated with a
384+
particular value for that attribute. For example, if the descriptor uses the
385+
"OpticalPathIdentifier" attribute, each channel will be associated with a
386+
string. Alternatively if an integer-valued attribute like "SegmentNumber" is
387+
used, each channel will be associated with an integer. We refer to this type as
388+
the descriptor's "value type".
389+
390+
This code snippet creates channel descriptors using some DICOM attribute, and
391+
checks the corresponding value types:
392+
393+
.. code-block:: python
394+
395+
import highdicom as hd
396+
397+
398+
# Channel descriptor using the "OpticalPathIdentifier"
399+
optical_path_descriptor = hd.ChannelDescriptor('OpticalPathIdentifier')
400+
401+
# Using the hexcode for the attribute is equivalent
402+
optical_path_descriptor = hd.ChannelDescriptor(0x0048_0106)
403+
404+
# Channel descriptor using the "DiffusionBValue"
405+
bvalue_descriptor = hd.ChannelDescriptor('DiffusionBValue')
406+
407+
# Check that the value types are as expected
408+
print(optical_path_descriptor.value_type)
409+
# <class 'str'>
410+
411+
print(bvalue_descriptor.value_type)
412+
# <class 'float'>
413+
414+
Alternatively, it is possible to define custom identifiers that do not use a
415+
DICOM attribute. In this case, you must specify the value type yourself. The
416+
value type must be either ``int``, ``str``, or ``float`` (or a sub-type of one
417+
of these types), or an enumerated type derived from the Python standard library
418+
``enum.Enum``.
419+
420+
.. code-block:: python
421+
422+
from enum import Enum
423+
import highdicom as hd
424+
425+
# A custom descriptor using integer values
426+
custom_int_descriptor = hd.ChannelDescriptor(
427+
'my_int_descriptor',
428+
is_custom=True,
429+
value_type=int,
430+
)
431+
432+
# A custom descriptor using an enumerated type
433+
class MyEnum(Enum):
434+
VALUE1 = "VALUE1"
435+
VALUE2 = "VALUE2"
436+
437+
custom_enum_descriptor = hd.ChannelDescriptor(
438+
'my_enum_descriptor',
439+
is_custom=True,
440+
value_type=MyEnum,
441+
)
442+
443+
One very common channel descriptor that does not correspond to a DICOM
444+
attribute is RGB color channels. The enum :class:`highdicom.RGBColorChannels`
445+
is used as the value type for volumes with color channels, and the descriptor
446+
for this channel is provided as a constant in
447+
``highdicom.RGB_COLOR_CHANNEL_DESCRIPTOR``.
448+
449+
To create a volume with channels, you must provide a dictionary that contains,
450+
for each channel dimension, the channel descriptor and the values of each
451+
channel along that dimension:
452+
453+
.. code-block:: python
454+
455+
import numpy as np
456+
import highdicom as hd
457+
458+
# Array with three spatial dimensions plus 3 color channels and 4 optical
459+
# paths
460+
array = np.random.randint(0, 10, size=(1, 50, 50, 3, 4))
461+
462+
# Names of the 4 optical paths
463+
path_names = ['path1', 'path2', 'path3', 'path4']
464+
465+
vol = hd.Volume.from_components(
466+
direction=np.eye(3),
467+
center_position=[98.1, 78.4, 23.1],
468+
spacing=[2.0, 0.5, 0.5],
469+
coordinate_system="SLIDE",
470+
array=array,
471+
channels={
472+
hd.RGB_COLOR_CHANNEL_DESCRIPTOR: ['R', 'G', 'B'],
473+
'OpticalPathIdentifier': path_names
474+
},
475+
)
476+
477+
# The total shape of the volume includes the channel dimensions
478+
assert vol.shape == (1, 50, 50, 3, 4)
479+
480+
# But the spatial shape excludes them
481+
assert vol.spatial_shape == (1, 50, 50)
482+
483+
# The channel shape includes only the channel dimensions, not the spatial
484+
# dimensions
485+
assert vol.channel_shape == (3, 4)
486+
assert vol.number_of_channel_dimensions == 2
487+
488+
# You can access the descriptors like this
489+
assert vol.channel_descriptors == (
490+
hd.RGB_COLOR_CHANNEL_DESCRIPTOR,
491+
hd.ChannelDescriptor('OpticalPathIdentifier'),
492+
)
493+
494+
The order of the items in the dictionary is significant and must match the
495+
order of the channel dimensions in the array.
496+
497+
For most purposes, a volume with channels can be treated just like one without.
498+
All spatial operations (including indexing) only alter the array along the
499+
spatial dimensions and leave the channel dimensions unchanged. A separate set
500+
of methods are used to alter the channel dimensions:
501+
502+
* :meth:`highdicom.Volume.get_channel()`: Get a new volume containing just one
503+
channel of the original volume for a given channel value.
504+
* :meth:`highdicom.Volume.get_channel_values()`: Get the channel values for a
505+
given channel dimension.
506+
* :meth:`highdicom.Volume.permute_channel_axes()`: Permute the channels
507+
dimensions to a given order specified by the descriptors.
508+
* :meth:`highdicom.Volume.permute_channel_axes_by_index()`: Permute the channel
509+
dimensions to a given order specified by the channel dimension index.
510+
511+
This snippet, using the same volume as above, demonstrates how to use these
512+
methods:
513+
514+
.. code-block:: python
515+
516+
import numpy as np
517+
import highdicom as hd
518+
519+
# Array with three spatial dimensions plus 3 color channels and 4 optical
520+
# paths
521+
array = np.random.randint(0, 10, size=(1, 50, 50, 3, 4))
522+
523+
# Names of the 4 optical paths
524+
path_names = ['path1', 'path2', 'path3', 'path4']
525+
526+
vol = hd.Volume.from_components(
527+
direction=np.eye(3),
528+
center_position=[98.1, 78.4, 23.1],
529+
spacing=[2.0, 0.5, 0.5],
530+
coordinate_system="SLIDE",
531+
array=array,
532+
channels={
533+
hd.RGB_COLOR_CHANNEL_DESCRIPTOR: ['R', 'G', 'B'],
534+
'OpticalPathIdentifier': path_names
535+
},
536+
)
537+
538+
assert (
539+
vol.get_channel_values('OpticalPathIdentifier') ==
540+
path_names
541+
)
542+
543+
# Get a new volume containing just optical path 'path2'
544+
path_2_vol = vol.get_channel(OpticalPathIdentifier='path2')
545+
546+
# Swap the two channel axes by descriptor
547+
permuted_vol = vol.permute_channel_axes(
548+
['OpticalPathIdentifier', 'RGBColorChannel']
549+
)
550+
551+
# Swap the two channel axes by index
552+
permuted_vol = vol.permute_channel_axes_by_index([1, 0])
553+
371554
Full Example
372555
------------
373556

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.24.0"
7+
version = "0.25.0"
88
description = "High-level DICOM abstractions."
99
readme = "README.md"
1010
requires-python = ">=3.10"

0 commit comments

Comments
 (0)