How can I speed up an animation? - python

I'm trying to create a Matplotlib animation of my paw data, where you can see the pressure distribution on the entire pressure plate over time (256x64 sensors for 250 frames).
I found a working example on Matplotlib's own site and managed to get it working on my own data. However the 'animation' is awfully slow and I have no idea how to speed it up.
Here's an example of a gif Joe Kington made in another answer, which is about the speed with which it gets displayed. Considering the measurements are done at 125 Hz, this makes the measurements look awfully slow. If it ran at 30-60 fps, it could be run in 4 or 8 seconds rather than the current 20+.
I don't mind using whatever tool I need to get the job done, as long as there's some good documentation to figure out how to do it.
So my question is: how can I speed up these animations?
I've implemented Ignacio's suggestion to put in t.Start(1), however it only runs 'decently' when the Figure is this large:
class PlotFigure(Frame):
""" This class draws a window and updates it with data from DataCollect
"""
def __init__(self):
Frame.__init__(self, None, -1, "Test embedded wxFigure")
#Varying the size of Figure has a big influence on the speed
self.fig = Figure((3,3), 75)
self.canvas = FigureCanvasWxAgg(self, -1, self.fig)
EVT_TIMER(self, TIMER_ID, self.onTimer)
def init_plot_data(self):
self.datagen = DataCollect(array3d)
self.axes = self.fig.add_subplot(111)
self.axes.imshow(self.datagen.next().T)
def onTimer(self, evt):
self.data = self.datagen.next()
self.axes.imshow(self.datagen.next().T)
self.canvas.draw()
When I resize the window during the animation, it immediately slows down to a crawl. Which makes me suspect the delay isn't the only cause of the slow down. So any other suggestions? In case you're curious, here's a link to one of the ASCII files.

I found Joe Kington's answer that mentioned using Glumpy instead. At first I couldn't get it to work on my own data, but with some help on chat we managed to figure out how to adapt one of the Matplotlib examples that come with Glumpy to work on my data.
import numpy, glumpy
from glumpy.pylab import *
window = glumpy.Window(256,64)
Z = data.astype(numpy.float32)
t0, frames, t = 0,0,0
fig = plt.figure(figsize=(7,7))
ax = plt.subplot(111)
ax = imshow(Z[:,:,0], origin='lower', interpolation='bilinear')
show()
window = glumpy.active_window()
#window.event
def on_idle(dt):
global Z, t0, frames, t
t += dt
frames = frames + 1
if frames > 248:
fps = float(frames)/(t-t0)
print 'FPS: %.2f (%d frames in %.2f seconds)' % (fps, frames, t-t0)
frames,t0 = 0, t
for image, axis, alpha in items:
image.data[...] = Z[:,:,frames]
image.update()
window.draw()
window.mainloop()
The end result can be seen here, it doesn't matter how large I make the window, it will run at a very steady 58+ fps. So I must say, I'm very pleased with the end result!

The value passed to wx.Timer.Start() is the trigger rate in milliseconds. Pass a smaller value.

Use a Profiler to find the root cause, frame skipping might be useful too as a last resort.
Or switch to an alternative solution like Double Buffering using Device Contexts or PyOpenGL...

Related

How do I find, plot, and output the peaks of a live plotted Fast Fourier Transform (FFT) in Python?

