matplotlib - wxpython backend - fast update - python

As part of a big GUI effort that is meant to plotting complex scientific figures I am trying to speed up interactions and figure updates. So far I've been using canvas.draw() method to update any changes to any drawn object in the figure.
I won't be able to reproduce an example code as it's a thousands lines of code but this is a snapshot of what I am dealing with
the above figure is a fairly congested example image with 3 Axes, contour plot, path, arrow, png image, different transparency objects, shadows, lines, fills, colorbar, etc
normally a user will be playing with a GUI like the one above to add, delete update or modify any plotted object.
For such a figure any modification is slow because it calls canvas.draw() at the backend.
#self.__canvas.Refresh()
#self.__canvas.Update()
###self.__canvas.update() # 'FigureCanvasWxAgg' object has no attribute 'update'
#self.__canvas.Refresh()
#self.__canvas.flush_events()
#self.__canvas.blit(self.__selectedAxes.bbox)
self.__canvas.draw()
I have tried using all the above but only canvas.draw results in updating the figure all the others won't. So far I am not sure how to speed up re-drawing the image after updating only one object.
Also, according to this post blit results in memory leaks. Did anyone tried to verify this hypothesis ?
Any suggestion is appreciated

Instead of using self.__canvas.draw()and redrawing all data on the plots, you can use blitting. By useage of blitting you can add specific new elements to a plot instead of redrawing the whole thing. This saves a massive amount of time.
In order to start blitting, the canvas has to be drawn at least once somewhere in your code. Otherwise there will be nothing 'to blit to'. So unfortunately you can't get fully rid of self.__canvas.draw().
To blit a certain element, for example a rectangle, you will first have to add the rectangle element to the axes. A rectangle is a matplotlib.patch and to add a patch to the axes you will have to use: self.axes.add_patch(rectangle). Once added you will need to draw it on the axes by using: self.axes.draw_artist(rectangle). After it has been drawn, you can blit it to the canvas using: self.canvas.blit(self.axes.bbox).
Save the plot with the blitted element as a background image using: self.background = self.canvas.copy_from_bbox(self.axes.bbox) and restore it to your canvas using: self.canvas.restore_region(self.background).
Some example code that blits a Rectangle to the canvas:
import matplotlib
matplotlib.use('WXAgg')
from matplotlib.figure import Figure
from matplotlib.backends.backend.wxagg import FigureCanvasWxAgg as FigureCanvas
import wx
class Panel(wx.Frame):
wx.Frame.__init__(self, parent, id, 'Title')
self.figure = Figure()
self.axes = self.figure.add_subplot(111)
self.canvas = FigureCanvas(self, -1, self.figure)
self.sizer = wx.BoxSizer(wx.VERTICAL)
self.sizer.Add(self.canvas, proportion=1, flag= wx.ALL | wx.GROW)
"""Plot data and stuff to canvas...."""
self.canvas.draw()
self.background = self.canvas.copy_from_bbox(self.axes.bbox)
square = matplotlib.patches.Rectangle((xPos,yPos), width, height)
self.canvas.restore_region(self.background)
self.axes.add_patch(square)
self.axes.draw_artist(square)
self.canvas.blit(self.axes.bbox)
self.background = self.canvas.copy_from_bbox(self.axes.bbox)
Can be I made some typos. But you will get the idea of it.

Related

How can I more efficiently display a pygame screen inside a tkinter GUII Python?

