# SPDX-License-Identifier: Apache-2.0
# Copyright (C) 2020 ifm electronic gmbh
#
# THE PROGRAM IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND.
#
"""
This is the image data type used in the example files. The image type is selected
for simplicty and for minimal external dependencies. In the C++ part of the API, the
the ImageHeader ctypes structure is mapped to a corresponding C structure.
nexxT itself only knows about QByteArray's for data transport, the contents must
be defined by the user.
"""
import ctypes as ct
import numpy as np
from nexxT.Qt.QtCore import QByteArray
# The supported image formats and the mapping to the number of channels and the numpy type.
# Note that QT's QImage natively only supports intensity_u8, intensity_u16 and rgb_u8 images.
ImageFormats = {
"intensity_u8": (1, np.uint8), # 1 channel, uint8_t
"intensity_u16": (1, np.uint16), # 1 channel, uint16_t
"intensity_u32": (1, np.uint32), # 1 channel, uint32_t
"intensity_f32": (1, np.float32), # 1 channel, float32_t
"intensity_f64": (1, np.float64), # 1 channel, float64_t
"rgb_u8": (3, np.uint8), # 3 channels RGB, uint8_t
"rgb_u16": (3, np.uint16), # 3 channels RGB, uint16_t
"rgb_u32": (3, np.uint32), # 3 channels RGB, uint32_t
"rgb_f32": (3, np.float32), # 3 channels RGB, float32_t
"rgb_f64": (3, np.float64), # 3 channels RGB, float64_t
# ...
}
[docs]
def byteArrayToNumpy(qByteArray):
"""
Interpret the input instance as an image and convert that to a numpy array. If the alignment is ok, then this
operation is a zero-copy operation, otherwise one copy is made.
:param qByteArray: a QByteArray instance
:return: a numpy instance
"""
# efficient zero-copy cast to a python memoryview instance
mv = memoryview(qByteArray)
# interpret the ImageHeader structure from this buffer (zero-copy)
hdr = ImageHeader.from_buffer(mv)
# convert the format bytes instance to a string
fmt = hdr.format.decode()
# sanity check
if not fmt in ImageFormats:
raise RuntimeError(f"Unknown image format {fmt}")
# get number of channels and the numpy dtype of the target array
numChannels, dtype = ImageFormats[hdr.format.decode()]
# calculate the number of bytes per pixel
bpp = dtype().nbytes*numChannels
if hdr.lineInc % bpp != 0:
# there is a non-convertable padding at the end of the lines, so we have to fix that
# first interpret the image data as a numpy uint8 buffer (zero-copy)
tmp = np.frombuffer(mv, dtype=np.uint8, offset=ct.sizeof(hdr))
# reshape to 2D with with lineInc as width
tmp = np.reshape(tmp, (-1, hdr.lineInc))
# crop the non-aligned padding bytes from the image,
tmp = tmp[:, :(hdr.lineInc//bpp)*bpp]
# we have to create a copy here (the frombuffer call does not work on memoryview(tmp))
mv = bytes(tmp)
# create the target array
res = np.frombuffer(mv, dtype=dtype, offset=ct.sizeof(hdr))
# reshape to requested dimenstions
return np.reshape(res, (-1, max(1,hdr.lineInc//bpp), numChannels))
[docs]
def numpyToByteArray(img):
"""
Convert a numpy image to the corresponding QByteArray (and make a copy).
:param img: a numpy array instance with 2 or 3 dimensions
:return: a QByteArray instance
"""
# make sure that img is a contiguous array
img = np.ascontiguousarray(img)
# allocate the result
res = QByteArray(img.nbytes + ct.sizeof(ImageHeader), 0)
# create a memory view
mv = memoryview(res)
# map the header into this view
hdr = ImageHeader.from_buffer(mv)
hdr.width = img.shape[1]
hdr.height = img.shape[0]
hdr.lineInc = img[0, ...].nbytes
# select the format
if img.dtype is np.dtype(np.uint8):
hdr.format = b"intensity_u8" if len(img.shape) < 3 else b"rgb_u8"
if img.dtype is np.dtype(np.uint16):
hdr.format = b"intensity_u16" if len(img.shape) < 3 else b"rgb_u16"
if img.dtype is np.dtype(np.uint32):
hdr.format = b"intensity_u32" if len(img.shape) < 3 else b"rgb_u32"
if img.dtype is np.dtype(np.float32):
hdr.format = b"intensity_f32" if len(img.shape) < 3 else b"rgb_f32"
if img.dtype is np.dtype(np.float64):
hdr.format = b"intensity_f64" if len(img.shape) < 3 else b"rgb_f64"
# assert reasonable shape
assert len(img.shape) == 2 or (len(img.shape) == 3 and img.shape[2] == 3)
# map the image data into the view
tmp = np.frombuffer(mv, img.dtype, offset=ct.sizeof(hdr))
# assign the pixels
tmp[...] = img.flatten()
return res