Embedding Images and matplotlib Plots together on on tk canvas - python-3.x

A few years ago I wrote a script in python2 that would pull images and generate plots and show them together in one gui, following this old documentation. However, I'm trying to update this to python3, and I've run into an issue with the way that plots are put on tk windows in the latest version of matplotlib. There are updated guides for how to embed matplotlib graphs in tk windows, of course, but these don't seem to be exactly what I want, since I want to plot various images and graphs together.
Currently:
# Start by creating the GUI as root
root=Tk()
root.wm_title("JADESView")
# Create the canvas
canvas=Canvas(root, height=canvasheight, width=canvaswidth)
# Plot the EAZY SED
#image = Image.open(EAZY_files+str(current_id)+"_EAZY_SED.png")
image = getEAZYimage(current_id)
# Crop out the thumbnails
image = cropEAZY(image)
photo = resizeimage(image)
item4 = canvas.create_image(eazy_positionx, eazy_positiony, image=photo)
Label(root, text="EAZY FIT", font=('helvetica', int(textsizevalue*1.5))).place(x=eazytext_positionx, y = eazytext_positiony)
# Plot the BEAGLE SED
#new_image = Image.open(BEAGLE_files+str(current_id)+"_BEAGLE_SED.png")
new_image = getBEAGLEimage(current_id)
new_photo = resizeimage(new_image)
item5 = canvas.create_image(beagle_positionx, beagle_positiony, image=new_photo)
Label(root, text="BEAGLE FIT", font=('helvetica', int(textsizevalue*1.5))).place(x=beagletext_positionx, y = beagletext_positiony)
canvas.pack(side = TOP, expand=True, fill=BOTH)
# Plot the thumbnails
fig_photo_objects = np.empty(0, dtype = 'object')
fig_photo_objects = create_thumbnails(canvas, fig_photo_objects, current_id, current_index, defaultstretch)
I create a tk.canvas object, and then use canvas.create_image to place two PIL photo objects, with labels (the "EAZY SED" and BEAGLE SED"), and this still works with python3. However, the create_thumbnails function I've written creates individual figure objects and then calls a function "draw_figure" to embed them, which I'll post below, but it's from the tk example I linked above:
def draw_figure(canvas, figure, loc=(0, 0)):
""" Draw a matplotlib figure onto a Tk canvas
loc: location of top-left corner of figure on canvas in pixels.
Inspired by matplotlib source: lib/matplotlib/backends/backend_tkagg.py
"""
figure_canvas_agg = FigureCanvasAgg(figure)
figure_canvas_agg.draw()
figure_x, figure_y, figure_w, figure_h = figure.bbox.bounds
figure_w, figure_h = int(figure_w), int(figure_h)
photo = PhotoImage(master=canvas, width=figure_w, height=figure_h)
# Position: convert from top-left anchor to center anchor
canvas.create_image(loc[0] + figure_w/2, loc[1] + figure_h/2, image=photo)
# Unfortunately, there's no accessor for the pointer to the native renderer
tkagg.blit(photo, figure_canvas_agg.get_renderer()._renderer, colormode=2)
# Return a handle which contains a reference to the photo object
# which must be kept live or else the picture disappears
return photo
And this breaks in all sorts of ways.
I'm open to overhauling how everything is done, but I don't see examples that show how this sort of thing might work. The examples for the latest version of matplotlib all explain that you should use FigureCanvasTkAgg, but I don't exactly know how to use this if I already have an existing canvas with photos embedded.
Any help would be appreciated, and I can also explain more if necessary!

Related

Qt6/PySide6 - how to position graphics elements

I'm new to Qt and trying to figure out how position graphics items?
For example, I want to overlay some text onto a video. However, instead of overlaying them, the text is vertically stacked above the video.
My code is below, I've tried setting the position of the video/text elements (e.g. video.setPos(0,0)) but it didn't work. I also tried using QGraphicsLayout but ran into problems adding the video/text elements.
import sys
from PySide6 import QtWidgets, QtCore, QtMultimedia, QtMultimediaWidgets
class MyWidget(QtWidgets.QWidget):
def __init__(self):
super().__init__()
view = QtWidgets.QGraphicsView(self)
# Create a QT player
player = QtMultimedia.QMediaPlayer(self)
player.setSource(QtCore.QUrl("https://archive.org/download/SampleVideo1280x7205mb/SampleVideo_1280x720_5mb.mp4"))
video = QtMultimediaWidgets.QGraphicsVideoItem()
player.setVideoOutput(video)
text = QtWidgets.QGraphicsTextItem("Hello World")
scene = QtWidgets.QGraphicsScene()
scene.addItem(video)
scene.addItem(text)
view.setScene(scene)
view.setFixedSize(800,800)
player.play()
player.setLoops(-1)
if __name__ == "__main__":
app = QtWidgets.QApplication([])
widget = MyWidget()
widget.resize(800, 800)
widget.show()
sys.exit(app.exec())
The problem is caused by two aspects:
QGraphicsVideoItem has a default size of 320x240; for conceptual and optimization reasons, it does not change its size when video content is loaded or changed;
the video you're using has a different aspect ratio: 320x240 is 4:3, while the video is 1280x720, which is the standard widescreen ratio, 16:9;
By default, QGraphicsVideoItem just adapts the video contents to its current size, respecting the aspectRatioMode. The result is that you get some blank space above and below the video, similarly to what was common when showing movies in old TVs ("letterboxing"):
Since graphics items are always positioned using the top left corner of their coordinate system, you see a shifted image. In fact, if you just print the boundingRect of the video item when playing starts, you'll see that it has a vertical offset:
player.mediaStatusChanged.connect(lambda: print(video.boundingRect()))
# result:
QtCore.QRectF(0.0, 30.0, 320.0, 180.0)
^^^^ down by 30 pixels!
There are various possibilities to solve this, but it all depends on your needs, and it all resorts on connecting to the nativeSizeChanged signal. Then it's just a matter of properly setting the size, adjust the content to the visible viewport area, or eventually always call fitInView() while adjusting the position of other items (and ignoring transformations).
For instance, the following code will keep the existing size (320x240) but will change the offset based on the adapted size of the video:
# ...
self.video = QtMultimediaWidgets.QGraphicsVideoItem()
# ...
self.video.nativeSizeChanged.connect(self.updateOffset)
def updateOffset(self, nativeSize):
if nativeSize.isNull():
return
realSize = self.video.size()
scaledSize = nativeSize.scaled(realSize,
QtCore.Qt.AspectRatioMode.KeepAspectRatio)
yOffset = (scaledSize.height() - realSize.height()) / 2
self.video.setOffset(QtCore.QPointF(0, yOffset))

Transparent background with matplotlib and tkinter

I'm programming a data-analysis programm for whatsapp chats and I have a matplotlib graph in my tkinter window. I know how to customize colors, but how do I set the background to transparent?
f = Figure(figsize=(4.1, 4.1), dpi=100)
f.set_facecolor('xkcd:gray') # this should be transparent
a = f.add_subplot(111)
a.plot(dates, mes_count)
a.xaxis.set_major_locator(MaxNLocator(prune='both',nbins=6))
a.set_facecolor('xkcd:gray')
canvas = FigureCanvasTkAgg(f, mainloop.root)
canvas.get_tk_widget().place(x=190, y=80)
I got a bit frustrated by trying to match the background color of the axes/figure with the background color of the tkinter app on each platform so I did a little digging:
In general, two things need to be done:
Tell matplotlib to use a transparent background for the figure and axes object by setting the facecolor to "none":
f.set_facecolor("none")
a.set_facecolor("none")
Tell the backend renderer to also make the background transparent, e.g. for saving it as png:
f.savefig('mygraph.png', transparent=True)
The problem is: How do I tell this to FigureCanvasTkAgg? Looking at the sourcecode of FigureCanvasTk, that does the drawing, one can see that the tk.Canvas always gets created with a white background. AFAIK a Canvas can't be transparent (please correct me), but when created without a specific background color, it will have the same background color as the Frame surrounding it, given by the ttk.Style of it. So there are two workarounds I can think of:
After creating the FigureCanvasTkAgg, set the background color of the canvas to the same as the current style:
s = ttk.Style()
bg = s.lookup("TFrame", "background")
bg_16bit = self.winfo_rgb(bg)
bg_string = "#" + "".join([hex(bg_color >> 8)[2:] for bg_color in bg_16bit])
canvas.get_tk_widget().config(bg=bg_string)
Lines 3 & 4 are necessary on e.g. Windows, since s.lookup("TFrame", "background") can return a system color name here, that then needs to be converted to a standard #aabbcc hex color
Making your own MyFigureCanvasTk that's just a copy of the code in matplotlib, that skips setting the background color of the Canvas:
class MyFigureCanvasTk(FigureCanvasTk):
# Redefine __init__ to get rid of the background="white" in the tk.Canvas
def __init__(self, figure=None, master=None):
super().__init__(figure)
self._idle_draw_id = None
self._event_loop_id = None
w, h = self.get_width_height(physical=True)
self._tkcanvas = tk.Canvas(
master=master,
#background="white"
width=w,
height=h,
borderwidth=0,
highlightthickness=0,
)
self._tkphoto = tk.PhotoImage(master=self._tkcanvas, width=w, height=h)
self._tkcanvas.create_image(w // 2, h // 2, image=self._tkphoto)
self._tkcanvas.bind("<Configure>", self.resize)
if sys.platform == "win32":
self._tkcanvas.bind("<Map>", self._update_device_pixel_ratio)
self._tkcanvas.bind("<Key>", self.key_press)
self._tkcanvas.bind("<Motion>", self.motion_notify_event)
self._tkcanvas.bind("<Enter>", self.enter_notify_event)
self._tkcanvas.bind("<Leave>", self.leave_notify_event)
self._tkcanvas.bind("<KeyRelease>", self.key_release)
for name in ["<Button-1>", "<Button-2>", "<Button-3>"]:
self._tkcanvas.bind(name, self.button_press_event)
for name in ["<Double-Button-1>", "<Double-Button-2>", "<Double-Button-3>"]:
self._tkcanvas.bind(name, self.button_dblclick_event)
for name in ["<ButtonRelease-1>", "<ButtonRelease-2>", "<ButtonRelease-3>"]:
self._tkcanvas.bind(name, self.button_release_event)
# Mouse wheel on Linux generates button 4/5 events
for name in "<Button-4>", "<Button-5>":
self._tkcanvas.bind(name, self.scroll_event)
# Mouse wheel for windows goes to the window with the focus.
# Since the canvas won't usually have the focus, bind the
# event to the window containing the canvas instead.
# See https://wiki.tcl-lang.org/3893 (mousewheel) for details
root = self._tkcanvas.winfo_toplevel()
root.bind("<MouseWheel>", self.scroll_event_windows, "+")
class MyFigureCanvasTkAgg(FigureCanvasAgg, MyFigureCanvasTk):
def draw(self):
super().draw()
self.blit()
def blit(self, bbox=None):
_backend_tk.blit(
self._tkphoto, self.renderer.buffer_rgba(), (0, 1, 2, 3), bbox=bbox
)
This let's the graph blend in with whatever background color might be surrounding it. It should work on all platforms (tested only on Windows and Linux, with python 3.11, tkinter 8.6.12). Maybe this helps someone stumbling over this question.

Updating bqplot image widget

I'm working on a project that uses ipywidgets and bqplot to display and interact with an image.
Using ipywidgets and open cv I can modify an image, save it and update the value of the widget. But I also need the on_click_element aspect of bqplot, so I use the widget from the last one. I still have problems figuring out how to do the same thing with the widget in bqplot.
I would like to avoid to redraw the hole thing, but if needed it would have to close and redraw only the widget image since this is part of a bigger set of widgets. For example I would like to binarize the image using an arbitrary treshold.
From here I got the information on how to use the bqplot image widget: https://github.com/bqplot/bqplot/blob/master/examples/Marks/Object%20Model/Image.ipynb
I use something very similar to this to create the widget that I display.
from IPython.display import display
import ipywidgets as widgets
import bqplot as bq
with open('chelsea.png', 'rb') as f:
raw_image = f.read()
ipyimage = widgets.Image(value=raw_image, format='png')
x_sc = bq.LinearScale()
y_sc = bq.LinearScale()
bq_image = bq.Image(image=ipyimage, scales={'x':x_sc, 'y':y_sc})
img_ani = bq.Figure(marks=[bq_image], animation_duration=400)
display(img_ani)
After this I can't update the figure without redrawing the hole thing.
Any ideas?
jupyter 5.7.8,
ipython 7.5.0,
ipywidgets 7.5.1,
bqplot 0.12.10
Update the bqplot image mark by assigning a new image....
with open("chelsea2.png", 'rb') as f:
raw_image2 = f.read()
# ipyimage.value = raw_image2 # This doesn't seems to sync with widget display. Would require a redisplay of bqplot figure
# create new ipywidgets image and assign it to bqplot image
ipyimage2 = widgets.Image(value=raw_image2, format='png')
bq_image.image = ipyimage2

Picture zooming doesnt select the right area

I have an application that displays images. Since people need to read some information out of the image i implemented a zoom functionality.
I set the picture Widget to 600x600. To Preserve the aspect ratio i then scale the picture and draw it to the widget. This works really well.
For the zoom functionality the user should click anywhere on the picture and it should cut out the are 150x150 pixesl around where the cursor clicks. To be precises the click of the cursore should mark the middle of the rectangle i cut out. So if i click on x=300 y=300 the area should be x=225 y=225 width=150 height=150.
To Archive that i scale the coordinates where the user clicks back to the original image resolution, cut out the subimage and scale it back down. Cutting out the scaled image allready loaded in my programm would yield a far worse quality.
The error is simple. The area cut out is not exactly the aerea i would like to cut out. Sometimes it is to far left. Sometimes to far right. Somtes to high sometimes to low... And i fail to see where the problem lies.
I wrote a barebone prototype whith just the functionality needed. After you put in a path to a jpeg picture you should be able to run it.
# -*- coding: utf-8 -*-
"""
Created on Sun Jan 12 12:22:25 2020
#author: Paddy
"""
import wx
class ImageTest(wx.App):
def __init__(self, redirect=False, filename=None):
wx.App.__init__(self, redirect, filename)
self.frame = wx.Frame(None, title='Radsteuer Eintreiber')
self.panelleft = wx.Panel(self.frame)
self.picturepresent=False
self.index=0
self.PhotoMaxSize = 600
self.zoomed=False
#Change path here
self.imagepath='F:\Geolocation\Test\IMG_20191113_174257.jpg'
self.createUI()
self.frame.Centre()
self.frame.Show()
self.frame.Raise()
self.onView()
#Creates UI Elements on Initiation
def createUI(self):
#instructions = 'Bild'
img = wx.Image(self.PhotoMaxSize,self.PhotoMaxSize,clear=True)
self.imageCtrl = wx.StaticBitmap(self.panelleft, wx.ID_ANY,
wx.Bitmap(img),size=(self.PhotoMaxSize,self.PhotoMaxSize))
self.mainSizer = wx.BoxSizer(wx.VERTICAL)
self.imageCtrl.Bind(wx.EVT_LEFT_UP, self.onImageClick)
self.mainSizer.Add(self.imageCtrl, 0, wx.ALL|wx.ALIGN_CENTER, 5)
self.panelleft.SetSizer(self.mainSizer)
self.mainSizer.Fit(self.frame)
self.panelleft.Layout()
def onImageClick(self,event):
if self.zoomed:
self.onView()
self.zoomed=False
else :
#Determin position of mouse
ctrl_pos = event.GetPosition()
print(ctrl_pos)
picturecutof=self.PhotoMaxSize/4
if ctrl_pos[0]-((self.PhotoMaxSize-self.NewW)/2)>0:
xpos=ctrl_pos[0]-((self.PhotoMaxSize-self.NewW)/2)
else:
xpos=0
if ctrl_pos[0]+picturecutof>self.NewW:
xpos=self.NewW-picturecutof
if ctrl_pos[1]-((self.PhotoMaxSize-self.NewW)/2)>0:
ypos=ctrl_pos[1]-((self.PhotoMaxSize-self.NewW)/2)
else:
ypos=0
if ctrl_pos[1]+picturecutof>self.NewH:
ypos=self.NewH-picturecutof
xpos=xpos*self.W/self.NewW
ypos=ypos*self.H/self.NewH
picturecutofx=picturecutof*self.W/self.NewW
picturecutofy=picturecutof*self.H/self.NewH
rectangle=wx.Rect(xpos,ypos,picturecutofx,picturecutofy)
self.img = wx.Image(self.imagepath, wx.BITMAP_TYPE_ANY)
self.img=self.img.GetSubImage(rectangle)
self.img=self.img.Scale(600,600,wx.IMAGE_QUALITY_BICUBIC)
self.imageCtrl.SetBitmap(wx.Bitmap(self.img))
self.imageCtrl.Fit()
self.panelleft.Refresh()
self.zoomed=True
def onView(self,event=None):
self.img = wx.Image(self.imagepath, wx.BITMAP_TYPE_ANY)
# scale the image, preserving the aspect ratio
self.W = self.img.GetWidth()
self.H = self.img.GetHeight()
if self.W > self.H:
self.NewW = self.PhotoMaxSize
self.NewH = self.PhotoMaxSize * self.H / self.W
else:
self.NewH = self.PhotoMaxSize
self.NewW = self.PhotoMaxSize * self.W / self.H
self.img = self.img.Scale(self.NewW,self.NewH,wx.IMAGE_QUALITY_BICUBIC)
self.imageCtrl.SetBitmap(wx.Bitmap(self.img))
self.imageCtrl.Fit()
self.panelleft.Refresh()
if __name__ == '__main__':
app = ImageTest()
app.MainLoop()
There maybe some wird stuff happening in the code that doesnt make entirely sense. Most of it is because the normal programm is much bigger and i removed many features in the prototpy that are having nothing to do with the zooming. It might very well be that i do the scaling wrong. But im out of ideas.
The Functionality for this protopye is simple: Replace the Path to a jpge image of your choiche. Run the programm. Click on the image and it should zoom. Click around your image and you will see zooming is wrong.
Thats it. Thanks for your help.
So i found the answer. But i changed something on the logic also. The picture will now be centered at the position where the user clicked. This is much more intuitive to use. I onle post the onImageClick Function. If you want to use the whole thing feel free to replace it in the original code from the question.
def onImageClick(self,event):
if self.zoomed:
self.onView()
self.zoomed=False
else :
#Determin position of mouse
ctrl_pos = event.GetPosition()
#Set magnification.
scalingfactor=4
#Set Picture size for rectangle
picturecutof=self.PhotoMaxSize/scalingfactor
##Find coordinates by adjusting for picture position
xpos=ctrl_pos[0]-((self.PhotoMaxSize-self.NewW)/2)
ypos=ctrl_pos[1]-((self.PhotoMaxSize-self.NewH)/2)
#if position is out of range adjust
if xpos>self.NewW:
xpos=self.NewW
if xpos<0:
xpos=0
if ypos>self.NewH:
ypos=self.NewH
if ypos<0:
ypos=0
#scale rectangle area to size of the unscaled image
picturecutofx=picturecutof*self.W/self.NewW
picturecutofy=picturecutof*self.H/self.NewH
#scale coordinates to unscaled image
xpos=xpos*self.W/self.NewW
ypos=ypos*self.H/self.NewH
#centeres image onto the coordinates where they were clicked
xpos=xpos-((ctrl_pos[0]*self.W/self.NewW)/scalingfactor)
ypos=ypos-((ctrl_pos[1]*self.H/self.NewH)/scalingfactor)
#if position is out of range adjust
if xpos>self.W-picturecutofx:
xpos=self.W-picturecutofx-5
if xpos<0:
xpos=0
if ypos>self.H-picturecutofy:
ypos=self.H-picturecutofy-5
if ypos<0:
ypos=0
#create rectangle to cut from original image
rectangle=wx.Rect(xpos,ypos,picturecutofx,picturecutofy)
#load original image again
self.img = wx.Image(self.imagepath, wx.BITMAP_TYPE_ANY)
#get subimage
self.img=self.img.GetSubImage(rectangle)
#scale subimage to picture area
self.img=self.img.Scale(self.PhotoMaxSize,self.PhotoMaxSize,wx.IMAGE_QUALITY_BICUBIC)
self.imageCtrl.SetBitmap(wx.Bitmap(self.img))
self.imageCtrl.Fit()
self.panelleft.Refresh()
self.zoomed=True
event.Skip()

Tkinter Create buttons from list with different image

I want to create button from a list and I want them to have their own image.
I have tried this but only the last created button work.
liste_boutton = ['3DS','DS','GB']
for num,button_name in enumerate(liste_boutton):
button = Button(type_frame)
button['bg'] = "grey72"
photo = PhotoImage(file=".\dbinb\img\\{}.png".format(button_name))
button.config(image=photo, width="180", height="50")
button.grid(row=num, column=0, pady=5, padx=8)
Only your last button has an image as it is the only one that has reference in the global scope, or in any scope for that matter in your specific case. As is, you have only one referable button and image objects, namely button and photo.
Short answer would be:
photo = list()
for...
photo.append(PhotoImage(file=".\dbinb\img\\{}.png".format(button_name)))
But this would still have bad practice written all over it.
Your code appears okay, but you are need to keep a reference of the image effbot and also I do believe PhotoImage can only read GIF and PGM/PPM images from files, so you need another library PIL seems to be a good choice. I used a couple images from a google search for the images, they were placed in the same directory as my .py file, so i changed the code a little bit. Also the button width and height can cut off parts of the image if not careful.
from Tkinter import *
from PIL import Image, ImageTk
type_frame = Tk()
liste_boutton = ['3DS','DS','GB']
for num,button_name in enumerate(liste_boutton):
button = Button(type_frame)
button['bg'] = "grey72"
# this example works, if .py and images in same directory
image = Image.open("{}.png".format(button_name))
image = image.resize((180, 100), Image.ANTIALIAS) # resize the image to ratio needed, but there are better ways
photo = ImageTk.PhotoImage(image) # to support png, etc image files
button.image = photo # save reference
button.config(image=photo, width="180", height="100")
# be sure to check the width and height of the images, so there is no cut off
button.grid(row=num, column=0, pady=5, padx=8)
mainloop()
Output:
[https://i.stack.imgur.com/lxthT.png]
With all your comments I was able to achieve what I expected !
Thank ! I'm new to programming so this will not necessarily be the best solution but it works
import tkinter as tk
root = tk.Tk()
frame1 = tk.Frame(root)
frame1.pack(side=tk.TOP, fill=tk.X)
liste_boutton = ['3DS','DS','GB']
button = list()
photo = list()
for num,button_name in enumerate(liste_boutton):
button.append(tk.Button(frame1))
photo.append(tk.PhotoImage(file=".\dbinb\img\\{}.png".format(button_name)))
button[-1].config(bg="grey72",image=photo[-1], width="180", height="50")
button[-1].grid(row=num,column=0)
root.mainloop()

Resources