diff --git a/doc/source/graphicsItems/imageitem.rst b/doc/source/graphicsItems/imageitem.rst index 49a981dc..6f878050 100644 --- a/doc/source/graphicsItems/imageitem.rst +++ b/doc/source/graphicsItems/imageitem.rst @@ -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 ` 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__ - diff --git a/doc/source/images/example_imageitem_transform.png b/doc/source/images/example_imageitem_transform.png new file mode 100644 index 00000000..648df2c2 Binary files /dev/null and b/doc/source/images/example_imageitem_transform.png differ diff --git a/doc/source/images/gen_example_false_color_image.py b/doc/source/images/gen_example_false_color_image.py index 22e6e006..9114c9a1 100644 --- a/doc/source/images/gen_example_false_color_image.py +++ b/doc/source/images/gen_example_false_color_image.py @@ -42,4 +42,4 @@ main_window = MainWindow() ## Start Qt event loop if __name__ == '__main__': - mkQApp().exec_() + pg.exec() diff --git a/doc/source/images/gen_example_gradient_plot.py b/doc/source/images/gen_example_gradient_plot.py index be5c8047..f2cade2a 100644 --- a/doc/source/images/gen_example_gradient_plot.py +++ b/doc/source/images/gen_example_gradient_plot.py @@ -52,4 +52,4 @@ main_window = MainWindow() ## Start Qt event loop if __name__ == '__main__': - mkQApp().exec_() + pg.exec() diff --git a/doc/source/images/gen_example_imageitem_transform.py b/doc/source/images/gen_example_imageitem_transform.py new file mode 100644 index 00000000..6ca17c6f --- /dev/null +++ b/doc/source/images/gen_example_imageitem_transform.py @@ -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() diff --git a/examples/MatrixDisplayExample.py b/examples/MatrixDisplayExample.py new file mode 100644 index 00000000..e255af6e --- /dev/null +++ b/examples/MatrixDisplayExample.py @@ -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() diff --git a/examples/utils.py b/examples/utils.py index c506b5d4..3a3ad30f 100644 --- a/examples/utils.py +++ b/examples/utils.py @@ -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)), diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 28459923..5a86d9f2 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -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.""" diff --git a/pyqtgraph/graphicsItems/DateAxisItem.py b/pyqtgraph/graphicsItems/DateAxisItem.py index 7cfdefff..a25804e7 100644 --- a/pyqtgraph/graphicsItems/DateAxisItem.py +++ b/pyqtgraph/graphicsItems/DateAxisItem.py @@ -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 diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py index 3955531f..11c9d1cb 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -25,26 +25,20 @@ __all__ = ['ImageItem'] class ImageItem(GraphicsObject): """ **Bases:** :class:`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 `) and/or colored - with a lookup table (see :func:`setLookupTable `) - before being displayed. - - ImageItem is frequently used in conjunction with - :class:`HistogramLUTItem ` or - :class:`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 ` 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 ` - for more details on how levels are applied. + Sets image scaling levels. + See :func:`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 ` 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 ` 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 ` - or :class:`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 ` 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 ` - - ================= ========================================================================= - **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 `. - 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 ` - 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 `. + + 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 `. + 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 diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index 8ecf9be7..c7189e24 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -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 `, :func:`setRange `, :func:`autoRange `, + :func:`setDefaultPadding `, :func:`setXLink `, :func:`setYLink `, :func:`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. ' diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index f1987ced..41f424fa 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -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 ` - *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 ` - *invertX* (bool) See :func:`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 ` + *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 ` + *invertX* (bool) See :func:`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): """ diff --git a/tests/graphicsItems/test_ImageItem.py b/tests/graphicsItems/test_ImageItem.py index efd94779..7b57f469 100644 --- a/tests/graphicsItems/test_ImageItem.py +++ b/tests/graphicsItems/test_ImageItem.py @@ -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():