Merge pull request #370 from campagnola/image-alignment

Image alignment
This commit is contained in:
Luke Campagnola 2016-09-07 23:18:51 -07:00 committed by GitHub
commit 4b9f1a20a4
23 changed files with 419 additions and 154 deletions

View File

@ -6,6 +6,7 @@ Contents:
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 2
config_options
functions functions
graphicsItems/index graphicsItems/index
widgets/index widgets/index

View File

@ -0,0 +1,41 @@
.. currentmodule:: pyqtgraph
.. _apiref_config:
Global Configuration Options
============================
PyQtGraph has several global configuration options that allow you to change its
default behavior. These can be accessed using the :func:`setConfigOptions` and
:func:`getConfigOption` functions:
================== =================== ================== ================================================================================
**Option** **Type** **Default**
leftButtonPan bool True If True, dragging the left mouse button over a ViewBox
causes the view to be panned. If False, then dragging
the left mouse button draws a rectangle that the
ViewBox will zoom to.
foreground See :func:`mkColor` 'd' Default foreground color for text, lines, axes, etc.
background See :func:`mkColor` 'k' Default background for :class:`GraphicsView`.
antialias bool False Enabling antialiasing causes lines to be drawn with
smooth edges at the cost of reduced performance.
imageAxisOrder str 'col-major' For 'row-major', image data is expected in the standard row-major
(row, col) order. For 'col-major', image data is expected in
reversed column-major (col, row) order.
The default is 'col-major' for backward compatibility, but this may
change in the future.
editorCommand str or None None Command used to invoke code editor from ConsoleWidget.
exitCleanup bool True Attempt to work around some exit crash bugs in PyQt and PySide.
useWeave bool False Use weave to speed up some operations, if it is available.
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.
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.
================== =================== ================== ================================================================================
.. autofunction:: pyqtgraph.setConfigOptions
.. autofunction:: pyqtgraph.getConfigOption

View File

@ -2,7 +2,7 @@
""" """
This example demonstrates a very basic use of flowcharts: filter data, This example demonstrates a very basic use of flowcharts: filter data,
displaying both the input and output of the filter. The behavior of displaying both the input and output of the filter. The behavior of
he filter can be reprogrammed by the user. the filter can be reprogrammed by the user.
Basic steps are: Basic steps are:
- create a flowchart and two plots - create a flowchart and two plots

View File

@ -17,6 +17,9 @@ import numpy as np
from pyqtgraph.Qt import QtCore, QtGui from pyqtgraph.Qt import QtCore, QtGui
import pyqtgraph as pg import pyqtgraph as pg
# Interpret image data as row-major instead of col-major
pg.setConfigOptions(imageAxisOrder='row-major')
app = QtGui.QApplication([]) app = QtGui.QApplication([])
## Create window with ImageView widget ## Create window with ImageView widget
@ -42,7 +45,7 @@ sig[40:] += np.exp(-np.linspace(1,10, 60))
sig[70:] += np.exp(-np.linspace(1,10, 30)) sig[70:] += np.exp(-np.linspace(1,10, 30))
sig = sig[:,np.newaxis,np.newaxis] * 3 sig = sig[:,np.newaxis,np.newaxis] * 3
data[:,50:60,50:60] += sig data[:,50:60,30:40] += sig
## Display the data and assign each frame a time value from 1.0 to 3.0 ## Display the data and assign each frame a time value from 1.0 to 3.0

View File

@ -11,6 +11,7 @@ import pyqtgraph as pg
from pyqtgraph.Qt import QtCore, QtGui from pyqtgraph.Qt import QtCore, QtGui
import numpy as np import numpy as np
pg.setConfigOptions(imageAxisOrder='row-major')
## Create image to display ## Create image to display
arr = np.ones((100, 100), dtype=float) arr = np.ones((100, 100), dtype=float)
@ -24,6 +25,11 @@ arr[:, 50] = 10
arr += np.sin(np.linspace(0, 20, 100)).reshape(1, 100) arr += np.sin(np.linspace(0, 20, 100)).reshape(1, 100)
arr += np.random.normal(size=(100,100)) arr += np.random.normal(size=(100,100))
# add an arrow for asymmetry
arr[10, :50] = 10
arr[9:12, 44:48] = 10
arr[8:13, 44:46] = 10
## create GUI ## create GUI
app = QtGui.QApplication([]) app = QtGui.QApplication([])

View File

@ -8,23 +8,15 @@ from pyqtgraph.Qt import QtCore, QtGui
import numpy as np import numpy as np
import pyqtgraph as pg import pyqtgraph as pg
pg.setConfigOptions(imageAxisOrder='row-major')
## create GUI ## create GUI
app = QtGui.QApplication([]) app = QtGui.QApplication([])
w = pg.GraphicsWindow(size=(800,800), border=True) w = pg.GraphicsWindow(size=(800,800), border=True)
v = w.addViewBox(colspan=2) v = w.addViewBox(colspan=2)
#w = QtGui.QMainWindow()
#w.resize(800,800)
#v = pg.GraphicsView()
v.invertY(True) ## Images usually have their Y-axis pointing downward v.invertY(True) ## Images usually have their Y-axis pointing downward
v.setAspectLocked(True) v.setAspectLocked(True)
#v.enableMouse(True)
#v.autoPixelScale = False
#w.setCentralWidget(v)
#s = v.scene()
#v.setRange(QtCore.QRectF(-2, -2, 220, 220))
## Create image to display ## Create image to display
@ -37,6 +29,11 @@ arr[:, 75] = 5
arr[50, :] = 10 arr[50, :] = 10
arr[:, 50] = 10 arr[:, 50] = 10
# add an arrow for asymmetry
arr[10, :50] = 10
arr[9:12, 44:48] = 10
arr[8:13, 44:46] = 10
## Create image items, add to scene and set position ## Create image items, add to scene and set position
im1 = pg.ImageItem(arr) im1 = pg.ImageItem(arr)
im2 = pg.ImageItem(arr) im2 = pg.ImageItem(arr)
@ -44,6 +41,7 @@ v.addItem(im1)
v.addItem(im2) v.addItem(im2)
im2.moveBy(110, 20) im2.moveBy(110, 20)
v.setRange(QtCore.QRectF(0, 0, 200, 120)) v.setRange(QtCore.QRectF(0, 0, 200, 120))
im1.scale(0.8, 0.5)
im3 = pg.ImageItem() im3 = pg.ImageItem()
v2 = w.addViewBox(1,0) v2 = w.addViewBox(1,0)

View File

@ -103,6 +103,9 @@ def mkData():
if dtype[0] != 'float': if dtype[0] != 'float':
data = np.clip(data, 0, mx) data = np.clip(data, 0, mx)
data = data.astype(dt) 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) cache = {dtype: data} # clear to save memory (but keep one to prevent unnecessary regeneration)
data = cache[dtype] data = cache[dtype]

View File

@ -12,8 +12,11 @@ import pyqtgraph as pg
from pyqtgraph.Qt import QtCore, QtGui from pyqtgraph.Qt import QtCore, QtGui
import numpy as np import numpy as np
pg.mkQApp()
# Interpret image data as row-major instead of col-major
pg.setConfigOptions(imageAxisOrder='row-major')
pg.mkQApp()
win = pg.GraphicsLayoutWidget() win = pg.GraphicsLayoutWidget()
win.setWindowTitle('pyqtgraph example: Image Analysis') win.setWindowTitle('pyqtgraph example: Image Analysis')
@ -57,10 +60,10 @@ win.show()
# Generate image data # Generate image data
data = np.random.normal(size=(100, 200)) data = np.random.normal(size=(200, 100))
data[20:80, 20:80] += 2. data[20:80, 20:80] += 2.
data = pg.gaussianFilter(data, (3, 3)) data = pg.gaussianFilter(data, (3, 3))
data += np.random.normal(size=(100, 200)) * 0.1 data += np.random.normal(size=(200, 100)) * 0.1
img.setImage(data) img.setImage(data)
hist.setLevels(data.min(), data.max()) hist.setLevels(data.min(), data.max())
@ -79,7 +82,7 @@ p1.autoRange()
def updatePlot(): def updatePlot():
global img, roi, data, p2 global img, roi, data, p2
selected = roi.getArrayRegion(data, img) selected = roi.getArrayRegion(data, img)
p2.plot(selected.mean(axis=1), clear=True) p2.plot(selected.mean(axis=0), clear=True)
roi.sigRegionChanged.connect(updatePlot) roi.sigRegionChanged.connect(updatePlot)
updatePlot() updatePlot()

