Tkinter + matplotlib 动画填充内存

Posted

技术标签:

【中文标题】Tkinter + matplotlib 动画填充内存【英文标题】:Tkinter + matplot animation filling memory 【发布时间】:2020-04-19 08:01:29 【问题描述】:

我正在尝试为回流炉控制器做简单的 GUI。 GUI 每 1 秒采样一次温度(现在它从随机函数中获取,最终从 arduino 获取)。但是现在每 1 秒内存使用量增加大约 3.5 mb - 我猜这与 matplot 动画有关,并且它只在旧图像之上绘制新图像(第 103-115 行)但是如何解决这个问题? 然后运行大约 385 秒:

C:\Users\veeti\PycharmProjects\Reflow\venv\Scripts\python.exe C:/Users/veeti/PycharmProjects/Reflow/Main.py Tkinter 中的异常 回调 Traceback(最近一次调用最后一次):文件“C:\ Program Files (x86)\python37\lib\tkinter__init__.py",第 1705 行,在 call 中 在 callit 中返回 self.func(*args) 文件“C:\Program Files (x86)\python37\lib\tkinter__init__.py”,第 749 行 func(*args) 文件 "C:\Users\veeti\PycharmProjects\Reflow\venv\lib\site-packages\matplotlib\backends_backend_tk.py", 第 118 行,在 _on_timer TimerBase._on_timer(self) 文件“C:\Users\veeti\PycharmProjects\Reflow\venv\lib\site-packages\matplotlib\backend_bases.py”, 第 1194 行,在 _on_timer ret = func(*args, **kwargs) 文件“C:\Users\veeti\PycharmProjects\Reflow\venv\lib\site-packages\matplotlib\animation.py”, 第 1447 行,在 _step still_going = Animation._step(self, *args) 文件“C:\Users\veeti\PycharmProjects\Reflow\venv\lib\site-packages\matplotlib\animation.py”, 第 1173 行,在 _step self._draw_next_frame(framedata, self._blit) 文件“C:\Users\veeti\PycharmProjects\Reflow\venv\lib\site-packages\matplotlib\animation.py”, 第 1192 行,在 _draw_next_frame self._draw_frame(framedata) 文件 "C:\Users\veeti\PycharmProjects\Reflow\venv\lib\site-packages\matplotlib\animation.py", 第 1755 行,在_draw_frame 中 self._drawn_artists = self._func(framedata, self._args) 文件“C:/Users/veeti/PycharmProjects/Reflow/Main.py”,第 115 行,动画 canvas.draw() 文件 "C:\Users\veeti\PycharmProjects\Reflow\venv\lib\site-packages\matplotlib\backends\backend_tkagg.py", 第 9 行,平局 super(FigureCanvasTkAgg, self).draw() 文件 "C:\Users\veeti\PycharmProjects\Reflow\venv\lib\site-packages\matplotlib\backends\backend_agg.py", 第 386 行,平局 self.renderer = self.get_renderer(cleared=True) 文件 "C:\Users\veeti\PycharmProjects\Reflow\venv\lib\site-packages\matplotlib\backends\backend_agg.py", 第 399 行,在 get_renderer 中 self.renderer = RendererAgg(w, h, self.figure.dpi) 文件 "C:\Users\veeti\PycharmProjects\Reflow\venv\lib\site-packages\matplotlib\backends\backend_agg.py", 第 86 行,在 init 中 self._renderer = _RendererAgg(int(width), int(height), dpi) MemoryError: In RendererAgg: Out of memory

如果没有第 124 行命令“animate(1)”,动画也不会打开 - 为什么会这样?

import tkinter
import matplotlib.pyplot as plt
import random
import matplotlib.animation as animation
from matplotlib.backends.backend_tkagg import (
    FigureCanvasTkAgg)

def exitHandler():
    print("at exit handler")
    ani.event_source.stop()
    root.destroy()

def startButtonClick():
    print("start")

def stopButtonClick():
    print("stop")

def readTemperature():
    return random.randint(0,250)

def updateTimeLabel(time = -99):
    timeLabel.config(text= "Time: ".format (time))

def updateTargetTempLabel(temp = -99) :
    targetTempLabel.config(text="Target temp: \n  ".format (temp))

def updateCurrentTempLabel(temp = -99) :
    currentTempLabel.config(text="Current temp: \n  ".format (temp))

def updateHeaterStatus(temp,target):
    if (temp < target):
        heaterStatusLabel.config(text="Heater: \n On")
    else:
        heaterStatusLabel.config(text="Heater: \n Off")

