Bypass makeARGB in some cases (#1786)
* refactor out _ndarray_to_qimage()
* combine levels back with lut
* make use of Grayscale8, RGB888 and Indexed8 QImage formats
Grayscale8 and RGB888 images are those that are ready for display
without further processing.
* add Grayscale16
* apply the efflut early for uint16 mono/rgb, uint8 rgb
* ndarray indexing is faster than np.take
* handle uint16 rgb(a) with no levels same as levels=[0, 65535]
* add support for Format_RGBA64
* fix: support colormaps of shape (h, 1)
* check ImageItem uint8 and uint16 QImage formats
* uint16 mono with rgb lut -> RGBX8888
* got width and height swapped in array dimensions
* set ImageItem as row-major
* no need to form a 1d 32-bit lut for array indexing
you can index (y, x) into a lookup table of shape (nentry, 3) or
(nentry, 4) and get an output of shape (y, x, 3) or (y, x, 4)
* Revert "no need to form a 1d 32-bit lut for array indexing"
This reverts commit 45cf3100de
.
* distinguish between levels_lut and colors_lut
this allows uint16 images with user lut to be rendered as
Format_Indexed8
* uint8 (1-chan) images should always combine to efflut
this efflut will then be used for Indexed8 format color table.
previously, we would be taking a performance hit with doing a numpy
lookup with levels_lut.
* adapt benchmarks/makeARGB.py to renderImageItem.py
* restructure uint8 and uint16 codepaths
* normalize 1-chan images to ndim==2 earlier up
* refactor long code into functions
* bug: qimage may not be assigned
* fix: assign to self.qimage only if not None
* for uint16, do rescale rather than do levels_lut lookup
* cases 2,3 are already handled
i.e. no more using lut to do rescale of uint16 image data.
* rescale rgb images by computation, not by memory lookup
* setImage() does not take an output argument
* try to be cupy compatible
use "xp" instead of numpy module
* add numba to benchmarking
* fix: lut_big is dtype uint8 with more than 256 entries
* bug: applying colors_lut needs C-order
* support float with no nans
* fix: variable could be uninitialized
* add float32 format tests
* avoid explicitly forcing to C-contiguous
* cache effective lut only if combination took place
every one of the four branches now does its own return.
this makes it easier to follow.
* fix cupy benchmark : typo in renderQImage
* remove for loop of 1 iteration
* use float32 for floating point benchmark
* superceded by renderImageItem.py
* lint
* benchmark without lut conversion
* put the lut onto the substrate
* fix editor complaints
* handle lack of cupy
* leading underscores imply privacy
Co-authored-by: KIU Shueng Chuan <nixchuan@gmail.com>
This commit is contained in:
parent
9566e2ba36
commit
a91953e93d
@ -1,130 +0,0 @@
|
||||
import numpy as np
|
||||
|
||||
import pyqtgraph as pg
|
||||
from pyqtgraph.functions import makeARGB
|
||||
|
||||
try:
|
||||
import cupy as cp
|
||||
|
||||
pg.setConfigOption("useCupy", True)
|
||||
except ImportError:
|
||||
cp = None
|
||||
|
||||
|
||||
class _TimeSuite(object):
|
||||
def __init__(self):
|
||||
super(_TimeSuite, self).__init__()
|
||||
self.float_data = None
|
||||
self.uint8_data = None
|
||||
self.uint8_lut = None
|
||||
self.uint16_data = None
|
||||
self.uint16_lut = None
|
||||
self.output = None
|
||||
self.cupy_output = None
|
||||
|
||||
def setup(self):
|
||||
size = (self.size, self.size)
|
||||
self.float_data, self.uint16_data, self.uint8_data, self.uint16_lut, self.uint8_lut = self._create_data(
|
||||
size, np
|
||||
)
|
||||
self.output = np.zeros(size + (4,), dtype=np.ubyte)
|
||||
makeARGB(self.uint16_data["data"]) # prime the cpu
|
||||
if cp:
|
||||
self.cupy_output = cp.zeros(size + (4,), dtype=cp.ubyte)
|
||||
makeARGB(cp.asarray(self.uint16_data["data"])) # prime the gpu
|
||||
|
||||
@staticmethod
|
||||
def _create_data(size, xp):
|
||||
float_data = {
|
||||
"data": xp.random.normal(size=size),
|
||||
"levels": [-4.0, 4.0],
|
||||
}
|
||||
uint16_data = {
|
||||
"data": xp.random.randint(100, 4500, size=size).astype("uint16"),
|
||||
"levels": [250, 3000],
|
||||
}
|
||||
uint8_data = {
|
||||
"data": xp.random.randint(0, 255, size=size).astype("ubyte"),
|
||||
"levels": [20, 220],
|
||||
}
|
||||
c_map = xp.array([[-500.0, 255.0], [-255.0, 255.0], [0.0, 500.0]])
|
||||
uint8_lut = xp.zeros((256, 4), dtype="ubyte")
|
||||
for i in range(3):
|
||||
uint8_lut[:, i] = xp.clip(xp.linspace(c_map[i][0], c_map[i][1], 256), 0, 255)
|
||||
uint8_lut[:, 3] = 255
|
||||
uint16_lut = xp.zeros((2 ** 16, 4), dtype="ubyte")
|
||||
for i in range(3):
|
||||
uint16_lut[:, i] = xp.clip(xp.linspace(c_map[i][0], c_map[i][1], 2 ** 16), 0, 255)
|
||||
uint16_lut[:, 3] = 255
|
||||
return float_data, uint16_data, uint8_data, uint16_lut, uint8_lut
|
||||
|
||||
|
||||
def make_test(dtype, use_cupy, use_levels, lut_name, func_name):
|
||||
def time_test(self):
|
||||
data = getattr(self, dtype + "_data")
|
||||
levels = data["levels"] if use_levels else None
|
||||
lut = getattr(self, lut_name + "_lut", None) if lut_name is not None else None
|
||||
for _ in range(10):
|
||||
img_data = data["data"]
|
||||
output = self.output
|
||||
if use_cupy:
|
||||
img_data = cp.asarray(img_data)
|
||||
output = self.cupy_output
|
||||
makeARGB(
|
||||
img_data, lut=lut, levels=levels, output=output,
|
||||
)
|
||||
if use_cupy:
|
||||
output.get(out=self.output)
|
||||
|
||||
time_test.__name__ = func_name
|
||||
return time_test
|
||||
|
||||
|
||||
for cupy in [True, False]:
|
||||
if cupy and cp is None:
|
||||
continue
|
||||
for dtype in ["float", "uint16", "uint8"]:
|
||||
for levels in [True, False]:
|
||||
if dtype == "float" and not levels:
|
||||
continue
|
||||
for lutname in [None, "uint8", "uint16"]:
|
||||
name = (
|
||||
f'time_10x_makeARGB_{"cupy" if cupy else ""}{dtype}_{"" if levels else "no"}levels_{lutname or "no"}lut'
|
||||
)
|
||||
setattr(_TimeSuite, name, make_test(dtype, cupy, levels, lutname, name))
|
||||
|
||||
|
||||
class Time0256Suite(_TimeSuite):
|
||||
def __init__(self):
|
||||
self.size = 256
|
||||
super(Time0256Suite, self).__init__()
|
||||
|
||||
|
||||
class Time0512Suite(_TimeSuite):
|
||||
def __init__(self):
|
||||
self.size = 512
|
||||
super(Time0512Suite, self).__init__()
|
||||
|
||||
|
||||
class Time1024Suite(_TimeSuite):
|
||||
def __init__(self):
|
||||
self.size = 1024
|
||||
super(Time1024Suite, self).__init__()
|
||||
|
||||
|
||||
class Time2048Suite(_TimeSuite):
|
||||
def __init__(self):
|
||||
self.size = 2048
|
||||
super(Time2048Suite, self).__init__()
|
||||
|
||||
|
||||
class Time3072Suite(_TimeSuite):
|
||||
def __init__(self):
|
||||
self.size = 3072
|
||||
super(Time3072Suite, self).__init__()
|
||||
|
||||
|
||||
class Time4096Suite(_TimeSuite):
|
||||
def __init__(self):
|
||||
self.size = 4096
|
||||
super(Time4096Suite, self).__init__()
|
139
benchmarks/renderImageItem.py
Normal file
139
benchmarks/renderImageItem.py
Normal file
@ -0,0 +1,139 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import numpy as np
|
||||
|
||||
import pyqtgraph as pg
|
||||
|
||||
try:
|
||||
import cupy as cp
|
||||
|
||||
pg.setConfigOption("useCupy", True)
|
||||
except ImportError:
|
||||
cp = None
|
||||
|
||||
try:
|
||||
import numba
|
||||
except ImportError:
|
||||
numba = None
|
||||
|
||||
|
||||
def renderQImage(*args, **kwargs):
|
||||
imgitem = pg.ImageItem(axisOrder='row-major')
|
||||
if 'autoLevels' not in kwargs:
|
||||
kwargs['autoLevels'] = False
|
||||
imgitem.setImage(*args, **kwargs)
|
||||
imgitem.render()
|
||||
|
||||
|
||||
def prime_numba():
|
||||
shape = (64, 64)
|
||||
lut_small = np.random.randint(256, size=(256, 3), dtype=np.uint8)
|
||||
lut_big = np.random.randint(256, size=(512, 3), dtype=np.uint8)
|
||||
for lut in [lut_small, lut_big]:
|
||||
renderQImage(np.zeros(shape, dtype=np.uint8), levels=(20, 220), lut=lut)
|
||||
renderQImage(np.zeros(shape, dtype=np.uint16), levels=(250, 3000), lut=lut)
|
||||
renderQImage(np.zeros(shape, dtype=np.float32), levels=(-4.0, 4.0), lut=lut)
|
||||
|
||||
|
||||
class _TimeSuite(object):
|
||||
def __init__(self):
|
||||
super(_TimeSuite, self).__init__()
|
||||
self.size = None
|
||||
self.float_data = None
|
||||
self.uint8_data = None
|
||||
self.uint8_lut = None
|
||||
self.uint16_data = None
|
||||
self.uint16_lut = None
|
||||
self.cupy_uint16_lut = None
|
||||
self.cupy_uint8_lut = None
|
||||
|
||||
def setup(self):
|
||||
size = (self.size, self.size)
|
||||
self.float_data, self.uint16_data, self.uint8_data, self.uint16_lut, self.uint8_lut = self._create_data(
|
||||
size, np
|
||||
)
|
||||
if numba is not None:
|
||||
# ensure JIT compilation
|
||||
pg.setConfigOption("useNumba", True)
|
||||
prime_numba()
|
||||
pg.setConfigOption("useNumba", False)
|
||||
if cp:
|
||||
_d1, _d2, _d3, self.cupy_uint16_lut, self.cupy_uint8_lut = self._create_data(size, cp)
|
||||
renderQImage(cp.asarray(self.uint16_data["data"])) # prime the gpu
|
||||
|
||||
@property
|
||||
def numba_uint16_lut(self):
|
||||
return self.uint16_lut
|
||||
|
||||
@property
|
||||
def numba_uint8_lut(self):
|
||||
return self.uint8_lut
|
||||
|
||||
@property
|
||||
def numpy_uint16_lut(self):
|
||||
return self.uint16_lut
|
||||
|
||||
@property
|
||||
def numpy_uint8_lut(self):
|
||||
return self.uint8_lut
|
||||
|
||||
@staticmethod
|
||||
def _create_data(size, xp):
|
||||
float_data = {
|
||||
"data": xp.random.normal(size=size).astype("float32"),
|
||||
"levels": [-4.0, 4.0],
|
||||
}
|
||||
uint16_data = {
|
||||
"data": xp.random.randint(100, 4500, size=size).astype("uint16"),
|
||||
"levels": [250, 3000],
|
||||
}
|
||||
uint8_data = {
|
||||
"data": xp.random.randint(0, 255, size=size).astype("ubyte"),
|
||||
"levels": [20, 220],
|
||||
}
|
||||
c_map = xp.array([[-500.0, 255.0], [-255.0, 255.0], [0.0, 500.0]])
|
||||
uint8_lut = xp.zeros((256, 4), dtype="ubyte")
|
||||
for i in range(3):
|
||||
uint8_lut[:, i] = xp.clip(xp.linspace(c_map[i][0], c_map[i][1], 256), 0, 255)
|
||||
uint8_lut[:, 3] = 255
|
||||
uint16_lut = xp.zeros((2 ** 16, 4), dtype="ubyte")
|
||||
for i in range(3):
|
||||
uint16_lut[:, i] = xp.clip(xp.linspace(c_map[i][0], c_map[i][1], 2 ** 16), 0, 255)
|
||||
uint16_lut[:, 3] = 255
|
||||
return float_data, uint16_data, uint8_data, uint16_lut, uint8_lut
|
||||
|
||||
|
||||
def make_test(dtype, kind, use_levels, lut_name, func_name):
|
||||
def time_test(self):
|
||||
data = getattr(self, dtype + "_data")
|
||||
levels = data["levels"] if use_levels else None
|
||||
lut = getattr(self, f"{kind}_{lut_name}_lut", None) if lut_name is not None else None
|
||||
pg.setConfigOption("useNumba", kind == "numba")
|
||||
img_data = data["data"]
|
||||
if kind == "cupy":
|
||||
img_data = cp.asarray(img_data)
|
||||
renderQImage(img_data, lut=lut, levels=levels)
|
||||
|
||||
time_test.__name__ = func_name
|
||||
return time_test
|
||||
|
||||
|
||||
for option in ["cupy", "numba", "numpy"]:
|
||||
if option == "cupy" and cp is None:
|
||||
continue
|
||||
if option == "numba" and numba is None:
|
||||
continue
|
||||
for data_type in ["float", "uint16", "uint8"]:
|
||||
for lvls in [True, False]:
|
||||
if data_type == "float" and not lvls:
|
||||
continue
|
||||
for lutname in [None, "uint8", "uint16"]:
|
||||
name = (
|
||||
f'time_1x_renderImageItem_{option}_{data_type}_{"" if lvls else "no"}levels_{lutname or "no"}lut'
|
||||
)
|
||||
setattr(_TimeSuite, name, make_test(data_type, option, lvls, lutname, name))
|
||||
|
||||
|
||||
class Time4096Suite(_TimeSuite):
|
||||
def __init__(self):
|
||||
super(Time4096Suite, self).__init__()
|
||||
self.size = 4096
|
@ -1390,6 +1390,47 @@ def try_fastpath_argb(xp, ain, aout, useRGBA):
|
||||
return True
|
||||
|
||||
|
||||
def ndarray_to_qimage(arr, fmt):
|
||||
"""
|
||||
Low level function to encapsulate QImage creation differences between bindings.
|
||||
"arr" is assumed to be C-contiguous.
|
||||
"""
|
||||
|
||||
# C++ QImage has two kind of constructors
|
||||
# - QImage(const uchar*, ...)
|
||||
# - QImage(uchar*, ...)
|
||||
# If the const constructor is used, subsequently calling any non-const method
|
||||
# will trigger the COW mechanism, i.e. a copy is made under the hood.
|
||||
|
||||
if QT_LIB.startswith('PyQt'):
|
||||
if QtCore.PYQT_VERSION == 0x60000:
|
||||
# PyQt5 -> const
|
||||
# PyQt6 >= 6.0.1 -> const
|
||||
# PyQt6 == 6.0.0 -> non-const
|
||||
img_ptr = Qt.sip.voidptr(arr)
|
||||
else:
|
||||
# PyQt5 -> non-const
|
||||
# PyQt6 >= 6.0.1 -> non-const
|
||||
img_ptr = int(Qt.sip.voidptr(arr)) # or arr.ctypes.data
|
||||
else:
|
||||
# bindings that support ndarray
|
||||
# PyQt5 -> const
|
||||
# PyQt6 >= 6.0.1 -> const
|
||||
# PySide2 -> non-const
|
||||
# PySide6 -> non-const
|
||||
img_ptr = arr
|
||||
|
||||
h, w = arr.shape[:2]
|
||||
bytesPerLine = arr.strides[0]
|
||||
qimg = QtGui.QImage(img_ptr, w, h, bytesPerLine, fmt)
|
||||
|
||||
# Note that the bindings that support ndarray directly already hold a reference
|
||||
# to it. The manual reference below is only needed for those bindings that take
|
||||
# in a raw pointer.
|
||||
qimg.data = arr
|
||||
return qimg
|
||||
|
||||
|
||||
def makeQImage(imgData, alpha=None, copy=True, transpose=True):
|
||||
"""
|
||||
Turn an ARGB array into QImage.
|
||||
@ -1467,34 +1508,8 @@ def makeQImage(imgData, alpha=None, copy=True, transpose=True):
|
||||
|
||||
profile("copy")
|
||||
|
||||
# C++ QImage has two kind of constructors
|
||||
# - QImage(const uchar*, ...)
|
||||
# - QImage(uchar*, ...)
|
||||
# If the const constructor is used, subsequently calling any non-const method
|
||||
# will trigger the COW mechanism, i.e. a copy is made under the hood.
|
||||
return ndarray_to_qimage(imgData, imgFormat)
|
||||
|
||||
if QT_LIB.startswith('PyQt'):
|
||||
if QtCore.PYQT_VERSION == 0x60000:
|
||||
# PyQt5 -> const
|
||||
# PyQt6 >= 6.0.1 -> const
|
||||
# PyQt6 == 6.0.0 -> non-const
|
||||
img_ptr = Qt.sip.voidptr(imgData)
|
||||
else:
|
||||
# PyQt5 -> non-const
|
||||
# PyQt6 >= 6.0.1 -> non-const
|
||||
img_ptr = int(Qt.sip.voidptr(imgData)) # or imgData.ctypes.data
|
||||
else:
|
||||
# bindings that support ndarray
|
||||
# PyQt5 -> const
|
||||
# PyQt6 >= 6.0.1 -> const
|
||||
# PySide2 -> non-const
|
||||
# PySide6 -> non-const
|
||||
img_ptr = imgData
|
||||
|
||||
img = QtGui.QImage(img_ptr, imgData.shape[1], imgData.shape[0], imgFormat)
|
||||
|
||||
img.data = imgData
|
||||
return img
|
||||
|
||||
def imageToArray(img, copy=False, transpose=True):
|
||||
"""
|
||||
|
@ -159,11 +159,27 @@ class ImageItem(GraphicsObject):
|
||||
or :class:`GradientEditorItem <pyqtgraph.GradientEditorItem>`.
|
||||
"""
|
||||
if lut is not self.lut:
|
||||
if self._xp is not None:
|
||||
lut = self._ensure_proper_substrate(lut, self._xp)
|
||||
self.lut = lut
|
||||
self._effectiveLut = None
|
||||
if update:
|
||||
self.updateImage()
|
||||
|
||||
@staticmethod
|
||||
def _ensure_proper_substrate(data, substrate):
|
||||
if data is None or isinstance(data, Callable) or isinstance(data, substrate.ndarray):
|
||||
return data
|
||||
cupy = getCupy()
|
||||
if substrate == cupy and not isinstance(data, cupy.ndarray):
|
||||
data = cupy.asarray(data)
|
||||
elif substrate == numpy:
|
||||
if cupy is not None and isinstance(data, cupy.ndarray):
|
||||
data = data.get()
|
||||
else:
|
||||
data = numpy.asarray(data)
|
||||
return data
|
||||
|
||||
def setAutoDownsample(self, ads):
|
||||
"""
|
||||
Set the automatic downsampling mode for this ImageItem.
|
||||
@ -393,6 +409,7 @@ class ImageItem(GraphicsObject):
|
||||
|
||||
# Request a lookup table if this image has only one channel
|
||||
if self.image.ndim == 2 or self.image.shape[2] == 1:
|
||||
self.lut = self._ensure_proper_substrate(self.lut, self._xp)
|
||||
if isinstance(self.lut, Callable):
|
||||
lut = self.lut(self.image)
|
||||
else:
|
||||
@ -416,26 +433,6 @@ class ImageItem(GraphicsObject):
|
||||
else:
|
||||
image = self.image
|
||||
|
||||
# if the image data is a small int, then we can combine levels + lut
|
||||
# into a single lut for better performance
|
||||
levels = self.levels
|
||||
if levels is not None and lut is not None and levels.ndim == 1 and \
|
||||
image.dtype in (self._xp.ubyte, self._xp.uint16):
|
||||
if self._effectiveLut is None:
|
||||
eflsize = 2**(image.itemsize*8)
|
||||
ind = self._xp.arange(eflsize)
|
||||
minlev, maxlev = levels
|
||||
levdiff = maxlev - minlev
|
||||
levdiff = 1 if levdiff == 0 else levdiff # don't allow division by 0
|
||||
lutdtype = self._xp.min_scalar_type(lut.shape[0] - 1)
|
||||
efflut = fn.rescaleData(ind, scale=(lut.shape[0]-1)/levdiff,
|
||||
offset=minlev, dtype=lutdtype, clip=(0, lut.shape[0]-1))
|
||||
efflut = lut[efflut]
|
||||
|
||||
self._effectiveLut = efflut
|
||||
lut = self._effectiveLut
|
||||
levels = None
|
||||
|
||||
# Convert single-channel image to 2D array
|
||||
if image.ndim == 3 and image.shape[-1] == 1:
|
||||
image = image[..., 0]
|
||||
@ -443,7 +440,30 @@ class ImageItem(GraphicsObject):
|
||||
# Assume images are in column-major order for backward compatibility
|
||||
# (most images are in row-major order)
|
||||
if self.axisOrder == 'col-major':
|
||||
image = image.transpose((1, 0, 2)[:image.ndim])
|
||||
image = image.swapaxes(0, 1)
|
||||
|
||||
levels = self.levels
|
||||
augmented_alpha = False
|
||||
|
||||
if image.dtype.kind == 'f':
|
||||
image, levels, lut, augmented_alpha = self._try_rescale_float(image, levels, lut)
|
||||
# if we succeeded, we will have an uint8 image with levels None.
|
||||
# lut if not None will have <= 256 entries
|
||||
|
||||
# if the image data is a small int, then we can combine levels + lut
|
||||
# into a single lut for better performance
|
||||
elif image.dtype in (self._xp.ubyte, self._xp.uint16):
|
||||
image, levels, lut, augmented_alpha = self._try_combine_lut(image, levels, lut)
|
||||
|
||||
qimage = self._try_make_qimage(image, levels, lut, augmented_alpha)
|
||||
|
||||
if qimage is not None:
|
||||
self._processingBuffer = None
|
||||
self._displayBuffer = None
|
||||
self.qimage = qimage
|
||||
self._renderRequired = False
|
||||
self._unrenderable = False
|
||||
return
|
||||
|
||||
if self._processingBuffer is None or self._processingBuffer.shape[:2] != image.shape[:2]:
|
||||
self._buildQImageBuffer(image.shape)
|
||||
@ -451,9 +471,243 @@ class ImageItem(GraphicsObject):
|
||||
fn.makeARGB(image, lut=lut, levels=levels, output=self._processingBuffer)
|
||||
if self._xp == getCupy():
|
||||
self._processingBuffer.get(out=self._displayBuffer)
|
||||
|
||||
self._renderRequired = False
|
||||
self._unrenderable = False
|
||||
|
||||
def _try_rescale_float(self, image, levels, lut):
|
||||
xp = self._xp
|
||||
augmented_alpha = False
|
||||
|
||||
can_handle = False
|
||||
while True:
|
||||
if levels is None or levels.ndim != 1:
|
||||
# float images always need levels
|
||||
# can't handle multi-channel levels
|
||||
break
|
||||
|
||||
# awkward, but fastest numpy native nan evaluation
|
||||
if xp.isnan(image.min()):
|
||||
# don't handle images with nans
|
||||
# this should be an uncommon case
|
||||
break
|
||||
|
||||
can_handle = True
|
||||
break
|
||||
|
||||
if not can_handle:
|
||||
return image, levels, lut, augmented_alpha
|
||||
|
||||
# Decide on maximum scaled value
|
||||
if lut is not None:
|
||||
scale = lut.shape[0]
|
||||
num_colors = lut.shape[0]
|
||||
else:
|
||||
scale = 255.
|
||||
num_colors = 256
|
||||
dtype = xp.min_scalar_type(num_colors-1)
|
||||
|
||||
minVal, maxVal = levels
|
||||
if minVal == maxVal:
|
||||
maxVal = xp.nextafter(maxVal, 2*maxVal)
|
||||
rng = maxVal - minVal
|
||||
rng = 1 if rng == 0 else rng
|
||||
image = fn.rescaleData(image, scale/rng, offset=minVal, dtype=dtype, clip=(0, num_colors-1))
|
||||
|
||||
levels = None
|
||||
|
||||
if image.dtype == xp.uint16 and image.ndim == 2:
|
||||
image, augmented_alpha = self._apply_lut_for_uint16_mono(image, lut)
|
||||
lut = None
|
||||
|
||||
# image is now of type uint8
|
||||
return image, levels, lut, augmented_alpha
|
||||
|
||||
def _try_combine_lut(self, image, levels, lut):
|
||||
augmented_alpha = False
|
||||
xp = self._xp
|
||||
|
||||
can_handle = False
|
||||
while True:
|
||||
if levels is not None and levels.ndim != 1:
|
||||
# can't handle multi-channel levels
|
||||
break
|
||||
if image.dtype == xp.uint16 and levels is None and \
|
||||
image.ndim == 3 and image.shape[2] == 3:
|
||||
# uint16 rgb can't be directly displayed, so make it
|
||||
# pass through effective lut processing
|
||||
levels = [0, 65535]
|
||||
if levels is None and lut is None:
|
||||
# nothing to combine
|
||||
break
|
||||
|
||||
can_handle = True
|
||||
break
|
||||
|
||||
if not can_handle:
|
||||
return image, levels, lut, augmented_alpha
|
||||
|
||||
# distinguish between lut for levels and colors
|
||||
levels_lut = None
|
||||
colors_lut = lut
|
||||
lut = None
|
||||
|
||||
eflsize = 2**(image.itemsize*8)
|
||||
if levels is None:
|
||||
info = xp.iinfo(image.dtype)
|
||||
minlev, maxlev = info.min, info.max
|
||||
else:
|
||||
minlev, maxlev = levels
|
||||
levdiff = maxlev - minlev
|
||||
levdiff = 1 if levdiff == 0 else levdiff # don't allow division by 0
|
||||
|
||||
if colors_lut is None:
|
||||
if image.dtype == xp.ubyte and image.ndim == 2:
|
||||
# uint8 mono image
|
||||
ind = xp.arange(eflsize)
|
||||
levels_lut = fn.rescaleData(ind, scale=255./levdiff,
|
||||
offset=minlev, dtype=xp.ubyte)
|
||||
# image data is not scaled. instead, levels_lut is used
|
||||
# as (grayscale) Indexed8 ColorTable to get the same effect.
|
||||
# due to the small size of the input to rescaleData(), we
|
||||
# do not bother caching the result
|
||||
return image, None, levels_lut, augmented_alpha
|
||||
else:
|
||||
# uint16 mono, uint8 rgb, uint16 rgb
|
||||
# rescale image data by computation instead of by memory lookup
|
||||
image = fn.rescaleData(image, scale=255./levdiff,
|
||||
offset=minlev, dtype=xp.ubyte)
|
||||
return image, None, colors_lut, augmented_alpha
|
||||
else:
|
||||
num_colors = colors_lut.shape[0]
|
||||
effscale = num_colors / levdiff
|
||||
lutdtype = xp.min_scalar_type(num_colors - 1)
|
||||
|
||||
if image.dtype == xp.ubyte or lutdtype != xp.ubyte:
|
||||
# combine if either:
|
||||
# 1) uint8 mono image
|
||||
# 2) colors_lut has more entries than will fit within 8-bits
|
||||
if self._effectiveLut is None:
|
||||
ind = xp.arange(eflsize)
|
||||
levels_lut = fn.rescaleData(ind, scale=effscale,
|
||||
offset=minlev, dtype=lutdtype, clip=(0, num_colors-1))
|
||||
efflut = colors_lut[levels_lut]
|
||||
levels_lut = None
|
||||
colors_lut = None
|
||||
self._effectiveLut = efflut
|
||||
efflut = self._effectiveLut
|
||||
|
||||
# apply the effective lut early for the following types:
|
||||
if image.dtype == xp.uint16 and image.ndim == 2:
|
||||
image, augmented_alpha = self._apply_lut_for_uint16_mono(image, efflut)
|
||||
efflut = None
|
||||
return image, None, efflut, augmented_alpha
|
||||
else:
|
||||
# uint16 image with colors_lut <= 256 entries
|
||||
# don't combine, we will use QImage ColorTable
|
||||
image = fn.rescaleData(image, scale=effscale,
|
||||
offset=minlev, dtype=lutdtype, clip=(0, num_colors-1))
|
||||
return image, None, colors_lut, augmented_alpha
|
||||
|
||||
def _apply_lut_for_uint16_mono(self, image, lut):
|
||||
# Note: compared to makeARGB(), we have already clipped the data to range
|
||||
|
||||
xp = self._xp
|
||||
augmented_alpha = False
|
||||
|
||||
# if lut is 1d, then lut[image] is fastest
|
||||
# if lut is 2d, then lut.take(image, axis=0) is faster than lut[image]
|
||||
|
||||
if not image.flags.c_contiguous:
|
||||
image = lut.take(image, axis=0)
|
||||
|
||||
# if lut had dimensions (N, 1), then our resultant image would
|
||||
# have dimensions (h, w, 1)
|
||||
if image.ndim == 3 and image.shape[-1] == 1:
|
||||
image = image[..., 0]
|
||||
|
||||
return image, augmented_alpha
|
||||
|
||||
# if we are contiguous, we can take a faster codepath where we
|
||||
# ensure that the lut is 1d
|
||||
|
||||
if lut.ndim == 2:
|
||||
if lut.shape[1] == 3: # rgb
|
||||
# convert rgb lut to rgba so that it is 32-bits
|
||||
lut = xp.column_stack([lut, xp.full(lut.shape[0], 255, dtype=xp.uint8)])
|
||||
augmented_alpha = True
|
||||
if lut.shape[1] == 4: # rgba
|
||||
lut = lut.view(xp.uint32)
|
||||
|
||||
image = lut.ravel()[image]
|
||||
lut = None
|
||||
# now both levels and lut are None
|
||||
if image.dtype == xp.uint32:
|
||||
image = image.view(xp.uint8).reshape(image.shape + (4,))
|
||||
|
||||
return image, augmented_alpha
|
||||
|
||||
def _try_make_qimage(self, image, levels, lut, augmented_alpha):
|
||||
xp = self._xp
|
||||
|
||||
ubyte_nolvl = image.dtype == xp.ubyte and levels is None
|
||||
is_passthru8 = ubyte_nolvl and lut is None
|
||||
is_indexed8 = ubyte_nolvl and image.ndim == 2 and \
|
||||
lut is not None and lut.shape[0] <= 256
|
||||
is_passthru16 = image.dtype == xp.uint16 and levels is None and lut is None
|
||||
can_grayscale16 = is_passthru16 and image.ndim == 2 and \
|
||||
hasattr(QtGui.QImage.Format, 'Format_Grayscale16')
|
||||
is_rgba64 = is_passthru16 and image.ndim == 3 and image.shape[2] == 4
|
||||
|
||||
# bypass makeARGB for supported combinations
|
||||
supported = is_passthru8 or is_indexed8 or can_grayscale16 or is_rgba64
|
||||
if not supported:
|
||||
return None
|
||||
|
||||
if self._xp == getCupy():
|
||||
image = image.get()
|
||||
|
||||
# worthwhile supporting non-contiguous arrays
|
||||
image = numpy.ascontiguousarray(image)
|
||||
|
||||
fmt = None
|
||||
ctbl = None
|
||||
if is_passthru8:
|
||||
# both levels and lut are None
|
||||
# these images are suitable for display directly
|
||||
if image.ndim == 2:
|
||||
fmt = QtGui.QImage.Format.Format_Grayscale8
|
||||
elif image.shape[2] == 3:
|
||||
fmt = QtGui.QImage.Format.Format_RGB888
|
||||
elif image.shape[2] == 4:
|
||||
if augmented_alpha:
|
||||
fmt = QtGui.QImage.Format.Format_RGBX8888
|
||||
else:
|
||||
fmt = QtGui.QImage.Format.Format_RGBA8888
|
||||
elif is_indexed8:
|
||||
# levels and/or lut --> lut-only
|
||||
fmt = QtGui.QImage.Format.Format_Indexed8
|
||||
if lut.ndim == 1 or lut.shape[1] == 1:
|
||||
ctbl = [QtGui.qRgb(x,x,x) for x in lut.ravel().tolist()]
|
||||
elif lut.shape[1] == 3:
|
||||
ctbl = [QtGui.qRgb(*rgb) for rgb in lut.tolist()]
|
||||
elif lut.shape[1] == 4:
|
||||
ctbl = [QtGui.qRgba(*rgba) for rgba in lut.tolist()]
|
||||
elif can_grayscale16:
|
||||
# single channel uint16
|
||||
# both levels and lut are None
|
||||
fmt = QtGui.QImage.Format.Format_Grayscale16
|
||||
elif is_rgba64:
|
||||
# uint16 rgba
|
||||
# both levels and lut are None
|
||||
fmt = QtGui.QImage.Format.Format_RGBA64 # endian-independent
|
||||
if fmt is None:
|
||||
raise ValueError("unsupported image type")
|
||||
qimage = fn.ndarray_to_qimage(image, fmt)
|
||||
if ctbl is not None:
|
||||
qimage.setColorTable(ctbl)
|
||||
return qimage
|
||||
|
||||
def paint(self, p, *args):
|
||||
profile = debug.Profiler()
|
||||
if self.image is None:
|
||||
|
@ -33,6 +33,34 @@ def test_useCupy_can_be_set_after_init():
|
||||
pg.setConfigOption("useCupy", prev_setting)
|
||||
|
||||
|
||||
@pytest.mark.skipif(cupy is None, reason="CuPy unavailable to test")
|
||||
def test_ensuring_substrate():
|
||||
prev_setting = pg.getConfigOption("useCupy")
|
||||
try:
|
||||
pg.setConfigOption("useCupy", True)
|
||||
ii = pg.ImageItem()
|
||||
|
||||
data = cupy.random.randint(0, 255, size=(32, 32)).astype(cupy.uint8)
|
||||
assert data is ii._ensure_proper_substrate(data, cupy)
|
||||
assert isinstance(ii._ensure_proper_substrate(data, cupy), cupy.ndarray)
|
||||
assert data is not ii._ensure_proper_substrate(data, np)
|
||||
assert isinstance(ii._ensure_proper_substrate(data, np), np.ndarray)
|
||||
|
||||
data = np.random.randint(0, 255, size=(32, 32)).astype(np.uint8)
|
||||
assert data is ii._ensure_proper_substrate(data, np)
|
||||
assert isinstance(ii._ensure_proper_substrate(data, np), np.ndarray)
|
||||
assert data is not ii._ensure_proper_substrate(data, cupy)
|
||||
assert isinstance(ii._ensure_proper_substrate(data, cupy), cupy.ndarray)
|
||||
|
||||
data = range(0, 255)
|
||||
assert data is not ii._ensure_proper_substrate(data, np)
|
||||
assert isinstance(ii._ensure_proper_substrate(data, np), np.ndarray)
|
||||
assert data is not ii._ensure_proper_substrate(data, cupy)
|
||||
assert isinstance(ii._ensure_proper_substrate(data, cupy), cupy.ndarray)
|
||||
finally:
|
||||
pg.setConfigOption("useCupy", prev_setting)
|
||||
|
||||
|
||||
def test_ImageItem(transpose=False):
|
||||
w = pg.GraphicsLayoutWidget()
|
||||
w.show()
|
||||
|
168
pyqtgraph/graphicsItems/tests/test_ImageItemFormat.py
Normal file
168
pyqtgraph/graphicsItems/tests/test_ImageItemFormat.py
Normal file
@ -0,0 +1,168 @@
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
from pyqtgraph.Qt import QtGui
|
||||
|
||||
|
||||
def check_format(shape, dtype, levels, lut, expected_format):
|
||||
data = np.zeros(shape, dtype=dtype)
|
||||
item = pg.ImageItem(axisOrder='row-major')
|
||||
item.setImage(data, autoLevels=False, lut=lut, levels=levels)
|
||||
item.render()
|
||||
assert item.qimage.format() == expected_format
|
||||
|
||||
|
||||
def test_uint8():
|
||||
Format = QtGui.QImage.Format
|
||||
dtype = np.uint8
|
||||
w, h = 192, 108
|
||||
lo, hi = 50, 200
|
||||
lut_none = None
|
||||
lut_mono1 = np.random.randint(256, size=256, dtype=np.uint8)
|
||||
lut_mono2 = np.random.randint(256, size=(256, 1), dtype=np.uint8)
|
||||
lut_rgb = np.random.randint(256, size=(256, 3), dtype=np.uint8)
|
||||
lut_rgba = np.random.randint(256, size=(256, 4), dtype=np.uint8)
|
||||
|
||||
# lut with less than 256 entries
|
||||
lut_mono1_s = np.random.randint(256, size=255, dtype=np.uint8)
|
||||
lut_mono2_s = np.random.randint(256, size=(255, 1), dtype=np.uint8)
|
||||
lut_rgb_s = np.random.randint(256, size=(255, 3), dtype=np.uint8)
|
||||
lut_rgba_s = np.random.randint(256, size=(255, 4), dtype=np.uint8)
|
||||
|
||||
# lut with more than 256 entries
|
||||
lut_mono1_l = np.random.randint(256, size=257, dtype=np.uint8)
|
||||
lut_mono2_l = np.random.randint(256, size=(257, 1), dtype=np.uint8)
|
||||
lut_rgb_l = np.random.randint(256, size=(257, 3), dtype=np.uint8)
|
||||
lut_rgba_l = np.random.randint(256, size=(257, 4), dtype=np.uint8)
|
||||
|
||||
levels = None
|
||||
check_format((h, w), dtype, levels, lut_none, Format.Format_Grayscale8)
|
||||
check_format((h, w, 3), dtype, levels, lut_none, Format.Format_RGB888)
|
||||
check_format((h, w, 4), dtype, levels, lut_none, Format.Format_RGBA8888)
|
||||
|
||||
levels = [lo, hi]
|
||||
check_format((h, w), dtype, levels, lut_none, Format.Format_Indexed8)
|
||||
levels = None
|
||||
check_format((h, w), dtype, levels, lut_mono1, Format.Format_Indexed8)
|
||||
check_format((h, w), dtype, levels, lut_mono2, Format.Format_Indexed8)
|
||||
check_format((h, w), dtype, levels, lut_rgb, Format.Format_Indexed8)
|
||||
check_format((h, w), dtype, levels, lut_rgba, Format.Format_Indexed8)
|
||||
levels = [lo, hi]
|
||||
check_format((h, w), dtype, levels, lut_mono1, Format.Format_Indexed8)
|
||||
check_format((h, w), dtype, levels, lut_mono2, Format.Format_Indexed8)
|
||||
check_format((h, w), dtype, levels, lut_rgb, Format.Format_Indexed8)
|
||||
check_format((h, w), dtype, levels, lut_rgba, Format.Format_Indexed8)
|
||||
|
||||
check_format((h, w), dtype, levels, lut_mono1_s, Format.Format_Indexed8)
|
||||
check_format((h, w), dtype, levels, lut_mono2_s, Format.Format_Indexed8)
|
||||
check_format((h, w), dtype, levels, lut_rgb_s, Format.Format_Indexed8)
|
||||
check_format((h, w), dtype, levels, lut_rgba_s, Format.Format_Indexed8)
|
||||
|
||||
check_format((h, w), dtype, levels, lut_mono1_l, Format.Format_Indexed8)
|
||||
check_format((h, w), dtype, levels, lut_mono2_l, Format.Format_Indexed8)
|
||||
check_format((h, w), dtype, levels, lut_rgb_l, Format.Format_Indexed8)
|
||||
check_format((h, w), dtype, levels, lut_rgba_l, Format.Format_Indexed8)
|
||||
|
||||
levels = [lo, hi]
|
||||
check_format((h, w, 3), dtype, levels, lut_none, Format.Format_RGB888)
|
||||
|
||||
|
||||
def test_uint16():
|
||||
Format = QtGui.QImage.Format
|
||||
dtype = np.uint16
|
||||
w, h = 192, 108
|
||||
lo, hi = 100, 10000
|
||||
lut_none = None
|
||||
|
||||
lut_mono1 = np.random.randint(256, size=256, dtype=np.uint8)
|
||||
lut_mono2 = np.random.randint(256, size=(256, 1), dtype=np.uint8)
|
||||
lut_rgb = np.random.randint(256, size=(256, 3), dtype=np.uint8)
|
||||
lut_rgba = np.random.randint(256, size=(256, 4), dtype=np.uint8)
|
||||
|
||||
# lut with less than 256 entries
|
||||
lut_mono1_s = np.random.randint(256, size=255, dtype=np.uint8)
|
||||
lut_mono2_s = np.random.randint(256, size=(255, 1), dtype=np.uint8)
|
||||
lut_rgb_s = np.random.randint(256, size=(255, 3), dtype=np.uint8)
|
||||
lut_rgba_s = np.random.randint(256, size=(255, 4), dtype=np.uint8)
|
||||
|
||||
# lut with more than 256 entries
|
||||
lut_mono1_l = np.random.randint(256, size=257, dtype=np.uint8)
|
||||
lut_mono2_l = np.random.randint(256, size=(257, 1), dtype=np.uint8)
|
||||
lut_rgb_l = np.random.randint(256, size=(257, 3), dtype=np.uint8)
|
||||
lut_rgba_l = np.random.randint(256, size=(257, 4), dtype=np.uint8)
|
||||
|
||||
levels = None
|
||||
try:
|
||||
fmt_gray16 = Format.Format_Grayscale16
|
||||
except AttributeError:
|
||||
fmt_gray16 = Format.Format_ARGB32
|
||||
check_format((h, w), dtype, levels, lut_none, fmt_gray16)
|
||||
check_format((h, w, 3), dtype, levels, lut_none, Format.Format_RGB888)
|
||||
check_format((h, w, 4), dtype, levels, lut_none, Format.Format_RGBA64)
|
||||
|
||||
levels = [lo, hi]
|
||||
check_format((h, w), dtype, levels, lut_none, Format.Format_Grayscale8)
|
||||
levels = None
|
||||
check_format((h, w), dtype, levels, lut_mono1, Format.Format_Indexed8)
|
||||
check_format((h, w), dtype, levels, lut_mono2, Format.Format_Indexed8)
|
||||
check_format((h, w), dtype, levels, lut_rgb, Format.Format_Indexed8)
|
||||
check_format((h, w), dtype, levels, lut_rgba, Format.Format_Indexed8)
|
||||
|
||||
check_format((h, w), dtype, levels, lut_mono1_s, Format.Format_Indexed8)
|
||||
check_format((h, w), dtype, levels, lut_mono2_s, Format.Format_Indexed8)
|
||||
check_format((h, w), dtype, levels, lut_rgb_s, Format.Format_Indexed8)
|
||||
check_format((h, w), dtype, levels, lut_rgba_s, Format.Format_Indexed8)
|
||||
|
||||
check_format((h, w), dtype, levels, lut_mono1_l, Format.Format_Grayscale8)
|
||||
check_format((h, w), dtype, levels, lut_mono2_l, Format.Format_Grayscale8)
|
||||
check_format((h, w), dtype, levels, lut_rgb_l, Format.Format_RGBX8888)
|
||||
check_format((h, w), dtype, levels, lut_rgba_l, Format.Format_RGBA8888)
|
||||
|
||||
levels = [lo, hi]
|
||||
check_format((h, w, 3), dtype, levels, lut_none, Format.Format_RGB888)
|
||||
|
||||
|
||||
def test_float32():
|
||||
Format = QtGui.QImage.Format
|
||||
dtype = np.float32
|
||||
w, h = 192, 108
|
||||
lo, hi = -1, 1
|
||||
lut_none = None
|
||||
|
||||
lut_mono1 = np.random.randint(256, size=256, dtype=np.uint8)
|
||||
lut_mono2 = np.random.randint(256, size=(256, 1), dtype=np.uint8)
|
||||
lut_rgb = np.random.randint(256, size=(256, 3), dtype=np.uint8)
|
||||
lut_rgba = np.random.randint(256, size=(256, 4), dtype=np.uint8)
|
||||
|
||||
# lut with less than 256 entries
|
||||
lut_mono1_s = np.random.randint(256, size=255, dtype=np.uint8)
|
||||
lut_mono2_s = np.random.randint(256, size=(255, 1), dtype=np.uint8)
|
||||
lut_rgb_s = np.random.randint(256, size=(255, 3), dtype=np.uint8)
|
||||
lut_rgba_s = np.random.randint(256, size=(255, 4), dtype=np.uint8)
|
||||
|
||||
# lut with more than 256 entries
|
||||
lut_mono1_l = np.random.randint(256, size=257, dtype=np.uint8)
|
||||
lut_mono2_l = np.random.randint(256, size=(257, 1), dtype=np.uint8)
|
||||
lut_rgb_l = np.random.randint(256, size=(257, 3), dtype=np.uint8)
|
||||
lut_rgba_l = np.random.randint(256, size=(257, 4), dtype=np.uint8)
|
||||
|
||||
levels = [lo, hi]
|
||||
|
||||
check_format((h, w), dtype, levels, lut_none, Format.Format_Grayscale8)
|
||||
check_format((h, w, 3), dtype, levels, lut_none, Format.Format_RGB888)
|
||||
check_format((h, w, 4), dtype, levels, lut_none, Format.Format_RGBA8888)
|
||||
|
||||
check_format((h, w), dtype, levels, lut_mono1, Format.Format_Indexed8)
|
||||
check_format((h, w), dtype, levels, lut_mono2, Format.Format_Indexed8)
|
||||
check_format((h, w), dtype, levels, lut_rgb, Format.Format_Indexed8)
|
||||
check_format((h, w), dtype, levels, lut_rgba, Format.Format_Indexed8)
|
||||
|
||||
check_format((h, w), dtype, levels, lut_mono1_s, Format.Format_Indexed8)
|
||||
check_format((h, w), dtype, levels, lut_mono2_s, Format.Format_Indexed8)
|
||||
check_format((h, w), dtype, levels, lut_rgb_s, Format.Format_Indexed8)
|
||||
check_format((h, w), dtype, levels, lut_rgba_s, Format.Format_Indexed8)
|
||||
|
||||
check_format((h, w), dtype, levels, lut_mono1_l, Format.Format_Grayscale8)
|
||||
check_format((h, w), dtype, levels, lut_mono2_l, Format.Format_Grayscale8)
|
||||
check_format((h, w), dtype, levels, lut_rgb_l, Format.Format_RGBX8888)
|
||||
check_format((h, w), dtype, levels, lut_rgba_l, Format.Format_RGBA8888)
|
||||
|
Loading…
Reference in New Issue
Block a user