View File

@ -3,6 +3,7 @@ from .Qt import QtCore, QtGui
from .Point import Point from .Point import Point
import numpy as np import numpy as np
class SRTTransform(QtGui.QTransform): class SRTTransform(QtGui.QTransform):
"""Transform that can always be represented as a combination of 3 matrices: scale * rotate * translate """Transform that can always be represented as a combination of 3 matrices: scale * rotate * translate
This transform has no shear; angles are always preserved. This transform has no shear; angles are always preserved.
@ -165,6 +166,7 @@ class SRTTransform(QtGui.QTransform):
def matrix(self): def matrix(self):
return np.array([[self.m11(), self.m12(), self.m13()],[self.m21(), self.m22(), self.m23()],[self.m31(), self.m32(), self.m33()]]) return np.array([[self.m11(), self.m12(), self.m13()],[self.m21(), self.m22(), self.m23()],[self.m31(), self.m32(), self.m33()]])
if __name__ == '__main__': if __name__ == '__main__':
from . import widgets from . import widgets

View File

@ -59,16 +59,32 @@ CONFIG_OPTIONS = {
'exitCleanup': True, ## Attempt to work around some exit crash bugs in PyQt and PySide 'exitCleanup': True, ## Attempt to work around some exit crash bugs in PyQt and PySide
'enableExperimental': False, ## Enable experimental features (the curious can search for this key in the code) 'enableExperimental': False, ## Enable experimental features (the curious can search for this key in the code)
'crashWarning': False, # If True, print warnings about situations that may result in a crash 'crashWarning': False, # If True, print warnings about situations that may result in a crash
'imageAxisOrder': 'col-major', # For 'row-major', image data is expected in the standard (row, col) order.
# 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.
} }
def setConfigOption(opt, value): def setConfigOption(opt, value):
global CONFIG_OPTIONS
if opt not in CONFIG_OPTIONS:
raise KeyError('Unknown configuration option "%s"' % opt)
if opt == 'imageAxisOrder' and value not in ('row-major', 'col-major'):
raise ValueError('imageAxisOrder must be either "row-major" or "col-major"')
CONFIG_OPTIONS[opt] = value CONFIG_OPTIONS[opt] = value
def setConfigOptions(**opts): def setConfigOptions(**opts):
CONFIG_OPTIONS.update(opts) """Set global configuration options.
Each keyword argument sets one global option.
"""
for k,v in opts.items():
setConfigOption(k, v)
def getConfigOption(opt): def getConfigOption(opt):
"""Return the value of a single global configuration option.
"""
return CONFIG_OPTIONS[opt] return CONFIG_OPTIONS[opt]

View File