def calculateTarget(time):
    global timePoints, tempQuidance, targetTemp

    #find current slope and calculate the target temp

    for i in range (len(timePoints)):
        if (timePoints[i] < time < timePoints[i+1]):
            slope = (tempQuidance[i+1] - tempQuidance[i]) / (timePoints[i+1] -timePoints[i])
            currentTarget = (time -timePoints[i]) * slope + tempQuidance[i]

            return (currentTarget)

def animate(i):
    global timePoints,tempQuidance,numberOfPoints,measuredTemp, time
    time = time+1

    measuredTemp.append(readTemperature())

    numberOfPoints.append(len(measuredTemp))

    ax.clear()
    ax.plot(timePoints,tempQuidance)
    ax.plot(numberOfPoints,measuredTemp)

    updateTimeLabel(time)
    updateTargetTempLabel(calculateTarget(time))
    updateCurrentTempLabel(measuredTemp[-1])
    updateHeaterStatus(measuredTemp[-1], 100)

    canvas = FigureCanvasTkAgg(fig,plotFrame)
    canvas.draw()
    canvas.get_tk_widget().grid(row=3,column=0,columnspan = 4)


global measuredTemp, numberOfPoints, time, targetTemp

time = 0
measuredTemp=[]
numberOfPoints=[]
targetTemp = 0

#temperature profile
timePoints = [0,300,400,460,500]
tempQuidance =[0,150,150,250,0]

root=tkinter.Tk()
root.title('Reflow oven controller')
root.geometry("1600x800")

controlFrame = tkinter.LabelFrame(root,width = 500, height = 800, borderwidth = 3, padx=5,pady=5)
plotFrame = tkinter.LabelFrame(root,padx=5,pady=5)

controlFrame.grid(row = 0, column = 0 )
controlFrame.grid_propagate(0)
plotFrame.grid(row = 0, column= 1)

timeLabel=tkinter.Label(plotFrame,text = "Time: ")
targetTempLabel = tkinter.Label(plotFrame,text = "Target temp")
currentTempLabel = tkinter.Label(plotFrame,text = "Current temp")
heaterStatusLabel = tkinter.Label(plotFrame,text = "Heater status: \n unknown")

timeLabel.grid(row= 0, column=0)
targetTempLabel.grid(row=0, column= 1)
currentTempLabel.grid(row=0, column= 2)
heaterStatusLabel.grid(row = 0, column = 3)

fig=plt.figure()
ax = fig.add_subplot(1, 1, 1)

startButton = tkinter.Button(controlFrame, text = "Start", command = startButtonClick)
stopButton = tkinter.Button(controlFrame, text="Stop", command = stopButtonClick)

startButton.grid(row = 0,column = 0)
stopButton.grid(row = 0 ,column= 1 )
animate(1)
ani = animation.FuncAnimation(fig, animate, interval=1000) #run animation every 1s, animate func takes care of getting new values and GUI update

root.protocol("WM_DELETE_WINDOW", exitHandler)
root.mainloop()

提前谢谢

【问题讨论】:

你能提供一个Minimal, Reproducible Example吗? 转载。仅供参考,我找到了this 并尝试了垃圾收集但没有成功。 看起来您正在构建画布并将其一遍又一遍地放置在旧画布之上,而不是更新它。那会导致你的记忆问题。解决方法是更新画布。 【参考方案1】:

问题是您一遍又一遍地堆叠画布。我们希望在 animate 函数之外创建画布,而不是像这样重新创建画布并堆叠它们,然后将 draw 留在函数中。

解决方法是改变这个:

fig=plt.figure()
ax = fig.add_subplot(1, 1, 1)

def animate(i):
    global timePoints,tempQuidance,numberOfPoints,measuredTemp, time
    time = time+1
    measuredTemp.append(readTemperature())
    numberOfPoints.append(len(measuredTemp))
    ax.clear()
    ax.plot(timePoints,tempQuidance)
    ax.plot(numberOfPoints,measuredTemp)
    updateTimeLabel(time)
    updateTargetTempLabel(calculateTarget(time))
    updateCurrentTempLabel(measuredTemp[-1])
    updateHeaterStatus(measuredTemp[-1], 100)
    canvas = FigureCanvasTkAgg(fig,plotFrame)
    canvas.draw()
    canvas.get_tk_widget().grid(row=3,column=0,columnspan = 4)

到这里:

fig=plt.figure()
ax = fig.add_subplot(1, 1, 1)
canvas = FigureCanvasTkAgg(fig,plotFrame)
canvas.get_tk_widget().grid(row=3,column=0,columnspan = 4)