I have made a simulation in pygame. It makes use of a controlled value that the user can set through a tkinter slider and also has a graph displaying some of its statistics (made with matplotlib). Since the pygame, matplotlib and tkinter were all separate windows and I wanted them all in one place as in an app, I first embedded the matplotlib graph into the tk box using FigureCanvasTkAgg. I then tried the same with the pygame window. Having not found an answer on the internet, I then cobbled together a solution:
class PygameWin():
def __init__(self, root):
self.surface = pygame.display.set_mode(
[globalvars.screen_width, globalvars.screen_height])
self.surface.fill((255, 255, 255))
self.canvas = tk.Canvas(
root, height=globalvars.screen_height, width=globalvars.screen_width)
def update(self):
globalvars.amoebas.update()
self.surface.fill((255, 255, 255))
globalvars.amoebas.draw(self.surface)
pygame.image.save(self.surface, 'pygamewin.png')
self.img = ImageTk.PhotoImage(Image.open("pygamewin.png"))
self.canvas.create_image(250,275, image=self.img)
self.canvas.pack()
It basically saves the pygame window as an image and then adds it into a canvas in the tk interface. It works as intended except for the fact that it is very slow and only updates every 100 ticks (times the amoeba are updated). Could anyone help me make it more efficient? I realise that other parts of my code will be impacting the speed, and I can add the full program in if needed.
P.S. Although I can see the pygame window in the tk box as planned, it is still showing up as a separate window as well, so if anyone could tell me how to get rid of that it would be much appreciated.
The reason why your solution is slow is that you are saving the pygame surface as an image and then reloading it every time you want to update the display in tkinter. This is an unnecessary overhead that can slow down the application, especially if you are doing it every 100 ticks.
Instead of saving and reloading the image, you can create a pygame surface that acts as a buffer, draw your amoebas onto that surface, and then convert the surface to a PhotoImage to display it in your tkinter canvas. This approach will reduce the overhead of saving and reloading the image.
Here's an example implementation of this approach:
class PygameWin():
def __init__(self, root):
self.surface = pygame.Surface(
(globalvars.screen_width, globalvars.screen_height))
self.surface.fill((255, 255, 255))
self.canvas = tk.Canvas(
root, height=globalvars.screen_height, width=globalvars.screen_width)
# create a buffer to hold the image data
self.buffer = pygame.Surface(
(globalvars.screen_width, globalvars.screen_height))
def update(self):
globalvars.amoebas.update()
# draw the amoebas onto the buffer
self.buffer.fill((255, 255, 255))
globalvars.amoebas.draw(self.buffer)
# convert the buffer to a PhotoImage and display it in the canvas
img = ImageTk.PhotoImage(Image.frombytes(
'RGB', (self.buffer.get_width(), self.buffer.get_height()),
pygame.image.tostring(self.buffer, 'RGB')))
self.canvas.create_image(0, 0, image=img, anchor='nw')
self.canvas.pack()
# update the pygame display
pygame.display.update()
Regarding the issue of the pygame window showing up as a separate window, you can try setting the position of the window off-screen using the following code:
os.environ['SDL_VIDEO_WINDOW_POS'] = '-1000,-1000'
This should position the window off-screen and prevent it from being visible. You can add this code before the pygame initialization.

Specifying figure size in print_figure function in Matplotlib.FigureCanvasBase

I have a Canvas class that inherits from the FigureCanvas class in Matplotlib.
from matplotlib.backends.backend_qt5agg import FigureCanvas
class Canvas(FigureCanvas):
def __init__(self):
fig = Figure(figsize=(5, 3))
super().__init__(fig)
And I display canvas figures in the PyQt5 window. The canvas size changes depending on the size of the window. When I print these canvases, it always prints at the current size of the canvas (solutions in How do I change the size of figures drawn with Matplotlib? and Specify figure size in centimeter in matplotlib do not work).
However, I would like to print them in a fixed size, no matter what the current size is. As far as I could tell from the internet, I cannot specify size in print_figure() function. How can I print all the figures at the fixed size?

Matplotlib live plot relim after using navigation bar in tkinter gui

