2

I would like to enable the user to zoom in on an image in QGraphicView's Scene, allowing him to zoom in on the mouse position.

I have tried to implent this zoom in several ways, yet unfortunately I haven't been able to get it work as needed.

If implementing this via QGraphicsPixmapItem's setScale and setTransformOriginpoint, the solution works fine and dandy as long as the user does not move the position of the mouse on the zoomed in image, if he does so - the displayed scaled image is incorrect. Please find a minimum reproducible example below:

import sys
from PyQt5 import QtWidgets
from PyQt5.QtWidgets import QApplication, QLabel, QMainWindow, QGraphicsView,QGraphicsPixmapItem,QGraphicsScene,QPushButton, QWidget, QVBoxLayout, QShortcut
from PyQt5.QtCore import Qt
from PyQt5 import QtCore, QtGui
from PyQt5.QtGui import QKeySequence,QShortcutEvent, QCursor
global qpixmap
global lvlofzoom
lvlofzoom =1
class MainWindow(QMainWindow):

    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)
        self.setFixedSize(1200, 800)

        self.ImageDisplay = QGraphicsView()


        self.qwidget = QWidget()

        self.Zoom_inBtn = QPushButton("Zoom in (CTRL + F)")
        self.Zoom_outBtn = QPushButton("Zoom out (CTRL + G)")
        self.setimgBtn = QPushButton("Load Image (CTRL+P)")


        self.Zoom_inBtn.clicked.connect(lambda: self.zoomin())
        self.Zoom_outBtn.clicked.connect(lambda: self.zoomout())
        self.setimgBtn.clicked.connect(lambda:self.loadimg())
        self.Zoom_inBtn.setShortcut(QKeySequence(Qt.CTRL + Qt.Key_F))
        self.Zoom_outBtn.setShortcut(QKeySequence(Qt.CTRL + Qt.Key_G))
        self.setimgBtn.setShortcut(QKeySequence(Qt.CTRL + Qt.Key_P))

        self.vlayout = QVBoxLayout()
        self.vlayout.addWidget(self.ImageDisplay)
        self.vlayout.addWidget(self.setimgBtn)
        self.vlayout.addWidget(self.Zoom_outBtn)
        self.vlayout.addWidget(self.Zoom_inBtn)
        self.qwidget.setLayout(self.vlayout)


        self.setCentralWidget(self.qwidget)


    def zoomin(self):
        print("zoom in")
        global lvlofzoom
        lvlofzoom = lvlofzoom *1.25
        mousepos = self.ImageDisplay.mapFromGlobal(QCursor.pos())
        mousepos = self.ImageDisplay.mapToScene(mousepos)
        #mousepos = self.ImageDisplay.mapFromScene(QCursor.pos())

        global qpixmap
        pixmap_to_show = QGraphicsPixmapItem(qpixmap)
        pixmap_to_show.setTransformationMode(Qt.SmoothTransformation)
        pixmap_to_show.setTransformOriginPoint(mousepos)
        pixmap_to_show.setScale(lvlofzoom)
        pixmap_scene = QGraphicsScene(self.ImageDisplay)
        pixmap_scene.addItem(pixmap_to_show)
        #   pixmap_scene.
        self.ImageDisplay.setScene(pixmap_scene)

    def zoomout(self):
        print("zoom out")
        global lvlofzoom
        lvlofzoom = lvlofzoom * 0.8
        mousepos = self.ImageDisplay.mapFromGlobal(QCursor.pos())
        mousepos = self.ImageDisplay.mapToScene(mousepos)

        #mousepos = self.ImageDisplay.mapFromScene(QCursor.pos())
        global qpixmap
        pixmap_to_show = QGraphicsPixmapItem(qpixmap)
        pixmap_to_show.setTransformationMode(Qt.SmoothTransformation)
        pixmap_to_show.setTransformOriginPoint(mousepos)
        pixmap_to_show.setScale(lvlofzoom)
        pixmap_scene = QGraphicsScene(self.ImageDisplay)
        pixmap_scene.addItem(pixmap_to_show)
        #   pixmap_scene.
        self.ImageDisplay.setScene(pixmap_scene)

    def loadimg(self):
        global qpixmap
        print("load img")
        fileName, _ = QtWidgets.QFileDialog.getOpenFileName(None, "Select Image", "",
                                                            "Image Files (*.png *.jpg *jpeg *.bmp)")

        if fileName:
            qpixmap = QtGui.QPixmap(fileName)
            qpixmap = qpixmap.scaledToWidth(self.ImageDisplay.width())
            qgraphicsitem = QGraphicsPixmapItem(qpixmap)
            qscene = QGraphicsScene(self.ImageDisplay)
            qscene.addItem(qgraphicsitem)
            self.ImageDisplay.setScene(qscene)
            self.ImageDisplay.setMouseTracking(True)
            #   self.ui.label_Pano.scene().
            self.ImageDisplay.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
            self.ImageDisplay.setResizeAnchor(QGraphicsView.AnchorUnderMouse)
            self.ImageDisplay.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
            self.ImageDisplay.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)


app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()

I understand that this is due to the fact that I am taking mouse position from global, and scaling the full image with the mouse's global position as the transformation originpoint. However, even after mapping it to scene, it does not work as needed.

