Add NonUniformImage, example and tests. (#509)

* Add NonUniformImage, example and tests.

* Use updated test-data tag

* Fix line ending, remove lut.setLevels keyword arguments call

Co-authored-by: Ogi <ognyan.moore@gmail.com>
This commit is contained in:
Torsten Sommer 2021-01-20 06:43:58 +01:00 committed by GitHub
parent f2b4a15b2d
commit 510626c15f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 322 additions and 1 deletions

View File

@ -0,0 +1,86 @@
# -*- coding: utf-8 -*-
"""
Display a non-uniform image.
This example displays 2-d data as an image with non-uniformly
distributed sample points.
"""
import initExample ## Add path to library (just for examples; you do not need this)
from pyqtgraph.Qt import QtGui, QtCore
import pyqtgraph as pg
import numpy as np
from pyqtgraph.graphicsItems.GradientEditorItem import Gradients
from pyqtgraph.graphicsItems.NonUniformImage import NonUniformImage
RPM2RADS = 2 * np.pi / 60
RADS2RPM = 1 / RPM2RADS
kfric = 1 # [Ws/rad] angular damping coefficient [0;100]
kfric3 = 1.5e-6 # [Ws3/rad3] angular damping coefficient (3rd order) [0;10-3]
psi = 0.2 # [Vs] flux linkage [0.001;10]
res = 5e-3 # [Ohm] resistance [0;100]
v_ref = 200 # [V] reference DC voltage [0;1000]
k_v = 5 # linear voltage coefficient [-100;100]
# create the (non-uniform) scales
tau = np.array([0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 120, 140, 160, 180, 200, 220], dtype=np.float32)
w = np.array([0, 250, 500, 750, 1000, 1500, 2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000, 10000], dtype=np.float32) * RPM2RADS
v = 380
# calculate the power losses
TAU, W = np.meshgrid(tau, w, indexing='ij')
V = np.ones_like(TAU) * v
P_loss = kfric * W + kfric3 * W ** 3 + (res * (TAU / psi) ** 2) + k_v * (V - v_ref)
P_mech = TAU * W
P_loss[P_mech > 1.5e5] = np.NaN
# green - orange - red
Gradients['gor'] = {'ticks': [(0.0, (74, 158, 71)), (0.5, (255, 230, 0)), (1, (191, 79, 76))], 'mode': 'rgb'}
app = QtGui.QApplication([])
win = QtGui.QMainWindow()
cw = pg.GraphicsLayoutWidget()
win.show()
win.resize(600, 400)
win.setCentralWidget(cw)
win.setWindowTitle('pyqtgraph example: Non-uniform Image')
p = cw.addPlot(title="Power Losses [W]", row=0, col=0)
lut = pg.HistogramLUTItem()
p.setMouseEnabled(x=False, y=False)
cw.addItem(lut)
# load the gradient
lut.gradient.loadPreset('gor')
image = NonUniformImage(w * RADS2RPM, tau, P_loss.T)
image.setLookupTable(lut, autoLevel=True)
image.setZValue(-1)
p.addItem(image)
h = image.getHistogram()
lut.plot.setData(*h)
p.showGrid(x=True, y=True)
p.setLabel(axis='bottom', text='Speed [rpm]')
p.setLabel(axis='left', text='Torque [Nm]')
# elevate the grid lines
p.axes['bottom']['item'].setZValue(1000)
p.axes['left']['item'].setZValue(1000)
## Start Qt event loop unless running in interactive mode or using pyside.
if __name__ == '__main__':
import sys
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
QtGui.QApplication.instance().exec_()

View File

@ -41,6 +41,7 @@ examples = OrderedDict([
('FillBetweenItem', 'FillBetweenItem.py'),
('ImageItem - video', 'ImageItem.py'),
('ImageItem - draw', 'Draw.py'),
('Non-uniform Image', 'NonUniformImage.py'),
('Region-of-Interest', 'ROIExamples.py'),
('Bar Graph', 'BarGraphItem.py'),
('GraphicsLayout', 'GraphicsLayout.py'),

View File

@ -0,0 +1,130 @@
from ..Qt import QtGui, QtCore
import numpy as np
from ..colormap import ColorMap
from .GraphicsObject import GraphicsObject
from .. import mkBrush, mkPen
from .. import functions as fn
class NonUniformImage(GraphicsObject):
"""
**Bases:** :class:`GraphicsObject <pyqtgraph.GraphicsObject>`
GraphicsObject displaying an image with non-uniform sample points. It's
commonly used to display 2-d or slices of higher dimensional data that
have a regular but non-uniform grid e.g. measurements or simulation results.
"""
def __init__(self, x, y, z, border=None):
GraphicsObject.__init__(self)
# convert to numpy arrays
x = np.asarray(x, dtype=np.float64)
y = np.asarray(y, dtype=np.float64)
z = np.asarray(z, dtype=np.float64)
if x.ndim != 1 or y.ndim != 1:
raise Exception("x and y must be 1-d arrays.")
if np.any(np.diff(x) < 0) or np.any(np.diff(y) < 0):
raise Exception("The values in x and y must be monotonically increasing.")
if len(z.shape) != 2 or z.shape != (x.size, y.size):
raise Exception("The length of x and y must match the shape of z.")
# default colormap (black - white)
self.cmap = ColorMap(pos=[0.0, 1.0], color=[(0.0, 0.0, 0.0, 1.0), (1.0, 1.0, 1.0, 1.0)])
self.data = (x, y, z)
self.lut = None
self.border = border
self.generatePicture()
def setLookupTable(self, lut, autoLevel=False):
lut.sigLevelsChanged.connect(self.generatePicture)
lut.gradient.sigGradientChanged.connect(self.generatePicture)
self.lut = lut
if autoLevel:
_, _, z = self.data
f = z[np.isfinite(z)]
lut.setLevels(f.min(), f.max())
self.generatePicture()
def setColorMap(self, cmap):
self.cmap = cmap
self.generatePicture()
def getHistogram(self, **kwds):
"""Returns x and y arrays containing the histogram values for the current image.
For an explanation of the return format, see numpy.histogram().
"""
z = self.data[2]
z = z[np.isfinite(z)]
hist = np.histogram(z, **kwds)
return hist[1][:-1], hist[0]
def generatePicture(self):
x, y, z = self.data
self.picture = QtGui.QPicture()
p = QtGui.QPainter(self.picture)
p.setPen(mkPen(None))
# normalize
if self.lut is not None:
mn, mx = self.lut.getLevels()
else:
f = z[np.isfinite(z)]
mn = f.min()
mx = f.max()
# draw the tiles
for i in range(x.size):
for j in range(y.size):
value = z[i, j]
if np.isneginf(value):
value = 0.0
elif np.isposinf(value):
value = 1.0
elif np.isnan(value):
continue # ignore NaN
else:
value = (value - mn) / (mx - mn) # normalize
if self.lut:
color = self.lut.gradient.getColor(value)
else:
color = self.cmap.mapToQColor(value)
p.setBrush(mkBrush(color))
# left, right, bottom, top
l = x[0] if i == 0 else (x[i - 1] + x[i]) / 2
r = (x[i] + x[i + 1]) / 2 if i < x.size - 1 else x[-1]
b = y[0] if j == 0 else (y[j - 1] + y[j]) / 2
t = (y[j] + y[j + 1]) / 2 if j < y.size - 1 else y[-1]
p.drawRect(QtCore.QRectF(l, t, r - l, b - t))
if self.border is not None:
p.setPen(self.border)
p.setBrush(fn.mkBrush(None))
p.drawRect(self.boundingRect())
p.end()
self.update()
def paint(self, p, *args):
p.drawPicture(0, 0, self.picture)
def boundingRect(self):
return QtCore.QRectF(self.picture.boundingRect())

View File

@ -0,0 +1,104 @@
import numpy as np
import pyqtgraph as pg
from pyqtgraph.Qt import QtTest
from pyqtgraph.graphicsItems.NonUniformImage import NonUniformImage
from pyqtgraph.tests import assertImageApproved
from pyqtgraph.colormap import ColorMap
import pyqtgraph.functions as fn
import pytest
app = pg.mkQApp()
def test_NonUniformImage_scale_dimensions():
x = [1.0, 3.0, 10.0]
y = [1.0, 2.0, 4.0]
X, Y = np.meshgrid(x, y, indexing='ij')
Z = X * Y
for args in [(Z, y, Z), (x, Z, Z)]:
with pytest.raises(Exception) as ex:
NonUniformImage(*args)
assert "x and y must be 1-d arrays." in str(ex)
def test_NonUniformImage_scale_monotonicity():
x = [1.0, 0.0, 10.0]
y = [1.0, 2.0, 4.0]
X, Y = np.meshgrid(x, y, indexing='ij')
Z = X * Y
for args in [(x, y, Z), (y, x, Z)]:
with pytest.raises(Exception) as ex:
NonUniformImage(*args)
assert "The values in x and y must be monotonically increasing." in str(ex)
def test_NonUniformImage_data_dimensions():
x = [1.0, 3.0, 10.0]
y = [1.0, 2.0, 4.0]
with pytest.raises(Exception) as ex:
NonUniformImage(x, y, x)
assert "The length of x and y must match the shape of z." in str(ex)
def test_NonUniformImage_lut():
window = pg.GraphicsWindow()
viewbox = pg.ViewBox()
window.setCentralWidget(viewbox)
window.resize(200, 200)
window.show()
x = [1.0, 3.0, 10.0]
y = [1.0, 2.0, 4.0]
X, Y = np.meshgrid(x, y, indexing='ij')
Z = X * Y
image = NonUniformImage(x, y, Z, border=fn.mkPen('g'))
viewbox.addItem(image)
lut = pg.HistogramLUTItem()
window.addItem(lut)
image.setLookupTable(lut, autoLevel=True)
h = image.getHistogram()
lut.plot.setData(*h)
QtTest.QTest.qWaitForWindowShown(window)
QtTest.QTest.qWait(100)
assertImageApproved(window, 'nonuniform_image/lut-3x3')
def test_NonUniformImage_colormap():
window = pg.GraphicsWindow()
viewbox = pg.ViewBox()
window.setCentralWidget(viewbox)
window.resize(200, 200)
window.show()
x = [1.0, 3.0, 10.0]
y = [1.0, 2.0, 4.0]
X, Y = np.meshgrid(x, y, indexing='ij')
Z = X * Y
Z[:, 0] = [np.NINF, np.NAN, np.PINF]
image = NonUniformImage(x, y, Z, border=fn.mkPen('g'))
cmap = ColorMap(pos=[0.0, 1.0], color=[(0.0, 0.0, 0.0, 1.0), (1.0, 1.0, 1.0, 1.0)], mode='rgb')
image.setColorMap(cmap)
viewbox.addItem(image)
QtTest.QTest.qWaitForWindowShown(window)
QtTest.QTest.qWait(100)
assertImageApproved(window, 'nonuniform_image/colormap-3x3')

View File

@ -44,7 +44,7 @@ Procedure for unit-testing with images:
# pyqtgraph should be tested against. When adding or changing test images,
# create and push a new tag and update this variable. To test locally, begin
# by creating the tag in your ~/.pyqtgraph/test-data repository.
testDataTag = 'test-data-7'
testDataTag = 'test-data-8'
import time