Pixel Data Encoder Plugins

Note

This guide is intended for advanced users who need support for something not provided by the existing encoder plugins.

Pixel Data encoding in pydicom uses an Encoder instance for the specific Transfer Syntax as a manager for plugins that perform the encoding work. This guide covers the requirements for those plugins and how to add them to pydicom. For a more general introduction to compression in pydicom see the tutorial instead.

Plugin Requirements

Each available pixel data encoder in pydicom corresponds directly to a single DICOM Transfer Syntax UID, and is intended to provide a mechanism for converting raw unencoded source data to meet the requirements of that transfer syntax. In order to do so, each encoder has at least one encoding plugin which performs the actual conversion.

An encoding plugin must implement three objects within the same module:

  • A function that performs the encoding with the following function signature:

    def foo(src: bytes, runner: EncodeRunner) -> bytes | bytearray:
    

    Where:

    • src is the raw uncompressed data to be encoded as bytes. When the data in src represents multi-byte values (such as 16-bit pixels), then src will use little-endian byte ordering by default. Support for big-endian byte ordering by the encoding function is completely optional.

      The data in src will be sized as:

      • 1 byte per sample for 0 < Bits Stored <= 8

      • 2 bytes per sample for 8 < Bits Stored <= 16

      • 4 bytes per sample for 16 < Bits Stored <= 32

      • 8 bytes per sample for 32 < Bits Stored <= 64

    • runner is an EncodeRunner instance that manages the encoding process and has access to the encoding options, either directly through the class properties or indirectly with the get_option() method.

      • 'transfer_syntax_uid': UID - the intended Transfer Syntax UID of the encoded data.

      • 'byteorder': str - the byte ordering used by src, '<' for little-endian (the default), '>' for big-endian.

      • 'rows': int - the number of rows of pixels in the src.

      • 'columns': int - the number of columns of pixels in the src.

      • 'samples_per_pixel': int - the number of samples used per pixel, e.g. 1 for grayscale images or 3 for RGB.

      • 'number_of_frames': int - the number of image frames contained in src

      • 'bits_allocated': int - the number of bits used to contain each pixel in src, should be a multiple of 8.

      • 'bits_stored': int - the number of bits actually used by each pixel in src, e.g. 12-bit pixel data (range 0 to 4095) will be contained by 16-bits (range 0 to 65535).

      • 'pixel_representation': int - the type of data in src, 0 for unsigned integers, 1 for 2’s complement (signed) integers.

      • 'photometric_interpretation': str - the intended color space of the encoded data, such as 'YBR_FULL'

      If your encoder needs to signal that one of the encoding option values needs to be modified then this can be done with the set_option() method. This should only be done after successfully encoding the frame, as if the encoding fails changing the option value may cause issues with other encoding plugins that may also attempt to encode the same frame. It’s also important to be aware that any changes you make will also affect following frames (if any).

    At a minimum the encoding function must support the encoding of little-endian byte ordered data and should return the encoded data in a format meeting the requirements of the corresponding Transfer Syntax UID as bytes or bytearray.

  • A function named is_available with the following signature:

    def is_available(uid: pydicom.uid.UID) -> bool:
    

    Where uid is the Transfer Syntax UID for the corresponding encoder as a UID. If the plugin supports the uid and has its dependencies met then it should return True, otherwise it should return False.

  • A dict named ENCODER_DEPENDENCIES with the type Dict[pydicom.uid.UID, Tuple[str, ...], such as:

    from pydicom.uid import RLELossless, JPEG2000
    
    ENCODER_DEPENDENCIES = {
        RLELossless: ('numpy', 'pillow', 'imagecodecs'),
        JPEG2000: ('numpy', 'gdcm'),
    }
    

    This will be used to provide the user with a list of missing dependencies required by the plugin.

An example of the requirements of a plugin is available here.

Adding Plugins to an Encoder

Additional plugins can be added to an existing encoder with the add_plugin() method, which takes the a unique str plugin_label, and a tuple of ('the import path to the encoder function's module', 'encoder function name'). For example, if you’d import your encoder function my_encoder_func with from my_package.encoders import my_encoder_func, then you’d do the following:

from pydicom.pixels.encoders import RLELosslessEncoder

RLELosslessEncoder.add_plugin(
    'my_encoder',  # the plugin's label
    ('my_package.encoders', 'my_encoder_func')  # the import paths
)

The my_package.encoders module must contain the encoding function and the ENCODER_DEPENDENCIES and is_available objects.