How to build image viewer app with ipywidgets using OOP? - python

I want to build an image viewer app (like the Photos app on Windows) with ipywidgets, where I can step through images and extend it to delete them if required. I am using the code from here.
I would like to use an object oriented approach instead of using global variables as its confusing, but I can't figure out how to get the image to update when the prev_image or next_image buttons are clicked, similar to how a button is connected to a function in PyQt5. How can I do this? Here is my code:
import os
from ipywidgets import Button, Box
from IPython.display import display, Image
class ImageApp():
def __init__(self, path):
self.prev_image = widgets.Button(layout=Layout(width='auto', height='auto'),
description='Prev',
icon='backward')
self.next_image = widgets.Button(layout=Layout(width='auto', height='auto'),
description='Next',
icon='forward')
self.out = widgets.Output()
self.i =1
self.list_of_images = self.get_list_of_images(path)
self.image = Image(self.list_of_images[self.i], width = 600, height = 300)
with self.out:
display(self.image)
# linking button and function together using a button's method
self.prev_image.on_click(on_prev_button_clicked)
self.next_image.on_click(on_next_button_clicked)
self.buttons = Box([self.prev_image, self.next_image])
self.buttons.layout.display = 'flex'
self.buttons.layout.justify_content = 'center'
self.image_layout = Box([self.out])
self.image_layout.layout.display = 'flex'
self.image_layout.layout.justify_content = 'center'
display(self.image_layout, self.buttons)
def get_list_of_images(self, path):
list_of_images = []
for image in os.listdir(path):
if image.endswith('.jpg'):
imagepath=os.path.join(path,image)
list_of_images.append(imagepath)
return list_of_images
def on_next_button_clicked(self,_):
# "linking function with output"
with self.out:
# what happens when we press the button
self.out.clear_output()
self.i +=1
self.image = Image(self.list_of_images[self.i], width = 600, height = 300)
display(self.image)
def on_prev_button_clicked(self,_):
with self.out:
# what happens when we press the button
self.out.clear_output()
self.i -=1
self.image = Image(self.list_of_images[self.i], width = 600, height = 300)
display(self.image)
ImageApp(path)

Related

Resizing Canvas on Tkinter first run issue

