PyQt5 VideoPlayer: Converting to Object Orientated Code Prevents Playback - python-3.x

I've been looking into how to incorporate a video or livestream into an app. I have found some functional code that works fine:
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
import cv2 # OpenCV
import qimage2ndarray # for a memory leak,see gist
import sys # for exiting
# Minimal implementation...
def displayFrame():
ret, frame = cap.read()
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
image = qimage2ndarray.array2qimage(frame)
label.setPixmap(QPixmap.fromImage(image))
app = QApplication([])
window = QWidget()
# OPENCV
cap = cv2.VideoCapture("Vid.mp4")
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 320)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 240)
# timer for getting frames
timer = QTimer()
timer.timeout.connect(displayFrame)
timer.start(60)
label = QLabel('No Camera Feed')
button = QPushButton("Quiter")
button.clicked.connect(sys.exit) # quiter button
layout = QVBoxLayout()
layout.addWidget(button)
layout.addWidget(label)
window.setLayout(layout)
window.show()
app.exec_()
And I am trying to be able to use this in some object orientated code, with the aim of creating a video playback widget to incorporate into other apps:
import cv2
import sys
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import qimage2ndarray # for a memory leak,see gist
import sys # for exiting
# Minimal implementation...
class basicWindow(QMainWindow):
def __init__(self):
super(basicWindow, self).__init__()
# OPENCV
cap = cv2.VideoCapture("Vid.mp4")
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 320)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 240)
# timer for getting frames
timer = QTimer()
timer.timeout.connect(displayFrame)
timer.start(60)
label = QLabel('No Camera Feed')
button = QPushButton("Quiter")
button.clicked.connect(sys.exit) # quiter button
layout = QVBoxLayout()
layout.addWidget(button)
layout.addWidget(label)
widget = QWidget()
widget.setLayout(layout)
self.setCentralWidget(widget)
def displayFrame():
ret, frame = cap.read()
if ret:
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
image = qimage2ndarray.array2qimage(frame)
try:
label.setPixmap(QPixmap.fromImage(image))
except Exception as e:
print(e)
if __name__ == '__main__':
app = QApplication(sys.argv)
windowExample = basicWindow()
windowExample.show()
sys.exit(app.exec_())
I'm new to both OO coding and PyQt5, so any advice on how I'm either misinterpreting how the code works or what Im missing would be great. I have tried already setting label to a global variable, as I wasnt sure the function displayFrame() was aware of what label to change with label.setPixmap, but otherwise Im a little lost.

In your first example it works because label is a global variable, so displayFrame can access it as such.
In the other case, label is only declared in the scope of the __init__ of the basicWindow instance, so displayFrame knows nothing about it.
Make label a member of the instance (self.label = QLabel()), displayFrame a method of the basicWindow class (def displayFrame(self):) and then access the label correctly; note that you also need to make both cap and timer member of the instance (self), otherwise their objects will be immediately "garbage collected" after __init__ returns.
class basicWindow(QMainWindow):
def __init__(self):
super(basicWindow, self).__init__()
# ...
self.cap = cv2.VideoCapture("Vid.mp4")
# ...
self.timer = QTimer()
self.timer.timeout.connect(self.displayFrame)
self.timer.start(60)
self.label = QLabel('No Camera Feed')
# ...
def displayFrame(self):
ret, frame = self.cap.read()
if ret:
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
image = qimage2ndarray.array2qimage(frame)
try:
self.label.setPixmap(QPixmap.fromImage(image))
except Exception as e:
print(e)
Since you're new to OO programming, I suggest you to start by studying how classes and instances and name resolution work in python.

Related

Kivy strange behavior when it updates Image Texture

