Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Blitting for live update in Tkinter GUI - performance and image overlap issues

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:

  1. 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

  2. .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 )

  3. 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!

like image 831
JRMueller Avatar asked Feb 01 '17 15:02

JRMueller


People also ask

How to create a GUI using Tkinter in Python?

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’.

Why do I have two different Tkinter objects in my program?

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.

What is blitting and how does it work?

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.

What is Tkinter in Java?

Tkinter: Tkinter is library with the help of which we can make GUI (Graphical User Interface).


1 Answers

In short: the solution

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()

The Changes

Moving to python3

I moved

import Tkinter as Tk

to

import tkinter as Tk

Simulating your adc values

I 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)

Setting up the canvas

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.

Simulating ADC

From:

Flow,_,_ = self.Data.get_adc_val(1)

to

Flow = random.uniform(1, 5)

Obviously you could keep your own code for this

Looping with 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!

Answer to question 3

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.

like image 192
jimbob88 Avatar answered Oct 12 '22 03:10

jimbob88