is there any way to clear matplotlib memory usage from a tkinter application, the following code is taken from Embedding in Tk, i just put it in a loop to make the memory leak more clear.
import tkinter
import matplotlib
print(matplotlib._version.version)
matplotlib.use("TkAgg")
from matplotlib.backends.backend_tkagg import (
FigureCanvasTkAgg, NavigationToolbar2Tk)
import gc
# Implement the default Matplotlib key bindings.
from matplotlib.backend_bases import key_press_handler
from matplotlib.figure import Figure
import psutil
import os, psutil
process = psutil.Process(os.getpid())
import numpy as np
import time
root = tkinter.Tk()
frame = tkinter.Frame(root)
root.wm_title("Embedding in Tk")
import matplotlib.pyplot as plt
def my_func():
global root,frame
fig = Figure(figsize=(5, 4), dpi=100)
t = np.arange(0, 3, .01)
ax = fig.add_subplot()
line = ax.plot(t, 2 * np.sin(2 * np.pi * t))
ax.set_xlabel("time [s]")
ax.set_ylabel("f(t)")
canvas = FigureCanvasTkAgg(fig, master=frame) # A tk.DrawingArea.
canvas.draw()
# pack_toolbar=False will make it easier to use a layout manager later on.
toolbar = NavigationToolbar2Tk(canvas, frame, pack_toolbar=False)
toolbar.update()
toolbar.pack(side=tkinter.BOTTOM, fill=tkinter.X)
canvas.get_tk_widget().pack(side=tkinter.TOP, fill=tkinter.BOTH, expand=True)
time.sleep(0.1)
# everything i tried to clear memory
ax = fig.axes[0]
ax.clear()
canvas.get_tk_widget().pack_forget()
toolbar.pack_forget()
canvas.figure.clear()
canvas.figure.clf()
canvas.get_tk_widget().destroy()
toolbar.destroy()
mem = process.memory_info().rss/2**20
print(mem) # in bytes
if mem > 1000:
root.destroy()
frame.destroy()
frame = tkinter.Frame(root)
root.after(10,my_func)
gc.collect()
if __name__ == "__main__":
root.after(1000,my_func)
root.mainloop()
it just keeps eating memory up to 1000 MBs,
i tried everything to remove this memory leak without hope, i tried the answer here, but it also didn't work How to clear memory completely of all matplotlib plots.
just updating the figure instead of creating a new figure on each loop iteration would "avoid" some of the memory leak, but it doesn't "fix it", how do i reclaim this memory ?
this issue seems related https://github.com/matplotlib/matplotlib/issues/20490 but i am using version 3.6.2 which should have it fixed, i can duplicate it on almost all python versions on windows, (but the code in the issue doesn't produce this problem)
tracemalloc only shows around 1 MB was allocated on python side, so the rest of the leak is on C side ... something isn't getting cleaned up.
Edit: this also seems related Tkinter - memory leak with canvas, but the canvases are correctly reclaimed, so it's not a bug in the canvases or tk.
Edit2: the renderer on the C side is not getting freed ... althought there seems to be no reference to it.
seems like tkinter keeps reference to the canvases callbacks, which prevents matplotlib from deleting its objects, and as callbacks are tied to the toplevel in use, making a new toplevel for each plot seems to correctly delete the references and therefore prevent the memory leak.
import tkinter
import matplotlib
print(matplotlib._version.version)
matplotlib.use("TkAgg")
from matplotlib.backends.backend_tkagg import (
FigureCanvasTkAgg, NavigationToolbar2Tk)
import gc
from matplotlib.figure import Figure
import os, psutil
process = psutil.Process(os.getpid())
import numpy as np
import time
root = tkinter.Tk()
frame = tkinter.Frame(root)
root.wm_title("Embedding in Tk")
new_window = tkinter.Toplevel(master=root)
import matplotlib.pyplot as plt
def test_func():
global root,frame, new_window
fig = Figure(figsize=(5, 4), dpi=100)
canvas = FigureCanvasTkAgg(fig, master=frame) # A tk.DrawingArea.
canvas.draw()
canvas.get_tk_widget().pack(side=tkinter.TOP, fill=tkinter.BOTH, expand=True)
time.sleep(0.1)
ax = fig.axes[0]
ax.clear()
canvas.get_tk_widget().pack_forget()
canvas.figure.clear()
canvas.figure.clf()
canvas.get_tk_widget().destroy()
mem = process.memory_info().rss/2**20
print(mem) # in bytes
if mem > 1000:
root.destroy()
frame.destroy()
new_window.destroy()
new_window = tkinter.Toplevel(master=root)
frame = tkinter.Frame(new_window)
gc.collect()
root.after(10,test_func)
if __name__ == "__main__":
root.after(1000,test_func)
root.mainloop()
there are multiple issues on github for matplotlib where they disucessed these problems, but i hope it gets fixed in the future.
https://github.com/matplotlib/matplotlib/pull/22002
for now the workaround is to only create a finite number of FigureCanvasTkAgg and keep modifying them, and hope you never need to reclaim their memory, or just destroy your entire window momentarily, or keep the plots separate from your main window.
canvas = FigureCanvasTkAgg(master=frame) # A tk.DrawingArea.
toolbar = NavigationToolbar2Tk(canvas, frame, pack_toolbar=False)
toolbar.pack(side=tkinter.BOTTOM, fill=tkinter.X)
canvas.get_tk_widget().pack(side=tkinter.TOP, fill=tkinter.BOTH, expand=True)
fig = canvas.figure
def my_func():
ax = fig.add_subplot()
t = np.arange(0, 3, .01)
line = ax.plot(t, 2 * np.sin(2 * np.pi * t))
ax.set_xlabel("time [s]")
ax.set_ylabel("f(t)")
canvas.draw()
canvas.get_tk_widget().update()
# pack_toolbar=False will make it easier to use a layout manager later on.
toolbar.update()
time.sleep(0.1)
# everything i tried to clear memory
mem = process.memory_info().rss / 2 ** 20
print(mem) # in bytes
if mem > 1000:
root.destroy()
root.after(10, my_func)
fig.clear()
gc.collect()
if __name__ == "__main__":
root.after(1000, my_func)
root.mainloop()
Edit: going with my answer, you could manually reclaim the memory by deleting the references to matplotlib objects yourself using
canvas.get_tk_widget().pack_forget()
toolbar.pack_forget()
canvas.get_tk_widget().destroy()
toolbar.destroy()
frame.destroy()
[delattr(canvas,x) for x in vars(canvas).copy() if x != "_tkcanvas"]
which is a very hacky way to fix the memory leak.
Related
I'm trying to create a GUI that will plot data from an array that is updated when I click a button (in this case move a slider), I can get the data to replot, however, the scale doesn't update and I can't work out how to do it. does anyone have any ideas?
the data is created in a function dataMaker and just returns 2 arrays, one of the time increments and the other of random data.
many thanks
import tkinter
from matplotlib.backends.backend_tkagg import (
FigureCanvasTkAgg, NavigationToolbar2Tk)
# Implement the default Matplotlib key bindings.
from matplotlib.backend_bases import key_press_handler
from matplotlib.figure import Figure
import numpy as np
import dataMaker as DM
root = tkinter.Tk()
root.wm_title("Embedding in Tk")
fig = Figure(figsize=(5, 4), dpi=100)
rt_data = DM.comboP(1, 1000)
sample = rt_data[1]
torque = rt_data[2]
ax = fig.add_subplot()
line, = ax.plot(sample, torque)
canvas = FigureCanvasTkAgg(fig, master=root) # A tk.DrawingArea.
canvas.draw()
# pack_toolbar=False will make it easier to use a layout manager later on.
toolbar = NavigationToolbar2Tk(canvas, root, pack_toolbar=False)
toolbar.update()
canvas.mpl_connect(
"key_press_event", lambda event: print(f"you pressed {event.key}"))
canvas.mpl_connect("key_press_event", key_press_handler)
button_quit = tkinter.Button(master=root, text="Quit", command=root.destroy)
def update_frequency(new_val):
rt_data = DM.comboP(1, 1000)
sample = rt_data[1]
torque = rt_data[2]
line.set_data(sample, torque)
ax.autoscale_view()
# required to update canvas and attached toolbar!
canvas.draw()
slider_update = tkinter.Scale(root, from_=1, to=5, orient=tkinter.HORIZONTAL,
command=update_frequency, label="Frequency [Hz]")
# Packing order is important. Widgets are processed sequentially and if there
# is no space left, because the window is too small, they are not displayed.
# The canvas is rather flexible in its size, so we pack it last which makes
# sure the UI controls are displayed as long as possible.
button_quit.pack(side=tkinter.BOTTOM)
slider_update.pack(side=tkinter.BOTTOM)
toolbar.pack(side=tkinter.BOTTOM, fill=tkinter.X)
canvas.get_tk_widget().pack(side=tkinter.TOP, fill=tkinter.BOTH, expand=True)
tkinter.mainloop()
I've searched the internet but can't find a solution, id like the scale the change to fit the data that's in the array
I'm trying to plot data from serial port in a tkinter gui. The plot should be plotted and updated only when a specific pack comes from serial.
I can parse the new incoming data and update the GUI (text area). But when I call the "plot()" function from the "update_gui" thread, the program quit and I get the
"Process finished with exit code -1073741819 (0xC0000005)"
message.
Instead, if I call "plot()" from somewhere else (command button, or simply before mainloop()), the plot is generated and shown.
Here is the relevant part of the code:
import threading
import tkinter as tk
import tkinter.scrolledtext as st
import rx_seriale as rx_ser
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import (FigureCanvasTkAgg, NavigationToolbar2Tk)
import queue
running = True
serial_data = ''
filter_data = ''
update_period = 5
serial_object = None
gui = tk.Tk()
gui.title("UART Interface")
my_queue = queue.Queue()
t1 = ''
def plot(valori):
global frame_plot1
try:
# the figure that will contain the plot
fig = Figure(figsize=(4.5, 4), dpi=100)
# adding the subplot
plot1 = fig.add_subplot(111)
#dummy values
y = [i ** 2 for i in range(101)]
# plotting the graph
plot1.plot(y)
# creating the Tkinter canvas
# containing the Matplotlib figure
canvas = FigureCanvasTkAgg(fig, master=frame_plot1)
canvas.draw()
# placing the canvas on the Tkinter window
canvas.get_tk_widget().pack()
# creating the Matplotlib toolbar
#toolbar = NavigationToolbar2Tk(canvas, frame_plot1)
#toolbar.update()
# placing the toolbar on the Tkinter window
#canvas.get_tk_widget().pack()
except Exception as e:
print('Errore:' + str(e))
def update_gui():
global filter_data
global update_period
global my_queue
global type_test, test_status,flag
while (1):
data = my_queue.get(block=True)
text.insert('end', test_status[data[0]] + " - " + type_test[data[1]])
text.insert('end', '\n')
text.see('end')
if (data[1] == 6):
plot(1)
if __name__ == "__main__":
'''
...
all the stuff for design TK windows
...
'''
# threads
t2 = threading.Thread(target=update_gui)
t2.daemon = True
t2.start()
# mainloop
gui.geometry('1000x500')
gui.mainloop()
What goes wrong? Thank you.
EDIT2:
So after some more work I created the smallest sample which reproduces the Memoryleak pretty decent on every machine.
This code just creates a TKinter Window and a matplotlib canvas with 3 Sine waves, than tries to animate then. See that blit is true, so it shoudnt do anything because the drawing does not change.
I also tried following ways from this post
How can I release memory after creating matplotlib figures
gc.collect() - did nothing
axes.clear - it did stop endless memory to be leaked but also completely screws the picture
figure.clf - same as clear but worse.
Any other idea why FuncAnimation increases in memory usage over time?????
Thanks
import tkinter as tk
from threading import Thread
from numpy import sin, cos, pi
#--Mainwindow--
class Drehstromdemonstrator(tk.Tk):
def __init__(self):
#Predefine
tk.Tk.__init__(self)
tk.Tk.wm_title(self, "Minimal")
container = tk.Frame(self)
container.pack(side="top", fill="both", expand=True)
container.grid_rowconfigure(0, weight=1)
container.grid_columnconfigure(0, weight=1)
self.frame = plotSine(self,(tk.Tk.winfo_screenwidth(self)*0.7,tk.Tk.winfo_screenheight(self)*0.7))
import numpy,matplotlib
matplotlib.use('TkAgg')
from numpy import sin, cos, pi, deg2rad, linspace, arange
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import matplotlib.ticker as tck
import time
import math
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
from math import ceil
global U1ausg, U2ausg, U3ausg, Counter_1, Counter_2, Counter_3
U1ausg = U2ausg = U3ausg = Counter_3 = 50
Counter_1 = 120
Counter_2 = 240
class plotSine:
def __init__(self,masterframe,size):
self._running = True
global U1ausg
global U2ausg
global U3ausg
(w,h) = size
inchsize = (w/25.5, h/25.4)
fig = self.figure = Figure(inchsize)
self.axes = fig.add_subplot(111)
self.axes.xaxis.set_ticks(arange(0,390,30))
self.axes.margins(x=0)
self.axes.yaxis.set_ticks(arange(-120,130,20))
self.axes.set_ylim(-120,120)
#create canvas as matplotlib drawing area
self.canvas = FigureCanvasTkAgg(fig, master=masterframe)
self.canvas.get_tk_widget().pack()
self.x = linspace(0,360,1000)
self.axes.grid()
#self.drawStuff()
#Draw the plot
def drawStuff(self,*args):
ax = self.axes
self.axes.legend(["U1","U2","U3"])
#Changed everything to degree instead of PI, better looking
ysin = int(ceil(U1ausg))*sin(50/Counter_3 * deg2rad(self.x))
ysin2 = int(ceil(U2ausg))*sin(50/Counter_3 * deg2rad(self.x) + deg2rad(Counter_1))
ysin3 = int(ceil(U3ausg))*sin(50/Counter_3 * deg2rad(self.x) + deg2rad(Counter_2))
lineU1 = ax.plot(self.x, ysin, "-r",label="U1")
lineU2 = ax.plot(self.x, ysin2, "-g",label="U2")
lineU3 = ax.plot(self.x, ysin3, "-b",label="U3")
return [*lineU1,*lineU2,*lineU3]
#Animation to redraw when value changed
def animate(self):
self.ani = animation.FuncAnimation(self.figure, self.drawStuff, interval=10, blit=True)
#--Run Mainprog
try:
app = Drehstromdemonstrator()
plt.show()
app.frame.animate()
app.mainloop
# Aufraeumarbeiten nachdem das Programm beendet wurde
except UnicodeDecodeError:
print("Interrupt aswell!")
lampensteuerung.terminate()
except KeyboardInterrupt:
print("Interrupt")
lampensteuerung.terminate()
except Exception as e:
print(e)
EDIT1:
By try-and-error I could figure out that the problem seems to be in my matplotlib, as if I disable it the memory increase stops.
So my task is to finish this project from someone else - it simulates a 3-phased system via a raspberry and some lights. When I disable the GUI the code works splendid, no problems, fast reaction. Sometimes the PI says he has low voltage but I just think that is some weird bug because the sourcepower is really enough to power it.
Full Code removed because not needed.
I am buildin a GUI with an embedded plot using Tkinter and matplotlib. I have embedded a figure in my window and am now looking to use matplotlib's event handler to get two sets of x,y coordinates from the graph and then use these coordinates to create a straight line which is subtracted from the data in the graph. A simplified version of the code looks like this:
import matplotlib
matplotlib.use("TkAgg")
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
from matplotlib.figure import Figure
import tkinter as tk
#ideally this uses matplotlib's event handler and also waits for a click before registering the cooridnates
def choose_points():
points = []
window.bind("<Button-1>", on_click)
points.append(graph_xy)
window.bind("<Button-1>", on_click)
points.append(graph_xy)
return points
def on_click(event):
window.unbind("<Button-1")
window.config(cursor="arrow")
graph_xy[0]=event.x
graph_xy[1]=event.y
def line(x1=0,y1=0,x2=1,y2=1000):
m=(y2-y1)/(x2-x1)
c=y2-m*x2
line_data=[]
for val in range(0,20):
line_data.append(val*m + c)
return line_data
def build_line():
points = []
points = choose_points()
#store line in line_list
line_list=line(points[0],points[1],points[2],points[3])
#lists needed
line_list=[]
graph_xy=[0,0]
#GUI
window=tk.Tk()
window.title("IPES Graphing Tool")
window.geometry('1150x840')
#Make a frame for the graph
plot_frame = tk.Frame(window)
plot_frame.pack(side = tk.TOP,padx=5,pady=5)
#Button for making the straight line
line_btn = ttk.Button(plot_frame,text="Build line", command = build_line)
line_btn.grid(row=4, column=2,sticky='w')
#make empty figure
fig1=plt.figure(figsize=(9,7))
ax= fig1.add_axes([0.1,0.1,0.65,0.75])
#embed matplotlib figure
canvas = FigureCanvasTkAgg(fig1, plot_frame)
mpl_canvas=canvas.get_tk_widget()
canvas.get_tk_widget().pack(padx=20,side=tk.BOTTOM, fill=tk.BOTH, expand=False)
toolbar = NavigationToolbar2Tk(canvas, plot_frame)
toolbar.update()
canvas._tkcanvas.pack(side=tk.TOP, fill=tk.BOTH, expand=False)
window.mainloop()
Obviously this example does not plot or use the line in any way, nor are the coordinates correct, since they are not converted to the coordinates of the graph. I tried replacing the window.bind("<Button-1>",wait_click) with plt.connect('button_press_event',on_click) but this does not wait for the click, and so an error occurs since the program tries to access points but it is empty.
I would like to use the functionality of the matplotlib event handling, so that I can use methods such as event.xdata and event.inaxes to avoid unnecessary extra work.
Thank you.
You should use canvas.mpl_connect to trigger your events, and then retrieve the xdata and ydata to plot the line. See below for sample:
import matplotlib
matplotlib.use("TkAgg")
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
from matplotlib.figure import Figure
import tkinter as tk
window=tk.Tk()
window.title("IPES Graphing Tool")
window.geometry('1150x840')
plot_frame = tk.Frame(window)
plot_frame.pack(side = tk.TOP,padx=5,pady=5)
fig1=Figure(figsize=(9,7))
ax= fig1.add_axes([0.1,0.1,0.65,0.75])
canvas = FigureCanvasTkAgg(fig1, window)
canvas.get_tk_widget().pack(padx=20,side=tk.TOP, fill=tk.BOTH, expand=False)
toolbar = NavigationToolbar2Tk(canvas, window)
toolbar.update()
class DrawLine: # a simple class to store previous cords
def __init__(self):
self.x = None
self.y = None
def get_cords(self, event):
if self.x and self.y:
ax.plot([self.x, event.xdata], [self.y, event.ydata])
canvas.draw_idle()
self.x, self.y = event.xdata, event.ydata
draw = DrawLine()
canvas.mpl_connect('button_press_event', draw.get_cords)
window.mainloop()
So, I managed to get the functionality I wanted by tweaking #Henry Yik 's answer. The issue was resolved by simply using canvas.mpl_disconnect(cid).
I basically used a button to use a function called choose_cords() which would take in an object called draw of type DrawLine containing the x and y coordinates to build the line. The function would then issue the command canvas.mpl_connect('button_press_event', draw.get_cords) to start listening for clicks on the graph. Once two clicks had been registered the matplotlib event handler would be disconnected from within the draw object. The code goes like this
import matplotlib
matplotlib.use("TkAgg")
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
from matplotlib.figure import Figure
import tkinter as tk
window=tk.Tk()
window.title("IPES Graphing Tool")
window.geometry('1150x840')
plot_frame = tk.Frame(window)
plot_frame.pack(side = tk.TOP,padx=5,pady=5)
fig1=Figure(figsize=(9,7))
ax= fig1.add_axes([0.1,0.1,0.65,0.75])
canvas = FigureCanvasTkAgg(fig1, window)
canvas.get_tk_widget().pack(padx=20,side=tk.TOP, fill=tk.BOTH, expand=False)
toolbar = NavigationToolbar2Tk(canvas, window)
toolbar.update()
def choose_cords(draw):
draw.cid=canvas.mpl_connect('button_press_event', draw.get_cords)
class DrawLine: # a simple class to store previous cords
def __init__(self):
self.x = None
self.y = None
self.cid = None
def get_cords(self, event):
if self.x and self.y:
ax.plot([self.x, event.xdata], [self.y, event.ydata])
canvas.draw_idle()
canvas.mpl_disconnect(self.cid)
self.__init__()
return
self.x, self.y = event.xdata, event.ydata
draw = DrawLine()
draw_btn = tk.Button(window, text="Draw the Line", command=lambda:choose_cords(draw)).pack(side=tk.TOP,padx=5,pady=5)
window.mainloop()
I added a return after plotting the line because I want a new line every time, and not a continuation.
Thanks again Henry for your answer which spawend this one
I am trying to write a simple program that reads the values of three different widgets and plots a graph of a function depending on what the input values are. I run into the following problems:
1) On the button press I get the error "App_Window instance has no attribute 'refreshFigure'"
2)On exiting ipython (I use ipython-listener in conjunction with a gedit plugin) I get the error "Exception RuntimeError: 'main thread is not in main loop' in del of
I've based this mostly on this thread - Interactive plot based on Tkinter and matplotlib and this thread - How do I refresh a matplotlib plot in a Tkinter window?
#!/usr/bin/python
from Tkinter import Tk, Frame
from Tkinter import *
import Tkinter
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2TkAgg
#this bit I added just because the NVC13 module is in another directory
import sys
for i in range(len(sys.path)):
if sys.path[i]!='/home/captain-pants/NV_centri':
if i==len(sys.path)-1:
sys.path.append('/home/captain-pants/NV_centri')
continue
else:
break
import multiprocessing as mp
import numpy as np
import matplotlib.pyplot as plt
#This module contains all the code necessary to return a list of values that depend on the input parameters
import NVC13_1lev_advanced_ex_nocoh as C13
from matplotlib.figure import Figure
#this function takes a list of parameters and returns a list of values dependant on those parameters.
def plotfunc(c13,Eg,Bz):
Epam=[]
nv = C13.NVC13(c13,[2580,1423],[Eg,Eg],0,0,Bz,0)
evals = nv.intH()
for i in range(len(evals[0])):
Epam.append(np.real_if_close(evals[0][i]))
return Epam
class App_Window:
def __init__(self, parent):
frame = Tkinter.Frame(parent)
self.Egdata=DoubleVar()
self.c13input=IntVar()
self.Bzdata=DoubleVar()
parent.title("Simple")
Tkinter.Button(text='lulz',command=self.main1).grid(row=3,column=0)
Tkinter.Label(textvariable=self.Egdata).grid(row=5,column=0)
self.c13 = Tkinter.Entry(textvariable=self.c13input)
self.Eg = Tkinter.Scale(orient='horizontal')
self.Bz = Tkinter.Scale(orient='horizontal')
self.c13.grid(row=6,column=0)
self.Bz.grid(row=5,column=0)
self.Eg.grid(row=4,column=0)
Fig = Figure()
FigSubPlot = Fig.add_subplot(111)
self.line, = FigSubPlot.plot(range(10),'bo')
x=[]
y=[]
self.canvas = FigureCanvasTkAgg(Fig, master=parent)
self.canvas.show()
self.canvas.get_tk_widget().grid(row=0,column=0,columnspan=2)
frame.grid(row=0,column=0)
def refreshFigure(self,y):
x=np.arange(len(y))
self.line.set_xdata(x)
self.line.set_ydata(y)
self.canvas.draw()
def main1(self):
self.c13input=int(self.c13.get())
self.Egdata=self.Eg.get()
self.Bzdata=self.Bz.get()
values = plotfunc(self.c13input,self.Egdata,self.Bzdata)
#Just to see whether I actually get the right thing in the input.
print self.c13input
self.refreshFigure(values)
root = Tkinter.Tk()
app = App_Window(root)
root.mainloop()