Source code for herosdevices.hardware.ids.pixelformat

"""Various methods to unpack pixel formats supported by the IDS cameras."""

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from ids_peak_ipl.ids_peak_ipl import Image


import numpy as np

try:
    from ids_peak_common.datatypes.pixelformat import PixelFormat

    _ids_peak_available = True
except ModuleNotFoundError:
    _ids_peak_available = False


def _unpack_mono12g24(buffer: bytearray, shape: tuple | None = None) -> np.ndarray:
    """Unpack the MONO12G24IDS format used by some IDS cameras.

    The format packs 2 x 12bit into 3 bytes as shown in
    https://www.1stvision.com/cameras/IDS/IDS-manuals/en/basics-monochrome-pixel-formats.html

    In short for the two consecutive pixels A and B with bits A0..A11 and B0..B11 (A0 and B0 being MSB) the packing is

    | A0..A7 | B0..B7 | B8..B11 A8..A11 |
    | byte 0 | byte 1 |      byte 2     |

    The method returns a uint16 ndarray in the given shape.

    Args:
        buffer: the buffer/memoryview holding the packed data
        shape: shape of the numpy array to return. If none is given a flat array is returned.
    """
    np_buffer = np.frombuffer(buffer, dtype=np.uint8).astype(np.uint16)

    img = np.empty(int(len(np_buffer) / 3) * 2, dtype=np.uint16)
    img[0::2] = (np_buffer[0::3] << 4) + (np_buffer[2::3] & 0xF)
    img[1::2] = (np_buffer[1::3] << 4) + ((np_buffer[2::3] & 0xF0) >> 4)

    shape = shape if shape is not None else (len(img),)

    return img.reshape(shape)


def _unpack_mono10g40(buffer: bytearray, shape: tuple | None = None) -> np.ndarray:
    """Unpack the MONO10G40IDS format used by some IDS cameras.

    The format packs 4 x 10bit into 5 bytes as shown in
    https://www.1stvision.com/cameras/IDS/IDS-manuals/en/basics-monochrome-pixel-formats.html

    In short for the four consecutive pixels A to D with bits A0..A9 to D0..D9 (A0 and D0 being MSB) the packing is

    | A0..A7 | B0..B7 | C0..C7 | D0..D7 | D8..D9 C8..C9 B8..B9 A8..A9 |
    | byte 0 | byte 1 | byte 2 | byte 3 |            byte 4           |

    The method returns a uint16 ndarray in the given shape.

    Args:
        buffer: the buffer/memoryview holding the packed data
        shape: shape of the numpy array to return. If none is given a flat array is returned.
    """
    np_buffer = np.frombuffer(buffer, dtype=np.uint8).astype(np.uint16)

    img = np.empty(int(len(np_buffer) / 5) * 4, dtype=np.uint16)
    img[0::4] = (np_buffer[0::5] << 2) + (np_buffer[4::5] & 0x3)
    img[1::4] = (np_buffer[1::5] << 2) + ((np_buffer[4::5] >> 2) & 0x3)
    img[2::4] = (np_buffer[2::5] << 2) + ((np_buffer[4::5] >> 4) & 0x3)
    img[3::4] = (np_buffer[3::5] << 2) + (np_buffer[4::5] >> 6)

    shape = shape if shape is not None else (len(img),)

    return img.reshape(shape)


def _unpack_mono12p(buffer: bytearray, shape: tuple | None = None) -> np.ndarray:
    """Unpack the MONO12P format.

    The format packs 2 x 12bit into 3 bytes as shown in
    https://www.1stvision.com/cameras/IDS/IDS-manuals/en/basics-monochrome-pixel-formats.html

    The bytes hold a continuous little-endian bit stream of the pixel values, i.e. for the
    two consecutive pixels A and B with bits A0..A11 and B0..B11 (A0 and B0 being MSB) the packing is

    | A4..A11 | B8..B11 A0..A3 | B0..B7 |
    | byte 0  |     byte 1     | byte 2 |

    The method returns a uint16 ndarray in the given shape.

    Args:
        buffer: the buffer/memoryview holding the packed data
        shape: shape of the numpy array to return. If none is given a flat array is returned.
    """
    np_buffer = np.frombuffer(buffer, dtype=np.uint8).astype(np.uint16)

    img = np.empty(int(len(np_buffer) / 3) * 2, dtype=np.uint16)
    img[0::2] = np_buffer[0::3] + ((np_buffer[1::3] & 0xF) << 8)
    img[1::2] = (np_buffer[2::3] << 4) + (np_buffer[1::3] >> 4)

    shape = shape if shape is not None else (len(img),)

    return img.reshape(shape)