def animate(i):
    global timePoints,tempQuidance,numberOfPoints,measuredTemp, time
    time = time+1
    measuredTemp.append(readTemperature())
    numberOfPoints.append(len(measuredTemp))
    ax.clear()
    ax.plot(timePoints,tempQuidance)
    ax.plot(numberOfPoints,measuredTemp)
    updateTimeLabel(time)
    updateTargetTempLabel(calculateTarget(time))
    updateCurrentTempLabel(measuredTemp[-1])
    updateHeaterStatus(measuredTemp[-1], 100)
    canvas.draw()

为了遵循 PEP8 标准,我已经稍微清理了您的代码。见下文:

import tkinter as tk
import matplotlib.pyplot as plt
import random
import matplotlib.animation as animation
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg


def exit_handler():
    print("at exit handler")
    ani.event_source.stop()
    root.destroy()


def start_button_click():
    print("start")


def stop_button_click():
    print("stop")


def read_temperature():
    return random.randint(0, 250)


time = 0
measured_temp = []
number_of_points = []
targetTemp = 0

time_points = [0, 300, 400, 460, 500]
temp_quidance = [0, 150, 150, 250, 0]

root = tk.Tk()
root.title('Reflow oven controller')
root.geometry("1600x800")

control_frame = tk.LabelFrame(root, width=500, height=800, borderwidth=3, padx=5, pady=5)
plot_frame = tk.LabelFrame(root, padx=5, pady=5)

control_frame.grid(row=0, column=0)
control_frame.grid_propagate(0)
plot_frame.grid(row=0, column=1)

time_label = tk.Label(plot_frame, text="Time: ")
target_temp_label = tk.Label(plot_frame, text="Target temp")
current_temp_label = tk.Label(plot_frame, text="Current temp")
heater_status_label = tk.Label(plot_frame, text="Heater status: \n unknown")

time_label.grid(row=0, column=0)
target_temp_label.grid(row=0, column=1)
current_temp_label.grid(row=0, column=2)
heater_status_label.grid(row=0, column=3)


def update_time_label(time=-99):
    time_label.config(text="Time:\n".format(time))


def update_target_temp_label(temp=-99):
    target_temp_label.config(text="Target temp:\n".format(temp))


def update_current_temp_label(temp=-99) :
    current_temp_label.config(text="Current temp:\n".format(temp))


def update_heater_status(temp, target):
    if temp < target:
        heater_status_label.config(text="Heater:\nOn")
    else:
        heater_status_label.config(text="Heater:\nOff")


def calculate_target(time):
    for i in range(len(time_points)):
        if time_points[i] < time < time_points[i+1]:
            slope = (temp_quidance[i+1] - temp_quidance[i]) / (time_points[i+1] - time_points[i])
            current_target = (time - time_points[i]) * slope + temp_quidance[i]
            return current_target


fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
canvas = FigureCanvasTkAgg(fig, plot_frame)
canvas.get_tk_widget().grid(row=3, column=0, columnspan=4)


def animate(_=None):
    global time
    time = time+1
    measured_temp.append(read_temperature())
    number_of_points.append(len(measured_temp))
    ax.clear()
    ax.plot(time_points, temp_quidance)
    ax.plot(number_of_points, measured_temp)
    update_time_label(time)
    update_target_temp_label(calculate_target(time))
    update_current_temp_label(measured_temp[-1])
    update_heater_status(measured_temp[-1], 100)
    canvas.draw()


start_button = tk.Button(control_frame, text="Start", command=start_button_click)
stop_button = tk.Button(control_frame, text="Stop", command=stop_button_click)

start_button.grid(row=0, column=0)
stop_button.grid(row=0, column=1)

animate()
ani = animation.FuncAnimation(fig, animate, interval=1000)
root.protocol("WM_DELETE_WINDOW", exit_handler)
root.mainloop()

内存接近动画开始:

记忆 1 分钟后:

如您所见,内存增长已停止,不再超过 3 MB/秒。

至于是否需要使用animate() 来启动动画我不是100% 确定的。我没有使用足够的matplotlib 来了解一切是如何工作的。我什至无法在这里推测,因此必须由其他人来回答。

【讨论】:

以上是关于Tkinter + matplotlib 动画填充内存的主要内容,如果未能解决你的问题,请参考以下文章

Tkinter 回调异常:matplotlib 动画

使用带有 pylab/matplotlib 嵌入的 Tkinter 播放、暂停、停止功能的彩色绘图动画:无法更新图形/画布?

在两个 TKinter“页面”中使用 FigureCanvasTkAgg

tkinter 与 matplotlib

matplotlib 在 tkinter 画布中的缩放功能

tkinter的GUI设计:界面与逻辑分离-- 与 matplotlib 结合