Skip to content

Commit e2bce7c

Browse files
authored
Merge pull request #322 from ImagingDataCommons/volume_docs
Add channel section to volume user guide
2 parents 7c9c1c9 + 17a044f commit e2bce7c

1 file changed

Lines changed: 183 additions & 0 deletions

File tree

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

0 commit comments

Comments
 (0)