diff --git a/pyqtgraph/SRTTransform.py b/pyqtgraph/SRTTransform.py index 23281343..b1aea297 100644 --- a/pyqtgraph/SRTTransform.py +++ b/pyqtgraph/SRTTransform.py @@ -3,6 +3,7 @@ from .Qt import QtCore, QtGui from .Point import Point import numpy as np + class SRTTransform(QtGui.QTransform): """Transform that can always be represented as a combination of 3 matrices: scale * rotate * translate This transform has no shear; angles are always preserved. @@ -165,6 +166,7 @@ class SRTTransform(QtGui.QTransform): def matrix(self): return np.array([[self.m11(), self.m12(), self.m13()],[self.m21(), self.m22(), self.m23()],[self.m31(), self.m32(), self.m33()]]) + if __name__ == '__main__': from . import widgets diff --git a/pyqtgraph/graphicsItems/GraphicsItem.py b/pyqtgraph/graphicsItems/GraphicsItem.py index 2ca35193..d45818dc 100644 --- a/pyqtgraph/graphicsItems/GraphicsItem.py +++ b/pyqtgraph/graphicsItems/GraphicsItem.py @@ -37,9 +37,6 @@ class GraphicsItem(object): if register: GraphicsScene.registerObject(self) ## workaround for pyqt bug in graphicsscene.items() - - - def getViewWidget(self): """ Return the view widget for this item. @@ -95,7 +92,6 @@ class GraphicsItem(object): def forgetViewBox(self): self._viewBox = None - def deviceTransform(self, viewportTransform=None): """ Return the transform that converts local item coordinates to device coordinates (usually pixels). diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py index b4e8bfc6..4dd895f2 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -279,6 +279,42 @@ class ImageItem(GraphicsObject): if gotNewData: self.sigImageChanged.emit() + def dataTransform(self): + """Return 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. + """ + # 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 + + def inverseDataTransform(self): + """Return the transform that maps from this image's local coordinate + system to its input array. + + See dataTransform() for more information. + """ + tr = QtGui.QTransform() + if self.axisOrder == 'row-major': + # transpose + tr.scale(1, -1) + tr.rotate(-90) + return tr + + def mapToData(self, obj): + tr = self.inverseDataTransform() + return tr.map(obj) + + def mapFromData(self, obj): + tr = self.dataTransform() + return tr.map(obj) + def quickMinMax(self, targetSize=1e6): """ Estimate the min/max values of the image data by subsampling. diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index 2e588f5a..b543ac57 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -1017,7 +1017,7 @@ class ROI(GraphicsObject): If returnSlice is set to False, the function returns a pair of tuples with the values that would have been used to generate the slice objects. ((ax0Start, ax0Stop), (ax1Start, ax1Stop)) - If the slice can not be computed (usually because the scene/transforms are not properly + If the slice cannot be computed (usually because the scene/transforms are not properly constructed yet), then the method returns None. """ ## Determine shape of array along ROI axes @@ -1104,7 +1104,8 @@ class ROI(GraphicsObject): shape, vectors, origin = self.getAffineSliceParams(data, img, axes, fromBoundingRect=fromBR) if not returnMappedCoords: - return fn.affineSlice(data, shape=shape, vectors=vectors, origin=origin, axes=axes, **kwds) + rgn = fn.affineSlice(data, shape=shape, vectors=vectors, origin=origin, axes=axes, **kwds) + return rgn else: kwds['returnCoords'] = True result, coords = fn.affineSlice(data, shape=shape, vectors=vectors, origin=origin, axes=axes, **kwds) @@ -1119,29 +1120,34 @@ class ROI(GraphicsObject): (shape, vectors, origin) to extract a subset of *data* using this ROI and *img* to specify the subset. + If *fromBoundingRect* is True, then the ROI's bounding rectangle is used + rather than the shape of the ROI. + See :func:`getArrayRegion ` for more information. """ if self.scene() is not img.scene(): raise Exception("ROI and target item must be members of the same scene.") - origin = self.mapToItem(img, QtCore.QPointF(0, 0)) + origin = img.mapToData(self.mapToItem(img, QtCore.QPointF(0, 0))) ## vx and vy point in the directions of the slice axes, but must be scaled properly - vx = self.mapToItem(img, QtCore.QPointF(1, 0)) - origin - vy = self.mapToItem(img, QtCore.QPointF(0, 1)) - origin + vx = img.mapToData(self.mapToItem(img, QtCore.QPointF(1, 0))) - origin + vy = img.mapToData(self.mapToItem(img, QtCore.QPointF(0, 1))) - origin lvx = np.sqrt(vx.x()**2 + vx.y()**2) lvy = np.sqrt(vy.x()**2 + vy.y()**2) - pxLen = img.width() / float(data.shape[axes[0]]) - #img.width is number of pixels, not width of item. - #need pxWidth and pxHeight instead of pxLen ? - sx = pxLen / lvx - sy = pxLen / lvy + #pxLen = img.width() / float(data.shape[axes[0]]) + ##img.width is number of pixels, not width of item. + ##need pxWidth and pxHeight instead of pxLen ? + #sx = pxLen / lvx + #sy = pxLen / lvy + sx = 1.0 / lvx + sy = 1.0 / lvy vectors = ((vx.x()*sx, vx.y()*sx), (vy.x()*sy, vy.y()*sy)) if fromBoundingRect is True: shape = self.boundingRect().width(), self.boundingRect().height() - origin = self.mapToItem(img, self.boundingRect().topLeft()) + origin = img.mapToData(self.mapToItem(img, self.boundingRect().topLeft())) origin = (origin.x(), origin.y()) else: shape = self.state['size'] @@ -1150,10 +1156,10 @@ class ROI(GraphicsObject): shape = [abs(shape[0]/sx), abs(shape[1]/sy)] if getConfigOption('imageAxisOrder') == 'row-major': - vectors = [vectors[1][::-1], vectors[0][::-1]] + # transpose output + vectors = vectors[::-1] shape = shape[::-1] - origin = origin[::-1] - + return shape, vectors, origin def renderShapeMask(self, width, height): diff --git a/pyqtgraph/graphicsItems/tests/test_ROI.py b/pyqtgraph/graphicsItems/tests/test_ROI.py index 2837119d..9e67fb8d 100644 --- a/pyqtgraph/graphicsItems/tests/test_ROI.py +++ b/pyqtgraph/graphicsItems/tests/test_ROI.py @@ -8,7 +8,7 @@ from pyqtgraph.tests import assertImageApproved, mouseMove, mouseDrag, mouseClic app = pg.mkQApp() -def test_getArrayRegion(): +def test_getArrayRegion(transpose=False): pr = pg.PolyLineROI([[0, 0], [27, 0], [0, 28]], closed=True) pr.setPos(1, 1) rois = [ @@ -23,13 +23,19 @@ def test_getArrayRegion(): origMode = pg.getConfigOption('imageAxisOrder') try: - pg.setConfigOptions(imageAxisOrder='col-major') - check_getArrayRegion(roi, 'roi/'+name, testResize) - pg.setConfigOptions(imageAxisOrder='row-major') - check_getArrayRegion(roi, 'roi/'+name, testResize, transpose=True) + if transpose: + pg.setConfigOptions(imageAxisOrder='row-major') + check_getArrayRegion(roi, 'roi/'+name, testResize, transpose=True) + else: + pg.setConfigOptions(imageAxisOrder='col-major') + check_getArrayRegion(roi, 'roi/'+name, testResize) finally: pg.setConfigOptions(imageAxisOrder=origMode) + +def test_getArrayRegion_axisorder(): + test_getArrayRegion(transpose=True) + def check_getArrayRegion(roi, name, testResize=True, transpose=False): initState = roi.getState() @@ -55,8 +61,8 @@ def check_getArrayRegion(roi, name, testResize=True, transpose=False): vb2.setPos(6, 203) vb2.resize(188, 191) - img1 = TransposedImageItem(border='w', transpose=transpose) - img2 = TransposedImageItem(border='w', transpose=transpose) + img1 = pg.ImageItem(border='w') + img2 = pg.ImageItem(border='w') vb1.addItem(img1) vb2.addItem(img2) @@ -68,6 +74,9 @@ def check_getArrayRegion(roi, name, testResize=True, transpose=False): data[:, :, 2, :] += 10 data[:, :, :, 3] += 10 + if transpose: + data = data.transpose(0, 2, 1, 3) + img1.setImage(data[0, ..., 0]) vb1.setAspectLocked() vb1.enableAutoRange(True, True) @@ -75,6 +84,12 @@ def check_getArrayRegion(roi, name, testResize=True, transpose=False): roi.setZValue(10) vb1.addItem(roi) + if isinstance(roi, pg.RectROI): + if transpose: + assert roi.getAffineSliceParams(data, img1, axes=(1, 2)) == ([28.0, 27.0], ((1.0, 0.0), (0.0, 1.0)), (1.0, 1.0)) + else: + assert roi.getAffineSliceParams(data, img1, axes=(1, 2)) == ([27.0, 28.0], ((1.0, 0.0), (0.0, 1.0)), (1.0, 1.0)) + rgn = roi.getArrayRegion(data, img1, axes=(1, 2)) #assert np.all((rgn == data[:, 1:-2, 1:-2, :]) | (rgn == 0)) img2.setImage(rgn[0, ..., 0])