For certain applications, you may find it useful to embed OpenCV in a PyQt interface. In this tutorial, we’ll look at how to correctly integrate and manage a video captured by OpenCV in a PyQt application.
N.B.: We use Pyside, but conversion to PyQt is quite straightforward.
Prerequisites:
- Installing Python
- OpenCV installation (pip install opencv-python)
- PySide or PyQt (pip install pyside6 or pip install PyQt5)
Code to capture a video with OpenCV
Here’s the basic code for displaying a webcam video with openCV
import sys import cv2 def main(args): cap = cv2.VideoCapture(0) #default camera while(True): ret, frame = cap.read() if ret: frame=cv2.resize(frame, (800, 600)) cv2.imshow("Video",frame) if cv2.waitKey(1) & 0xFF == ord('q'): #click q to stop capturing break cap.release() cv2.destroyAllWindows() return 0 if __name__ == '__main__': sys.exit(main(sys.argv))
- The run function is the function containing the openCV code that will loop when the QThread.start function is called.
- The stop function is used to cleanly stop the thread
- The changePixmap signal lets the application know that a new image is available.
class Thread(QThread): changePixmap = pyqtSignal(QImage) def run(self): self.isRunning=True cap = cv2.VideoCapture(0) while self.isRunning: ret, frame = cap.read() if ret: rgbImage = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch = rgbImage.shape bytesPerLine = ch * w convertToQtFormat = QImage(rgbImage.data, w, h, bytesPerLine, QImage.Format_RGB888) p = convertToQtFormat.scaled(640, 480, Qt.KeepAspectRatio) self.changePixmap.emit(p) def stop(self): self.isRunning=False self.quit() self.terminate()
Creating the PyQt application
For the application, we’ll create a QLabel in a simple QWidget that will contain the video image and instantiate the QThread. The video will be updated automatically using the setImage function, which is called when the changePixmap signal is received.
- function setImage
@pyqtSlot(QImage) def setImage(self, image): #update image self.label.setPixmap(QPixmap.fromImage(image))
- signal changePixmap
self.th.changePixmap.connect(self.setImage)
class VideoContainer(QWidget): def __init__(self): super().__init__() self.title = 'PySide Video' self.left = 100 self.top = 100 self.fwidth = 640 self.fheight = 480 self.initUI() @pyqtSlot(QImage) def setImage(self, image): #update image self.label.setPixmap(QPixmap.fromImage(image)) def initUI(self): self.setWindowTitle(self.title) self.setGeometry(self.left, self.top, self.fwidth, self.fheight) self.resize(1200, 800) # create a label self.label = QLabel(self) self.label.resize(640, 480) self.th = Thread(self) self.th.changePixmap.connect(self.setImage) self.th.start() self.show()
Complete code for displaying a video in a PyQt window
import cv2 import sys #from PyQt5.QtWidgets import QWidget, QLabel, QApplication #from PyQt5.QtCore import QThread, Qt, pyqtSignal, pyqtSlot #from PyQt5.QtGui import QImage, QPixmap from PySide6.QtWidgets import QWidget, QLabel, QApplication from PySide6.QtCore import QThread, Qt, Signal, Slot from PySide6.QtGui import QImage, QPixmap pyqtSignal = Signal pyqtSlot = Slot class Thread(QThread): changePixmap = pyqtSignal(QImage) def run(self): self.isRunning=True cap = cv2.VideoCapture(0) while self.isRunning: ret, frame = cap.read() if ret: # https://stackoverflow.com/a/55468544/6622587 rgbImage = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch = rgbImage.shape bytesPerLine = ch * w convertToQtFormat = QImage(rgbImage.data, w, h, bytesPerLine, QImage.Format_RGB888) p = convertToQtFormat.scaled(640, 480, Qt.KeepAspectRatio) self.changePixmap.emit(p) def stop(self): self.isRunning=False self.quit() self.terminate() class VideoContainer(QWidget): def __init__(self): super().__init__() self.title = 'Video' self.left = 100 self.top = 100 self.fwidth = 640 self.fheight = 480 self.initUI() @pyqtSlot(QImage) def setImage(self, image): #update image self.label.setPixmap(QPixmap.fromImage(image)) def initUI(self): self.setWindowTitle(self.title) self.setGeometry(self.left, self.top, self.fwidth, self.fheight) self.resize(1200, 800) # create a label self.label = QLabel(self) self.label.resize(640, 480) self.th = Thread(self) self.th.changePixmap.connect(self.setImage) self.th.start() self.show() if __name__ == '__main__': app = QApplication(sys.argv) ex = VideoContainer() sys.exit(app.exec())
A “Video” window appears, containing the image from the webcam.
Bonus: improved closing interface
The code works well and may be sufficient, but there are a few problems with this implementation:
- Application cannot be closed with Ctrl+C (KeyboardInterrupt)
- When you close the window, Qthread does not stop
- If you resize the window, the video size does not change.
In order to close the application with Ctrl+C, you can use the interrupt signal. To do this, simply add the following code before calling the application (cleaner methods exist)
import signal #close signal with Ctrl+C signal.signal(signal.SIGINT, signal.SIG_DFL)
To terminate the QThread when the window is closed, you can use the application’s aboutToQuit signal to call the QThread’s stop function
app.aboutToQuit.connect(ex.th.stop) #stop qthread when closing window
Finally, to resize the video with the window at each refresh, we use the size of the window to calculate the size of the image and the position of the label so that it’s centered and the video retains its proportions.
@pyqtSlot(QImage) def setImage(self, image): #resize image with window and center imWidth=self.width()-2*self.padding imHeight=self.height()-2*self.padding image = image.scaled(imWidth, imHeight, Qt.KeepAspectRatio) # remove Qt.KeepAspectRatio if not needed self.label.resize(image.width(), image.height()) #(640, 480) self.label.move((self.width()-image.width())/2, (self.height()-image.height())/2) #update image self.label.setPixmap(QPixmap.fromImage(image))
Here is the complete code with enhancement
import cv2 import sys #from PyQt5.QtWidgets import QWidget, QLabel, QApplication #from PyQt5.QtCore import QThread, Qt, pyqtSignal, pyqtSlot #from PyQt5.QtGui import QImage, QPixmap from PySide6.QtWidgets import QWidget, QLabel, QApplication from PySide6.QtCore import QThread, Qt, Signal, Slot from PySide6.QtGui import QImage, QPixmap pyqtSignal = Signal pyqtSlot = Slot class Thread(QThread): changePixmap = pyqtSignal(QImage) def run(self): self.isRunning=True cap = cv2.VideoCapture(0) while self.isRunning: ret, frame = cap.read() if ret: # https://stackoverflow.com/a/55468544/6622587 rgbImage = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch = rgbImage.shape bytesPerLine = ch * w convertToQtFormat = QImage(rgbImage.data, w, h, bytesPerLine, QImage.Format_RGB888) p = convertToQtFormat.scaled(640, 480, Qt.KeepAspectRatio) self.changePixmap.emit(p) def stop(self): self.isRunning=False self.quit() self.terminate() class VideoContainer(QWidget): def __init__(self): super().__init__() self.title = 'PySide Video' self.left = 100 self.top = 100 self.fwidth = 640 self.fheight = 480 self.padding = 10 self.initUI() @pyqtSlot(QImage) def setImage(self, image): #resize image with window and center imWidth=self.width()-2*self.padding imHeight=self.height()-2*self.padding image = image.scaled(imWidth, imHeight, Qt.KeepAspectRatio) # remove Qt.KeepAspectRatio if not needed self.label.resize(image.width(), image.height()) #(640, 480) self.label.move((self.width()-image.width())/2, (self.height()-image.height())/2) #update image self.label.setPixmap(QPixmap.fromImage(image)) def initUI(self): self.setWindowTitle(self.title) self.setGeometry(self.left, self.top, self.fwidth, self.fheight) self.resize(1200, 800) # create a label self.label = QLabel(self) self.label.resize(self.width()-2*self.padding,self.height()-2*self.padding) #(640, 480) self.th = Thread(self) self.th.changePixmap.connect(self.setImage) self.th.start() self.show() import signal #close signal with Ctrl+C signal.signal(signal.SIGINT, signal.SIG_DFL) if __name__ == '__main__': app = QApplication(sys.argv) ex = VideoContainer() app.aboutToQuit.connect(ex.th.stop) #stop qthread when closing window sys.exit(app.exec())