Skip to content

Commit

Permalink
Merge pull request #323 from ImagingDataCommons/v0.25.0dev
Browse files Browse the repository at this point in the history
v0.25.0dev
  • Loading branch information
CPBridge authored Feb 9, 2025
2 parents 4121bc1 + e2bce7c commit a4dfcb5
Show file tree
Hide file tree
Showing 9 changed files with 565 additions and 21 deletions.
11 changes: 11 additions & 0 deletions docs/image.rst
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,17 @@ processing unnecessary frames. If you know that you are likely to access frames
multiple times, you can force caching of the stored values by accessing the
``.pixel_array`` property (inherited from ``pydicom.Dataset``).

Additionally, there are two methods for accessing multiple frames at a time:

* :meth:`highdicom.Image.get_stored_frames()`: Returns a stack of multiple
stored frames. The first parameter is a list (or other iterable) of frame
numbers. If omitted, all frames are returned in the order they are stored in
the image.
* :meth:`highdicom.Image.get_frames()`: Returns a stack of multiple
frames with pixel transforms applied. The first parameter is a list (or other
iterable) of frame numbers. If omitted, all frames are returned in the order
they are stored in the image.

Accessing Total Pixel Matrices
------------------------------

Expand Down
1 change: 1 addition & 0 deletions docs/pixel_transforms.rst
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ The :class:`highdicom.Image` class has several methods that return frames or
arrangements of frames from a DICOM image:

* :meth:`highdicom.Image.get_frame()`
* :meth:`highdicom.Image.get_frames()`
* :meth:`highdicom.Image.get_volume()`
* :meth:`highdicom.Image.get_total_pixel_matrix()`

Expand Down
11 changes: 2 additions & 9 deletions docs/seg.rst
Original file line number Diff line number Diff line change
Expand Up @@ -345,8 +345,6 @@ segmentation type.

.. code-block:: python
import numpy as np
from pydicom.sr.codedict import codes
from pydicom.data import get_testdata_file
Expand All @@ -355,13 +353,8 @@ segmentation type.
# Load an enhanced (multiframe) CT image
source_image = hd.imread(get_testdata_file('eCT_Supplemental.dcm'))
# Stack all the frames of the image
image_array = np.stack(
[
source_image.get_frame(i + 1)
for i in range(source_image.number_of_frames)
]
)
# Get a stack of all the frames of the image
image_array = source_image.get_frames()
# Create a segmentation by thresholding the CT image at 0 HU
mask = image_array > 0
Expand Down
183 changes: 183 additions & 0 deletions docs/volume.rst
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,189 @@ Patient orientations may be represented as strings or as tuples of the
Channels
--------

In addition to the three spatial dimensions, a volume may have further
non-spatial dimensions that are referred to as "channels". Channel dimensions
are stacked after the spatial dimensions in the volume's pixel array. The
meaning of each channel is explicitly described in the volume. Common uses for
channels include RGB channels in color images, optical paths in microscopy
images, or contrast phases in radiology images.

The :class:`highdicom.ChannelDescriptor` class is used to describe the meaning
of a single channel dimension. Where possible, it is recommended to use DICOM
attributes to describe channels. A DICOM keyword or the corresponding tag value
may be passed to the :class:`highdicom.ChannelDescriptor` constructor.

When using a DICOM attribute, each channel of the volume is associated with a
particular value for that attribute. For example, if the descriptor uses the
"OpticalPathIdentifier" attribute, each channel will be associated with a
string. Alternatively if an integer-valued attribute like "SegmentNumber" is
used, each channel will be associated with an integer. We refer to this type as
the descriptor's "value type".

This code snippet creates channel descriptors using some DICOM attribute, and
checks the corresponding value types:

.. code-block:: python
import highdicom as hd
# Channel descriptor using the "OpticalPathIdentifier"
optical_path_descriptor = hd.ChannelDescriptor('OpticalPathIdentifier')
# Using the hexcode for the attribute is equivalent
optical_path_descriptor = hd.ChannelDescriptor(0x0048_0106)
# Channel descriptor using the "DiffusionBValue"
bvalue_descriptor = hd.ChannelDescriptor('DiffusionBValue')
# Check that the value types are as expected
print(optical_path_descriptor.value_type)
# <class 'str'>
print(bvalue_descriptor.value_type)
# <class 'float'>
Alternatively, it is possible to define custom identifiers that do not use a
DICOM attribute. In this case, you must specify the value type yourself. The
value type must be either ``int``, ``str``, or ``float`` (or a sub-type of one
of these types), or an enumerated type derived from the Python standard library
``enum.Enum``.

