From a76fc371129e1bae8b94b3daaa67e6703988c714 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 16 Jun 2016 08:54:52 -0700 Subject: [PATCH] imageAxisOrder config option now accepts "row-major" and "col-major" instead of "normal" and "legacy" ImageItems can individually control their axis order image tests pass with axis order check --- doc/source/config_options.rst | 7 ++-- pyqtgraph/__init__.py | 6 ++-- pyqtgraph/graphicsItems/ImageItem.py | 27 +++++++++------- pyqtgraph/graphicsItems/ROI.py | 12 +++---- .../graphicsItems/tests/test_ImageItem.py | 32 +++++++++++++++++-- pyqtgraph/tests/image_testing.py | 8 ++--- 6 files changed, 62 insertions(+), 30 deletions(-) diff --git a/doc/source/config_options.rst b/doc/source/config_options.rst index 6dd441ce..23560f67 100644 --- a/doc/source/config_options.rst +++ b/doc/source/config_options.rst @@ -19,9 +19,10 @@ foreground See :func:`mkColor` 'd' Default foreground col 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 'legacy' For 'normal', image data is expected in the standard row-major (row, col) order. - For 'legacy', image data is expected in reversed column-major (col, row) order. - The default is 'legacy' for backward compatibility, but this may +imageAxisOrder str 'legacy' 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. diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index e472854c..1630abc0 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -59,9 +59,9 @@ CONFIG_OPTIONS = { 'exitCleanup': True, ## Attempt to work around some exit crash bugs in PyQt and PySide 'enableExperimental': False, ## Enable experimental features (the curious can search for this key in the code) 'crashWarning': False, # If True, print warnings about situations that may result in a crash - 'imageAxisOrder': 'legacy', # For 'normal', image data is expected in the standard (row, col) order. - # For 'legacy', image data is expected in reversed (col, row) order. - # The default is 'legacy' for backward compatibility, but this will + '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. } diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py index a79fcb15..b4e8bfc6 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -48,6 +48,8 @@ class ImageItem(GraphicsObject): self.lut = None self.autoDownsample = False + self.axisOrder = getConfigOption('imageAxisOrder') + # In some cases, we use a modified lookup table to handle both rescaling # and LUT more efficiently self._effectiveLut = None @@ -87,13 +89,13 @@ class ImageItem(GraphicsObject): def width(self): if self.image is None: return None - axis = 0 if getConfigOption('imageAxisOrder') == 'legacy' else 1 + axis = 0 if self.axisOrder == 'col-major' else 1 return self.image.shape[axis] def height(self): if self.image is None: return None - axis = 1 if getConfigOption('imageAxisOrder') == 'legacy' else 0 + axis = 1 if self.axisOrder == 'col-major' else 0 return self.image.shape[axis] def boundingRect(self): @@ -150,7 +152,8 @@ class ImageItem(GraphicsObject): self.update() def setOpts(self, update=True, **kargs): - + if 'axisOrder' in kargs: + self.axisOrder = kargs['axisOrder'] if 'lut' in kargs: self.setLookupTable(kargs['lut'], update=update) if 'levels' in kargs: @@ -220,8 +223,8 @@ class ImageItem(GraphicsObject): imageitem.setImage(imagedata.T) - This requirement can be changed by the ``imageAxisOrder`` - :ref:`global configuration option `. + This requirement can be changed by calling ``image.setOpts(axisOrder='row-major')`` or + by changing the ``imageAxisOrder`` :ref:`global configuration option `. """ @@ -320,10 +323,12 @@ class ImageItem(GraphicsObject): if w == 0 or h == 0: self.qimage = None return - xds = int(1.0/w) - yds = int(1.0/h) - image = fn.downsample(self.image, xds, axis=0) - image = fn.downsample(image, yds, axis=1) + xds = max(1, int(1.0 / w)) + yds = max(1, int(1.0 / h)) + axes = [1, 0] if self.axisOrder == 'row-major' else [0, 1] + image = fn.downsample(self.image, xds, axis=axes[0]) + image = fn.downsample(image, yds, axis=axes[1]) + self._lastDownsample = (xds, yds) else: image = self.image @@ -351,7 +356,7 @@ class ImageItem(GraphicsObject): # Assume images are in column-major order for backward compatibility # (most images are in row-major order) - if getConfigOption('imageAxisOrder') == 'legacy': + if self.axisOrder == 'col-major': image = image.transpose((1, 0, 2)[:image.ndim]) argb, alpha = fn.makeARGB(image, lut=lut, levels=levels) @@ -370,7 +375,7 @@ class ImageItem(GraphicsObject): p.setCompositionMode(self.paintMode) profile('set comp mode') - shape = self.image.shape[:2] if getConfigOption('imageAxisOrder') == 'legacy' else self.image.shape[:2][::-1] + shape = self.image.shape[:2] if self.axisOrder == 'col-major' else self.image.shape[:2][::-1] p.drawImage(QtCore.QRectF(0,0,*shape), self.qimage) profile('p.drawImage') if self.border is not None: diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index 6dc1312a..2e588f5a 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -1031,7 +1031,7 @@ class ROI(GraphicsObject): ## Modify transform to scale from image coords to data coords axisOrder = getConfigOption('imageAxisOrder') - if axisOrder == 'normal': + 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()) @@ -1040,7 +1040,7 @@ class ROI(GraphicsObject): dataBounds = tr.mapRect(self.boundingRect()) ## Intersect transformed ROI bounds with data bounds - if axisOrder == 'normal': + 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])) @@ -1050,7 +1050,7 @@ class ROI(GraphicsObject): (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()))) ) - if axisOrder == 'normal': + if axisOrder == 'row-major': bounds = bounds[::-1] if returnSlice: @@ -1078,7 +1078,7 @@ class ROI(GraphicsObject): correspond to the (x, y) axes of *img*. If the global configuration variable :ref:`imageAxisOrder ` is set to - 'normal', then the axes are instead specified in + 'row-major', then the axes are instead specified in (y, x) order. returnMappedCoords (bool) If True, the array slice is returned along with a corresponding array of coordinates that were @@ -1149,7 +1149,7 @@ class ROI(GraphicsObject): shape = [abs(shape[0]/sx), abs(shape[1]/sy)] - if getConfigOption('imageAxisOrder') == 'normal': + if getConfigOption('imageAxisOrder') == 'row-major': vectors = [vectors[1][::-1], vectors[0][::-1]] shape = shape[::-1] origin = origin[::-1] @@ -1649,7 +1649,7 @@ class MultiRectROI(QtGui.QGraphicsObject): ## make sure orthogonal axis is the same size ## (sometimes fp errors cause differences) - if getConfigOption('imageAxisOrder') == 'normal': + if getConfigOption('imageAxisOrder') == 'row-major': axes = axes[::-1] ms = min([r.shape[axes[1]] for r in rgns]) sl = [slice(None)] * rgns[0].ndim diff --git a/pyqtgraph/graphicsItems/tests/test_ImageItem.py b/pyqtgraph/graphicsItems/tests/test_ImageItem.py index d13d703c..a4a77389 100644 --- a/pyqtgraph/graphicsItems/tests/test_ImageItem.py +++ b/pyqtgraph/graphicsItems/tests/test_ImageItem.py @@ -7,15 +7,24 @@ from pyqtgraph.tests import assertImageApproved app = pg.mkQApp() +class TransposedImageItem(pg.ImageItem): + def setImage(self, image=None, **kwds): + if image is not None: + image = np.swapaxes(image, 0, 1) + return pg.ImageItem.setImage(self, image, **kwds) -def test_ImageItem(): + +def test_ImageItem(transpose=False): w = pg.GraphicsWindow() view = pg.ViewBox() w.setCentralWidget(view) w.resize(200, 200) w.show() - img = pg.ImageItem(border=0.5) + if transpose: + img = TransposedImageItem(border=0.5) + else: + img = pg.ImageItem(border=0.5) view.addItem(img) @@ -77,7 +86,10 @@ def test_ImageItem(): assertImageApproved(w, 'imageitem/gradient_rgba_float', 'RGBA float gradient.') # checkerboard to test alpha - img2 = pg.ImageItem() + if transpose: + img2 = TransposedImageItem() + else: + img2 = pg.ImageItem() img2.setImage(np.fromfunction(lambda x,y: (x+y)%2, (10, 10)), levels=[-1,2]) view.addItem(img2) img2.scale(10, 10) @@ -103,9 +115,23 @@ def test_ImageItem(): img.setAutoDownsample(True) assertImageApproved(w, 'imageitem/resolution_with_downsampling_x', 'Resolution test with downsampling axross x axis.') + assert img._lastDownsample == (5, 1) img.setImage(data.T, levels=[-1, 1]) assertImageApproved(w, 'imageitem/resolution_with_downsampling_y', 'Resolution test with downsampling across y axis.') + assert img._lastDownsample == (1, 5) + + 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") diff --git a/pyqtgraph/tests/image_testing.py b/pyqtgraph/tests/image_testing.py index bab3acc4..edf55ce7 100644 --- a/pyqtgraph/tests/image_testing.py +++ b/pyqtgraph/tests/image_testing.py @@ -344,7 +344,7 @@ class ImageTester(QtGui.QWidget): for i, v in enumerate(self.views): v.setAspectLocked(1) v.invertY() - v.image = ImageItem() + v.image = ImageItem(axisOrder='row-major') v.image.setAutoDownsample(True) v.addItem(v.image) v.label = TextItem(labelText[i]) @@ -371,9 +371,9 @@ class ImageTester(QtGui.QWidget): message += '\nImage1: %s %s Image2: %s %s' % (im1.shape, im1.dtype, im2.shape, im2.dtype) self.label.setText(message) - self.views[0].image.setImage(im1.transpose(1, 0, 2)) - self.views[1].image.setImage(im2.transpose(1, 0, 2)) - diff = makeDiffImage(im1, im2).transpose(1, 0, 2) + self.views[0].image.setImage(im1) + self.views[1].image.setImage(im2) + diff = makeDiffImage(im1, im2) self.views[2].image.setImage(diff) self.views[0].autoRange()