I am trying to update the Kivy Image.texture with the OpenCV image, and I am using a new thread to do it. I found some discussion that "the graphics operation should be in the main thread". But still, I want to figure out why the code below works.
from kivy.app import App
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.image import Image
from kivy.graphics.texture import Texture
import cv2
from threading import Thread
class MainApp(App):
def __init__(self):
super().__init__()
self.layout = FloatLayout()
self.image = Image()
self.layout.add_widget(self.image)
Thread(target=self.calculate).start()
def build(self):
return self.layout
def calculate(self):
img = cv2.imread("test.png")
w, h = img.shape[1], img.shape[0]
img = cv2.flip(img, flipCode=0)
buf = img.tostring()
texture = Texture(0, 0, 0).create(size=(w, h), colorfmt="bgr")
texture.blit_buffer(buf, colorfmt='bgr', bufferfmt='ubyte')
self.image.texture = texture
def main():
Image(source='test.png') # remove this line will froze the app
MainApp().run()
if __name__ == "__main__":
main()
If I remove this line:
Image(source='test.png')
the app is frozen. Can someone help me to understand why decalring an Image object outside the main loop will affect the MainApp running.
The "test.png" image can be any simple image, like below. You need to put the image in the same directory as this script.
Not sure exactly why that line affects your code, but the main problem is that not only is the GUI manipulation required to be done on the main thread, but also any blit_buffer() must also be done on the main thread. So, a working version of your code (with the Image() removed) looks like this:
from functools import partial
from kivy.app import App
from kivy.clock import Clock
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.image import Image
from kivy.graphics.texture import Texture
import cv2
from threading import Thread
class MainApp(App):
def __init__(self):
super().__init__()
self.layout = FloatLayout()
self.image = Image()
self.layout.add_widget(self.image)
Thread(target=self.calculate).start()
def build(self):
return self.layout
def calculate(self):
img = cv2.imread("test.png")
w, h = img.shape[1], img.shape[0]
img = cv2.flip(img, flipCode=0)
buf = img.tostring()
Clock.schedule_once(partial(self.do_blit, buf, w, h))
def do_blit(self, buf, w, h, dt):
texture = Texture(0, 0, 0).create(size=(w, h), colorfmt="bgr")
texture.blit_buffer(buf, colorfmt='bgr', bufferfmt='ubyte')
self.image.texture = texture
def main():
# Image(source='test.png') # remove this line will froze the app
MainApp().run()
if __name__ == "__main__":
main()

PyQt5 add second function to thread but not work