.. code-block:: python
from enum import Enum
import highdicom as hd
# A custom descriptor using integer values
custom_int_descriptor = hd.ChannelDescriptor(
'my_int_descriptor',
is_custom=True,
value_type=int,
)
# A custom descriptor using an enumerated type
class MyEnum(Enum):
VALUE1 = "VALUE1"
VALUE2 = "VALUE2"
custom_enum_descriptor = hd.ChannelDescriptor(
'my_enum_descriptor',
is_custom=True,
value_type=MyEnum,
)
One very common channel descriptor that does not correspond to a DICOM
attribute is RGB color channels. The enum :class:`highdicom.RGBColorChannels`
is used as the value type for volumes with color channels, and the descriptor
for this channel is provided as a constant in
``highdicom.RGB_COLOR_CHANNEL_DESCRIPTOR``.

To create a volume with channels, you must provide a dictionary that contains,
for each channel dimension, the channel descriptor and the values of each
channel along that dimension:

.. code-block:: python
import numpy as np
import highdicom as hd
# Array with three spatial dimensions plus 3 color channels and 4 optical
# paths
array = np.random.randint(0, 10, size=(1, 50, 50, 3, 4))
# Names of the 4 optical paths
path_names = ['path1', 'path2', 'path3', 'path4']
vol = hd.Volume.from_components(
direction=np.eye(3),
center_position=[98.1, 78.4, 23.1],
spacing=[2.0, 0.5, 0.5],
coordinate_system="SLIDE",
array=array,
channels={
hd.RGB_COLOR_CHANNEL_DESCRIPTOR: ['R', 'G', 'B'],
'OpticalPathIdentifier': path_names
},
)
# The total shape of the volume includes the channel dimensions
assert vol.shape == (1, 50, 50, 3, 4)
# But the spatial shape excludes them
assert vol.spatial_shape == (1, 50, 50)
# The channel shape includes only the channel dimensions, not the spatial
# dimensions
assert vol.channel_shape == (3, 4)
assert vol.number_of_channel_dimensions == 2
# You can access the descriptors like this
assert vol.channel_descriptors == (
hd.RGB_COLOR_CHANNEL_DESCRIPTOR,
hd.ChannelDescriptor('OpticalPathIdentifier'),
)
The order of the items in the dictionary is significant and must match the
order of the channel dimensions in the array.

For most purposes, a volume with channels can be treated just like one without.
All spatial operations (including indexing) only alter the array along the
spatial dimensions and leave the channel dimensions unchanged. A separate set
of methods are used to alter the channel dimensions:

* :meth:`highdicom.Volume.get_channel()`: Get a new volume containing just one
channel of the original volume for a given channel value.
* :meth:`highdicom.Volume.get_channel_values()`: Get the channel values for a
given channel dimension.
* :meth:`highdicom.Volume.permute_channel_axes()`: Permute the channels
dimensions to a given order specified by the descriptors.
* :meth:`highdicom.Volume.permute_channel_axes_by_index()`: Permute the channel
dimensions to a given order specified by the channel dimension index.

This snippet, using the same volume as above, demonstrates how to use these
methods:

.. code-block:: python
import numpy as np
import highdicom as hd
# Array with three spatial dimensions plus 3 color channels and 4 optical
# paths
array = np.random.randint(0, 10, size=(1, 50, 50, 3, 4))
# Names of the 4 optical paths
path_names = ['path1', 'path2', 'path3', 'path4']
vol = hd.Volume.from_components(
direction=np.eye(3),
center_position=[98.1, 78.4, 23.1],
spacing=[2.0, 0.5, 0.5],
coordinate_system="SLIDE",
array=array,
channels={
hd.RGB_COLOR_CHANNEL_DESCRIPTOR: ['R', 'G', 'B'],
'OpticalPathIdentifier': path_names
},
)
assert (
vol.get_channel_values('OpticalPathIdentifier') ==
path_names
)
# Get a new volume containing just optical path 'path2'
path_2_vol = vol.get_channel(OpticalPathIdentifier='path2')
# Swap the two channel axes by descriptor
permuted_vol = vol.permute_channel_axes(
['OpticalPathIdentifier', 'RGBColorChannel']
)
# Swap the two channel axes by index
permuted_vol = vol.permute_channel_axes_by_index([1, 0])
Full Example
------------

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.24.0"
version = "0.25.0"
description = "High-level DICOM abstractions."
readme = "README.md"
requires-python = ">=3.10"
Expand Down
Loading

0 comments on commit a4dfcb5

Please sign in to comment.