Frame by frame Play / Stop video file, in tkinter - python

I am trying to start / stop a video file in tkinter on a raspberry pi, using python 3.
I need the video to start from the beginning every time an infrared sensor is LOW (broken) and stop as soon as the sensor is HIGH again. Ideally the video should be inside a tkinter canvas so that I can display other elements on the screen at the same time (for example a loading bar).
I managed to get everything running except for the video, which runs as soon as the sensor is detected, but it freezes all other process (for example the loading bar) and it does not stop when the sensor is HIGH.
here is a simplified (and unchecked) version of the code for you the get an idea of the general structure (the real code is much longer):
import tkinter as TK
import RPi.GPIO as GPIO
import os
GPIO.setmode(GPIO.BCM)
GPIO.setup(14, GPIO.IN)
class App:
def __init__(self, root):
self.root = root
self.root.config(background = 'black', cursor = 'none')
self.background = TK.Canvas(root, width = 1024, height = 600, bg = 'black')
self.background.pack()
self.ext = 0
self.trial = 0
self.infrared()
def infrared(self):
if (GPIO.input(14) == False):
self.makebar()
if (self.ext == 0):
self.runvideo()
else:
os.system("killall omxplayer.bin")
self.ext = 0
self.root.after(16, self.infrared)
def runvideo(self):
os.system("omxplayer /home/pi/Desktop/testvideo.m4v")
def makebar():
self.stimulus_rect = TK.Canvas(root, width = 1024, height = 50, bg= 'white')
if self.ext < 1000
self.ext = self.ext + 10
self.stimulus_rect.create_rectangle(0, 0, self.ext, 50, fill="red")
self.stimulus_rect.place(x=0, y=0, anchor="new")
else:
self.trial = self.trial + 1
self.ext = 0
root = TK.Tk()
App(root)
root.mainloop()
From what I was able to find online:
1) tkinter might be coupled with opencv to achieve this, but it does not look like installing opencv on the raspberry pi is a straightforward operation;
2) In general options involving "os" seem to be bound to fail in what I want to achieve.
I couldn't find a clean way of doing this. My dream scenario would be to load into a canvas the video frames one by one and do so at 60hz (screen frequency). I would then check the sensor at exactly the same frequency and prevent the next frame to be loaded if the sensor is not broken. In pseudocode this would look like this
def infrared(self):
if (GPIO.input(14) == False):
self.makebar()
if (self.ext == 0):
self.runvideo()
else:
self.video.stop
self.ext = 0
self.frame = 0
self.root.after(16, self.infrared)
def runvideo(self):
self.frame = self.frame + 1
video.run("testvideo.m4v", self.frame)
Any idea on how to achieve this in tkinter on a raspberry pi?
thanks
ant

After a week of research and trials and errors this is how I currently achieve what I needed (in pseudocode):
#### PSEUDOCODE ####
from subprocess import Popen # this library is used to open the video file
class App:
def __init__(self, root):
self.moviestart = False
self.movieduration = 130000
self.movie = "/home/pi/Desktop/test.mp4"
def infrared(self):
if (GPIO.input(IR) == False):
if not self.moviestart:
self.makevideo() # Call the function to start the video
self.moviestart = True # Flag that the video has started
self.moviestop = False # Flag that the video is currently playing
self.root.after(self.videoduration,
self.stopvideo) # manually call the stop video function
# to stop the video after 13 seconds (video's length).
# I could not find a more elegant way of doing this, unfortunately.
else:
self.clear_screen()
self.root.after(self.refreshIR, self.infrared)
def makevideo(self):
# popen will open the movie in a window of the size I specified,
# so that other element from tkinter can be placed on top of the movie window
omxc = Popen(['omxplayer', self.movie, '--win', "0 30 800 450"])
def stopvideo(self):
self.moviestart = False # flag that movie has been stopped
if (self.moviestop == False): # check if the movie is currently playing
try: # this is a cheap workaround other problems I had, do not try this at home
os.system('killall omxplayer.bin') # this literally kills any omxplayer instance
# currently open
self.moviestop = True # flag that the movie is not playing at the moment
except:
pass
I hope this can be useful to anybody else with similar problems. I will update the answer if I find a better solution. For now this works good enough.

Related

How can you run two python tkinter files simultaneously when one has a loop in it?