@ -959,6 +959,8 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
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 = np.array([-s, s-1])
elif data.dtype.kind == 'b':
levels = np.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, np.ndarray):
@ -1727,7 +1729,7 @@ def isosurface(data, level):
See Paul Bourke, "Polygonising a Scalar Field" See Paul Bourke, "Polygonising a Scalar Field"
(http://paulbourke.net/geometry/polygonise/) (http://paulbourke.net/geometry/polygonise/)
*data* 3D numpy array of scalar values *data* 3D numpy array of scalar values. Must be contiguous.
*level* The level at which to generate an isosurface *level* The level at which to generate an isosurface
Returns an array of vertex coordinates (Nv, 3) and an array of Returns an array of vertex coordinates (Nv, 3) and an array of
@ -2079,7 +2081,10 @@ def isosurface(data, level):
else: else:
faceShiftTables, edgeShifts, edgeTable, nTableFaces = IsosurfaceDataCache faceShiftTables, edgeShifts, edgeTable, nTableFaces = IsosurfaceDataCache
# We use strides below, which means we need contiguous array input.
# Ideally we can fix this just by removing the dependency on strides.
if not data.flags['C_CONTIGUOUS']:
raise TypeError("isosurface input data must be c-contiguous.")
## mark everything below the isosurface level ## mark everything below the isosurface level
mask = data < level mask = data < level

View File

@ -37,9 +37,6 @@ class GraphicsItem(object):
if register: if register:
GraphicsScene.registerObject(self) ## workaround for pyqt bug in graphicsscene.items() GraphicsScene.registerObject(self) ## workaround for pyqt bug in graphicsscene.items()
def getViewWidget(self): def getViewWidget(self):
""" """
Return the view widget for this item. Return the view widget for this item.
@ -95,7 +92,6 @@ class GraphicsItem(object):
def forgetViewBox(self): def forgetViewBox(self):
self._viewBox = None self._viewBox = None
def deviceTransform(self, viewportTransform=None): def deviceTransform(self, viewportTransform=None):
""" """
Return the transform that converts local item coordinates to device coordinates (usually pixels). Return the transform that converts local item coordinates to device coordinates (usually pixels).

View File

@ -179,8 +179,8 @@ class HistogramLUTItem(GraphicsWidget):
return self.lut return self.lut
def regionChanged(self): def regionChanged(self):
#if self.imageItem is not None: if self.imageItem() is not None:
#self.imageItem.setLevels(self.region.getRegion()) self.imageItem().setLevels(self.region.getRegion())
self.sigLevelChangeFinished.emit(self) self.sigLevelChangeFinished.emit(self)
#self.update() #self.update()

View File

@ -7,6 +7,8 @@ from .. import functions as fn
from .. import debug as debug from .. import debug as debug
from .GraphicsObject import GraphicsObject from .GraphicsObject import GraphicsObject
from ..Point import Point from ..Point import Point
from .. import getConfigOption
__all__ = ['ImageItem'] __all__ = ['ImageItem']
@ -28,7 +30,6 @@ class ImageItem(GraphicsObject):
for controlling the levels and lookup table used to display the image. for controlling the levels and lookup table used to display the image.
""" """
sigImageChanged = QtCore.Signal() sigImageChanged = QtCore.Signal()
sigRemoveRequested = QtCore.Signal(object) # self; emitted when 'remove' is selected from context menu sigRemoveRequested = QtCore.Signal(object) # self; emitted when 'remove' is selected from context menu
@ -47,6 +48,8 @@ class ImageItem(GraphicsObject):
self.lut = None self.lut = None
self.autoDownsample = False self.autoDownsample = False
self.axisOrder = getConfigOption('imageAxisOrder')
# In some cases, we use a modified lookup table to handle both rescaling # In some cases, we use a modified lookup table to handle both rescaling
# and LUT more efficiently # and LUT more efficiently
self._effectiveLut = None self._effectiveLut = None
@ -86,12 +89,14 @@ class ImageItem(GraphicsObject):
def width(self): def width(self):
if self.image is None: if self.image is None:
return None return None
return self.image.shape[0] axis = 0 if self.axisOrder == 'col-major' else 1
return self.image.shape[axis]
def height(self): def height(self):
if self.image is None: if self.image is None:
return None return None
return self.image.shape[1] axis = 1 if self.axisOrder == 'col-major' else 0
return self.image.shape[axis]
def boundingRect(self): def boundingRect(self):
if self.image is None: if self.image is None:
@ -147,7 +152,11 @@ class ImageItem(GraphicsObject):
self.update() self.update()
def setOpts(self, update=True, **kargs): def setOpts(self, update=True, **kargs):
if 'axisOrder' in kargs:
val = kargs['axisOrder']
if val not in ('row-major', 'col-major'):
raise ValueError('axisOrder must be either "row-major" or "col-major"')
self.axisOrder = val
if 'lut' in kargs: if 'lut' in kargs:
self.setLookupTable(kargs['lut'], update=update) self.setLookupTable(kargs['lut'], update=update)
if 'levels' in kargs: if 'levels' in kargs:
@ -190,7 +199,7 @@ class ImageItem(GraphicsObject):
image (numpy array) Specifies the image data. May be 2D (width, height) or image (numpy array) Specifies the image data. May be 2D (width, height) or
3D (width, height, RGBa). The array dtype must be integer or floating 3D (width, height, RGBa). The array dtype must be integer or floating
point of any bit depth. For 3D arrays, the third dimension must point of any bit depth. For 3D arrays, the third dimension must
be of length 3 (RGB) or 4 (RGBA). be of length 3 (RGB) or 4 (RGBA). See *notes* below.
autoLevels (bool) If True, this forces the image to automatically select autoLevels (bool) If True, this forces the image to automatically select
levels based on the maximum and minimum values in the data. levels based on the maximum and minimum values in the data.
By default, this argument is true unless the levels argument is By default, this argument is true unless the levels argument is
@ -201,12 +210,26 @@ class ImageItem(GraphicsObject):
data. By default, this will be set to the minimum and maximum values data. By default, this will be set to the minimum and maximum values
in the image. If the image array has dtype uint8, no rescaling is necessary. in the image. If the image array has dtype uint8, no rescaling is necessary.
opacity (float 0.0-1.0) opacity (float 0.0-1.0)
compositionMode see :func:`setCompositionMode <pyqtgraph.ImageItem.setCompositionMode>` compositionMode See :func:`setCompositionMode <pyqtgraph.ImageItem.setCompositionMode>`
border Sets the pen used when drawing the image border. Default is None. border Sets the pen used when drawing the image border. Default is None.
autoDownsample (bool) If True, the image is automatically downsampled to match the autoDownsample (bool) If True, the image is automatically downsampled to match the
screen resolution. This improves performance for large images and screen resolution. This improves performance for large images and
reduces aliasing. reduces aliasing.
================= ========================================================================= ================= =========================================================================
**Notes:**
For backward compatibility, image data is assumed to be in column-major order (column, row).
However, most image data is stored in row-major order (row, column) and will need to be
transposed before calling setImage()::
imageitem.setImage(imagedata.T)
This requirement can be changed by calling ``image.setOpts(axisOrder='row-major')`` or
by changing the ``imageAxisOrder`` :ref:`global configuration option <apiref_config>`.
""" """
profile = debug.Profiler() profile = debug.Profiler()
@ -259,6 +282,42 @@ class ImageItem(GraphicsObject):
if gotNewData: if gotNewData:
self.sigImageChanged.emit() self.sigImageChanged.emit()
def dataTransform(self):
"""Return the transform that maps from this image's input array to its
local coordinate system.
This transform corrects for the transposition that occurs when image data
is interpreted in row-major order.
"""
# Might eventually need to account for downsampling / clipping here
tr = QtGui.QTransform()
if self.axisOrder == 'row-major':
# transpose
tr.scale(1, -1)
tr.rotate(-90)
return tr
def inverseDataTransform(self):
"""Return the transform that maps from this image's local coordinate
system to its input array.
See dataTransform() for more information.
"""
tr = QtGui.QTransform()
if self.axisOrder == 'row-major':
# transpose
tr.scale(1, -1)
tr.rotate(-90)
return tr
def mapToData(self, obj):
tr = self.inverseDataTransform()
return tr.map(obj)
def mapFromData(self, obj):
tr = self.dataTransform()
return tr.map(obj)
def quickMinMax(self, targetSize=1e6): def quickMinMax(self, targetSize=1e6):
""" """
Estimate the min/max values of the image data by subsampling. Estimate the min/max values of the image data by subsampling.
@ -303,10 +362,12 @@ class ImageItem(GraphicsObject):
if w == 0 or h == 0: if w == 0 or h == 0:
self.qimage = None self.qimage = None
return return
xds = int(1.0/w) xds = max(1, int(1.0 / w))
yds = int(1.0/h) yds = max(1, int(1.0 / h))
image = fn.downsample(self.image, xds, axis=0) axes = [1, 0] if self.axisOrder == 'row-major' else [0, 1]
image = fn.downsample(image, yds, axis=1) image = fn.downsample(self.image, xds, axis=axes[0])
image = fn.downsample(image, yds, axis=axes[1])
self._lastDownsample = (xds, yds)
else: else:
image = self.image image = self.image
@ -318,20 +379,28 @@ class ImageItem(GraphicsObject):
eflsize = 2**(image.itemsize*8) eflsize = 2**(image.itemsize*8)
ind = np.arange(eflsize) ind = np.arange(eflsize)
minlev, maxlev = levels minlev, maxlev = levels
levdiff = maxlev - minlev
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./(maxlev-minlev), efflut = fn.rescaleData(ind, scale=255./levdiff,
offset=minlev, dtype=np.ubyte) offset=minlev, dtype=np.ubyte)
else: else:
lutdtype = np.min_scalar_type(lut.shape[0]-1) lutdtype = np.min_scalar_type(lut.shape[0]-1)
efflut = fn.rescaleData(ind, scale=(lut.shape[0]-1)/(maxlev-minlev), 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]
self._effectiveLut = efflut self._effectiveLut = efflut
lut = self._effectiveLut lut = self._effectiveLut
levels = None levels = None
argb, alpha = fn.makeARGB(image.transpose((1, 0, 2)[:image.ndim]), lut=lut, levels=levels) # Assume images are in column-major order for backward compatibility
# (most images are in row-major order)
if self.axisOrder == 'col-major':
image = image.transpose((1, 0, 2)[:image.ndim])
argb, alpha = fn.makeARGB(image, lut=lut, levels=levels)
self.qimage = fn.makeQImage(argb, alpha, transpose=False) self.qimage = fn.makeQImage(argb, alpha, transpose=False)
def paint(self, p, *args): def paint(self, p, *args):
@ -347,7 +416,8 @@ class ImageItem(GraphicsObject):
p.setCompositionMode(self.paintMode) p.setCompositionMode(self.paintMode)
profile('set comp mode') profile('set comp mode')
p.drawImage(QtCore.QRectF(0,0,self.image.shape[0],self.image.shape[1]), self.qimage) shape = self.image.shape[:2] if self.axisOrder == 'col-major' else self.image.shape[:2][::-1]
p.drawImage(QtCore.QRectF(0,0,*shape), self.qimage)
profile('p.drawImage') profile('p.drawImage')
if self.border is not None: if self.border is not None:
p.setPen(self.border) p.setPen(self.border)
@ -398,6 +468,7 @@ class ImageItem(GraphicsObject):
bins = 500 bins = 500
kwds['bins'] = bins kwds['bins'] = bins
stepData = stepData[np.isfinite(stepData)]
hist = np.histogram(stepData, **kwds) hist = np.histogram(stepData, **kwds)
return hist[1][:-1], hist[0] return hist[1][:-1], hist[0]
@ -433,21 +504,6 @@ class ImageItem(GraphicsObject):
self.qimage = None self.qimage = None
self.update() self.update()
#def mousePressEvent(self, ev):
#if self.drawKernel is not None and ev.button() == QtCore.Qt.LeftButton:
#self.drawAt(ev.pos(), ev)
#ev.accept()
#else:
#ev.ignore()
#def mouseMoveEvent(self, ev):
##print "mouse move", ev.pos()
#if self.drawKernel is not None:
#self.drawAt(ev.pos(), ev)
#def mouseReleaseEvent(self, ev):
#pass
def mouseDragEvent(self, ev): def mouseDragEvent(self, ev):
if ev.button() != QtCore.Qt.LeftButton: if ev.button() != QtCore.Qt.LeftButton:
ev.ignore() ev.ignore()
@ -484,24 +540,18 @@ class ImageItem(GraphicsObject):
self.menu.remAct = remAct self.menu.remAct = remAct
return self.menu return self.menu
def hoverEvent(self, ev): def hoverEvent(self, ev):
if not ev.isExit() and self.drawKernel is not None and ev.acceptDrags(QtCore.Qt.LeftButton): if not ev.isExit() and self.drawKernel is not None and ev.acceptDrags(QtCore.Qt.LeftButton):
ev.acceptClicks(QtCore.Qt.LeftButton) ## we don't use the click, but we also don't want anyone else to use it. ev.acceptClicks(QtCore.Qt.LeftButton) ## we don't use the click, but we also don't want anyone else to use it.
ev.acceptClicks(QtCore.Qt.RightButton) ev.acceptClicks(QtCore.Qt.RightButton)
#self.box.setBrush(fn.mkBrush('w'))
elif not ev.isExit() and self.removable: elif not ev.isExit() and self.removable:
ev.acceptClicks(QtCore.Qt.RightButton) ## accept context menu clicks ev.acceptClicks(QtCore.Qt.RightButton) ## accept context menu clicks
#else:
#self.box.setBrush(self.brush)
#self.update()
def tabletEvent(self, ev): def tabletEvent(self, ev):
print(ev.device()) pass
print(ev.pointerType()) #print(ev.device())
print(ev.pressure()) #print(ev.pointerType())
#print(ev.pressure())
def drawAt(self, pos, ev=None): def drawAt(self, pos, ev=None):
pos = [int(pos.x()), int(pos.y())] pos = [int(pos.x()), int(pos.y())]

