Search code examples
pythonvideopyqtqpainterqlabel

PyQt5 Paint Circle Over Video in QLabel


I want to draw a circle that displays over a video at the cursor location when pressing the mouse. The video is playing in a QLabel object that is in a MainWindow. I’m using OpenCV to read frames from the webcam at 10 fps. I’m converting the frames to QPixmap and displaying them in the QLabel object (self.vidWindow).

In the code below, the circle is painted immediately when the MainWindow is launched (not what I want) and is then covered up by the video stream. Text displays in the mask Qlabel object and a message is printed in the MainWindow when the mouse button is pressed.

Can I draw a circle in a QLabel object? If so, should I use the mask QLabel object or is there a way to overlay directly over the video in the self.vidWindow?

In the minimalized version of the code, the video displays, but an error is triggered when I try to draw the ellipse.

import sys, cv2
from PyQt5.QtWidgets import QMainWindow, QApplication, QLabel
from PyQt5.QtGui import QImage, QPixmap, QPainter, QPen, QFont
from PyQt5.QtCore import QTimer, Qt, QCoreApplication

class MainWindow(QMainWindow):

    def __init__(self):
        super().__init__()        
        self.initUI()        

    def initUI(self):                       
        self.statusBar().showMessage('Ready')        
        self.setGeometry(50, 50, 800, 600)
        self.setWindowTitle('Statusbar')
        self.vidWindow = QLabel(self)
        self.vidWindow.setGeometry(20, 20, 640, 480)
        self.maskWindow = QLabel(self)
        self.maskWindow.setGeometry(20, 20, 640, 480)
        self.maskWindow.setStyleSheet('background-color: rgba(0,0,0,0%)')
        font = QFont()
        font.setPointSize(18)
        font.setBold(True)
        font.setWeight(75)
        self.maskWindow.setFont(font)
        self.maskWindow.setText('Message is on the mask Qlabel object')
        self.msgLabel = QLabel(self)
        self.msgLabel.setGeometry(675, 300, 100, 20)
        self.cap = cv2.VideoCapture(0)
        self.pix = QImage()
        self.timer = QTimer()
        self.frame_rate = 5
        self.show()
        self.start()

    def nextFrameSlot(self):
        ret, frame = self.cap.read()
        if ret == True:
            frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
            img = QImage(frame,frame.shape[1], frame.shape[0], QImage.Format_RGB888)
            img = img.scaled(640, 480, Qt.KeepAspectRatio)
            self.pix = QPixmap.fromImage(img)
            self.vidWindow.setPixmap(self.pix)

    def mousePressEvent(self, QMouseEvent):        
        self.msgLabel.setText('Mouse Clicked!')

    def paintEvent(self, QMouseEvent):
        e = QMouseEvent
        painter = QPainter(self)
        painter.setPen(QPen(Qt.green,  4, Qt.SolidLine))
        painter.drawEllipse(e.x(), e.y(), 100)

    def start(self):
        rate = int(1000.0 / self.frame_rate)        
        self.timer.setTimerType(Qt.PreciseTimer)
        self.timer.timeout.connect(self.nextFrameSlot)
        self.timer.start(rate)

    def closeEvent(self, event):
        if self.cap.isOpened():
            self.cap.release()
            self.vidWindow.clear()        
        QCoreApplication.quit()

if __name__ == '__main__':    
    app = QApplication(sys.argv)
    ex = MainWindow()
sys.exit(app.exec_())

Solution

  • Although QPainter is used to paint a widget it will not work for this case since it paints the "MainWindow" that is below its children as the QLabels. There are at least 2 possible solutions:

    • Create a custom QLabel and detect the click and paint the circle,

    • Create a QLabel that shows a QPixmap that has the circle and move it based on the mouse information.

    In this case I will implement the second method:

    import sys, cv2
    from PyQt5.QtWidgets import QMainWindow, QApplication, QLabel
    from PyQt5.QtGui import QImage, QPixmap, QPainter, QPen, QFont
    from PyQt5.QtCore import QTimer, Qt
    
    
    class MainWindow(QMainWindow):
        def __init__(self):
            super().__init__()
            self.initUI()
    
        def initUI(self):
            self.statusBar().showMessage("Ready")
            self.setGeometry(50, 50, 800, 600)
            self.setWindowTitle("Statusbar")
            self.vidWindow = QLabel(self)
            self.vidWindow.setGeometry(20, 20, 640, 480)
            self.maskWindow = QLabel(self)
            self.maskWindow.setGeometry(20, 20, 640, 480)
            self.maskWindow.setStyleSheet("background-color: rgba(0,0,0,0%)")
            font = QFont()
            font.setPointSize(18)
            font.setBold(True)
            font.setWeight(75)
            self.maskWindow.setFont(font)
            self.maskWindow.setText("Message is on the mask Qlabel object")
            self.msgLabel = QLabel(self)
            self.msgLabel.setGeometry(675, 300, 100, 20)
    
            self.marker_label = QLabel(self)
    
            pixmap = QPixmap(100, 100)
            pixmap.fill(Qt.transparent)
    
            painter = QPainter(pixmap)
            painter.setPen(QPen(Qt.green, 4, Qt.SolidLine))
            painter.drawEllipse(pixmap.rect().adjusted(4, 4, -4, -4))
            painter.end()
    
            self.marker_label.setPixmap(pixmap)
            self.marker_label.adjustSize()
            self.marker_label.hide()
            self.marker_label.raise_()
    
            self.cap = cv2.VideoCapture(0)
            self.timer = QTimer()
            self.frame_rate = 5
            self.show()
            self.start()
    
        def nextFrameSlot(self):
            ret, frame = self.cap.read()
            if ret == True:
                frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
                img = QImage(frame, frame.shape[1], frame.shape[0], QImage.Format_RGB888)
                img = img.scaled(640, 480, Qt.KeepAspectRatio)
                pix = QPixmap.fromImage(img)
                self.vidWindow.setPixmap(pix)
    
        def mousePressEvent(self, event):
            self.msgLabel.setText("Mouse Clicked!")
            if self.vidWindow.rect().contains(event.pos()):
                self.marker_label.move(event.pos() - self.marker_label.rect().center())
                self.marker_label.show()
            super().mousePressEvent(event)
    
        def start(self):
            rate = int(1000.0 / self.frame_rate)
            self.timer.setTimerType(Qt.PreciseTimer)
            self.timer.timeout.connect(self.nextFrameSlot)
            self.timer.start(rate)
    
        def closeEvent(self, event):
            if self.cap.isOpened():
                self.cap.release()
            super().closeEvent(event)
    
    
    if __name__ == "__main__":
        app = QApplication(sys.argv)
        ex = MainWindow()
        sys.exit(app.exec_())