def _unpack_mono10p(buffer: bytearray, shape: tuple | None = None) -> np.ndarray:
    """Unpack the MONO10P format.

    The format packs 4 x 10bit into 5 bytes as shown in
    https://www.1stvision.com/cameras/IDS/IDS-manuals/en/basics-monochrome-pixel-formats.html

    The bytes hold a continuous little-endian bit stream of the pixel values, i.e. for the
    four consecutive pixels A to D with bits A0..A9 to D0..D9 (A0 and D0 being MSB) the packing is

    | A2..A9 | B4..B9 A0..A1 | C6..C9 B0..B3 | D8..D9 C0..C5 | D0..D7 |
    | byte 0 |    byte 1     |    byte 2     |    byte 3     | byte 4 |

    The method returns a uint16 ndarray in the given shape.

    Args:
        buffer: the buffer/memoryview holding the packed data
        shape: shape of the numpy array to return. If none is given a flat array is returned.
    """
    np_buffer = np.frombuffer(buffer, dtype=np.uint8).astype(np.uint16)

    img = np.empty(int(len(np_buffer) / 5) * 4, dtype=np.uint16)
    img[0::4] = np_buffer[0::5] + ((np_buffer[1::5] & 0x3) << 8)
    img[1::4] = (np_buffer[1::5] >> 2) + ((np_buffer[2::5] & 0xF) << 6)
    img[2::4] = (np_buffer[2::5] >> 4) + ((np_buffer[3::5] & 0x3F) << 4)
    img[3::4] = (np_buffer[3::5] >> 6) + (np_buffer[4::5] << 2)

    shape = shape if shape is not None else (len(img),)

    return img.reshape(shape)


if _ids_peak_available:
    _unpackers = {
        PixelFormat.MONO_10_GROUPED_40_IDS: _unpack_mono10g40,
        PixelFormat.MONO_10_PACKED: _unpack_mono10p,
        PixelFormat.MONO_12_GROUPED_24_IDS: _unpack_mono12g24,
        PixelFormat.MONO_12_PACKED: _unpack_mono12p,
    }


def _convert_nonpacked(ipl_image: "Image") -> np.ndarray:
    """Convert images that are not in packed data format to ndarray.

    we rely on the `get_numpy_XX_XX()` methods in the ipl_image objects
    here to do the conversion.

    Args:
        ipl_image: image in a non-packed pixel format
    """
    pixel_format = PixelFormat(ipl_image.PixelFormat().PixelFormatName())

    # monochrome image format -> return 2D ndarray
    if pixel_format.is_single_channel:
        if pixel_format.allocated_bits_per_pixel == 8:  # noqa: PLR2004
            return ipl_image.get_numpy_2D()
        return ipl_image.get_numpy_2D_16()

    # color image format -> return 3D ndarray
    if pixel_format.allocated_bits_per_pixel == 8:  # noqa: PLR2004
        return ipl_image.get_numpy_3D()
    return ipl_image.get_numpy_3D_16()


[docs] def ipl_image_to_ndarray(ipl_image: "Image") -> np.ndarray: """Convert a IDS Peak image to a numpy array and perform pixel format conversion if necessary. Args: ipl_image: the IDS peak image object() """ pixel_format = PixelFormat(ipl_image.PixelFormat().PixelFormatName()) # special treatment for IDS packed pixel formats if pixel_format in _unpackers: img = _unpackers[pixel_format](bytearray(ipl_image.DataView()), (ipl_image.Height(), ipl_image.Width())) else: img = _convert_nonpacked(ipl_image) return img