View File

@ -1,5 +1,4 @@
from .. import getConfigOption
from .GraphicsObject import * from .GraphicsObject import *
from .. import functions as fn from .. import functions as fn
from ..Qt import QtGui, QtCore from ..Qt import QtGui, QtCore
@ -9,12 +8,10 @@ class IsocurveItem(GraphicsObject):
""" """
**Bases:** :class:`GraphicsObject <pyqtgraph.GraphicsObject>` **Bases:** :class:`GraphicsObject <pyqtgraph.GraphicsObject>`
Item displaying an isocurve of a 2D array.To align this item correctly with an Item displaying an isocurve of a 2D array. To align this item correctly with an
ImageItem,call isocurve.setParentItem(image) ImageItem, call ``isocurve.setParentItem(image)``.
""" """
def __init__(self, data=None, level=0, pen='w', axisOrder=None):
def __init__(self, data=None, level=0, pen='w'):
""" """
Create a new isocurve item. Create a new isocurve item.
@ -25,6 +22,9 @@ class IsocurveItem(GraphicsObject):
level The cutoff value at which to draw the isocurve. level The cutoff value at which to draw the isocurve.
pen The color of the curve item. Can be anything valid for pen The color of the curve item. Can be anything valid for
:func:`mkPen <pyqtgraph.mkPen>` :func:`mkPen <pyqtgraph.mkPen>`
axisOrder May be either 'row-major' or 'col-major'. By default this uses
the ``imageAxisOrder``
:ref:`global configuration option <apiref_config>`.
============== =============================================================== ============== ===============================================================
""" """
GraphicsObject.__init__(self) GraphicsObject.__init__(self)
@ -32,9 +32,9 @@ class IsocurveItem(GraphicsObject):
self.level = level self.level = level
self.data = None self.data = None
self.path = None self.path = None
self.axisOrder = getConfigOption('imageAxisOrder') if axisOrder is None else axisOrder
self.setPen(pen) self.setPen(pen)
self.setData(data, level) self.setData(data, level)
def setData(self, data, level=None): def setData(self, data, level=None):
""" """
@ -54,7 +54,6 @@ class IsocurveItem(GraphicsObject):
self.path = None self.path = None
self.prepareGeometryChange() self.prepareGeometryChange()
self.update() self.update()
def setLevel(self, level): def setLevel(self, level):
"""Set the level at which the isocurve is drawn.""" """Set the level at which the isocurve is drawn."""
@ -62,7 +61,6 @@ class IsocurveItem(GraphicsObject):
self.path = None self.path = None
self.prepareGeometryChange() self.prepareGeometryChange()
self.update() self.update()
def setPen(self, *args, **kwargs): def setPen(self, *args, **kwargs):
"""Set the pen used to draw the isocurve. Arguments can be any that are valid """Set the pen used to draw the isocurve. Arguments can be any that are valid
@ -75,18 +73,8 @@ class IsocurveItem(GraphicsObject):
for :func:`mkBrush <pyqtgraph.mkBrush>`""" for :func:`mkBrush <pyqtgraph.mkBrush>`"""
self.brush = fn.mkBrush(*args, **kwargs) self.brush = fn.mkBrush(*args, **kwargs)
self.update() self.update()
def updateLines(self, data, level): def updateLines(self, data, level):
##print "data:", data
##print "level", level
#lines = fn.isocurve(data, level)
##print len(lines)
#self.path = QtGui.QPainterPath()
#for line in lines:
#self.path.moveTo(*line[0])
#self.path.lineTo(*line[1])
#self.update()
self.setData(data, level) self.setData(data, level)
def boundingRect(self): def boundingRect(self):
@ -100,7 +88,13 @@ class IsocurveItem(GraphicsObject):
if self.data is None: if self.data is None:
self.path = None self.path = None
return return
lines = fn.isocurve(self.data, self.level, connected=True, extendToEdge=True)
if self.axisOrder == 'row-major':
data = self.data.T
else:
data = self.data
lines = fn.isocurve(data, self.level, connected=True, extendToEdge=True)
self.path = QtGui.QPainterPath() self.path = QtGui.QPainterPath()
for line in lines: for line in lines:
self.path.moveTo(*line[0]) self.path.moveTo(*line[0])

View File

