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)
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!
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 am working to extend this solution given to me previously.
The user can draw randomly using the mouse on a TkInter Canvas widget, after that, the same curves with the same pixels coordinates are drawn on the OpenCV window.
I want to modify that solution so that the TkInter Canvas and the OpenCV window must be shown in the same time: each time the suer finishes drawing one curve on TkInter it is immediately redrawn on the OpenCV window.
Is there a way to fulfill this goal ?
Thank you in advance for any help.
This is arguably even easier to do than to draw all lines in OpenCV later.
I'd make the MaClasse class only make a blank image, open a window and show it on initiation, and give it a method to draw a single line and show the image again.Then you can create a MaClasse object in Test and draw a line in OpenCV each time you draw a line in Tkinter. Then you won't even need to save all lines you've drawn (you can completely remove self.liste).
from Tkinter import *
import numpy as np
import cv2
class Test:
def __init__(self):
self.b1="up"
self.xold=None
self.yold=None
self.liste=[]
self.maclasse = MaClasse()
def test(self,obj):
self.drawingArea=Canvas(obj)
self.drawingArea.pack()
self.drawingArea.bind("<Motion>",self.motion)
self.drawingArea.bind("<ButtonPress-1>",self.b1down)
self.drawingArea.bind("<ButtonRelease-1>",self.b1up)
def b1down(self,event):
self.b1="down"
def b1up(self,event):
self.b1="up"
self.xold=None
self.yold=None
self.liste.append((self.xold,self.yold))
def motion(self,event):
if self.b1=="down":
if self.xold is not None and self.yold is not None:
event.widget.create_line(self.xold,self.yold,event.x,event.y,fill="red",width=3,smooth=TRUE)
self.maclasse.dessiner_ligne(self.xold,self.yold,event.x,event.y)
self.xold=event.x
self.yold=event.y
self.liste.append((self.xold,self.yold))
class MaClasse:
def __init__(self):
self.s=600,600,3
self.ma=np.zeros(self.s,dtype=np.uint8)
cv2.namedWindow("OpenCV",cv2.WINDOW_AUTOSIZE)
cv2.imshow("OpenCV",self.ma)
def dessiner_ligne(self, xold, yold, x, y):
cv2.line(self.ma,(xold, yold),(x,y),[255,255,255],2)
cv2.imshow("OpenCV",self.ma)
if __name__=="__main__":
root = Tk()
root.wm_title("Test")
v = Test()
v.test(root)
root.mainloop()
Since the above code using a Tkinter window and a OpenCV window does not work for you, you could also show the OpenCV image in a Tkinter Toplevel window.
from Tkinter import *
import numpy as np
import cv2
import Image, ImageTk
class Test:
def __init__(self, parent):
self.parent = parent
self.b1="up"
self.xold=None
self.yold=None
self.liste=[]
self.maclasse = MaClasse(self.parent)
def test(self):
self.drawingArea=Canvas(self.parent)
self.drawingArea.pack()
self.drawingArea.bind("<Motion>",self.motion)
self.drawingArea.bind("<ButtonPress-1>",self.b1down)
self.drawingArea.bind("<ButtonRelease-1>",self.b1up)
def b1down(self,event):
self.b1="down"
def b1up(self,event):
self.b1="up"
self.xold=None
self.yold=None
self.liste.append((self.xold,self.yold))
def motion(self,event):
if self.b1=="down":
if self.xold is not None and self.yold is not None:
event.widget.create_line(self.xold,self.yold,event.x,event.y,fill="red",width=3,smooth=TRUE)
self.maclasse.dessiner_ligne(self.xold,self.yold,event.x,event.y)
self.xold=event.x
self.yold=event.y
self.liste.append((self.xold,self.yold))
class MaClasse:
def __init__(self, parent):
self.s=600,600,3
self.ma=np.zeros(self.s,dtype=np.uint8)
self.top = Toplevel(parent)
self.top.wm_title("OpenCV Image")
self.label = Label(self.top)
self.label.pack()
self.show_image()
def dessiner_ligne(self, xold, yold, x, y):
cv2.line(self.ma,(xold, yold),(x,y),[255,255,255],2)
self.show_image()
def show_image(self):
self.im = Image.fromarray(self.ma)
self.imgtk = ImageTk.PhotoImage(image=self.im)
self.label.config(image=self.imgtk)
if __name__=="__main__":
root = Tk()
root.wm_title("Test")
v = Test(root)
v.test()
root.mainloop()
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.
I am trying to display live images from my 1394 camera.
Currently my code is able to obtain images in a loop from the camera and I was looking for any quick GUI that will update dynamically (as a separate thread). I can do this in PyQt maybe using QThreads but is there any recommendation or faster way of doing this??
Here's my code
#Loop capturing frames from camera
for frame in range(1,500):
print 'frame:',frame
TIME.sleep(1) #capture frame every second
image_binary = pycam.cam.RetrieveBuffer()
#convert to PIL Image
pilimg = PIL.Image.frombuffer("L",(cimg.GetCols(),cimg.GetRows()),image_binary,'raw', "RGBA", 0, 1)
# At this point I want to send my image data to a GUI window and display it
Thank you.
Here's wxPython code that will do it...
import wx
from PIL import Image
SIZE = (640, 480)
def get_image():
# Put your code here to return a PIL image from the camera.
return Image.new('L', SIZE)
def pil_to_wx(image):
width, height = image.size
buffer = image.convert('RGB').tostring()
bitmap = wx.BitmapFromBuffer(width, height, buffer)
return bitmap
class Panel(wx.Panel):
def __init__(self, parent):
super(Panel, self).__init__(parent, -1)
self.SetSize(SIZE)
self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM)
self.Bind(wx.EVT_PAINT, self.on_paint)
self.update()
def update(self):
self.Refresh()
self.Update()
wx.CallLater(15, self.update)
def create_bitmap(self):
image = get_image()
bitmap = pil_to_wx(image)
return bitmap
def on_paint(self, event):
bitmap = self.create_bitmap()
dc = wx.AutoBufferedPaintDC(self)
dc.DrawBitmap(bitmap, 0, 0)
class Frame(wx.Frame):
def __init__(self):
style = wx.DEFAULT_FRAME_STYLE & ~wx.RESIZE_BORDER & ~wx.MAXIMIZE_BOX
super(Frame, self).__init__(None, -1, 'Camera Viewer', style=style)
panel = Panel(self)
self.Fit()
def main():
app = wx.PySimpleApp()
frame = Frame()
frame.Center()
frame.Show()
app.MainLoop()
if __name__ == '__main__':
main()
I thought I'd try PyQt4 imageviewer.py example and it worked for me.
Thanks for all your help guys.
Here's my modified code:
from PyQt4 import QtCore, QtGui
class CameraViewer(QtGui.QMainWindow):
def __init__(self):
super(CameraViewer, self).__init__()
self.imageLabel = QtGui.QLabel()
self.imageLabel.setBackgroundRole(QtGui.QPalette.Base)
self.imageLabel.setScaledContents(True)
self.scrollArea = QtGui.QScrollArea()
self.scrollArea.setWidget(self.imageLabel)
self.setCentralWidget(self.scrollArea)
self.setWindowTitle("Image Viewer")
self.resize(640, 480)
timer = QtCore.QTimer(self)
timer.timeout.connect(self.open)
timer.start(33) #30 Hz
def open(self):
#get data and display
pilimg = getMyPILImageDatFromCamera()
image = PILQT.ImageQt.ImageQt(pilimg)
if image.isNull():
QtGui.QMessageBox.information(self, "Image Viewer","Cannot load %s." % fileName)
return
self.imageLabel.setPixmap(QtGui.QPixmap.fromImage(image))
self.imageLabel.adjustSize()
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
CameraViewer = CameraViewer()
CameraViewer.show()
sys.exit(app.exec_())
I recommend using Tkinter since it's already part of python. I've never used PIL but a quick google shows it's easy to use PIL images in Tk widgets (via the pil.ImageTk.PhotoImage() method).
If you already have a Tkinter widget set up to display images (a Label widget works fine) all you need to do is arrange for the image to be updated every second or so. You can do this by using the after command of tkinter.
Here's an example; I don't have PIL so it uses a static image but it illustrates how to use the event loop to fetch images every second:
import Tkinter
class App(Tkinter.Tk):
def __init__(self):
Tkinter.Tk.__init__(self)
self.label = Tkinter.Label(text="your image here", compound="top")
self.label.pack(side="top", padx=8, pady=8)
self.iteration=0
self.UpdateImage(1000)
def UpdateImage(self, delay, event=None):
# this is merely so the display changes even though the image doesn't
self.iteration += 1
self.image = self.get_image()
self.label.configure(image=self.image, text="Iteration %s" % self.iteration)
# reschedule to run again in 1 second
self.after(delay, self.UpdateImage, 1000)
def get_image(self):
# this is where you get your image and convert it to
# a Tk PhotoImage. For demonstration purposes I'll
# just return a static image
data = '''
R0lGODlhIAAgALMAAAAAAAAAgHCAkC6LV76+vvXeswD/ANzc3DLNMubm+v/6zS9PT6Ai8P8A////
/////yH5BAEAAAkALAAAAAAgACAAAAS00MlJq7046803AF3ofAYYfh8GIEvpoUZcmtOKAO5rLMva
0rYVKqX5IEq3XDAZo1GGiOhw5rtJc09cVGo7orYwYtYo3d4+DBxJWuSCAQ30+vNTGcxnOIARj3eT
YhJDQ3woDGl7foNiKBV7aYeEkHEignKFkk4ciYaImJqbkZ+PjZUjaJOElKanqJyRrJyZgSKkokOs
NYa2q7mcirC5I5FofsK6hcHHgsSgx4a9yzXK0rrV19gRADs=
'''
image = Tkinter.PhotoImage(data=data)
return image
if __name__ == "__main__":
app=App()
app.mainloop()
Since the good answers are pretty large, I feel like I should post a library I built specifically for this:
from cvpubsubs.webcam_pub import VideoHandlerThread
import numpy as np
image_np = numpy.array(pilImage)
def update_function(frame, cam_id):
frame[...] = image_np[...]
VideoHandlerThread(video_source=image_np, callbacks=update_function).display()
Actually, that's if image_binary is a new numpy array every time. If it's assigned to the same location, then just this should work:
from cvpubsubs.webcam_pub import VideoHandlerThread
VideoHandlerThread(video_source=image_np).display()
I know OpenCV barely counts as a GUI, but this is quick code wise.
Try to take a look at gstreamer. This is the first result google gave me searching for "gstreamer 1394" and this one is the first for "gstreamer pyqt".