Qt: getting a thumbnail for a video - python

Is there a better way to get a thumbnail for a video? OS is primarily Linux, but hopefully there's a cross platform way to do it.
This is what I have right now:
from PySide6 import QtMultimedia as qtm
from PySide6 import QtMultimediaWidgets as qtmw
from PySide6 import QtCore as qtc
app = qtw.QApplication()
thumbnail_file = "video.mp4"
loop = qtc.QEventLoop()
widget = qtmw.QVideoWidget()
widget.setVisible(False)
media_player = qtm.QMediaPlayer()
media_player.setVideoOutput(widget)
media_player.mediaStatusChanged.connect(loop.exit)
media_player.positionChanged.connect(loop.exit)
media_player.setSource(thumbnail_file)
loop.exec()
media_player.mediaStatusChanged.disconnect()
media_player.play()
if media_player.isSeekable():
media_player.setPosition(media_player.duration() // 2)
loop.exec()
media_player.positionChanged.disconnect()
media_player.stop()
image = media_player.videoSink().videoFrame().toImage()
image.save('thumbnail.jpg')
app.exec()
This will be ran in a separate thread so the time is not really an issue, but it's still pretty convoluted.

There are many different approaches to this, since QMediaPlayer can undergo multiple state changes which can be monitored in a variety of ways. So the question of what is "best" probably comes down to what is most reliable/predictable on the target platform(s). I have only tested on Arch Linux using Qt-6.3.1 with a GStreamer-1.20.3 backend, but I think the solution presented below should work correctly with most other setups.
As it stands, the example code in the question doesn't work on Linux. As pointed out in the comments, this can be fixed by using the videoFrameChanged signal of the mediad-player's video-sink. (Unfortunately, at time of writing, these APIs are rather poorly documented). However, the example can be simplified and improved in various ways, so I have provided an alternative solution below.
During testing, I found that the durationChanged and postionChanged signals cannot be relied upon to give the relevant notifications at the appropriate time, so I have avoided using them. It's best to wait for the player to reach the buffered-state before setting the position, and then wait until a frame is received that can be verified as matching the requested position before saving the image. (I would also advise adding a suitable timeout in case the media-player gets stuck in an indeterminate state).
To illustrate the timing issues alluded to above, here is some sample output:
frame changed: 0
duration change: 13504
status change: b'LoadedMedia'
frame changed: 0
status change: b'BufferedMedia'
set position: 6752
frame changed: 6752
save: exit
frame changed: 0
frame changed: 0
duration change: 13504
status changed: b'LoadedMedia'
status changed: b'BufferedMedia'
set position: 6752
frame changed: 6752
save: exit
As you can see, the exact sequence of events isn't totally predictable. If the position is set before the buffered-state is reached, the output looks like this:
frame changed: 0
duration changed: 13504
status changed: LoadedMedia
set position: 0
frame changed: 0
status change: BufferedMedia
frame changed: 40
frame changed: 80
... # long list of changes
frame changed: 6720
frame changed: 6760
save: exit
Here is the demo script (which works with both PySide6 and PyQt6):
import os
from PySide6.QtCore import QCoreApplication, QTimer, QEventLoop, QUrl
from PySide6.QtMultimedia import QMediaPlayer, QVideoSink
# from PyQt6.QtCore import QCoreApplication, QTimer, QEventLoop, QUrl
# from PyQt6.QtMultimedia import QMediaPlayer, QVideoSink
def thumbnail(url):
position = 0
image = None
loop = QEventLoop()
QTimer.singleShot(15000, lambda: loop.exit(1))
player = QMediaPlayer()
player.setVideoSink(sink := QVideoSink())
player.setSource(url)
def handle_status(status):
nonlocal position
print('status changed:', status.name)
# if status == QMediaPlayer.MediaStatus.LoadedMedia:
if status == QMediaPlayer.MediaStatus.BufferedMedia:
player.setPosition(position := player.duration() // 2)
print('set position:', player.position())
def handle_frame(frame):
nonlocal image
print('frame changed:', frame.startTime() // 1000)
if (start := frame.startTime() // 1000) and start >= position:
sink.videoFrameChanged.disconnect()
image = frame.toImage()
print('save: exit')
loop.exit()
player.mediaStatusChanged.connect(handle_status)
sink.videoFrameChanged.connect(handle_frame)
player.durationChanged.connect(
lambda value: print('duration changed:', value))
player.play()
if loop.exec() == 1:
print('ERROR: process timed out')
return image
video_file = 'video.mp4'
thumbnail_file = 'thumbnail.jpg'
try:
os.remove(thumbnail_file)
except OSError:
pass
app = QCoreApplication(['Test'])
image = thumbnail(QUrl.fromLocalFile(video_file))
if image is not None:
image.save(thumbnail_file)

Answer reached thanks to the discussion in the comments of OP:
from PySide6 import QtWidgets as qtw
from PySide6 import QtMultimedia as qtm
from PySide6 import QtMultimediaWidgets as qtmw
from PySide6 import QtCore as qtc
app = qtw.QApplication()
thumbnail_file = "video.mp4"
loop = qtc.QEventLoop()
video_sink = qtm.QVideoSink()
media_player = qtm.QMediaPlayer()
media_player.setVideoSink(video_sink)
media_player.mediaStatusChanged.connect(loop.exit)
media_player.setSource(thumbnail_file)
loop.exec()
media_player.mediaStatusChanged.disconnect()
if media_player.isSeekable() and media_player.duration():
media_player.positionChanged.connect(loop.exit)
media_player.setPosition(media_player.duration() // 2)
media_player.play()
loop.exec()
media_player.positionChanged.disconnect()
else:
media_player.play()
video_sink.videoFrameChanged.connect(loop.exit)
loop.exec()
video_sink.videoFrameChanged.disconnect()
media_player.stop()
image = media_player.videoSink().videoFrame().toImage()
image.save('thumbnail.jpg')

Related

QML gauges not updating from Python

I'm still getting to grips with QT... I've made a python file and a QML file. the Python file updates the gauge's value from data it gets over UDP.
This only works once though... the first UDP packet comes in and updates the gauge, but when it gets the next packet,despite the value updating, the gauge itself does not.
QML
CircularGauge {
id: circularGauge
x: 30
y: 30
value: itt1value
minimumValue: 0
maximumValue: 1200
tickmarksVisible: false
style: CircularGaugeStyle {
maximumValueAngle: 400
minimumValueAngle: 90
}
}
Python:
def configureApplication():
# Set up the application window
app = QGuiApplication(sys.argv)
view = QQuickView()
view.setResizeMode(QQuickView.SizeRootObjectToView)
view.setTitle("my title")
# Load the QML file
qml_file = os.path.join(os.path.dirname(__file__), "maingui.qml")
view.setSource(QUrl.fromLocalFile(os.path.abspath(qml_file)))
# load the slots into the QML file
view.rootContext().setContextProperty("itt1value", 0)
t = threading.Thread(target=receivedata, args=(view,))
t.start()
# Show the window
if view.status() == QQuickView.Error:
sys.exit(-1)
view.show()
# execute and cleanup
app.exec_()
del view
In the threaded method receivedata() I get the data from UDP, process it, then send it to the gauge like so:
view.rootContext().setContextProperty("itt1value", itt)
receivedata() has a while loop in it with the above details, but the gauge only actually updates once. If I put a statement in the QML file to display itt1value, it always has the correct value, so do I need to put in a method to detect the change to this value and re-paint the gauge?
Edit: I was asked for the details of receivedata(), so I have attached it here:
def receivedata(view):
print("Starting UDP server...")
UDP_IP = "192.168.0.14"
UDP_PORT = 49000
sock = socket.socket(socket.AF_INET, # Internet
socket.SOCK_DGRAM) # UDP
sock.bind((UDP_IP, UDP_PORT))
olditt = 0
loopruns = 0 # for debugging
while True:
rawstring = sock.recv(1024)
hexarray = []
#lots of irrelevent formatting here, result is int(value)
itt = float(hextoint(value, olditt))
olditt = itt
itt = format(itt, '.3f')
current = str(loopruns) # for debugging
view.setTitle(current) # for debugging
view.rootContext().setContextProperty("itt1value", itt)
loopruns = loopruns + 1
print(itt)
You have the following errors:
You cannot directly modify the GUI from another thread.
A value can be exported again with setContextProperty(), this will not change the previous value unless the QML is reloaded.
If you want "itt" to modify any value in QML it must be of compatible types, in this case the value of CircularGauge is "real" and therefore the type of data supported in python is float.
Considering the above, I have created a QObject since it can notify changes through signals since it is thread-safe, and export the QObject making connections using Connections.
main.py
import os
import random
import sys
import threading
import time
from PySide2.QtCore import QObject, QUrl, Signal
from PySide2.QtGui import QGuiApplication
from PySide2.QtQuick import QQuickView
class Connections(QObject):
titleChanged = Signal(str, arguments=["title"])
valueChanged = Signal(float, arguments=["value"])
def receivedata(connector):
# configurations
loopruns = 0
while True:
# other stuff
time.sleep(0.1)
itt = random.uniform(0.0, 1200.0)
connector.valueChanged.emit(itt)
connector.titleChanged.emit(str(loopruns))
loopruns += 1
def main(args):
app = QGuiApplication(args)
view = QQuickView(title="my title", resizeMode=QQuickView.SizeRootObjectToView)
connector = Connections()
connector.titleChanged.connect(view.setTitle)
view.rootContext().setContextProperty("connector", connector)
# Load the QML file
qml_file = os.path.join(os.path.dirname(__file__), "maingui.qml")
view.setSource(QUrl.fromLocalFile(os.path.abspath(qml_file)))
# start thread
threading.Thread(target=receivedata, args=(connector,)).start()
# Show the window
if view.status() == QQuickView.Error:
return -1
view.show()
# execute and cleanup
ret = app.exec_()
del view
return ret
if __name__ == "__main__":
sys.exit(main(sys.argv))
maingui.qml
import QtQml 2.13
import QtQuick.Extras 1.4
import QtQuick.Controls.Styles 1.4
CircularGauge {
id: circularGauge
value: 100
minimumValue: 0
maximumValue: 1200
tickmarksVisible: false
style: CircularGaugeStyle {
maximumValueAngle: 400
minimumValueAngle: 90
}
Connections{
target: connector
onValueChanged: circularGauge.value = value
}
}

Read and plot real time live self updating csv file

So, I have a .csv file which updates itself. I would like to do some things with it and am not sure how to approach it, hope you can help me.
The data in the csv looks like this:
There is no headers. I can join the date and time to be in same column without a delimiter too.
07/12/2017,23:50,113.179,113.182,113.168,113.180,113.187,113.189,113.176,113.186,144
07/12/2017,23:51,113.180,113.190,113.180,113.187,113.186,113.196,113.186,113.193,175
07/12/2017,23:52,113.187,113.188,113.174,113.186,113.193,113.194,113.181,113.192,340
07/12/2017,23:53,113.186,113.192,113.175,113.181,113.192,113.199,113.182,113.188,282
07/12/2017,23:54,113.181,113.183,113.170,113.171,113.188,113.188,113.176,113.179,74
07/12/2017,23:55,113.171,113.181,113.170,113.179,113.179,113.188,113.176,113.186,329
07/12/2017,23:56,113.179,113.189,113.174,113.181,113.186,113.195,113.181,113.187,148
07/12/2017,23:57,113.181,113.181,113.169,113.169,113.187,113.187,113.175,113.175,55
07/12/2017,23:58,113.169,113.183,113.169,113.182,113.175,113.188,113.175,113.187,246
07/12/2017,23:59,113.182,113.210,113.175,113.203,113.187,113.215,113.181,113.209,378
08/12/2017,00:00,113.203,113.213,113.180,113.183,113.209,113.220,113.187,113.190,651
08/12/2017,00:01,113.183,113.190,113.164,113.167,113.190,113.196,113.171,113.174,333
08/12/2017,00:02,113.167,113.182,113.156,113.156,113.174,113.188,113.162,113.163,265
08/12/2017,00:03,113.156,113.165,113.151,113.163,113.163,113.172,113.158,113.170,222
08/12/2017,00:04,113.163,113.163,113.154,113.159,113.170,113.170,113.159,113.166,148
08/12/2017,00:05,113.159,113.163,113.153,113.154,113.166,113.168,113.159,113.162,162
For starters I would be interested in using just the first two (or 3 if date and time are separate) columns for this exercise. So for example:
07/12/2017,21:54,113.098
07/12/2017,21:55,113.096
07/12/2017,21:56,113.087
07/12/2017,21:57,113.075
07/12/2017,21:58,113.087
07/12/2017,21:59,113.079
New rows are being added with more recent date time every second or so.
I can do something like
df = pd.read_csv("C:\\Users\\xxx\\Desktop\\csvexport\\thefile.csv")
print(df[-1:])
To see the last row (tail) from the dataframe
Now, I can't see how to do the following and appreciate your help:
Update the dataframe so that I have the most recent version up to date available to make calculations on when new rows appear (without using sleep timer?)
Be able to plot the data with the newly updating data being reflected in the plot automatically as new data arrives (datetime on x axis, float on y)
The output I see in the command window from the program generating the .csv file is like this, if that matters
asset 08/12/2017 05:16:37 float:113.336 floattwo:113.328 digit:20
asset 08/12/2017 05:16:40 float:113.334 floattwo:113.328 digit:21
asset 08/12/2017 05:16:40 float:113.335 floattwo:113.323 digit:22
asset 08/12/2017 05:16:41 float:113.331 floattwo:113.328 digit:23
asset 08/12/2017 05:16:43 float:113.334 floattwo:113.327 digit:24
asset 08/12/2017 05:16:47 float:113.332 floattwo:113.328 digit:25
So you can see the updates are not exactly one second apart, they can have gaps, and can sometimes occur within the same second too (05:16:40 twice)
Therefore, what I would like to happen is keep the plot at equal time intervals actually (1 minute, or 5 minutes, etc) but keep changing the most recent point according to the float vlaue in the .csv belonging to that minute. When a row with the next minute arrives, only then should the plot move to the right (but constantly fluctuate in value as the float number is changing)... Hope you get the idea. I would like to use pyqtgraph for the plot.
I managed to code this much... but it is not the greatest example, excuse me. Of course the plot is not meant to look like this. Just illustrating what I would like to see. So the green bar should be changing value constantly until the next time step is added to the csv
import pyqtgraph as pg
from pyqtgraph import QtCore, QtGui
import pandas as pd
import datetime
x = pd.read_csv("C:\\Users\\xxx\\Desktop\\csvexport\\thefile.csv")
z = x[-1:]
def getlastrow():
for a in z.iterrows():
d = ((int(((a[1][0]).split("/")[0]))))
m = ((int(((a[1][0]).split("/")[1]))))
y = ((int(((a[1][0]).split("/")[2]))))
hh = ((int(((a[1][1]).split(":")[0]))))
mm = ((int(((a[1][1]).split(":")[1]))))
#ss = ((int(((a[1][1]).split(":")[2]))))
thedate = datetime.date(y, m, d)
thetime = datetime.time(hh, mm)
p = (a[1][2])
return ((thedate,thetime,p))
# print(str(getlastrow()[0]).replace("-",""))
# print(getlastrow()[1])
# print(getlastrow()[2])
class CandlestickItem(pg.GraphicsObject):
def __init__(self):
pg.GraphicsObject.__init__(self)
self.flagHasData = False
def set_data(self, data):
self.data = data
self.flagHasData = True
self.generatePicture()
self.informViewBoundsChanged()
def generatePicture(self):
self.picture = QtGui.QPicture()
p = QtGui.QPainter(self.picture)
p.setPen(pg.mkPen('w'))
w = (self.data[1][0] - self.data[0][0]) / 2.
for (t, open) in self.data:
p.drawLine(QtCore.QPointF(t, open), QtCore.QPointF(t, open))
p.setBrush(pg.mkBrush('r'))
if open > 122.8:
p.setBrush(pg.mkBrush('g'))
p.drawRect(QtCore.QRectF(t-w, open, w*2, open))
p.end()
def paint(self, p, *args):
if self.flagHasData:
p.drawPicture(0, 0, self.picture)
def boundingRect(self):
return QtCore.QRectF(self.picture.boundingRect())
app = QtGui.QApplication([])
data = [
[(int(str(getlastrow()[0]).replace("-",""))), (getlastrow()[2])],
[(int(str(getlastrow()[0]).replace("-","")))+1, (getlastrow()[2])+0.1],
[(int(str(getlastrow()[0]).replace("-","")))+2, (getlastrow()[2])+0.2],
]
item = CandlestickItem()
item.set_data(data)
plt = pg.plot()
plt.addItem(item)
plt.setWindowTitle('pyqtgraph example: customGraphicsItem')
def update():
global item, data
new_bar = (int(str(getlastrow()[0]).replace("-","")))+3, ((getlastrow()[2])+10)
data.append(new_bar)
item.set_data(data)
app.processEvents()
timer = QtCore.QTimer()
timer.timeout.connect(update)
timer.start(100)
if __name__ == '__main__':
import sys
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
QtGui.QApplication.instance().exec_()
Hopefully the code below will help with point(1). I realise this is a partial answer. I tested using Linux. The code should be OS agnostic, but I have not tested this.
The code monitors the directory defined in TEST_DIR using the watchdog library. If the file defined in TEST_FILE is changed, then a message is sent from the event handling class called MyHandler to the main function. I put in some ugly time checking as each time a file is altered, multiple events are triggered. So only a single dispatch will be triggered for events occurring within THRESHOLD time. I set this to 0.01 s.
Add code to the dispatcher_receiver function to read in the updated file.
import ntpath
# pip3 install pydispatcher --user
from pydispatch import dispatcher
import sys
import time
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
MYHANDLER_SENDER = 'myhandler_sender'
MYHANDLER_SIGNAL = 'myhandler_signal'
TEST_FILE = 'test_data.csv'
TEST_DIR = '/home/bill/data/documents/infolab2/progs/jupyter_notebooks/pyqtgraph/test_data/'
THRESHOLD_TIME = 0.01
class MyHandler(FileSystemEventHandler):
''' handle events from the file system '''
def __init__(self):
self.start_time = time.time()
def on_modified(self, event):
now_time = time.time()
# filter out multiple modified events occuring for a single file operation
if (now_time - self.start_time) < THRESHOLD_TIME:
print('repeated event, not triggering')
return
changed_file = ntpath.basename(event.src_path)
if changed_file == TEST_FILE:
print('changed file: {}'.format(changed_file))
print('event type: {}'.format(event.event_type))
print('do something...')
# print(event)
message = '{} changed'.format(changed_file)
dispatcher.send(message=message, signal=MYHANDLER_SIGNAL, sender=MYHANDLER_SENDER)
self.start_time = now_time
def main():
dispatcher.connect(dispatcher_receive, signal=MYHANDLER_SIGNAL, sender=MYHANDLER_SENDER)
observer = Observer()
observer.schedule(event_handler, path=TEST_DIR, recursive=False)
observer.start()
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
observer.stop()
observer.join()
def dispatcher_receive(message):
print('received dispatch: {}'.format(message))
# read in the altered file
if __name__ == "__main__":
event_handler = MyHandler()
main()

Trying to control a GNUradio flow graph object

Good Day:
I am trying to write some python code to control a gnuradio block. To illustrate the issue I encountered, I have created a simple flowgraph consisting of an audio source connected to the sound card. There is a single gnuradio companion WX GUI element (a variable slider) to control the audio frequency. I tried to take the python code created by gnuradio companion and create an object within python, then create two threads. One thread starts the GNUradio object, the second thread queries the user to input a frequency, then queries the object's frequency and prints it to the terminal for confirmation.
When the code is run, the audio generator starts, the WX GUI slider is shown, and the terminal prompts the user to input a frequency. When the frequency is input via the terminal query, that number is echoed back as expected but the GNUradio block does not change its frequency. Changing the frequency via the WX GUI slider works as expected.
Obviously I'm not linking the variable to the GNUradio block correctly. The code is copied below, any assistance would be appreciated. Thank you.
-Ed
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
##################################################
# GNU Radio Python Flow Graph
# Title: Top Block
# Generated: Wed Oct 4 06:04:54 2017
##################################################
if __name__ == '__main__':
import ctypes
import sys
if sys.platform.startswith('linux'):
try:
x11 = ctypes.cdll.LoadLibrary('libX11.so')
x11.XInitThreads()
except:
print "Warning: failed to XInitThreads()"
from gnuradio import analog
from gnuradio import audio
from gnuradio import eng_notation
from gnuradio import gr
from gnuradio.eng_option import eng_option
from gnuradio.filter import firdes
from gnuradio.wxgui import forms
from grc_gnuradio import wxgui as grc_wxgui
from optparse import OptionParser
import wx
import threading
from threading import Thread
class top_block(grc_wxgui.top_block_gui):
def __init__(self):
grc_wxgui.top_block_gui.__init__(self, title="Top Block")
_icon_path = "/usr/share/icons/hicolor/32x32/apps/gnuradio-grc.png"
self.SetIcon(wx.Icon(_icon_path, wx.BITMAP_TYPE_ANY))
##################################################
# Variables
##################################################
self.samp_rate = samp_rate = 32000
self.freq = freq = 1000
##################################################
# Blocks
##################################################
_freq_sizer = wx.BoxSizer(wx.VERTICAL)
self._freq_text_box = forms.text_box(
parent=self.GetWin(),
sizer=_freq_sizer,
value=self.freq,
callback=self.set_freq,
label="frequency",
converter=forms.float_converter(),
proportion=0,
)
self._freq_slider = forms.slider(
parent=self.GetWin(),
sizer=_freq_sizer,
value=self.freq,
callback=self.set_freq,
minimum=300,
maximum=5000,
num_steps=100,
style=wx.SL_HORIZONTAL,
cast=float,
proportion=1,
)
self.Add(_freq_sizer)
self.audio_sink_0 = audio.sink(samp_rate, "", True)
self.analog_sig_source_x_0 = analog.sig_source_f(samp_rate, analog.GR_COS_WAVE, freq, .25, 0)
##################################################
# Connections
##################################################
self.connect((self.analog_sig_source_x_0, 0), (self.audio_sink_0, 0))
def get_samp_rate(self):
return self.samp_rate
def set_samp_rate(self, samp_rate):
self.samp_rate = samp_rate
self.analog_sig_source_x_0.set_sampling_freq(self.samp_rate)
def get_freq(self):
return self.freq
def set_freq(self, freq):
self.freq = freq
self.analog_sig_source_x_0.set_frequency(self.freq)
self._freq_slider.set_value(self.freq)
self._freq_text_box.set_value(self.freq)
toneGen = top_block()
def runToneGen():
toneGen.Start(True)
toneGen.Wait()
def userInput():
while True:
freq = raw_input("Enter frequency: ")
toneGen.freq = freq
print "tone generator freq set to: ",toneGen.freq," Hz"
#initiate thread to query user for freq
uiThread = Thread(target=userInput, args=())
uiThread.start()
#initiate thread to run gnuradio block
gnuThread = Thread(target=runToneGen, args=())
gnuThread.start()
I believe I have found the answer to my own question. In the code sample above, in the 'userInput()' definition, I was trying to change the frequency of the tone generator block by directly setting the variable 'toneGen.freq'. This did not work. In order to correctly set the frequency the method 'set_freq' in the toneGen object must be used with this syntax:
toneGen.set_freq(int(freq))
this function call will correctly set the audio frequency of the generator and update the UI slider.

Displaying images like a presentation without showing the desktop during update

I'm trying to display images that are stored under ./pics directory in a row like a presentation on my secondary monitor using the following Python code under Xubuntu 14.04.
from PyQt4 import QtCore, QtGui
import sys
import os
import multiprocessing as mp
import time
def displayImage( impath ):
app = QtGui.QApplication(sys.argv)
window = QtGui.QMainWindow()
# Select monitor
screenres = QtGui.QApplication.desktop().screenGeometry(1);
window.move(QtCore.QPoint(screenres.x(), screenres.y()))
window.resize(screenres.width(), screenres.height())
# Set the image to be shown
pic = QtGui.QLabel(window)
pic.setGeometry(0, 0, 1960, 1080)
pic.setPixmap(QtGui.QPixmap( impath ))
window.showFullScreen()
app.exec_()
return
def controller( imdir ):
# List the contents of the directory
im_paths = os.listdir( imdir )
# Keep only image files
pc = list(im_paths)
for p in pc:
print p
if not p.split('.')[-1] in [ 'jpg', 'JPG', 'png', 'PNG' ]:
im_paths.remove( p )
if not im_paths:
return 1
# Start showing the images by calling project function as a new process (multiprocessing module)
for f in im_paths:
print 'Showing image:', p, '...'
# Run project process
p = mp.Process( target=displayImage, args=( imdir+f, ) )
p.start()
# Keep dispaying image for 3 seconds
time.sleep(2)
p.terminate()
return 0
if __name__ == '__main__':
controller( './pics/' )
exit(0)
The issue
There is a time interval when the process that displays the image A is terminated and until the process
that displays the image B to come up where the application displays nothing and for a moment the desktop is presented destroying the user experience.
Any ideas how to display images using Qt4 continuously?
PS. Answers that involve matplotlib and/or opencv may be accepted but I think that the straight forward path is through Qt.
Finally, I figured out the solution. The alexblae's idea that the qt application should not be restarted on each image update was the key point. But first... I had to understand in some detail the architecture of the Qt framework, how the one class inherits from another and how events are generated and handled.
Let me quote the updated code in order other users to review and get advantage when facing similar problems.
from PyQt4 import QtCore, QtGui
import multiprocessing as mp
import sys
import os
import time
class Pic( QtGui.QLabel ):
def __init__( self, w, im_paths ):
QtGui.QLabel.__init__(self,w)
self.ims = im_paths
# Set the first image that will be displayed
self.setPixmap(QtGui.QPixmap( self.ims[0] ) )
self.i = 1
# Catch timer events. On each event display the next image
def timerEvent(self):
# Update the displayed image
self.setPixmap(QtGui.QPixmap( self.ims[self.i] ) )
self.i = ( self.i + 1 ) % len( self.ims )
def displayImages( im_paths ):
app = QtGui.QApplication(sys.argv)
window = QtGui.QMainWindow()
screenres = QtGui.QApplication.desktop().screenGeometry(1);
window.move(QtCore.QPoint(screenres.x(), screenres.y()))
window.resize(screenres.width(), screenres.height())
pic = Pic(window, im_paths)
pic.setGeometry(0, 0, 1960, 1080)
window.showFullScreen()
# Generate a timerEvent every 1 second. On each second the handler of
# the event updates the displayed image
timer = QtCore.QTimer()
timer.timeout.connect(pic.timerEvent)
timer.start(1000) #1 second
sys.exit(app.exec_())
def controller( imdir ):
# List the contents of the directory
im_paths = sorted( os.listdir( imdir ) )
for i, path in enumerate( im_paths ):
im_paths[i] = imdir + '/' + path
# Keep only image files
pc = list(im_paths)
for p in pc:
if not p.split('.')[-1] in [ 'jpg', 'JPG', 'png', 'PNG' ]:
im_paths.remove( p )
if not im_paths:
return 1
# Run project process
p = mp.Process( target=displayImages, args=( im_paths, ) )
p.start()
# Keep displaying images for 1 minute
time.sleep(5)
p.terminate()
return 0
if __name__ == '__main__':
controller( './pics/' )
exit(0)
Pic class inherits from QLabel in order the timerEvent method to be defined. Using timer events we are able to inform the qt app when to update the displayed image.
Also, let me note that the usage of the multiprocessing module seems to be a bit overkill. Someone may call the displayImages function directly.
I have a solution for this using OpenCV with python.
Here is the code along with the explanation:
import cv2 # for read/display images
import glob # for linking to image file (.jpg)
#--- following is the directory containing my images of jpg format ---
jpg_files=glob.glob("C:/Users/Desktop/Stack/*.jpg")
#--- Here I run through every image file, read it and then display it with a lapse time of 1 second ---
for img_no in range(len(jpg_files)):
x = jpg_files[img_no]
img = cv2.imread(x,1)
cv2.imshow('BINGO.jpg',img)
cv2.waitKey(1000)
cv2.destroyAllWindows()
Hope this helps
:D

How to create video thumbnails with Python and Gstreamer

I'd like to create thumbnails for MPEG-4 AVC videos using Gstreamer and Python. Essentially:
Open the video file
Seek to a certain point in time (e.g. 5 seconds)
Grab the frame at that time
Save the frame to disc as a .jpg file
I've been looking at this other similar question, but I cannot quite figure out how to do the seek and frame capture automatically without user input.
So in summary, how can I capture a video thumbnail with Gstreamer and Python as per the steps above?
To elaborate on ensonic's answer, here's an example:
import os
import sys
import gst
def get_frame(path, offset=5, caps=gst.Caps('image/png')):
pipeline = gst.parse_launch('playbin2')
pipeline.props.uri = 'file://' + os.path.abspath(path)
pipeline.props.audio_sink = gst.element_factory_make('fakesink')
pipeline.props.video_sink = gst.element_factory_make('fakesink')
pipeline.set_state(gst.STATE_PAUSED)
# Wait for state change to finish.
pipeline.get_state()
assert pipeline.seek_simple(
gst.FORMAT_TIME, gst.SEEK_FLAG_FLUSH, offset * gst.SECOND)
# Wait for seek to finish.
pipeline.get_state()
buffer = pipeline.emit('convert-frame', caps)
pipeline.set_state(gst.STATE_NULL)
return buffer
def main():
buf = get_frame(sys.argv[1])
with file('frame.png', 'w') as fh:
fh.write(str(buf))
if __name__ == '__main__':
main()
This generates a PNG image. You can get raw image data using gst.Caps("video/x-raw-rgb,bpp=24,depth=24") or something like that.
Note that in GStreamer 1.0 (as opposed to 0.10), playbin2 has been renamed to playbin and the convert-frame signal is named convert-sample.
The mechanics of seeking are explained in this chapter of the GStreamer Application Development Manual. The 0.10 playbin2 documentation no longer seems to be online, but the documentation for 1.0 is here.
An example in Vala, with GStreamer 1.0 :
var playbin = Gst.ElementFactory.make ("playbin", null);
playbin.set ("uri", "file:///path/to/file");
// some code here.
var caps = Gst.Caps.from_string("image/png");
Gst.Sample sample;
Signal.emit_by_name(playbin, "convert-sample", caps, out sample);
if(sample == null)
return;
var sample_caps = sample.get_caps ();
if(sample_caps == null)
return;
unowned Gst.Structure structure = sample_caps.get_structure(0);
int width = (int)structure.get_value ("width");
int height = (int)structure.get_value ("height");
var memory = sample.get_buffer().get_memory (0);
Gst.MapInfo info;
memory.map (out info, Gst.MapFlags.READ);
uint8[] data = info.data;
It's an old question but I still haven't found it documented anywhere.
I found that the following worked on a playing video with Gstreamer 1.0
import gi
import time
gi.require_version('Gst', '1.0')
from gi.repository import Gst
def get_frame():
caps = Gst.Caps('image/png')
pipeline = Gst.ElementFactory.make("playbin", "playbin")
pipeline.set_property('uri','file:///home/rolf/GWPE.mp4')
pipeline.set_state(Gst.State.PLAYING)
#Allow time for it to start
time.sleep(0.5)
# jump 30 seconds
seek_time = 30 * Gst.SECOND
pipeline.seek(1.0, Gst.Format.TIME,(Gst.SeekFlags.FLUSH | Gst.SeekFlags.ACCURATE),Gst.SeekType.SET, seek_time , Gst.SeekType.NONE, -1)
#Allow video to run to prove it's working, then take snapshot
time.sleep(1)
buffer = pipeline.emit('convert-sample', caps)
buff = buffer.get_buffer()
result, map = buff.map(Gst.MapFlags.READ)
if result:
data = map.data
pipeline.set_state(Gst.State.NULL)
return data
else:
return
if __name__ == '__main__':
Gst.init(None)
image = get_frame()
with open('frame.png', 'wb') as snapshot:
snapshot.write(image)
The code should run with both Python2 and Python3, I hope it helps someone.
Use playbin2. set the uri to the media file, use gst_element_seek_simple to seek to the desired time position and then use g_signal_emit to invoke the "convert-frame" action signal.

Categories