I'm a novice using matplotlib as an embedded control in my PyQt4 application to display image data. I'd like to be able to allow the user to interactively draw a line on the image by clicking and dragging. I have it working but it is so slow as to be unusable, leading me to believe I'm not going about it the correct way. The only way I can get the line to appear is by forcing the canvas to redraw each time the mouse moves (I suspect this is the cause of the slowdown).
For example, on the mouse down event I store the current coordinates and add a Line2D object to the plot as follows:
def onMouseMove(self, event):
if self.drawingLine:
self.lineStartX = event.xdata
self.lineStopX = event.xdata
self.lineStartY = event.ydata
self.lineStopY = event.ydata
self.line = Line2D([self.lineStartX, self.lineStopX], [self.lineStartY, self.lineStopY], linewidth = 1.5, color = 'r')
self.axes.add_line(self.line)
Then, in my mouse move event I redraw the line as follows:
def onMouseMove(self, event):
if self.drawingLine:
self.lineStopX = event.xdata
self.lineStopY = event.ydata
# Adjust the line to the new endpoint:
self.line.set_data([self.lineStartX, self.lineStopX], [self.lineStartY, self.lineStopY])
# Force a redraw otherwise you don't see any changes:
self.fig.canvas.draw()
As I've stated this approach is unusably slow and hence probably wrong. Can somebody please clue me in to what the proper approach is here? Thank you all in advance.
First off, you will already gain a little by using
self.fig.canvas.draw_idle()
instead of draw(). This redraws the canvas only when it's not currently beeing repainted, saving you a lot of draws.
If this is not enough, you would need to use the technique of blitting. Now since you don't have a minimal example, I will not provide any complete solution for this here, but e.g. the answer to this question, why is plotting with Matplotlib so slow?, has an example of that.
The idea is to store the background, and only redraw the part that changes (here the line).
background = fig.canvas.copy_from_bbox(ax.bbox)
# then during mouse move
fig.canvas.restore_region(background)
line.set_data(...)
ax.draw_artist(line)
fig.canvas.blit(ax.bbox)
# only after mouse has stopped moving
fig.canvas.draw_idle()
This technique is also used internally by some matplotlib widgets, e.g. matplotlib.widgets.Cursor to let the lines follow the cursor quickly.
This brings me to the last point, which is: You don't need to reinvent the wheel. There is a matplotlib.widgets.RectangleSelector, which by defaut draws a rectangle for selection. But you may use its drawtype='line' argument, to change the selection to a line, together with the argument blit=True this should already give you what you need - you will just need to add the code to finally draw a line once the selection is finished.
Note that in the newest matplotlib version, there is even a matplotlib.widgets.PolygonSelector, which may directly be what you need.
matplotlib is built to be flexible and to work with multiple different backends. It is very slow at real-time plotting. The problem is that your mouse move events are very rapid. Anything trying to keep up with the mouse movement will probably be slow. You need to call the plot less often. You can do this by checking the time in your mouse move function and trying to limit the plotting calls to whatever works.
import time
def onMouseMove(self, event):
if self.drawingLine and time.time() - last_time > 0.03: # Change the 0.03 to change how often you plot.
last_time = time.time()
...
I highly suggest pyqtgraph. pyqtgraph has built in rate limiting signals that you can work with to do this.
Below is a basic example of how you can do this.
# Change the style to look like matplotlib
pyqtgraph.setConfigOption("background", QtGui.QColor.fromRgbF(230/255, 230/255, 234/255, 255/255))
pyqtgraph.setConfigOption("background", 'w')
pyqtgraph.setConfigOption("foreground", 'k')
pyqtgraph.setConfigOption("antialias", True)
# Create the widgets and plot items
glw = pyqtgraph.GraphicsLayoutWidget()
pg = glw.addPlot(0, 0)
class MyClass(object):
...
...
def onMouseMove(self, event):
if self.drawingLine:
scene_pos = event[0]
data_pos = pg.getViewBox().mapSceneToView(scene_pos)
x, y = data_pos.x(), data_pos.y()
self.lineStopX = x
self.lineStopY = y
# Adjust the line to the new endpoint:
if not self.line:
self.line = pg.plot(x=[], y=[])
self.line.setData(x=[self.lineStartX, self.lineStopX],
y=[self.lineStartY, self.lineStopY])
mouse_move_sig = pyqtgraph.SignalProxy(pg.scene().sigMouseMoved,
rateLimit=60, slot=onMouseMove)
Related
I have a function "makeGrid(tensor)" that takes as an argument a pytorch tensor of shape (B,3,H,W), and uses ImageGrid to make a figure that displays the batch of figures in a grid.
Now, the model that outputs "tensor", depends on one parameter, "alpha". I would like to include a slider on the figure such that I can modify alpha "live". I am using the "Slider" widget from matplotlib roughly as such :
result = model(tensor)
images,grid = makeGrid(result)
ifig = plt.figure()# Figure with slider
axalpha = ifig.add_axes([0.25, 0.1, 0.65, 0.03])
# How to add the "images" to ifig ???
alpha_slider = Slider(
ax=axsmol,
valmin=-2,
valmax=2,
valinit=1,
)
def update(val):
model.alpha = alpha_slider.val
result= model(img_batch)
images,grid = makeGrid(result)
# Same problem, need to update ifig with new images
alpha_slider.on_changed(update)
plt.show()
So, my main problem is I have no idea how to use the already created figure (images) and/or grid (which is an ImageGrid object, roughly a list of axes afaik) as a subplot of "ifig", the interactive figure which contains slider and images.
Very sorry as this seems to be a basic question, but searching for "how to add already created figure as subplot of figure" or other things didn't yield solutions to my problem (or at least, in my limited point of view).
So I'm using FuncAnimation from matplotlib so dynamically plot some data as it arrives from a serial port (in my project is the vehicle class from dronekit, which is displayed with the green dot), what I have basically is the animation called which every loop is receiving a new vehicle class with data changed so it can be plotted, but for some reason it plots but after a couple of seconds later after the thread of the mission(which allows the "refresh" of the vehicle data it pops up and kills python (Wheel of death), here's what I get:
I've put some tracking prints inside the function that is called when the FuncAnimation starts running, looks like this:
def droneAnimation(i, vehicle, droneScatter):
time.sleep(1)
lat = [vehicle.location.global_relative_frame.lat]
lon = [vehicle.location.global_relative_frame.lon]
alt = [vehicle.location.global_relative_frame.alt]
print("Alt received: " + str(alt))
droneScatter._offsets3d = (lat,lon,alt)
print("Changed pos")
As you can see those prints are triggered the first few seconds but still crashes after a few iterations.
The FuncAnimation is called like this:
fig,droneScatter = plotLiveSimpleFacade(vehicle,w,2)
ani = FuncAnimation(fig,droneAnimation, fargs = (vehicle,droneScatter))
plt.draw()
plt.pause(0.1)
m = threading.Thread(target=MissionStart(vehicle,hmax) , name = "MISSION")
m.start()
For reference: fig is a plt.figure(),droneScatter is just a scatter point, vehicle is the vehicle class containing the data that dynamically updates and the MissionStart thread is just a thread to make the vehicle class change overtime.
I would like to mention as well that the fig is on interactive mode on, and the axes limits are set well (I saw that when you dynamically change data but don't scale the axes may have problems) also, trying different combinations of plt.draw() and plt.plot(block = False) leads me to not plotting at all or just a blank plot.
Since I have no idea of what is causing this I'll put the dronekit tag on this and the threading to see if anyone has any idea!
I've looked onto threading with matplotlib and looks like threading with this said library it's not the best as it's not thread safe, the best bet is to look at multiprocessing with python or approaching the problem in a different manner.
You can find more information at this post
I made a simple animation slider for viewing slices of 3D images. I adapted it from this post. One small problem though, after a little sliding around it always freezes up. After some fiddling around I realised that the freeze stops from happening if I add a print statement to my update method. So I figured that it has something to do with threads getting in each others way (I'm no expert, just guessing) so instead of an ugly print statement I tried including a sleep in its place. But t doesn't work! The only thing that stops it from freezing is a print statement. Why might this be?
def animate(img, cmap='viridis', clim=None):
fig, _ = plt.subplots()
index = 0
max_index = len(img)
plot = plt.imshow(img[index])
plt.set_cmap(cmap)
if clim is not None: plt.clim(clim[0], clim[1])
ax = plt.axes([0.25, .03, 0.50, 0.02])
slider = Slider(ax, "Index", 0, (max_index - 1), valinit=index, valfmt='%d')
def update(i):
print("Hello World!") # Delay to stop freeze. Only thing that seems to work, not even sleep works.
i = int(slider.val)
plot.set_data(img[i])
fig.canvas.draw_idle()
slider.on_changed(update)
plt.show()
EDIT: After some further investigation I noticed that if the image is deeper (more slices) even the print statement is not sufficient. Because of this I am starting to believe that it has something to do with that the update method is called too many times in a short time span.
EDIT 2: Calling the function from the python console with animate([np.random.rand(300,300) for _ in range(20)], clim=[0,1]) produces the error on my machine. You need to swipe the slider back and forth rapidly a little while.
EDIT 3: I am using TkAgg version 3.1.2
I've developed an gui with python pyqt. There I have a matplotlib figure with x,y-Data and vlines that needs to change dynamically with a QSlider.
Right now I change the data just with deleting everything and plot again but this is not effective
This is how I do it:
def update_verticalLines(self, Data, xData, valueSlider1, valueSlider2, PlotNr, width_wg):
if PlotNr == 2:
self.axes.cla()
self.axes.plot(xData, Data, color='b', linewidth=2)
self.axes.vlines(valueSlider1,min(Data),max(Data),color='r',linewidth=1.5, zorder = 4)
self.axes.vlines(valueSlider2,min(Data),max(Data),color='r',linewidth=1.5, zorder = 4)
self.axes.text(1,0.8*max(Data),str(np.round(width_wg,2))+u"µm", fontsize=16, bbox=dict(facecolor='m', alpha=0.5))
self.axes.text(1,0.6*max(Data),"Pos1: "+str(round(valueSlider1,2))+u"µm", fontsize=16, bbox=dict(facecolor='m', alpha=0.5))
self.axes.text(1,0.4*max(Data),"Pos2: "+str(round(valueSlider2,2))+u"µm", fontsize=16, bbox=dict(facecolor='m', alpha=0.5))
self.axes.grid(True)
self.draw()
"vlines" are LineCollections in matplotlib. I searched in the documentation but could not find any hint to a function like 'set_xdata' How can I change the x value of vertical lines when they are already drawn and embedded into FigureCanvas?
I have the same problem with changing the x and y data. When trying the known functions of matplotlib like 'set_data', I get an error that AxisSubPlot does not have this attribute.
In the following is my code for the FigureCanvas Class. The def update_verticalLines should only contain commands for changing the x coord of the vlines and not complete redraw.
Edit: solution
Thanks #Craigular Joe
This was not exactly how it worked for me. I needed to change something:
def update_verticalLines(self, Data, xData, valueSlider1, valueSlider2, PlotNr, width_wg):
self.vLine1.remove()
self.vLine1 = self.axes.vlines(valueSlider1,min(Data), max(Data), color='g', linewidth=1.5, zorder = 4)
self.vLine2.remove()
self.vLine2 = self.axes.vlines(valueSlider2,min(Data), max(Data), color='g', linewidth=1.5, zorder = 4)
self.axes.draw_artist(self.vLine1)
self.axes.draw_artist(self.vLine2)
#self.update()
#self.flush_events()
self.draw()
update() did not work without draw(). (The old vlines stayed)
flush_events() did some crazy stuff. I have two instances of FigureCanvas. flush_events() caused that within the second instance call the vlines moved with the slider but moved then back to the start position.
When you create the vlines, save a reference to them, e.g.
self.my_vlines = self.axes.vlines(...)
so that when you want to change them, you can just remove and replace them, e.g.
self.my_vlines.remove()
self.my_vlines = self.axes.vlines(...)
# Redraw vline
self.axes.draw_artist(self.my_vlines)
# Add newly-rendered lines to drawing backend
self.update()
# Flush GUI events for figure
self.flush_events()
By the way, in the future you should try your best to pare down your code sample to just the essential parts. Having a lot of unnecessary sample code makes it hard to understand your question. :)
I often produce individual figures with matplotlib which are supposed to be aligned vertically or horizontally e.g. in a LaTeX document. My goals are:
Avoid excessive margins or clipping in all figures. For stand-alone figures this is easily solved by using bbox_inches='tight' in savefig.
Use the exact same bounding box for all figures, since I want my axes elements to line up nicely in the resulting document.
My current solution is a very inconvenient trial and error approach: I manually try to guess reasonable margins and set them via plt.subplots_adjust(). This often takes a lot of time until I have a satisfactory result.
I'm wondering if it is possible to combine the power of bbox_inches='tight' with plt.subplots_adjust()? Is it possible to programmatically get the resulting bounding box of bbox_inches='tight'? This would allow my to determining the bounding box for the figure with the largest axis labels/titles, and use this bounding box for all other figures.
Thanks to #Schorsch and #tcaswell for pointing out that ax.get_tightbbox almost solves this problem. The only tricky part is to convert the renderer dependent bbox into an "inches" bbox. The general solution is:
tight_bbox_raw = ax.get_tightbbox(fig.canvas.get_renderer())
tight_bbox = TransformedBbox(tight_bbox_raw, Affine2D().scale(1./fig.dpi))
I can now reuse the tight_bbox for my other plots as well, resulting in nice, identically boxed figures.
I have also written a small helper class -- not elegant, but useful: I call max_box_tracker.add_figure for every figure I create, and it will internally update a maximum bounding box. After generating all plots, I can print out the resulting bounding box (directly in the form of code to add to my script).
class MaxBoxTracker:
def __init__(self):
self.box = None
def add_figure(self, fig, ax):
from matplotlib.transforms import TransformedBbox, Affine2D
box_raw = ax.get_tightbbox(fig.canvas.get_renderer())
box = TransformedBbox(box_raw, Affine2D().scale(1./fig.dpi)).get_points()
if self.box is None:
self.box = box
else:
self.box[0][0] = min(self.box[0][0], box[0][0])
self.box[0][1] = min(self.box[0][1], box[0][1])
self.box[1][0] = max(self.box[1][0], box[1][0])
self.box[1][1] = max(self.box[1][1], box[1][1])
def print_max(self):
print 'Code to set maximum bbox:\nfrom matplotlib.transforms import Bbox\ntight_bbox = Bbox([[%f, %f], [%f, %f]])' % (
self.box[0][0], self.box[0][1], self.box[1][0], self.box[1][1]
)