Ideally, I would like to solve the issue in a different manner, implementing the zoom function via scaling the pixmap itself. This is the preferred option, as it requires me to rewrite the least amount code (part of a larger "pet project", and other functions are interacting with zoom - ex: drawing on the image, whilst zoomed).  Please find the reimplantation of zoomin & loadimg below (the rest stays the same, except zoomout which is partially irrelevant):

 def zoomin(self):
    print("zoom in")
    global lvlofzoom
    lvlofzoom = lvlofzoom *1.25
    global qpixmap
    mousepos = self.ImageDisplay.mapFromGlobal(QCursor.pos())

    pixmap = qpixmap.scaledToWidth(int(self.ImageDisplay.width() * lvlofzoom))
    pixmap_to_show = QGraphicsPixmapItem(pixmap)
    pixmap_scene = QGraphicsScene(self.ImageDisplay)
    pixmap_scene.addItem(pixmap_to_show)
    self.ImageDisplay.setScene(pixmap_scene)


    
def loadimg(self):
    global qpixmap
    print("load img")
    fileName, _ = QtWidgets.QFileDialog.getOpenFileName(None, "Select Image", "",
                                                        "Image Files (*.png *.jpg *jpeg *.bmp)")

    if fileName:
        qpixmap = QtGui.QPixmap(fileName)
        qpixmap2 = qpixmap.scaledToWidth(self.ImageDisplay.width())
        qgraphicsitem = QGraphicsPixmapItem(qpixmap2)
        qscene = QGraphicsScene(self.ImageDisplay)
        qscene.addItem(qgraphicsitem)
        self.ImageDisplay.setScene(qscene)
        self.ImageDisplay.setMouseTracking(True)
        self.ImageDisplay.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
        self.ImageDisplay.setResizeAnchor(QGraphicsView.AnchorUnderMouse)
        self.ImageDisplay.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)           
        self.ImageDisplay.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)

I would ideally like to add a mouse anchor to the scaledtoWidth and multiply the self.ImageDisplay.width with the lvlofzoom OR scale ImageDisplay, with Transformation mode of Qt.SmoothTransformation.

  • I have 2 doubts: 1) I understand that when the pixel is zoomed under the mouse it remains fixed, so my doubt is that since the zoom action is launched by pressing the button or pressing the shorcut keys, I understand that when it is use the shorcut the mouse will be on the image and that is the fixed point but what is the fixed point when the user clicks on the button? 2) What do you mean by *the solution works fine and dandy as long as the user does not move the position of the mouse on the zoomed in image, if he does so - the displayed scaled image is incorrect*? – eyllanesc Jul 30 '21 at 23:18
  • 1) If the function is run via button click, the transformation originpoint should be the middle of the screen (in reality, this is not really a problem - very unlikely that the user will click zoom via button).2) i) Open the image in the link: https://cf.geekdo-images.com/0-DjGBOko6RV8zg5ejhYIg__opengraph_letterbox/img/bgqjGyaORNVAXZSIlVdEHHbZviQ=/fit-in/1200x630/filters:fill(auto):strip_icc()/pic260745.jpg ii) Zoom in on A1 5x, now try to zoom in on c2 and the the imageDisplay displays g4 – usario121233 Jul 30 '21 at 23:38
  • Hoped that cleared it up, if not let me know & I'll post some illustrative images in the original question post regarding question 2. Thanks for the help eyllanesc! – usario121233 Jul 30 '21 at 23:57
  • I have one more question: I think you should also know that you can also zoom through QGraphicsView (using the scale() method), have you used that method? If you have used it then have there been any problems with that method? – eyllanesc Jul 31 '21 at 00:01
  • 1
    I did try, yet zooming in via QGraphicsView.scale() I lose quality, as scale() function lacks the "transformMode=Qt.SmoothTransformation" parameter. Even if using setRenderHints( QPainter.Antialiasing | QPainter.SmoothPixmapTransform | QPainter.HighQualityAntialiasing), the quality of the image is considerably worse than transformMode=Qt.SmoothTransformation – usario121233 Jul 31 '21 at 00:19
  • Okay, I wanted that information because I think you had already published a similar post where you pointed that out and then I wanted to confirm those problems – eyllanesc Jul 31 '21 at 00:21
  • @eyllanesc I believe you're referring to [this](https://stackoverflow.com/questions/68596121/zooming-on-mouse-position-qgraphicsview-scene-without-compromising-quality) deleted post from the same author. – musicamante Jul 31 '21 at 01:17

1 Answers1

1

Solved by the use of setSceneRect(), as guided by another answer by eyllanesc (PyQt5: I can't understand QGraphicsScene's setSceneRect(x, y, w, h))

def zoomin(self):
    print("zoom in")
    global lvlofzoom
    lvlofzoom = lvlofzoom *1.25
    global qpixmap
    mousepos = self.ImageDisplay.mapFromGlobal(QCursor.pos())

    pixmap = qpixmap.scaledToWidth(int(self.ImageDisplay.width() * lvlofzoom))
    pixmap_to_show = QGraphicsPixmapItem(pixmap)
    pixmap_scene = QGraphicsScene(self.ImageDisplay)
    pixmap_scene.addItem(pixmap_to_show)
    p = self.ImageDisplay.mapToScene(QCursor.pos())#p0)
    r = self.ImageDisplay.sceneRect() 
                        r.moveCenter(p)
    self.ImageDisplay.setScene(pixmap_scene)
    self.ImageDisplay.setSceneRect(r)
  • As I'm seeing quite some views on this q,uestion I'd like to emphasise that this solution is not optimal, as although it zooms in, it does not allow for moving around the pixmap (as I am literally scaling the pixmap).. As I would ideally like to be able to move around the pixmap, the question is still open for answers and I'd be more than happy to accept an answer that resolves my issue. – usario121233 Jun 21 '22 at 14:21