I've got some issues about blitting a matplotlib plot, which is itself embedded in a Tkinter GUI - the whole program will eventually run on a Raspberry Pi. The question involves various levels, this is my first question, so sorry in advance for any unclarities.
In few words, what I'm doing is this: I'm working on a Tk GUI to read out a number of sensors simultaneously and I'd like to have some real-time updating of the sensor data on said GUI. I'd like to have each measurable quantity on a separate frame, which is why I decided to set up a class for each Sensor. One of the sensors is a Flow Sensor, which is read out and plotted as follows:
import Tkinter as Tk
import numpy as np
import matplotlib.pyplot as plt
from datetime import datetime
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from Backend import Backend #self written data acquisition handler
#global variables
t0 = datetime.now() #start time of program
#Returns time difference in seconds
def time_difference(t1, t2):
delta = t2-t1
delta = delta.total_seconds()
return delta
# Define Class for Flow data display
class FlowFig():
def __init__(self, master): #master:Parent frame for plot
#Initialize plot data
self.t = []
self.Flow = []
#Initialize plot for FlowFig
self.fig = plt.figure(figsize=(4,4))
self.ax = self.fig.add_subplot(111)
self.ax.set_title("Flow Control Monitor")
self.ax.set_xlabel("Time")
self.ax.set_ylabel("Flow")
self.ax.axis([0,100,0,5])
self.line = self.ax.plot(self.t, self.Flow, '-')
#Set up canvas
self.canvas = FigureCanvasTkAgg(self.fig, master = master)
self.canvas.show()
self.background = self.canvas.copy_from_bbox(self.ax.bbox)
self.ax.grid(True)
# Initialize handler for data aqcuisition
self.Data= Backend()
self.Data.initialize()
#update figure
def update(self):
# get new data values
self.t.append(time_difference(t0, datetime.now()))
Flow,_,_ = self.Data.get_adc_val(1)
self.Flow.append(Flow)
# shorten data vector if too long
if len(self.t) > 200:
del self.t[0]
del self.Flow[0]
#adjust xlims, add new data to plot
self.ax.set_xlim([np.min(self.t), np.max(self.t)])
self.line[0].set_data(self.t, self.Flow)
#blit new data into old frame
self.canvas.restore_region(self.background)
self.ax.draw_artist(self.line[0])
self.canvas.blit(self.ax.bbox)
root.after(25, Flowmonitor.Flowdata.update) #Recursive update
#Flow Frame of GUI
class FlowPage(Tk.Frame):
def __init__(self, parent, controller):
Tk.Frame.__init__(self,parent)
self.parent = parent
self.FlowPlot = FlowFig(self)
self.FlowPlot.canvas.get_tk_widget().grid(row=0, column=0, rowspan=9, columnspan=9)
# Mainloop
root= Tk.Tk()
root.rowconfigure(0, weight=1)
root.columnconfigure(0, weight=1)
Flowmonitor = FlowPage(root, root)
Flowmonitor.grid(row =0, column=0, rowspan =10, columnspan=10)
Flowmonitor.rowconfigure(0, weight=1)
Flowmonitor.columnconfigure(0, weight=1)
root.after(25, Flowmonitor.FlowPlot.update)
root.mainloop()
What troubles me with my resulting images is this:
When I use the statement copy_from_bbox(self.ax.bbox)
I get a graph like this
Obviously, the size of the blitted background doesn't fit the image into which it is blitted. So, I tried to blit the figure's bbox instead (copy_from_bbox(self.fig.bbox)
) and got this
Versions of these shifts happen with all combinations of fig.bbox
and ax.bbox
,
So here come my actual questions:
Can anybody help me find the bug in my above code which causes the missmatch? I'm aware that it is probably very simple yet subtle misstake. It seems very much related to this thread, yet I can't quite glue it together, using bbox.expanded()
in the argument of copy_from_bbox()
doesn't do much of a difference
.blit()
vs. .draw()
has already been discussed here.
But since speed is of the essence for my application I think I have to blit. Redrawing the plot gives me framerates of fps=10, whereas blitting runs almost 10x faster. In any case - is there a way to update one of the axes (e.g. time axis) while using blit? (The answer to this is probably closely related to question No.1 )
Lastly, a rather basic question about my application alltogether: Since my sensordata is currently fetched within an infinite, recursive loop - is it possible to run several of such loops in parallel or should I rather go for threading instead, making my code considerably more complex? What are the risks of running infinite, recursive loops? Or should these be avoided in general?
After days of blitting back and forth I'm rather confused about the possibilities regarding ax/fig blitting, so any help regarding the matter is much, much appreciated^^ Please let me know if you need more info about anything, I hope I could illustrate my problem well.
Thanks a lot for your help!
Creating a GUI using tkinter is an easy task. Apply the event Trigger on the widgets. Importing tkinter is same as importing any other module in the Python code. Note that the name of the module in Python 2.x is ‘Tkinter’ and in Python 3.x it is ‘tkinter’.
You called a class tkinter.Tk () and did not assigned it to a variable, so system saved it to a temporary memory. Then you called tkinter.Tk () again and did not assigned it too. Then system saved it to another temporary memory. So you have 2 different tkinter object.
Blitting speeds up repetitive drawing by rendering all non-changing graphic elements into a background image once. Then, for every draw, only the changing elements need to be drawn onto this background.
Tkinter: Tkinter is library with the help of which we can make GUI (Graphical User Interface).
This was written in Python3, but it should be virtually the exact same in your version of Python2
import tkinter as Tk
import numpy as np
import matplotlib.pyplot as plt
from datetime import datetime
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
# from Backend import Backend #self written data acquisition handler
import random
#global variables
t0 = datetime.now() #start time of program
#Returns time difference in seconds
def time_difference(t1, t2):
delta = t2-t1
delta = delta.total_seconds()
return delta
# Define Class for Flow data display
class FlowFig():
def __init__(self, master): #master:Parent frame for plot
#Initialize plot data
self.t = []
self.Flow = []
#Initialize plot for FlowFig
self.fig = plt.figure(figsize=(4,4))
self.ax = self.fig.add_subplot(111)
self.ax.set_title("Flow Control Monitor")
self.ax.set_xlabel("Time")
self.ax.set_ylabel("Flow")
self.ax.axis([0,100,0,5])
self.line = self.ax.plot(self.t, self.Flow, '-')
#Set up canvas
self.canvas = FigureCanvasTkAgg(self.fig, master = master)
self.canvas.draw()
self.ax.add_line(self.line[0])
self.background = self.canvas.copy_from_bbox(self.ax.bbox)
self.ax.grid(True)
# # Initialize handler for data aqcuisition
# self.Data= Backend()
# self.Data.initialize()
#update figure
def update(self):
# get new data values
self.t.append(time_difference(t0, datetime.now()))
Flow = random.uniform(1, 5)
self.Flow.append(Flow)
# shorten data vector if too long
if len(self.t) > 200:
del self.t[0]
del self.Flow[0]
#adjust xlims, add new data to plot
self.ax.set_xlim([np.min(self.t), np.max(self.t)])
self.line[0].set_data(self.t, self.Flow)
#blit new data into old frame
self.canvas.restore_region(self.background)
self.ax.draw_artist(self.line[0])
self.canvas.blit(self.ax.bbox)
self.canvas.flush_events()
root.after(1,self.update)
#Flow Frame of GUI
class FlowPage(Tk.Frame):
def __init__(self, parent, controller):
Tk.Frame.__init__(self,parent)
self.parent = parent
self.FlowPlot = FlowFig(self)
self.FlowPlot.canvas.get_tk_widget().grid(row=0, column=0, rowspan=9, columnspan=9)
# Mainloop
root= Tk.Tk()
root.rowconfigure(0, weight=1)
root.columnconfigure(0, weight=1)
Flowmonitor = FlowPage(root, root)
Flowmonitor.grid(row =0, column=0, rowspan =10, columnspan=10)
Flowmonitor.rowconfigure(0, weight=1)
Flowmonitor.columnconfigure(0, weight=1)
root.after(25, Flowmonitor.FlowPlot.update)
root.mainloop()
I moved
import Tkinter as Tk
to
import tkinter as Tk
adc
valuesI moved
from Backend import Backend
to
import random
Because I don't have access to the Backend
, I just used a random number generator (obviously it isn't the best example of ADC readings, but it is good enough for a tester)
I moved
self.canvas = FigureCanvasTkAgg(self.fig, master = master)
self.canvas.show()
self.background = self.canvas.copy_from_bbox(self.ax.bbox)
self.ax.grid(True)
to
self.canvas = FigureCanvasTkAgg(self.fig, master = master)
self.canvas.draw()
self.ax.add_line(self.line[0])
self.background = self.canvas.copy_from_bbox(self.ax.bbox)
self.ax.grid(True)
You must first draw()
the canvas, because otherwise matplotlib
will throw an error AttributeError: draw_artist can only be used after an initial draw which caches the renderer
.
Then, we now add the self.line
, with no values in it. Currently it is just an empty point sitting on the canvas.
From:
Flow,_,_ = self.Data.get_adc_val(1)
to
Flow = random.uniform(1, 5)
Obviously you could keep your own code for this
after
Your system of using the after
function wasn't entirely correct, as you should have inherited the Flowmonitor.Flowdata
from the pre-existing window. Otherwise you would be updating values that simply don't exist, hence, I replaced it with a self.
function
root.after(25, Flowmonitor.Flowdata.update)
to
self.canvas.flush_events()
root.after(1,self.update)
I decreased the after
value, to show that the window could continue plotting correctly when doing it even faster!
The flush_events()
function causes the window to properly update and keep track of other things it's doing!
I'd thoroughly dissuade you from going down the threading
route, because it is awful with tkinter
. The amount of issues and loop-holes you have to jump through are awful, and quite often, even with threading, the program still begins to feel quite slow and unresponsive.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With