I am working with the pyaudio and matplotlib packages for the first time and I am attempting to plot live audio data from microphone input, transform it to frequency domain information, and then output peaks with an input distance. This project is a modification of the three-part guide to build a spectrum analyzer found here.
Currently the code is formatted in a class as I have alternative methods that I am applying to the audio but I am only posting the class with the relevant methods as they don't make reference to each and are self-contained. Another quirk of the program is that it calls upon a local file though it only uses input from the user microphone; this is a leftover from the original functionality of plotting a sound file's intensity while it played and is no longer integral to the code.
import pyaudio
import wave
import struct
import pandas as pd
from scipy.fftpack import fft
from scipy.signal import find_peaks
import matplotlib.pyplot as plt
import numpy as np
class Wave:
def __init__(self, file) -> None:
self.CHUNK = 1024 * 4
self.obj = wave.open(file, "r")
self.callback_output = []
self.data = self.obj.readframes(self.CHUNK)
self.rate = 44100
# Initiate an instance of PyAudio
self.p = pyaudio.PyAudio()
# Open a stream with the file specifications
self.stream = self.p.open(format = pyaudio.paInt16,
channels = self.obj.getnchannels(),
rate = self.rate,
output = True,
input = True,
frames_per_buffer = self.CHUNK)
def fft_plot(self, distance: float):
x_fft = np.linspace(0, self.rate, self.CHUNK)
fig, ax = plt.subplots()
line_fft, = ax.semilogx(x_fft, np.random.rand(self.CHUNK), "-", lw = 2)
# Bind plot window sizes
ax.set_xlim(20, self.rate / 2)
plot_data = self.stream.read(self.CHUNK)
self.data_int = pd.DataFrame(struct.unpack(\
str(self.CHUNK * 2) + 'h', plot_data)).astype(dtype = "b")[::2]
y_fft = fft(self.data_int)
line_fft.set_ydata(np.abs(y_fft[0:self.CHUNK]) / (256 * self.CHUNK))
plt.show(block = False)
while True:
# Read incoming audio data
data = self.stream.read(self.CHUNK)
# Convert data to bits then to array
self.data_int = struct.unpack(str(4 * self.CHUNK) + 'B', data)
# Recompute FFT and update line
yf = fft(self.data_int)
line_data = np.abs(yf[0:self.CHUNK]) / (128 * self.CHUNK)
line_fft.set_ydata(line_data)
# Find all values above threshold
peaks, _ = find_peaks(line_data, distance = distance)
# Update the plot
plt.plot(peaks, line_data[peaks], "x")
fig.canvas.draw()
fig.canvas.flush_events()
# Exit program when plot window is closed
fig.canvas.mpl_connect('close_event', exit)
test_file = "C:/Users/Tam/Documents/VScode/Final Project/PrismGuitars.wav"
audio_test = Wave(test_file)
audio_test.fft_plot(2000)
The code does not throw any errors and runs fine with an okay framerate and only terminates when the plot window is closed, all of which is good. The issue I'm encountering is with the determination and plotting of the peaks of line_data as when I run this code the output over time looks like this matplotlib graph instance.
It seems that the peaks (or peak) are being found but at a lower frequency than the x of line_data and as such are shifted comparatively. The other, more minor, issue is that since this is a live plot I would like to clear the previous instance of the peak marker so that it only shows the current instance and not all of the ones plotted prior.
I have attempted in prior fixes to use the line_fft in the peak detection but as it is cast to a Line2D format the peak detection algorithm isn't able to deal with the data type. I have also tried implementing a list comprehension as seen in this post but the time to cast to list is prohibitively slow and did not return any peak markers when I ran it.
EDIT: Following Jody's input the program now returns the proper values as I was only printing an index for the x-coordinate of the peak marker. Nevertheless I would still appreciate some insight as to whether it is possible to update per marker rather than having all the previous ones constantly displayed.
As for the marker updating I have attempted to clear the plot in the while loop both before and after drawing the markers (in different tests of course) but I only ever end up with a completely blank graph.
Please let me know if there is anything I should clarify and thank you for your time.
As Jody pointed out the peaks variable contains indexes for the detected peaks that then need to be retrieved from x_fft and line_data in order to match up with the displayed data.
First we create a scatter plot:
scat = ax.scatter([], [], c = "purple", marker = "x")
This data can then be stacked using a container variable in the while loop as such:
array_peaks = np.c_[x_fft[peaks], line_data[peaks]]
and update the data in the while loop with:
scat.set_offsets(array_peaks)

Question about python threads timing and how they compare to a ROS implementation