Previously I have tried to use Flask for doing the followings simultaneously:
Display live video streaming
Display real-time data streaming
Control the robot car
As the above is just for demonstration, with the video streaming performance not good enough, I decided to change the whole application to PyQt5 for further development and production. Now I can create the GUI for displaying live video streaming well, while the real-time data streaming cannot be done well. The error is
QObject::startTimer: Timers can only be used with threads started with QThread
The following is the whole program. Please help to see what's wrong in the adding thread issue. Thanks!
import sys
from PyQt5 import QtWidgets, QtCore, QtGui
import cv2
from vidgear.gears import CamGear
from random import random
data_list=[]
fps=60
options_cam={"CAP_PROP_FRAME_WIDTH":640,"CAP_PROP_FRAME_HEIGHT":480,"CAP_PROP_FPS":fps}
stream=CamGear(source=0,logging=False,**options_cam).start()
class MainWindow(QtWidgets.QWidget):
def __init__(self,*args):
super(MainWindow, self).__init__()
self.setWindowTitle('Vehicle control')
self.grid_layout=QtWidgets.QGridLayout()
self.video_label = QtWidgets.QLabel('Video streaming',self)
self.video_frame = QtWidgets.QLabel(self)
self.grid_layout.addWidget(self.video_label,0,0)
self.grid_layout.addWidget(self.video_frame,1,0)
self.data_label = QtWidgets.QLabel('Data streaming',self)
self.data_frame = QtWidgets.QListWidget(self)
self.grid_layout.addWidget(self.data_label,0,1)
self.grid_layout.addWidget(self.data_frame,1,1)
self.setLayout(self.grid_layout)
#self.thread=QtCore.QThread()
#self.thread.started.connect(self.nextFrameSlot)
#self.thread.start()
self.timer=QtCore.QTimer()
self.timer.timeout.connect(self.video_stream)
self.timer.start(0)
self.thread=QtCore.QThread()
self.thread.start()
self.timer2=QtCore.QTimer()
self.timer2.moveToThread(self.thread)
self.timer2.timeout.connect(self.data_stream)
self.timer2.start(0)
def video_stream(self):
frame = stream.read()
# My webcam yields frames in BGR format
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
img = QtGui.QImage(frame, frame.shape[1], frame.shape[0], QtGui.QImage.Format_RGB888)
pix = QtGui.QPixmap.fromImage(img)
self.video_frame.setPixmap(pix)
QtCore.QThread.sleep(0)
def data_stream(self):
print("data stream")
stream_data=round(random()*10,3)
data_list.insert(0,str(stream_data)+'\n')
if len(data_list)>10:
del data_list[-1]
for i in range(len(data_list)):
self.data_frame.addItem(data_list[i])
self.data_frame.show()
QtCore.QThread.sleep(1000)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
Edit:
Thanks #musicamante's answer. I have updated the code as follows but still have the error "segmentation fault" for the video streaming, while if I run for data stream only, the updated list can be shown. So what's wrong with the setPixmap function? Thanks again!
import sys
from PyQt5 import QtWidgets, QtCore, QtGui
import cv2
from vidgear.gears import CamGear
from random import random
fps=60
options_cam={"CAP_PROP_FRAME_WIDTH":480,"CAP_PROP_FRAME_HEIGHT":480,"CAP_PROP_FPS":fps}
stream=CamGear(source=0,logging=False,**options_cam).start()
class CamGrabber(QtCore.QThread):
frame = QtCore.pyqtSignal(QtGui.QImage)
def run(self):
while True:
new_frame = stream.read()
new_frame = cv2.cvtColor(new_frame, cv2.COLOR_BGR2RGB)
img = QtGui.QImage(new_frame, new_frame.shape[1], new_frame.shape[0], QtGui.QImage.Format_RGB888)
self.frame.emit(img)
class DataProvider(QtCore.QThread):
data = QtCore.pyqtSignal(object)
def run(self):
while True:
newData = round(random()*10,3)
self.data.emit(newData)
QtCore.QThread.sleep(1)
class MainWindow(QtWidgets.QWidget):
def __init__(self,*args):
super(MainWindow, self).__init__()
self.setWindowTitle('Vehicle control')
self.grid_layout=QtWidgets.QGridLayout()
self.video_label = QtWidgets.QLabel('Video streaming',self)
self.video_frame = QtWidgets.QLabel(self)
self.grid_layout.addWidget(self.video_label,0,0)
self.grid_layout.addWidget(self.video_frame,1,0)
self.data_label = QtWidgets.QLabel('Data streaming',self)
self.data_frame = QtWidgets.QListWidget(self)
self.grid_layout.addWidget(self.data_label,0,1)
self.grid_layout.addWidget(self.data_frame,1,1)
self.setLayout(self.grid_layout)
self.camObject = CamGrabber()
self.camObject.frame.connect(self.newFrame)
self.camObject.start()
self.dataProvider = DataProvider()
self.dataProvider.data.connect(self.newData)
self.dataProvider.start()
def newFrame(self, img):
self.video_frame.setPixmap(QtGui.QPixmap.fromImage(img))
def newData(self, data):
self.data_frame.insertItem(0,str(data))
if self.data_frame.count() > 10:
self.data_frame.takeItem(9)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())```
The QTimer error basically means that the a QTimer can only be started from the thread it exists.
Besides that, GUI element should always be directly accessed or modified from the main thread, not from another one.
In order to accomplish that, you'll need to create a separate "worker" thread, and communicate with the main one by taking advantage of the signal/slot mechanism.
class CamGrabber(QtCore.QThread):
frame = QtCore.pyqtSignal(QtGui.QImage)
def run(self):
while True:
frame = stream.read()
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
img = QtGui.QImage(frame, frame.shape[1], frame.shape[0], QtGui.QImage.Format_RGB888)
self.frame.emit(img)
class DataProvider(QtCore.QThread):
data = QtCore.pyqtSignal(object)
def run(self):
while True:
newData = round(random()*10,3)
self.data.emit(newData)
# note that QThread.sleep unit is seconds, not milliseconds
QtCore.QThread.sleep(1)
class MainWindow(QtWidgets.QWidget):
def __init__(self,*args):
super(MainWindow, self).__init__()
self.setWindowTitle('Vehicle control')
self.grid_layout=QtWidgets.QGridLayout()
self.video_label = QtWidgets.QLabel('Video streaming',self)
self.video_frame = QtWidgets.QLabel(self)
self.grid_layout.addWidget(self.video_label,0,0)
self.grid_layout.addWidget(self.video_frame,1,0)
self.data_label = QtWidgets.QLabel('Data streaming',self)
self.data_frame = QtWidgets.QListWidget(self)
self.grid_layout.addWidget(self.data_label,0,1)
self.grid_layout.addWidget(self.data_frame,1,1)
self.setLayout(self.grid_layout)
self.camObject = CamGrabber()
self.camObject.frame.connect(self.newFrame)
self.camObject.start()
self.dataProvider = DataProvider()
self.dataProvider.data.connect(self.newData)
self.dataProvider.start()
def newFrame(self, img):
self.video_frame.setPixmap(QtGui.QPixmap.fromImage(img))
def newData(self, data):
self.data_frame.addItem(str(data))
if self.data_frame.count() > 10:
self.data_frame.takeItem(0)
If, for any reason, you want to control the data fetching from the main thread via a QTimer, you could use a Queue:
from queue import Queue
class DataProvider(QtCore.QObject):
data = QtCore.pyqtSignal(object)
def __init__(self):
super().__init__()
self.queue = Queue()
def run(self):
while True:
multi = self.queue.get()
# simulate a time consuming process
QtCore.QThread.sleep(5)
newData = round(multi * 10, 3)
self.data.emit(newData)
def pushData(self, data):
self.queue.put(data)
class MainWindow(QtWidgets.QWidget):
def __init__(self,*args):
# ...
self.requestTimer = QtCore.QTimer()
self.requestTimer.setInterval(1000)
self.requestTimer.timeout.connect(self.requestData)
# this will cause the timer to be executed only once after each time
# start() is called, so no new requests will overlap
self.requestTimer.setSingleShot(True)
self.requestTimer.start()
def requestData(self):
value = random()
print('requesting data with value {}'.format(value))
self.dataProvider.pushData(value)
print('waiting for result')
def newFrame(self, img):
self.video_frame.setPixmap(QtGui.QPixmap.fromImage(img))
def newData(self, data):
print('data received')
self.data_frame.addItem(str(data))
if self.data_frame.count() > 10:
self.data_frame.takeItem(0)
# restart the timer
self.requestTimer.start()

Flickering video for tkinter video

I am trying to make a simple play/pause application in tkinter. Basically I want to show a video and have a play/pause button underneath.
So, after some research I found this suitable post to show a video using tkinter and opencv:
to show video streaming inside frame in tkinter
When using the code, given in the accepted answer to show a video, there is no problem and I don't see any flickering. Here is the code:
# import the necessary packages
from __future__ import print_function
import tkinter as tk
from PIL import ImageTk, Image
import cv2
root = tk.Tk()
# Create a frame
app = tk.Frame(root, bg="white")
app.grid()
# Create a label in the frame
lmain = tk.Label(app)
lmain.grid()
# Capture from camera
cap = cv2.VideoCapture(r'PATH_TO_VIDEO_FILE')
# function for video streaming
frame_number = 0
def video_stream():
global frame_number
cap.set(cv2.CAP_PROP_POS_FRAMES, frame_number)
success, frame = cap.read()
cv2image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGBA)
img = Image.fromarray(cv2image)
imgtk = ImageTk.PhotoImage(image=img)
lmain.imgtk = imgtk
lmain.configure(image=imgtk)
lmain.after(1, video_stream)
frame_number += 1
video_stream()
root.mainloop()
Now, I slightly altered the code to be able to use the grid manager and add a play button:
# import the necessary packages
from __future__ import print_function
import tkinter as tk
from PIL import ImageTk, Image
import cv2
class PhotoBoothApp:
def __init__(self, path_to_video):
# initialize the root window
self.window = tk.Tk()
self.window.title("Video_Player")
self.videocap = cv2.VideoCapture(path_to_video)
self.frame_number = 0
# Initalize
self.videocap.set(cv2.CAP_PROP_POS_FRAMES, self.frame_number)
success, self.frame = self.videocap.read()
cv2image = cv2.cvtColor(self.frame, cv2.COLOR_BGR2RGBA)
self.img = Image.fromarray(cv2image)
self.imgtk = ImageTk.PhotoImage(image=self.img)
# Show frames
self.picture_label = tk.Label(self.window, image=self.imgtk, relief=tk.RIDGE).grid(row=0, column=0)
self.btn_next_image=tk.Button(self.window, text="Play", width=50, bg ="green",command=self.video_stream).grid(row=1,column=0)
self.window.mainloop()
def video_stream(self):
self.videocap.set(cv2.CAP_PROP_POS_FRAMES, self.frame_number)
sucess, frame = self.videocap.read()
cv2image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGBA)
img = Image.fromarray(cv2image)
self.imgtk = ImageTk.PhotoImage(image=img)
self.picture_label = tk.Label(self.window, image=self.imgtk, relief=tk.RIDGE).grid(row=0, column=0)
# Update Frame Number to display
self.frame_number = self.frame_number + 1
self.window.after(1, self.video_stream)
ph = PhotoBoothApp(r'PATH_TO_FILE')
The problem is that when I execute the above code, the video flickers as if tkinter need to reload something in-between frames. I have no clue why this happens.
P.S. This post here Flickering video in opencv-tkinter integration did not help me.
You need to make two changes: split your self.picture_label line to create a proper reference to your Label object, and then use self.picure_label.config(...) to change the image.
class PhotoBoothApp:
def __init__(self, path_to_video):
# initialize the root window
...
self.picture_label = tk.Label(self.window, image=self.imgtk, relief=tk.RIDGE)
self.picture_label.grid(row=0, column=0)
...
def video_stream(self):
...
img = Image.fromarray(cv2image)
self.imgtk = ImageTk.PhotoImage(image=img)
self.picture_label.config(image=self.imgtk)
# Update Frame Number to display
self.frame_number = self.frame_number + 1
self.window.after(1, self.video_stream)
ph = PhotoBoothApp(r'PATH_TO_FILE')

OpenCV : Memory leak while using QPixmap

I am trying to video stream from a 1920x1080 HD camera with 60fps,using opencv. The issue is not with the streaming but the memory leak, I am losing almost 6GB of my memory within the first minute of streaming. Please help me how to stop this.
from PySide.QtCore import *
from PySide.QtGui import *
import cv2
import sys
class MainApp(QWidget):
def __init__(self):
QWidget.__init__(self)
self.video_size = QSize(1920, 1080)
self.setup_ui()
self.setup_camera()
def setup_ui(self):
"""Initialize widgets.
"""
self.image_label = QLabel()
self.image_label.setFixedSize(self.video_size)
self.quit_button = QPushButton("Quit")
self.quit_button.clicked.connect(self.close)
self.main_layout = QVBoxLayout()
self.main_layout.addWidget(self.image_label)
self.main_layout.addWidget(self.quit_button)
self.setLayout(self.main_layout)
def setup_camera(self):
"""Initialize camera.
"""
self.capture = cv2.VideoCapture(0)
self.capture.set(cv2.CAP_PROP_FRAME_WIDTH, self.video_size.width())
self.capture.set(cv2.CAP_PROP_FRAME_HEIGHT, self.video_size.height())
self.timer = QTimer()
self.timer.timeout.connect(self.display_video_stream)
self.timer.start(30)
def display_video_stream(self):
"""Read frame from camera and repaint QLabel widget.
"""
_, frame = self.capture.read()
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
image = QImage(frame, frame.shape[1], frame.shape[0],
frame.strides[0], QImage.Format_RGB888)
self.image_label.setPixmap(QPixmap.fromImage(image))
if __name__ == "__main__":
app = QApplication(sys.argv)
win = MainApp()
win.show()
sys.exit(app.exec_())
EDIT
I found where the memory leak is self.timer.start(30) If I increase the time call , the memory leak is slower. Any suggestions how to stop the memory leak?
If anyone having the same issue, please import qimage2ndarray this library and add
the following changes to the function display_video_stream
def display_video_stream(self):
"""Read frame from camera and repaint QLabel widget.
"""
_, frame = self.capture.read()
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# frame = cv2.flip(frame, 1)
image = qimage2ndarray.array2qimage(frame) #Solution for memory leak
self.image_label.setPixmap(QPixmap.fromImage(image))

PyQt4 Mdi sub window not resizing properly

In the following example, when you click the button, the entire form is rebuilt adding a new label each time. At the end is a resize call that doesn't appear to work. While debugging, I validated the sizeHint() is returning the correct dimensions, and internally the widget thinks it is the correct size, but what is drawn is not correct. What can I do to force the MDI window to resize correctly? Also of note, when not sized correctly, if you manually start resizing, it suddenly snaps to the appropriate size.
import sys
import os
from PyQt4.QtCore import *
from PyQt4.QtGui import *
import PyQt4.Qt
class MdiWindow(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.count = 0
self.buildWindow()
def buildWindow(self):
main = QVBoxLayout()
button = QPushButton('Change Count')
button.clicked.connect(self.changeCount)
main.addWidget(button)
for i in range(self.count):
main.addWidget(QLabel(str(i)))
widget = QWidget()
widget.setLayout(main)
self.setCentralWidget(widget)
self.resize(main.sizeHint())
def changeCount(self, event):
self.count += 1
self.buildWindow()
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle('Resize Test')
self.mdiArea = QMdiArea()
self.setCentralWidget(self.mdiArea)
child = MdiWindow()
self.mdiArea.addSubWindow(child)
if __name__ == '__main__':
app = QApplication(sys.argv)
mainWin = MainWindow()
mainWin.show()
sys.exit(app.exec_())

Resources