@ -21,6 +21,7 @@ from math import cos, sin
from .. import functions as fn from .. import functions as fn
from .GraphicsObject import GraphicsObject from .GraphicsObject import GraphicsObject
from .UIGraphicsItem import UIGraphicsItem from .UIGraphicsItem import UIGraphicsItem
from .. import getConfigOption
__all__ = [ __all__ = [
'ROI', 'ROI',
@ -1016,14 +1017,11 @@ class ROI(GraphicsObject):
If returnSlice is set to False, the function returns a pair of tuples with the values that would have If returnSlice is set to False, the function returns a pair of tuples with the values that would have
been used to generate the slice objects. ((ax0Start, ax0Stop), (ax1Start, ax1Stop)) been used to generate the slice objects. ((ax0Start, ax0Stop), (ax1Start, ax1Stop))
If the slice can not be computed (usually because the scene/transforms are not properly If the slice cannot be computed (usually because the scene/transforms are not properly
constructed yet), then the method returns None. constructed yet), then the method returns None.
""" """
#print "getArraySlice"
## Determine shape of array along ROI axes ## Determine shape of array along ROI axes
dShape = (data.shape[axes[0]], data.shape[axes[1]]) dShape = (data.shape[axes[0]], data.shape[axes[1]])
#print " dshape", dShape
## Determine transform that maps ROI bounding box to image coordinates ## Determine transform that maps ROI bounding box to image coordinates
try: try:
@ -1032,25 +1030,28 @@ class ROI(GraphicsObject):
return None return None
## Modify transform to scale from image coords to data coords ## Modify transform to scale from image coords to data coords
#m = QtGui.QTransform() axisOrder = img.axisOrder
tr.scale(float(dShape[0]) / img.width(), float(dShape[1]) / img.height()) if axisOrder == 'row-major':
#tr = tr * m tr.scale(float(dShape[1]) / img.width(), float(dShape[0]) / img.height())
else:
tr.scale(float(dShape[0]) / img.width(), float(dShape[1]) / img.height())
## Transform ROI bounds into data bounds ## Transform ROI bounds into data bounds
dataBounds = tr.mapRect(self.boundingRect()) dataBounds = tr.mapRect(self.boundingRect())
#print " boundingRect:", self.boundingRect()
#print " dataBounds:", dataBounds
## Intersect transformed ROI bounds with data bounds ## Intersect transformed ROI bounds with data bounds
intBounds = dataBounds.intersected(QtCore.QRectF(0, 0, dShape[0], dShape[1])) if axisOrder == 'row-major':
#print " intBounds:", intBounds intBounds = dataBounds.intersected(QtCore.QRectF(0, 0, dShape[1], dShape[0]))
else:
intBounds = dataBounds.intersected(QtCore.QRectF(0, 0, dShape[0], dShape[1]))
## Determine index values to use when referencing the array. ## Determine index values to use when referencing the array.
bounds = ( bounds = (
(int(min(intBounds.left(), intBounds.right())), int(1+max(intBounds.left(), intBounds.right()))), (int(min(intBounds.left(), intBounds.right())), int(1+max(intBounds.left(), intBounds.right()))),
(int(min(intBounds.bottom(), intBounds.top())), int(1+max(intBounds.bottom(), intBounds.top()))) (int(min(intBounds.bottom(), intBounds.top())), int(1+max(intBounds.bottom(), intBounds.top())))
) )
#print " bounds:", bounds if axisOrder == 'row-major':
bounds = bounds[::-1]
if returnSlice: if returnSlice:
## Create slice objects ## Create slice objects
@ -1074,7 +1075,10 @@ class ROI(GraphicsObject):
Used to determine the relationship between the Used to determine the relationship between the
ROI and the boundaries of *data*. ROI and the boundaries of *data*.
axes (length-2 tuple) Specifies the axes in *data* that axes (length-2 tuple) Specifies the axes in *data* that
correspond to the x and y axes of *img*. correspond to the (x, y) axes of *img*. If the
image's axis order is set to
'row-major', then the axes are instead specified in
(y, x) order.
returnMappedCoords (bool) If True, the array slice is returned along returnMappedCoords (bool) If True, the array slice is returned along
with a corresponding array of coordinates that were with a corresponding array of coordinates that were
used to extract data from the original array. used to extract data from the original array.
@ -1099,7 +1103,8 @@ class ROI(GraphicsObject):
shape, vectors, origin = self.getAffineSliceParams(data, img, axes, fromBoundingRect=fromBR) shape, vectors, origin = self.getAffineSliceParams(data, img, axes, fromBoundingRect=fromBR)
if not returnMappedCoords: if not returnMappedCoords:
return fn.affineSlice(data, shape=shape, vectors=vectors, origin=origin, axes=axes, **kwds) rgn = fn.affineSlice(data, shape=shape, vectors=vectors, origin=origin, axes=axes, **kwds)
return rgn
else: else:
kwds['returnCoords'] = True kwds['returnCoords'] = True
result, coords = fn.affineSlice(data, shape=shape, vectors=vectors, origin=origin, axes=axes, **kwds) result, coords = fn.affineSlice(data, shape=shape, vectors=vectors, origin=origin, axes=axes, **kwds)
@ -1114,29 +1119,34 @@ class ROI(GraphicsObject):
(shape, vectors, origin) to extract a subset of *data* using this ROI (shape, vectors, origin) to extract a subset of *data* using this ROI
and *img* to specify the subset. and *img* to specify the subset.
If *fromBoundingRect* is True, then the ROI's bounding rectangle is used
rather than the shape of the ROI.
See :func:`getArrayRegion <pyqtgraph.ROI.getArrayRegion>` for more information. See :func:`getArrayRegion <pyqtgraph.ROI.getArrayRegion>` for more information.
""" """
if self.scene() is not img.scene(): if self.scene() is not img.scene():
raise Exception("ROI and target item must be members of the same scene.") raise Exception("ROI and target item must be members of the same scene.")
origin = self.mapToItem(img, QtCore.QPointF(0, 0)) origin = img.mapToData(self.mapToItem(img, QtCore.QPointF(0, 0)))
## vx and vy point in the directions of the slice axes, but must be scaled properly ## vx and vy point in the directions of the slice axes, but must be scaled properly
vx = self.mapToItem(img, QtCore.QPointF(1, 0)) - origin vx = img.mapToData(self.mapToItem(img, QtCore.QPointF(1, 0))) - origin
vy = self.mapToItem(img, QtCore.QPointF(0, 1)) - origin vy = img.mapToData(self.mapToItem(img, QtCore.QPointF(0, 1))) - origin
lvx = np.sqrt(vx.x()**2 + vx.y()**2) lvx = np.sqrt(vx.x()**2 + vx.y()**2)
lvy = np.sqrt(vy.x()**2 + vy.y()**2) lvy = np.sqrt(vy.x()**2 + vy.y()**2)
pxLen = img.width() / float(data.shape[axes[0]]) #pxLen = img.width() / float(data.shape[axes[0]])
#img.width is number of pixels, not width of item. ##img.width is number of pixels, not width of item.
#need pxWidth and pxHeight instead of pxLen ? ##need pxWidth and pxHeight instead of pxLen ?
sx = pxLen / lvx #sx = pxLen / lvx
sy = pxLen / lvy #sy = pxLen / lvy
sx = 1.0 / lvx
sy = 1.0 / lvy
vectors = ((vx.x()*sx, vx.y()*sx), (vy.x()*sy, vy.y()*sy)) vectors = ((vx.x()*sx, vx.y()*sx), (vy.x()*sy, vy.y()*sy))
if fromBoundingRect is True: if fromBoundingRect is True:
shape = self.boundingRect().width(), self.boundingRect().height() shape = self.boundingRect().width(), self.boundingRect().height()
origin = self.mapToItem(img, self.boundingRect().topLeft()) origin = img.mapToData(self.mapToItem(img, self.boundingRect().topLeft()))
origin = (origin.x(), origin.y()) origin = (origin.x(), origin.y())
else: else:
shape = self.state['size'] shape = self.state['size']
@ -1144,6 +1154,11 @@ class ROI(GraphicsObject):
shape = [abs(shape[0]/sx), abs(shape[1]/sy)] shape = [abs(shape[0]/sx), abs(shape[1]/sy)]
if img.axisOrder == 'row-major':
# transpose output
vectors = vectors[::-1]
shape = shape[::-1]
return shape, vectors, origin return shape, vectors, origin
def renderShapeMask(self, width, height): def renderShapeMask(self, width, height):
@ -1166,7 +1181,7 @@ class ROI(GraphicsObject):
p.translate(-bounds.topLeft()) p.translate(-bounds.topLeft())
p.drawPath(shape) p.drawPath(shape)
p.end() p.end()
mask = fn.imageToArray(im)[:,:,0].astype(float) / 255. mask = fn.imageToArray(im, transpose=True)[:,:,0].astype(float) / 255.
return mask return mask
def getGlobalTransform(self, relativeTo=None): def getGlobalTransform(self, relativeTo=None):
@ -1639,6 +1654,8 @@ class MultiRectROI(QtGui.QGraphicsObject):
## make sure orthogonal axis is the same size ## make sure orthogonal axis is the same size
## (sometimes fp errors cause differences) ## (sometimes fp errors cause differences)
if img.axisOrder == 'row-major':
axes = axes[::-1]
ms = min([r.shape[axes[1]] for r in rgns]) ms = min([r.shape[axes[1]] for r in rgns])
sl = [slice(None)] * rgns[0].ndim sl = [slice(None)] * rgns[0].ndim
sl[axes[1]] = slice(0,ms) sl[axes[1]] = slice(0,ms)
@ -1998,7 +2015,7 @@ class PolyLineROI(ROI):
p.lineTo(self.handles[0]['item'].pos()) p.lineTo(self.handles[0]['item'].pos())
return p return p
def getArrayRegion(self, data, img, axes=(0,1)): def getArrayRegion(self, data, img, axes=(0,1), **kwds):
""" """
Return the result of ROI.getArrayRegion(), masked by the shape of the Return the result of ROI.getArrayRegion(), masked by the shape of the
ROI. Values outside the ROI shape are set to 0. ROI. Values outside the ROI shape are set to 0.
@ -2006,8 +2023,15 @@ class PolyLineROI(ROI):
br = self.boundingRect() br = self.boundingRect()
if br.width() > 1000: if br.width() > 1000:
raise Exception() raise Exception()
sliced = ROI.getArrayRegion(self, data, img, axes=axes, fromBoundingRect=True) sliced = ROI.getArrayRegion(self, data, img, axes=axes, fromBoundingRect=True, **kwds)
mask = self.renderShapeMask(sliced.shape[axes[0]], sliced.shape[axes[1]])
if img.axisOrder == 'col-major':
mask = self.renderShapeMask(sliced.shape[axes[0]], sliced.shape[axes[1]])
else:
mask = self.renderShapeMask(sliced.shape[axes[1]], sliced.shape[axes[0]])
mask = mask.T
# reshape mask to ensure it is applied to the correct data axes
shape = [1] * data.ndim shape = [1] * data.ndim
shape[axes[0]] = sliced.shape[axes[0]] shape[axes[0]] = sliced.shape[axes[0]]
shape[axes[1]] = sliced.shape[axes[1]] shape[axes[1]] = sliced.shape[axes[1]]
@ -2085,7 +2109,7 @@ class LineSegmentROI(ROI):
return p return p
def getArrayRegion(self, data, img, axes=(0,1)): def getArrayRegion(self, data, img, axes=(0,1), order=1, **kwds):
""" """
Use the position of this ROI relative to an imageItem to pull a slice Use the position of this ROI relative to an imageItem to pull a slice
from an array. from an array.
@ -2101,7 +2125,7 @@ class LineSegmentROI(ROI):
for i in range(len(imgPts)-1): for i in range(len(imgPts)-1):
d = Point(imgPts[i+1] - imgPts[i]) d = Point(imgPts[i+1] - imgPts[i])
o = Point(imgPts[i]) o = Point(imgPts[i])
r = fn.affineSlice(data, shape=(int(d.length()),), vectors=[Point(d.norm())], origin=o, axes=axes, order=1) r = fn.affineSlice(data, shape=(int(d.length()),), vectors=[Point(d.norm())], origin=o, axes=axes, order=order, **kwds)
rgns.append(r) rgns.append(r)
return np.concatenate(rgns, axis=axes[0]) return np.concatenate(rgns, axis=axes[0])

View File

@ -3,22 +3,22 @@ import pytest
from pyqtgraph.Qt import QtCore, QtGui, QtTest from pyqtgraph.Qt import QtCore, QtGui, QtTest
import numpy as np import numpy as np
import pyqtgraph as pg import pyqtgraph as pg
from pyqtgraph.tests import assertImageApproved from pyqtgraph.tests import assertImageApproved, TransposedImageItem
app = pg.mkQApp() app = pg.mkQApp()
def test_ImageItem(): def test_ImageItem(transpose=False):
w = pg.GraphicsWindow() w = pg.GraphicsWindow()
view = pg.ViewBox() view = pg.ViewBox()
w.setCentralWidget(view) w.setCentralWidget(view)
w.resize(200, 200) w.resize(200, 200)
w.show() w.show()
img = pg.ImageItem(border=0.5) img = TransposedImageItem(border=0.5, transpose=transpose)
view.addItem(img) view.addItem(img)
# test mono float # test mono float
np.random.seed(0) np.random.seed(0)
data = np.random.normal(size=(20, 20)) data = np.random.normal(size=(20, 20))
@ -60,6 +60,18 @@ def test_ImageItem():
img.setLevels([127, 128]) img.setLevels([127, 128])
assertImageApproved(w, 'imageitem/gradient_mono_byte_levels', 'Mono byte gradient w/ levels to isolate diagonal.') assertImageApproved(w, 'imageitem/gradient_mono_byte_levels', 'Mono byte gradient w/ levels to isolate diagonal.')
# test monochrome image
data = np.zeros((10, 10), dtype='uint8')
data[:5,:5] = 1
data[5:,5:] = 1
img.setImage(data)
assertImageApproved(w, 'imageitem/monochrome', 'Ubyte image with only 0,1 values.')
# test bool
data = data.astype(bool)
img.setImage(data)
assertImageApproved(w, 'imageitem/bool', 'Boolean mask.')
# test RGBA byte # test RGBA byte
data = np.zeros((100, 100, 4), dtype='ubyte') data = np.zeros((100, 100, 4), dtype='ubyte')
data[..., 0] = np.linspace(0, 255, 100).reshape(100, 1) data[..., 0] = np.linspace(0, 255, 100).reshape(100, 1)
@ -77,7 +89,7 @@ def test_ImageItem():
assertImageApproved(w, 'imageitem/gradient_rgba_float', 'RGBA float gradient.') assertImageApproved(w, 'imageitem/gradient_rgba_float', 'RGBA float gradient.')
# checkerboard to test alpha # checkerboard to test alpha
img2 = pg.ImageItem() img2 = TransposedImageItem(transpose=transpose)
img2.setImage(np.fromfunction(lambda x,y: (x+y)%2, (10, 10)), levels=[-1,2]) img2.setImage(np.fromfunction(lambda x,y: (x+y)%2, (10, 10)), levels=[-1,2])
view.addItem(img2) view.addItem(img2)
img2.scale(10, 10) img2.scale(10, 10)
@ -103,9 +115,23 @@ def test_ImageItem():
img.setAutoDownsample(True) img.setAutoDownsample(True)
assertImageApproved(w, 'imageitem/resolution_with_downsampling_x', 'Resolution test with downsampling axross x axis.') assertImageApproved(w, 'imageitem/resolution_with_downsampling_x', 'Resolution test with downsampling axross x axis.')
assert img._lastDownsample == (4, 1)
img.setImage(data.T, levels=[-1, 1]) img.setImage(data.T, levels=[-1, 1])
assertImageApproved(w, 'imageitem/resolution_with_downsampling_y', 'Resolution test with downsampling across y axis.') assertImageApproved(w, 'imageitem/resolution_with_downsampling_y', 'Resolution test with downsampling across y axis.')
assert img._lastDownsample == (1, 4)
view.hide()
def test_ImageItem_axisorder():
# All image tests pass again using the opposite axis order
origMode = pg.getConfigOption('imageAxisOrder')
altMode = 'row-major' if origMode == 'col-major' else 'col-major'
pg.setConfigOptions(imageAxisOrder=altMode)
try:
test_ImageItem(transpose=True)
finally:
pg.setConfigOptions(imageAxisOrder=origMode)
@pytest.mark.skipif(pg.Qt.USE_PYSIDE, reason="pyside does not have qWait") @pytest.mark.skipif(pg.Qt.USE_PYSIDE, reason="pyside does not have qWait")

View File

@ -2,13 +2,13 @@ import numpy as np
import pytest import pytest
import pyqtgraph as pg import pyqtgraph as pg
from pyqtgraph.Qt import QtCore, QtTest from pyqtgraph.Qt import QtCore, QtTest
from pyqtgraph.tests import assertImageApproved, mouseMove, mouseDrag, mouseClick from pyqtgraph.tests import assertImageApproved, mouseMove, mouseDrag, mouseClick, TransposedImageItem
app = pg.mkQApp() app = pg.mkQApp()
def test_getArrayRegion(): def test_getArrayRegion(transpose=False):
pr = pg.PolyLineROI([[0, 0], [27, 0], [0, 28]], closed=True) pr = pg.PolyLineROI([[0, 0], [27, 0], [0, 28]], closed=True)
pr.setPos(1, 1) pr.setPos(1, 1)
rois = [ rois = [
@ -21,10 +21,23 @@ def test_getArrayRegion():
# For some ROIs, resize should not be used. # For some ROIs, resize should not be used.
testResize = not isinstance(roi, pg.PolyLineROI) testResize = not isinstance(roi, pg.PolyLineROI)
check_getArrayRegion(roi, 'roi/'+name, testResize) origMode = pg.getConfigOption('imageAxisOrder')
try:
if transpose:
pg.setConfigOptions(imageAxisOrder='row-major')
check_getArrayRegion(roi, 'roi/'+name, testResize, transpose=True)
else:
pg.setConfigOptions(imageAxisOrder='col-major')
check_getArrayRegion(roi, 'roi/'+name, testResize)
finally:
pg.setConfigOptions(imageAxisOrder=origMode)
def test_getArrayRegion_axisorder():
test_getArrayRegion(transpose=True)
def check_getArrayRegion(roi, name, testResize=True): def check_getArrayRegion(roi, name, testResize=True, transpose=False):
initState = roi.getState() initState = roi.getState()
#win = pg.GraphicsLayoutWidget() #win = pg.GraphicsLayoutWidget()
@ -50,6 +63,7 @@ def check_getArrayRegion(roi, name, testResize=True):
img1 = pg.ImageItem(border='w') img1 = pg.ImageItem(border='w')
img2 = pg.ImageItem(border='w') img2 = pg.ImageItem(border='w')
vb1.addItem(img1) vb1.addItem(img1)
vb2.addItem(img2) vb2.addItem(img2)
@ -60,6 +74,9 @@ def check_getArrayRegion(roi, name, testResize=True):
data[:, :, 2, :] += 10 data[:, :, 2, :] += 10
data[:, :, :, 3] += 10 data[:, :, :, 3] += 10
if transpose:
data = data.transpose(0, 2, 1, 3)
img1.setImage(data[0, ..., 0]) img1.setImage(data[0, ..., 0])
vb1.setAspectLocked() vb1.setAspectLocked()
vb1.enableAutoRange(True, True) vb1.enableAutoRange(True, True)
@ -67,8 +84,14 @@ def check_getArrayRegion(roi, name, testResize=True):
roi.setZValue(10) roi.setZValue(10)
vb1.addItem(roi) vb1.addItem(roi)
if isinstance(roi, pg.RectROI):
if transpose:
assert roi.getAffineSliceParams(data, img1, axes=(1, 2)) == ([28.0, 27.0], ((1.0, 0.0), (0.0, 1.0)), (1.0, 1.0))
else:
assert roi.getAffineSliceParams(data, img1, axes=(1, 2)) == ([27.0, 28.0], ((1.0, 0.0), (0.0, 1.0)), (1.0, 1.0))
rgn = roi.getArrayRegion(data, img1, axes=(1, 2)) rgn = roi.getArrayRegion(data, img1, axes=(1, 2))
assert np.all((rgn == data[:, 1:-2, 1:-2, :]) | (rgn == 0)) #assert np.all((rgn == data[:, 1:-2, 1:-2, :]) | (rgn == 0))
img2.setImage(rgn[0, ..., 0]) img2.setImage(rgn[0, ..., 0])
vb2.setAspectLocked() vb2.setAspectLocked()
vb2.enableAutoRange(True, True) vb2.enableAutoRange(True, True)
@ -122,6 +145,9 @@ def check_getArrayRegion(roi, name, testResize=True):
img2.setImage(rgn[0, ..., 0]) img2.setImage(rgn[0, ..., 0])
app.processEvents() app.processEvents()
assertImageApproved(win, name+'/roi_getarrayregion_anisotropic', 'Simple ROI region selection, image scaled anisotropically.') assertImageApproved(win, name+'/roi_getarrayregion_anisotropic', 'Simple ROI region selection, image scaled anisotropically.')
# allow the roi to be re-used
roi.scene().removeItem(roi)
def test_PolyLineROI(): def test_PolyLineROI():

View File

@ -30,6 +30,7 @@ from ..graphicsItems.GradientEditorItem import addGradientListToDocstring
from .. import ptime as ptime from .. import ptime as ptime
from .. import debug as debug from .. import debug as debug
from ..SignalProxy import SignalProxy from ..SignalProxy import SignalProxy
from .. import getConfigOption
try: try:
from bottleneck import nanmin, nanmax from bottleneck import nanmin, nanmax
@ -203,9 +204,10 @@ class ImageView(QtGui.QWidget):
""" """
Set the image to be displayed in the widget. Set the image to be displayed in the widget.
================== ======================================================================= ================== ===========================================================================
**Arguments:** **Arguments:**
img (numpy array) the image to be displayed. img (numpy array) the image to be displayed. See :func:`ImageItem.setImage` and
*notes* below.
xvals (numpy array) 1D array of z-axis values corresponding to the third axis xvals (numpy array) 1D array of z-axis values corresponding to the third axis
in a 3D image. For video, this array should contain the time of each frame. in a 3D image. For video, this array should contain the time of each frame.
autoRange (bool) whether to scale/pan the view to fit the image. autoRange (bool) whether to scale/pan the view to fit the image.
@ -222,7 +224,19 @@ class ImageView(QtGui.QWidget):
and *scale*. and *scale*.
autoHistogramRange If True, the histogram y-range is automatically scaled to fit the autoHistogramRange If True, the histogram y-range is automatically scaled to fit the
image data. image data.
================== ======================================================================= ================== ===========================================================================
**Notes:**
For backward compatibility, image data is assumed to be in column-major order (column, row).
However, most image data is stored in row-major order (row, column) and will need to be
transposed before calling setImage()::
imageview.setImage(imagedata.T)
This requirement can be changed by the ``imageAxisOrder``
:ref:`global configuration option <apiref_config>`.
""" """
profiler = debug.Profiler() profiler = debug.Profiler()
@ -239,28 +253,22 @@ class ImageView(QtGui.QWidget):
self.image = img self.image = img
self.imageDisp = None self.imageDisp = None
if xvals is not None:
self.tVals = xvals
elif hasattr(img, 'xvals'):
try:
self.tVals = img.xvals(0)
except:
self.tVals = np.arange(img.shape[0])
else:
self.tVals = np.arange(img.shape[0])
profiler() profiler()
if axes is None: if axes is None:
x,y = (0, 1) if self.imageItem.axisOrder == 'col-major' else (1, 0)
if img.ndim == 2: if img.ndim == 2:
self.axes = {'t': None, 'x': 0, 'y': 1, 'c': None} self.axes = {'t': None, 'x': x, 'y': y, 'c': None}
elif img.ndim == 3: elif img.ndim == 3:
# Ambiguous case; make a guess
if img.shape[2] <= 4: if img.shape[2] <= 4:
self.axes = {'t': None, 'x': 0, 'y': 1, 'c': 2} self.axes = {'t': None, 'x': x, 'y': y, 'c': 2}
else: else:
self.axes = {'t': 0, 'x': 1, 'y': 2, 'c': None} self.axes = {'t': 0, 'x': x+1, 'y': y+1, 'c': None}
elif img.ndim == 4: elif img.ndim == 4:
self.axes = {'t': 0, 'x': 1, 'y': 2, 'c': 3} # Even more ambiguous; just assume the default
self.axes = {'t': 0, 'x': x+1, 'y': y+1, 'c': 3}
else: else:
raise Exception("Can not interpret image with dimensions %s" % (str(img.shape))) raise Exception("Can not interpret image with dimensions %s" % (str(img.shape)))
elif isinstance(axes, dict): elif isinstance(axes, dict):
@ -274,6 +282,18 @@ class ImageView(QtGui.QWidget):
for x in ['t', 'x', 'y', 'c']: for x in ['t', 'x', 'y', 'c']:
self.axes[x] = self.axes.get(x, None) self.axes[x] = self.axes.get(x, None)
axes = self.axes
if xvals is not None:
self.tVals = xvals
elif axes['t'] is not None:
if hasattr(img, 'xvals'):
try:
self.tVals = img.xvals(axes['t'])
except:
self.tVals = np.arange(img.shape[axes['t']])
else:
self.tVals = np.arange(img.shape[axes['t']])
profiler() profiler()
@ -455,7 +475,7 @@ class ImageView(QtGui.QWidget):
def setCurrentIndex(self, ind): def setCurrentIndex(self, ind):
"""Set the currently displayed frame index.""" """Set the currently displayed frame index."""
self.currentIndex = np.clip(ind, 0, self.getProcessedImage().shape[0]-1) self.currentIndex = np.clip(ind, 0, self.getProcessedImage().shape[self.axes['t']]-1)
self.updateImage() self.updateImage()
self.ignoreTimeLine = True self.ignoreTimeLine = True
self.timeLine.setValue(self.tVals[self.currentIndex]) self.timeLine.setValue(self.tVals[self.currentIndex])
@ -543,6 +563,7 @@ class ImageView(QtGui.QWidget):
axes = (1, 2) axes = (1, 2)
else: else:
return return
data, coords = self.roi.getArrayRegion(image.view(np.ndarray), self.imageItem, axes, returnMappedCoords=True) data, coords = self.roi.getArrayRegion(image.view(np.ndarray), self.imageItem, axes, returnMappedCoords=True)
if data is not None: if data is not None:
while data.ndim > 1: while data.ndim > 1:
@ -638,11 +659,21 @@ class ImageView(QtGui.QWidget):
if autoHistogramRange: if autoHistogramRange:
self.ui.histogram.setHistogramRange(self.levelMin, self.levelMax) self.ui.histogram.setHistogramRange(self.levelMin, self.levelMax)
if self.axes['t'] is None:
self.imageItem.updateImage(image) # Transpose image into order expected by ImageItem
if self.imageItem.axisOrder == 'col-major':
axorder = ['t', 'x', 'y', 'c']
else: else:
axorder = ['t', 'y', 'x', 'c']
axorder = [self.axes[ax] for ax in axorder if self.axes[ax] is not None]
image = image.transpose(axorder)
# Select time index
if self.axes['t'] is not None:
self.ui.roiPlot.show() self.ui.roiPlot.show()
self.imageItem.updateImage(image[self.currentIndex]) image = image[self.currentIndex]
self.imageItem.updateImage(image)
def timeIndex(self, slider): def timeIndex(self, slider):

View File

@ -7,5 +7,6 @@ def test_nan_image():
img = np.ones((10,10)) img = np.ones((10,10))
img[0,0] = np.nan img[0,0] = np.nan
v = pg.image(img) v = pg.image(img)
v.imageItem.getHistogram()
app.processEvents() app.processEvents()
v.window().close() v.window().close()

View File

@ -1,2 +1,2 @@
from .image_testing import assertImageApproved from .image_testing import assertImageApproved, TransposedImageItem
from .ui_testing import mousePress, mouseMove, mouseRelease, mouseDrag, mouseClick from .ui_testing import mousePress, mouseMove, mouseRelease, mouseDrag, mouseClick

View File

@ -42,7 +42,7 @@ Procedure for unit-testing with images:
# pyqtgraph should be tested against. When adding or changing test 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 # create and push a new tag and update this variable. To test locally, begin
# by creating the tag in your ~/.pyqtgraph/test-data repository. # by creating the tag in your ~/.pyqtgraph/test-data repository.
testDataTag = 'test-data-5' testDataTag = 'test-data-6'
import time import time
@ -67,6 +67,30 @@ from .. import ImageItem, TextItem
tester = None tester = None
# Convenient stamp used for ensuring image orientation is correct
axisImg = [
" 1 1 1 ",
" 1 1 1 1 1 1 ",
" 1 1 1 1 1 1 1 1 1 1",
" 1 1 1 1 1 ",
" 1 1 1 1 1 1 ",
" 1 1 ",
" 1 1 ",
" 1 ",
" ",
" 1 ",
" 1 ",
" 1 ",
"1 1 1 1 1 ",
"1 1 1 1 1 ",
" 1 1 1 ",
" 1 1 1 ",
" 1 ",
" 1 ",
]
axisImg = np.array([map(int, row[::2].replace(' ', '0')) for row in axisImg])
def getTester(): def getTester():
global tester global tester
@ -159,12 +183,15 @@ def assertImageApproved(image, standardFile, message=None, **kwargs):
if bool(os.getenv('PYQTGRAPH_PRINT_TEST_STATE', False)): if bool(os.getenv('PYQTGRAPH_PRINT_TEST_STATE', False)):
print(graphstate) print(graphstate)
if os.getenv('PYQTGRAPH_AUDIT_ALL') == '1':
raise Exception("Image test passed, but auditing due to PYQTGRAPH_AUDIT_ALL evnironment variable.")
except Exception: except Exception:
if stdFileName in gitStatus(dataPath): if stdFileName in gitStatus(dataPath):
print("\n\nWARNING: unit test failed against modified standard " print("\n\nWARNING: unit test failed against modified standard "
"image %s.\nTo revert this file, run `cd %s; git checkout " "image %s.\nTo revert this file, run `cd %s; git checkout "
"%s`\n" % (stdFileName, dataPath, standardFile)) "%s`\n" % (stdFileName, dataPath, standardFile))
if os.getenv('PYQTGRAPH_AUDIT') == '1': if os.getenv('PYQTGRAPH_AUDIT') == '1' or os.getenv('PYQTGRAPH_AUDIT_ALL') == '1':
sys.excepthook(*sys.exc_info()) sys.excepthook(*sys.exc_info())
getTester().test(image, stdImage, message) getTester().test(image, stdImage, message)
stdPath = os.path.dirname(stdFileName) stdPath = os.path.dirname(stdFileName)
@ -344,7 +371,7 @@ class ImageTester(QtGui.QWidget):
for i, v in enumerate(self.views): for i, v in enumerate(self.views):
v.setAspectLocked(1) v.setAspectLocked(1)
v.invertY() v.invertY()
v.image = ImageItem() v.image = ImageItem(axisOrder='row-major')
v.image.setAutoDownsample(True) v.image.setAutoDownsample(True)
v.addItem(v.image) v.addItem(v.image)
v.label = TextItem(labelText[i]) v.label = TextItem(labelText[i])
@ -371,9 +398,9 @@ class ImageTester(QtGui.QWidget):
message += '\nImage1: %s %s Image2: %s %s' % (im1.shape, im1.dtype, im2.shape, im2.dtype) message += '\nImage1: %s %s Image2: %s %s' % (im1.shape, im1.dtype, im2.shape, im2.dtype)
self.label.setText(message) self.label.setText(message)
self.views[0].image.setImage(im1.transpose(1, 0, 2)) self.views[0].image.setImage(im1)
self.views[1].image.setImage(im2.transpose(1, 0, 2)) self.views[1].image.setImage(im2)
diff = makeDiffImage(im1, im2).transpose(1, 0, 2) diff = makeDiffImage(im1, im2)
self.views[2].image.setImage(diff) self.views[2].image.setImage(diff)
self.views[0].autoRange() self.views[0].autoRange()
@ -584,3 +611,15 @@ def transformStr(t):
def indent(s, pfx): def indent(s, pfx):
return '\n'.join([pfx+line for line in s.split('\n')]) return '\n'.join([pfx+line for line in s.split('\n')])
class TransposedImageItem(ImageItem):
# used for testing image axis order; we can test row-major and col-major using
# the same test images
def __init__(self, *args, **kwds):
self.__transpose = kwds.pop('transpose', False)
ImageItem.__init__(self, *args, **kwds)
def setImage(self, image=None, **kwds):
if image is not None and self.__transpose is True:
image = np.swapaxes(image, 0, 1)
return ImageItem.setImage(self, image, **kwds)

View File

@ -63,7 +63,7 @@ class GraphicsView(QtGui.QGraphicsView):
:func:`mkColor <pyqtgraph.mkColor>`. By :func:`mkColor <pyqtgraph.mkColor>`. By
default, the background color is determined using the default, the background color is determined using the
'backgroundColor' configuration option (see 'backgroundColor' configuration option (see
:func:`setConfigOption <pyqtgraph.setConfigOption>`. :func:`setConfigOptions <pyqtgraph.setConfigOptions>`).
============== ============================================================ ============== ============================================================
""" """