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
This commit is contained in:
Luke Campagnola 2016-06-16 08:54:52 -07:00
parent 54fbfdb918
commit a76fc37112
6 changed files with 62 additions and 30 deletions

View File

@ -19,9 +19,10 @@ foreground See :func:`mkColor` 'd' Default foreground col
background See :func:`mkColor` 'k' Default background for :class:`GraphicsView`. background See :func:`mkColor` 'k' Default background for :class:`GraphicsView`.
antialias bool False Enabling antialiasing causes lines to be drawn with antialias bool False Enabling antialiasing causes lines to be drawn with
smooth edges at the cost of reduced performance. 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. imageAxisOrder str 'legacy' For 'row-major', image data is expected in the standard row-major
For 'legacy', image data is expected in reversed column-major (col, row) order. (row, col) order. For 'col-major', image data is expected in
The default is 'legacy' for backward compatibility, but this may reversed column-major (col, row) order.
The default is 'col-major' for backward compatibility, but this may
change in the future. change in the future.
editorCommand str or None None Command used to invoke code editor from ConsoleWidget. 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. exitCleanup bool True Attempt to work around some exit crash bugs in PyQt and PySide.

View File

@ -59,9 +59,9 @@ CONFIG_OPTIONS = {
'exitCleanup': True, ## Attempt to work around some exit crash bugs in PyQt and PySide 'exitCleanup': True, ## Attempt to work around some exit crash bugs in PyQt and PySide
'enableExperimental': False, ## Enable experimental features (the curious can search for this key in the code) 'enableExperimental': False, ## Enable experimental features (the curious can search for this key in the code)
'crashWarning': False, # If True, print warnings about situations that may result in a crash 'crashWarning': False, # If True, print warnings about situations that may result in a crash
'imageAxisOrder': 'legacy', # For 'normal', image data is expected in the standard (row, col) order. 'imageAxisOrder': 'col-major', # For 'row-major', image data is expected in the standard (row, col) order.
# For 'legacy', image data is expected in reversed (col, row) order. # For 'col-major', image data is expected in reversed (col, row) order.
# The default is 'legacy' for backward compatibility, but this will # The default is 'col-major' for backward compatibility, but this may
# change in the future. # change in the future.
} }

View File

@ -48,6 +48,8 @@ class ImageItem(GraphicsObject):
self.lut = None self.lut = None
self.autoDownsample = False self.autoDownsample = False
self.axisOrder = getConfigOption('imageAxisOrder')
# In some cases, we use a modified lookup table to handle both rescaling # In some cases, we use a modified lookup table to handle both rescaling
# and LUT more efficiently # and LUT more efficiently
self._effectiveLut = None self._effectiveLut = None
@ -87,13 +89,13 @@ class ImageItem(GraphicsObject):
def width(self): def width(self):
if self.image is None: if self.image is None:
return 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] return self.image.shape[axis]
def height(self): def height(self):
if self.image is None: if self.image is None:
return None return None
axis = 1 if getConfigOption('imageAxisOrder') == 'legacy' else 0 axis = 1 if self.axisOrder == 'col-major' else 0
return self.image.shape[axis] return self.image.shape[axis]
def boundingRect(self): def boundingRect(self):
@ -150,7 +152,8 @@ class ImageItem(GraphicsObject):
self.update() self.update()
def setOpts(self, update=True, **kargs): def setOpts(self, update=True, **kargs):
if 'axisOrder' in kargs:
self.axisOrder = kargs['axisOrder']
if 'lut' in kargs: if 'lut' in kargs:
self.setLookupTable(kargs['lut'], update=update) self.setLookupTable(kargs['lut'], update=update)
if 'levels' in kargs: if 'levels' in kargs:
@ -220,8 +223,8 @@ class ImageItem(GraphicsObject):
imageitem.setImage(imagedata.T) imageitem.setImage(imagedata.T)
This requirement can be changed by the ``imageAxisOrder`` This requirement can be changed by calling ``image.setOpts(axisOrder='row-major')`` or
:ref:`global configuration option <apiref_config>`. by changing the ``imageAxisOrder`` :ref:`global configuration option <apiref_config>`.
""" """
@ -320,10 +323,12 @@ class ImageItem(GraphicsObject):
if w == 0 or h == 0: if w == 0 or h == 0:
self.qimage = None self.qimage = None
return return
xds = int(1.0/w) xds = max(1, int(1.0 / w))
yds = int(1.0/h) yds = max(1, int(1.0 / h))
image = fn.downsample(self.image, xds, axis=0) axes = [1, 0] if self.axisOrder == 'row-major' else [0, 1]
image = fn.downsample(image, yds, axis=1) image = fn.downsample(self.image, xds, axis=axes[0])
image = fn.downsample(image, yds, axis=axes[1])
self._lastDownsample = (xds, yds)
else: else:
image = self.image image = self.image
@ -351,7 +356,7 @@ class ImageItem(GraphicsObject):
# Assume images are in column-major order for backward compatibility # Assume images are in column-major order for backward compatibility
# (most images are in row-major order) # (most images are in row-major order)
if getConfigOption('imageAxisOrder') == 'legacy': if self.axisOrder == 'col-major':
image = image.transpose((1, 0, 2)[:image.ndim]) image = image.transpose((1, 0, 2)[:image.ndim])
argb, alpha = fn.makeARGB(image, lut=lut, levels=levels) argb, alpha = fn.makeARGB(image, lut=lut, levels=levels)
@ -370,7 +375,7 @@ class ImageItem(GraphicsObject):
p.setCompositionMode(self.paintMode) p.setCompositionMode(self.paintMode)
profile('set comp mode') 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) p.drawImage(QtCore.QRectF(0,0,*shape), self.qimage)
profile('p.drawImage') profile('p.drawImage')
if self.border is not None: if self.border is not None:

View File

@ -1031,7 +1031,7 @@ class ROI(GraphicsObject):
## Modify transform to scale from image coords to data coords ## Modify transform to scale from image coords to data coords
axisOrder = getConfigOption('imageAxisOrder') axisOrder = getConfigOption('imageAxisOrder')
if axisOrder == 'normal': if axisOrder == 'row-major':
tr.scale(float(dShape[1]) / img.width(), float(dShape[0]) / img.height()) tr.scale(float(dShape[1]) / img.width(), float(dShape[0]) / img.height())
else: else:
tr.scale(float(dShape[0]) / img.width(), float(dShape[1]) / img.height()) tr.scale(float(dShape[0]) / img.width(), float(dShape[1]) / img.height())
@ -1040,7 +1040,7 @@ class ROI(GraphicsObject):
dataBounds = tr.mapRect(self.boundingRect()) dataBounds = tr.mapRect(self.boundingRect())
## Intersect transformed ROI bounds with data bounds ## 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])) intBounds = dataBounds.intersected(QtCore.QRectF(0, 0, dShape[1], dShape[0]))
else: else:
intBounds = dataBounds.intersected(QtCore.QRectF(0, 0, dShape[0], dShape[1])) 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.left(), intBounds.right())), int(1+max(intBounds.left(), intBounds.right()))),
(int(min(intBounds.bottom(), intBounds.top())), int(1+max(intBounds.bottom(), intBounds.top()))) (int(min(intBounds.bottom(), intBounds.top())), int(1+max(intBounds.bottom(), intBounds.top())))
) )
if axisOrder == 'normal': if axisOrder == 'row-major':
bounds = bounds[::-1] bounds = bounds[::-1]
if returnSlice: if returnSlice:
@ -1078,7 +1078,7 @@ class ROI(GraphicsObject):
correspond to the (x, y) axes of *img*. If the correspond to the (x, y) axes of *img*. If the
global configuration variable global configuration variable
:ref:`imageAxisOrder <apiref_config>` is set to :ref:`imageAxisOrder <apiref_config>` is set to
'normal', then the axes are instead specified in 'row-major', then the axes are instead specified in
(y, x) order. (y, x) order.
returnMappedCoords (bool) If True, the array slice is returned along returnMappedCoords (bool) If True, the array slice is returned along
with a corresponding array of coordinates that were with a corresponding array of coordinates that were
@ -1149,7 +1149,7 @@ class ROI(GraphicsObject):
shape = [abs(shape[0]/sx), abs(shape[1]/sy)] 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]] vectors = [vectors[1][::-1], vectors[0][::-1]]
shape = shape[::-1] shape = shape[::-1]
origin = origin[::-1] origin = origin[::-1]
@ -1649,7 +1649,7 @@ class MultiRectROI(QtGui.QGraphicsObject):
## make sure orthogonal axis is the same size ## make sure orthogonal axis is the same size
## (sometimes fp errors cause differences) ## (sometimes fp errors cause differences)
if getConfigOption('imageAxisOrder') == 'normal': if getConfigOption('imageAxisOrder') == 'row-major':
axes = axes[::-1] axes = axes[::-1]
ms = min([r.shape[axes[1]] for r in rgns]) ms = min([r.shape[axes[1]] for r in rgns])
sl = [slice(None)] * rgns[0].ndim sl = [slice(None)] * rgns[0].ndim

