Related
Hope you all are well!
Spent the last couple weeks researching image processing for my tkinter application and came up with this script:
import contextlib
import tkinter as tk
from PIL import Image, ImageTk, ImageSequence
import requests
from itertools import cycle
class ImageLabel(tk.Label):
"""
A Label that displays images, and plays them if they are gifs
:im: A PIL Image instance or a string filename
"""
def load(self, url, width, height=None):
request = requests.get(url, stream=True).raw
im = Image.open(request)
if (height != None):
size = (width, height)
else:
size = (width, get_relative_height(im, width))
try:
self.delay = im.info['duration']
except Exception:
self.delay = 100
global frames_complete
frames_complete = False
self.frames_chunk = cycle(process_frames(im, size))
if frames_complete:
self.next_frame()
def next_frame(self):
self.config(image=next(self.frames_chunk))
self.after(self.delay, self.next_frame)
def unload(self):
self.destroy()
def get_relative_height(source, mywidth):
_, height = source.size
wpercent = (mywidth/float(height))
return int((float(height)*float(wpercent)))
def process_frames(im, size): # resize and arrange gifs
frames_chunk = []
mode = analyseImage(im)["mode"]
last_frame = im.convert("RGBA")
for i, frame in enumerate(ImageSequence.Iterator(im)):
frame_image = Image.new("RGBA", frame.size)
if mode == "partial":
frame_image.paste(last_frame)
print(f'Processing frame {i}')
frame_image.paste(frame, (0, 0), frame.convert("RGBA"))
frame_image.thumbnail(size, Image.BICUBIC)
new_frame = ImageTk.PhotoImage(frame_image)
frames_chunk.append(new_frame)
print("appended frame to frames_chunk")
print("frames completed")
global frames_complete
frames_complete = True
return frames_chunk
def analyseImage(im):
"""
Pre-process pass over the image to determine the mode (full or additive).
Necessary as assessing single frames isn't reliable. Need to know the mode
before processing all frames.ll
"""
results = {
"size": im.size,
"mode": "full",
}
with contextlib.suppress(EOFError):
while True:
if im.tile:
tile = im.tile[0]
update_region = tile[1]
update_region_dimensions = update_region[2:]
if update_region_dimensions != im.size:
results["mode"] = "partial"
break
im.seek(im.tell() + 1)
return results
# test:
root = tk.Tk()
lbl = ImageLabel(root)
lbl.pack()
lbl.load("https://www.saic.edu/~anelso13/gif/images/cat14.gif", 300)
root.mainloop()
running this from inside a tkinter app slows the app down and also freezes the GUI until the frames are finished processing.
This class works alright when it's alone, but there are two major issues I've been struggling to solve,
The process_frames function itterates frame by frame and is very slow. in the app I'm working on I instance this class two times and it takes about 10 seconds to process and resize every frame. I ran the function inside a thread but it didn't seem to improve speed whatsoever.
2: The Main tkinter application freezes until both sets of frames process. I've looked at a few resources and tried a few implementations (tkinter: preventing main loop from freezing) and here.
I have a thread running in the program already which does work as expected but using the same method for processing the frames does not work.
Any and all help is greatly appreciated!
is there any way to embed a HTML Video File in an tkinter window ?
I would be pleased for all kind of input!
Thanks a lot
One of solution is to use cv2 to read video frame-by-frame and replace frame on tk.Label or tk.Canvas - and you will see video.
(And cv2 uses ffmpeg for this)
cv2 can read from video file, local webcam or from remote stream (HTTP or RTMP)
Because it works with frames so you can draw some text or figures on frame before displaying - this way you can add some buttons or descriptions. You may also make modifications - crop, flip, convert to gray, etc.
But it has one problem: cv2 doesn't work with audio - so it can't play audio from file.
import tkinter as tk
from PIL import Image, ImageTk
import cv2
# --- functions ---
def update_frame():
ret, frame = video.read()
if ret: # check status - because sometimes it may have problem to read frame
# cv2 keeps image as `BGR` and it needs to convert to `RGB`
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# resize to tkinter's window
frame = cv2.resize(frame, (800, 600))
image = Image.fromarray(frame) # convert numpy.array to PIL.Image
photo.paste(image) # copy image on photo
# update again after some time (in milliseconds) (ie. 1000ms/25fps = 40ms)
root.after(int(1000/fps), update_frame)
# --- main ---
#video = cv2.VideoCapture(0) # local webcam
video = cv2.VideoCapture(BigBuckBunny.mp4")
#w = video.get(cv2.CAP_PROP_FRAME_WIDTH)
#h = video.get(cv2.CAP_PROP_FRAME_HEIGHT)
fps = video.get(cv2.CAP_PROP_FPS)
# get first frame to create photo
ret, frame = video.read()
# cv2 keeps image as `BGR` and it needs to convert to `RGB`
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# resize to tkinter's window
frame = cv2.resize(frame, (800, 600))
image = Image.fromarray(frame) # convert numpy.array to PIL.Image
# - GUI -
root = tk.Tk()
root.geometry('800x600')
photo = ImageTk.PhotoImage(image) # it has to be after `tk.Tk()`
canvas = tk.Canvas(root, width=photo.width(), height=photo.height())
canvas.pack(fill='both', expand=True)
image_id = canvas.create_image((0,0), image=photo, anchor='nw')
update_frame() # update it first time
root.mainloop() # run loop all time - it shows window
# - after close window -
# close stream
video.release()
Using Google I found module tkVideo.
It uses imageio to read frame-by-frame. (And imageio uses ffmpeg for this).
And it use threading instead of root.after().
It has the same problem: iamgeio doesn't work with audio - so it can't play audio from file.
EDIT:
Other method is to embed some Video Player like VLC, MPlayer, etc.. And it should gives video with audio but it doesn't allow to draw elements
I found example code in documentation for video player VLC but it seems complex because it adds also buttons to start/stop/pause video.
http://git.videolan.org/?p=vlc/bindings/python.git;a=blob;f=examples/tkvlc.py;h=55314cab09948fc2b7c84f14a76c6d1a7cbba127;hb=HEAD
#! /usr/bin/python
# -*- coding: utf-8 -*-
# tkinter example for VLC Python bindings
# Copyright (C) 2015 the VideoLAN team
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
#
"""A simple example for VLC python bindings using tkinter.
Requires Python 3.4 or later.
Author: Patrick Fay
Date: 23-09-2015
"""
# Tested with Python 3.7.4, tkinter/Tk 8.6.9 on macOS 10.13.6 only.
__version__ = '20.05.04' # mrJean1 at Gmail
# import external libraries
import vlc
# import standard libraries
import sys
if sys.version_info[0] < 3:
import Tkinter as Tk
from Tkinter import ttk
from Tkinter.filedialog import askopenfilename
from Tkinter.tkMessageBox import showerror
else:
import tkinter as Tk
from tkinter import ttk
from tkinter.filedialog import askopenfilename
from tkinter.messagebox import showerror
from os.path import basename, expanduser, isfile, join as joined
from pathlib import Path
import time
_isMacOS = sys.platform.startswith('darwin')
_isWindows = sys.platform.startswith('win')
_isLinux = sys.platform.startswith('linux')
if _isMacOS:
from ctypes import c_void_p, cdll
# libtk = cdll.LoadLibrary(ctypes.util.find_library('tk'))
# returns the tk library /usr/lib/libtk.dylib from macOS,
# but we need the tkX.Y library bundled with Python 3+,
# to match the version number of tkinter, _tkinter, etc.
try:
libtk = 'libtk%s.dylib' % (Tk.TkVersion,)
prefix = getattr(sys, 'base_prefix', sys.prefix)
libtk = joined(prefix, 'lib', libtk)
dylib = cdll.LoadLibrary(libtk)
# getNSView = dylib.TkMacOSXDrawableView is the
# proper function to call, but that is non-public
# (in Tk source file macosx/TkMacOSXSubwindows.c)
# and dylib.TkMacOSXGetRootControl happens to call
# dylib.TkMacOSXDrawableView and return the NSView
_GetNSView = dylib.TkMacOSXGetRootControl
# C signature: void *_GetNSView(void *drawable) to get
# the Cocoa/Obj-C NSWindow.contentView attribute, the
# drawable NSView object of the (drawable) NSWindow
_GetNSView.restype = c_void_p
_GetNSView.argtypes = c_void_p,
del dylib
except (NameError, OSError): # image or symbol not found
def _GetNSView(unused):
return None
libtk = "N/A"
C_Key = "Command-" # shortcut key modifier
else: # *nix, Xwindows and Windows, UNTESTED
libtk = "N/A"
C_Key = "Control-" # shortcut key modifier
class _Tk_Menu(Tk.Menu):
'''Tk.Menu extended with .add_shortcut method.
Note, this is a kludge just to get Command-key shortcuts to
work on macOS. Other modifiers like Ctrl-, Shift- and Option-
are not handled in this code.
'''
_shortcuts_entries = {}
_shortcuts_widget = None
def add_shortcut(self, label='', key='', command=None, **kwds):
'''Like Tk.menu.add_command extended with shortcut key.
If needed use modifiers like Shift- and Alt_ or Option-
as before the shortcut key character. Do not include
the Command- or Control- modifier nor the <...> brackets
since those are handled here, depending on platform and
as needed for the binding.
'''
# <https://TkDocs.com/tutorial/menus.html>
if not key:
self.add_command(label=label, command=command, **kwds)
elif _isMacOS:
# keys show as upper-case, always
self.add_command(label=label, accelerator='Command-' + key,
command=command, **kwds)
self.bind_shortcut(key, command, label)
else: # XXX not tested, not tested, not tested
self.add_command(label=label, underline=label.lower().index(key),
command=command, **kwds)
self.bind_shortcut(key, command, label)
def bind_shortcut(self, key, command, label=None):
"""Bind shortcut key, default modifier Command/Control.
"""
# The accelerator modifiers on macOS are Command-,
# Ctrl-, Option- and Shift-, but for .bind[_all] use
# <Command-..>, <Ctrl-..>, <Option_..> and <Shift-..>,
# <https://www.Tcl.Tk/man/tcl8.6/TkCmd/bind.htm#M6>
if self._shortcuts_widget:
if C_Key.lower() not in key.lower():
key = "<%s%s>" % (C_Key, key.lstrip('<').rstrip('>'))
self._shortcuts_widget.bind(key, command)
# remember the shortcut key for this menu item
if label is not None:
item = self.index(label)
self._shortcuts_entries[item] = key
# The Tk modifier for macOS' Command key is called
# Meta, but there is only Meta_L[eft], no Meta_R[ight]
# and both keyboard command keys generate Meta_L events.
# Similarly for macOS' Option key, the modifier name is
# Alt and there's only Alt_L[eft], no Alt_R[ight] and
# both keyboard option keys generate Alt_L events. See:
# <https://StackOverflow.com/questions/6378556/multiple-
# key-event-bindings-in-tkinter-control-e-command-apple-e-etc>
def bind_shortcuts_to(self, widget):
'''Set the widget for the shortcut keys, usually root.
'''
self._shortcuts_widget = widget
def entryconfig(self, item, **kwds):
"""Update shortcut key binding if menu entry changed.
"""
Tk.Menu.entryconfig(self, item, **kwds)
# adjust the shortcut key binding also
if self._shortcuts_widget:
key = self._shortcuts_entries.get(item, None)
if key is not None and "command" in kwds:
self._shortcuts_widget.bind(key, kwds["command"])
class Player(Tk.Frame):
"""The main window has to deal with events.
"""
_geometry = ''
_stopped = None
def __init__(self, parent, title=None, video=''):
Tk.Frame.__init__(self, parent)
self.parent = parent # == root
self.parent.title(title or "tkVLCplayer")
self.video = expanduser(video)
# Menu Bar
# File Menu
menubar = Tk.Menu(self.parent)
self.parent.config(menu=menubar)
fileMenu = _Tk_Menu(menubar)
fileMenu.bind_shortcuts_to(parent) # XXX must be root?
fileMenu.add_shortcut("Open...", 'o', self.OnOpen)
fileMenu.add_separator()
fileMenu.add_shortcut("Play", 'p', self.OnPlay) # Play/Pause
fileMenu.add_command(label="Stop", command=self.OnStop)
fileMenu.add_separator()
fileMenu.add_shortcut("Mute", 'm', self.OnMute)
fileMenu.add_separator()
fileMenu.add_shortcut("Close", 'w' if _isMacOS else 's', self.OnClose)
if _isMacOS: # intended for and tested on macOS
fileMenu.add_separator()
fileMenu.add_shortcut("Full Screen", 'f', self.OnFullScreen)
menubar.add_cascade(label="File", menu=fileMenu)
self.fileMenu = fileMenu
self.playIndex = fileMenu.index("Play")
self.muteIndex = fileMenu.index("Mute")
# first, top panel shows video
self.videopanel = ttk.Frame(self.parent)
self.canvas = Tk.Canvas(self.videopanel)
self.canvas.pack(fill=Tk.BOTH, expand=1)
self.videopanel.pack(fill=Tk.BOTH, expand=1)
# panel to hold buttons
self.buttons_panel = Tk.Toplevel(self.parent)
self.buttons_panel.title("")
self.is_buttons_panel_anchor_active = False
buttons = ttk.Frame(self.buttons_panel)
self.playButton = ttk.Button(buttons, text="Play", command=self.OnPlay)
stop = ttk.Button(buttons, text="Stop", command=self.OnStop)
self.muteButton = ttk.Button(buttons, text="Mute", command=self.OnMute)
self.playButton.pack(side=Tk.LEFT)
stop.pack(side=Tk.LEFT)
self.muteButton.pack(side=Tk.LEFT)
self.volMuted = False
self.volVar = Tk.IntVar()
self.volSlider = Tk.Scale(buttons, variable=self.volVar, command=self.OnVolume,
from_=0, to=100, orient=Tk.HORIZONTAL, length=200,
showvalue=0, label='Volume')
self.volSlider.pack(side=Tk.RIGHT)
buttons.pack(side=Tk.BOTTOM, fill=Tk.X)
# panel to hold player time slider
timers = ttk.Frame(self.buttons_panel)
self.timeVar = Tk.DoubleVar()
self.timeSliderLast = 0
self.timeSlider = Tk.Scale(timers, variable=self.timeVar, command=self.OnTime,
from_=0, to=1000, orient=Tk.HORIZONTAL, length=500,
showvalue=0) # label='Time',
self.timeSlider.pack(side=Tk.BOTTOM, fill=Tk.X, expand=1)
self.timeSliderUpdate = time.time()
timers.pack(side=Tk.BOTTOM, fill=Tk.X)
# VLC player
args = []
if _isLinux:
args.append('--no-xlib')
self.Instance = vlc.Instance(args)
self.player = self.Instance.media_player_new()
self.parent.bind("<Configure>", self.OnConfigure) # catch window resize, etc.
self.parent.update()
# After parent.update() otherwise panel is ignored.
self.buttons_panel.overrideredirect(True)
# Estetic, to keep our video panel at least as wide as our buttons panel.
self.parent.minsize(width=502, height=0)
if _isMacOS:
# Only tested on MacOS so far. Enable for other OS after verified tests.
self.is_buttons_panel_anchor_active = True
# Detect dragging of the buttons panel.
self.buttons_panel.bind("<Button-1>", lambda event: setattr(self, "has_clicked_on_buttons_panel", event.y < 0))
self.buttons_panel.bind("<B1-Motion>", self._DetectButtonsPanelDragging)
self.buttons_panel.bind("<ButtonRelease-1>", lambda _: setattr(self, "has_clicked_on_buttons_panel", False))
self.has_clicked_on_buttons_panel = False
else:
self.is_buttons_panel_anchor_active = False
self._AnchorButtonsPanel()
self.OnTick() # set the timer up
def OnClose(self, *unused):
"""Closes the window and quit.
"""
# print("_quit: bye")
self.parent.quit() # stops mainloop
self.parent.destroy() # this is necessary on Windows to avoid
# ... Fatal Python Error: PyEval_RestoreThread: NULL tstate
def _DetectButtonsPanelDragging(self, _):
"""If our last click was on the boarder
we disable the anchor.
"""
if self.has_clicked_on_buttons_panel:
self.is_buttons_panel_anchor_active = False
self.buttons_panel.unbind("<Button-1>")
self.buttons_panel.unbind("<B1-Motion>")
self.buttons_panel.unbind("<ButtonRelease-1>")
def _AnchorButtonsPanel(self):
video_height = self.parent.winfo_height()
panel_x = self.parent.winfo_x()
panel_y = self.parent.winfo_y() + video_height + 23 # 23 seems to put the panel just below our video.
panel_height = self.buttons_panel.winfo_height()
panel_width = self.parent.winfo_width()
self.buttons_panel.geometry("%sx%s+%s+%s" % (panel_width, panel_height, panel_x, panel_y))
def OnConfigure(self, *unused):
"""Some widget configuration changed.
"""
# <https://www.Tcl.Tk/man/tcl8.6/TkCmd/bind.htm#M12>
self._geometry = '' # force .OnResize in .OnTick, recursive?
if self.is_buttons_panel_anchor_active:
self._AnchorButtonsPanel()
def OnFullScreen(self, *unused):
"""Toggle full screen, macOS only.
"""
# <https://www.Tcl.Tk/man/tcl8.6/TkCmd/wm.htm#M10>
f = not self.parent.attributes("-fullscreen") # or .wm_attributes
if f:
self._previouscreen = self.parent.geometry()
self.parent.attributes("-fullscreen", f) # or .wm_attributes
self.parent.bind("<Escape>", self.OnFullScreen)
else:
self.parent.attributes("-fullscreen", f) # or .wm_attributes
self.parent.geometry(self._previouscreen)
self.parent.unbind("<Escape>")
def OnMute(self, *unused):
"""Mute/Unmute audio.
"""
# audio un/mute may be unreliable, see vlc.py docs.
self.volMuted = m = not self.volMuted # self.player.audio_get_mute()
self.player.audio_set_mute(m)
u = "Unmute" if m else "Mute"
self.fileMenu.entryconfig(self.muteIndex, label=u)
self.muteButton.config(text=u)
# update the volume slider text
self.OnVolume()
def OnOpen(self, *unused):
"""Pop up a new dialow window to choose a file, then play the selected file.
"""
# if a file is already running, then stop it.
self.OnStop()
# Create a file dialog opened in the current home directory, where
# you can display all kind of files, having as title "Choose a video".
video = askopenfilename(initialdir = Path(expanduser("~")),
title = "Choose a video",
filetypes = (("all files", "*.*"),
("mp4 files", "*.mp4"),
("mov files", "*.mov")))
self._Play(video)
def _Pause_Play(self, playing):
# re-label menu item and button, adjust callbacks
p = 'Pause' if playing else 'Play'
c = self.OnPlay if playing is None else self.OnPause
self.fileMenu.entryconfig(self.playIndex, label=p, command=c)
# self.fileMenu.bind_shortcut('p', c) # XXX handled
self.playButton.config(text=p, command=c)
self._stopped = False
def _Play(self, video):
# helper for OnOpen and OnPlay
if isfile(video): # Creation
m = self.Instance.media_new(str(video)) # Path, unicode
self.player.set_media(m)
self.parent.title("tkVLCplayer - %s" % (basename(video),))
# set the window id where to render VLC's video output
h = self.videopanel.winfo_id() # .winfo_visualid()?
if _isWindows:
self.player.set_hwnd(h)
elif _isMacOS:
# XXX 1) using the videopanel.winfo_id() handle
# causes the video to play in the entire panel on
# macOS, covering the buttons, sliders, etc.
# XXX 2) .winfo_id() to return NSView on macOS?
v = _GetNSView(h)
if v:
self.player.set_nsobject(v)
else:
self.player.set_xwindow(h) # plays audio, no video
else:
self.player.set_xwindow(h) # fails on Windows
# FIXME: this should be made cross-platform
self.OnPlay()
def OnPause(self, *unused):
"""Toggle between Pause and Play.
"""
if self.player.get_media():
self._Pause_Play(not self.player.is_playing())
self.player.pause() # toggles
def OnPlay(self, *unused):
"""Play video, if none is loaded, open the dialog window.
"""
# if there's no video to play or playing,
# open a Tk.FileDialog to select a file
if not self.player.get_media():
if self.video:
self._Play(expanduser(self.video))
self.video = ''
else:
self.OnOpen()
# Try to play, if this fails display an error message
elif self.player.play(): # == -1
self.showError("Unable to play the video.")
else:
self._Pause_Play(True)
# set volume slider to audio level
vol = self.player.audio_get_volume()
if vol > 0:
self.volVar.set(vol)
self.volSlider.set(vol)
def OnResize(self, *unused):
"""Adjust the window/frame to the video aspect ratio.
"""
g = self.parent.geometry()
if g != self._geometry and self.player:
u, v = self.player.video_get_size() # often (0, 0)
if v > 0 and u > 0:
# get window size and position
g, x, y = g.split('+')
w, h = g.split('x')
# alternatively, use .winfo_...
# w = self.parent.winfo_width()
# h = self.parent.winfo_height()
# x = self.parent.winfo_x()
# y = self.parent.winfo_y()
# use the video aspect ratio ...
if u > v: # ... for landscape
# adjust the window height
h = round(float(w) * v / u)
else: # ... for portrait
# adjust the window width
w = round(float(h) * u / v)
self.parent.geometry("%sx%s+%s+%s" % (w, h, x, y))
self._geometry = self.parent.geometry() # actual
def OnStop(self, *unused):
"""Stop the player, resets media.
"""
if self.player:
self.player.stop()
self._Pause_Play(None)
# reset the time slider
self.timeSlider.set(0)
self._stopped = True
# XXX on macOS libVLC prints these error messages:
# [h264 # 0x7f84fb061200] get_buffer() failed
# [h264 # 0x7f84fb061200] thread_get_buffer() failed
# [h264 # 0x7f84fb061200] decode_slice_header error
# [h264 # 0x7f84fb061200] no frame!
def OnTick(self):
"""Timer tick, update the time slider to the video time.
"""
if self.player:
# since the self.player.get_length may change while
# playing, re-set the timeSlider to the correct range
t = self.player.get_length() * 1e-3 # to seconds
if t > 0:
self.timeSlider.config(to=t)
t = self.player.get_time() * 1e-3 # to seconds
# don't change slider while user is messing with it
if t > 0 and time.time() > (self.timeSliderUpdate + 2):
self.timeSlider.set(t)
self.timeSliderLast = int(self.timeVar.get())
# start the 1 second timer again
self.parent.after(1000, self.OnTick)
# adjust window to video aspect ratio, done periodically
# on purpose since the player.video_get_size() only
# returns non-zero sizes after playing for a while
if not self._geometry:
self.OnResize()
def OnTime(self, *unused):
if self.player:
t = self.timeVar.get()
if self.timeSliderLast != int(t):
# this is a hack. The timer updates the time slider.
# This change causes this rtn (the 'slider has changed' rtn)
# to be invoked. I can't tell the difference between when
# the user has manually moved the slider and when the timer
# changed the slider. But when the user moves the slider
# tkinter only notifies this rtn about once per second and
# when the slider has quit moving.
# Also, the tkinter notification value has no fractional
# seconds. The timer update rtn saves off the last update
# value (rounded to integer seconds) in timeSliderLast if
# the notification time (sval) is the same as the last saved
# time timeSliderLast then we know that this notification is
# due to the timer changing the slider. Otherwise the
# notification is due to the user changing the slider. If
# the user is changing the slider then I have the timer
# routine wait for at least 2 seconds before it starts
# updating the slider again (so the timer doesn't start
# fighting with the user).
self.player.set_time(int(t * 1e3)) # milliseconds
self.timeSliderUpdate = time.time()
def OnVolume(self, *unused):
"""Volume slider changed, adjust the audio volume.
"""
vol = min(self.volVar.get(), 100)
v_M = "%d%s" % (vol, " (Muted)" if self.volMuted else '')
self.volSlider.config(label="Volume " + v_M)
if self.player and not self._stopped:
# .audio_set_volume returns 0 if success, -1 otherwise,
# e.g. if the player is stopped or doesn't have media
if self.player.audio_set_volume(vol): # and self.player.get_media():
self.showError("Failed to set the volume: %s." % (v_M,))
def showError(self, message):
"""Display a simple error dialog.
"""
self.OnStop()
showerror(self.parent.title(), message)
if __name__ == "__main__":
_video = ''
while len(sys.argv) > 1:
arg = sys.argv.pop(1)
if arg.lower() in ('-v', '--version'):
# show all versions, sample output on macOS:
# % python3 ./tkvlc.py -v
# tkvlc.py: 2019.07.28 (tkinter 8.6 /Library/Frameworks/Python.framework/Versions/3.7/lib/libtk8.6.dylib)
# vlc.py: 3.0.6109 (Sun Mar 31 20:14:16 2019 3.0.6)
# LibVLC version: 3.0.6 Vetinari (0x3000600)
# LibVLC compiler: clang: warning: argument unused during compilation: '-mmacosx-version-min=10.7' [-Wunused-command-line-argument]
# Plugin path: /Applications/VLC3.0.6.app/Contents/MacOS/plugins
# Python: 3.7.4 (64bit) macOS 10.13.6
# Print version of this vlc.py and of the libvlc
print('%s: %s (%s %s %s)' % (basename(__file__), __version__,
Tk.__name__, Tk.TkVersion, libtk))
try:
vlc.print_version()
vlc.print_python()
except AttributeError:
pass
sys.exit(0)
elif arg.startswith('-'):
print('usage: %s [-v | --version] [<video_file_name>]' % (sys.argv[0],))
sys.exit(1)
elif arg: # video file
_video = expanduser(arg)
if not isfile(_video):
print('%s error: no such file: %r' % (sys.argv[0], arg))
sys.exit(1)
# Create a Tk.App() to handle the windowing event loop
root = Tk.Tk()
player = Player(root, video=_video)
root.protocol("WM_DELETE_WINDOW", player.OnClose) # XXX unnecessary (on macOS)
root.mainloop()
I'm completely new at python, learned not much yet. My goal is, to build a slideshow app, which should display images, images with transparent text frames and also videos. Playing a video is my main problem at the moment.
A little test script with static content. Later, all content should be loaded from a web service.
import tkinter as tk
import tkvideo as tv
from PIL import Image
from PIL import ImageTk
import time
# fullscreen window
window = tk.Tk()
window.attributes('-fullscreen', True)
# dimensions
window_width = window.winfo_screenwidth()
window_height = window.winfo_screenheight()
text_width = int(window_width * 0.4)
text_x = int(window_width - text_width)
# transparent rectangle for text
imgs=[]
def create_rectangle(x,y,a,b,**options):
if 'alpha' in options:
# Calculate the alpha transparency for every color(RGB)
alpha = int(options.pop('alpha') * 255)
# Use the fill variable to fill the shape with transparent color
fill = options.pop('fill')
fill = window.winfo_rgb(fill) + (alpha,)
img = Image.new('RGBA', (a-x, b-y), fill)
imgs.append(ImageTk.PhotoImage(img, master=canvas))
canvas.create_image(x, y, image=imgs[-1], anchor='nw')
canvas.create_rectangle(x, y, a, b, **options)
# fullscreen canvas
canvas = tk.Canvas(window, bg="white", bd=0)
canvas.pack(fill=tk.BOTH, expand=True)
# only image slideshow
images = ['quiz1.jpg', 'quiz2.jpg', 'quiz3.jpg', 'quiz4.jpg']
for img in images:
image = Image.open("/home/jpm/Bilder/" + img)
newimage = image.resize((window_width, window_height))
photo = ImageTk.PhotoImage(newimage, master=canvas)
canvas.create_image(0, 0, anchor="nw", image=photo)
canvas.update()
time.sleep(5)
# image with text slideshow
images = ['1658724794aff.jpg', '1658724768kar.jpg']
headlines = ['Headline 1',
'Headline 2']
paragraphs = [['paragraph 1',
'paragraph 2',
'paragraph 3',
'paragraph 4'],
['paragraph 1',
'paragraph 2',
'paragraph 3',
'paragraph 4']]
i=0
for img in images:
image = Image.open("/home/jpm/Bilder/" + img)
newimage = image.resize((window_width, window_height))
photo = ImageTk.PhotoImage(newimage, master=canvas)
canvas.create_image(0, 0, anchor="nw", image=photo)
create_rectangle(text_x, 0, window_width, window_height, fill= "white", alpha=.80, width=0)
head = canvas.create_text(text_x+20, 20, text=headlines[i], fill="#72B765", font=('Helvetica 34 bold'), anchor='nw', width=text_width-20)
canvas.update()
x0, y0, x1, y1 = canvas.bbox(head)
for paragraph in paragraphs[i]:
time.sleep(4)
line = canvas.create_text(text_x+20, y1+10, text=paragraph, fill="#000000", font=('Helvetica 18 bold'), anchor='nw', width=text_width-20)
x0, y0, x1, y1 = canvas.bbox(line)
canvas.update()
canvas.update()
time.sleep(8)
i = i + 1
canvas.destroy()
my_label = tk.Label(window)
my_label.pack(fill='both', expand=True)
player = tv.tkvideo("/home/jpm/Bilder/docsite_promotion.mp4", my_label, loop = 0, size = (window_width,window_height))
player.play()
window.mainloop()
The image and image with text parts running fine. My problem is the video. First of all, it is only displayed at the end of the script. If I put the Label with the video between the two loops or in front of the first, nothing is shown. I also found out, that the program is not waiting till the video ends.
It's all too confusing for me, to explain it better. Maybe I don't understand the flow control of python programs at all. Maybe tkvideo is not the best option. I played also with ffpyplayer, but can't find out, how use it with tkinter.
I only want to display these three types of content in any order and finally also in an endless loop.
Hope you understand me and can give me some tips.
.play() doesn't wait for end of video (it runs code in separated thread) and next image may replace it (hide it) before you see video.
I tried to use sleep() to wait for end of video but it makes problem - it raises error because function doesn't run in main thread.
You would need to use .after(millisecond, my_function) to check periodically if there is end of video and later display images.
If in tkvideo you use self.thread instead of thread then you can check periodically self.thread.is_alive() to catch end of thread. But this need also some method to run next code which will change images. First idea is to create function which changes images and send it as callback to function which check end of video - and it would run this callback() when is_alive() is False. But all this makes problem when you would like to create something more complex.
Here example which uses window.after() to check self.thread.is_alive() and later it runs callback()
I uses tkvideo from my other answer - I added self.running to have method to stop video in any moment.
import time
import tkinter as tk
import threading
from time import perf_counter, sleep
import imageio
from PIL import Image, ImageTk
import os
class tkvideo():
def __init__(self, path, label, loop=False, size=(640,360), hz=0):
self.path = path
self.label = label
self.loop = loop
self.size = size
self.hz = hz
self.running = True # <-- variable to control loop
def load(self, path, label, loop, hz):
"""
Loads the video's frames recursively onto the selected label widget's image parameter.
Loop parameter controls whether the function will run in an infinite loop
or once.
"""
frame_data = imageio.get_reader(path)
if hz > 0:
frame_duration = float(1 / hz)
else:
frame_duration = float(0)
if loop:
while True:
before = perf_counter()
for image in frame_data.iter_data():
frame_image = ImageTk.PhotoImage(Image.fromarray(image).resize(self.size))
if not self.running: # <-- variable to control loop
return # exit function and stop thread
label.config(image=frame_image)
label.image = frame_image
diff = frame_duration + before
after = perf_counter()
diff = diff - after
if diff > 0:
sleep(diff)
before = perf_counter()
print('[load] end of loop')
else:
before = perf_counter()
for image in frame_data.iter_data():
frame_image = ImageTk.PhotoImage(Image.fromarray(image).resize(self.size))
if not self.running: # <-- variable to control loop
return # exit function and stop thread
label.config(image=frame_image)
label.image = frame_image
diff = frame_duration + before
after = perf_counter()
diff = diff - after
if diff > 0:
sleep(diff)
before = perf_counter()
print('[load] end of loop')
def play(self):
"""
Creates and starts a thread as a daemon that plays the video by rapidly going through
the video's frames.
"""
# uses `self.thread` instead of `thread` to have access to `self.thread.is_alive()`
self.thread = threading.Thread(target=self.load, args=(self.path, self.label, self.loop, self.hz))
self.thread.daemon = True
self.thread.start()
def show_video(filename, size, callback=None):
global player
global my_label
fullpath = os.path.join(folder, filename)
my_label = tk.Label(window)
my_label.pack(fill='both', expand=True)
player = tkvideo(fullpath, my_label, loop=False, size=size)
player.play()
# check after 5ms
window.after(5, check_end_video, callback)
def check_end_video(callback):
global my_label
if player.thread.is_alive(): # check if it end of thread
# check again after 5ms
window.after(5, check_end_video, callback)
else:
# remove label
print('The End')
my_label.destroy()
# run next function
if callback:
callback()
def show_image(filename, size, callback=None):
global my_label
fullpath = os.path.join(folder, filename)
frame_image = ImageTk.PhotoImage(Image.open(fullpath).resize(size))
my_label = tk.Label(window)
my_label.pack(fill='both', expand=True)
my_label['image'] = frame_image # show image on label
my_label.image = frame_image # solution for bug in PhotoImage
# check after 5000ms (5s)
window.after(5000, check_end_image, callback)
def check_end_image(callback):
global my_label
# remove label
print('The End')
my_label.destroy()
# run next function
if callback:
callback()
def other_function():
button = tk.Button(window, text='Close', command=window.destroy)
button.pack(fill='both', expand=True, padx=250, pady=250)
# --- main ---
folder = "/home/jpm/Bilder/"
window = tk.Tk()
window.update()
#window.attributes('-fullscreen', True)
#window_width = window.winfo_screenwidth()
#window_height = window.winfo_screenheight()
window.geometry('800x600+800+300')
window_width = 800
window_height = 600
# ---
size = (window_width, window_height)
# run video and it will later run `show_image`
show_video("docsite_promotion.mp4", size,
lambda:show_image('quiz1.jpg', size, other_function))
# ---
window.mainloop()
I'm trying to make a memory game for fun and as a learning experience, and I've run into the issue where even with something like time.sleep(.5) I still can't get buttons to update correctly with a delay. In fact the second button seems to update to hidden as it's about to show the proper image. I'm assuming the issue lies somewhere in the buttonClicked() function.
I'm trying to figure out how I can make it show one button, then the second, then wait half a second and hide both. And if someone understands why this is happening or where I could look into the issue and read up on my own, that would be helpful.
Thanks.
from re import A
import time
import tkinter as tk
from tkinter import *
from typing_extensions import Self
from PIL import Image, ImageTk
import glob
import os, os.path
import numpy as np
from sqlalchemy import null
#resize images and button
imgButtonWidth = 100
imgButtonHeight = 100
imgButtonSize = (imgButtonWidth,imgButtonHeight)
#Set the height and width of the game by number of items.
width = 6
height = 6
#buttons = [[Button]*width]*height
#Total number of items 36 (0-35)
count = width*height-1
buttonList = []
#Will be a 2d array of [button, id]
answersList = []
clickedCount = 0
imgs = []
hiddenImg = null
# Create frame, set default size of frame and background color.
root = Tk()
root.title('Memory Game')
root.geometry(str(imgButtonWidth * (width+1)) + "x" + str(imgButtonHeight * (height+1)))
root.config(bg='darkblue')
frame = Frame(root, bg='darkblue')
# Fetch images from location and create a list of Image objects, then return.
def getImages():
imgs = []
path = "/home/paul/Programming/Python/MyMiniProjects/Mid/MemoryGame/"
valid_images = [".jpg",".gif",".png",".tga"]
for f in os.listdir(path):
ext = os.path.splitext(f)[1]
if ext.lower() not in valid_images:
continue
imgs.append([Image.open(os.path.join(path,f)).resize(imgButtonSize), f])
return imgs + imgs
#Shuffle images for the game
imgs = getImages()
random.shuffle(imgs)
#Simple image to cover the tiles
hiddenImg = ImageTk.PhotoImage(Image.new('RGB', (imgButtonWidth, imgButtonHeight), (0,0,105)))
#Disable buttons after a match
def disable():
global clickedCount, answersList
clickedCount = 0
for a in answersList:
a[0]["state"] = "disabled"
a[0]["bg"] = "green"
answersList = []
#Hide buttons again
def hide():
global clickedCount, answersList
clickedCount = 0
for a in answersList:
#a[0].config(image = hiddenImg)
a[0]["image"] = hiddenImg
a[0]["state"] = "normal"
a[0]["bg"] = "white"
answersList = []
def wrong():
for a in answersList:
a[0]["bg"] = "red"
def buttonClicked(picture, id, button):
global clickedCount, answersList
print(clickedCount, len(answersList))
#print(button.image, "1", hiddenImg, picture)
if button.image is hiddenImg and clickedCount < 2:
button["image"] = picture
button["state"] = "disabled"
clickedCount += 1
answersList.append([button, id])
if len(answersList) == 2:
#Check id but make sure it's not the same button pressed twice
if answersList[0][1] is answersList[1][1]:#and answersList[0][0] is not answersList[1][0]:
disable()
else:
wrong()
hide()
#Create the actual buttons with their respective image
for h in range(height): #print(buttons[w][::],"\n")
newList = []
for w in range(width):
tempImage = imgs.pop(count)
picture = ImageTk.PhotoImage(tempImage[0])
id = tempImage[1]
button = Button(frame, image=hiddenImg, state=NORMAL, height=imgButtonHeight, width=imgButtonWidth)
#Need to split this up because of how python handles closures
button["command"] = lambda pic_temp=picture, id_temp=id, button_temp = button: buttonClicked(pic_temp, id_temp, button_temp)
button.image = hiddenImg
#buttons[w][h].name = str(w + h)
#buttons[w][h].grid(row=w, column=h, ipadx=random.randint(0,40), ipady=random.randint(0,40), padx=random.randint(0,5), pady=random.randint(0,5))
button.grid(row=h, column=w, padx=1, pady=1)
#Button(frame, image=picture).grid(row=w, column=h, ipadx=random.randint(0,40), ipady=random.randint(0,40), padx=random.randint(0,5), pady=random.randint(0,5))
count -= 1
# buttonList.append(buttons[h][w])
newList.append(button)
buttonList.append(newList)
# for y in range(height):
# for x in range(width):
# print(ButtonList[y][x])
# print("")
frame.pack(expand=True)
root.mainloop()```
I have a GUI where the user can click a button named "next set" that allows them to move onto the next task. I wanted to add a timer that starts as soon as they start the application and run the timer until they press the button "next set". When clicked, I want the time elapsed to print and the timer to restart until they press "next set" button again. I would like the timer to start automatically when the code runs. Currently, the "next set" button has two actions, one is to retrieve the next set of images and the other action I am trying to include is to reset the timer and print time elapsed. I also only included part of the code that felt relevant because it is long.
import time
import tkinter as tk
import csv
from pathlib import Path
import PIL.Image
import PIL.ImageDraw
import PIL.ImageTk
MAX_HEIGHT = 500
IMAGES_PATH = Path("Images")
CSV_LABELS_KEY = "New Labels"
CSV_FILE_NAME_KEY = "FolderNum_SeriesNum"
CSV_BOUNDING_BOX_KEY = "correct_flip_bbox"
counter = 0
timer_id = None
class App(tk.Frame):
def __init__(self, master=None):
super().__init__(master) # python3 style
self.config_paths = ["config 1.yaml", "config 2.yaml", "config 3.yaml"]
self.config_index = 0
self.clickStatus = tk.StringVar()
self.loadedImages = dict()
self.loadedBoxes = dict() # this dictionary will keep track of all the boxes drawn on the images
self.master.title('Slideshow')
frame = tk.Frame(self)
tk.Button(frame, text=" Next set ", command=lambda:[self.get_next_image_set(), self.reset()]).pack(side=tk.RIGHT)
tk.Button(frame, text=" Exit ", command=self.destroy).pack(side=tk.RIGHT)
frame.pack(side=tk.TOP, fill=tk.BOTH)
self.canvas = tk.Canvas(self)
self.canvas.pack()
self._load_dataset()
self.reset()
self.start_timer = None
t = time()
t.start()
def start_timer(self, evt=None):
if self._start_timer is not None:
self._start_timer = time.perf_counter()
# global counter
# counter += 1
# label.config(text=str(counter))
# label.after(1000, count)
def reset(self):
if self._start_timer is None:
elapsed_time = time.perf_counter() - self._start_timer
self._start_timer = None
print('Time elapsed (hh:mm:ss.ms) {}'.format(elapsed_time))
def _load_dataset(self):
try:
config_path = self.config_paths[self.config_index]
self.config_index += 1
except IndexError:
return
image_data = loadData(config_path)
# drawing the image on the label
self.image_data = image_data
self.currentIndex = 0
# start from 0th image
self._load_image()
def _load_image(self):
imgName = self.image_data[self.currentIndex]['image_file']
if imgName not in self.loadedImages:
self.im = PIL.Image.open(self.image_data[self.currentIndex]['image_file'])
ratio = MAX_HEIGHT / self.im.height
# ratio divided by existing height -> to get constant amount
height, width = int(self.im.height * ratio), int(self.im.width * ratio)
# calculate the new h and w and then resize next
self.canvas.config(width=width, height=height)
self.im = self.im.resize((width, height))
if self.im.mode == "1":
self.img = PIL.ImageTk.BitmapImage(self.im, foreground="white")
else:
self.img = PIL.ImageTk.PhotoImage(self.im)
imgData = self.loadedImages.setdefault(self.image_data[self.currentIndex]['image_file'], dict())
imgData['image'] = self.img
imgData['shapes'] = self.image_data[self.currentIndex]['shapes']
# for next and previous so it loads the same image adn don't do calculations again
self.img = self.loadedImages[self.image_data[self.currentIndex]['image_file']]['image']
self.canvas.create_image(0, 0, anchor=tk.NW, image=self.img)
self.show_drag_box()
def loadData(fname):
with open(fname, mode='r') as f:
return yaml.load(f.read(), Loader=yaml.SafeLoader)
if __name__ == "__main__":
data = loadData('config 1.yaml')
app = App(data)
app.pack() # goes here
app.mainloop()
I have used datetime instead of time, as subtracting two datetime objects will give an output with hours and minutes included, whereas subtracting two time objects only gives seconds. However, both will work, you may just need to do more reformatting using time.
Read the current time when the application starts and store it. Each time you press the button, subtract your stored time from the current time which gives you your time elapsed. Then simply store your new current time until the next button press. The code below demonstrates this.
import tkinter as tk
import datetime as dt
class TimeButton(tk.Frame):
def __init__(self, parent):
super().__init__(parent)
# Start timer
self.current_time = dt.datetime.today()
# Button
self.next_set = tk.Button(self, text='Next Set', command = self.clicked)
self.next_set.pack()
def clicked(self):
now = dt.datetime.today()
time_elapsed = now - self.current_time
print(time_elapsed)
self.current_time = now
if __name__ == "__main__":
window = tk.Tk()
button = TimeButton(window)
button.pack()
window.mainloop()