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+ * `pyopengl` on macOS Big Sur only works with python 3.9.1+
* `hdf5` for large hdf5 binary format support * `hdf5` for large hdf5 binary format support
* `colorcet` for supplemental colormaps * `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 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. 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 useOpenGL bool False Enable OpenGL in GraphicsView. This can have unpredictable effects on stability
and performance. 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). 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. 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 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 numpy as np
import pyqtgraph as pg import pyqtgraph as pg
import pyqtgraph.ptime as ptime import pyqtgraph.ptime as ptime
from pyqtgraph.Qt import QtGui, QtCore, QT_LIB
import importlib import importlib
ui_template = importlib.import_module(f'VideoTemplate_{QT_LIB.lower()}') 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') #QtGui.QApplication.setGraphicsSystem('raster')
app = QtGui.QApplication([]) app = QtGui.QApplication([])
@ -31,14 +49,46 @@ win.show()
try: try:
from pyqtgraph.widgets.RawImageWidget import RawImageGLWidget from pyqtgraph.widgets.RawImageWidget import RawImageGLWidget
except ImportError: except ImportError:
RawImageGLWidget = None
ui.rawGLRadio.setEnabled(False) ui.rawGLRadio.setEnabled(False)
ui.rawGLRadio.setText(ui.rawGLRadio.text() + " (OpenGL not available)") ui.rawGLRadio.setText(ui.rawGLRadio.text() + " (OpenGL not available)")
else: else:
ui.rawGLImg = RawImageGLWidget() ui.rawGLImg = RawImageGLWidget()
ui.stack.addWidget(ui.rawGLImg) 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.maxSpin1.setOpts(value=255, step=1)
ui.minSpin1.setOpts(value=0, 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. #ui.graphicsView.useOpenGL() ## buggy, but you can try it if you need extra speed.
@ -47,7 +97,8 @@ ui.graphicsView.setCentralItem(vb)
vb.setAspectLocked() vb.setAspectLocked()
img = pg.ImageItem() img = pg.ImageItem()
vb.addItem(img) vb.addItem(img)
vb.setRange(QtCore.QRectF(0, 0, 512, 512))
LUT = None LUT = None
def updateLUT(): def updateLUT():
@ -58,75 +109,95 @@ def updateLUT():
else: else:
n = 4096 n = 4096
LUT = ui.gradient.getLookupTable(n, alpha=ui.alphaCheck.isChecked()) LUT = ui.gradient.getLookupTable(n, alpha=ui.alphaCheck.isChecked())
if _has_cupy and xp == cp:
LUT = cp.asarray(LUT)
ui.gradient.sigGradientChanged.connect(updateLUT) ui.gradient.sigGradientChanged.connect(updateLUT)
updateLUT() updateLUT()
ui.alphaCheck.toggled.connect(updateLUT) ui.alphaCheck.toggled.connect(updateLUT)
def updateScale(): def updateScale():
global ui global ui, levelSpins
spins = [ui.minSpin1, ui.maxSpin1, ui.minSpin2, ui.maxSpin2, ui.minSpin3, ui.maxSpin3]
if ui.rgbLevelsCheck.isChecked(): if ui.rgbLevelsCheck.isChecked():
for s in spins[2:]: for s in levelSpins[2:]:
s.setEnabled(True) s.setEnabled(True)
else: else:
for s in spins[2:]: for s in levelSpins[2:]:
s.setEnabled(False) s.setEnabled(False)
updateScale()
ui.rgbLevelsCheck.toggled.connect(updateScale) ui.rgbLevelsCheck.toggled.connect(updateScale)
cache = {} cache = {}
def mkData(): def mkData():
with pg.BusyCursor(): with pg.BusyCursor():
global data, cache, ui global data, cache, ui, xp
frames = ui.framesSpin.value() frames = ui.framesSpin.value()
width = ui.widthSpin.value() width = ui.widthSpin.value()
height = ui.heightSpin.value() height = ui.heightSpin.value()
dtype = (ui.dtypeCombo.currentText(), ui.rgbCheck.isChecked(), frames, width, height) cacheKey = (ui.dtypeCombo.currentText(), ui.rgbCheck.isChecked(), frames, width, height)
if dtype not in cache: if cacheKey not in cache:
if dtype[0] == 'uint8': if cacheKey[0] == 'uint8':
dt = np.uint8 dt = xp.uint8
loc = 128 loc = 128
scale = 64 scale = 64
mx = 255 mx = 255
elif dtype[0] == 'uint16': elif cacheKey[0] == 'uint16':
dt = np.uint16 dt = xp.uint16
loc = 4096 loc = 4096
scale = 1024 scale = 1024
mx = 2**16 mx = 2**16
elif dtype[0] == 'float': elif cacheKey[0] == 'float':
dt = np.float dt = xp.float
loc = 1.0 loc = 1.0
scale = 0.1 scale = 0.1
mx = 1.0 mx = 1.0
else:
raise ValueError(f"unable to handle dtype: {cacheKey[0]}")
if ui.rgbCheck.isChecked(): 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)) data = pg.gaussianFilter(data, (0, 6, 6, 0))
else: 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)) data = pg.gaussianFilter(data, (0, 6, 6))
if dtype[0] != 'float': if cacheKey[0] != 'float':
data = np.clip(data, 0, mx) data = xp.clip(data, 0, mx)
data = data.astype(dt) data = data.astype(dt)
data[:, 10, 10:50] = mx data[:, 10, 10:50] = mx
data[:, 9:12, 48] = mx data[:, 9:12, 48] = mx
data[:, 8:13, 47] = mx data[:, 8:13, 47] = mx
cache = {dtype: data} # clear to save memory (but keep one to prevent unnecessary regeneration) cache = {cacheKey: data} # clear to save memory (but keep one to prevent unnecessary regeneration)
data = cache[dtype] data = cache[cacheKey]
updateLUT() updateLUT()
updateSize() updateSize()
def updateSize(): def updateSize():
global ui global ui, vb
frames = ui.framesSpin.value() frames = ui.framesSpin.value()
width = ui.widthSpin.value() width = ui.widthSpin.value()
height = ui.heightSpin.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 rgb = 3 if ui.rgbCheck.isChecked() else 1
ui.sizeLabel.setText('%d MB' % (frames * width * height * rgb * dtype.itemsize / 1e6)) 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() mkData()
@ -139,7 +210,7 @@ ui.framesSpin.editingFinished.connect(mkData)
ui.widthSpin.valueChanged.connect(updateSize) ui.widthSpin.valueChanged.connect(updateSize)
ui.heightSpin.valueChanged.connect(updateSize) ui.heightSpin.valueChanged.connect(updateSize)
ui.framesSpin.valueChanged.connect(updateSize) ui.framesSpin.valueChanged.connect(updateSize)
ui.cudaCheck.toggled.connect(noticeCudaCheck)
ptr = 0 ptr = 0

View File

@ -15,6 +15,13 @@
</property> </property>
<widget class="QWidget" name="centralwidget"> <widget class="QWidget" name="centralwidget">
<layout class="QGridLayout" name="gridLayout_2"> <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"> <item row="8" column="0" colspan="2">
<widget class="QCheckBox" name="downsampleCheck"> <widget class="QCheckBox" name="downsampleCheck">
<property name="text"> <property name="text">

View File

@ -30,6 +30,9 @@ class Ui_MainWindow(object):
self.centralwidget.setObjectName(_fromUtf8("centralwidget")) self.centralwidget.setObjectName(_fromUtf8("centralwidget"))
self.gridLayout_2 = QtGui.QGridLayout(self.centralwidget) self.gridLayout_2 = QtGui.QGridLayout(self.centralwidget)
self.gridLayout_2.setObjectName(_fromUtf8("gridLayout_2")) 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 = QtGui.QCheckBox(self.centralwidget)
self.downsampleCheck.setObjectName(_fromUtf8("downsampleCheck")) self.downsampleCheck.setObjectName(_fromUtf8("downsampleCheck"))
self.gridLayout_2.addWidget(self.downsampleCheck, 8, 0, 1, 2) self.gridLayout_2.addWidget(self.downsampleCheck, 8, 0, 1, 2)
@ -189,6 +192,7 @@ class Ui_MainWindow(object):
def retranslateUi(self, MainWindow): def retranslateUi(self, MainWindow):
MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow", None)) 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.downsampleCheck.setText(_translate("MainWindow", "Auto downsample", None))
self.scaleCheck.setText(_translate("MainWindow", "Scale Data", None)) self.scaleCheck.setText(_translate("MainWindow", "Scale Data", None))
self.rawRadio.setText(_translate("MainWindow", "RawImageWidget", None)) self.rawRadio.setText(_translate("MainWindow", "RawImageWidget", None))

View File

@ -2,12 +2,15 @@
# Form implementation generated from reading ui file 'examples/VideoTemplate.ui' # 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 from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_MainWindow(object): class Ui_MainWindow(object):
def setupUi(self, MainWindow): def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow") MainWindow.setObjectName("MainWindow")
@ -16,6 +19,9 @@ class Ui_MainWindow(object):
self.centralwidget.setObjectName("centralwidget") self.centralwidget.setObjectName("centralwidget")
self.gridLayout_2 = QtWidgets.QGridLayout(self.centralwidget) self.gridLayout_2 = QtWidgets.QGridLayout(self.centralwidget)
self.gridLayout_2.setObjectName("gridLayout_2") 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 = QtWidgets.QCheckBox(self.centralwidget)
self.downsampleCheck.setObjectName("downsampleCheck") self.downsampleCheck.setObjectName("downsampleCheck")
self.gridLayout_2.addWidget(self.downsampleCheck, 8, 0, 1, 2) self.gridLayout_2.addWidget(self.downsampleCheck, 8, 0, 1, 2)
@ -176,6 +182,7 @@ class Ui_MainWindow(object):
def retranslateUi(self, MainWindow): def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate _translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow")) MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
self.cudaCheck.setText(_translate("MainWindow", "Use CUDA (GPU) if available"))
self.downsampleCheck.setText(_translate("MainWindow", "Auto downsample")) self.downsampleCheck.setText(_translate("MainWindow", "Auto downsample"))
self.scaleCheck.setText(_translate("MainWindow", "Scale Data")) self.scaleCheck.setText(_translate("MainWindow", "Scale Data"))
self.rawRadio.setText(_translate("MainWindow", "RawImageWidget")) self.rawRadio.setText(_translate("MainWindow", "RawImageWidget"))
@ -194,6 +201,5 @@ class Ui_MainWindow(object):
self.fpsLabel.setText(_translate("MainWindow", "FPS")) self.fpsLabel.setText(_translate("MainWindow", "FPS"))
self.rgbCheck.setText(_translate("MainWindow", "RGB")) self.rgbCheck.setText(_translate("MainWindow", "RGB"))
self.label_5.setText(_translate("MainWindow", "Image size")) self.label_5.setText(_translate("MainWindow", "Image size"))
from pyqtgraph import GradientWidget, GraphicsView, SpinBox from pyqtgraph import GradientWidget, GraphicsView, SpinBox
from pyqtgraph.widgets.RawImageWidget import RawImageWidget from pyqtgraph.widgets.RawImageWidget import RawImageWidget

View File

@ -17,6 +17,9 @@ class Ui_MainWindow(object):
self.centralwidget.setObjectName("centralwidget") self.centralwidget.setObjectName("centralwidget")
self.gridLayout_2 = QtGui.QGridLayout(self.centralwidget) self.gridLayout_2 = QtGui.QGridLayout(self.centralwidget)
self.gridLayout_2.setObjectName("gridLayout_2") 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 = QtGui.QCheckBox(self.centralwidget)
self.downsampleCheck.setObjectName("downsampleCheck") self.downsampleCheck.setObjectName("downsampleCheck")
self.gridLayout_2.addWidget(self.downsampleCheck, 8, 0, 1, 2) self.gridLayout_2.addWidget(self.downsampleCheck, 8, 0, 1, 2)
@ -176,6 +179,7 @@ class Ui_MainWindow(object):
def retranslateUi(self, MainWindow): def retranslateUi(self, MainWindow):
MainWindow.setWindowTitle(QtGui.QApplication.translate("MainWindow", "MainWindow", None, QtGui.QApplication.UnicodeUTF8)) 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.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.scaleCheck.setText(QtGui.QApplication.translate("MainWindow", "Scale Data", None, QtGui.QApplication.UnicodeUTF8))
self.rawRadio.setText(QtGui.QApplication.translate("MainWindow", "RawImageWidget", 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 -*- # -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'VideoTemplate.ui' ################################################################################
# ## Form 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 ## Created by: Qt User Interface Compiler version 5.15.2
# ##
# WARNING! All changes made in this file will be lost! ## 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): class Ui_MainWindow(object):
def setupUi(self, MainWindow): def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow") if not MainWindow.objectName():
MainWindow.setObjectName(u"MainWindow")
MainWindow.resize(695, 798) MainWindow.resize(695, 798)
self.centralwidget = QtWidgets.QWidget(MainWindow) self.centralwidget = QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget") self.centralwidget.setObjectName(u"centralwidget")
self.gridLayout_2 = QtWidgets.QGridLayout(self.centralwidget) self.gridLayout_2 = QGridLayout(self.centralwidget)
self.gridLayout_2.setObjectName("gridLayout_2") self.gridLayout_2.setObjectName(u"gridLayout_2")
self.downsampleCheck = QtWidgets.QCheckBox(self.centralwidget) self.cudaCheck = QCheckBox(self.centralwidget)
self.downsampleCheck.setObjectName("downsampleCheck") 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.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_2.addWidget(self.scaleCheck, 4, 0, 1, 1)
self.gridLayout = QtWidgets.QGridLayout()
self.gridLayout.setObjectName("gridLayout") self.gridLayout = QGridLayout()
self.rawRadio = QtWidgets.QRadioButton(self.centralwidget) self.gridLayout.setObjectName(u"gridLayout")
self.rawRadio.setObjectName("rawRadio") self.rawRadio = QRadioButton(self.centralwidget)
self.rawRadio.setObjectName(u"rawRadio")
self.gridLayout.addWidget(self.rawRadio, 3, 0, 1, 1) 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.setChecked(True)
self.gfxRadio.setObjectName("gfxRadio")
self.gridLayout.addWidget(self.gfxRadio, 2, 0, 1, 1) self.gridLayout.addWidget(self.gfxRadio, 2, 0, 1, 1)
self.stack = QtWidgets.QStackedWidget(self.centralwidget)
self.stack.setObjectName("stack") self.stack = QStackedWidget(self.centralwidget)
self.page = QtWidgets.QWidget() self.stack.setObjectName(u"stack")
self.page.setObjectName("page") self.page = QWidget()
self.gridLayout_3 = QtWidgets.QGridLayout(self.page) self.page.setObjectName(u"page")
self.gridLayout_3.setObjectName("gridLayout_3") self.gridLayout_3 = QGridLayout(self.page)
self.gridLayout_3.setObjectName(u"gridLayout_3")
self.graphicsView = GraphicsView(self.page) 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.gridLayout_3.addWidget(self.graphicsView, 0, 0, 1, 1)
self.stack.addWidget(self.page) self.stack.addWidget(self.page)
self.page_2 = QtWidgets.QWidget() self.page_2 = QWidget()
self.page_2.setObjectName("page_2") self.page_2.setObjectName(u"page_2")
self.gridLayout_4 = QtWidgets.QGridLayout(self.page_2) self.gridLayout_4 = QGridLayout(self.page_2)
self.gridLayout_4.setObjectName("gridLayout_4") self.gridLayout_4.setObjectName(u"gridLayout_4")
self.rawImg = RawImageWidget(self.page_2) 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.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0) sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.rawImg.sizePolicy().hasHeightForWidth()) sizePolicy.setHeightForWidth(self.rawImg.sizePolicy().hasHeightForWidth())
self.rawImg.setSizePolicy(sizePolicy) self.rawImg.setSizePolicy(sizePolicy)
self.rawImg.setObjectName("rawImg")
self.gridLayout_4.addWidget(self.rawImg, 0, 0, 1, 1) self.gridLayout_4.addWidget(self.rawImg, 0, 0, 1, 1)
self.stack.addWidget(self.page_2) 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.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.addWidget(self.rawGLRadio, 4, 0, 1, 1)
self.gridLayout_2.addLayout(self.gridLayout, 1, 0, 1, 4) 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.addItem("")
self.dtypeCombo.addItem("") self.dtypeCombo.addItem("")
self.dtypeCombo.setObjectName(u"dtypeCombo")
self.gridLayout_2.addWidget(self.dtypeCombo, 3, 2, 1, 1) 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.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.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 = SpinBox(self.centralwidget)
self.minSpin2.setObjectName(u"minSpin2")
self.minSpin2.setEnabled(False) self.minSpin2.setEnabled(False)
self.minSpin2.setObjectName("minSpin2")
self.horizontalLayout_2.addWidget(self.minSpin2) self.horizontalLayout_2.addWidget(self.minSpin2)
self.label_3 = QtWidgets.QLabel(self.centralwidget)
self.label_3.setAlignment(QtCore.Qt.AlignCenter) self.label_3 = QLabel(self.centralwidget)
self.label_3.setObjectName("label_3") self.label_3.setObjectName(u"label_3")
self.label_3.setAlignment(Qt.AlignCenter)
self.horizontalLayout_2.addWidget(self.label_3) self.horizontalLayout_2.addWidget(self.label_3)
self.maxSpin2 = SpinBox(self.centralwidget) self.maxSpin2 = SpinBox(self.centralwidget)
self.maxSpin2.setObjectName(u"maxSpin2")
self.maxSpin2.setEnabled(False) self.maxSpin2.setEnabled(False)
self.maxSpin2.setObjectName("maxSpin2")
self.horizontalLayout_2.addWidget(self.maxSpin2) self.horizontalLayout_2.addWidget(self.maxSpin2)
self.gridLayout_2.addLayout(self.horizontalLayout_2, 5, 2, 1, 1) 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 = SpinBox(self.centralwidget)
self.minSpin1.setObjectName("minSpin1") self.minSpin1.setObjectName(u"minSpin1")
self.horizontalLayout.addWidget(self.minSpin1) self.horizontalLayout.addWidget(self.minSpin1)
self.label_2 = QtWidgets.QLabel(self.centralwidget)
self.label_2.setAlignment(QtCore.Qt.AlignCenter) self.label_2 = QLabel(self.centralwidget)
self.label_2.setObjectName("label_2") self.label_2.setObjectName(u"label_2")
self.label_2.setAlignment(Qt.AlignCenter)
self.horizontalLayout.addWidget(self.label_2) self.horizontalLayout.addWidget(self.label_2)
self.maxSpin1 = SpinBox(self.centralwidget) self.maxSpin1 = SpinBox(self.centralwidget)
self.maxSpin1.setObjectName("maxSpin1") self.maxSpin1.setObjectName(u"maxSpin1")
self.horizontalLayout.addWidget(self.maxSpin1) self.horizontalLayout.addWidget(self.maxSpin1)
self.gridLayout_2.addLayout(self.horizontalLayout, 4, 2, 1, 1) 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 = SpinBox(self.centralwidget)
self.minSpin3.setObjectName(u"minSpin3")
self.minSpin3.setEnabled(False) self.minSpin3.setEnabled(False)
self.minSpin3.setObjectName("minSpin3")
self.horizontalLayout_3.addWidget(self.minSpin3) self.horizontalLayout_3.addWidget(self.minSpin3)
self.label_4 = QtWidgets.QLabel(self.centralwidget)
self.label_4.setAlignment(QtCore.Qt.AlignCenter) self.label_4 = QLabel(self.centralwidget)
self.label_4.setObjectName("label_4") self.label_4.setObjectName(u"label_4")
self.label_4.setAlignment(Qt.AlignCenter)
self.horizontalLayout_3.addWidget(self.label_4) self.horizontalLayout_3.addWidget(self.label_4)
self.maxSpin3 = SpinBox(self.centralwidget) self.maxSpin3 = SpinBox(self.centralwidget)
self.maxSpin3.setObjectName(u"maxSpin3")
self.maxSpin3.setEnabled(False) self.maxSpin3.setEnabled(False)
self.maxSpin3.setObjectName("maxSpin3")
self.horizontalLayout_3.addWidget(self.maxSpin3) self.horizontalLayout_3.addWidget(self.maxSpin3)
self.gridLayout_2.addLayout(self.horizontalLayout_3, 6, 2, 1, 1) 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.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.gridLayout_2.addWidget(self.alphaCheck, 7, 1, 1, 1)
self.gradient = GradientWidget(self.centralwidget) self.gradient = GradientWidget(self.centralwidget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred) self.gradient.setObjectName(u"gradient")
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.gradient.sizePolicy().hasHeightForWidth()) sizePolicy.setHeightForWidth(self.gradient.sizePolicy().hasHeightForWidth())
self.gradient.setSizePolicy(sizePolicy) self.gradient.setSizePolicy(sizePolicy)
self.gradient.setObjectName("gradient")
self.gridLayout_2.addWidget(self.gradient, 7, 2, 1, 2) 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.horizontalSpacer = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
self.fpsLabel = QtWidgets.QLabel(self.centralwidget)
font = QtGui.QFont() 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) font.setPointSize(12)
self.fpsLabel.setFont(font) self.fpsLabel.setFont(font)
self.fpsLabel.setAlignment(QtCore.Qt.AlignCenter) self.fpsLabel.setAlignment(Qt.AlignCenter)
self.fpsLabel.setObjectName("fpsLabel")
self.gridLayout_2.addWidget(self.fpsLabel, 0, 0, 1, 4) 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.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.gridLayout_2.addWidget(self.label_5, 2, 0, 1, 1)
self.horizontalLayout_4 = QtWidgets.QHBoxLayout()
self.horizontalLayout_4.setObjectName("horizontalLayout_4") self.horizontalLayout_4 = QHBoxLayout()
self.framesSpin = QtWidgets.QSpinBox(self.centralwidget) self.horizontalLayout_4.setObjectName(u"horizontalLayout_4")
self.framesSpin.setButtonSymbols(QtWidgets.QAbstractSpinBox.NoButtons) self.framesSpin = QSpinBox(self.centralwidget)
self.framesSpin.setProperty("value", 10) self.framesSpin.setObjectName(u"framesSpin")
self.framesSpin.setObjectName("framesSpin") self.framesSpin.setButtonSymbols(QAbstractSpinBox.NoButtons)
self.framesSpin.setValue(10)
self.horizontalLayout_4.addWidget(self.framesSpin) 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.setMaximum(10000)
self.widthSpin.setProperty("value", 512) self.widthSpin.setValue(512)
self.widthSpin.setObjectName("widthSpin")
self.horizontalLayout_4.addWidget(self.widthSpin) 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.setMaximum(10000)
self.heightSpin.setProperty("value", 512) self.heightSpin.setValue(512)
self.heightSpin.setObjectName("heightSpin")
self.horizontalLayout_4.addWidget(self.heightSpin) self.horizontalLayout_4.addWidget(self.heightSpin)
self.gridLayout_2.addLayout(self.horizontalLayout_4, 2, 1, 1, 2) self.gridLayout_2.addLayout(self.horizontalLayout_4, 2, 1, 1, 2)
self.sizeLabel = QtWidgets.QLabel(self.centralwidget)
self.sizeLabel.setText("") self.sizeLabel = QLabel(self.centralwidget)
self.sizeLabel.setObjectName("sizeLabel") self.sizeLabel.setObjectName(u"sizeLabel")
self.gridLayout_2.addWidget(self.sizeLabel, 2, 3, 1, 1) self.gridLayout_2.addWidget(self.sizeLabel, 2, 3, 1, 1)
MainWindow.setCentralWidget(self.centralwidget) MainWindow.setCentralWidget(self.centralwidget)
self.retranslateUi(MainWindow) self.retranslateUi(MainWindow)
self.stack.setCurrentIndex(2)
QtCore.QMetaObject.connectSlotsByName(MainWindow) self.stack.setCurrentIndex(1)
QMetaObject.connectSlotsByName(MainWindow)
# setupUi
def retranslateUi(self, MainWindow): def retranslateUi(self, MainWindow):
MainWindow.setWindowTitle(QtWidgets.QApplication.translate("MainWindow", "MainWindow", None, -1)) MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"MainWindow", None))
self.downsampleCheck.setText(QtWidgets.QApplication.translate("MainWindow", "Auto downsample", None, -1)) self.cudaCheck.setText(QCoreApplication.translate("MainWindow", u"Use CUDA (GPU) if available", None))
self.scaleCheck.setText(QtWidgets.QApplication.translate("MainWindow", "Scale Data", None, -1)) self.downsampleCheck.setText(QCoreApplication.translate("MainWindow", u"Auto downsample", None))
self.rawRadio.setText(QtWidgets.QApplication.translate("MainWindow", "RawImageWidget", None, -1)) self.scaleCheck.setText(QCoreApplication.translate("MainWindow", u"Scale Data", None))
self.gfxRadio.setText(QtWidgets.QApplication.translate("MainWindow", "GraphicsView + ImageItem", None, -1)) self.rawRadio.setText(QCoreApplication.translate("MainWindow", u"RawImageWidget", None))
self.rawGLRadio.setText(QtWidgets.QApplication.translate("MainWindow", "RawGLImageWidget", None, -1)) self.gfxRadio.setText(QCoreApplication.translate("MainWindow", u"GraphicsView + ImageItem", None))
self.dtypeCombo.setItemText(0, QtWidgets.QApplication.translate("MainWindow", "uint8", None, -1)) self.rawGLRadio.setText(QCoreApplication.translate("MainWindow", u"RawGLImageWidget", None))
self.dtypeCombo.setItemText(1, QtWidgets.QApplication.translate("MainWindow", "uint16", None, -1)) self.dtypeCombo.setItemText(0, QCoreApplication.translate("MainWindow", u"uint8", None))
self.dtypeCombo.setItemText(2, QtWidgets.QApplication.translate("MainWindow", "float", None, -1)) self.dtypeCombo.setItemText(1, QCoreApplication.translate("MainWindow", u"uint16", None))
self.label.setText(QtWidgets.QApplication.translate("MainWindow", "Data type", None, -1)) self.dtypeCombo.setItemText(2, QCoreApplication.translate("MainWindow", u"float", None))
self.rgbLevelsCheck.setText(QtWidgets.QApplication.translate("MainWindow", "RGB", None, -1))
self.label_3.setText(QtWidgets.QApplication.translate("MainWindow", "<--->", None, -1)) self.label.setText(QCoreApplication.translate("MainWindow", u"Data type", None))
self.label_2.setText(QtWidgets.QApplication.translate("MainWindow", "<--->", None, -1)) self.rgbLevelsCheck.setText(QCoreApplication.translate("MainWindow", u"RGB", None))
self.label_4.setText(QtWidgets.QApplication.translate("MainWindow", "<--->", None, -1)) self.label_3.setText(QCoreApplication.translate("MainWindow", u"<--->", None))
self.lutCheck.setText(QtWidgets.QApplication.translate("MainWindow", "Use Lookup Table", None, -1)) self.label_2.setText(QCoreApplication.translate("MainWindow", u"<--->", None))
self.alphaCheck.setText(QtWidgets.QApplication.translate("MainWindow", "alpha", None, -1)) self.label_4.setText(QCoreApplication.translate("MainWindow", u"<--->", None))
self.fpsLabel.setText(QtWidgets.QApplication.translate("MainWindow", "FPS", None, -1)) self.lutCheck.setText(QCoreApplication.translate("MainWindow", u"Use Lookup Table", None))
self.rgbCheck.setText(QtWidgets.QApplication.translate("MainWindow", "RGB", None, -1)) self.alphaCheck.setText(QCoreApplication.translate("MainWindow", u"alpha", None))
self.label_5.setText(QtWidgets.QApplication.translate("MainWindow", "Image size", None, -1)) 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"]) exceptionCondition = namedtuple("exceptionCondition", ["condition", "reason"])
conditionalExamples = { 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( "hdf5.py": exceptionCondition(
False, False,
reason="Example requires user interaction" 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. # For 'col-major', image data is expected in reversed (col, row) order.
# The default is 'col-major' for backward compatibility, but this may # The default is 'col-major' for backward compatibility, but this may
# change in the future. # 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 from __future__ import division
import warnings
import numpy as np
import decimal, re
import ctypes 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 .pgcollections import OrderedDict
from .python2_3 import asUnicode, basestring 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 = { Colors = {
'b': QtGui.QColor(0,0,255,255), 'b': QtGui.QColor(0,0,255,255),
@ -940,80 +944,53 @@ def rescaleData(data, scale, offset, dtype=None, clip=None):
The scaling operation is:: The scaling operation is::
data => (data-offset) * scale data => (data-offset) * scale
""" """
if dtype is None: if dtype is None:
dtype = data.dtype dtype = data.dtype
else: else:
dtype = np.dtype(dtype) dtype = np.dtype(dtype)
try: d2 = data.astype(np.float) - float(offset)
if not getConfigOption('useWeave'): d2 *= scale
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 # Clip before converting dtype to avoid overflow
if not data.dtype.isnative: if dtype.kind in 'ui':
data = data.astype(data.dtype.newbyteorder('=')) lim = np.iinfo(dtype)
if not dtype.isnative: if clip is None:
weaveDtype = dtype.newbyteorder('=') # don't let rescale cause integer overflow
d2 = np.clip(d2, lim.min, lim.max)
else: else:
weaveDtype = dtype d2 = np.clip(d2, max(clip[0], lim.min), min(clip[1], lim.max))
else:
newData = np.empty((data.size,), dtype=weaveDtype) if clip is not None:
flat = np.ascontiguousarray(data).reshape(data.size) d2 = np.clip(d2, *clip)
size = data.size data = d2.astype(dtype)
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)
return data return data
def applyLookupTable(data, lut): def applyLookupTable(data, lut):
""" """
Uses values in *data* as indexes to select values from *lut*. Uses values in *data* as indexes to select values from *lut*.
The returned data has shape data.shape + lut.shape[1:] The returned data has shape data.shape + lut.shape[1:]
Note: color gradient lookup tables can be generated using GradientWidget. 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'): if data.dtype.kind not in ('i', 'u'):
data = data.astype(int) 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): def makeRGBA(*args, **kwds):
@ -1022,7 +999,7 @@ def makeRGBA(*args, **kwds):
return makeARGB(*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, Convert an array of values into an ARGB array suitable for building QImages,
OpenGL textures, etc. OpenGL textures, etc.
@ -1062,29 +1039,31 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
is BGRA). is BGRA).
============== ================================================================================== ============== ==================================================================================
""" """
cp = getCupy()
xp = cp.get_array_module(data) if cp else np
profile = debug.Profiler() profile = debug.Profiler()
if data.ndim not in (2, 3): if data.ndim not in (2, 3):
raise TypeError("data must be 2D or 3D") raise TypeError("data must be 2D or 3D")
if data.ndim == 3 and data.shape[2] > 4: if data.ndim == 3 and data.shape[2] > 4:
raise TypeError("data.shape[2] must be <= 4") raise TypeError("data.shape[2] must be <= 4")
if lut is not None and not isinstance(lut, np.ndarray): if lut is not None and not isinstance(lut, xp.ndarray):
lut = np.array(lut) lut = xp.array(lut)
if levels is None: if levels is None:
# automatically decide levels based on data dtype # automatically decide levels based on data dtype
if data.dtype.kind == 'u': 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': elif data.dtype.kind == 'i':
s = 2**(data.itemsize*8 - 1) s = 2**(data.itemsize*8 - 1)
levels = np.array([-s, s-1]) levels = xp.array([-s, s-1])
elif data.dtype.kind == 'b': elif data.dtype.kind == 'b':
levels = np.array([0,1]) levels = xp.array([0,1])
else: else:
raise Exception('levels argument is required for float input types') raise Exception('levels argument is required for float input types')
if not isinstance(levels, np.ndarray): if not isinstance(levels, xp.ndarray):
levels = np.array(levels) levels = xp.array(levels)
levels = levels.astype(np.float) levels = levels.astype(xp.float)
if levels.ndim == 1: if levels.ndim == 1:
if levels.shape[0] != 2: if levels.shape[0] != 2:
raise Exception('levels argument must have length 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: else:
raise Exception("levels argument must be 1D or 2D (got shape=%s)." % repr(levels.shape)) raise Exception("levels argument must be 1D or 2D (got shape=%s)." % repr(levels.shape))
profile() profile('check inputs')
# Decide on maximum scaled value # Decide on maximum scaled value
if scale is None: 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 # Decide on the dtype we want after scaling
if lut is None: if lut is None:
dtype = np.ubyte dtype = xp.ubyte
else: 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 # awkward, but fastest numpy native nan evaluation
# #
nanMask = None nanMask = None
if data.dtype.kind == 'f' and np.isnan(data.min()): if data.dtype.kind == 'f' and xp.isnan(data.min()):
nanMask = np.isnan(data) nanMask = xp.isnan(data)
if data.ndim > 2: if data.ndim > 2:
nanMask = np.any(nanMask, axis=-1) nanMask = xp.any(nanMask, axis=-1)
# Apply levels if given # Apply levels if given
if levels is not None: 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 # we are going to rescale each channel independently
if levels.shape[0] != data.shape[-1]: 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])") 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]): for i in range(data.shape[-1]):
minVal, maxVal = levels[i] minVal, maxVal = levels[i]
if minVal == maxVal: if minVal == maxVal:
maxVal = np.nextafter(maxVal, 2*maxVal) maxVal = xp.nextafter(maxVal, 2*maxVal)
rng = maxVal-minVal rng = maxVal-minVal
rng = 1 if rng == 0 else rng rng = 1 if rng == 0 else rng
newData[...,i] = rescaleData(data[...,i], scale / rng, minVal, dtype=dtype) 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 minVal, maxVal = levels
if minVal != 0 or maxVal != scale: if minVal != 0 or maxVal != scale:
if minVal == maxVal: if minVal == maxVal:
maxVal = np.nextafter(maxVal, 2*maxVal) maxVal = xp.nextafter(maxVal, 2*maxVal)
rng = maxVal-minVal rng = maxVal-minVal
rng = 1 if rng == 0 else rng rng = 1 if rng == 0 else rng
data = rescaleData(data, scale/rng, minVal, dtype=dtype) data = rescaleData(data, scale/rng, minVal, dtype=dtype)
profile() profile('apply levels')
# apply LUT if given # apply LUT if given
if lut is not None: if lut is not None:
data = applyLookupTable(data, lut) data = applyLookupTable(data, lut)
else: else:
if data.dtype is not np.ubyte: if data.dtype is not xp.ubyte:
data = np.clip(data, 0, 255).astype(np.ubyte) data = xp.clip(data, 0, 255).astype(xp.ubyte)
profile() profile('apply lut')
# this will be the final image array # 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 # decide channel order
if useRGBA: if useRGBA:
@ -1167,7 +1150,7 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
# copy data into image array # copy data into image array
if data.ndim == 2: if data.ndim == 2:
# This is tempting: # This is tempting:
# imgData[..., :3] = data[..., np.newaxis] # imgData[..., :3] = data[..., xp.newaxis]
# ..but it turns out this is faster: # ..but it turns out this is faster:
for i in range(3): for i in range(3):
imgData[..., i] = data 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]): for i in range(0, data.shape[2]):
imgData[..., i] = data[..., order[i]] imgData[..., i] = data[..., order[i]]
profile() profile('reorder channels')
# add opaque alpha channel if needed # add opaque alpha channel if needed
if data.ndim == 2 or data.shape[2] == 3: 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 alpha = True
imgData[nanMask, 3] = 0 imgData[nanMask, 3] = 0
profile() profile('alpha channel')
return imgData, alpha return imgData, alpha
@ -1364,7 +1347,9 @@ def gaussianFilter(data, sigma):
(note: results are only approximately equal to the output of (note: results are only approximately equal to the output of
gaussian_filter) 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 sigma = (sigma,) * data.ndim
baseline = data.mean() baseline = data.mean()
@ -1376,17 +1361,17 @@ def gaussianFilter(data, sigma):
# generate 1D gaussian kernel # generate 1D gaussian kernel
ksize = int(s * 6) ksize = int(s * 6)
x = np.arange(-ksize, ksize) x = xp.arange(-ksize, ksize)
kernel = np.exp(-x**2 / (2*s**2)) kernel = xp.exp(-x**2 / (2*s**2))
kshape = [1,] * data.ndim kshape = [1,] * data.ndim
kshape[ax] = len(kernel) kshape[ax] = len(kernel)
kernel = kernel.reshape(kshape) kernel = kernel.reshape(kshape)
# convolve as product of FFTs # convolve as product of FFTs
shape = data.shape[ax] + ksize shape = data.shape[ax] + ksize
scale = 1.0 / (abs(s) * (2*np.pi)**0.5) scale = 1.0 / (abs(s) * (2*xp.pi)**0.5)
filtered = scale * np.fft.irfft(np.fft.rfft(filtered, shape, axis=ax) * filtered = scale * xp.fft.irfft(xp.fft.rfft(filtered, shape, axis=ax) *
np.fft.rfft(kernel, shape, axis=ax), xp.fft.rfft(kernel, shape, axis=ax),
axis=ax) axis=ax)
# clip off extra data # clip off extra data

View File

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