6

I am having a strange issue with PyQt5 in Python 3.5. I have two classes, FrontEnd(QWidget) and TimerThread(Thread). I have a number of QLabels defined in the init function of FrontEnd, all of which work properly.

Shown are the relevant few functions of FrontEnd:

def update_ui(self):
    ret, frame = self.cam_capture.read()

    if self.results_pending:
        if not path.isfile('output.jpg'):
            self.results_pending = False
            with open('.out') as content_file:
                content = content_file.readlines()[2:-2]
            system('rm .out')
            self.handle_image_classification(content)

    if self.take_picture:
        cv2.imwrite('output.jpg', frame)
        self.user_prompt.setText('Please wait...')
        system('./classifyimage.py --mean mean.binaryproto --nogpu --labels labels.txt model.caffemodel deploy.prototxt output.jpg > .out && rm output.jpg')
        self.take_picture = False
        self.results_pending = True

    image = QImage(frame, frame.shape[1], frame.shape[0], QImage.Format_RGB888).rgbSwapped()
    pix = QPixmap.fromImage(image)
    self.video_frame.setPixmap(pix)

def update_bar_graph(self, data):
    palette = QPalette()
    palette.setColor(QPalette.Background, Qt.white)
    for i in range(0, 8):
        self.bar_graph_labels[i].setText(str(data[i]) + "%")
        height = int(data[i] * 5)
        self.bar_graph[i].setFixedSize(self.bar_width, height)
        self.bar_graph[i].move(1280 + (i * (self.bar_width + self.bar_spacing)), 640 - height)

def handle_image_classification(self, raw_output):
    data = [None] * 8
    for i in range(0, len(raw_output)):
        raw_output[i] = raw_output[i].strip()
        data[int(raw_output[i][-2]) - 1] = float(raw_output[i][:-10])
    self.update_bar_graph(data)

And the entire TimerThread class:

class TimerThread(Thread):
    front_end = None

    def __init__(self, event):
        Thread.__init__(self)
        self.stopped = event

    def run(self):
        while not self.stopped.wait(0.02):    
            FrontEnd.update_ui(self.front_end)

(The front_end element of TimerThread is set on init of FrontEnd)

The problem is in the update_bar_graph function. When the setFixedSize call is commented out, the program runs fine, although without properly displaying the bars of the bar graph in my application (which are QLabels). The move function seems to run properly. However, the setFixedSize call causes this error:

QObject::startTimer: Timers cannot be started from another thread
QObject::startTimer: Timers cannot be started from another thread
QObject::killTimer: Timers cannot be stopped from another thread
QObject::startTimer: Timers cannot be started from another thread

I have absolutely no idea why this occurs, and why the move function, seemingly similar in nature, works fine. Any help would be much appreciated. (If I should be using a different sort of timer class or different methods for drawing large rectangles in PyQt, I am open to any such suggestions).

EDIT:

This is some weird stuff. I ran it twice the next day, with no changes to the code. (I think...) One time the bar graphs did not display but no errors were thrown. The other time I got this:

7fdfaf931000-7fdfaf932000 r--p 0007a000 08:07 655633                     /usr/lib/x86_64-linux-gnu/libQt5DBus.so.5.5.1
7fdfaf932000-7fdfaf933000 rw-p 0007b000 08:07 655633                     /usr/lib/x86_64-linux-gnu/libQt5DBus.so.5.5.1
7fdfaf933000-7fdfaf934000 rw-p 00000000 00:00 0 
7fdfaf934000-7fdfaf971000 r-xp 00000000 08:07 667112                     /usr/lib/x86_64-linux-gnu/libxkbcommon.so.0.0.0
7fdfaf971000-7fdfafb70000 ---p 0003d000 08:07 667112                     /usr/lib/x86_64-linux-gnu/libxkbcommon.so.0.0.0
7fdfafb70000-7fdfafb72000 r--p 0003c000 08:07 667112                     /usr/lib/x86_64-linux-gnu/libxkbcommon.so.0.0.0
7fdfafb72000-7fdfafb73000 rw-p 0003e000 08:07 667112                     /usr/lib/x86_64-linux-gnu/libxkbcommon.so.0.0.0
7fdfafb73000-7fdfafb7a000 r-xp 00000000 08:07 667110                     /usr/lib/x86_64-linux-gnu/libxkbcommon-x11.so.0.0.0
7fdfafb7a000-7fdfafd79000 ---p 00007000 08:07 667110                     /usr/lib/x86_64-linux-gnu/libxkbcommon-x11.so.0.0.0

