@@ -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(0x 0048_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