I don't have a particular problem that I have to solve, but a question about (python) threads and their speed of execution.
The background
(or situation 1)
I have a python script using ROS that basically is (simplifying it):
class Viewer:
def __init__(self):
rospy.init_node("viewer")
rospy.Subscriber("/message",
MessageType, self.onMessage)
_, self.ax = plt.subplots(1, 1)
self.ax.set_ylim((-self.ax_lim, self.ax_lim))
self.ax.set_xlim((-self.ax_lim, self.ax_lim))
self.ax.set_aspect('equal')
self.ax.grid()
self.textvar = self.ax.text(-self.ax_lim-10, -self.ax_lim-10, '0.0')
plt.show()
def onMessage(self,msg):
#Here do some plotting
plt.pause(0.01) #<------NOTICE THIS!
#Here remove the plotting
As you see there is a ROS node that shows a matplotlib canvas and everytime we receive a message some plotting is done there. Notice there is a small pause
The threaded code (situation 2)
I have tried to implement something similar without ROS
So I have
class Viewer:
def __init__(self):
_, self.ax = plt.subplots(1, 1)
self.ax.set_ylim((-self.ax_lim, self.ax_lim))
self.ax.set_xlim((-self.ax_lim, self.ax_lim))
self.ax.set_aspect('equal')
self.ax.grid()
self.textvar = self.ax.text(-self.ax_lim-10, -self.ax_lim-10, '0.0')
self.i=0
#The threaded part
x = threading.Thread(target=self.thread_function, args=(1,))
x.start()
plt.show()
def thread_function(self,name):
while(self.i<5000):
#Do some plotting here
self.i+=1
plt.pause(0.01) #<-----NOTICE THIS
#here remove the plotting
The number of messages received by the situation 1 is around 5000 (therefore in situation 2 I put 5000 in the while loop
Now the thing is, written as it is above the situation 2 program takes more than 40 minutes!! to complete, while the situation 1 program completes in a couple of minutes
I tried commenting the plt.pause in the second one and it works much better. It completes in 4 minutes
My question is, why is the threaded version much slower than the first one. Take into account that the plot operations are the same.

PyQT Painter drawing Polygons

In my QT application I'm drawing lots of polygons like this:
I'm animating these, so some polygons will receive a new color. This animation runs 4-5 times per second.
However, calling the paintEvent() of the Qt.Painter() 4-5 times/second redraws ALL polygons which results in performance issues. Its only updated once a second, which is too slow. As you may see in the picture below, only some polygons in the first 12 rows needs to be updated:
[![enter image description here][2]][2]
In the QT docs I have read that you can't really save the state of the things you've already drawn. So you have to redraw everything again. Am I missing something? Is there a trick to still achieve this?
This is what my paintEvent() basically looks like (simplified, reduced cyclomatic complexity)
for y in range(len(self.array)):
for x in range(len(self.array[0])):
if(this): # simplified to reduce cyclomatic complexity
painter.setBrush(QBrush(QColor(20, 0, 255)))
elif(that):
painter.setBrush(QBrush(QColor(175, 175, 175)))
else:
painter.setBrush(QBrush(QColor(0, 0, 0)))
hexa_size = self.array[y][x]
hexas = createHexagon(x, y, hexa_size) # external functions to calculate the hexagon size and position
painter.drawPolygon(hexas)
painter.end()
call (update on each Pin change):
while True:
while(stempel.readPin(0) == 0):
QApplication.processEvents()
time.sleep(0.01)
self.draw_area.update() # Pin state changed, update polygons
while(stempel.readPin(0) == 1):
QApplication.processEvents()
time.sleep(0.01)
Qt allows scheduling an update for only a portion (region) of the widget, thus optimizing the result. This requires two step:
calling update(QRect) with an appropriate rectangle that covers only the part of the widget that requires repainting;
checking the event.rect() and then implement painting in order to paint only that region;
If you know for sure that only the first X rows are going to change color, then:
self.draw_area.update(
QRect(0, 0, self.draw_area.width(), <height of the repainted rows>)
Then, in the paintEvent:
if event.rect().bottom() < <height of the repainted rows>:
rowRange = range(indexOfTheLastRowToRepaint + 1)
else:
rowRange = range(len(self.array))
Note that another solution could be using QPicture, which is a way to "serialize" a QPainter in order to improve performance and avoid unnecessary computations.
class DrawArea(QWidget):
cache = None
def paintEvent(self, event):
if not self.cache:
self.cache = QPicture()
cachePainter = QPainter(self.cache)
# draw on the painter
cachePainter.end()
painter = QPainter(self)
painter.drawPicture(0, 0, self.cache)
def resizeEvent(self, event):
self.cache = None
The code above is very minimalistic, you might create multiple QPictures for every group of row and then decide which one paint whenever you require it, even by combining the event.rect() checking as explained above.
The major benefit of this technique is that QPainter usually processes a QPicture pretty fast, so you don't have to do all computations required for rows, polygons, etc.
Finally, the image you provided seems very repetitive, almost like a texture. In that case, you might consider using a QPixmap for each group of rows and then create a QBrush with that QPixmap. In that case, you'll only need to call painter.fillRect(self.rect(), self.textureBrush).
Solved it myself by using a QGraphicsScene + QGraphicsView:
self.scene = QGraphicsScene()
self.graphicView = QGraphicsView(self.scene, self)
Creating a list where all polygons are being saved:
self.polygons = [ [0] * len(array[0]) for _ in range(len(array))]
Initial drawing of all polygons:
for y in range(len(array)):
for x in range(len(array[0])):
polygon_size = self.array[y][x]
polygon = createPoly(x, y, polygon_size)
self.polygons[y][x] = self.scene.addPolygon(polygon, QPen(Qt.NoPen), QBrush(Qt.black))
if(y % 50 == 0): QApplication.processEvents()
Update rows indivudually:
for poly_size in active_rows:
for active_row in active_rows[poly_size]:
for x in range(0, len(array[0])):
if(array[active_row][x] == int(poly_size)):
self.polygons[active_row][x].setBrush(QBrush(QColor(20, 0, 255)))
if(array[active_row - 2][x] > 0 and array[active_row - 2][x] == int(poly_size)):
self.polygons[active_row - 2][x].setBrush(QBrush(QColor(175, 175, 175)))

pyqtgraph plotwidget multiple Y axis plots in wrong area

I have an embedded widget in a QT5 gui connected via PlotWidget.
I am trying to plot 2 streams of live data Voltage (self.p1) & Current (self.p2). Voltage on the left axis & Current on the right. So far I have each data stream associated with its relevant axis.
However my problem is that the Current plot (self.p2) is not in the correct area of the display. This particular plot appears in the upper left hand corner of the widget, it appears before the LHD axis. Its best to view the image to view the problem.
View Me
.
I know the problem lies in the setup function & self.p2 (Current) is being placed in the wrong location but my searching hasn't produced an answer.
Could someone please help?
Code used to generate graph, is called once on start up:
def pg_plot_setup(self): # not working still on left axis
self.p1 = self.graphicsView.plotItem
# x axis
self.p1.setLabel('bottom', 'Time', units='s', color='g', **{'font-size':'12pt'})
self.p1.getAxis('bottom').setPen(pg.mkPen(color='g', width=3))
# Y1 axis
self.p1.setLabel('left', 'Voltage', units='V', color='r', **{'font-size':'12pt'})
self.p1.getAxis('left').setPen(pg.mkPen(color='r', width=3))
self.p2 = pg.ViewBox()
self.p1.showAxis('right')
self.p1.scene().addItem(self.p2)
self.p1.getAxis('right').linkToView(self.p2)
self.p2.setXLink(self.p1)
# Y2 axis
self.p1.setLabel('right', 'Current', units="A", color='c', **{'font-size':'12pt'}) #<font>Ω</font>
self.p1.getAxis('right').setPen(pg.mkPen(color='c', width=3))
and code used to update display, is called via QTimer:
def update_graph_plot(self):
start = time.time()
X = np.asarray(self.graph_X, dtype=np.float32)
Y1 = np.asarray(self.graph_Y1, dtype=np.float32)
Y2 = np.asarray(self.graph_Y2, dtype=np.float32)
pen1=pg.mkPen(color='r',width=1.0)
pen2=pg.mkPen(color='c',width=1.0)
self.p1.plot(X,Y1,pen=pen1, name="V", clear=True)
self.p2.addItem(pg.PlotCurveItem(X,Y2,pen=pen2, name="I"))
I found the answer buried in here MultiplePlotAxes.py
adding this self.p2.setGeometry(self.p1.vb.sceneBoundingRect()) to function 'update_graph_plot' adjusts the size of viewbox each time the scene is updated, but it has to be in the update loop.
or add this self.p1.vb.sigResized.connect(self.updateViews) to function 'pg_plot_setup' as part of the setup, which shall then automatically call this function
def updateViews(self):
self.p2.setGeometry(self.p1.vb.sceneBoundingRect())
to resize viewbox (self.p2) each time self.p1 updates.

Reading real time values with pySerial while plotting

So here is the deal, I have a module which sends out data over the serial port at 9600 baud and I am using the matplotlib to plot that data in real time. I wrote this code
#! python
############ import section ###################
import serial
import struct
import numpy as np
import matplotlib.pyplot as plt
###############################################
############ Serial Configuration #############
com = serial.Serial(14,9600)
print ("Successfully opened " + com.portstr)
###############################################
def get_new_values():
s = com.read(20)
raw_data = struct.unpack('!BBBBBBBBBBBBBBBBBBBB', s)
j = 0;
norm_data = []
for i in range(0,20,2):
big_byte = raw_data[i+1]+(raw_data[i]*256)
norm_data.append(big_byte)
j = j+1
return norm_data
x = range(0,10,1)
y = get_new_values()
plt.ion()
line, = plt.plot(x,y)
while(1):
a = get_new_values()
line.set_ydata(a)
plt.draw()
com.close()
print ("Port Closed Successfully")
I get 20 bytes and then make 10 big bytes (2 byte data is split as two 1 byte values while transmitting). But I just noticed that I am not getting real time values from this. I am on windows 7 home basic if that matters.
Any clue why this is happening?
EDIT
Also, whenever I click on the plot it hangs.
I know the code below seems long and overly complex for your simple problem, but manual optimization is usually more code and more potential bugs. That's why premature optimization is almost always a mistake.
In __init__ it sets up the plot, setting references for the axis, canvas, line (it starts with a line drawn off screen), and the pre-animation background. Additionally __init__ registers callbacks to handle resizing and shutdown. The on_resize callback is needed to update the background (used for the blit) when the window is resized. The on_close callback uses a lock to update the running status. I haven't eliminated all race conditions with this, but this works to prevent a _tkinter.TclError caused by trying to blit to a terminated Tk app. I only tested with Tk, and on only one machine. YMMV, and I'm open to suggestions.
In the run method I've added a call to canvas.flush_events(). This should keep the plot window from hanging if you try to drag the window around and resize it. The while loop in this method calls self.get_new_values() to set the data in the plot. It then updates the plot using the configured method. If self.blit is true, it uses canvas.blit, else it uses pyplot.draw.
The variable spf (samples per frame) controls how many samples are plotted in a frame. You can use it in your implementation of get_new_values to determine the number of bytes to read (e.g. 2 * self.spf for 2 bytes per sample). I've set the default to 120, which is 5 frames per second if your data rate is 600 samples per second. You have to find the sweet spot that maximizes throughput vs time resolution in the graph while also keeping up with the incoming data.
Reading your data into a NumPy array instead of using a Python list will likely speed up processing. Also, it would give you easy access to tools to downsample and analyze the signal. You can read into a NumPy array directly from a byte string, but make sure you get the endianess right:
>>> data = b'\x01\xff' #big endian 256 + 255 = 511
>>> np.little_endian #my machine is little endian
True
>>> y = np.fromstring(data, dtype=np.uint16); y #so this is wrong
array([65281], dtype=uint16)
>>> if np.little_endian: y = y.byteswap()
>>> y #fixed
array([511], dtype=uint16)
Code:
from __future__ import division
from matplotlib import pyplot
import threading
class Main(object):
def __init__(self, samples_per_frame=120, blit=True):
self.blit = blit
self.spf = samples_per_frame
pyplot.ion()
self.ax = pyplot.subplot(111)
self.line, = self.ax.plot(range(self.spf), [-1] * self.spf)
self.ax.axis([0, self.spf, 0, 65536])
pyplot.draw()
self.canvas = self.ax.figure.canvas
self.background = self.canvas.copy_from_bbox(self.ax.bbox)
self.canvas.mpl_connect('resize_event', self.on_resize)
self.canvas.mpl_connect('close_event', self.on_close)
self.lock = threading.Lock()
self.run()
def get_new_values(self):
import time
import random
#simulate receiving data at 9600 bps (no cntrl/crc)
time.sleep(2 * self.spf * 8 / 9600)
y = [random.randrange(65536) for i in range(self.spf)]
self.line.set_ydata(y)
def on_resize(self, event):
self.line.set_ydata([-1] * self.spf)
pyplot.draw()
self.background = self.canvas.copy_from_bbox(self.ax.bbox)
def on_close(self, event):
with self.lock:
self.running = False
def run(self):
with self.lock:
self.running = True
while self.running:
self.canvas.flush_events()
with self.lock:
self.get_new_values()
if self.running:
if self.blit:
#blit for a higher frame rate
self.canvas.restore_region(self.background)
self.ax.draw_artist(self.line)
self.canvas.blit(self.ax.bbox)
else:
#versus a regular draw
pyplot.draw()
if __name__ == '__main__':
Main(samples_per_frame=120, blit=True)

Categories

Resources