Here's my problem, when I run the following code a very small window appears and if I resize it I can see the correct image names cycling but no images. I know I need to save a reference of the image object to keep it from getting garbage collected, but I can't seem to figure out how to do that given the situation.
from itertools import cycle
try:
# Python2
import Tkinter as tk
except ImportError:
# Python3
import tkinter as tk
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 x, y position only
self.geometry('+{}+{}'.format(x, y))
self.delay = delay
# allows repeat cycling through the pictures
# store as (img_object, img_name) tuple
self.pictures = cycle((tk.PhotoImage(image), image)for image in image_files)
self.picture_display = tk.Label(self)
self.picture_display.pack()
def show_slides(self):
'''cycle through the images and show them'''
# next works with Python26 or higher
img_object, img_name = next(self.pictures)
self.picture_display.config(image=img_object)
# shows the image filename, but could be expanded
# to show an associated description of the image
self.title(img_name)
self.after(self.delay, self.show_slides)
def run(self):
self.mainloop()
# set milliseconds time between slides
delay = 3500
# get a series of gif images you have in the working folder
# or use full path, or set directory to where the images are
image_files = []
for number in range(1,10):
image_files.append("DPS%05d.jpx" % number)
# upper left corner coordinates of app window
x = 100
y = 50
app = App(image_files, x, y, delay)
app.show_slides()
app.run()
I think this will work, but I'm not really sure. Remember to have a .gif image
from itertools import cycle
try:
# Python2
import Tkinter as tk
except ImportError:
# Python3
import tkinter as tk
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 x, y position only
self.geometry('+{}+{}'.format(x, y))
self.delay = delay
# allows repeat cycling through the pictures
# store as (img_object, img_name) tuple
for image in image_files:
self.pictures = ((tk.PhotoImage(image), image))
self.picture_display = tk.Label(image = self.pictures)
self.picture_display.pack()
def show_slides(self):
'''cycle through the images and show them'''
# next works with Python26 or higher
self.picture_display.config(image=self.pictures)
# shows the image filename, but could be expanded
# to show an associated description of the image
self.after(self.delay, self.show_slides)
def run(self):
self.mainloop()
# set milliseconds time between slides
delay = 3500
# get a series of gif images you have in the working folder
# or use full path, or set directory to where the images are
image_files = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
for x in list(image_files):
image_files.append("DPS%05d.gif" % x)
# upper left corner coordinates of app window
x = 100
y = 50
app = App(image_files, x, y, delay)
app.show_slides()
app.run()
This is just a stab in the dark kind of thing but it might work (i hope so), with only like 6 views i figured anything might be helpful.
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!
I intend to make a Py code which creates a tkinter dot that turns on a key press and deletes on a key press of couple keys.
The dot already is functional but i need it switch on and off on certain keypresses/mouse clicks which means i need an outside tkinter.mainloop() Update function.
The Update function with a while in it to constantly check if conditions to turn it off/on are present. But the Tkinter widget Somehow gets applied to the screen Only when the function nds. Like widget could be created but it will only take effect when function ends. And i need to turn it off/on dynamically.
I have tried to use a tkinter.after() with additional one at the end of called function only to find out an error of Recursion depth. What i expected to happen was that the function would be called over and over again, instead it runs that function like a while loop. I also have tried Asyncio.run() but it would result not making it visible till the function ends at least once. And I need to change it dynamically.
from tkinter import *
from tkinter import Canvas
from winsound import Beep
from time import sleep
import asyncio
import keyboard
import mouse
root = Tk()
width = root.winfo_screenwidth()
height = root.winfo_screenheight()
class tk_Dot():
def __init__(self,x=-1,y=-1,radius=4,color="red"):
self.x = x
if x == -1:
self.x = width/2-radius//2
print(self.x)
self.y = y
if y == -1:
self.y = height/2+radius//2
print(self.y)
self.radius=radius
self.color = color
self.lines = []
self.count = 1
def line(self,i):
return canvas.create_line(self.x, self.y-i, self.x+self.radius, self.y-i, fill=self.color)
def create(self):
self.lines = []
for i in range(0,self.radius):
self.lines.append(self.line(i))
def delete(self):
for i in range(0,self.radius):
canvas.delete(self.lines[i])
canvas.dtag(self.lines[i])
opacity_of_tk_window = 1 # From 0 to 1 0 meaning completely transparent 1 meaning everything created in canvas will give the color it was given
root.attributes('-alpha',opacity_of_tk_window)
# Invisible Tkinter window label
root.overrideredirect(True)
# Makes Transparent background
transparent_color = '#f0f0f0'
root.wm_attributes('-transparent', transparent_color)
canvas = Canvas()
# Rectangle filled with color that is specified above as the transparent color so practically making transparent background
canvas.create_rectangle(0, 0, width, height, fill=transparent_color)
canvas.pack(fill=BOTH, expand=1)
radius = 2
radius = 1+radius\*2
# Create a dot class
game_dot = tk_Dot(width/2-radius//2+1,height/2+1+radius//2,radius,"Red")
# Create a Dot at the middle of the calorant crosshair
# game_dot.create()
# Delete the dot
# game_dot.delete()
def Update():
game_dot.create()
print("Dot should be visible by now")
print("Is it?")
sleep(5) #sec
print("Oh yeah after the function ends.") # the problem
def Delete():
game_dot.delete()
root.geometry('%dx%d+%d+%d' % (width, height, -2,-2))
# Tkinter window always on top
root.attributes('-topmost',True)
root.after(1000,Update())
root.mainloop()
My GUI is a slideshow of images and there is a certain correct image on each page of the slideshow. The correct image is the top-left triangle, heart balloon, the circle, and the hippo. I am completely new to python so I have been struggling with how to create box around the selected image when a user is using the slideshow. This box forms around the correct or the incorrect image, whichever is pressed. To further explain, this includes inserting a box around an image based on where the user clicks but it does not say whether the image selected is the correct image or not. Thank you for your help
import PIL.Image
import PIL.ImageDraw
import tkinter as tk
import PIL.ImageTk
import csv
from PIL import Image
MAX_HEIGHT = 500
# height of the window (dimensions of the image)
class App(tk.Frame):
def __init__(self, imageData, master=None):
tk.Frame.__init__(self, master)
self.clickStatus = tk.StringVar()
self.loadedImages = dict()
self.master.title('Slideshow')
fram = tk.Frame(self)
tk.Button(fram, text="Previous Image", command=self.prev).pack(side=tk.LEFT)
tk.Button(fram, text=" Next Image ", command=self.next).pack(side=tk.LEFT)
tk.Label(fram, textvariable=self.clickStatus, font='Helvetica 18 bold').pack(side=tk.RIGHT)
# inside or outside
fram.pack(side=tk.TOP, fill=tk.BOTH)
self.imageLabel = tk.Label(self)
# drawing the image on the label
self.imageData = imageData
self.currentIndex = 0
# start from 0th image
self.__loadImage__()
self.imageLabel.bind("<Button-1>", self.clicked_evt)
# when you click button, it opens event of clicked_evt
self.imageLabel.pack()
self.pack()
def clicked_evt(self, evt):
x, y = evt.x, evt.y
imgData = self.loadedImages[self.imageData[self.currentIndex]['image_file']]
(l, t), (r,b) = imgData['lt'], imgData['rb']
if t<=y<=b and l<=x<=r:
##self.clickStatus.set('inside')
print('Inside')
else:
##self.clickStatus.set('outside')
print('Outside')
def __loadImage__(self):
if self.imageData[self.currentIndex]['image_file'] not in self.loadedImages:
self.im = PIL.Image.open(self.imageData[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.im = self.im.resize((width, height))
lt = (int(self.imageData[self.currentIndex]['left']*ratio), int(self.imageData[self.currentIndex]['top']*ratio))
rb = (int(self.imageData[self.currentIndex]['right']*ratio), int(self.imageData[self.currentIndex]['bottom']*ratio))
# modifying new ratios with new height and width
#shape = [lt, rb]
# print(shape)
#img1 = PIL.ImageDraw.Draw(self.im)
#img1.rectangle(shape, outline ="red")
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.imageData[self.currentIndex]['image_file'], dict())
imgData['image'] = self.img
imgData['lt'] = lt
imgData['rb'] = rb
# for next and previous so it loads the same image adn don't do calculations again
self.img = self.loadedImages[self.imageData[self.currentIndex]['image_file']]['image']
self.imageLabel.config(image=self.img, width=self.img.width(), height=self.img.height())
def prev(self):
self.currentIndex = (self.currentIndex+len(self.imageData) - 1 ) % len(self.imageData)
self.__loadImage__()
# here if i go to the first one and press back, goes to last, round robbin
def next(self):
self.currentIndex = (self.currentIndex + 1) % len(self.imageData)
self.__loadImage__()
# here if i go to the last one and press next, goes to first, round robbin
def loadData(fname):
with open(fname, mode='r') as f:
reader = csv.DictReader(f)
data = [dict(row) for row in reader]
for row in data:
row['top'], row['bottom'], row['left'], row['right'] = int(row['top']),int(row['bottom']),int(row['left']),int(row['right'])
return data
if __name__ == "__main__":
data = loadData('bounding_box.csv')
app = App(data)
app.mainloop()
As far as I understand you simply want to draw a rectangle around an image when it is clicked (explanation in code comments):
from tkinter import Tk, Canvas
from PIL import Image, ImageTk
# below code is used to create images, you can also load them from a file if you need
mode = 'RGB'
size = (150, 150)
color_lst = ['red', 'green', 'blue', 'yellow']
# best to get all the images for the slide in a single list for further easier workings
img_lst = [Image.new(mode, size, color) for color in color_lst]
# selecting function that will be called when user clicks on image
def select(id_):
canvas.create_rectangle(canvas.bbox(id_), width=5)
# here you will put other code to be executed after
# user clicks on the image like going to the next frame or sth
# additionally you could use this if you want the other rectangles to disappear
# but kinda pointless if you will switch frames and stuff
# for canvas_id in canvas_images:
# if canvas_id == id_:
# continue
# canvas.create_rectangle(canvas.bbox(canvas_id), width=5, outline='white')
root = Tk()
# here create a photoimage list from the above image list
photo_lst = [ImageTk.PhotoImage(image) for image in img_lst]
# create canvas
canvas = Canvas(root, width=400, height=400)
canvas.pack()
# set the coordinates for the four images
coordinates = [(100, 100), (300, 100), (100, 300), (300, 300)]
# create the images and append their id to a list
canvas_images = [
canvas.create_image(pos[0], pos[1], image=photo) for pos, photo in zip(coordinates, photo_lst)
]
# bind the ids from `canvas_images` to being clicked and execute simple drawing method
for c_img in canvas_images:
canvas.tag_bind(
c_img, '<Button-1>', lambda e, i=c_img: select(i)
)
root.mainloop()
A common problem with Tkinter is that in order to use images in Labels and Buttons, you need a reference to the PhotoImage object... somehow.
I have written a wrapper class around Button to add my own functionalities, because I want to use GIFs instead of images, and I want to be able to switch between gifs when I press the button (or use a keyboard hotkey). The first GIF runs fine and loops perfectly. When I switch to the second GIF, I get the error message, saying _tkinter.TclError: image "pyimage48 ... pyimage55" doesn't exist. It looks like the following:
from tkinter import *
from PIL import ImageTk, Image
class AnimatedButton(Button)
def __init__(self, master, size, img_paths):
self.size = size
self.seq_count = len(img_paths) # Number of gif files
self.sequences = []
for path in img_paths:
gif, delay = loadGif(path)
# Create a tuple of all frames in a gif, with the delay between frames. Store this tuple in self.sequences
self.sequences.append(([ImageTk.PhotoImage(frame) for frame in gif], delay))
self.delay = self.sequences[0][1]
self.current_sequence = self.sequences[0][0]
self.image = self.current_sequence[0]
self.seq_id = 0 # Sequence counter
self.frame_id = 0 # Frame counter
Button.__init__(self, master, image=self.image, width=size, height=size)
self.cancel = self.after(self.delay, self.play)
def play(self):
self.image = self.current_sequence[self.frame_id]
self.config(image=self.image)
# More stuff below to loop through the frames etc.
What is strange is that I don't have any of this with my other Button class, MyButton, also a wrapper class.
class MyButton(Button):
def __init__(self, master, size, img_paths):
self.image_count = len(img_paths)
self.image_id = 0
self.size = size
self.images = []
for path in img_paths:
try:
im = Image.open(path)
except:
print("Could not open file {}".format(path))
photo_image = ImageTk.PhotoImage(im, image_mode)
self.images.append(photo_image)
self.image = self.images[0]
Button.__init__(self, master, image=self.image, width=size,
height=size)
Most Google searches came up with the fact that you shouldn't use two tkinter.Tk() calls, but I am only using one (Yes, I made sure).
Any ideas are very much appreciated!
Thanks to the hint by stovfl in the comments above, I was missing [0] in play():
Correct code should be:
def play(self):
self.image = self.current_sequence[self.frame_id][0]
self.config(image=self.image)
# More stuff below to loop through the frames etc.
I tried to create a slideshow in python to loop over 5500 images faster than I could manually. I used tkinter and the parameter slide_interval should do the job. The slideshow will indeed be longer if I set slide_interval=5000 for example, but it makes no differnce if I set it to 500,50 or 5, it will still take approximately same number of seconds to display each image, while what I would be interested in would be 1 or 0.5 seconds spent per image.
Here is the code:
#!/usr/bin/env python3
"""Display a slideshow from a list of filenames"""
import os
import tkinter
from itertools import cycle
from PIL import Image, ImageTk
class Slideshow(tkinter.Tk):
"""Display a slideshow from a list of filenames"""
def __init__(self, images, slide_interval):
"""Initialize
images = a list of filename
slide_interval = milliseconds to display image
"""
tkinter.Tk.__init__(self)
self.geometry("+0+0")
self.slide_interval = slide_interval
self.images = None
self.set_images(images)
self.slide = tkinter.Label(self)
self.slide.pack()
def set_images(self, images):
self.images = cycle(images)
def center(self):
"""Center the slide window on the screen"""
self.update_idletasks()
w = self.winfo_screenwidth()
h = self.winfo_screenheight()
size = tuple(int(_) for _ in self.geometry().split('+')[0].split('x'))
x = w / 2 - size[0] / 2
y = h / 2 - size[1] / 2
self.geometry("+%d+%d" % (x, y))
def set_image(self):
"""Setup image to be displayed"""
self.image_name = next(self.images)
filename, ext = os.path.splitext(self.image_name)
self.image = ImageTk.PhotoImage(Image.open(self.image_name))
def main(self):
"""Display the images"""
self.set_image()
self.slide.config(image=self.image)
self.title(self.image_name)
self.center()
self.after(self.slide_interval, self.start)
def start(self):
"""Start method"""
self.main()
self.mainloop()
if __name__ == "__main__":
slide_interval = 1
import glob
images = glob.glob("traralgon/*.jpg")
# start the slideshow
slideshow = Slideshow(images, slide_interval)
slideshow.start()
Something like this maybe:
import tkinter as tk
class Application(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
self.title("Slideshow")
self.geometry("256x256")
self.resizable(width=False, height=False)
self.current_slide = tk.Label(self)
self.current_slide.pack()
self.duration_ms = 1000
def set_image_directory(self, path):
from pathlib import Path
from PIL import Image, ImageTk
from itertools import cycle
image_paths = Path(path).glob("*.jpg")
self.images = cycle(zip(map(lambda p: p.name, image_paths), map(ImageTk.PhotoImage, map(Image.open, image_paths))))
def display_next_slide(self):
name, self.next_image = next(self.images)
self.current_slide.config(image=self.next_image)
self.title(name)
self.after(self.duration_ms, self.display_next_slide)
def start(self):
self.display_next_slide()
def main():
application = Application()
application.set_image_directory("dir/to/images")
application.start()
application.mainloop()
if __name__ == "__main__":
import sys
sys.exit(main())
You'll have to ask yourself if you want to load all images upfront before the slideshow starts, and just keep them in memory (this could take some time depending on how many images you have), or if you want to load images only when they should be displayed (if the interval between images is especially short, you may notice that loading the next image slows things down).
The problem is that you're calling self.main() and self.mainloop() on every interval. That will cause huge performance problems and will probably crash after only a second or two. You should never call mainloop() more than once, and there's no point in recreating the entire UI on every loop.
Instead, you need to write a function that gets the image and then configures the existing label rather than recreating the whole GUI on each iteration.
Example:
def main(self):
...
self.next_image()
def next_image(self):
self.image_name = next(self.images)
filename, ext = os.path.splitext(self.image_name)
self.image = tkinter.PhotoImage(file=self.image_name)
self.slide.configure(image=self.image)
self.after(self.slide_interval, self.next_image)