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::
:maxdepth: 2
config_options
functions
graphicsItems/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,
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:
- create a flowchart and two plots

View File

@ -17,6 +17,9 @@ import numpy as np
from pyqtgraph.Qt import QtCore, QtGui
import pyqtgraph as pg
# Interpret image data as row-major instead of col-major
pg.setConfigOptions(imageAxisOrder='row-major')
app = QtGui.QApplication([])
## 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 = 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

View File

@ -11,6 +11,7 @@ import pyqtgraph as pg
from pyqtgraph.Qt import QtCore, QtGui
import numpy as np
pg.setConfigOptions(imageAxisOrder='row-major')
## Create image to display
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.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
app = QtGui.QApplication([])

View File

@ -8,23 +8,15 @@ from pyqtgraph.Qt import QtCore, QtGui
import numpy as np
import pyqtgraph as pg
pg.setConfigOptions(imageAxisOrder='row-major')
## create GUI
app = QtGui.QApplication([])
w = pg.GraphicsWindow(size=(800,800), border=True)
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.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
@ -37,6 +29,11 @@ arr[:, 75] = 5
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
im1 = pg.ImageItem(arr)
im2 = pg.ImageItem(arr)
@ -44,6 +41,7 @@ v.addItem(im1)
v.addItem(im2)
im2.moveBy(110, 20)
v.setRange(QtCore.QRectF(0, 0, 200, 120))
im1.scale(0.8, 0.5)
im3 = pg.ImageItem()
v2 = w.addViewBox(1,0)

View File

@ -103,6 +103,9 @@ def mkData():
if dtype[0] != 'float':
data = np.clip(data, 0, mx)
data = data.astype(dt)
data[:, 10, 10:50] = mx
data[:, 9:12, 48] = mx
data[:, 8:13, 47] = mx
cache = {dtype: data} # clear to save memory (but keep one to prevent unnecessary regeneration)
data = cache[dtype]

View File

@ -12,8 +12,11 @@ import pyqtgraph as pg
from pyqtgraph.Qt import QtCore, QtGui
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.setWindowTitle('pyqtgraph example: Image Analysis')
@ -57,10 +60,10 @@ win.show()
# Generate image data
data = np.random.normal(size=(100, 200))
data = np.random.normal(size=(200, 100))
data[20:80, 20:80] += 2.
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)
hist.setLevels(data.min(), data.max())
@ -79,7 +82,7 @@ p1.autoRange()
def updatePlot():
global img, roi, data, p2
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)
updatePlot()

View File

@ -3,6 +3,7 @@ from .Qt import QtCore, QtGui
from .Point import Point
import numpy as np
class SRTTransform(QtGui.QTransform):
"""Transform that can always be represented as a combination of 3 matrices: scale * rotate * translate
This transform has no shear; angles are always preserved.
@ -165,6 +166,7 @@ class SRTTransform(QtGui.QTransform):
def matrix(self):
return np.array([[self.m11(), self.m12(), self.m13()],[self.m21(), self.m22(), self.m23()],[self.m31(), self.m32(), self.m33()]])
if __name__ == '__main__':
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
'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
'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):
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
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):
"""Return the value of a single global configuration option.
"""
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':
s = 2**(data.itemsize*8 - 1)
levels = np.array([-s, s-1])
elif data.dtype.kind == 'b':
levels = np.array([0,1])
else:
raise Exception('levels argument is required for float input types')
if not isinstance(levels, np.ndarray):
@ -1727,7 +1729,7 @@ def isosurface(data, level):
See Paul Bourke, "Polygonising a Scalar Field"
(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
Returns an array of vertex coordinates (Nv, 3) and an array of
@ -2079,7 +2081,10 @@ def isosurface(data, level):
else:
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
mask = data < level

View File

@ -37,9 +37,6 @@ class GraphicsItem(object):
if register:
GraphicsScene.registerObject(self) ## workaround for pyqt bug in graphicsscene.items()
def getViewWidget(self):
"""
Return the view widget for this item.
@ -95,7 +92,6 @@ class GraphicsItem(object):
def forgetViewBox(self):
self._viewBox = None
def deviceTransform(self, viewportTransform=None):
"""
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
def regionChanged(self):
#if self.imageItem is not None:
#self.imageItem.setLevels(self.region.getRegion())
if self.imageItem() is not None:
self.imageItem().setLevels(self.region.getRegion())
self.sigLevelChangeFinished.emit(self)
#self.update()

View File

@ -7,6 +7,8 @@ from .. import functions as fn
from .. import debug as debug
from .GraphicsObject import GraphicsObject
from ..Point import Point
from .. import getConfigOption
__all__ = ['ImageItem']
@ -28,7 +30,6 @@ class ImageItem(GraphicsObject):
for controlling the levels and lookup table used to display the image.
"""
sigImageChanged = QtCore.Signal()
sigRemoveRequested = QtCore.Signal(object) # self; emitted when 'remove' is selected from context menu
@ -47,6 +48,8 @@ class ImageItem(GraphicsObject):
self.lut = None
self.autoDownsample = False
self.axisOrder = getConfigOption('imageAxisOrder')
# In some cases, we use a modified lookup table to handle both rescaling
# and LUT more efficiently
self._effectiveLut = None
@ -86,12 +89,14 @@ class ImageItem(GraphicsObject):
def width(self):
if self.image is 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):
if self.image is 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):
if self.image is None:
@ -147,7 +152,11 @@ class ImageItem(GraphicsObject):
self.update()
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:
self.setLookupTable(kargs['lut'], update=update)
if 'levels' in kargs:
@ -190,7 +199,7 @@ class ImageItem(GraphicsObject):
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
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
levels based on the maximum and minimum values in the data.
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
in the image. If the image array has dtype uint8, no rescaling is necessary.
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.
autoDownsample (bool) If True, the image is automatically downsampled to match the
screen resolution. This improves performance for large images and
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()
@ -259,6 +282,42 @@ class ImageItem(GraphicsObject):
if gotNewData:
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):
"""
Estimate the min/max values of the image data by subsampling.
@ -303,10 +362,12 @@ class ImageItem(GraphicsObject):
if w == 0 or h == 0:
self.qimage = None
return
xds = int(1.0/w)
yds = int(1.0/h)
image = fn.downsample(self.image, xds, axis=0)
image = fn.downsample(image, yds, axis=1)
xds = max(1, int(1.0 / w))
yds = max(1, int(1.0 / h))
axes = [1, 0] if self.axisOrder == 'row-major' else [0, 1]
image = fn.downsample(self.image, xds, axis=axes[0])
image = fn.downsample(image, yds, axis=axes[1])
self._lastDownsample = (xds, yds)
else:
image = self.image
@ -318,20 +379,28 @@ class ImageItem(GraphicsObject):
eflsize = 2**(image.itemsize*8)
ind = np.arange(eflsize)
minlev, maxlev = levels
levdiff = maxlev - minlev
levdiff = 1 if levdiff == 0 else levdiff # don't allow division by 0
if lut is None:
efflut = fn.rescaleData(ind, scale=255./(maxlev-minlev),
efflut = fn.rescaleData(ind, scale=255./levdiff,
offset=minlev, dtype=np.ubyte)
else:
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))
efflut = lut[efflut]
self._effectiveLut = efflut
lut = self._effectiveLut
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)
def paint(self, p, *args):
@ -347,7 +416,8 @@ class ImageItem(GraphicsObject):
p.setCompositionMode(self.paintMode)
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')
if self.border is not None:
p.setPen(self.border)
@ -398,6 +468,7 @@ class ImageItem(GraphicsObject):
bins = 500
kwds['bins'] = bins
stepData = stepData[np.isfinite(stepData)]
hist = np.histogram(stepData, **kwds)
return hist[1][:-1], hist[0]
@ -433,21 +504,6 @@ class ImageItem(GraphicsObject):
self.qimage = None
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):
if ev.button() != QtCore.Qt.LeftButton:
ev.ignore()
@ -484,24 +540,18 @@ class ImageItem(GraphicsObject):
self.menu.remAct = remAct
return self.menu
def hoverEvent(self, ev):
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.RightButton)
#self.box.setBrush(fn.mkBrush('w'))
elif not ev.isExit() and self.removable:
ev.acceptClicks(QtCore.Qt.RightButton) ## accept context menu clicks
#else:
#self.box.setBrush(self.brush)
#self.update()
def tabletEvent(self, ev):
print(ev.device())
print(ev.pointerType())
print(ev.pressure())
pass
#print(ev.device())
#print(ev.pointerType())
#print(ev.pressure())
def drawAt(self, pos, ev=None):
pos = [int(pos.x()), int(pos.y())]

View File

@ -1,5 +1,4 @@
from .. import getConfigOption
from .GraphicsObject import *
from .. import functions as fn
from ..Qt import QtGui, QtCore
@ -9,12 +8,10 @@ class IsocurveItem(GraphicsObject):
"""
**Bases:** :class:`GraphicsObject <pyqtgraph.GraphicsObject>`
Item displaying an isocurve of a 2D array.To align this item correctly with an
ImageItem,call isocurve.setParentItem(image)
Item displaying an isocurve of a 2D array. To align this item correctly with an
ImageItem, call ``isocurve.setParentItem(image)``.
"""
def __init__(self, data=None, level=0, pen='w'):
def __init__(self, data=None, level=0, pen='w', axisOrder=None):
"""
Create a new isocurve item.
@ -25,6 +22,9 @@ class IsocurveItem(GraphicsObject):
level The cutoff value at which to draw the isocurve.
pen The color of the curve item. Can be anything valid for
: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)
@ -32,9 +32,9 @@ class IsocurveItem(GraphicsObject):
self.level = level
self.data = None
self.path = None
self.axisOrder = getConfigOption('imageAxisOrder') if axisOrder is None else axisOrder
self.setPen(pen)
self.setData(data, level)
def setData(self, data, level=None):
"""
@ -54,7 +54,6 @@ class IsocurveItem(GraphicsObject):
self.path = None
self.prepareGeometryChange()
self.update()
def setLevel(self, level):
"""Set the level at which the isocurve is drawn."""
@ -62,7 +61,6 @@ class IsocurveItem(GraphicsObject):
self.path = None
self.prepareGeometryChange()
self.update()
def setPen(self, *args, **kwargs):
"""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>`"""
self.brush = fn.mkBrush(*args, **kwargs)
self.update()
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)
def boundingRect(self):
@ -100,7 +88,13 @@ class IsocurveItem(GraphicsObject):
if self.data is None:
self.path = None
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()
for line in lines:
self.path.moveTo(*line[0])

View File

@ -21,6 +21,7 @@ from math import cos, sin
from .. import functions as fn
from .GraphicsObject import GraphicsObject
from .UIGraphicsItem import UIGraphicsItem
from .. import getConfigOption
__all__ = [
'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
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.
"""
#print "getArraySlice"
## Determine shape of array along ROI axes
dShape = (data.shape[axes[0]], data.shape[axes[1]])
#print " dshape", dShape
## Determine transform that maps ROI bounding box to image coordinates
try:
@ -1032,25 +1030,28 @@ class ROI(GraphicsObject):
return None
## Modify transform to scale from image coords to data coords
#m = QtGui.QTransform()
tr.scale(float(dShape[0]) / img.width(), float(dShape[1]) / img.height())
#tr = tr * m
axisOrder = img.axisOrder
if axisOrder == 'row-major':
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
dataBounds = tr.mapRect(self.boundingRect())
#print " boundingRect:", self.boundingRect()
#print " dataBounds:", dataBounds
## Intersect transformed ROI bounds with data bounds
intBounds = dataBounds.intersected(QtCore.QRectF(0, 0, dShape[0], dShape[1]))
#print " intBounds:", intBounds
if axisOrder == 'row-major':
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.
bounds = (
(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())))
)
#print " bounds:", bounds
if axisOrder == 'row-major':
bounds = bounds[::-1]
if returnSlice:
## Create slice objects
@ -1074,7 +1075,10 @@ class ROI(GraphicsObject):
Used to determine the relationship between the
ROI and the boundaries of *data*.
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
with a corresponding array of coordinates that were
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)
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:
kwds['returnCoords'] = True
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
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.
"""
if self.scene() is not img.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 = self.mapToItem(img, QtCore.QPointF(1, 0)) - origin
vy = self.mapToItem(img, QtCore.QPointF(0, 1)) - origin
vx = img.mapToData(self.mapToItem(img, QtCore.QPointF(1, 0))) - origin
vy = img.mapToData(self.mapToItem(img, QtCore.QPointF(0, 1))) - origin
lvx = np.sqrt(vx.x()**2 + vx.y()**2)
lvy = np.sqrt(vy.x()**2 + vy.y()**2)
pxLen = img.width() / float(data.shape[axes[0]])
#img.width is number of pixels, not width of item.
#need pxWidth and pxHeight instead of pxLen ?
sx = pxLen / lvx
sy = pxLen / lvy
#pxLen = img.width() / float(data.shape[axes[0]])
##img.width is number of pixels, not width of item.
##need pxWidth and pxHeight instead of pxLen ?
#sx = pxLen / lvx
#sy = pxLen / lvy
sx = 1.0 / lvx
sy = 1.0 / lvy
vectors = ((vx.x()*sx, vx.y()*sx), (vy.x()*sy, vy.y()*sy))
if fromBoundingRect is True:
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())
else:
shape = self.state['size']
@ -1144,6 +1154,11 @@ class ROI(GraphicsObject):
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
def renderShapeMask(self, width, height):
@ -1166,7 +1181,7 @@ class ROI(GraphicsObject):
p.translate(-bounds.topLeft())
p.drawPath(shape)
p.end()
mask = fn.imageToArray(im)[:,:,0].astype(float) / 255.
mask = fn.imageToArray(im, transpose=True)[:,:,0].astype(float) / 255.
return mask
def getGlobalTransform(self, relativeTo=None):
@ -1639,6 +1654,8 @@ class MultiRectROI(QtGui.QGraphicsObject):
## make sure orthogonal axis is the same size
## (sometimes fp errors cause differences)
if img.axisOrder == 'row-major':
axes = axes[::-1]
ms = min([r.shape[axes[1]] for r in rgns])
sl = [slice(None)] * rgns[0].ndim
sl[axes[1]] = slice(0,ms)
@ -1998,7 +2015,7 @@ class PolyLineROI(ROI):
p.lineTo(self.handles[0]['item'].pos())
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
ROI. Values outside the ROI shape are set to 0.
@ -2006,8 +2023,15 @@ class PolyLineROI(ROI):
br = self.boundingRect()
if br.width() > 1000:
raise Exception()
sliced = ROI.getArrayRegion(self, data, img, axes=axes, fromBoundingRect=True)
mask = self.renderShapeMask(sliced.shape[axes[0]], sliced.shape[axes[1]])
sliced = ROI.getArrayRegion(self, data, img, axes=axes, fromBoundingRect=True, **kwds)
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[axes[0]] = sliced.shape[axes[0]]
shape[axes[1]] = sliced.shape[axes[1]]
@ -2085,7 +2109,7 @@ class LineSegmentROI(ROI):
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
from an array.
@ -2101,7 +2125,7 @@ class LineSegmentROI(ROI):
for i in range(len(imgPts)-1):
d = Point(imgPts[i+1] - 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)
return np.concatenate(rgns, axis=axes[0])

View File

@ -3,22 +3,22 @@ import pytest
from pyqtgraph.Qt import QtCore, QtGui, QtTest
import numpy as np
import pyqtgraph as pg
from pyqtgraph.tests import assertImageApproved
from pyqtgraph.tests import assertImageApproved, TransposedImageItem
app = pg.mkQApp()
def test_ImageItem():
def test_ImageItem(transpose=False):
w = pg.GraphicsWindow()
view = pg.ViewBox()
w.setCentralWidget(view)
w.resize(200, 200)
w.show()
img = pg.ImageItem(border=0.5)
img = TransposedImageItem(border=0.5, transpose=transpose)
view.addItem(img)
# test mono float
np.random.seed(0)
data = np.random.normal(size=(20, 20))
@ -60,6 +60,18 @@ def test_ImageItem():
img.setLevels([127, 128])
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
data = np.zeros((100, 100, 4), dtype='ubyte')
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.')
# 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])
view.addItem(img2)
img2.scale(10, 10)
@ -103,9 +115,23 @@ def test_ImageItem():
img.setAutoDownsample(True)
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])
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")

View File

@ -2,13 +2,13 @@ import numpy as np
import pytest
import pyqtgraph as pg
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()
def test_getArrayRegion():
def test_getArrayRegion(transpose=False):
pr = pg.PolyLineROI([[0, 0], [27, 0], [0, 28]], closed=True)
pr.setPos(1, 1)
rois = [
@ -21,10 +21,23 @@ def test_getArrayRegion():
# For some ROIs, resize should not be used.
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()
#win = pg.GraphicsLayoutWidget()
@ -50,6 +63,7 @@ def check_getArrayRegion(roi, name, testResize=True):
img1 = pg.ImageItem(border='w')
img2 = pg.ImageItem(border='w')
vb1.addItem(img1)
vb2.addItem(img2)
@ -60,6 +74,9 @@ def check_getArrayRegion(roi, name, testResize=True):
data[:, :, 2, :] += 10
data[:, :, :, 3] += 10
if transpose:
data = data.transpose(0, 2, 1, 3)
img1.setImage(data[0, ..., 0])
vb1.setAspectLocked()
vb1.enableAutoRange(True, True)
@ -67,8 +84,14 @@ def check_getArrayRegion(roi, name, testResize=True):
roi.setZValue(10)
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))
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])
vb2.setAspectLocked()
vb2.enableAutoRange(True, True)
@ -122,6 +145,9 @@ def check_getArrayRegion(roi, name, testResize=True):
img2.setImage(rgn[0, ..., 0])
app.processEvents()
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():

View File

@ -30,6 +30,7 @@ from ..graphicsItems.GradientEditorItem import addGradientListToDocstring
from .. import ptime as ptime
from .. import debug as debug
from ..SignalProxy import SignalProxy
from .. import getConfigOption
try:
from bottleneck import nanmin, nanmax
@ -203,9 +204,10 @@ class ImageView(QtGui.QWidget):
"""
Set the image to be displayed in the widget.
================== =======================================================================
================== ===========================================================================
**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
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.
@ -222,7 +224,19 @@ class ImageView(QtGui.QWidget):
and *scale*.
autoHistogramRange If True, the histogram y-range is automatically scaled to fit the
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()
@ -239,28 +253,22 @@ class ImageView(QtGui.QWidget):
self.image = img
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()
if axes is None:
x,y = (0, 1) if self.imageItem.axisOrder == 'col-major' else (1, 0)
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:
# Ambiguous case; make a guess
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:
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:
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:
raise Exception("Can not interpret image with dimensions %s" % (str(img.shape)))
elif isinstance(axes, dict):
@ -274,6 +282,18 @@ class ImageView(QtGui.QWidget):
for x in ['t', 'x', 'y', 'c']:
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()
@ -455,7 +475,7 @@ class ImageView(QtGui.QWidget):
def setCurrentIndex(self, ind):
"""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.ignoreTimeLine = True
self.timeLine.setValue(self.tVals[self.currentIndex])
@ -543,6 +563,7 @@ class ImageView(QtGui.QWidget):
axes = (1, 2)
else:
return
data, coords = self.roi.getArrayRegion(image.view(np.ndarray), self.imageItem, axes, returnMappedCoords=True)
if data is not None:
while data.ndim > 1:
@ -638,11 +659,21 @@ class ImageView(QtGui.QWidget):
if autoHistogramRange:
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:
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.imageItem.updateImage(image[self.currentIndex])
image = image[self.currentIndex]
self.imageItem.updateImage(image)
def timeIndex(self, slider):

View File

@ -7,5 +7,6 @@ def test_nan_image():
img = np.ones((10,10))
img[0,0] = np.nan
v = pg.image(img)
v.imageItem.getHistogram()
app.processEvents()
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

View File

@ -42,7 +42,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-5'
testDataTag = 'test-data-6'
import time
@ -67,6 +67,30 @@ from .. import ImageItem, TextItem
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():
global tester
@ -159,12 +183,15 @@ def assertImageApproved(image, standardFile, message=None, **kwargs):
if bool(os.getenv('PYQTGRAPH_PRINT_TEST_STATE', False)):
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:
if stdFileName in gitStatus(dataPath):
print("\n\nWARNING: unit test failed against modified standard "
"image %s.\nTo revert this file, run `cd %s; git checkout "
"%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())
getTester().test(image, stdImage, message)
stdPath = os.path.dirname(stdFileName)
@ -344,7 +371,7 @@ class ImageTester(QtGui.QWidget):
for i, v in enumerate(self.views):
v.setAspectLocked(1)
v.invertY()
v.image = ImageItem()
v.image = ImageItem(axisOrder='row-major')
v.image.setAutoDownsample(True)
v.addItem(v.image)
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)
self.label.setText(message)
self.views[0].image.setImage(im1.transpose(1, 0, 2))
self.views[1].image.setImage(im2.transpose(1, 0, 2))
diff = makeDiffImage(im1, im2).transpose(1, 0, 2)
self.views[0].image.setImage(im1)
self.views[1].image.setImage(im2)
diff = makeDiffImage(im1, im2)
self.views[2].image.setImage(diff)
self.views[0].autoRange()
@ -584,3 +611,15 @@ def transformStr(t):
def indent(s, pfx):
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
default, the background color is determined using the
'backgroundColor' configuration option (see
:func:`setConfigOption <pyqtgraph.setConfigOption>`.
:func:`setConfigOptions <pyqtgraph.setConfigOptions>`).
============== ============================================================
"""