|
@@ -0,0 +1,433 @@
|
|
|
|
+""" ImageViewer.py: PyQt image viewer widget based on QGraphicsView with mouse croping.
|
|
|
|
+"""
|
|
|
|
+
|
|
|
|
+import os.path
|
|
|
|
+
|
|
|
|
+from PySide6.QtCore import Qt, QRectF, QRect, QPoint, QPointF, QEvent, QSize, Signal
|
|
|
|
+from PySide6.QtGui import QImage, QPixmap, QPainterPath, QMouseEvent, QPainter, QPen
|
|
|
|
+from PySide6.QtWidgets import QGraphicsView, QGraphicsScene, QFileDialog, QSizePolicy, QMdiArea
|
|
|
|
+
|
|
|
|
+# numpy is optional: only needed if you want to display numpy 2d arrays as images.
|
|
|
|
+try:
|
|
|
|
+ import numpy as np
|
|
|
|
+except ImportError:
|
|
|
|
+ np = None
|
|
|
|
+
|
|
|
|
+# qimage2ndarray is optional: useful for displaying numpy 2d arrays as images.
|
|
|
|
+# !!! qimage2ndarray requires PyQt5.
|
|
|
|
+# Some custom code in the viewer appears to handle the conversion from numpy 2d arrays,
|
|
|
|
+# so qimage2ndarray probably is not needed anymore. I've left it here just in case.
|
|
|
|
+try:
|
|
|
|
+ import qimage2ndarray
|
|
|
|
+except ImportError:
|
|
|
|
+ qimage2ndarray = None
|
|
|
|
+
|
|
|
|
+__author__ = "Alex Sidorov <alex.sidorof@ya.ru>"
|
|
|
|
+__version__ = '0.1'
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+class ImageViewer(QGraphicsView):
|
|
|
|
+ """ PyQt image viewer widget based on QGraphicsView with mouse croping.
|
|
|
|
+ Image File:
|
|
|
|
+ -----------
|
|
|
|
+ Calling set_file()
|
|
|
|
+ Image:
|
|
|
|
+ ------
|
|
|
|
+ Use the setImage(im) method to set the image data in the viewer.
|
|
|
|
+ - im can be a QImage, QPixmap, or NumPy 2D array (the later requires the package qimage2ndarray).
|
|
|
|
+ For display in the QGraphicsView the image will be converted to a QPixmap.
|
|
|
|
+ Some useful image format conversion utilities:
|
|
|
|
+ qimage2ndarray: NumPy ndarray <==> QImage (https://github.com/hmeine/qimage2ndarray)
|
|
|
|
+ ImageQt: PIL Image <==> QImage (https://github.com/python-pillow/Pillow/blob/master/PIL/ImageQt.py)
|
|
|
|
+ Mouse:
|
|
|
|
+ ------
|
|
|
|
+ Mouse interactions for cropted and panning is fully customizable by simply setting the desired button interactions:
|
|
|
|
+ e.g.,
|
|
|
|
+ regionZoomButton = Qt.LeftButton # Drag a zoom box.
|
|
|
|
+ zoomOutButton = Qt.RightButton # Pop end of zoom stack (double click clears zoom stack).
|
|
|
|
+ panButton = Qt.MiddleButton # Drag to pan.
|
|
|
|
+ wheelZoomFactor = 1.25 # Set to None or 1 to disable mouse wheel zoom.
|
|
|
|
+ To disable any interaction, just disable its button.
|
|
|
|
+ e.g., to disable panning:
|
|
|
|
+ panButton = None
|
|
|
|
+ """
|
|
|
|
+
|
|
|
|
+ # Mouse button signals emit image scene (x, y) coordinates.
|
|
|
|
+ # !!! For image (row, column) matrix indexing, row = y and column = x.
|
|
|
|
+ # !!! These signals will NOT be emitted if the event is handled by an interaction such as zoom or pan.
|
|
|
|
+ # !!! If aspect ratio prevents image from filling viewport, emitted position may be outside image bounds.
|
|
|
|
+ leftMouseButtonPressed = Signal(float, float)
|
|
|
|
+ leftMouseButtonReleased = Signal(float, float)
|
|
|
|
+ middleMouseButtonPressed = Signal(float, float)
|
|
|
|
+ middleMouseButtonReleased = Signal(float, float)
|
|
|
|
+ rightMouseButtonPressed = Signal(float, float)
|
|
|
|
+ rightMouseButtonReleased = Signal(float, float)
|
|
|
|
+ leftMouseButtonDoubleClicked = Signal(float, float)
|
|
|
|
+ rightMouseButtonDoubleClicked = Signal(float, float)
|
|
|
|
+
|
|
|
|
+ # Emitted upon zooming/panning.
|
|
|
|
+ viewChanged = Signal()
|
|
|
|
+
|
|
|
|
+ # Emitted on mouse motion.
|
|
|
|
+ # Emits mouse position over image in image pixel coordinates.
|
|
|
|
+ # !!! setMouseTracking(True) if you want to use this at all times.
|
|
|
|
+ mousePositionOnImageChanged = Signal(QPoint)
|
|
|
|
+
|
|
|
|
+ def __init__(self, parent=None):
|
|
|
|
+ super(ImageViewer, self).__init__(parent)
|
|
|
|
+
|
|
|
|
+ # Image is displayed as a QPixmap in a QGraphicsScene attached to this QGraphicsView.
|
|
|
|
+ self.scene = QGraphicsScene()
|
|
|
|
+ self.setScene(self.scene)
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ # Better quality pixmap scaling?
|
|
|
|
+ # self.setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform)
|
|
|
|
+
|
|
|
|
+ # Displayed image pixmap in the QGraphicsScene.
|
|
|
|
+ self._image: QPixmap = None
|
|
|
|
+
|
|
|
|
+ # Image aspect ratio mode.
|
|
|
|
+ # Qt.IgnoreAspectRatio: Scale image to fit viewport.
|
|
|
|
+ # Qt.KeepAspectRatio: Scale image to fit inside viewport, preserving aspect ratio.
|
|
|
|
+ # Qt.KeepAspectRatioByExpanding: Scale image to fill the viewport, preserving aspect ratio.
|
|
|
|
+ self.aspectRatioMode = Qt.AspectRatioMode.KeepAspectRatio
|
|
|
|
+
|
|
|
|
+ # Scroll bar behaviour.
|
|
|
|
+ # Qt.ScrollBarAlwaysOff: Never shows a scroll bar.
|
|
|
|
+ # Qt.ScrollBarAlwaysOn: Always shows a scroll bar.
|
|
|
|
+ # Qt.ScrollBarAsNeeded: Shows a scroll bar only when zoomed.
|
|
|
|
+ self.setHorizontalScrollBarPolicy(
|
|
|
|
+ Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
|
|
|
+ self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
|
|
|
+
|
|
|
|
+ # Interactions (set buttons to None to disable interactions)
|
|
|
|
+ # !!! Events handled by interactions will NOT emit *MouseButton* signals.
|
|
|
|
+ # Note: regionZoomButton will still emit a *MouseButtonReleased signal on a click (i.e. tiny box).
|
|
|
|
+ self.regionZoomButton = Qt.MouseButton.LeftButton # Drag a zoom box.
|
|
|
|
+ # Pop end of zoom stack (double click clears zoom stack).
|
|
|
|
+ self.zoomOutButton = Qt.MouseButton.RightButton
|
|
|
|
+ self.panButton = Qt.MouseButton.MiddleButton # Drag to pan.
|
|
|
|
+ # Set to None or 1 to disable mouse wheel zoom.
|
|
|
|
+ self.wheelZoomFactor = 1.25
|
|
|
|
+
|
|
|
|
+ # Stack of QRectF zoom boxes in scene coordinates.
|
|
|
|
+ # !!! If you update this manually, be sure to call updateViewer() to reflect any changes.
|
|
|
|
+ self.zoomStack: list[QRectF] = []
|
|
|
|
+
|
|
|
|
+ #Stack of image
|
|
|
|
+ self.imageStack: list[QPixmap] = []
|
|
|
|
+
|
|
|
|
+ # Flags for active zooming/panning.
|
|
|
|
+ self._isZooming: bool = False
|
|
|
|
+ self._isPanning: bool = False
|
|
|
|
+
|
|
|
|
+ # Store temporary position in screen pixels or scene units.
|
|
|
|
+ self._pixelPosition = QPoint()
|
|
|
|
+ self._scenePosition = QPointF()
|
|
|
|
+
|
|
|
|
+ # Track mouse position. e.g., For displaying coordinates in a UI.
|
|
|
|
+ # self.setMouseTracking(True)
|
|
|
|
+
|
|
|
|
+ self.setSizePolicy(QSizePolicy.Policy.Expanding,
|
|
|
|
+ QSizePolicy.Policy.Expanding)
|
|
|
|
+
|
|
|
|
+ def sizeHint(self):
|
|
|
|
+ return QSize(900, 600)
|
|
|
|
+
|
|
|
|
+ def hasImage(self):
|
|
|
|
+ """ Returns whether the scene contains an image pixmap.
|
|
|
|
+ """
|
|
|
|
+ return self._image is not None
|
|
|
|
+
|
|
|
|
+ def clearImage(self):
|
|
|
|
+ """ Removes the current image pixmap from the scene if it exists.
|
|
|
|
+ """
|
|
|
|
+ if self.hasImage():
|
|
|
|
+ self.scene.removeItem(self._image)
|
|
|
|
+ self._image = None
|
|
|
|
+
|
|
|
|
+ def pixmap(self):
|
|
|
|
+ """ Returns the scene's current image pixmap as a QPixmap, or else None if no image exists.
|
|
|
|
+ :rtype: QPixmap | None
|
|
|
|
+ """
|
|
|
|
+ if self.hasImage():
|
|
|
|
+ return self._image.pixmap()
|
|
|
|
+ return None
|
|
|
|
+
|
|
|
|
+ def image(self):
|
|
|
|
+ """ Returns the scene's current image pixmap as a QImage, or else None if no image exists.
|
|
|
|
+ :rtype: QImage | None
|
|
|
|
+ """
|
|
|
|
+ if self.hasImage():
|
|
|
|
+ return self._image.pixmap().toImage()
|
|
|
|
+ return None
|
|
|
|
+
|
|
|
|
+ def setImage(self, image):
|
|
|
|
+ """ Set the scene's current image pixmap to the input QImage or QPixmap.
|
|
|
|
+ Raises a RuntimeError if the input image has type other than QImage or QPixmap.
|
|
|
|
+ :type image: QImage | QPixmap
|
|
|
|
+ """
|
|
|
|
+ if type(image) is QPixmap:
|
|
|
|
+ pixmap = image
|
|
|
|
+ elif type(image) is QImage:
|
|
|
|
+ pixmap = QPixmap.fromImage(image)
|
|
|
|
+ elif type(image) is str:
|
|
|
|
+ pixmap = QPixmap.fromImage(QImage(image))
|
|
|
|
+ elif (np is not None) and (type(image) is np.ndarray):
|
|
|
|
+ if qimage2ndarray is not None:
|
|
|
|
+ qimage = qimage2ndarray.array2qimage(image, True)
|
|
|
|
+ pixmap = QPixmap.fromImage(qimage)
|
|
|
|
+ else:
|
|
|
|
+ image = image.astype(np.float32)
|
|
|
|
+ image -= image.min()
|
|
|
|
+ image /= image.max()
|
|
|
|
+ image *= 255
|
|
|
|
+ image[image > 255] = 255
|
|
|
|
+ image[image < 0] = 0
|
|
|
|
+ image = image.astype(np.uint8)
|
|
|
|
+ height, width = image.shape
|
|
|
|
+ bytes = image.tobytes()
|
|
|
|
+ qimage = QImage(bytes, width, height,
|
|
|
|
+ QImage.Format.Format_Grayscale8)
|
|
|
|
+ pixmap = QPixmap.fromImage(qimage)
|
|
|
|
+ else:
|
|
|
|
+ raise RuntimeError(
|
|
|
|
+ "ImageViewer.setImage: Argument must be a QImage, QPixmap, or numpy.ndarray.")
|
|
|
|
+
|
|
|
|
+ if self.hasImage():
|
|
|
|
+ self._image.setPixmap(pixmap)
|
|
|
|
+ else:
|
|
|
|
+ self._image = self.scene.addPixmap(pixmap)
|
|
|
|
+
|
|
|
|
+ self.imageStack.append(pixmap)
|
|
|
|
+ # Better quality pixmap scaling?
|
|
|
|
+ # !!! This will distort actual pixel data when zoomed way in.
|
|
|
|
+ # For scientific image analysis, you probably don't want this.
|
|
|
|
+ self._image.setTransformationMode(Qt.SmoothTransformation)
|
|
|
|
+
|
|
|
|
+ # Set scene size to image size.
|
|
|
|
+ self.setSceneRect(QRectF(pixmap.rect()))
|
|
|
|
+ self.updateViewer()
|
|
|
|
+
|
|
|
|
+ def updateViewer(self):
|
|
|
|
+ """ Show current zoom (if showing entire image, apply current aspect ratio mode).
|
|
|
|
+ """
|
|
|
|
+ if not self.hasImage():
|
|
|
|
+ return
|
|
|
|
+ if len(self.imageStack) > 1:
|
|
|
|
+ # Show zoomed rect.
|
|
|
|
+ # self.fitInView(self.zoomStack[-1], self.aspectRatioMode)
|
|
|
|
+ self._image.setPixmap(self.imageStack[-1])
|
|
|
|
+ else:
|
|
|
|
+ # Show entire image.
|
|
|
|
+ self._image.setPixmap(self.imageStack[0])
|
|
|
|
+ # self.fitInView(self.sceneRect(), self.aspectRatioMode)
|
|
|
|
+
|
|
|
|
+ def clearZoom(self):
|
|
|
|
+ if len(self.zoomStack) > 0:
|
|
|
|
+ self.zoomStack = []
|
|
|
|
+ self.imageStack = self.imageStack[0]
|
|
|
|
+ self.updateViewer()
|
|
|
|
+ self.viewChanged.emit()
|
|
|
|
+
|
|
|
|
+ def resizeEvent(self, event):
|
|
|
|
+ """ Maintain current zoom on resize.
|
|
|
|
+ """
|
|
|
|
+ self.updateViewer()
|
|
|
|
+
|
|
|
|
+ def mousePressEvent(self, event):
|
|
|
|
+ """ Start mouse pan or zoom mode.
|
|
|
|
+ """
|
|
|
|
+ # Ignore dummy events. e.g., Faking pan with left button ScrollHandDrag.
|
|
|
|
+ dummyModifiers = Qt.KeyboardModifier(Qt.KeyboardModifier.ShiftModifier | Qt.KeyboardModifier.ControlModifier
|
|
|
|
+ | Qt.KeyboardModifier.AltModifier | Qt.KeyboardModifier.MetaModifier)
|
|
|
|
+ if event.modifiers() == dummyModifiers:
|
|
|
|
+ QGraphicsView.mousePressEvent(self, event)
|
|
|
|
+ event.accept()
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ # Start dragging a region zoom box?
|
|
|
|
+ if (self.regionZoomButton is not None) and (event.button() == self.regionZoomButton):
|
|
|
|
+ self._pixelPosition = event.pos() # store pixel position
|
|
|
|
+ self.setDragMode(QGraphicsView.DragMode.RubberBandDrag)
|
|
|
|
+ QGraphicsView.mousePressEvent(self, event)
|
|
|
|
+ event.accept()
|
|
|
|
+ self._isZooming = True
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ if (self.zoomOutButton is not None) and (event.button() == self.zoomOutButton):
|
|
|
|
+ if len(self.zoomStack):
|
|
|
|
+ self.zoomStack.pop()
|
|
|
|
+ self.imageStack.pop()
|
|
|
|
+ self.updateViewer()
|
|
|
|
+ self.viewChanged.emit()
|
|
|
|
+ event.accept()
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ # Start dragging to pan?
|
|
|
|
+ if (self.panButton is not None) and (event.button() == self.panButton):
|
|
|
|
+ self._pixelPosition = event.pos() # store pixel position
|
|
|
|
+ self.setDragMode(QGraphicsView.DragMode.ScrollHandDrag)
|
|
|
|
+ if self.panButton == Qt.MouseButton.LeftButton:
|
|
|
|
+ QGraphicsView.mousePressEvent(self, event)
|
|
|
|
+ else:
|
|
|
|
+ # ScrollHandDrag ONLY works with LeftButton, so fake it.
|
|
|
|
+ # Use a bunch of dummy modifiers to notify that event should NOT be handled as usual.
|
|
|
|
+ self.viewport().setCursor(Qt.CursorShape.ClosedHandCursor)
|
|
|
|
+ dummyModifiers = Qt.KeyboardModifier(Qt.KeyboardModifier.ShiftModifier
|
|
|
|
+ | Qt.KeyboardModifier.ControlModifier
|
|
|
|
+ | Qt.KeyboardModifier.AltModifier
|
|
|
|
+ | Qt.KeyboardModifier.MetaModifier)
|
|
|
|
+ dummyEvent = QMouseEvent(QEvent.Type.MouseButtonPress, QPointF(event.pos()), Qt.MouseButton.LeftButton,
|
|
|
|
+ event.buttons(), dummyModifiers)
|
|
|
|
+ self.mousePressEvent(dummyEvent)
|
|
|
|
+ sceneViewport = self.mapToScene(
|
|
|
|
+ self.viewport().rect()).boundingRect().intersected(self.sceneRect())
|
|
|
|
+ self._scenePosition = sceneViewport.topLeft()
|
|
|
|
+ event.accept()
|
|
|
|
+ self._isPanning = True
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ scenePos = self.mapToScene(event.pos())
|
|
|
|
+ if event.button() == Qt.MouseButton.LeftButton:
|
|
|
|
+ self.leftMouseButtonPressed.emit(scenePos.x(), scenePos.y())
|
|
|
|
+ elif event.button() == Qt.MouseButton.MiddleButton:
|
|
|
|
+ self.middleMouseButtonPressed.emit(scenePos.x(), scenePos.y())
|
|
|
|
+ elif event.button() == Qt.MouseButton.RightButton:
|
|
|
|
+ self.rightMouseButtonPressed.emit(scenePos.x(), scenePos.y())
|
|
|
|
+
|
|
|
|
+ QGraphicsView.mousePressEvent(self, event)
|
|
|
|
+
|
|
|
|
+ def mouseReleaseEvent(self, event):
|
|
|
|
+ """ Stop mouse pan or zoom mode (apply zoom if valid).
|
|
|
|
+ """
|
|
|
|
+ # Ignore dummy events. e.g., Faking pan with left button ScrollHandDrag.
|
|
|
|
+ dummyModifiers = Qt.KeyboardModifier(Qt.KeyboardModifier.ShiftModifier | Qt.KeyboardModifier.ControlModifier
|
|
|
|
+ | Qt.KeyboardModifier.AltModifier | Qt.KeyboardModifier.MetaModifier)
|
|
|
|
+ if event.modifiers() == dummyModifiers:
|
|
|
|
+ QGraphicsView.mouseReleaseEvent(self, event)
|
|
|
|
+ event.accept()
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ # Finish dragging a region zoom box?
|
|
|
|
+ if (self.regionZoomButton is not None) and (event.button() == self.regionZoomButton):
|
|
|
|
+ QGraphicsView.mouseReleaseEvent(self, event)
|
|
|
|
+ zoomRect = self.scene.selectionArea().boundingRect().intersected(self.sceneRect())
|
|
|
|
+ # Clear current selection area (i.e. rubberband rect).
|
|
|
|
+ self.scene.setSelectionArea(QPainterPath())
|
|
|
|
+ self.setDragMode(QGraphicsView.DragMode.NoDrag)
|
|
|
|
+ # If zoom box is 3x3 screen pixels or smaller, do not zoom and proceed to process as a click release.
|
|
|
|
+ zoomPixelWidth = abs(event.pos().x() - self._pixelPosition.x())
|
|
|
|
+ zoomPixelHeight = abs(event.pos().y() - self._pixelPosition.y())
|
|
|
|
+ if zoomPixelWidth > 3 and zoomPixelHeight > 3:
|
|
|
|
+ if zoomRect.isValid() and (zoomRect != self.sceneRect()):
|
|
|
|
+ self.zoomStack.append(zoomRect)
|
|
|
|
+ self.crop_image()
|
|
|
|
+ self.updateViewer()
|
|
|
|
+ self.viewChanged.emit()
|
|
|
|
+ event.accept()
|
|
|
|
+ self._isZooming = False
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ # Finish panning?
|
|
|
|
+ if (self.panButton is not None) and (event.button() == self.panButton):
|
|
|
|
+ if self.panButton == Qt.MouseButton.LeftButton:
|
|
|
|
+ QGraphicsView.mouseReleaseEvent(self, event)
|
|
|
|
+ else:
|
|
|
|
+ # ScrollHandDrag ONLY works with LeftButton, so fake it.
|
|
|
|
+ # Use a bunch of dummy modifiers to notify that event should NOT be handled as usual.
|
|
|
|
+ self.viewport().setCursor(Qt.CursorShape.ArrowCursor)
|
|
|
|
+ dummyModifiers = Qt.KeyboardModifier(Qt.KeyboardModifier.ShiftModifier
|
|
|
|
+ | Qt.KeyboardModifier.ControlModifier
|
|
|
|
+ | Qt.KeyboardModifier.AltModifier
|
|
|
|
+ | Qt.KeyboardModifier.MetaModifier)
|
|
|
|
+ dummyEvent = QMouseEvent(QEvent.Type.MouseButtonRelease, QPointF(event.pos()),
|
|
|
|
+ Qt.MouseButton.LeftButton, event.buttons(), dummyModifiers)
|
|
|
|
+ self.mouseReleaseEvent(dummyEvent)
|
|
|
|
+ self.setDragMode(QGraphicsView.DragMode.NoDrag)
|
|
|
|
+ if len(self.zoomStack) > 0:
|
|
|
|
+ sceneViewport = self.mapToScene(
|
|
|
|
+ self.viewport().rect()).boundingRect().intersected(self.sceneRect())
|
|
|
|
+ delta = sceneViewport.topLeft() - self._scenePosition
|
|
|
|
+ self.zoomStack[-1].translate(delta)
|
|
|
|
+ self.zoomStack[-1] = self.zoomStack[-1].intersected(
|
|
|
|
+ self.sceneRect())
|
|
|
|
+ self.viewChanged.emit()
|
|
|
|
+ event.accept()
|
|
|
|
+ self._isPanning = False
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ scenePos = self.mapToScene(event.pos())
|
|
|
|
+ if event.button() == Qt.MouseButton.LeftButton:
|
|
|
|
+ self.leftMouseButtonReleased.emit(scenePos.x(), scenePos.y())
|
|
|
|
+ elif event.button() == Qt.MouseButton.MiddleButton:
|
|
|
|
+ self.middleMouseButtonReleased.emit(scenePos.x(), scenePos.y())
|
|
|
|
+ elif event.button() == Qt.MouseButton.RightButton:
|
|
|
|
+ self.rightMouseButtonReleased.emit(scenePos.x(), scenePos.y())
|
|
|
|
+
|
|
|
|
+ QGraphicsView.mouseReleaseEvent(self, event)
|
|
|
|
+
|
|
|
|
+ def mouseDoubleClickEvent(self, event):
|
|
|
|
+ """ Show entire image.
|
|
|
|
+ """
|
|
|
|
+ # Zoom out on double click?
|
|
|
|
+ if (self.zoomOutButton is not None) and (event.button() == self.zoomOutButton):
|
|
|
|
+ self.clearZoom()
|
|
|
|
+ event.accept()
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ scenePos = self.mapToScene(event.pos())
|
|
|
|
+ if event.button() == Qt.MouseButton.LeftButton:
|
|
|
|
+ self.leftMouseButtonDoubleClicked.emit(scenePos.x(), scenePos.y())
|
|
|
|
+ elif event.button() == Qt.MouseButton.RightButton:
|
|
|
|
+ self.rightMouseButtonDoubleClicked.emit(scenePos.x(), scenePos.y())
|
|
|
|
+
|
|
|
|
+ QGraphicsView.mouseDoubleClickEvent(self, event)
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ def mouseMoveEvent(self, event):
|
|
|
|
+ # Emit updated view during panning.
|
|
|
|
+ if self._isPanning:
|
|
|
|
+ QGraphicsView.mouseMoveEvent(self, event)
|
|
|
|
+ if len(self.zoomStack) > 0:
|
|
|
|
+ sceneViewport = self.mapToScene(
|
|
|
|
+ self.viewport().rect()).boundingRect().intersected(self.sceneRect())
|
|
|
|
+ delta = sceneViewport.topLeft() - self._scenePosition
|
|
|
|
+ self._scenePosition = sceneViewport.topLeft()
|
|
|
|
+ self.zoomStack[-1].translate(delta)
|
|
|
|
+ self.zoomStack[-1] = self.zoomStack[-1].intersected(
|
|
|
|
+ self.sceneRect())
|
|
|
|
+ self.updateViewer()
|
|
|
|
+ self.viewChanged.emit()
|
|
|
|
+
|
|
|
|
+ scenePos = self.mapToScene(event.pos())
|
|
|
|
+ if self.sceneRect().contains(scenePos):
|
|
|
|
+ # Pixel index offset from pixel center.
|
|
|
|
+ x = int(round(scenePos.x() - 0.5))
|
|
|
|
+ y = int(round(scenePos.y() - 0.5))
|
|
|
|
+ imagePos = QPoint(x, y)
|
|
|
|
+ else:
|
|
|
|
+ # Invalid pixel position.
|
|
|
|
+ imagePos = QPoint(-1, -1)
|
|
|
|
+ self.mousePositionOnImageChanged.emit(imagePos)
|
|
|
|
+
|
|
|
|
+ QGraphicsView.mouseMoveEvent(self, event)
|
|
|
|
+
|
|
|
|
+ def enterEvent(self, event):
|
|
|
|
+ pass
|
|
|
|
+ # self.setCursor(Qt.CursorShape.CrossCursor)
|
|
|
|
+
|
|
|
|
+ def leaveEvent(self, event):
|
|
|
|
+ pass
|
|
|
|
+ # self.setCursor(Qt.CursorShape.ArrowCursor)
|
|
|
|
+
|
|
|
|
+ def crop_image(self) -> QPixmap:
|
|
|
|
+ if self.hasImage():
|
|
|
|
+ rect: QRect = self.zoomStack[-1].toRect()
|
|
|
|
+ image = self.imageStack[-1].copy(rect)
|
|
|
|
+ self._image.setPixmap(image)
|
|
|
|
+ self.imageStack.append(image)
|
|
|
|
+
|
|
|
|
+ def save_image(self, path_file: str) -> None:
|
|
|
|
+ self._image.save(path_file)
|