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:
parent
54fbfdb918
commit
a76fc37112
@ -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.
|
||||||
|
@ -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.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -7,14 +7,23 @@ 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()
|
||||||
|
if transpose:
|
||||||
|
img = TransposedImageItem(border=0.5)
|
||||||
|
else:
|
||||||
img = pg.ImageItem(border=0.5)
|
img = pg.ImageItem(border=0.5)
|
||||||
view.addItem(img)
|
view.addItem(img)
|
||||||
|
|
||||||
@ -77,6 +86,9 @@ 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
|
||||||
|
if transpose:
|
||||||
|
img2 = TransposedImageItem()
|
||||||
|
else:
|
||||||
img2 = pg.ImageItem()
|
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)
|
||||||
@ -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")
|
||||||
|
@ -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()
|
||||||
|
Loading…
Reference in New Issue
Block a user