Add axis convenience methods and matrix display example (#1726)

* add axis convenient methods and matrix display example

* wrestled wayward space back into docstring

* color map names are case sensitive on Linux

* docstring fix for PlotItem

* protect AxisItem.linkToView from being obscured by DateAxisItem override

* replaced setOrigin method by promoted setPos and setScale

* made tri-state axes switching explicit

* reverted setRect behavior, documentation pass for ImageItem

* minor text adjustment

* implmented some suggested revisions

* fix input parsing for setRect and add tests so that I don't break it again

* don't try to re-add transform after clearing it

* changed example and doc image generators to pg.exec()

* removed commented-out code

* cleaned up transform eqaulity assertion

* restored devoured comment

* restored devoured comment
This commit is contained in:
Nils Nemitz 2021-06-07 23:44:19 +09:00 committed by GitHub
parent 3a3d05b16f
commit 61f067bf7c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 522 additions and 157 deletions

View File

@ -1,8 +1,66 @@
ImageItem
=========
:class:`~pyqtgraph.ImageItem` displays images inside a :class:`~pyqtgraph.GraphicsView`, or a
:class:`~pyqtgraph.ViewBox`, which may itself be part of a :class:`~pyqtgraph.PlotItem`. It is designed
for rapid updates as needed for a video display. The supplied data is optionally scaled (see
:func:`~pyqtgraph.ImageItem.setLevels`) and/or colored according to a
lookup table (see :func:`~pyqtgraph.ImageItem.setLookupTable`.
Data is provided as a NumPy array with an ordering of either
* `col-major`, where the shape of the array represents (width, height) or
* `row-major`, where the shape of the array represents (height, width).
While `col-major` is the default, `row-major` ordering typically has the best performance. In either ordering,
a third dimension can be added to the array to hold individual
``[R,G,B]`` or ``[R,G,B,A]`` components.
Notes
-----
Data ordering can be set for each ImageItem, or in the :ref:`global configuration options <apiref_config>` by ::
pyqtgraph.setConfigOption('imageAxisOrder', 'row-major') # best performance
An image can be placed into a plot area of a given extent directly through the
:func:`~pyqtgraph.ImageItem.setRect` method or the ``rect`` keyword. This is internally realized through
assigning a ``QtGui.QTransform``. For other translation, scaling or rotations effects that
persist for all later image data, the user can also directly define and assign such a
transform, as shown in the example below.
ImageItem is frequently used in conjunction with :class:`~pyqtgraph.ColorBarItem` to provide
a color map display and interactive level adjustments, or with
:class:`~pyqtgraph.HistogramLUTItem` or :class:`~pyqtgraph.HistogramLUTWidget` for a full GUI
to control the levels and lookup table used to display the image.
If performance is critial, the following points may be worth investigating:
* Use row-major ordering and C-contiguous image data.
* Manually provide ``level`` information to avoid autoLevels sampling of the image.
* Prefer `float32` to `float64` for floating point data, avoid NaN values.
* Use lookup tables with <= 256 entries for false color images.
* Avoid individual level adjustments RGB components.
* Use the latest version of NumPy. Notably, SIMD code added in version 1.20 significantly improved performance on Linux platforms.
* Enable Numba with ``pyqtgraph.setConfigOption('useNumba', True)``, although the JIT compilation will only accelerate repeated image display.
.. _ImageItem_examples:
Examples
--------
.. literalinclude:: ../images/gen_example_imageitem_transform.py
:lines: 19-28
:dedent: 8
.. image::
../images/example_imageitem_transform.png
:width: 49%
:alt: Example of transformed image display
.. autoclass:: pyqtgraph.ImageItem
:members:
.. automethod:: pyqtgraph.ImageItem.__init__

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -42,4 +42,4 @@ main_window = MainWindow()
## Start Qt event loop
if __name__ == '__main__':
mkQApp().exec_()
pg.exec()

View File

@ -52,4 +52,4 @@ main_window = MainWindow()
## Start Qt event loop
if __name__ == '__main__':
mkQApp().exec_()
pg.exec()

View File

@ -0,0 +1,45 @@
"""
generates 'example_false_color_image.png'
"""
import numpy as np
import pyqtgraph as pg
import pyqtgraph.exporters as exp
from pyqtgraph.Qt import QtGui, mkQApp
class MainWindow(pg.GraphicsLayoutWidget):
""" example application main window """
def __init__(self):
super().__init__()
self.resize(420,400)
self.show()
plot = self.addPlot()
# Example: Transformed display of ImageItem
tr = QtGui.QTransform() # prepare ImageItem transformation:
tr.scale(6.0, 3.0) # scale horizontal and vertical axes
tr.translate(-1.5, -1.5) # move 3x3 image to locate center at axis origin
img = pg.ImageItem( image=np.eye(3), levels=(0,1) ) # create example image
img.setTransform(tr) # assign transform
plot.addItem( img ) # add ImageItem to PlotItem
plot.showAxes(True) # frame it with a full set of axes
plot.invertY(True) # vertical axis counts top to bottom
self.timer = pg.QtCore.QTimer( singleShot=True )
self.timer.timeout.connect(self.export)
self.timer.start(100)
def export(self):
print('exporting')
exporter = exp.ImageExporter(self.scene())
exporter.parameters()['width'] = 420
exporter.export('example_imageitem_transform.png')
mkQApp("ImageItem transform example")
main_window = MainWindow()
## Start Qt event loop
if __name__ == '__main__':
pg.exec()

View File

@ -0,0 +1,62 @@
# -*- coding: utf-8 -*-
"""
This example demonstrates ViewBox and AxisItem configuration to plot a correlation matrix.
"""
## Add path to library (just for examples; you do not need this)
import initExample
import numpy as np
import pyqtgraph as pg
from pyqtgraph.Qt import QtWidgets, mkQApp, QtGui
class MainWindow(QtWidgets.QMainWindow):
""" example application main window """
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
gr_wid = pg.GraphicsLayoutWidget(show=True)
self.setCentralWidget(gr_wid)
self.setWindowTitle('pyqtgraph example: Correlation matrix display')
self.resize(600,500)
self.show()
corrMatrix = np.array([
[ 1. , 0.5184571 , -0.70188642],
[ 0.5184571 , 1. , -0.86094096],
[-0.70188642, -0.86094096, 1. ]
])
columns = ["A", "B", "C"]
pg.setConfigOption('imageAxisOrder', 'row-major') # Switch default order to Row-major
correlogram = pg.ImageItem()
# create transform to center the corner element on the origin, for any assigned image:
tr = QtGui.QTransform().translate(-0.5, -0.5)
correlogram.setTransform(tr)
correlogram.setImage(corrMatrix)
plotItem = gr_wid.addPlot() # add PlotItem to the main GraphicsLayoutWidget
plotItem.invertY(True) # orient y axis to run top-to-bottom
plotItem.setDefaultPadding(0.0) # plot without padding data range
plotItem.addItem(correlogram) # display correlogram
# show full frame, label tick marks at top and left sides, with some extra space for labels:
plotItem.showAxes( True, showValues=(True, True, False, False), size=20 )
# define major tick marks and labels:
ticks = [ (idx, label) for idx, label in enumerate( columns ) ]
for side in ('left','top','right','bottom'):
plotItem.getAxis(side).setTicks( (ticks, []) ) # add list of major ticks; no minor ticks
plotItem.getAxis('bottom').setHeight(10) # include some additional space at bottom of figure
colorMap = pg.colormap.get("CET-D1") # choose perceptually uniform, diverging color map
# generate an adjustabled color bar, initially spanning -1 to 1:
bar = pg.ColorBarItem( values=(-1,1), cmap=colorMap)
# link color bar and color map to correlogram, and show it in plotItem:
bar.setImageItem(correlogram, insert_in=plotItem)
mkQApp("Correlation matrix display")
main_window = MainWindow()
## Start Qt event loop
if __name__ == '__main__':
pg.exec()

View File

@ -12,6 +12,7 @@ examples = OrderedDict([
('Plot Customization', 'customPlot.py'),
('Timestamps on x axis', 'DateAxisItem.py'),
('Image Analysis', 'imageAnalysis.py'),
('Matrix Display', 'MatrixDisplayExample.py'),
('Color Maps', 'colorMaps.py'),
('Color Gradient Plots', 'ColorGradientPlots.py'),
('ViewBox Features', Namespace(filename='ViewBoxFeatures.py', recommended=True)),

View File

@ -111,9 +111,10 @@ class AxisItem(GraphicsWidget):
self._linkedView = None
if linkView is not None:
self.linkToView(linkView)
self._linkToView_internal(linkView)
self.grid = False
#self.setCacheMode(self.DeviceCoordinateCache)
def setStyle(self, **kwds):
@ -529,8 +530,9 @@ class AxisItem(GraphicsWidget):
else:
return self._linkedView()
def linkToView(self, view):
"""Link this axis to a ViewBox, causing its displayed range to match the visible range of the view."""
def _linkToView_internal(self, view):
# We need this code to be available without override,
# even though DateAxisItem overrides the user-side linkToView method
self.unlinkFromView()
self._linkedView = weakref.ref(view)
@ -538,8 +540,11 @@ class AxisItem(GraphicsWidget):
view.sigYRangeChanged.connect(self.linkedViewChanged)
else:
view.sigXRangeChanged.connect(self.linkedViewChanged)
view.sigResized.connect(self.linkedViewChanged)
def linkToView(self, view):
"""Link this axis to a ViewBox, causing its displayed range to match the visible range of the view."""
self._linkToView_internal(view)
def unlinkFromView(self):
"""Unlink this axis from a ViewBox."""

View File

@ -306,7 +306,8 @@ class DateAxisItem(AxisItem):
self.minSpacing = density*size
def linkToView(self, view):
super(DateAxisItem, self).linkToView(view)
"""Link this axis to a ViewBox, causing its displayed range to match the visible range of the view."""
self._linkToView_internal(view) # calls original linkToView code
# Set default limits
_min = MIN_REGULAR_TIMESTAMP

View File

@ -25,26 +25,20 @@ __all__ = ['ImageItem']
class ImageItem(GraphicsObject):
"""
**Bases:** :class:`GraphicsObject <pyqtgraph.GraphicsObject>`
GraphicsObject displaying an image. Optimized for rapid update (ie video display).
This item displays either a 2D numpy array (height, width) or
a 3D array (height, width, RGBa). This array is optionally scaled (see
:func:`setLevels <pyqtgraph.ImageItem.setLevels>`) and/or colored
with a lookup table (see :func:`setLookupTable <pyqtgraph.ImageItem.setLookupTable>`)
before being displayed.
ImageItem is frequently used in conjunction with
:class:`HistogramLUTItem <pyqtgraph.HistogramLUTItem>` or
:class:`HistogramLUTWidget <pyqtgraph.HistogramLUTWidget>` to provide a GUI
for controlling the levels and lookup table used to display the image.
"""
# Overall description of ImageItem (including examples) moved to documentation text
sigImageChanged = QtCore.Signal()
sigRemoveRequested = QtCore.Signal(object) # self; emitted when 'remove' is selected from context menu
def __init__(self, image=None, **kargs):
"""
See :func:`setImage <pyqtgraph.ImageItem.setImage>` for all allowed initialization arguments.
See :func:`~pyqtgraph.ImageItem.setOpts` for further keyword arguments and
and :func:`~pyqtgraph.ImageItem.setImage` for information on supported formats.
Parameters
----------
image: array
Image data
"""
GraphicsObject.__init__(self)
self.menu = None
@ -64,6 +58,8 @@ class ImageItem(GraphicsObject):
self._defferedLevels = None
self.axisOrder = getConfigOption('imageAxisOrder')
self._dataTransform = self._inverseDataTransform = None
self._update_data_transforms( self.axisOrder ) # install initial transforms
# In some cases, we use a modified lookup table to handle both rescaling
# and LUT more efficiently
@ -79,25 +75,33 @@ class ImageItem(GraphicsObject):
self.setOpts(**kargs)
def setCompositionMode(self, mode):
"""Change the composition mode of the item (see QPainter::CompositionMode
in the Qt documentation). This is useful when overlaying multiple ImageItems.
"""
Change the composition mode of the item to `mode`, used when overlaying multiple ImageItems.
See ``QPainter::CompositionMode`` in the Qt documentation for details.
============================================ ============================================================
**Most common arguments:**
QtGui.QPainter.CompositionMode_SourceOver Default; image replaces the background if it
is opaque. Otherwise, it uses the alpha channel to blend
the image with the background.
QtGui.QPainter.CompositionMode_Overlay The image color is mixed with the background color to
reflect the lightness or darkness of the background.
QtGui.QPainter.CompositionMode_Plus Both the alpha and color of the image and background pixels
are added together.
QtGui.QPainter.CompositionMode_Multiply The output is the image color multiplied by the background.
============================================ ============================================================
Most common arguments:
- ``QtGui.QPainter.CompositionMode_SourceOver``:
(Default) Image replaces the background if it is opaque.
Otherwise the alpha channel controls the visibility of the background.
- ``QtGui.QPainter.CompositionMode_Overlay``:
The image color is mixed with the background color to reflect the lightness or darkness of the background.
- ``QtGui.QPainter.CompositionMode_Plus``:
Both the alpha and color of the image and background pixels are added together.
- ``QtGui.QPainter.CompositionMode_Multiply``:
The output is the image color multiplied by the background.
"""
self.paintMode = mode
self.update()
def setBorder(self, b):
"""
Defines the border drawn around the image. Accepts all arguments supported by
:func:`~pyqtgraph.functions.mkPen`.
"""
self.border = fn.mkPen(b)
self.update()
@ -125,13 +129,18 @@ class ImageItem(GraphicsObject):
def setLevels(self, levels, update=True):
"""
Set image scaling levels. Can be one of:
* [blackLevel, whiteLevel]
* [[minRed, maxRed], [minGreen, maxGreen], [minBlue, maxBlue]]
Only the first format is compatible with lookup tables. See :func:`makeARGB <pyqtgraph.makeARGB>`
for more details on how levels are applied.
Sets image scaling levels.
See :func:`makeARGB <pyqtgraph.makeARGB>` for more details on how levels are applied.
Parameters
----------
levels: list_like
- [`blackLevel`, `whiteLevel`]
sets black and white levels for monochrome data and can be used with a lookup table.
- [[`minR`, `maxR`], [`minG`, `maxG`], [`minB`, `maxB`]]
sets individual scaling for RGB values. Not compatible with lookup tables.
update: bool, optional
Controls if image immediately updates to reflect the new levels.
"""
if self._xp is None:
self.levels = levels
@ -145,18 +154,22 @@ class ImageItem(GraphicsObject):
self.updateImage()
def getLevels(self):
"""
Returns the list representing the current level settings. See :func:`~setLevels`.
When ``autoLevels`` is active, the format is [`blackLevel`, `whiteLevel`].
"""
return self.levels
#return self.whiteLevel, self.blackLevel
def setLookupTable(self, lut, update=True):
"""
Set the lookup table (numpy array) to use for this image. (see
:func:`makeARGB <pyqtgraph.makeARGB>` for more information on how this is used).
Optionally, lut can be a callable that accepts the current image as an
Sets lookup table `lut` to use for false color display of a monochrome image. See :func:`makeARGB <pyqtgraph.makeARGB>` for more
information on how this is used. Optionally, `lut` can be a callable that accepts the current image as an
argument and returns the lookup table to use.
Ordinarily, this table is supplied by a :class:`HistogramLUTItem <pyqtgraph.HistogramLUTItem>`
or :class:`GradientEditorItem <pyqtgraph.GradientEditorItem>`.
Ordinarily, this table is supplied by a :class:`~pyqtgraph.HistogramLUTItem`,
:class:`~pyqtgraph.GradientEditorItem` or :class:`~pyqtgraph.ColorBarItem`.
Setting `update` to False avoids an immediate image update.
"""
if lut is not self.lut:
if self._xp is not None:
@ -180,22 +193,57 @@ class ImageItem(GraphicsObject):
data = numpy.asarray(data)
return data
def setAutoDownsample(self, ads):
def setAutoDownsample(self, active=True):
"""
Set the automatic downsampling mode for this ImageItem.
Controls automatic downsampling for this ImageItem.
Added in version 0.9.9
If active is `True`, the image is automatically downsampled to match the
screen resolution. This improves performance for large images and
reduces aliasing. If autoDownsample is not specified, then ImageItem will
choose whether to downsample the image based on its size.
`False` disables automatic downsampling.
"""
self.autoDownsample = ads
self.autoDownsample = active
self._renderRequired = True
self.update()
def setOpts(self, update=True, **kargs):
"""
Sets display and processing options for this ImageItem. :func:`~pyqtgraph.ImageItem.__init__` and
:func:`~pyqtgraph.ImageItem.setImage` support all keyword arguments listed here.
Parameters
----------
autoDownsample: bool
See :func:`~pyqtgraph.ImageItem.setAutoDownsample`.
axisOrder: str
| `'col-major'`: The shape of the array represents (width, height) of the image. This is the default.
| `'row-major'`: The shape of the array represents (height, width).
border: bool
Sets a pen to draw to draw an image border. See :func:`~pyqtgraph.ImageItem.setBorder`.
compositionMode:
See :func:`~pyqtgraph.ImageItem.setCompositionMode`
lut: array
Sets a color lookup table to use when displaying the image.
See :func:`~pyqtgraph.ImageItem.setLookupTable`.
levels: list_like, usally [`min`, `max`]
Sets minimum and maximum values to use when rescaling the image data. By default, these will be set to
the estimated minimum and maximum values in the image. If the image array has dtype uint8, no rescaling
is necessary. See :func:`~pyqtgraph.ImageItem.setLevels`.
opacity: float, 0.0-1.0
Overall opacity for an RGB image.
rect: QRectF, QRect or array_like of floats (`x`,`y`,`w`,`h`)
Displays the current image within the specified rectangle in plot coordinates.
See :func:`~pyqtgraph.ImageItem.setRect`.
update : bool, optional
Controls if image immediately updates to reflect the new options.
"""
if 'axisOrder' in kargs:
val = kargs['axisOrder']
val = kargs['axisOrder']
if val not in ('row-major', 'col-major'):
raise ValueError('axisOrder must be either "row-major" or "col-major"')
raise ValueError("axisOrder must be either 'row-major' or 'col-major'")
self.axisOrder = val
self._update_data_transforms(self.axisOrder) # update cached transforms
if 'lut' in kargs:
self.setLookupTable(kargs['lut'], update=update)
if 'levels' in kargs:
@ -213,17 +261,40 @@ class ImageItem(GraphicsObject):
self.menu = None
if 'autoDownsample' in kargs:
self.setAutoDownsample(kargs['autoDownsample'])
if 'rect' in kargs:
self.setRect(kargs['rect'])
if update:
self.update()
def setRect(self, rect):
"""Scale and translate the image to fit within rect (must be a QRect or QRectF)."""
def setRect(self, *args):
"""
setRect(rect) or setRect(x,y,w,h)
Sets translation and scaling of this ImageItem to display the current image within the rectangle given
as ``QtCore.QRect`` or ``QtCore.QRectF`` `rect`, or described by parameters `x, y, w, h`, defining starting
position, width and height.
This method cannot be used before an image is assigned.
See the :ref:`examples <ImageItem_examples>` for how to manually set transformations.
"""
if len(args) == 0:
self.resetTransform() # reset scaling and rotation when called without argument
return
if isinstance(args[0], (QtCore.QRectF, QtCore.QRect)):
rect = args[0] # use QRectF or QRect directly
else:
if hasattr(args[0],'__len__'):
args = args[0] # promote tuple or list of values
rect = QtCore.QRectF( *args ) # QRectF(x,y,w,h), but also accepts other initializers
tr = QtGui.QTransform()
tr.translate(rect.left(), rect.top())
tr.scale(rect.width() / self.width(), rect.height() / self.height())
self.setTransform(tr)
def clear(self):
"""
Clears the assigned image.
"""
self.image = None
self.prepareGeometryChange()
self.informViewBoundsChanged()
@ -239,46 +310,39 @@ class ImageItem(GraphicsObject):
def setImage(self, image=None, autoLevels=None, **kargs):
"""
Update the image displayed by this item. For more information on how the image
is processed before displaying, see :func:`makeARGB <pyqtgraph.makeARGB>`
================= =========================================================================
**Arguments:**
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). 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
given.
lut (numpy array) The color lookup table to use when displaying the image.
See :func:`setLookupTable <pyqtgraph.ImageItem.setLookupTable>`.
levels (min, max) The minimum and maximum values to use when rescaling the image
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>`
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. If autoDownsample is not specified, then ImageItem will
choose whether to downsample the image based on its size.
================= =========================================================================
**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()::
Updates the image displayed by this ImageItem. For more information on how the image
is processed before displaying, see :func:`~pyqtgraph.makeARGB>`.
For backward compatibility, image data is assumed to be in column-major order (column, row) by default.
However, most data is stored in row-major order (row, column). It can either be transposed before assignment::
imageitem.setImage(imagedata.T)
or the interpretation of the data can be changed locally through the ``axisOrder`` keyword or by changing the
`imageAxisOrder` :ref:`global configuration option <apiref_config>`.
All keywords supported by :func:`~pyqtgraph.ImageItem.setOpts` are also allowed here.
This requirement can be changed by calling ``image.setOpts(axisOrder='row-major')`` or
by changing the ``imageAxisOrder`` :ref:`global configuration option <apiref_config>`.
Parameters
----------
image: array
Image data given as NumPy array with an integer or floating
point dtype of any bit depth. A 2-dimensional array describes single-valued (monochromatic) data.
A 3-dimensional array is used to give individual color components. The third dimension must
be of length 3 (RGB) or 4 (RGBA).
rect: QRectF, QRect or list_like of floats (`x, y, w, h`), optional
If given, sets translation and scaling to display the image within the specified rectangle. See
:func:`~pyqtgraph.ImageItem.setRect`.
autoLevels: bool, optional
If True, ImageItem will automatically select levels based on the maximum and minimum values encountered
in the data. For performance reasons, this search subsamples the images and may miss individual bright or
or dark points in the data set.
If False, the search will be omitted.
The default is `False` if a ``levels`` keyword argument is given, and `True` otherwise.
"""
profile = debug.Profiler()
@ -342,45 +406,55 @@ class ImageItem(GraphicsObject):
self._defferedLevels = None
self.setLevels((levels))
def _update_data_transforms(self, axisOrder='col-major'):
""" Sets up the transforms needed to map between input array and display """
self._dataTransform = QtGui.QTransform()
self._inverseDataTransform = QtGui.QTransform()
if self.axisOrder == 'row-major': # transpose both
self._dataTransform.scale(1, -1)
self._dataTransform.rotate(-90)
self._inverseDataTransform.scale(1, -1)
self._inverseDataTransform.rotate(-90)
def dataTransform(self):
"""Return the transform that maps from this image's input array to its
"""
Returns 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.
:meta private:
"""
# 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
# transforms are updated in setOpts call.
return self._dataTransform
def inverseDataTransform(self):
"""Return the transform that maps from this image's local coordinate
system to its input array.
See dataTransform() for more information.
:meta private:
"""
tr = QtGui.QTransform()
if self.axisOrder == 'row-major':
# transpose
tr.scale(1, -1)
tr.rotate(-90)
return tr
# transforms are updated in setOpts call.
return self._inverseDataTransform
def mapToData(self, obj):
tr = self.inverseDataTransform()
return tr.map(obj)
return self._inverseDataTransform.map(obj)
def mapFromData(self, obj):
tr = self.dataTransform()
return tr.map(obj)
return self._dataTransform.map(obj)
def quickMinMax(self, targetSize=1e6):
"""
Estimate the min/max values of the image data by subsampling.
Estimates the min/max values of the image data by subsampling.
Subsampling is performed at regular strides chosen to evaluate a number of samples
equal to or less than `targetSize`.
Returns (`min`, `max`).
"""
data = self.image
while data.size > targetSize:
@ -730,33 +804,36 @@ class ImageItem(GraphicsObject):
p.drawRect(self.boundingRect())
def save(self, fileName, *args):
"""Save this image to file. Note that this saves the visible image (after scale/color changes), not the original data."""
"""
Saves this image to file. Note that this saves the visible image (after scale/color changes), not the
original data.
"""
if self._renderRequired:
self.render()
self.qimage.save(fileName, *args)
def getHistogram(self, bins='auto', step='auto', perChannel=False, targetImageSize=200,
targetHistogramSize=500, **kwds):
"""Returns x and y arrays containing the histogram values for the current image.
"""
Returns `x` and `y` arrays containing the histogram values for the current image.
For an explanation of the return format, see numpy.histogram().
The *step* argument causes pixels to be skipped when computing the histogram to save time.
If *step* is 'auto', then a step is chosen such that the analyzed data has
dimensions roughly *targetImageSize* for each axis.
The `step` argument causes pixels to be skipped when computing the histogram to save time.
If `step` is 'auto', then a step is chosen such that the analyzed data has
dimensions approximating `targetImageSize` for each axis.
The *bins* argument and any extra keyword arguments are passed to
self.xp.histogram(). If *bins* is 'auto', then a bin number is automatically
The `bins` argument and any extra keyword arguments are passed to
``self.xp.histogram()``. If `bins` is `auto`, a bin number is automatically
chosen based on the image characteristics:
* Integer images will have approximately *targetHistogramSize* bins,
* Integer images will have approximately `targetHistogramSize` bins,
with each bin having an integer width.
* All other types will have *targetHistogramSize* bins.
* All other types will have `targetHistogramSize` bins.
If *perChannel* is True, then the histogram is computed once per channel
If `perChannel` is `True`, then a histogram is computed for each channel,
and the output is a list of the results.
This method is also used when automatically computing levels.
"""
# This method is also used when automatically computing levels.
if self.image is None or self.image.size == 0:
return None, None
if step == 'auto':
@ -812,10 +889,10 @@ class ImageItem(GraphicsObject):
def setPxMode(self, b):
"""
Set whether the item ignores transformations and draws directly to screen pixels.
Sets whether the item ignores transformations and draws directly to screen pixels.
If True, the item will not inherit any scale or rotation transformations from its
parent items, but its position will be transformed as usual.
(see GraphicsItem::ItemIgnoresTransformations in the Qt documentation)
(see ``GraphicsItem::ItemIgnoresTransformations`` in the Qt documentation)
"""
self.setFlag(self.ItemIgnoresTransformations, b)
@ -830,7 +907,9 @@ class ImageItem(GraphicsObject):
return QtGui.QPixmap.fromImage(self.qimage)
def pixelSize(self):
"""return scene-size of a single pixel in the image"""
"""
Returns the scene-size of a single pixel in the image
"""
br = self.sceneBoundingRect()
if self.image is None:
return 1,1

View File

@ -3,6 +3,7 @@ import importlib
import os
import warnings
import weakref
import collections.abc
import numpy as np
@ -53,6 +54,7 @@ class PlotItem(GraphicsWidget):
:func:`setYRange <pyqtgraph.ViewBox.setYRange>`,
:func:`setRange <pyqtgraph.ViewBox.setRange>`,
:func:`autoRange <pyqtgraph.ViewBox.autoRange>`,
:func:`setDefaultPadding <pyqtgraph.ViewBox.setDefaultPadding>`,
:func:`setXLink <pyqtgraph.ViewBox.setXLink>`,
:func:`setYLink <pyqtgraph.ViewBox.setYLink>`,
:func:`setAutoPan <pyqtgraph.ViewBox.setAutoPan>`,
@ -269,7 +271,7 @@ class PlotItem(GraphicsWidget):
#Important: don't use a settattr(m, getattr(self.vb, m)) as we'd be leaving the viebox alive
#because we had a reference to an instance method (creating wrapper methods at runtime instead).
for m in ['setXRange', 'setYRange', 'setXLink', 'setYLink', 'setAutoPan', # NOTE:
'setAutoVisible', 'setRange', 'autoRange', 'viewRect', 'viewRange', # If you update this list, please
'setAutoVisible', 'setDefaultPadding', 'setRange', 'autoRange', 'viewRect', 'viewRange', # If you update this list, please
'setMouseEnabled', 'setLimits', 'enableAutoRange', 'disableAutoRange', # update the class docstring
'setAspectLocked', 'invertY', 'invertX', 'register', 'unregister']: # as well.
@ -295,7 +297,6 @@ class PlotItem(GraphicsWidget):
============== ==========================================================================================
"""
if axisItems is None:
axisItems = {}
@ -332,7 +333,9 @@ class PlotItem(GraphicsWidget):
axis.linkToView(self.vb)
self.axes[k] = {'item': axis, 'pos': pos}
self.layout.addItem(axis, *pos)
axis.setZValue(-1000)
# axis.setZValue(-1000)
# place axis above images at z=0, items that want to draw over the axes should be placed at z>=1:
axis.setZValue(0.5)
axis.setFlag(axis.ItemNegativeZStacksBehindParent)
axisVisible = k in visibleAxes
@ -1171,7 +1174,63 @@ class PlotItem(GraphicsWidget):
def hideAxis(self, axis):
"""Hide one of the PlotItem's axes. ('left', 'bottom', 'right', or 'top')"""
self.showAxis(axis, False)
def showAxes(self, selection, showValues=True, size=False):
"""
Convenience method for quickly configuring axis settings.
Parameters
----------
selection: boolean or tuple of booleans (left, top, right, bottom)
Determines which AxisItems will be displayed.
A single boolean value will set all axes,
so that ``showAxes(True)`` configures the axes to draw a frame.
showValues: optional, boolean or tuple of booleans (left, top, right, bottom)
Determines if values will be displayed for the ticks of each axis.
True value shows values for left and bottom axis (default).
False shows no values.
None leaves settings unchanged.
If not specified, left and bottom axes will be drawn with values.
size: optional, float or tuple of floats (width, height)
Reserves as fixed amount of space (width for vertical axis, height for horizontal axis)
for each axis where tick values are enabled. If only a single float value is given, it
will be applied for both width and height. If `None` is given instead of a float value,
the axis reverts to automatic allocation of space.
"""
if selection is True: # shortcut: enable all axes, creating a frame
selection = (True, True, True, True)
elif selection is False: # shortcut: disable all axes
selection = (False, False, False, False)
if showValues is True: # shortcut: defaults arrangement with labels at left and bottom
showValues = (True, False, False, True)
elif showValues is False: # shortcut: disable all labels
showValues = (False, False, False, False)
elif showValues is None: # leave labelling untouched
showValues = (None, None, None, None)
if size is not False and not isinstance(size, collections.abc.Sized):
size = (size, size) # make sure that size is either False or a full set of (width, height)
all_axes = ('left','top','right','bottom')
for show_axis, show_value, axis_key in zip(selection, showValues, all_axes):
if show_axis is None:
pass # leave axis display as it is.
else:
if show_axis: self.showAxis(axis_key)
else : self.hideAxis(axis_key)
if show_value is None:
pass # leave value display as it is.
else:
ax = self.getAxis(axis_key)
ax.setStyle(showValues=show_value)
if size is not False: # size adjustment is requested
if axis_key in ('left','right'):
if show_value: ax.setWidth(size[0])
else : ax.setWidth( None )
elif axis_key in ('top', 'bottom'):
if show_value: ax.setHeight(size[1])
else : ax.setHeight( None )
def showScale(self, *args, **kargs):
warnings.warn(
'PlotItem.showScale has been deprecated and will be removed in 0.13. '

View File

@ -106,25 +106,27 @@ class ViewBox(GraphicsWidget):
NamedViews = weakref.WeakValueDictionary() # name: ViewBox
AllViews = weakref.WeakKeyDictionary() # ViewBox: None
def __init__(self, parent=None, border=None, lockAspect=False, enableMouse=True, invertY=False, enableMenu=True, name=None, invertX=False):
def __init__(self, parent=None, border=None, lockAspect=False, enableMouse=True, invertY=False, enableMenu=True, name=None, invertX=False, defaultPadding=0.02):
"""
============== =============================================================
================= =============================================================
**Arguments:**
*parent* (QGraphicsWidget) Optional parent widget
*border* (QPen) Do draw a border around the view, give any
single argument accepted by :func:`mkPen <pyqtgraph.mkPen>`
*lockAspect* (False or float) The aspect ratio to lock the view
coorinates to. (or False to allow the ratio to change)
*enableMouse* (bool) Whether mouse can be used to scale/pan the view
*invertY* (bool) See :func:`invertY <pyqtgraph.ViewBox.invertY>`
*invertX* (bool) See :func:`invertX <pyqtgraph.ViewBox.invertX>`
*enableMenu* (bool) Whether to display a context menu when
right-clicking on the ViewBox background.
*name* (str) Used to register this ViewBox so that it appears
in the "Link axis" dropdown inside other ViewBox
context menus. This allows the user to manually link
the axes of any other view to this one.
============== =============================================================
*parent* (QGraphicsWidget) Optional parent widget
*border* (QPen) Do draw a border around the view, give any
single argument accepted by :func:`mkPen <pyqtgraph.mkPen>`
*lockAspect* (False or float) The aspect ratio to lock the view
coorinates to. (or False to allow the ratio to change)
*enableMouse* (bool) Whether mouse can be used to scale/pan the view
*invertY* (bool) See :func:`invertY <pyqtgraph.ViewBox.invertY>`
*invertX* (bool) See :func:`invertX <pyqtgraph.ViewBox.invertX>`
*enableMenu* (bool) Whether to display a context menu when
right-clicking on the ViewBox background.
*name* (str) Used to register this ViewBox so that it appears
in the "Link axis" dropdown inside other ViewBox
context menus. This allows the user to manually link
the axes of any other view to this one.
*defaultPadding* (float) fraction of the data range that will be added
as padding by default
================= =============================================================
"""
GraphicsWidget.__init__(self, parent)
@ -147,11 +149,12 @@ class ViewBox(GraphicsWidget):
'xInverted': invertX,
'aspectLocked': False, ## False if aspect is unlocked, otherwise float specifies the locked ratio.
'autoRange': [True, True], ## False if auto range is disabled,
## otherwise float gives the fraction of data that is visible
## otherwise float gives the fraction of data that is visible
'autoPan': [False, False], ## whether to only pan (do not change scaling) when auto-range is enabled
'autoVisibleOnly': [False, False], ## whether to auto-range only to the visible portion of a plot
'linkedViews': [None, None], ## may be None, "viewName", or weakref.ref(view)
## a name string indicates that the view *should* link to another, but no view with that name exists yet.
'defaultPadding': defaultPadding,
'mouseEnabled': [enableMouse, enableMouse],
'mouseMode': ViewBox.PanMode if getConfigOption('leftButtonPan') else ViewBox.RectMode,
@ -497,8 +500,8 @@ class ViewBox(GraphicsWidget):
*xRange* (min,max) The range that should be visible along the x-axis.
*yRange* (min,max) The range that should be visible along the y-axis.
*padding* (float) Expand the view by a fraction of the requested range.
By default, this value is set between 0.02 and 0.1 depending on
the size of the ViewBox.
By default, this value is set between the default padding value
and 0.1 depending on the size of the ViewBox.
*update* (bool) If True, update the range of the ViewBox immediately.
Otherwise, the update is deferred until before the next render.
*disableAutoRange* (bool) If True, auto-ranging is diabled. Otherwise, it is left
@ -628,7 +631,7 @@ class ViewBox(GraphicsWidget):
"""
Set the visible Y range of the view to [*min*, *max*].
The *padding* argument causes the range to be set larger by the fraction specified.
(by default, this value is between 0.02 and 0.1 depending on the size of the ViewBox)
(by default, this value is between the default padding and 0.1 depending on the size of the ViewBox)
"""
self.setRange(yRange=[min, max], update=update, padding=padding)
@ -636,7 +639,7 @@ class ViewBox(GraphicsWidget):
"""
Set the visible X range of the view to [*min*, *max*].
The *padding* argument causes the range to be set larger by the fraction specified.
(by default, this value is between 0.02 and 0.1 depending on the size of the ViewBox)
(by default, this value is between the default padding and 0.1 depending on the size of the ViewBox)
"""
self.setRange(xRange=[min, max], update=update, padding=padding)
@ -646,14 +649,14 @@ class ViewBox(GraphicsWidget):
Note that this is not the same as enableAutoRange, which causes the view to
automatically auto-range whenever its contents are changed.
============== ============================================================
============== =============================================================
**Arguments:**
padding The fraction of the total data range to add on to the final
visible range. By default, this value is set between 0.02
and 0.1 depending on the size of the ViewBox.
visible range. By default, this value is set between the
default padding and 0.1 depending on the size of the ViewBox.
items If specified, this is a list of items to consider when
determining the visible range.
============== ============================================================
============== =============================================================
"""
if item is None:
bounds = self.childrenBoundingRect(items=items)
@ -665,10 +668,14 @@ class ViewBox(GraphicsWidget):
def suggestPadding(self, axis):
l = self.width() if axis==0 else self.height()
def_pad = self.state['defaultPadding']
if def_pad == 0.:
return def_pad # respect requested zero padding
max_pad = max(0.1, def_pad) # don't shrink a large default padding
if l > 0:
padding = fn.clip_scalar(1./(l**0.5), 0.02, 0.1)
padding = fn.clip_scalar( 50*def_pad / (l**0.5), def_pad, max_pad)
else:
padding = 0.02
padding = def_pad
return padding
def setLimits(self, **kwds):
@ -1110,6 +1117,13 @@ class ViewBox(GraphicsWidget):
"""
self.border = fn.mkPen(*args, **kwds)
self.borderRect.setPen(self.border)
def setDefaultPadding(self, padding=0.02):
"""
Sets the fraction of the data range that is used to pad the view range in when auto-ranging.
By default, this fraction is 0.02.
"""
self.state['defaultPadding'] = padding
def setAspectLocked(self, lock=True, ratio=1):
"""

View File

@ -2,7 +2,7 @@
import time
import pytest
from pyqtgraph.Qt import QtGui, QtTest
from pyqtgraph.Qt import QtGui, QtTest, QtCore
import numpy as np
import pyqtgraph as pg
from tests.image_testing import assertImageApproved, TransposedImageItem
@ -185,6 +185,47 @@ def test_ImageItem_axisorder():
test_ImageItem(transpose=True)
finally:
pg.setConfigOptions(imageAxisOrder=origMode)
def test_setRect():
def assert_equal_transforms(tr1, tr2):
dic = { # there seems to be no easy way to get the matrix in one call:
'tr11': ( tr1.m11(), tr2.m11() ),
'tr12': ( tr1.m12(), tr2.m12() ),
'tr13': ( tr1.m13(), tr2.m13() ),
'tr21': ( tr1.m21(), tr2.m21() ),
'tr22': ( tr1.m22(), tr2.m22() ),
'tr23': ( tr1.m23(), tr2.m23() ),
'tr31': ( tr1.m31(), tr2.m31() ),
'tr32': ( tr1.m32(), tr2.m32() ),
'tr33': ( tr1.m33(), tr2.m33() )
}
log_string = 'Matrix element mismatch\n'
good = True
for key, values in dic.items():
val1, val2 = values
if val1 != val2:
good = False
log_string += f'{key}: {val1} != {val2}\n'
assert good, log_string
tr = QtGui.QTransform() # construct a reference transform
tr.scale(2, 4) # scale 2x2 image to 4x8
tr.translate(-1, -1) # after shifting by -1, -1
# the transformed 2x2 image would cover (-2,-4) to (2,4).
# Now have setRect construct the same transform:
imgitem = pg.ImageItem(np.eye(2), rect=(-2,-4, 4,8) ) # test tuple of floats
assert_equal_transforms(tr, imgitem.transform())
imgitem = pg.ImageItem(np.eye(2), rect=QtCore.QRectF(-2,-4, 4,8) ) # test QRectF
assert_equal_transforms(tr, imgitem.transform())
imgitem = pg.ImageItem(np.eye(2))
imgitem.setRect(-2,-4, 4,8) # test individual parameters
assert_equal_transforms(tr, imgitem.transform())
imgitem = pg.ImageItem(np.eye(2))
imgitem.setRect(QtCore.QRect(-2,-4, 4,8)) # test QRect argument
assert_equal_transforms(tr, imgitem.transform())
def test_dividebyzero():