I want to make a program that begins as a small window, then when given a path to an image, it maximises the screen and places the image in the centre.
If you run the code below you will see that the window maximises, the image is loaded into memory, the code runs with no errors and self.open_image calls self.draw_image(self.pimg) which runs without error, however the image is not present on the canvas.
If I click the button "Fix" and call self.fix it calls self.draw_image(self.pimg) which runs without error and correctly draws the image.
How can you call the same function twice with the same arguments and get different results. What is different.
I get the feeling this is happening because something has taken place in the main loop that hasn't taken place at the end of self.__init__, so that when i call self.draw_image the second time self.cv.create_image is able to interact with something in the resizable canvas.
In this example I am happy to assume the program will always begin as a small window and become a maximised window untill it is closed, never being resized again, however in my real program I would like to make it more dynamic where the window handles resizing sensibly, this is just a minimum reproducible example. It is for this reason that I would like to use the ResizingCanvas class (or one like it) even though I feel that it is likely the cause of the issue I am experiencing.
I have tried using breakpoints and stepping through the code watching the variables get created but I cant see the difference between the self.cv the first time around and self.cv after I click the button.
I read about a similar issue here on this question and he suggests binding "<Configure>" To the canvas and passing the coords from the event to the canvas. However this has already been implemented in ResizingCanvas
from tkinter import *
from PIL import Image, ImageTk
class ResizingCanvas(Canvas):
# https://stackoverflow.com/a/22837522/992644
def __init__(self,parent,**kwargs):
Canvas.__init__(self,parent,**kwargs)
self.bind("<Configure>", self.on_resize)
self.height = self.winfo_reqheight()
self.width = self.winfo_reqwidth()
def on_resize(self,event):
""" determine the ratio of old width/height to new width/height"""
wscale = float(event.width)/self.width
hscale = float(event.height)/self.height
self.width = event.width
self.height = event.height
# resize the canvas
self.config(width=self.width, height=self.height)
# rescale all the objects tagged with the "all" tag
self.scale("all",0,0,wscale,hscale)
class main():
def __init__(self, name = None):
self.root = Tk()
self.name = name # Filename
myframe = Frame(self.root)
myframe.pack(fill=BOTH, expand=YES)
self.cv = ResizingCanvas(myframe, width=850, height=400, bg="dark grey", highlightthickness=0)
self.cv.pack(fill=BOTH, expand=YES)
self.b = Button(self.cv, text = 'Fix', command = self.fix).grid(row=1,column=1)
self.open_img()
def draw_image(self, img, x = None, y = None):
""" Handles the drawing of the main image"""
self.img = ImageTk.PhotoImage(img)
self.cv.create_image(self.root.winfo_screenwidth()/2,
self.root.winfo_screenheight()/2, image=self.img, tags=('all'))
def open_img(self, event=''):
self.pimg = Image.open(self.name)
self.root.state("zoomed")
self.draw_image(self.pimg)
def fix(self, event=''):
self.draw_image(self.pimg)
def run(self):
self.root.mainloop()
if __name__ == "__main__":
path = 'example.png'
app = main(path)
app.run()
What should happen in the video:
I click run and the image is displayed immediately, without having to click the fix button.
What does happen in the video:
I click run and the image is not displayed until I click the fix button, afterwhich it works.
Changing
self.root.state("zoomed") to self.root.state("normal")
in your code (I am working on Python3) I can only get:
[
the image above, played a little bit starting from How to get tkinter canvas to dynamically resize to window width?
and now the code seems to work with me:
from time import sleep
from tkinter import *
from PIL import Image, ImageTk
class ResizingCanvas(Canvas):
# https://stackoverflow.com/a/22837522/992644
def __init__(self,parent, **kwargs):
# Canvas.__init__(self,parent,**kwargs)
print(kwargs)
Canvas.__init__(self,parent,**kwargs)
self.bind("<Configure>", self.on_resize)
# self.height = self.winfo_reqheight()
# self.width = self.winfo_reqwidth()
self.height = self.winfo_height()
self.width = self.winfo_width()
# self.height = height
# self.width = width
# self.__dict__.update(kwargs)
def on_resize(self,event):
""" determine the ratio of old width/height to new width/height"""
wscale = (event.width)//self.width
hscale = (event.height)//self.height
self.width = event.width
self.height = event.height
# resize the canvas
self.config(width=self.width, height=self.height)
# rescale all the objects tagged with the "all" tag
self.scale("all",0,0,wscale,hscale)
class main():
def __init__(self, name = None):
self.pippo = Tk()
self.name = name # Filename
self.myframe = Frame(self.pippo)
self.myframe.pack(side = BOTTOM, expand=YES)
# myframe.pack(fill=BOTH, expand='TRUE')
self.cv = ResizingCanvas(self.myframe, width=850, height=400, bg="dark grey", highlightthickness=0)
self.cv.pack(fill=BOTH, expand=YES)
# sleep(2)
self.b = Button(self.myframe, text = 'Fix', command = self.fix)#.grid(row=1,column=1)
self.b.pack(side=TOP)
self.open_img()
# self.pippo.mainloop() ## use it if you eliminate def run
def draw_image(self, img, x = None, y = None):
""" Handles the drawing of the main image"""
self.img = ImageTk.PhotoImage(img)
# self.cv.create_image(self.pippo.winfo_screenwidth()/2,
# self.pippo.winfo_screenheight()/2, image=self.img, tags=('all'))
self.cv.create_image(self.pippo.winfo_width()/2,
self.pippo.winfo_reqheight()/2, image=self.img, tags=('all'))
def open_img(self, event=''):
self.pimg = Image.open(self.name)
self.pippo.state("normal")
self.draw_image(self.pimg)
def fix(self, event=''):
self.draw_image(self.pimg)
def run(self):
self.pippo.mainloop()
if __name__ == "__main__":
path = 'example.png'
app = main(path)
app.run()
don't know about your question though, but wanted to be sure your starting example works right. Let me know if it could be related to python/pillow/tkinter version or something else
Here my window image results before ad after pressing fix button :
At the end found out that your code does work as long as you use
self.root.attributes('-zoomed', True) instead of `self.root.state("zoomed")`
The problem is here. self.root.winfo_screenwidth()
Change it to self.cv.width. I don't know why.
def draw_image(self, img, x = None, y = None):
""" Handles the drawing of the main image"""
self.img = ImageTk.PhotoImage(img)
self.cv.create_image(self.root.winfo_screenwidth()/2,
self.root.winfo_screenheight()/2, image=self.img, tags=('all'))
Change the last line to
self.cv.create_image(self.cv.width/2,
self.cv.height/2, image=self.img, tags=('all'))
Fixes the issue.
Tk.winfo_screenwidth() according to https://tkdocs.com/shipman/universal.html returns the width of the screen, indepedant of the size of the window, so even if you have a small window on a 1920x1080 display, this function will return 1920.
self.cv.width returns the width of the canvas object.

TKinter dealing with multiple monitors and image display

I have functioning code but there are a few things which I would like to change about it but don't know how to so thought i'd ask here. My code is as follows:
import numpy as np
from PIL import Image
import matplotlib.pyplot as plt
import tkinter as tk
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
#Define the target, source and output arrays. Source has to be completely white otherwise it kills everything
def initialize(x,y):
xarr = np.zeros(x)
yarr = np.zeros(y)
target = np.meshgrid(xarr,yarr)
target = target[0]
source = np.meshgrid(xarr,yarr)
source = source[0]
output = np.meshgrid(xarr,yarr)
output = output[0]
for i in range(x):
for n in range(y):
source[n][i] = 1
return target, source, output
# creates trap between XTrapMin-XTrapMax and YTrapMin-YTrapMax on Array
def trap(xtmi,xtma,xs,ytmi,ytma,ys,array):
for i in range(xs):
if xtmi < i < xtma:
for n in range(ys):
if ytmi < n < ytma:
array[n][i] = 255
return
#Returns the amplitude of a complex number
def Amplitude(x):
if isinstance(x, complex):
return np.sqrt(x.real**2+x.imag**2)
else:
return np.abs(x)
#Returns the phase of a complex number
def Phase(z):
return np.angle(z)
#Main GS algorithm implementation using numpy FFT package
#performs the GS algorithm to obtain a phase distribution for the plane, Source
#such that its Fourier transform would have the amplitude distribution of the plane, Target.
def GS(target,source):
A = np.fft.ifft2(target)
for i in range(5):
B = Amplitude(source) * np.exp(1j * Phase(A))
C = np.fft.fft2(B)
D = Amplitude(target) * np.exp(1j * Phase(C))
A = np.fft.ifft2(D)
output = Phase(A)
return output
#Make array into PIL Image
def mkPIL(array):
im = Image.fromarray(np.uint8(array))
return im
def up():
global ytmi
global ytma
ytmi -= 10
ytma -= 10
return
def down():
global ytmi
global ytma
ytmi += 10
ytma += 10
return
def right():
global xtmi
global xtma
xtmi += 10
xtma += 10
return
def left():
global xtmi
global xtma
xtmi -= 10
xtma -= 10
return
xtmi = 125
xtma = 130
xs = 1024
ytmi = 0
ytma = 5
ys = 768
root = tk.Tk()
root.attributes('-fullscreen', True)
def main():
app = Lower(root)
root.mainloop()
class Lower:
def __init__(self, master):
self.master = master
self.frame = tk.Frame(self.master).pack()
self.displayimg = tk.Button(self.frame, text = 'Display', width = 25, command = self.plot)
self.displayimg.pack()
self.makewidg()
def makewidg(self):
self.fig = plt.figure(figsize=(100,100), frameon=False) #changing figsize doesnt cange the size of the plot display
self.fig.subplots_adjust(left=0, right=1, top=1, bottom=0)
self.fig.tight_layout()
self.ax = self.fig.add_subplot(111)
self.ax.set_yticklabels([])
self.ax.set_xticklabels([])
self.canvas = FigureCanvasTkAgg(self.fig, master=self.master)
self.canvas.get_tk_widget().pack(expand=True)
self.canvas.figure.tight_layout()
self.canvas.draw()
self.new_window()
def new_window(self):
self.newWindow = tk.Toplevel()
self.app = Display(self.newWindow)
def plot(self):
global xtmi, xtma, xs, ytmi, ytma, ys, i
target,source,output=initialize(xs,ys)
trap(xtmi,xtma,xs,ytmi,ytma,ys,target)
output = GS(target,source)
self.ax.imshow(output, cmap='gray')
self.ax.set_yticklabels([])
self.ax.set_xticklabels([])
self.canvas.draw()
self.ax.clear()
def kill(self):
root.destroy()
class Display:
def __init__(self, master):
self.master = master
self.frame = tk.Frame(self.master)
self.frame.pack()
self.up = tk.Button(self.frame, text = 'Up', width = 25, command = up)
self.up.pack()
self.down = tk.Button(self.frame, text = 'Down', width = 25, command = down)
self.down.pack()
self.right = tk.Button(self.frame, text = 'Right', width = 25, command = right)
self.right.pack()
self.left = tk.Button(self.frame, text = 'Left', width = 25, command = left)
self.left.pack()
self.kill = tk.Button(self.frame, text = 'Kill', width = 25, command = self.kill)
self.kill.pack()
def kill(self):
root.destroy()
main()
Currently the button displayimg from the class Lower is displayed above the image, is there a way in which I can have the display button on the Display class and still have it manipulate the image on the Lower screen? Also, I intend to display the window opened by Lower on a separate monitor, but can't drag it seeing as it is fullscreen, is there a way around that I can get it on my second monitor?
I try that as such:
self.displayimg = tk.Button(self.top, text = 'Display', width = 25, command = Lower.plot(Lower))
self.displayimg.pack()
But this causes a misreference I think as I get an error code
AttributeError: type object 'Lower' has no attribute 'ax'
Call to Lower.plot
You are using Lower.plot as your button command. It needs one argument, self which must be an instance of Lower - so Lower.plot(Lower) is passing a class where an instance is expected. Instead you need to use the app instance you've made, and call app.plot(). The arguement self is automatically the instance itself, this is fundamental to OOP in python. Calling the method on an instance passes self as the first arg, so it's missing from the call. Calling Lower.plot(...) is calling the method on the class Lower, so there is no instance, and you have to supply your own. I'd avoid calling methods without an instance like this in this situation, and use your app instance.
Command for the display button
Your button creation becomes something like:
self.displayimg = tk.Button(self.top, text = 'Display', width = 25, command = app.plot)
If you need to pass additional args to plot, you need to delay the function call so it happens on click, not on creation of the button. You can use lambda : app.plot("red", 10, whatever) to make a nameless function, taking no arguments, that when called will go on to call app.plot with the given args.
Positioning the window
You can control the position of the app window using wm_geometry:
app.wm_geometry("200x200+100+500")
Will cause the app window to be 200px by 200px, positioned 100px left and 500px down from the origin, and on a windows machine, this is the top left corner of your primary monitor. You can keep the width and height the same and just move the window with eg
app.wm_geometry("+100+500")
You can use more scripting to build the string +{xpos}+{ypos} with whichever values you like to match your desktop layout.

How to update an image repeatedly in tkinter

This is a function that creates an image:
def footgraph(self):
load = Image.open('centertext_out.png')
load= load.resize((500, 500), Image.ANTIALIAS)
render = ImageTk.PhotoImage(load)
self.img = Label( image=render)
self.img.image = render
self.img.place(x=150, y=5)
self.scale = tk.Scale(self.win, variable=self.value, orient="horizontal",length = 200,
from_=self.df['index'].unique().min(), to=self.df['index'].unique().max(), resolution =1,command=self.updateScaleFoot)
self.scale.place(x=250, y = 500)
self.stop.pack(expand='true',fill='both')
self.stop.place(x=200, y =500)
self.play.pack(expand='true',fill='both')
self.play.place(x=150, y = 500)
Here is a code for play button that keeps updating image:
while True:
self.index += 1
#update image(centertext_out.png) and save
load = Image.open('centertext_out.png')
load= load.resize((500, 500), Image.ANTIALIAS)
render = ImageTk.PhotoImage(load)
img = Label( image=render)
img.image = render
img.place(x=150, y=5)
time.sleep(10)
This loop is working fine. Self.index keeps updating. But image is not updating and screen hangs.
Edit:
When I use slider new image gets appended to previous one like this :
Edit:
I have narrowed down the problem. Below is the code for play function. When I click bar graph and if for bar graph turns true then code runs smoothly but it doesnt seem to be working for the second statement even if I have nothing inside of it.
def startanimation(self):
self.pause = 0
print("pause"+ str(self.pause))
while True:
if self.pause == 1:
break
self.index = self.index +1
print ("scale is now %s" % (self.index))
if "bar" in self.graphtype:
#some code
#draw canvas
self.fig.canvas.draw()
self.fig.canvas.flush_events()
time.sleep(0.2)
if "foot" in self.graphtype:
print("inside")
time.sleep(0.2)
Edit:
Changed code according to one of the answers.
class Application:
def __init__(self, master):
self.win = master
self.geo = self.win.geometry
self.geo("800x800+400+400")
self.win['bg'] = "black"
####################################################some code
def startanimation(self):
self.pause = 0
print("pause"+ str(self.pause))
if "foot" in self.graphtype:
self.win.after(1,self.test)
def test(self):
print("hi")
self.pause = 0
guide = pd.read_csv("guide.csv")
print("hey")
self.index +=1
test is called only once. hey is printed only once
Solved: check this link
Python Tkinter after() Only Executing Once

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

Image viewer/ images for buttons

I'm trying to implement simple image viewer, where you can choose from 2 pictures. I have one menubutton which offers these pictures. After choosing one of the images, the program creates 3 or 5 buttons. I would like to append to each of these buttons different images, so the first button would have one image and the second button would have another image and so on. I've created a function with for loop to create these buttons, but can't move on from that point. I can append one image to all of them, but can't do that one by one with different images.
Thanks for help
try:
import Tkinter as tk
except ImportError:
import tkinter as tk
from functools import partial
from PIL import Image, ImageTk
class Halabala():
def __init__(self):
self.master = tk.Tk()
self.master.geometry("1100x700")
self.lists_labels = []
self.rbutton = tk.Menubutton(self.master, text = "Choose picture")
self.picks2 = tk.Menu(self.rbutton)
self.rbutton.config(menu=self.picks2)
self.picks2.add_command(label = "Spider", command = partial(self.create_labels,3))
self.picks2.add_command(label = "Sugar", command = partial(self.create_labels,5))
self.rbutton.config(width = 30, bg = "white", bd = 5, relief = tk.RAISED)
self.rbutton.place(x = 900, y = 30)
self.master.mainloop()
def create_labels(self, num):
self.picture = Image.open("blue.gif")
self.picture.thumbnail((130,130))
self.tkim = ImageTk.PhotoImage(self.picture)
for label in self.lists_labels:
label.destroy()
self.lists_labels=[]
for i in range(num):
but = tk.Button(self.master, image = self.tkim)
but.grid(row = i + 1, column = 0)
self.lists_labels.append(but)
myapp = Halabala()
This is the code relevant for your question:
class Halabala():
def __init__(self):
.............
self.pictures = ["pavuk1", "pavuk2", "pavuk3"]
self.lists_labels = []
self.lists_images = []
self.init_image_list()
............
def init_image_list(self):
for i in self.pictures:
picture = Image.open(i)
picture.thumbnail((130, 130))
self.lists_images.append(ImageTk.PhotoImage(picture))
def create_labels(self, num):
for label in self.lists_labels:
label.destroy()
self.lists_labels=[]
for i in range(num):
but = tk.Button(self.master, image = self.lists_images[i])
but.grid(row = i + 1, column = 0)
self.lists_labels.append(but)

Categories

Resources