I think I may have found a bug in PyQt5.

brendon-ai
  • 517
  • 3
  • 7
  • 20
  • Btw, a friendly advice, if you're planning to run your PyQt program on Windows, run away from `QThread`s as far as you can. It causes random crashes and will drive you crazy. – The Quantum Physicist Feb 21 '17 at 13:24
  • 1
    You're not supposed to call methods that update QObjects [from a different thread](http://doc.qt.io/qt-5/thread-basics.html#simultaneous-access-to-data). You should create a signal, connect that to a slot and then you can safely emit that signal from a different thread, updates will be handled in the event loop the object ['lives'](http://doc.qt.io/qt-5/thread-basics.html#qobject-and-threads) in. – mata Feb 21 '17 at 13:25
  • 1
    @TheQuantumPhysicist - No, if that happens the chance is that you have not really understood how to use threading in Qt and that you're doing something wrong. Blaming Qt for programming errors won't help anybody. – mata Feb 21 '17 at 13:27
  • @mata I've been using Qt since 6 years. I'm not a newbie. QThread on C++ is fine. But QThread on Python is full of bugs. You're welcome to try yourself and suffer the consequences. Not only that I suffered from this, but I also asked many different people who got random crashes, and there's always a QThread behind it. I even did a WinGDB debugging of it and showed that QThread is the source of evil. You can try to be idealistic with me and claim that it's more likely that I'm wrong, but you'll only know the truth after you waste some good time with it like I did. Hail multiprocessing! – The Quantum Physicist Feb 21 '17 at 13:31
  • @TheQuantumPhysicist Do you have any specific examples/evidence to support your claim? I'd be very interested to see minimal working examples that crash. I've seen Pyside do bad things but PyQt has never given me trouble if the code is actually thread safe. If you don't have evidence you can present relevent to the particular question posted, then I really don't think the comments of a random question is the place to make such accusations. – three_pineapples Feb 21 '17 at 20:46
  • @three_pineapples The crashes are random. That's the biggest problem in this issue. No one is taking it seriously, and all the people I know who use PySide and PyQt on Windows with QThread have faced it. I posted here when I first thought it's because PySide is not stable: https://bugreports.qt.io/browse/PYSIDE-327 but then discovered that no matter what you do, the error is still there. You guys want solid evidence? Tell me how to provide evidence for a random crash. Now I use multiprocessing, and never look back. – The Quantum Physicist Feb 21 '17 at 21:02
  • @three_pineapples Btw, I make this accusation everywhere when QThread + Python is mentioned. No one suffered like I did from QThread on Python. So I'm hoping I'd be helping someone before they even start. – The Quantum Physicist Feb 21 '17 at 21:13
  • @TheQuantumPhysicist It doesn't matter if the crash is random. Make a self contained example (cut down as much as you can while still reproducing the random crash - aka a [mcve]) and state approximately how long you have to run it to see a crash along with the version of PyQt and Python you are running. Post this as a question on stack overflow and you'll get some attention as long as it is using PyQt and not PySide. PySide is notorious for bugs, and there are very few developers fixing them. I'm agreeing with you sentiments with PySide, but PyQt is a different beast and should be stable. – three_pineapples Feb 21 '17 at 21:24
  • @Ethcad could you show where `TimerThread.stopped` comes from? With that bit of information I'll be able to advise how to stop the crashing while maintaining the current behaviour of your code. Please post a comment once you update your post so I remember to come back and look at it! – three_pineapples Feb 21 '17 at 21:27
  • @TheQuantumPhysicist Okay, here is my code: [https://hastebin.com/fagocapuba.py]. I am not using Windows, I am on Ubuntu 16.04. I am not experiencing a crash; it is simply that the QLabels are not displaying. Also, I am open to the suggestion of using slots and making the changes from within the thread that the FrontEnd class is in, but I do not know how. Also, TimerThread.stopped is not being used outside of what I have already posted. – brendon-ai Feb 21 '17 at 21:38
  • @three_pineapples view comment above – brendon-ai Feb 21 '17 at 21:38
  • @mata view comment above (sorry, SO won't let me mention multiple people in one comment) – brendon-ai Feb 21 '17 at 21:40
  • @Ethcad On Linux you're fine with QThread. – The Quantum Physicist Feb 22 '17 at 07:28
  • @three_pineapples The challenge in this whole story is that there's no way for me to come out as the good guy. I know how you feel, because often people came to me with stupid mistakes claiming the compiler is buggy. You're a programmer and you definitely understand how hard it's to detect random errors. The average time for the problem is about 6 hours. Do you realize what that means? And let me point out that my program was super-repetitive. It's an acquisition program that reads data from a device and writes the data to a file and plots it on screen. That's it! [...] – The Quantum Physicist Feb 22 '17 at 07:33
  • @three_pineapples Hence there's not much room for random errors in principle that I could've done (ignoring the other people who had the same issue). One of these days I'll try to strip that program, but honestly I'm not that interested nor have I the enough time to do it (especially for a commercial project, Qt... I would've felt better about an open-source community project). Currently, my rule of thumb on Python + Qt is: Don't ever use QThread unless you must. – The Quantum Physicist Feb 22 '17 at 07:36
  • @TheQuantumPhysicist Unless you have some code/evidence/something to show (no matter how complex), I would encourage you not to go around using anecdotal evidence to attempt to convince people not to use a fundamental piece of the Qt toolkit (it's science 101). If you ever want to shoot me a link to your code I'd be very interested to take a look, not because I necessarily think you've done something wrong, but if there really is something wrong with `QThread`s on windows, I want to know about it (as I'm sure a lot of people on stack overflow would). – three_pineapples Feb 22 '17 at 10:53
  • @three_pineapples OK. I don't mind. I'll give you access to the repository with that code (but not public access, that would be a problem). Is there a way we can communicate privately so that I could give you access credentials? – The Quantum Physicist Feb 22 '17 at 11:02
  • @three_pineapples I haven't heard from you. Are you really interested in looking into the code? – The Quantum Physicist Feb 23 '17 at 08:11
  • @three_pineapples Sent you an e-mail. Let's continue there. Ciao! – The Quantum Physicist Feb 23 '17 at 12:10

1 Answers1

15

As mentioned by @mata your code is not thread safe. This is likely where the errant behaviour is coming from, and should certainly be fixed before debugging further (this is related).

The reason it is thread unsafe is because you are interacting with GUI objects from the secondary thread directly. You should instead emit a signal from your thread to a slot in the main thread where you can safely update your GUI. This however requires you use a QThread which is recommended anyway as per this post.

This requires the following changes:

class TimerThread(QThread):
    update = pyqtSignal()

    def __init__(self, event):
        QThread.__init__(self)
        self.stopped = event

    def run(self):
        while not self.stopped.wait(0.02):    
            self.update.emit()

class FrontEnd(QWidget):
    def __init__(self):
        super().__init__()

        ... # code as in your original

        stop_flag = Event()    
        self.timer_thread = TimerThread(stop_flag)
        self.timer_thread.update.connect(self.update_ui)
        self.timer_thread.start()

I've also modified your code so that it stores a reference to the TimerThread in the FrontEnd object so the thread is not garbage collected.

I would also add that this is an overly complex way of triggering an update every 0.02 seconds. You could use a QTimer to just call the update_ui method and ditch threads entirely but I'm taking the approach that you will likely want to do something more complex with your threads later, so have demonstrated how to do it safely!

Community
  • 1
  • 1
three_pineapples
  • 11,579
  • 5
  • 38
  • 75