0

I'd like to use the wheelEvent to resize an image and place a QGraphicPixmap into a QGraphicsScene.

Before adding the original image, it is resized to around 1/3rd its original size. In the wheelEvent, I'm calling a function that will resize the original image and create a QImage to set the QGraphicsPixmap.

After adding the resized pixmap to the scene, the pixels that were originally under the cursor before the scale have shifted. I'm not sure which positions I need to be mapping to/from the scene to achieve this.

I've tried scaling the graphicsPixmap, scaling and translating the graphicsPixmap, scaling the view and translating the graphicsPixmap/setting an offset.

I clearly don't something about what's happening but I'm not sure what that is..

The WheelEvent below works perfectly until maybe_resize is called.

Depending on the size of the current image in the viewer the maybe_resize method will either resize the current ndarray image, create a new qimage and set a new pixmap in the graphicPixmap, or it exits the method without resizing.

If you run the code as is, the pixmap is in the same place under the cursor, but if you uncomment maybe_resize this is no longer the case.

from PyQt5.QtCore import QRectF, QSize, Qt, pyqtSignal
import cv2
import numpy as np
from PyQt5.QtCore import QRectF, QSize, Qt, pyqtSignal
from PyQt5.QtGui import QImage, QPixmap
from PyQt5.QtWidgets import (QApplication,
                             QFrame,
                             QGraphicsPixmapItem,
                             QGraphicsScene,
                             QGraphicsView,
                             QMainWindow,
                             QSizePolicy)


class GraphicsView(QGraphicsView):
    def __init__(self, parent):
        super(GraphicsView, self).__init__(parent)
        self.pixmap = QPixmap()
        self._zoom_level = 0
        self._scene = Scene(self)
        self.setScene(self._scene)
        self.gpm = QGraphicsPixmapItem()
        self._scene.addItem(self.gpm)
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setFrameShape(QFrame.NoFrame)
        self.has_image = False

    def maybe_resize(self, factor):
        self.resize_requested(factor)

    def read_image(self, path):
        self.base_image = cv2.imread(path, -1)
        self._original_res = self.base_image.shape
        h, w = self.base_image.shape[0], self.base_image.shape[1]
        self.resized_image = cv2.resize(self.base_image, (w // 4, h // 4))
        self.has_image = True
        self.set_image(self.resized_image)
        return self.resized_image

    def resize_requested(self, factor):
        factor = max(1. * (self._zoom_level * factor), 1)
        h = int(self.resized_image.shape[0] * factor)
        w = int(self.resized_image.shape[1] * factor)
        src = cv2.resize(self.base_image, (w, h))
        dst = np.ndarray(src.shape, src.dtype)
        dst[:, :, :] = src
        self.set_image(dst)

    def wheelEvent(self, event):
        factor = 1.1
        if event.angleDelta().y() < 0:
            factor = 0.9
            self._zoom_level-=1
        else:
            self._zoom_level+=1
        view_pos = event.pos()
        scene_pos = self.mapToScene(view_pos)
        self.centerOn(scene_pos)
        self.scale(factor, factor)
        delta = self.mapToScene(view_pos) - self.mapToScene(self.viewport().rect().center())
        self.centerOn(scene_pos - delta)
        # self.maybe_resize(factor)

    def set_image(self, img):
        if not self.has_image:
            return
        shape = img.shape
        w = shape[1]
        h = shape[0]
        self._image = img
        q_img_format = QImage.Format_RGB888
        try:
            bands = shape[2]
        except IndexError:
            bands = 1
        q_img = QImage(img, w, h, w * bands, q_img_format)
        self.pixmap = self.pixmap.fromImage(q_img)
        self.setSceneRect(QRectF(self.pixmap.rect()))
        self.gpm.setPixmap(self.pixmap)


class Scene(QGraphicsScene):
    zoom_changed = pyqtSignal(float)

    def __init__(self, parent=None):
        super(Scene, self).__init__(parent)


class Window(QMainWindow):
    def __init__(self):
        super(Window, self).__init__()
        self.gv = GraphicsView(self)
        self.setCentralWidget(self.gv)

    def load_image(self, path):
        self.gv.read_image(path)

    def sizeHint(self):
        return QSize(800, 800)


if __name__ == "__main__":
    app = QApplication([])
    w = Window()
    w.load_image('test.jpg')
    w.show()
    app.exit(app.exec_())

Shilo
  • 313
  • 3
  • 16
  • please provide a [mre] – eyllanesc Jun 11 '20 at 02:58
  • @eyllanesc Added the example – Shilo Jun 11 '20 at 03:32
  • Why is it necessary to resize the image? Is not enough what scale() does? – eyllanesc Jun 11 '20 at 03:43
  • @eyllanesc I'm making a photo editor, so resizing the image to improve performance. If the MainWindow is only 800x800 and the image is 24 megapixels then there is no reason to process or show the full resolution image every time a slider is changed. Eventually I would like to be able to use the viewer as an ROI when the image completely fills the viewer so only the visible portion of the image is processed until the zoom is changed again. – Shilo Jun 11 '20 at 03:54
  • I explain in detail the reason for my question: Let's assume that the course is in the pixel (i, j) of the image, when the image is scaled that pixel can be removed or new pixels created, when the image is reduced then there are pixels removed On the contrary, if an image is enlarged then new pixels are created that are the division of the original pixel into parts. So, let's say that the image grew by a factor of 2 so by one pixel before the conversion there are 4 pixels, and let's say that the initial pixel was below the cursor. – eyllanesc Jun 11 '20 at 04:05
  • Which of the 4 generated pixels should be below the cursor? also if the factor is not integer then it becomes more complicated, and choosing that approximation an inevitable offset will be generated – eyllanesc Jun 11 '20 at 04:05
  • @eyllanesc I suppose one of the inner pixels should be under the cursor. It doesn't need to be pixel perfect, but currently i'm not even able to get a close approximation – Shilo Jun 11 '20 at 04:11
  • Ok, if that is enough for you then I will try to implement it but I already pointed out the noise that it would generate since if you scale the image, and then reduce it to the original size then probably the pixel that was and is under the cursor is not the same. – eyllanesc Jun 11 '20 at 04:14
  • @eyllanesc Thank you. The closest I was able to come was referencing this answer https://stackoverflow.com/questions/2916081/zoom-in-on-a-point-using-scale-and-translate/30410948#30410948. After implementing that, zooming worked for the most part, but sometimes when zooming with the cursor over certain portions of the image(bottom right quadrant IIRC) the image shifted – Shilo Jun 11 '20 at 04:34
  • @eyllanesc Did you have any luck getting this to work? – Shilo Jun 13 '20 at 02:48

0 Answers0