I have made a tkinter country guessing game witch works fine however takes along time to run. So i made a loading screen for it with a looping animation on in a separate file. I cant find a way to run the loading screen first and then run the game whilst the animation on the loading screen is still running.
Loading screen code:
from tkinter import *
from time import *
import os
import random
run = 0
loads = True
dotnum = 0
def task():
sleep(2)
root.destroy()
root = Tk()
root.title("Loading...")
root.geometry("1280x720")
Background = PhotoImage(file = "Images\Loadscreen.png")
Loaders = PhotoImage(file = "Images\Loader.gif")
image = Label(root,width=1000,height=500,image=Background)
image.place(x=0, y=0, relwidth=1, relheight=1)
frameCnt = 16
frames = [PhotoImage(file='Images\Loader.gif',format = 'gif -index %i' %(i)) for i in range(frameCnt)]
def update(ind):
frame = frames[ind]
ind += 1
if ind == frameCnt:
ind = 0
loadanim.configure(image=frame)
root.after(100, update, ind)
loadanim = Label(root, bg = "black")
loadanim.place(x = 450, y = 450)
root.after(0, update, 0)
root.mainloop()
To run the loading screen first, you can call the loading screen code before calling the game code.
You can modify the code to pass a function as an argument to the loading screen, and then call this function after the loading screen is destroyed. This will allow you to run the game code after the loading screen is done.
For example:
def run_game():
# game code here
pass
def run_loading_screen(callback):
# loading screen code here
root.after(2000, callback)
root.mainloop()
run_loading_screen(run_game)
Here, run_game is the function containing the game code, and run_loading_screen is the function containing the loading screen code. The run_loading_screen function takes a callback argument, which is the function to be called after the loading screen is destroyed. In the loading screen code, the root.after method is used to call the callback function after 2000 milliseconds (2 seconds).

Tkinter program works fine in IDE (Visual Studio) but when using pyinstaller to compile to .exe threading does not work same like it does in IDE

When I run my python project in my IDE the GUI and everything is responsive and works great. But when I run as .exe my threading components don't work like they do in IDE. The program's goal is to grab a live feed via RTSP and using opencv to display the images. This is done in its own thread here.
import time
import threading
import cv2
import PIL.Image
"""TODO: add docstring"""
class VideoCapture:
def __init__(self, xmlDict=None, width=None, height=None, fps=None):
"""TODO: add docstring"""
self.xmlDict = xmlDict
self.width = width
self.height = height
self.fps = int(self.xmlDict['FPS'])
self.running = False
# Open the video source
self.vid = cv2.VideoCapture(self.xmlDict['IpAddress'])
if not self.vid.isOpened():
raise ValueError("[MyVideoCapture] Unable to open video source", xmlDict['IpAddress'])
# Get video source width and height
if not self.width:
self.width = int(self.vid.get(cv2.CAP_PROP_FRAME_WIDTH)) # convert float to int
if not self.height:
self.height = int(self.vid.get(cv2.CAP_PROP_FRAME_HEIGHT)) # convert float to int
if not self.fps:
self.fps = int(self.vid.get(cv2.CAP_PROP_FPS)) # convert float to int
# default value at start
self.ret = False
self.frame = None
self.convert_color = cv2.COLOR_BGR2RGB
#self.convert_color = cv2.COLOR_BGR2GRAY
self.convert_pillow = True
# start thread
self.running = True
self.thread = threading.Thread(target=self.process)
self.thread.start()
def process(self):
"""TODO: add docstring"""
while self.running:
ret, frame = self.vid.read()
if ret:
# process image
frame = cv2.resize(frame, (self.width, self.height))
# it has to record before converting colors
if self.convert_pillow:
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
frame = PIL.Image.fromarray(frame)
else:
print('[MyVideoCapture] stream end:', self.video_source)
# TODO: reopen stream
self.running = False
if self.recording:
self.stop_recording()
break
# assign new frame
self.ret = ret
self.frame = frame
# sleep for next frame
#if self.fps != "FULL":
# time.sleep(1/int(self.fps))
I have a button setup called start that infers an image every 2 seconds and prints out the label and confidence. When I do this in .exe the live feed and GUI freeze while inference is being made but when I use program in IDE it does not freeze. Here is the code that does this.
#Button to start inference
self.btn_snapshot = tk.Button(self.btnFrame,width = 10,height = 2, text="Start", command=lambda:threading.Thread(target = self.snapshot).start())
self.btn_snapshot.grid(row = 1,column = 0)
#snapshot function
def snapshot(self):
self.recording = True
while self.recording:
filename = self.vid.snapshot()
result = self.predictImage(filename)
output = self.calculatePassFail(result)
if self.manager:
self.manager.onClick(output)
else:
print('something')
time.sleep(2)
The other two methods that the snapshot function calls are predictImage and calculatePassFail.
def predictImage(self,imageName):
onnxModel = ImageModel.load(self.xmlDict['ModelPath'])
result = onnxModel.predict_from_file(imageName)
return result
def calculatePassFail(self,result):
calcResult = result.labels[0]
self.labelName = calcResult[0]
self.imgScore = calcResult[1]*100
return f"{self.labelName} with score{self.imgScore}"
So I found a fix around this, not sure if its a proper fix but it works. So for some reason when I use pyinstaller to create .exe and there is a console window I have the issue but when I use the flag --noconsole when using pyinstaller to create w/out console the issue goes away and my inference on images acts in its own thread like it does in my IDE. Not sure why it does but it works I guess.