I am making a gui in tkinter with live, embedded matplotlib graphs. I am using FigureCanvasTkAgg for the canvas, NavigationToolbar2Tk for the navigation bar, and FuncAnimation to handle periodic updates of the given source of data.
The callback tied to FuncAnimation resets the data on a given line (i.e. the return value from Axes.plot(...)) every invocation (i.e. Line2D.set_data(...)). The callback also redetermines and applies the appropriate x- and y-axis limits to fit the new data via
axis.relim()
axis.autoscale_view()
where axis is an instance of AxesSubplot.
Before the navigation bar is used, this works great; any new data added is appropriately reflected in the graph and the axes automatically re-scale to fit it, which was my goal.
The problem I am facing is that if any of the functions on the navigation bar are used (pan, zoom, etc.) the re-scaling fails to work any longer, meaning the graph may grow out of view and the user's only way to see new data is to manually pan over to it or to manually zoom out, which is undesirable.
Realistically, this functionality make sense since it would be annoying to, for example, try to zoom in a part of the plot only to have it zoom out immediately to refit the axes to new data, which is why I had intended to add a tkinter.Checkbutton to temporarily disable the re-scaling.
I've tried to look into the source for the navigation bar, and it seems to change state on the axes and canvas which I can only assume is the problem, but I have so far been unsuccessful at finding a way to "undo" these changes. If such a way exists, I would bind it to a tkinter.Button or something so the automatic re-scaling can be re-enabled.
How might I fix this problem?
Below is a minimal example that demonstrates this problem.
import math
import itertools
import tkinter as tk
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
from matplotlib.animation import FuncAnimation
def xydata_generator(func, div):
for num in itertools.count():
num = num / div
yield num, func(num)
class Plot(tk.Frame):
def __init__(self, master, data_source, interval=100, *args, **kwargs):
super().__init__(master, *args, **kwargs)
self.data_source = data_source
self.figure = Figure((5, 5), 100)
self.canvas = FigureCanvasTkAgg(self.figure, self)
self.nav_bar = NavigationToolbar2Tk(self.canvas, self)
self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
self.axis = self.figure.add_subplot(111)
self.x_data = []
self.y_data = []
self.line = self.axis.plot([], [])[0] # Axes.plot returns a list
# Set the data to a mutable type so we only need to append to it then force the line to invalidate its cache
self.line.set_data(self.x_data, self.y_data)
self.ani = FuncAnimation(self.figure, self.update_plot, interval=interval)
def update_plot(self, _):
x, y = next(self.data_source) # (realistically the data source wouldn't be restricted to be a generator)
# Because the Line2D object stores a reference to the two lists, we need only update the lists and signal
# that the line needs to be updated.
self.x_data.append(x)
self.y_data.append(y)
self.line.recache_always()
self._refit_artists()
def _refit_artists(self):
self.axis.relim()
self.axis.autoscale_view()
root = tk.Tk()
data = xydata_generator(math.sin, 5)
plot = Plot(root, data)
plot.pack(fill=tk.BOTH, expand=True)
root.mainloop()
Turns out to be pretty simple. To reset the axes so that the calls to Axes.relim() and Axes.autoscale_view() take effect, one simply needs to call Axes.set_autoscale_on(True). This must be repeated every time the functions on the navigation bar (pan, zoom, etc.) are used.

Matplotlib animation with blit -- how to update plot title?

I use matplotlib to animate a plot, by copying the background and blitting:
f = Figure(tight_layout=True)
canvas = FigureCanvasTkAgg(f, master=pframe)
canvas.get_tk_widget().pack()
ax = f.add_subplot(111)
# Set inial plot title
title = ax.set_title("First title")
canvas.show()
# Capture the background of the figure
background = canvas.copy_from_bbox(ax.bbox)
line, = ax.plot(x, y)
canvas._tkcanvas.pack()
Periodically I update the plot:
# How to update the title here?
line.set_ydata(new_data)
ax.draw_artist(line)
canvas.blit(ax.bbox)
How could I update -- as efficiently as possible, the plot title every time I update the plot?
Edit:
title.set_text("New title")
ax.draw_artist(title)
either before or after
canvas.blit(ax.bbox)
does not update the title. I think somehow I should redraw the title artist, or I should only capture the graph, as blit(ax.bbox) overwrites the entire title plot area including the title.
Swooping in years later to say that I too was stuck on this problem for the last few days, and disabling blit wasn't an option for me (as the fps would become way too slow). For matplotlib 3.1.3 at least, if you send a resize event to the canvas, it properly flushes the background and regenerates it with any updates. So you can work around this by detecting when you need to update the title, and then calling fig.canvas.resize_event() to force a flush. Hope this helps future people!
The following draws the plot and allows you to make changes,
import matplotlib.pyplot as plt
import numpy as np
fig, ax = plt.subplots(1,1)
ax.plot(np.linspace(0.,10.,100))
title = ax.set_title("First title")
plt.show(block=False)
The title can then be updated using,
title.set_text("Second title")
plt.draw()

