0

I am creating an app using Python 3.10 and PyQT6. It is going to be a photo editor. The idea is that user presses the button #1 to choose an image file, then the image is shown in window (in a pixmap), and when a user presses the button #2, program draws something on image with OpenCV and replaces the image with a new one that is shown instead of the previous one.

This happened as expected (one image was replaced by another) until I put the drawing and replacing images function (actually the button #2 on-click connected function) in a thread. After that the previous image disappears and the new one doesn't appear.

The situation gets more complex due to the fact I am using a custom Pixmap Container as was adviced in a StackOverflow answer

I have tried searching the Net but failed to find a solution. I have tried experimenting with QtCore.QTimer.singleShot() to force the layout to get some update but I got confused were to place this function. I had a thought that deleteLater() deletes the new image but that was not the case. I have done debug and found that the problem appears in the line self.centralWidget().layout().replaceWidget(self.image, image) (see the code below). I have tried to find another way to replace a pixmap image but all I found was greatly close to my code. Now I am going to give you a minimal, reproducible example of code I had written.

Custom PixmapContainer class

class PixmapContainer(QLabel):
    def __init__(self, pixmap, parent=None):
        super(PixmapContainer, self).__init__(parent)
        self._pixmap = QPixmap(pixmap)
        self.setMinimumSize(256, 256)  # needed to be able to scale down the image

    def resizeEvent(self, event):
        w = min(self.width(), self._pixmap.width())
        h = min(self.height(), self._pixmap.height())
        self.setPixmap(self._pixmap.scaled(w, h, Qt.AspectRatioMode.KeepAspectRatio))

Main Window class (that is a lot of code so I made comments to emphasise the problem)

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        # initializing image
        self.imgpath = None
        self.image = QLabel()

        # button 1 - Choose photo
        button_img = QPushButton("Choose photo")
        button_img.clicked.connect(self.openfile)
        # button 2 - Start-some-work
        button_recgn = QPushButton("Start-some-work")
        button_recgn.clicked.connect(self.start_recogn_thread)    # THIS DOES NOT WORK
        # button_recgn.clicked.connect(self.recognize)            # THIS WORKS

        # adding image and buttons to layout
        layout = QVBoxLayout()
        layout.addWidget(self.image, stretch=1)
        layout.addWidget(button_img)
        layout.addWidget(button_recgn)


        wid = QWidget()
        wid.setLayout(layout)

        self.setCentralWidget(wid)


    # editing and replacing images itself
    def recognize(self):
        # set the widgets to disabled while editing (doesn't do anything without thread)
        self.centralWidget().setDisabled(True)

        img_data = cv2.imdecode(np.fromfile(self.imgpath, dtype=np.uint8), cv2.IMREAD_UNCHANGED) 
        img_data = cv2.cvtColor(img_data, cv2.COLOR_BGR2RGB) 
        cv2.rectangle(img_data, (10, 10), (100, 100), (255, 0, 0), 2) # some drawing

        h, w, c = img_data.shape

        image = PixmapContainer(QImage(img_data.data, w, h, w * 3, QImage.Format.Format_RGB888))
        

        # ------------------------- THE PROBLEM HERE!!! --------------------------------
        self.centralWidget().layout().replaceWidget(self.image, image)
        # -------------------------------^^^^^^^^---------------------------------------
        # ----------- Everything except this line works properly in thread -------------

        # deleting previous image
        self.image.deleteLater()
        self.image = image

        self.centralWidget().setDisabled(False)


    # editing and replacing images in thread
    def start_recogn_thread(self):
        th = threading.Thread(target=self.recognize)               # THREAD
        th.start()

    # open image from file
    def openfile(self):
        fname = QFileDialog.getOpenFileName(
            self,
            'Choose photo',
            '.',
            "Supported files (*.png;*.jpg;*.bmp);;PNG Files (*.png);;JPG Files (*.jpg);;BMP File (*.bmp)"
        )[0]
        # saving image path
        self.imgpath = fname

        # showing image
        image = PixmapContainer(fname)
        image.setAlignment(Qt.AlignmentFlag.AlignCenter)

        self.centralWidget().layout().replaceWidget(self.image, image)
        self.image.deleteLater()                                          # WORKS!
        self.image = image

Application

app = QApplication([])
window = MainWindow()
window.show()
app.exec()

Imports

from PyQt6.QtWidgets import QApplication, QWidget, QMainWindow, QPushButton,\
    QFileDialog, QVBoxLayout, QLabel
from PyQt6.QtGui import QPixmap, QImage
from PyQt6.QtCore import Qt
import threading
import cv2
import numpy as np 

I am very new to PyQT and I am very sorry to take your time on figuring out this problem.

teyo
  • 1
  • 1
  • Widgets are not thread safe, and should *never* be accessed from external threads. If you want to properly interact with the UI from a thread, implement a QThread (or a QObject moved to a QThread) and use custom signals. – musicamante Jun 14 '23 at 17:07

0 Answers0