Performance enhancement: use CUDA in ImageItem (#1466)

* Add CLI args to video speed test for easier / automated benchmarking

* use a buffer-qimage so we can avoid allocing so much

this should improve performance under windows

* playing with numba

* oh, mins/maxes in the other order

* maybe put the cupy in here and see what happens

* pre-alloc for gpu and cpu

* handle possibility of not having cupy

* no numba in this branch

* organize imports

* name them after their use, not their expected device

* cupy.take does not support clip mode, so do it explicitly

* add CUDA option to the VideoSpeedTest

* rename private attr xp to _xp

* handle resizes at the last moment

* cupy is less accepting of lists as args

* or somehow range isn't allowed? what histogram is this?

* construct the array with python objects

* get the python value right away

* put LUT into cupy if needed

* docstring about cuda toolkit version

* better handling and display of missing cuda lib

* lint

* import need

* handle switching between cupy and numpy in a single ImageItem

* only use xp when necessary

we can now depend on numpy >= 1.17, which means __array_function__-implementing cupy can
seamlessly pass into numpy functions. the remaining uses of xp are for our functions which
need to allocate new data structures, an operation that has to be substrate-specific.

remove empty_cupy; just check if the import succeeded, instead.

* use an option to control use of cupy

* convert cupy.ceil array to int for easier mathing

* RawImageWidget gets to use the getCupy function now, too

* raise error to calm linters; rename for clarity

* Add Generated Template Files

* document things better

* cruft removal

* warnings to communicate when cupy is expected but somehow broken

* playing with settings to suss out timeout

* playing with more stuff to suss out timeout

* replace with empty list

* skip test_ExampleApp on linux+pyside2 only

Co-authored-by: Luke Campagnola <luke.campagnola@gmail.com>
Co-authored-by: Ogi Moore <ognyan.moore@gmail.com>
This commit is contained in:
Martin Chase 2021-01-19 21:26:24 -08:00 committed by GitHub
parent 78bc0fd3ca
commit f2b4a15b2d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 550 additions and 323 deletions

View File

@ -44,6 +44,8 @@ Currently this means:
* `pyopengl` on macOS Big Sur only works with python 3.9.1+
* `hdf5` for large hdf5 binary format support
* `colorcet` for supplemental colormaps
* [`cupy`](https://docs.cupy.dev/en/stable/install.html) for CUDA-enhanced image processing
* On Windows, CUDA toolkit must be >= 11.1
Qt Bindings Test Matrix
-----------------------

View File

@ -30,6 +30,8 @@ useWeave bool False Use weave to speed up
weaveDebug bool False Print full error message if weave compile fails.
useOpenGL bool False Enable OpenGL in GraphicsView. This can have unpredictable effects on stability
and performance.
useCupy bool False Use cupy to perform calculations on the GPU. Only currently applies to
ImageItem and its associated functions.
enableExperimental bool False Enable experimental features (the curious can search for this key in the code).
crashWarning bool False If True, print warnings about situations that may result in a crash.
================== =================== ================== ================================================================================

View File

@ -6,18 +6,36 @@ it is being scaled and/or converted by lookup table, and whether OpenGL
is used by the view widget
"""
import argparse
import sys
import initExample ## Add path to library (just for examples; you do not need this)
from pyqtgraph.Qt import QtGui, QtCore, QT_LIB
import numpy as np
import pyqtgraph as pg
import pyqtgraph.ptime as ptime
from pyqtgraph.Qt import QtGui, QtCore, QT_LIB
import importlib
ui_template = importlib.import_module(f'VideoTemplate_{QT_LIB.lower()}')
try:
import cupy as cp
pg.setConfigOption("useCupy", True)
_has_cupy = True
except ImportError:
cp = None
_has_cupy = False
parser = argparse.ArgumentParser(description="Benchmark for testing video performance")
parser.add_argument('--cuda', default=False, action='store_true', help="Use CUDA to process on the GPU", dest="cuda")
parser.add_argument('--dtype', default='uint8', choices=['uint8', 'uint16', 'float'], help="Image dtype (uint8, uint16, or float)")
parser.add_argument('--frames', default=3, type=int, help="Number of image frames to generate (default=3)")
parser.add_argument('--image-mode', default='mono', choices=['mono', 'rgb'], help="Image data mode (mono or rgb)", dest='image_mode')
parser.add_argument('--levels', default=None, type=lambda s: tuple([float(x) for x in s.split(',')]), help="min,max levels to scale monochromatic image dynamic range, or rmin,rmax,gmin,gmax,bmin,bmax to scale rgb")
parser.add_argument('--lut', default=False, action='store_true', help="Use color lookup table")
parser.add_argument('--lut-alpha', default=False, action='store_true', help="Use alpha color lookup table", dest='lut_alpha')
parser.add_argument('--size', default='512x512', type=lambda s: tuple([int(x) for x in s.split('x')]), help="WxH image dimensions default='512x512'")
args = parser.parse_args(sys.argv[1:])
#QtGui.QApplication.setGraphicsSystem('raster')
app = QtGui.QApplication([])
@ -31,14 +49,46 @@ win.show()
try:
from pyqtgraph.widgets.RawImageWidget import RawImageGLWidget
except ImportError:
RawImageGLWidget = None
ui.rawGLRadio.setEnabled(False)
ui.rawGLRadio.setText(ui.rawGLRadio.text() + " (OpenGL not available)")
else:
ui.rawGLImg = RawImageGLWidget()
ui.stack.addWidget(ui.rawGLImg)
# read in CLI args
ui.cudaCheck.setChecked(args.cuda and _has_cupy)
ui.cudaCheck.setEnabled(_has_cupy)
ui.framesSpin.setValue(args.frames)
ui.widthSpin.setValue(args.size[0])
ui.heightSpin.setValue(args.size[1])
ui.dtypeCombo.setCurrentText(args.dtype)
ui.rgbCheck.setChecked(args.image_mode=='rgb')
ui.maxSpin1.setOpts(value=255, step=1)
ui.minSpin1.setOpts(value=0, step=1)
levelSpins = [ui.minSpin1, ui.maxSpin1, ui.minSpin2, ui.maxSpin2, ui.minSpin3, ui.maxSpin3]
if args.cuda and _has_cupy:
xp = cp
else:
xp = np
if args.levels is None:
ui.scaleCheck.setChecked(False)
ui.rgbLevelsCheck.setChecked(False)
else:
ui.scaleCheck.setChecked(True)
if len(args.levels) == 2:
ui.rgbLevelsCheck.setChecked(False)
ui.minSpin1.setValue(args.levels[0])
ui.maxSpin1.setValue(args.levels[1])
elif len(args.levels) == 6:
ui.rgbLevelsCheck.setChecked(True)
for spin,val in zip(levelSpins, args.levels):
spin.setValue(val)
else:
raise ValueError("levels argument must be 2 or 6 comma-separated values (got %r)" % (args.levels,))
ui.lutCheck.setChecked(args.lut)
ui.alphaCheck.setChecked(args.lut_alpha)
#ui.graphicsView.useOpenGL() ## buggy, but you can try it if you need extra speed.
@ -47,7 +97,8 @@ ui.graphicsView.setCentralItem(vb)
vb.setAspectLocked()
img = pg.ImageItem()
vb.addItem(img)
vb.setRange(QtCore.QRectF(0, 0, 512, 512))
LUT = None
def updateLUT():
@ -58,74 +109,94 @@ def updateLUT():
else:
n = 4096
LUT = ui.gradient.getLookupTable(n, alpha=ui.alphaCheck.isChecked())
if _has_cupy and xp == cp:
LUT = cp.asarray(LUT)
ui.gradient.sigGradientChanged.connect(updateLUT)
updateLUT()
ui.alphaCheck.toggled.connect(updateLUT)
def updateScale():
global ui
spins = [ui.minSpin1, ui.maxSpin1, ui.minSpin2, ui.maxSpin2, ui.minSpin3, ui.maxSpin3]
global ui, levelSpins
if ui.rgbLevelsCheck.isChecked():
for s in spins[2:]:
for s in levelSpins[2:]:
s.setEnabled(True)
else:
for s in spins[2:]:
for s in levelSpins[2:]:
s.setEnabled(False)
updateScale()
ui.rgbLevelsCheck.toggled.connect(updateScale)
cache = {}
def mkData():
with pg.BusyCursor():
global data, cache, ui
global data, cache, ui, xp
frames = ui.framesSpin.value()
width = ui.widthSpin.value()
height = ui.heightSpin.value()
dtype = (ui.dtypeCombo.currentText(), ui.rgbCheck.isChecked(), frames, width, height)
if dtype not in cache:
if dtype[0] == 'uint8':
dt = np.uint8
cacheKey = (ui.dtypeCombo.currentText(), ui.rgbCheck.isChecked(), frames, width, height)
if cacheKey not in cache:
if cacheKey[0] == 'uint8':
dt = xp.uint8
loc = 128
scale = 64
mx = 255
elif dtype[0] == 'uint16':
dt = np.uint16
elif cacheKey[0] == 'uint16':
dt = xp.uint16
loc = 4096
scale = 1024
mx = 2**16
elif dtype[0] == 'float':
dt = np.float
elif cacheKey[0] == 'float':
dt = xp.float
loc = 1.0
scale = 0.1
mx = 1.0
else:
raise ValueError(f"unable to handle dtype: {cacheKey[0]}")
if ui.rgbCheck.isChecked():
data = np.random.normal(size=(frames,width,height,3), loc=loc, scale=scale)
data = xp.random.normal(size=(frames,width,height,3), loc=loc, scale=scale)
data = pg.gaussianFilter(data, (0, 6, 6, 0))
else:
data = np.random.normal(size=(frames,width,height), loc=loc, scale=scale)
data = xp.random.normal(size=(frames,width,height), loc=loc, scale=scale)
data = pg.gaussianFilter(data, (0, 6, 6))
if dtype[0] != 'float':
data = np.clip(data, 0, mx)
if cacheKey[0] != 'float':
data = xp.clip(data, 0, mx)
data = data.astype(dt)
data[:, 10, 10:50] = mx
data[:, 9:12, 48] = mx
data[:, 8:13, 47] = mx
cache = {dtype: data} # clear to save memory (but keep one to prevent unnecessary regeneration)
data = cache[dtype]
cache = {cacheKey: data} # clear to save memory (but keep one to prevent unnecessary regeneration)
data = cache[cacheKey]
updateLUT()
updateSize()
def updateSize():
global ui
global ui, vb
frames = ui.framesSpin.value()
width = ui.widthSpin.value()
height = ui.heightSpin.value()
dtype = np.dtype(str(ui.dtypeCombo.currentText()))
dtype = xp.dtype(str(ui.dtypeCombo.currentText()))
rgb = 3 if ui.rgbCheck.isChecked() else 1
ui.sizeLabel.setText('%d MB' % (frames * width * height * rgb * dtype.itemsize / 1e6))
vb.setRange(QtCore.QRectF(0, 0, width, height))
def noticeCudaCheck():
global xp, cache
cache = {}
if ui.cudaCheck.isChecked():
if _has_cupy:
xp = cp
else:
xp = np
ui.cudaCheck.setChecked(False)
else:
xp = np
mkData()
mkData()
@ -139,7 +210,7 @@ ui.framesSpin.editingFinished.connect(mkData)
ui.widthSpin.valueChanged.connect(updateSize)
ui.heightSpin.valueChanged.connect(updateSize)
ui.framesSpin.valueChanged.connect(updateSize)
ui.cudaCheck.toggled.connect(noticeCudaCheck)
ptr = 0
@ -151,14 +222,14 @@ def update():
useLut = LUT
else:
useLut = None
downsample = ui.downsampleCheck.isChecked()
if ui.scaleCheck.isChecked():
if ui.rgbLevelsCheck.isChecked():
useScale = [
[ui.minSpin1.value(), ui.maxSpin1.value()],
[ui.minSpin2.value(), ui.maxSpin2.value()],
[ui.minSpin1.value(), ui.maxSpin1.value()],
[ui.minSpin2.value(), ui.maxSpin2.value()],
[ui.minSpin3.value(), ui.maxSpin3.value()]]
else:
useScale = [ui.minSpin1.value(), ui.maxSpin1.value()]
@ -175,7 +246,7 @@ def update():
img.setImage(data[ptr%data.shape[0]], autoLevels=False, levels=useScale, lut=useLut, autoDownsample=downsample)
ui.stack.setCurrentIndex(0)
#img.setImage(data[ptr%data.shape[0]], autoRange=False)
ptr += 1
now = ptime.time()
dt = now - lastTime
@ -190,7 +261,7 @@ def update():
timer = QtCore.QTimer()
timer.timeout.connect(update)
timer.start(0)
## Start Qt event loop unless running in interactive mode or using pyside.

View File

@ -15,6 +15,13 @@
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QGridLayout" name="gridLayout_2">
<item row="9" column="0" colspan="2">
<widget class="QCheckBox" name="cudaCheck">
<property name="text">
<string>Use CUDA (GPU) if available</string>
</property>
</widget>
</item>
<item row="8" column="0" colspan="2">
<widget class="QCheckBox" name="downsampleCheck">
<property name="text">

View File

@ -30,6 +30,9 @@ class Ui_MainWindow(object):
self.centralwidget.setObjectName(_fromUtf8("centralwidget"))
self.gridLayout_2 = QtGui.QGridLayout(self.centralwidget)
self.gridLayout_2.setObjectName(_fromUtf8("gridLayout_2"))
self.cudaCheck = QtGui.QCheckBox(self.centralwidget)
self.cudaCheck.setObjectName(_fromUtf8("cudaCheck"))
self.gridLayout_2.addWidget(self.cudaCheck, 9, 0, 1, 2)
self.downsampleCheck = QtGui.QCheckBox(self.centralwidget)
self.downsampleCheck.setObjectName(_fromUtf8("downsampleCheck"))
self.gridLayout_2.addWidget(self.downsampleCheck, 8, 0, 1, 2)
@ -189,6 +192,7 @@ class Ui_MainWindow(object):
def retranslateUi(self, MainWindow):
MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow", None))
self.cudaCheck.setText(_translate("MainWindow", "Use CUDA (GPU) if available", None))
self.downsampleCheck.setText(_translate("MainWindow", "Auto downsample", None))
self.scaleCheck.setText(_translate("MainWindow", "Scale Data", None))
self.rawRadio.setText(_translate("MainWindow", "RawImageWidget", None))

View File

@ -2,12 +2,15 @@
# Form implementation generated from reading ui file 'examples/VideoTemplate.ui'
#
# Created by: PyQt5 UI code generator 5.5.1
# Created by: PyQt5 UI code generator 5.15.1
#
# WARNING! All changes made in this file will be lost!
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
@ -16,6 +19,9 @@ class Ui_MainWindow(object):
self.centralwidget.setObjectName("centralwidget")
self.gridLayout_2 = QtWidgets.QGridLayout(self.centralwidget)
self.gridLayout_2.setObjectName("gridLayout_2")
self.cudaCheck = QtWidgets.QCheckBox(self.centralwidget)
self.cudaCheck.setObjectName("cudaCheck")
self.gridLayout_2.addWidget(self.cudaCheck, 9, 0, 1, 2)
self.downsampleCheck = QtWidgets.QCheckBox(self.centralwidget)
self.downsampleCheck.setObjectName("downsampleCheck")
self.gridLayout_2.addWidget(self.downsampleCheck, 8, 0, 1, 2)
@ -176,6 +182,7 @@ class Ui_MainWindow(object):
def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
self.cudaCheck.setText(_translate("MainWindow", "Use CUDA (GPU) if available"))
self.downsampleCheck.setText(_translate("MainWindow", "Auto downsample"))
self.scaleCheck.setText(_translate("MainWindow", "Scale Data"))
self.rawRadio.setText(_translate("MainWindow", "RawImageWidget"))
@ -194,6 +201,5 @@ class Ui_MainWindow(object):
self.fpsLabel.setText(_translate("MainWindow", "FPS"))
self.rgbCheck.setText(_translate("MainWindow", "RGB"))
self.label_5.setText(_translate("MainWindow", "Image size"))
from pyqtgraph import GradientWidget, GraphicsView, SpinBox
from pyqtgraph.widgets.RawImageWidget import RawImageWidget

View File

@ -17,6 +17,9 @@ class Ui_MainWindow(object):
self.centralwidget.setObjectName("centralwidget")
self.gridLayout_2 = QtGui.QGridLayout(self.centralwidget)
self.gridLayout_2.setObjectName("gridLayout_2")
self.cudaCheck = QtGui.QCheckBox(self.centralwidget)
self.cudaCheck.setObjectName("cudaCheck")
self.gridLayout_2.addWidget(self.cudaCheck, 9, 0, 1, 2)
self.downsampleCheck = QtGui.QCheckBox(self.centralwidget)
self.downsampleCheck.setObjectName("downsampleCheck")
self.gridLayout_2.addWidget(self.downsampleCheck, 8, 0, 1, 2)
@ -176,6 +179,7 @@ class Ui_MainWindow(object):
def retranslateUi(self, MainWindow):
MainWindow.setWindowTitle(QtGui.QApplication.translate("MainWindow", "MainWindow", None, QtGui.QApplication.UnicodeUTF8))
self.cudaCheck.setText(QtGui.QApplication.translate("MainWindow", "Use CUDA (GPU) if available", None, QtGui.QApplication.UnicodeUTF8))
self.downsampleCheck.setText(QtGui.QApplication.translate("MainWindow", "Auto downsample", None, QtGui.QApplication.UnicodeUTF8))
self.scaleCheck.setText(QtGui.QApplication.translate("MainWindow", "Scale Data", None, QtGui.QApplication.UnicodeUTF8))
self.rawRadio.setText(QtGui.QApplication.translate("MainWindow", "RawImageWidget", None, QtGui.QApplication.UnicodeUTF8))

View File

@ -1,207 +1,288 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'VideoTemplate.ui'
#
# Created: Sun Sep 18 19:22:41 2016
# by: pyside2-uic running on PySide2 2.0.0~alpha0
#
# WARNING! All changes made in this file will be lost!
################################################################################
## Form generated from reading UI file 'VideoTemplate.ui'
##
## Created by: Qt User Interface Compiler version 5.15.2
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
from PySide2.QtCore import *
from PySide2.QtGui import *
from PySide2.QtWidgets import *
from pyqtgraph import GraphicsView
from pyqtgraph.widgets.RawImageWidget import RawImageWidget
from pyqtgraph import GradientWidget
from pyqtgraph import SpinBox
from PySide2 import QtCore, QtGui, QtWidgets
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
if not MainWindow.objectName():
MainWindow.setObjectName(u"MainWindow")
MainWindow.resize(695, 798)
self.centralwidget = QtWidgets.QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")
self.gridLayout_2 = QtWidgets.QGridLayout(self.centralwidget)
self.gridLayout_2.setObjectName("gridLayout_2")
self.downsampleCheck = QtWidgets.QCheckBox(self.centralwidget)
self.downsampleCheck.setObjectName("downsampleCheck")
self.centralwidget = QWidget(MainWindow)
self.centralwidget.setObjectName(u"centralwidget")
self.gridLayout_2 = QGridLayout(self.centralwidget)
self.gridLayout_2.setObjectName(u"gridLayout_2")
self.cudaCheck = QCheckBox(self.centralwidget)
self.cudaCheck.setObjectName(u"cudaCheck")
self.gridLayout_2.addWidget(self.cudaCheck, 9, 0, 1, 2)
self.downsampleCheck = QCheckBox(self.centralwidget)
self.downsampleCheck.setObjectName(u"downsampleCheck")
self.gridLayout_2.addWidget(self.downsampleCheck, 8, 0, 1, 2)
self.scaleCheck = QtWidgets.QCheckBox(self.centralwidget)
self.scaleCheck.setObjectName("scaleCheck")
self.scaleCheck = QCheckBox(self.centralwidget)
self.scaleCheck.setObjectName(u"scaleCheck")
self.gridLayout_2.addWidget(self.scaleCheck, 4, 0, 1, 1)
self.gridLayout = QtWidgets.QGridLayout()
self.gridLayout.setObjectName("gridLayout")
self.rawRadio = QtWidgets.QRadioButton(self.centralwidget)
self.rawRadio.setObjectName("rawRadio")
self.gridLayout = QGridLayout()
self.gridLayout.setObjectName(u"gridLayout")
self.rawRadio = QRadioButton(self.centralwidget)
self.rawRadio.setObjectName(u"rawRadio")
self.gridLayout.addWidget(self.rawRadio, 3, 0, 1, 1)
self.gfxRadio = QtWidgets.QRadioButton(self.centralwidget)
self.gfxRadio = QRadioButton(self.centralwidget)
self.gfxRadio.setObjectName(u"gfxRadio")
self.gfxRadio.setChecked(True)
self.gfxRadio.setObjectName("gfxRadio")
self.gridLayout.addWidget(self.gfxRadio, 2, 0, 1, 1)
self.stack = QtWidgets.QStackedWidget(self.centralwidget)
self.stack.setObjectName("stack")
self.page = QtWidgets.QWidget()
self.page.setObjectName("page")
self.gridLayout_3 = QtWidgets.QGridLayout(self.page)
self.gridLayout_3.setObjectName("gridLayout_3")
self.stack = QStackedWidget(self.centralwidget)
self.stack.setObjectName(u"stack")
self.page = QWidget()
self.page.setObjectName(u"page")
self.gridLayout_3 = QGridLayout(self.page)
self.gridLayout_3.setObjectName(u"gridLayout_3")
self.graphicsView = GraphicsView(self.page)
self.graphicsView.setObjectName("graphicsView")
self.graphicsView.setObjectName(u"graphicsView")
self.gridLayout_3.addWidget(self.graphicsView, 0, 0, 1, 1)
self.stack.addWidget(self.page)
self.page_2 = QtWidgets.QWidget()
self.page_2.setObjectName("page_2")
self.gridLayout_4 = QtWidgets.QGridLayout(self.page_2)
self.gridLayout_4.setObjectName("gridLayout_4")
self.page_2 = QWidget()
self.page_2.setObjectName(u"page_2")
self.gridLayout_4 = QGridLayout(self.page_2)
self.gridLayout_4.setObjectName(u"gridLayout_4")
self.rawImg = RawImageWidget(self.page_2)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred)
self.rawImg.setObjectName(u"rawImg")
sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.rawImg.sizePolicy().hasHeightForWidth())
self.rawImg.setSizePolicy(sizePolicy)
self.rawImg.setObjectName("rawImg")
self.gridLayout_4.addWidget(self.rawImg, 0, 0, 1, 1)
self.stack.addWidget(self.page_2)
self.page_3 = QtWidgets.QWidget()
self.page_3.setObjectName("page_3")
self.gridLayout_5 = QtWidgets.QGridLayout(self.page_3)
self.gridLayout_5.setObjectName("gridLayout_5")
self.rawGLImg = RawImageGLWidget(self.page_3)
self.rawGLImg.setObjectName("rawGLImg")
self.gridLayout_5.addWidget(self.rawGLImg, 0, 0, 1, 1)
self.stack.addWidget(self.page_3)
self.gridLayout.addWidget(self.stack, 0, 0, 1, 1)
self.rawGLRadio = QtWidgets.QRadioButton(self.centralwidget)
self.rawGLRadio.setObjectName("rawGLRadio")
self.rawGLRadio = QRadioButton(self.centralwidget)
self.rawGLRadio.setObjectName(u"rawGLRadio")
self.gridLayout.addWidget(self.rawGLRadio, 4, 0, 1, 1)
self.gridLayout_2.addLayout(self.gridLayout, 1, 0, 1, 4)
self.dtypeCombo = QtWidgets.QComboBox(self.centralwidget)
self.dtypeCombo.setObjectName("dtypeCombo")
self.dtypeCombo = QComboBox(self.centralwidget)
self.dtypeCombo.addItem("")
self.dtypeCombo.addItem("")
self.dtypeCombo.addItem("")
self.dtypeCombo.setObjectName(u"dtypeCombo")
self.gridLayout_2.addWidget(self.dtypeCombo, 3, 2, 1, 1)
self.label = QtWidgets.QLabel(self.centralwidget)
self.label.setObjectName("label")
self.label = QLabel(self.centralwidget)
self.label.setObjectName(u"label")
self.gridLayout_2.addWidget(self.label, 3, 0, 1, 1)
self.rgbLevelsCheck = QtWidgets.QCheckBox(self.centralwidget)
self.rgbLevelsCheck.setObjectName("rgbLevelsCheck")
self.rgbLevelsCheck = QCheckBox(self.centralwidget)
self.rgbLevelsCheck.setObjectName(u"rgbLevelsCheck")
self.gridLayout_2.addWidget(self.rgbLevelsCheck, 4, 1, 1, 1)
self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
self.horizontalLayout_2.setObjectName("horizontalLayout_2")
self.horizontalLayout_2 = QHBoxLayout()
self.horizontalLayout_2.setObjectName(u"horizontalLayout_2")
self.minSpin2 = SpinBox(self.centralwidget)
self.minSpin2.setObjectName(u"minSpin2")
self.minSpin2.setEnabled(False)
self.minSpin2.setObjectName("minSpin2")
self.horizontalLayout_2.addWidget(self.minSpin2)
self.label_3 = QtWidgets.QLabel(self.centralwidget)
self.label_3.setAlignment(QtCore.Qt.AlignCenter)
self.label_3.setObjectName("label_3")
self.label_3 = QLabel(self.centralwidget)
self.label_3.setObjectName(u"label_3")
self.label_3.setAlignment(Qt.AlignCenter)
self.horizontalLayout_2.addWidget(self.label_3)
self.maxSpin2 = SpinBox(self.centralwidget)
self.maxSpin2.setObjectName(u"maxSpin2")
self.maxSpin2.setEnabled(False)
self.maxSpin2.setObjectName("maxSpin2")
self.horizontalLayout_2.addWidget(self.maxSpin2)
self.gridLayout_2.addLayout(self.horizontalLayout_2, 5, 2, 1, 1)
self.horizontalLayout = QtWidgets.QHBoxLayout()
self.horizontalLayout.setObjectName("horizontalLayout")
self.horizontalLayout = QHBoxLayout()
self.horizontalLayout.setObjectName(u"horizontalLayout")
self.minSpin1 = SpinBox(self.centralwidget)
self.minSpin1.setObjectName("minSpin1")
self.minSpin1.setObjectName(u"minSpin1")
self.horizontalLayout.addWidget(self.minSpin1)
self.label_2 = QtWidgets.QLabel(self.centralwidget)
self.label_2.setAlignment(QtCore.Qt.AlignCenter)
self.label_2.setObjectName("label_2")
self.label_2 = QLabel(self.centralwidget)
self.label_2.setObjectName(u"label_2")
self.label_2.setAlignment(Qt.AlignCenter)
self.horizontalLayout.addWidget(self.label_2)
self.maxSpin1 = SpinBox(self.centralwidget)
self.maxSpin1.setObjectName("maxSpin1")
self.maxSpin1.setObjectName(u"maxSpin1")
self.horizontalLayout.addWidget(self.maxSpin1)
self.gridLayout_2.addLayout(self.horizontalLayout, 4, 2, 1, 1)
self.horizontalLayout_3 = QtWidgets.QHBoxLayout()
self.horizontalLayout_3.setObjectName("horizontalLayout_3")
self.horizontalLayout_3 = QHBoxLayout()
self.horizontalLayout_3.setObjectName(u"horizontalLayout_3")
self.minSpin3 = SpinBox(self.centralwidget)
self.minSpin3.setObjectName(u"minSpin3")
self.minSpin3.setEnabled(False)
self.minSpin3.setObjectName("minSpin3")
self.horizontalLayout_3.addWidget(self.minSpin3)
self.label_4 = QtWidgets.QLabel(self.centralwidget)
self.label_4.setAlignment(QtCore.Qt.AlignCenter)
self.label_4.setObjectName("label_4")
self.label_4 = QLabel(self.centralwidget)
self.label_4.setObjectName(u"label_4")
self.label_4.setAlignment(Qt.AlignCenter)
self.horizontalLayout_3.addWidget(self.label_4)
self.maxSpin3 = SpinBox(self.centralwidget)
self.maxSpin3.setObjectName(u"maxSpin3")
self.maxSpin3.setEnabled(False)
self.maxSpin3.setObjectName("maxSpin3")
self.horizontalLayout_3.addWidget(self.maxSpin3)
self.gridLayout_2.addLayout(self.horizontalLayout_3, 6, 2, 1, 1)
self.lutCheck = QtWidgets.QCheckBox(self.centralwidget)
self.lutCheck.setObjectName("lutCheck")
self.lutCheck = QCheckBox(self.centralwidget)
self.lutCheck.setObjectName(u"lutCheck")
self.gridLayout_2.addWidget(self.lutCheck, 7, 0, 1, 1)
self.alphaCheck = QtWidgets.QCheckBox(self.centralwidget)
self.alphaCheck.setObjectName("alphaCheck")
self.alphaCheck = QCheckBox(self.centralwidget)
self.alphaCheck.setObjectName(u"alphaCheck")
self.gridLayout_2.addWidget(self.alphaCheck, 7, 1, 1, 1)
self.gradient = GradientWidget(self.centralwidget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
self.gradient.setObjectName(u"gradient")
sizePolicy.setHeightForWidth(self.gradient.sizePolicy().hasHeightForWidth())
self.gradient.setSizePolicy(sizePolicy)
self.gradient.setObjectName("gradient")
self.gridLayout_2.addWidget(self.gradient, 7, 2, 1, 2)
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.gridLayout_2.addItem(spacerItem, 3, 3, 1, 1)
self.fpsLabel = QtWidgets.QLabel(self.centralwidget)
font = QtGui.QFont()
self.horizontalSpacer = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
self.gridLayout_2.addItem(self.horizontalSpacer, 3, 3, 1, 1)
self.fpsLabel = QLabel(self.centralwidget)
self.fpsLabel.setObjectName(u"fpsLabel")
font = QFont()
font.setPointSize(12)
self.fpsLabel.setFont(font)
self.fpsLabel.setAlignment(QtCore.Qt.AlignCenter)
self.fpsLabel.setObjectName("fpsLabel")
self.fpsLabel.setAlignment(Qt.AlignCenter)
self.gridLayout_2.addWidget(self.fpsLabel, 0, 0, 1, 4)
self.rgbCheck = QtWidgets.QCheckBox(self.centralwidget)
self.rgbCheck.setObjectName("rgbCheck")
self.rgbCheck = QCheckBox(self.centralwidget)
self.rgbCheck.setObjectName(u"rgbCheck")
self.gridLayout_2.addWidget(self.rgbCheck, 3, 1, 1, 1)
self.label_5 = QtWidgets.QLabel(self.centralwidget)
self.label_5.setObjectName("label_5")
self.label_5 = QLabel(self.centralwidget)
self.label_5.setObjectName(u"label_5")
self.gridLayout_2.addWidget(self.label_5, 2, 0, 1, 1)
self.horizontalLayout_4 = QtWidgets.QHBoxLayout()
self.horizontalLayout_4.setObjectName("horizontalLayout_4")
self.framesSpin = QtWidgets.QSpinBox(self.centralwidget)
self.framesSpin.setButtonSymbols(QtWidgets.QAbstractSpinBox.NoButtons)
self.framesSpin.setProperty("value", 10)
self.framesSpin.setObjectName("framesSpin")
self.horizontalLayout_4 = QHBoxLayout()
self.horizontalLayout_4.setObjectName(u"horizontalLayout_4")
self.framesSpin = QSpinBox(self.centralwidget)
self.framesSpin.setObjectName(u"framesSpin")
self.framesSpin.setButtonSymbols(QAbstractSpinBox.NoButtons)
self.framesSpin.setValue(10)
self.horizontalLayout_4.addWidget(self.framesSpin)
self.widthSpin = QtWidgets.QSpinBox(self.centralwidget)
self.widthSpin.setButtonSymbols(QtWidgets.QAbstractSpinBox.PlusMinus)
self.widthSpin = QSpinBox(self.centralwidget)
self.widthSpin.setObjectName(u"widthSpin")
self.widthSpin.setButtonSymbols(QAbstractSpinBox.PlusMinus)
self.widthSpin.setMaximum(10000)
self.widthSpin.setProperty("value", 512)
self.widthSpin.setObjectName("widthSpin")
self.widthSpin.setValue(512)
self.horizontalLayout_4.addWidget(self.widthSpin)
self.heightSpin = QtWidgets.QSpinBox(self.centralwidget)
self.heightSpin.setButtonSymbols(QtWidgets.QAbstractSpinBox.NoButtons)
self.heightSpin = QSpinBox(self.centralwidget)
self.heightSpin.setObjectName(u"heightSpin")
self.heightSpin.setButtonSymbols(QAbstractSpinBox.NoButtons)
self.heightSpin.setMaximum(10000)
self.heightSpin.setProperty("value", 512)
self.heightSpin.setObjectName("heightSpin")
self.heightSpin.setValue(512)
self.horizontalLayout_4.addWidget(self.heightSpin)
self.gridLayout_2.addLayout(self.horizontalLayout_4, 2, 1, 1, 2)
self.sizeLabel = QtWidgets.QLabel(self.centralwidget)
self.sizeLabel.setText("")
self.sizeLabel.setObjectName("sizeLabel")
self.sizeLabel = QLabel(self.centralwidget)
self.sizeLabel.setObjectName(u"sizeLabel")
self.gridLayout_2.addWidget(self.sizeLabel, 2, 3, 1, 1)
MainWindow.setCentralWidget(self.centralwidget)
self.retranslateUi(MainWindow)
self.stack.setCurrentIndex(2)
QtCore.QMetaObject.connectSlotsByName(MainWindow)
self.stack.setCurrentIndex(1)
QMetaObject.connectSlotsByName(MainWindow)
# setupUi
def retranslateUi(self, MainWindow):
MainWindow.setWindowTitle(QtWidgets.QApplication.translate("MainWindow", "MainWindow", None, -1))
self.downsampleCheck.setText(QtWidgets.QApplication.translate("MainWindow", "Auto downsample", None, -1))
self.scaleCheck.setText(QtWidgets.QApplication.translate("MainWindow", "Scale Data", None, -1))
self.rawRadio.setText(QtWidgets.QApplication.translate("MainWindow", "RawImageWidget", None, -1))
self.gfxRadio.setText(QtWidgets.QApplication.translate("MainWindow", "GraphicsView + ImageItem", None, -1))
self.rawGLRadio.setText(QtWidgets.QApplication.translate("MainWindow", "RawGLImageWidget", None, -1))
self.dtypeCombo.setItemText(0, QtWidgets.QApplication.translate("MainWindow", "uint8", None, -1))
self.dtypeCombo.setItemText(1, QtWidgets.QApplication.translate("MainWindow", "uint16", None, -1))
self.dtypeCombo.setItemText(2, QtWidgets.QApplication.translate("MainWindow", "float", None, -1))
self.label.setText(QtWidgets.QApplication.translate("MainWindow", "Data type", None, -1))
self.rgbLevelsCheck.setText(QtWidgets.QApplication.translate("MainWindow", "RGB", None, -1))
self.label_3.setText(QtWidgets.QApplication.translate("MainWindow", "<--->", None, -1))
self.label_2.setText(QtWidgets.QApplication.translate("MainWindow", "<--->", None, -1))
self.label_4.setText(QtWidgets.QApplication.translate("MainWindow", "<--->", None, -1))
self.lutCheck.setText(QtWidgets.QApplication.translate("MainWindow", "Use Lookup Table", None, -1))
self.alphaCheck.setText(QtWidgets.QApplication.translate("MainWindow", "alpha", None, -1))
self.fpsLabel.setText(QtWidgets.QApplication.translate("MainWindow", "FPS", None, -1))
self.rgbCheck.setText(QtWidgets.QApplication.translate("MainWindow", "RGB", None, -1))
self.label_5.setText(QtWidgets.QApplication.translate("MainWindow", "Image size", None, -1))
MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"MainWindow", None))
self.cudaCheck.setText(QCoreApplication.translate("MainWindow", u"Use CUDA (GPU) if available", None))
self.downsampleCheck.setText(QCoreApplication.translate("MainWindow", u"Auto downsample", None))
self.scaleCheck.setText(QCoreApplication.translate("MainWindow", u"Scale Data", None))
self.rawRadio.setText(QCoreApplication.translate("MainWindow", u"RawImageWidget", None))
self.gfxRadio.setText(QCoreApplication.translate("MainWindow", u"GraphicsView + ImageItem", None))
self.rawGLRadio.setText(QCoreApplication.translate("MainWindow", u"RawGLImageWidget", None))
self.dtypeCombo.setItemText(0, QCoreApplication.translate("MainWindow", u"uint8", None))
self.dtypeCombo.setItemText(1, QCoreApplication.translate("MainWindow", u"uint16", None))
self.dtypeCombo.setItemText(2, QCoreApplication.translate("MainWindow", u"float", None))
self.label.setText(QCoreApplication.translate("MainWindow", u"Data type", None))
self.rgbLevelsCheck.setText(QCoreApplication.translate("MainWindow", u"RGB", None))
self.label_3.setText(QCoreApplication.translate("MainWindow", u"<--->", None))
self.label_2.setText(QCoreApplication.translate("MainWindow", u"<--->", None))
self.label_4.setText(QCoreApplication.translate("MainWindow", u"<--->", None))
self.lutCheck.setText(QCoreApplication.translate("MainWindow", u"Use Lookup Table", None))
self.alphaCheck.setText(QCoreApplication.translate("MainWindow", u"alpha", None))
self.fpsLabel.setText(QCoreApplication.translate("MainWindow", u"FPS", None))
self.rgbCheck.setText(QCoreApplication.translate("MainWindow", u"RGB", None))
self.label_5.setText(QCoreApplication.translate("MainWindow", u"Image size", None))
self.sizeLabel.setText("")
# retranslateUi
from pyqtgraph.widgets.RawImageWidget import RawImageGLWidget, RawImageWidget
from pyqtgraph import GradientWidget, SpinBox, GraphicsView

View File

@ -54,6 +54,10 @@ installedFrontends = sorted([
exceptionCondition = namedtuple("exceptionCondition", ["condition", "reason"])
conditionalExamples = {
"test_ExampleApp.py": exceptionCondition(
not(platform.system() == "Linux" and frontends[Qt.PYSIDE2]),
reason="Unexplained, intermittent segfault and subsequent timeout on CI"
),
"hdf5.py": exceptionCondition(
False,
reason="Example requires user interaction"

View File

@ -61,6 +61,7 @@ CONFIG_OPTIONS = {
# For 'col-major', image data is expected in reversed (col, row) order.
# The default is 'col-major' for backward compatibility, but this may
# change in the future.
'useCupy': False, # When True, attempt to use cupy ( currently only with ImageItem and related functions )
}

View File

@ -6,18 +6,22 @@ Distributed under MIT/X11 license. See license.txt for more information.
"""
from __future__ import division
import warnings
import numpy as np
import decimal, re
import ctypes
import sys, struct
import decimal
import re
import struct
import sys
import warnings
import numpy as np
from pyqtgraph.util.cupy_helper import getCupy
from . import debug, reload
from .Qt import QtGui, QtCore, QT_LIB, QtVersion
from .metaarray import MetaArray
from .pgcollections import OrderedDict
from .python2_3 import asUnicode, basestring
from .Qt import QtGui, QtCore, QT_LIB, QtVersion
from . import getConfigOption, setConfigOptions
from . import debug, reload
from .metaarray import MetaArray
Colors = {
'b': QtGui.QColor(0,0,255,255),
@ -940,80 +944,53 @@ def rescaleData(data, scale, offset, dtype=None, clip=None):
The scaling operation is::
data => (data-offset) * scale
"""
if dtype is None:
dtype = data.dtype
else:
dtype = np.dtype(dtype)
try:
if not getConfigOption('useWeave'):
raise Exception('Weave is disabled; falling back to slower version.')
try:
import scipy.weave
except ImportError:
raise Exception('scipy.weave is not importable; falling back to slower version.')
## require native dtype when using weave
if not data.dtype.isnative:
data = data.astype(data.dtype.newbyteorder('='))
if not dtype.isnative:
weaveDtype = dtype.newbyteorder('=')
d2 = data.astype(np.float) - float(offset)
d2 *= scale
# Clip before converting dtype to avoid overflow
if dtype.kind in 'ui':
lim = np.iinfo(dtype)
if clip is None:
# don't let rescale cause integer overflow
d2 = np.clip(d2, lim.min, lim.max)
else:
weaveDtype = dtype
newData = np.empty((data.size,), dtype=weaveDtype)
flat = np.ascontiguousarray(data).reshape(data.size)
size = data.size
code = """
double sc = (double)scale;
double off = (double)offset;
for( int i=0; i<size; i++ ) {
newData[i] = ((double)flat[i] - off) * sc;
}
"""
scipy.weave.inline(code, ['flat', 'newData', 'size', 'offset', 'scale'], compiler='gcc')
if dtype != weaveDtype:
newData = newData.astype(dtype)
data = newData.reshape(data.shape)
except:
if getConfigOption('useWeave'):
if getConfigOption('weaveDebug'):
debug.printExc("Error; disabling weave.")
setConfigOptions(useWeave=False)
#p = np.poly1d([scale, -offset*scale])
#d2 = p(data)
d2 = data - float(offset)
d2 *= scale
# Clip before converting dtype to avoid overflow
if dtype.kind in 'ui':
lim = np.iinfo(dtype)
if clip is None:
# don't let rescale cause integer overflow
d2 = np.clip(d2, lim.min, lim.max)
else:
d2 = np.clip(d2, max(clip[0], lim.min), min(clip[1], lim.max))
else:
if clip is not None:
d2 = np.clip(d2, *clip)
data = d2.astype(dtype)
d2 = np.clip(d2, max(clip[0], lim.min), min(clip[1], lim.max))
else:
if clip is not None:
d2 = np.clip(d2, *clip)
data = d2.astype(dtype)
return data
def applyLookupTable(data, lut):
"""
Uses values in *data* as indexes to select values from *lut*.
The returned data has shape data.shape + lut.shape[1:]
Note: color gradient lookup tables can be generated using GradientWidget.
Parameters
----------
data : ndarray
lut : ndarray
Either cupy or numpy arrays are accepted, though this function has only
consistently behaved correctly on windows with cuda toolkit version >= 11.1.
"""
if data.dtype.kind not in ('i', 'u'):
data = data.astype(int)
return np.take(lut, data, axis=0, mode='clip')
cp = getCupy()
if cp and cp.get_array_module(data) == cp:
# cupy.take only supports "wrap" mode
return cp.take(lut, cp.clip(data, 0, lut.shape[0] - 1), axis=0)
else:
return np.take(lut, data, axis=0, mode='clip')
def makeRGBA(*args, **kwds):
@ -1022,7 +999,7 @@ def makeRGBA(*args, **kwds):
return makeARGB(*args, **kwds)
def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False, output=None):
"""
Convert an array of values into an ARGB array suitable for building QImages,
OpenGL textures, etc.
@ -1062,29 +1039,31 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
is BGRA).
============== ==================================================================================
"""
cp = getCupy()
xp = cp.get_array_module(data) if cp else np
profile = debug.Profiler()
if data.ndim not in (2, 3):
raise TypeError("data must be 2D or 3D")
if data.ndim == 3 and data.shape[2] > 4:
raise TypeError("data.shape[2] must be <= 4")
if lut is not None and not isinstance(lut, np.ndarray):
lut = np.array(lut)
if lut is not None and not isinstance(lut, xp.ndarray):
lut = xp.array(lut)
if levels is None:
# automatically decide levels based on data dtype
if data.dtype.kind == 'u':
levels = np.array([0, 2**(data.itemsize*8)-1])
levels = xp.array([0, 2**(data.itemsize*8)-1])
elif data.dtype.kind == 'i':
s = 2**(data.itemsize*8 - 1)
levels = np.array([-s, s-1])
levels = xp.array([-s, s-1])
elif data.dtype.kind == 'b':
levels = np.array([0,1])
levels = xp.array([0,1])
else:
raise Exception('levels argument is required for float input types')
if not isinstance(levels, np.ndarray):
levels = np.array(levels)
levels = levels.astype(np.float)
if not isinstance(levels, xp.ndarray):
levels = xp.array(levels)
levels = levels.astype(xp.float)
if levels.ndim == 1:
if levels.shape[0] != 2:
raise Exception('levels argument must have length 2')
@ -1096,7 +1075,7 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
else:
raise Exception("levels argument must be 1D or 2D (got shape=%s)." % repr(levels.shape))
profile()
profile('check inputs')
# Decide on maximum scaled value
if scale is None:
@ -1107,28 +1086,28 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
# Decide on the dtype we want after scaling
if lut is None:
dtype = np.ubyte
dtype = xp.ubyte
else:
dtype = np.min_scalar_type(lut.shape[0]-1)
dtype = xp.min_scalar_type(lut.shape[0]-1)
# awkward, but fastest numpy native nan evaluation
#
nanMask = None
if data.dtype.kind == 'f' and np.isnan(data.min()):
nanMask = np.isnan(data)
if data.dtype.kind == 'f' and xp.isnan(data.min()):
nanMask = xp.isnan(data)
if data.ndim > 2:
nanMask = np.any(nanMask, axis=-1)
nanMask = xp.any(nanMask, axis=-1)
# Apply levels if given
if levels is not None:
if isinstance(levels, np.ndarray) and levels.ndim == 2:
if isinstance(levels, xp.ndarray) and levels.ndim == 2:
# we are going to rescale each channel independently
if levels.shape[0] != data.shape[-1]:
raise Exception("When rescaling multi-channel data, there must be the same number of levels as channels (data.shape[-1] == levels.shape[0])")
newData = np.empty(data.shape, dtype=int)
newData = xp.empty(data.shape, dtype=int)
for i in range(data.shape[-1]):
minVal, maxVal = levels[i]
if minVal == maxVal:
maxVal = np.nextafter(maxVal, 2*maxVal)
maxVal = xp.nextafter(maxVal, 2*maxVal)
rng = maxVal-minVal
rng = 1 if rng == 0 else rng
newData[...,i] = rescaleData(data[...,i], scale / rng, minVal, dtype=dtype)
@ -1138,25 +1117,29 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
minVal, maxVal = levels
if minVal != 0 or maxVal != scale:
if minVal == maxVal:
maxVal = np.nextafter(maxVal, 2*maxVal)
maxVal = xp.nextafter(maxVal, 2*maxVal)
rng = maxVal-minVal
rng = 1 if rng == 0 else rng
data = rescaleData(data, scale/rng, minVal, dtype=dtype)
profile()
profile('apply levels')
# apply LUT if given
if lut is not None:
data = applyLookupTable(data, lut)
else:
if data.dtype is not np.ubyte:
data = np.clip(data, 0, 255).astype(np.ubyte)
if data.dtype is not xp.ubyte:
data = xp.clip(data, 0, 255).astype(xp.ubyte)
profile()
profile('apply lut')
# this will be the final image array
imgData = np.empty(data.shape[:2]+(4,), dtype=np.ubyte)
if output is None:
imgData = xp.empty(data.shape[:2]+(4,), dtype=xp.ubyte)
else:
imgData = output
profile()
profile('allocate')
# decide channel order
if useRGBA:
@ -1167,7 +1150,7 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
# copy data into image array
if data.ndim == 2:
# This is tempting:
# imgData[..., :3] = data[..., np.newaxis]
# imgData[..., :3] = data[..., xp.newaxis]
# ..but it turns out this is faster:
for i in range(3):
imgData[..., i] = data
@ -1178,7 +1161,7 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
for i in range(0, data.shape[2]):
imgData[..., i] = data[..., order[i]]
profile()
profile('reorder channels')
# add opaque alpha channel if needed
if data.ndim == 2 or data.shape[2] == 3:
@ -1192,7 +1175,7 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
alpha = True
imgData[nanMask, 3] = 0
profile()
profile('alpha channel')
return imgData, alpha
@ -1364,7 +1347,9 @@ def gaussianFilter(data, sigma):
(note: results are only approximately equal to the output of
gaussian_filter)
"""
if np.isscalar(sigma):
cp = getCupy()
xp = cp.get_array_module(data) if cp else np
if xp.isscalar(sigma):
sigma = (sigma,) * data.ndim
baseline = data.mean()
@ -1376,17 +1361,17 @@ def gaussianFilter(data, sigma):
# generate 1D gaussian kernel
ksize = int(s * 6)
x = np.arange(-ksize, ksize)
kernel = np.exp(-x**2 / (2*s**2))
x = xp.arange(-ksize, ksize)
kernel = xp.exp(-x**2 / (2*s**2))
kshape = [1,] * data.ndim
kshape[ax] = len(kernel)
kernel = kernel.reshape(kshape)
# convolve as product of FFTs
shape = data.shape[ax] + ksize
scale = 1.0 / (abs(s) * (2*np.pi)**0.5)
filtered = scale * np.fft.irfft(np.fft.rfft(filtered, shape, axis=ax) *
np.fft.rfft(kernel, shape, axis=ax),
scale = 1.0 / (abs(s) * (2*xp.pi)**0.5)
filtered = scale * xp.fft.irfft(xp.fft.rfft(filtered, shape, axis=ax) *
xp.fft.rfft(kernel, shape, axis=ax),
axis=ax)
# clip off extra data

View File

@ -1,13 +1,15 @@
# -*- coding: utf-8 -*-
from __future__ import division
from ..Qt import QtGui, QtCore
import numpy as np
from .. import functions as fn
from .. import debug as debug
import numpy
from .GraphicsObject import GraphicsObject
from ..Point import Point
from .. import debug as debug
from .. import functions as fn
from .. import getConfigOption
from ..Point import Point
from ..Qt import QtGui, QtCore
from ..util.cupy_helper import getCupy
try:
from collections.abc import Callable
@ -53,6 +55,13 @@ class ImageItem(GraphicsObject):
self.lut = None
self.autoDownsample = False
self._lastDownsample = (1, 1)
self._processingBuffer = None
self._displayBuffer = None
self._renderRequired = True
self._unrenderable = False
self._cupy = getCupy()
self._xp = None # either numpy or cupy, to match the image data
self._defferedLevels = None
self.axisOrder = getConfigOption('imageAxisOrder')
@ -124,13 +133,16 @@ class ImageItem(GraphicsObject):
Only the first format is compatible with lookup tables. See :func:`makeARGB <pyqtgraph.makeARGB>`
for more details on how levels are applied.
"""
if levels is not None:
levels = np.asarray(levels)
if not fn.eq(levels, self.levels):
if self._xp is None:
self.levels = levels
self._effectiveLut = None
if update:
self.updateImage()
self._defferedLevels = levels
return
if levels is not None:
levels = self._xp.asarray(levels)
self.levels = levels
self._effectiveLut = None
if update:
self.updateImage()
def getLevels(self):
return self.levels
@ -159,7 +171,7 @@ class ImageItem(GraphicsObject):
Added in version 0.9.9
"""
self.autoDownsample = ads
self.qimage = None
self._renderRequired = True
self.update()
def setOpts(self, update=True, **kargs):
@ -200,6 +212,14 @@ class ImageItem(GraphicsObject):
self.informViewBoundsChanged()
self.update()
def _buildQImageBuffer(self, shape):
self._displayBuffer = numpy.empty(shape[:2] + (4,), dtype=numpy.ubyte)
if self._xp == self._cupy:
self._processingBuffer = self._xp.empty(shape[:2] + (4,), dtype=self._xp.ubyte)
else:
self._processingBuffer = self._displayBuffer
self.qimage = fn.makeQImage(self._displayBuffer, transpose=False, copy=False)
def setImage(self, image=None, autoLevels=None, **kargs):
"""
Update the image displayed by this item. For more information on how the image
@ -250,9 +270,14 @@ class ImageItem(GraphicsObject):
if self.image is None:
return
else:
old_xp = self._xp
self._xp = self._cupy.get_array_module(image) if self._cupy else numpy
gotNewData = True
shapeChanged = (self.image is None or image.shape != self.image.shape)
image = image.view(np.ndarray)
processingSubstrateChanged = old_xp != self._xp
if processingSubstrateChanged:
self._processingBuffer = None
shapeChanged = (processingSubstrateChanged or self.image is None or image.shape != self.image.shape)
image = image.view()
if self.image is None or image.dtype != self.image.dtype:
self._effectiveLut = None
self.image = image
@ -274,9 +299,9 @@ class ImageItem(GraphicsObject):
img = self.image
while img.size > 2**16:
img = img[::2, ::2]
mn, mx = np.nanmin(img), np.nanmax(img)
mn, mx = self._xp.nanmin(img), self._xp.nanmax(img)
# mn and mx can still be NaN if the data is all-NaN
if mn == mx or np.isnan(mn) or np.isnan(mx):
if mn == mx or self._xp.isnan(mn) or self._xp.isnan(mx):
mn = 0
mx = 255
kargs['levels'] = [mn,mx]
@ -287,13 +312,17 @@ class ImageItem(GraphicsObject):
profile()
self.qimage = None
self._renderRequired = True
self.update()
profile()
if gotNewData:
self.sigImageChanged.emit()
if self._defferedLevels is not None:
levels = self._defferedLevels
self._defferedLevels = None
self.setLevels((levels))
def dataTransform(self):
"""Return the transform that maps from this image's input array to its
@ -337,11 +366,11 @@ class ImageItem(GraphicsObject):
"""
data = self.image
while data.size > targetSize:
ax = np.argmax(data.shape)
ax = self._xp.argmax(data.shape)
sl = [slice(None)] * data.ndim
sl[ax] = slice(None, None, 2)
data = data[sl]
return np.nanmin(data), np.nanmax(data)
return self._xp.nanmin(data), self._xp.nanmax(data)
def updateImage(self, *args, **kargs):
## used for re-rendering qimage from self.image.
@ -356,8 +385,7 @@ class ImageItem(GraphicsObject):
def render(self):
# Convert data to QImage for display.
profile = debug.Profiler()
self._unrenderable = True
if self.image is None or self.image.size == 0:
return
@ -373,7 +401,6 @@ class ImageItem(GraphicsObject):
if self.autoDownsample:
xds, yds = self._computeDownsampleFactors()
if xds is None:
self.qimage = None
return
axes = [1, 0] if self.axisOrder == 'row-major' else [0, 1]
@ -390,18 +417,18 @@ class ImageItem(GraphicsObject):
# 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 levels.ndim == 1 and image.dtype in (np.ubyte, np.uint16):
if levels 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 = np.arange(eflsize)
ind = self._xp.arange(eflsize)
minlev, maxlev = levels
levdiff = maxlev - minlev
levdiff = 1 if levdiff == 0 else levdiff # don't allow division by 0
if lut is None:
efflut = fn.rescaleData(ind, scale=255./levdiff,
offset=minlev, dtype=np.ubyte)
offset=minlev, dtype=self._xp.ubyte)
else:
lutdtype = np.min_scalar_type(lut.shape[0]-1)
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]
@ -419,16 +446,22 @@ class ImageItem(GraphicsObject):
if self.axisOrder == 'col-major':
image = image.transpose((1, 0, 2)[:image.ndim])
argb, alpha = fn.makeARGB(image, lut=lut, levels=levels)
self.qimage = fn.makeQImage(argb, alpha, transpose=False)
if self._processingBuffer is None or self._processingBuffer.shape[:2] != image.shape[:2]:
self._buildQImageBuffer(image.shape)
fn.makeARGB(image, lut=lut, levels=levels, output=self._processingBuffer)
if self._xp == self._cupy:
self._processingBuffer.get(out=self._displayBuffer)
self._renderRequired = False
self._unrenderable = False
def paint(self, p, *args):
profile = debug.Profiler()
if self.image is None:
return
if self.qimage is None:
if self._renderRequired:
self.render()
if self.qimage is None:
if self._unrenderable:
return
profile('render QImage')
if self.paintMode is not None:
@ -444,7 +477,7 @@ class ImageItem(GraphicsObject):
def save(self, fileName, *args):
"""Save this image to file. Note that this saves the visible image (after scale/color changes), not the original data."""
if self.qimage is None:
if self._renderRequired:
self.render()
self.qimage.save(fileName, *args)
@ -458,7 +491,7 @@ class ImageItem(GraphicsObject):
dimensions roughly *targetImageSize* for each axis.
The *bins* argument and any extra keyword arguments are passed to
np.histogram(). If *bins* is 'auto', then a bin number is automatically
self.xp.histogram(). If *bins* is 'auto', then a bin number is automatically
chosen based on the image characteristics:
* Integer images will have approximately *targetHistogramSize* bins,
@ -473,33 +506,33 @@ class ImageItem(GraphicsObject):
if self.image is None or self.image.size == 0:
return None, None
if step == 'auto':
step = (max(1, int(np.ceil(self.image.shape[0] / targetImageSize))),
max(1, int(np.ceil(self.image.shape[1] / targetImageSize))))
if np.isscalar(step):
step = (max(1, int(self._xp.ceil(self.image.shape[0] / targetImageSize))),
max(1, int(self._xp.ceil(self.image.shape[1] / targetImageSize))))
if self._xp.isscalar(step):
step = (step, step)
stepData = self.image[::step[0], ::step[1]]
if isinstance(bins, str) and bins == 'auto':
mn = np.nanmin(stepData)
mx = np.nanmax(stepData)
mn = self._xp.nanmin(stepData).item()
mx = self._xp.nanmax(stepData).item()
if mx == mn:
# degenerate image, arange will fail
mx += 1
if np.isnan(mn) or np.isnan(mx):
if self._xp.isnan(mn) or self._xp.isnan(mx):
# the data are all-nan
return None, None
if stepData.dtype.kind in "ui":
# For integer data, we select the bins carefully to avoid aliasing
step = np.ceil((mx-mn) / 500.)
step = int(self._xp.ceil((mx - mn) / 500.))
bins = []
if step > 0.0:
bins = np.arange(mn, mx+1.01*step, step, dtype=np.int)
bins = self._xp.arange(mn, mx + 1.01 * step, step, dtype=self._xp.int)
else:
# for float data, let numpy select the bins.
bins = np.linspace(mn, mx, 500)
bins = self._xp.linspace(mn, mx, 500)
if len(bins) == 0:
bins = [mn, mx]
bins = self._xp.asarray((mn, mx))
kwds['bins'] = bins
@ -507,14 +540,20 @@ class ImageItem(GraphicsObject):
hist = []
for i in range(stepData.shape[-1]):
stepChan = stepData[..., i]
stepChan = stepChan[np.isfinite(stepChan)]
h = np.histogram(stepChan, **kwds)
hist.append((h[1][:-1], h[0]))
stepChan = stepChan[self._xp.isfinite(stepChan)]
h = self._xp.histogram(stepChan, **kwds)
if self._cupy:
hist.append((self._cupy.asnumpy(h[1][:-1]), self._cupy.asnumpy(h[0])))
else:
hist.append((h[1][:-1], h[0]))
return hist
else:
stepData = stepData[np.isfinite(stepData)]
hist = np.histogram(stepData, **kwds)
return hist[1][:-1], hist[0]
stepData = stepData[self._xp.isfinite(stepData)]
hist = self._xp.histogram(stepData, **kwds)
if self._cupy:
return self._cupy.asnumpy(hist[1][:-1]), self._cupy.asnumpy(hist[0])
else:
return hist[1][:-1], hist[0]
def setPxMode(self, b):
"""
@ -529,9 +568,9 @@ class ImageItem(GraphicsObject):
self.setPxMode(False)
def getPixmap(self):
if self.qimage is None:
if self._renderRequired:
self.render()
if self.qimage is None:
if self._unrenderable:
return None
return QtGui.QPixmap.fromImage(self.qimage)
@ -546,10 +585,11 @@ class ImageItem(GraphicsObject):
if self.autoDownsample:
xds, yds = self._computeDownsampleFactors()
if xds is None:
self.qimage = None
self._renderRequired = True
self._unrenderable = True
return
if (xds, yds) != self._lastDownsample:
self.qimage = None
self._renderRequired = True
self.update()
def _computeDownsampleFactors(self):

View File

@ -0,0 +1,18 @@
import os
from warnings import warn
from pyqtgraph import getConfigOption
def getCupy():
if getConfigOption("useCupy"):
try:
import cupy
except ImportError:
warn("cupy library could not be loaded, but 'useCupy' is set.")
return None
if os.name == "nt" and cupy.cuda.runtime.runtimeGetVersion() < 11000:
warn("In Windows, CUDA toolkit should be version 11 or higher, or some functions may misbehave.")
return cupy
else:
return None

View File

@ -5,6 +5,7 @@ Copyright 2010-2016 Luke Campagnola
Distributed under MIT/X11 license. See license.txt for more infomation.
"""
from .. import getConfigOption, functions as fn, getCupy
from ..Qt import QtCore, QtGui
try:
@ -17,8 +18,6 @@ except (ImportError, AttributeError):
# AttributeError upon import
HAVE_OPENGL = False
from .. import getConfigOption, functions as fn
class RawImageWidget(QtGui.QWidget):
"""
@ -37,6 +36,7 @@ class RawImageWidget(QtGui.QWidget):
self.scaled = scaled
self.opts = None
self.image = None
self._cp = getCupy()
def setImage(self, img, *args, **kargs):
"""
@ -52,6 +52,8 @@ class RawImageWidget(QtGui.QWidget):
return
if self.image is None:
argb, alpha = fn.makeARGB(self.opts[0], *self.opts[1], **self.opts[2])
if self._cp and self._cp.get_array_module(argb) == self._cp:
argb = argb.get() # transfer GPU data back to the CPU
self.image = fn.makeQImage(argb, alpha)
self.opts = ()
# if self.pixmap is None: