Merge pull request #370 from campagnola/image-alignment
Image alignment
This commit is contained in:
commit
4b9f1a20a4
@ -6,6 +6,7 @@ Contents:
|
|||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
||||||
|
config_options
|
||||||
functions
|
functions
|
||||||
graphicsItems/index
|
graphicsItems/index
|
||||||
widgets/index
|
widgets/index
|
||||||
|
41
doc/source/config_options.rst
Normal file
41
doc/source/config_options.rst
Normal 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
|
||||||
|
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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([])
|
||||||
|
@ -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)
|
||||||
|
@ -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]
|
||||||
|
@ -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()
|
||||||
|
@ -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.
|
||||||
@ -166,6 +167,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
|
||||||
import GraphicsView
|
import GraphicsView
|
||||||
|
@ -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]
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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).
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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,12 +379,14 @@ 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]
|
||||||
|
|
||||||
@ -331,7 +394,13 @@ class ImageItem(GraphicsObject):
|
|||||||
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())]
|
||||||
|
@ -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,10 +32,10 @@ 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):
|
||||||
"""
|
"""
|
||||||
Set the data/image to draw isocurves for.
|
Set the data/image to draw isocurves for.
|
||||||
@ -55,7 +55,6 @@ class IsocurveItem(GraphicsObject):
|
|||||||
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."""
|
||||||
self.level = level
|
self.level = level
|
||||||
@ -63,7 +62,6 @@ class IsocurveItem(GraphicsObject):
|
|||||||
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
|
||||||
for :func:`mkPen <pyqtgraph.mkPen>`"""
|
for :func:`mkPen <pyqtgraph.mkPen>`"""
|
||||||
@ -76,17 +74,7 @@ class IsocurveItem(GraphicsObject):
|
|||||||
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])
|
||||||
|
@ -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
|
||||||
|
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())
|
tr.scale(float(dShape[0]) / img.width(), float(dShape[1]) / img.height())
|
||||||
#tr = tr * m
|
|
||||||
|
|
||||||
## 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
|
||||||
|
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]))
|
intBounds = dataBounds.intersected(QtCore.QRectF(0, 0, dShape[0], dShape[1]))
|
||||||
#print " intBounds:", intBounds
|
|
||||||
|
|
||||||
## 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)
|
||||||
|
|
||||||
|
if img.axisOrder == 'col-major':
|
||||||
mask = self.renderShapeMask(sliced.shape[axes[0]], sliced.shape[axes[1]])
|
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])
|
||||||
|
@ -3,21 +3,21 @@ 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)
|
||||||
@ -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")
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
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)
|
check_getArrayRegion(roi, 'roi/'+name, testResize)
|
||||||
|
finally:
|
||||||
|
pg.setConfigOptions(imageAxisOrder=origMode)
|
||||||
|
|
||||||
|
|
||||||
def check_getArrayRegion(roi, name, testResize=True):
|
def test_getArrayRegion_axisorder():
|
||||||
|
test_getArrayRegion(transpose=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)
|
||||||
@ -123,6 +146,9 @@ def check_getArrayRegion(roi, name, testResize=True):
|
|||||||
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():
|
||||||
rois = [
|
rois = [
|
||||||
|
@ -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):
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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>`).
|
||||||
============== ============================================================
|
============== ============================================================
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user