Change resolution of Matplotlib Figure window when saving plot?

I'm using Windows XP v3/Python 2.7 with Canopy and Anaconda package managers/editors.
I am using Python/Matplotlib to produce some Bland-Altman plots (statistical scatter plots) for publication.
After processing the data, the plt.show() command opens a new "Figure" window containing the plot, which looks fine.
I want to be able to use the dynamic pan and zoom commands in this window to interactively optimise the appearance of my plot, then save it as it appears in the window as a high resolution press-quality png image (400-600 dpi, 7 x 5 inches).
The default setting for saving images from the "Figure" window appears to be set to screen resolution (800 x 600 pixels), and I cannot find any options in this window which allow me to change these settings.
I've read other posts on this forum which explain how to directly save a plot from Python in higher resolution by using the following commands to manipulate dpi and image size, e.g.:
plt.figure(figsize=(18, 12), dpi=400)
plt.savefig("myplot.png", dpi = 400)
However, this is not the solution that I'm looking for; as I want to be able to modify the plot using the dynamic pan and zoom features of the "Figure" window before saving in a higher resolution than the default screen resolution.
I'd be grateful for your help.
Many thanks in anticipation & Happy New Year.
Dave
(UK)
Try this:
Determine how to set width and height using a pixels-to-inches converter, like in the following matplotlib documentation. Then try:
import matplotlib.pyplot as plt
fig = plt.figure(frameon=False)
fig.set_size_inches(width,height)
I had this issue in spyder and found changing the value in Preferences > iPython Console > Inline Backend > Resolution changes the resolution when I save figures from the built in window viewing application.
One may register an event upon a key press that would save the figure with some previously given size and dpi. The following uses a class that stores some figsize and dpi and upon pressing t wll change the figure size and dpi of the figure. It will then save this figure and restore the old size and dpi such that the figure on screen remains unchanged.
import matplotlib
matplotlib.use("TkAgg")
import matplotlib.pyplot as plt
fig,ax=plt.subplots()
ax.plot([1,3,1])
class AnySizeSaver():
def __init__(self, fig=None, figsize=None, dpi=None, filename=None):
if not fig: fig=plt.gcf()
self.fig = fig
if not figsize: figsize=self.fig.get_size_inches()
self.figsize=figsize
if not dpi: dpi=self.fig.dpi
self.dpi=dpi
if not filename: filename="myplot.png"
self.filename=filename
self.cid = self.fig.canvas.mpl_connect("key_press_event", self.key_press)
def key_press(self, event):
if event.key == "t":
self.save()
def save(self):
oldfigsize = self.fig.get_size_inches()
olddpi=self.fig.dpi
self.fig.set_size_inches(self.figsize)
self.fig.set_dpi(self.dpi)
self.fig.savefig(self.filename, dpi=self.dpi)
self.fig.set_size_inches(oldfigsize, forward=True)
self.fig.set_dpi(olddpi)
self.fig.canvas.draw_idle()
print(fig.get_size_inches())
ass = AnySizeSaver(fig=fig, figsize=(3,3), dpi=600)
plt.show()

Categories

Resources