View File

@ -7,15 +7,24 @@ from pyqtgraph.tests import assertImageApproved
app = pg.mkQApp() 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() w = pg.GraphicsWindow()
view = pg.ViewBox() view = pg.ViewBox()
w.setCentralWidget(view) w.setCentralWidget(view)
w.resize(200, 200) w.resize(200, 200)
w.show() w.show()
img = pg.ImageItem(border=0.5) if transpose:
img = TransposedImageItem(border=0.5)
else:
img = pg.ImageItem(border=0.5)
view.addItem(img) view.addItem(img)
@ -77,7 +86,10 @@ def test_ImageItem():
assertImageApproved(w, 'imageitem/gradient_rgba_float', 'RGBA float gradient.') assertImageApproved(w, 'imageitem/gradient_rgba_float', 'RGBA float gradient.')
# checkerboard to test alpha # checkerboard to test alpha
img2 = pg.ImageItem() if transpose:
img2 = TransposedImageItem()
else:
img2 = pg.ImageItem()
img2.setImage(np.fromfunction(lambda x,y: (x+y)%2, (10, 10)), levels=[-1,2]) img2.setImage(np.fromfunction(lambda x,y: (x+y)%2, (10, 10)), levels=[-1,2])
view.addItem(img2) view.addItem(img2)
img2.scale(10, 10) img2.scale(10, 10)
@ -103,9 +115,23 @@ def test_ImageItem():
img.setAutoDownsample(True) img.setAutoDownsample(True)
assertImageApproved(w, 'imageitem/resolution_with_downsampling_x', 'Resolution test with downsampling axross x axis.') assertImageApproved(w, 'imageitem/resolution_with_downsampling_x', 'Resolution test with downsampling axross x axis.')
assert img._lastDownsample == (5, 1)
img.setImage(data.T, levels=[-1, 1]) img.setImage(data.T, levels=[-1, 1])
assertImageApproved(w, 'imageitem/resolution_with_downsampling_y', 'Resolution test with downsampling across y axis.') assertImageApproved(w, 'imageitem/resolution_with_downsampling_y', 'Resolution test with downsampling across y axis.')
assert img._lastDownsample == (1, 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") @pytest.mark.skipif(pg.Qt.USE_PYSIDE, reason="pyside does not have qWait")

View File

@ -344,7 +344,7 @@ class ImageTester(QtGui.QWidget):
for i, v in enumerate(self.views): for i, v in enumerate(self.views):
v.setAspectLocked(1) v.setAspectLocked(1)
v.invertY() v.invertY()
v.image = ImageItem() v.image = ImageItem(axisOrder='row-major')
v.image.setAutoDownsample(True) v.image.setAutoDownsample(True)
v.addItem(v.image) v.addItem(v.image)
v.label = TextItem(labelText[i]) v.label = TextItem(labelText[i])
@ -371,9 +371,9 @@ class ImageTester(QtGui.QWidget):
message += '\nImage1: %s %s Image2: %s %s' % (im1.shape, im1.dtype, im2.shape, im2.dtype) message += '\nImage1: %s %s Image2: %s %s' % (im1.shape, im1.dtype, im2.shape, im2.dtype)
self.label.setText(message) self.label.setText(message)
self.views[0].image.setImage(im1.transpose(1, 0, 2)) self.views[0].image.setImage(im1)
self.views[1].image.setImage(im2.transpose(1, 0, 2)) self.views[1].image.setImage(im2)
diff = makeDiffImage(im1, im2).transpose(1, 0, 2) diff = makeDiffImage(im1, im2)
self.views[2].image.setImage(diff) self.views[2].image.setImage(diff)
self.views[0].autoRange() self.views[0].autoRange()