fbpixel
Tags: , ,

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.

pyqt-opencv-result Displaying an OpenCV Image in a PyQt interface

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())
pyqt-opencv-result-better Displaying an OpenCV Image in a PyQt interface

Sources