Loading GUI Python script on boot for raspberry pi

I'm on Raspberry Pi 3B+ (Raspbian) and I'm trying to load a Python script on boot which display 720p images into full screen mode, the user should then be able to press a push button to go to the next image.
Currently, I have created the code which allows for the images to display and the images to change due to the push button.
Right now I'm having trouble with having the images load into full screen mode and boot on start-up. I have followed many guides and tutorials on how I can load a GUI Python script on boot but none of them have seemed to work for my specific script (but they loaded on boot successfully from their example scripts) which is making me think that there's an issue in the code for why it won't boot (probably because I haven't implemented so that they open in full screen). These are all the guides which I tried out (https://www.raspberrypi-spy.co.uk/2015/02/how-to-autorun-a-python-script-on-raspberry-pi-boot/ , raspberry pi : Auto run GUI on boot , https://raspberrypi.stackexchange.com/questions/4123/running-a-python-script-at-startup , https://www.digikey.ca/en/maker/blogs/2018/how-to-run-python-programs-on-a-raspberry-pi , https://www.instructables.com/Raspberry-Pi-Launch-Python-script-on-startup/ , https://bc-robotics.com/tutorials/running-python-program-boot-raspberry-pi/) and I'd like to focus on the GUI method from this tutorial for my question (https://learn.sparkfun.com/tutorials/how-to-run-a-raspberry-pi-program-on-startup/all )
I'm very much a beginner right now when it comes to GUI programming (I took the original script from online and modified it to include the push button) and I'm not too sure on how I can implement the full screen option into the code which displays the images.
Here is what I have currently (will load images into a window, you can then change to the next image using a push button). At the beginning of my code, I tried adding in some functions from the example listed later which allowed toggling of full screen and also their associated root functions at the bottom to complement this
import tkinter as tk
from PIL import Image, ImageTk
import time
import sys
import os
import keyboard
import RPi.GPIO as GPIO
import time
GPIO.setwarnings(False)
GPIO.setmode(GPIO.BOARD)
GPIO.setup(10, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
GPIO.add_event_detect(10, GPIO.FALLING)
def toggle_fullscreen(event=None): # I added this in to try full screen
global root
global fullscreen
# Toggle between fullscreen and windowed modes
fullscreen = not fullscreen
root.attributes('-fullscreen', fullscreen)
resize()
# Return to windowed mode
def end_fullscreen(event=None): # I added this in to try full screen
global root
global fullscreen
# Turn off fullscreen mode
fullscreen = False
root.attributes('-fullscreen', False)
root= tk.Tk() # I added this in to try full screen
class HiddenRoot(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
#hackish way, essentially makes root window
#as small as possible but still "focused"
#enabling us to use the binding on <esc>
self.wm_geometry("0x0+0+0")
self.window = MySlideShow(self)
#self.window.changeImage()
self.window.startSlideShow()
class MySlideShow(tk.Toplevel):
def __init__(self, *args, **kwargs):
tk.Toplevel.__init__(self, *args, **kwargs)
#remove window decorations
self.overrideredirect(True)
#save reference to photo so that garbage collection
#does not clear image variable in show_image()
self.persistent_image = None
self.imageList = []
self.pixNum = 0
#used to display as background image
self.label = tk.Label(self)
self.label.pack(side="top", fill="both", expand=True)
self.getImages()
#### Added the GPIO event here
GPIO.add_event_callback(10, self.changeImage)
def getImages(self):
'''
Get image directory from command line or use current directory
'''
if len(sys.argv) == 2:
curr_dir = sys.argv[1]
else:
curr_dir = '.'
for root, dirs, files in os.walk(curr_dir):
for f in files:
if f.endswith(".png") or f.endswith(".jpg"):
img_path = os.path.join(root, f)
print(img_path)
self.imageList.append(img_path)
def startSlideShow(self, delay=4):
myimage = self.imageList[self.pixNum]
self.pixNum = (self.pixNum + 1) % len(self.imageList)
self.showImage(myimage)
#self.after(delay*1000, self.startSlideShow)
# while True:
# buttonState = GPIO.input(buttonPin)
# if buttonState == False:
# self.startSlideShow(self, delay=4)
##staticmethod
def changeImage(self, pull_up_down=GPIO.PUD_DOWN):
prev_input = 0
while True:
#take a reading
input = GPIO.input(10)
#if the last reading was low and this one high, print
if ((not prev_input) and input):
myimage = self.imageList[self.pixNum]
self.pixNum = (self.pixNum + 1) % len(self.imageList)
self.showImage(myimage)
#update previous input
prev_input = input
#slight pause to debounce
time.sleep(0.05)
# myimage = self.imageList[self.pixNum]
# self.pixNum = (self.pixNum + 1) % len(self.imageList)
# self.showImage(myimage)
def showImage(self, filename):
image = Image.open(filename)
img_w, img_h = image.size
scr_w, scr_h = self.winfo_screenwidth(), self.winfo_screenheight()
width, height = min(scr_w, img_w), min(scr_h, img_h)
image.thumbnail((width, height), Image.ANTIALIAS)
#set window size after scaling the original image up/down to fit screen
#removes the border on the image
scaled_w, scaled_h = image.size
self.wm_geometry("{}x{}+{}+{}".format(scaled_w,scaled_h,0,0))
# create new image
self.persistent_image = ImageTk.PhotoImage(image)
self.label.configure(image=self.persistent_image)
slideShow = HiddenRoot()
slideShow.bind("<Escape>", lambda e: slideShow.destroy()) # exit on esc
slideShow.mainloop()
root.bind('<F11>', toggle_fullscreen) # I added this in to try full screen
root.bind('<Escape>', end_fullscreen) # I added this in to try full screen
toggle_fullscreen() # I added this in to try full screen
root.mainloop() # I added this in to try full screen
GPIO.cleanup()
And from that guide I said to note earlier, here is the code they which loads a clock into fullscreen mode (and I followed their method of loading the script on boot which worked)
import tkinter as tk
import tkinter.font as tkFont
import time
###############################################################################
# Parameters and global variables
# Default font size
font_size = -24
# Declare global variables
root = None
dfont = None
frame = None
dtime = None
# Global variable to remember if we are fullscreen or windowed
fullscreen = False
###############################################################################
# Functions
# Toggle fullscreen
def toggle_fullscreen(event=None):
global root
global fullscreen
# Toggle between fullscreen and windowed modes
fullscreen = not fullscreen
root.attributes('-fullscreen', fullscreen)
resize()
# Return to windowed mode
def end_fullscreen(event=None):
global root
global fullscreen
# Turn off fullscreen mode
fullscreen = False
root.attributes('-fullscreen', False)
resize()
# Automatically resize font size based on window size
def resize(event=None):
global time_dfont
global button_dfont
global frame
# Resize font based on frame height (minimum size of 12)
# Use negative number for "pixels" instead of "points"
new_size = -max(12, int((frame.winfo_height() / 2)))
time_dfont.configure(size=new_size)
new_size = -max(12, int((frame.winfo_height() / 30)))
button_dfont.configure(size=new_size)
# Read values from the sensors at regular intervals
def update():
global root
global dtime
# Get local time
local_time = time.localtime()
# Convert time to 12 hour clock
hours = local_time.tm_hour
if hours > 12:
hours -= 12
# Add leading 0s
shours = str(hours)
smin = str(local_time.tm_min)
if hours < 10:
shours = '0' + shours
if local_time.tm_min < 10:
smin = '0' + smin
# Construct string out of time
dtime.set(shours + ':' + smin)
# Schedule the poll() function for another 500 ms from now
root.after(500, update)
###############################################################################
# Main script
# Create the main window
root = tk.Tk()
root.title("My Clock")
# Create the main container
frame = tk.Frame(root, bg='black')
# Lay out the main container (expand to fit window)
frame.pack(fill=tk.BOTH, expand=1)
# Variables for holding temperature and light data
dtime = tk.StringVar()
# Create dynamic font for text
time_dfont = tkFont.Font(family='Courier New', size=font_size)
button_dfont = tkFont.Font(size=font_size)
# Create widgets
label_time = tk.Label( frame,
textvariable=dtime,
font=time_dfont,
fg='red',
bg='black')
button_quit = tk.Button(frame,
text="Quit",
font=button_dfont,
command=root.destroy,
borderwidth=0,
highlightthickness=0,
fg='gray10',
bg='black')
# Lay out widgets in a grid in the frame
label_time.grid(row=0, column=0, padx=20, pady=20)
button_quit.grid(row=1, column=0, padx=5, pady=5, sticky=tk.E)
# Make it so that the grid cells expand out to fill window
frame.rowconfigure(0, weight=10)
frame.rowconfigure(1, weight=1)
frame.columnconfigure(0, weight=1)
# Bind F11 to toggle fullscreen and ESC to end fullscreen
root.bind('<F11>', toggle_fullscreen)
root.bind('<Escape>', end_fullscreen)
# Have the resize() function be called every time the window is resized
root.bind('<Configure>', resize)
# Schedule the poll() function to be called periodically
root.after(20, update)
# Start in fullscreen mode and run
toggle_fullscreen()
root.mainloop()
Now what I'm trying to figure out is how I can implement their method of going into a full screen display into my program
Thank you :)!
I am not sure if I really understood your question.
I understood you want to change on certain events to full screen. If yes, you just need to change the geometry property of your root. Like for example
master.geometry("{0}x{1}+0+0".format(
master.winfo_screenwidth()-pad, master.winfo_screenheight()-pad))
above code checks for screen size and reduces some distance on w and h to fit in screen. Have a look to below link.
https://stackoverflow.com/questions/7966119/display-fullscreen-mode-on-tkinter#:~:text=This%20creates%20a%20fullscreen%20window,geometry%20and%20the%20previous%20geometry.&text=You%20can%20use%20wm_attributes%20instead%20of%20attributes%20%2C%20too.
you can create a function with that call and trigger it on start up or button pressed.
Hope my guess is right and this answers helps.
Cheers

Create a fullscreen app in the secondary screen using tkinter and python 3.7.2

I'm trying to create a simple slideshow using Tkinter and Python 3.7.2. I want the slide show to display images on secondary screen and in fullscreen mode. I have tried to use only one window and two windows as suggested here. This is the code I have written:
import tkinter as tk
from PIL import Image, ImageTk
class App(tk.Tk):
'''Tk window/label adjusts to size of image'''
def __init__(self, image_files, x, y, delay):
# the root will be self
tk.Tk.__init__(self)
# set width. height, x, y position
self.geometry('%dx%d+%d+%d'%(912,1140,0,0)) #Window on main screen
#create second screen window
self.top2 = tk.Toplevel(self,bg="grey85")
self.top2.geometry('%dx%d+%d+%d'%(912,1140,-912,0)) # The resolution of the second screen is 912x1140.
#The screen is on the left of the main screen
self.top2.attributes('-fullscreen', False) #Fullscreen mode
self.pictures = image_files
self.picture_display = tk.Label(self.top2, width=912, height=1140)
self.picture_display.pack()
self.delay = delay
self.index = 1
self.nImages = len(image_files)
def start_acquisition(self):
if self.index == self.nImages+1:
self.destroy()
return
self.load = Image.open(self.pictures[self.index-1])
self.render = ImageTk.PhotoImage(self.load)
self.picture_display['image'] = self.render
self.index += 1
self.after(self.delay, self.start_acquisition)
def run(self):
self.mainloop()
# set milliseconds time between slides
delay = 3500
image_files = [
'1805Circle Test Output.bmp', #images resolution is 912x1140
'8233Circle Test Input.bmp',
'cross.bmp'
]
x = 0 #Not used currently
y = 0 #Not used currently
app = App(image_files, x, y, delay)
app.start_acquisition()
app.run()
print('Finished')
The code works as expected when the fullscreen attribute is "False". As soon as I put this attribute "True" the "top2" window appears on the main screen. The same thing happen if only one window is used. Could you please help me find a solution for this problem? thx

Play an Animated GIF in python with tkinter

I am wanting to create a virtual pet style game using python3 and tkinter. So far I have the main window and have started putting labels in, but the issue I am having is playing an animated gif. I have searched on here and have found some answers, but they keep throwing errors. The result I found has the index position of the gif using PhotoImage continue through a certain range.
# Loop through the index of the animated gif
frame2 = [PhotoImage(file='images/ball-1.gif', format = 'gif -index %i' %i) for i in range(100)]
def update(ind):
frame = frame2[ind]
ind += 1
img.configure(image=frame)
ms.after(100, update, ind)
img = Label(ms)
img.place(x=250, y=250, anchor="center")
ms.after(0, update, 0)
ms.mainloop()
When I run this in terminal with "pyhton3 main.py" I get the following error:
_tkinter.TclError: no image data for this index
What am I overlooking or completely leaving out?
Here is the link to the GitHub repository to see the full project:VirtPet_Python
Thanks in advance!
The error means that you tried to load 100 frames, but the gif has less than that.
Animated gifs in tkinter are notoriously bad. I wrote this code an age ago that you can steal from, but will get laggy with anything but small gifs:
import tkinter as tk
from PIL import Image, ImageTk
from itertools import count
class ImageLabel(tk.Label):
"""a label that displays images, and plays them if they are gifs"""
def load(self, im):
if isinstance(im, str):
im = Image.open(im)
self.loc = 0
self.frames = []
try:
for i in count(1):
self.frames.append(ImageTk.PhotoImage(im.copy()))
im.seek(i)
except EOFError:
pass
try:
self.delay = im.info['duration']
except:
self.delay = 100
if len(self.frames) == 1:
self.config(image=self.frames[0])
else:
self.next_frame()
def unload(self):
self.config(image="")
self.frames = None
def next_frame(self):
if self.frames:
self.loc += 1
self.loc %= len(self.frames)
self.config(image=self.frames[self.loc])
self.after(self.delay, self.next_frame)
root = tk.Tk()
lbl = ImageLabel(root)
lbl.pack()
lbl.load('ball-1.gif')
root.mainloop()
First of all, you need to know what is the last range of your GIF file. so by changing the different value of i, you will get it.For my condition is 31.
then just need to put the condition.So it will play gif infinitely.
from tkinter import *
import time
import os
root = Tk()
frames = [PhotoImage(file='./images/play.gif',format = 'gif -index %i' %(i)) for i in range(31)]
def update(ind):
frame = frames[ind]
ind += 1
print(ind)
if ind>30: #With this condition it will play gif infinitely
ind = 0
label.configure(image=frame)
root.after(100, update, ind)
label = Label(root)
label.pack()
root.after(0, update, 0)
root.mainloop()
A very simple approach would be to use multithreading.
To run the GIF infinitely in a Tkinter window you should follow the following:
Create a function to run the GIF.
Put your code to run the GIF inside while True inside the function.
Create a thread to run the function.
Run root.mainloop() in the primary flow of the program.
Use time.sleep() to control the speed of your animation.
Refer to my code below:
i=0
ph = ImageTk.PhotoImage(Image.fromarray(imageframes[i]))
imglabel=Label(f2,image=ph)
imglabel.grid(row=0,column=0)
def runthegif(root,i):
while True:
i = i + 7
i= i % 150
ph=ImageTk.PhotoImage(PhotoImage(file='images/ball.gif',format='gif -index %i' %i))
imagelabel=Label(f2,image=ph)
imagelabel.grid(row=0,column=0)
time.sleep(0.1)
t1=threading.Thread(target=runthegif,args=(root,i))
t1.start()
root.mainloop()

Categories

Resources