8

I have a program with rapid animations which works perfectly under pygame, and for technical reasons, I need to do the same using only matplotlib or an other widespread module.

The program structure is roughly:

pygame.init()        
SURF = pygame.display.set_mode((500, 500))
arr = pygame.surfarray.pixels2d(SURF) # a view for numpy, as a 2D array
while ok:
    # modify some pixels of arr
    pygame.display.flip()
pygame.quit()

I have no low level matplotlib experience, but I think it is possible to do equivalent things with matplotlib. In other words :

How to share the bitmap of a figure, modify some pixels and refresh the screen ?

Here is a minimal working exemple, which flips 250 frames per second (more than the screen ...) on my computer :

import pygame,numpy,time
pygame.init()
size=(400,400)        
SURF = pygame.display.set_mode(size)
arr = pygame.surfarray.pixels2d(SURF) # buffer pour numpy   
t0=time.clock()

for counter in range(1000):
        arr[:]=numpy.random.randint(0,0xfffff,size)
        pygame.display.flip()      
pygame.quit()

print(counter/(time.clock()-t0))

EDIT

What I try with indications in answers :

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation

fig = plt.figure()


def f(x, y):
    return np.sin(x) + np.cos(y)

x = np.linspace(0, 2 * np.pi, 400)
y = np.linspace(0, 2 * np.pi, 400).reshape(-1, 1)

im = plt.imshow(f(x, y), animated=True)

count=0
t0=time.clock()+1
def updatefig(*args):
    global x, y,count,t0
    x += np.pi / 15.
    y += np.pi / 20.
    im.set_array(f(x, y))
    if time.clock()<t0:
        count+=1
    else:
        print (count)
        count=0
        t0=time.clock()+1     
    return im,

ani = animation.FuncAnimation(fig, updatefig, interval=50, blit=True)
plt.show()

But this only provides 20 fps....

B. M.
  • 18,243
  • 2
  • 35
  • 54
  • Maybe [this](https://matplotlib.org/api/animation_api.html) could be helpful for you. – zipa May 12 '18 at 21:46
  • This does not address editing the canvas bitmap, but for displaying the canvas live, the answer is `plt.draw(); plt.pause(0.000001)`. Where `plt` is from `from matplotlib import pyplot as plt`. A post that may be relevant to your problem is: https://stackoverflow.com/questions/40126176/fast-live-plotting-in-matplotlib-pyplot – Peter May 13 '18 at 10:44
  • 1
    Matplotlib is really not designed for high performance animations. *"I need to do the same using only matplotlib or an other widespread module."* Could you be a bit more precise about your requirements? What you consider to be a "widespread module"? Are you targeting a particular platform? – ali_m May 15 '18 at 20:58
  • You're limiting the framerate with a 50ms delay for each frame (`interval=50`). Maybe reducing that will be enough to get the performance you want... btw note that `time.clock()` is not really reliable as a wall clock on unix, `time.time()` or `timeit.default_timer` may be a better choice or `time.perf_counter()` on recent python – filippo May 16 '18 at 06:30

4 Answers4

8

It should be noted that the human brain is capable of "seeing" up to a framerate of ~25 fps. Faster updates are not actually resolved.

Matplotlib

With matplotlib and its animation module the example from the question runs with 84 fps on my computer.

import time
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation

fig, ax = plt.subplots()


def f(x, y):
    return np.sin(x) + np.cos(y)

x = np.linspace(0, 2 * np.pi, 400)
y = np.linspace(0, 2 * np.pi, 400).reshape(-1, 1)

im = ax.imshow(f(x, y), animated=True)
text = ax.text(200,200, "")

class FPS():
    def __init__(self, avg=10):
        self.fps = np.empty(avg)
        self.t0 = time.clock()
    def tick(self):
        t = time.clock()
        self.fps[1:] = self.fps[:-1]
        self.fps[0] = 1./(t-self.t0)
        self.t0 = t
        return self.fps.mean()

fps = FPS(100)

def updatefig(i):
    global x, y
    x += np.pi / 15.
    y += np.pi / 20.
    im.set_array(f(x, y))
    tx = 'Mean Frame Rate:\n {fps:.3f}FPS'.format(fps= fps.tick() ) 
    text.set_text(tx)     
    return im, text,

ani = animation.FuncAnimation(fig, updatefig, interval=1, blit=True)
plt.show()

PyQtGraph

In pyqtgraph a higher framerate is obtained, it would run with 295 fps on my computer.

import sys
import time
from pyqtgraph.Qt import QtCore, QtGui
import numpy as np
import pyqtgraph as pg

class FPS():
    def __init__(self, avg=10):
        self.fps = np.empty(avg)
        self.t0 = time.clock()
    def tick(self):
        t = time.clock()
        self.fps[1:] = self.fps[:-1]
        self.fps[0] = 1./(t-self.t0)
        self.t0 = t
        return self.fps.mean()

fps = FPS(100)

class App(QtGui.QMainWindow):
    def __init__(self, parent=None):
        super(App, self).__init__(parent)

        #### Create Gui Elements ###########
        self.mainbox = QtGui.QWidget()
        self.setCentralWidget(self.mainbox)
        self.mainbox.setLayout(QtGui.QVBoxLayout())

        self.canvas = pg.GraphicsLayoutWidget()
        self.mainbox.layout().addWidget(self.canvas)

        self.label = QtGui.QLabel()
        self.mainbox.layout().addWidget(self.label)

        self.view = self.canvas.addViewBox()
        self.view.setAspectLocked(True)
        self.view.setRange(QtCore.QRectF(0,0, 100, 100))

        #  image plot
        self.img = pg.ImageItem(border='w')
        self.view.addItem(self.img)

        #### Set Data  #####################
        self.x = np.linspace(0, 2 * np.pi, 400)
        self.y = np.linspace(0, 2 * np.pi, 400).reshape(-1, 1)

        #### Start  #####################
        self._update()
        
    def f(self, x, y):
            return np.sin(x) + np.cos(y)
        
    def _update(self):

        self.x += np.pi / 15.
        self.y += np.pi / 20.
        self.img.setImage(self.f(self.x, self.y))

        tx = 'Mean Frame Rate:\n {fps:.3f}FPS'.format(fps= fps.tick() ) 
        self.label.setText(tx)
        QtCore.QTimer.singleShot(1, self._update)


if __name__ == '__main__':

    app = QtGui.QApplication(sys.argv)
    thisapp = App()
    thisapp.show()
    sys.exit(app.exec_())
Community
  • 1
  • 1
ImportanceOfBeingErnest
  • 321,279
  • 53
  • 665
  • 712
  • Thanks for the post. of course 25 fps is sufficient , but for complex real time animations, building each image take time, "video" aspect must not be the bottelneck. – B. M. May 17 '18 at 21:15
  • 1
    That's my point. As long as your animation runs faster than 25 fps, "video" is not the bottleneck. – ImportanceOfBeingErnest May 17 '18 at 22:14
7

If you want to animate a plot, then you can take a look at the animation functionality in matplotlib under matplotlib.animation.Animation. Here's a great tutorial - https://jakevdp.github.io/blog/2012/08/18/matplotlib-animation-tutorial.

If you just want to periodically update an adhoc bitmap, I am not sure matplotlib is meant for what you are trying to achieve. From matplotlib docs:

Matplotlib is a Python 2D plotting library which produces publication quality figures in a variety of hardcopy formats and interactive environments across platforms.

If you would like to periodically update an adhoc image on the screen, you may want to look into GUI libraries for python. Here is a short summary of available options - https://docs.python.org/3/faq/gui.html. Tkinter is a pretty standard one and is shipped with python. You can use the ImageTk module in pillow to create/modify images for displaying via Tkinter - http://pillow.readthedocs.io/en/4.2.x/reference/ImageTk.html.

Vladimirs
  • 910
  • 11
  • 17
  • Thanks for your answer. but ImageTk docs says "this can be very slow if the photo image is displayed". That's my problem .... – B. M. May 13 '18 at 06:29
6

If you just need to animate a matplotlib canvas the animation framework is the answer. There's a simple example here that does basically what you ask.

If this is going to be part of a more complex application you probably want finer control over a specific backend.

Here's a quick attempt using Qt loosely based on this matplotlib example.

It's using a QTimer for the updates, probably there's also some idle callback in Qt you could attach to.

import sys

import numpy as np
import matplotlib as mpl
mpl.use('qt5agg')
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
from PyQt5 import QtWidgets, QtCore

size = (400, 400)

class GameCanvas(FigureCanvas):
    def __init__(self, parent=None, width=5, height=4, dpi=100):
        fig = Figure(figsize=(width, height), dpi=dpi)

        self.axes = fig.gca()
        self.init_figure()

        FigureCanvas.__init__(self, fig)
        self.setParent(parent)

        timer = QtCore.QTimer(self)
        timer.timeout.connect(self.update_figure)
        timer.start(10)

    def gen_frame(self):
        return np.random.randint(0,0xfffff,size)

    def init_figure(self):
        self.img = self.axes.imshow(self.gen_frame())

    def update_figure(self):
        self.img.set_data(self.gen_frame())
        self.draw()

class ApplicationWindow(QtWidgets.QMainWindow):
    def __init__(self):
        QtWidgets.QMainWindow.__init__(self)
        self.main_widget = QtWidgets.QWidget(self)

        dc = GameCanvas(self.main_widget, width=5, height=4, dpi=100)
        self.setCentralWidget(dc)

    def fileQuit(self):
        self.close()

    def closeEvent(self, ce):
        self.fileQuit()

app = QtWidgets.QApplication(sys.argv)
appw = ApplicationWindow()
appw.show()
sys.exit(app.exec_())

One thing you should be careful with is that imshow computes the image normalization on the first frame. In the subsequent frames it's calling set_data so the normalization stays the same. If you want to update it you can call imshow instead (probably slower). Or you could just fix it manually with vmin and vmax in the first imshow call and provide properly normalized frames.

filippo
  • 5,197
  • 2
  • 21
  • 44
  • Thanks for answer, but I have Qt5 on my computer which is not Qt4 compatible. I don't want to embed my project so I am looking after a solution with basic canvas actions. FuncAnimation it to slow for the moment (see my EDIT). – B. M. May 14 '18 at 21:04
  • @B.M. see my edit, ported it to `Qt5`. You're limiting your framerate to `20 fps` with the `interval=50` which sets a delay in milliseconds between frames. I get 100 fps with `interval=1` – filippo May 15 '18 at 02:15
  • By the way, this was meant as a proof of concept of what you can do working directly with the backend, if you just want to animate a canvas I doubt you can get better than matplotlib's own framework. `100 fps` seems pretty reasonable to me given, even with blitting, you're redrawing almost the full window at every frame. Matplotlib is designed to produce pubblication quality plots, not so much for performance... – filippo May 15 '18 at 02:32
6

Given you talked about using widespread modules, here's a proof of concept using OpenCV. It runs pretty fast here, up to 250-300 generated frames per second. It's nothing too fancy, just to show that maybe if you're not using any plotting feature matplotlib shouldn't really be your first choice.

import sys                                                                                 
import time                                                                                
import numpy as np                                                                         
import cv2                                                                                 

if sys.version_info >= (3, 3):                                                             
    timer = time.perf_counter                                                              
else:                                                                                      
    timer = time.time                                                                      

def f(x, y):                                                                               
    return np.sin(x) + np.cos(y)                                                           

# ESC, q or Q to quit                                                                      
quitkeys = 27, 81, 113                                                                     
# delay between frames                                                                     
delay = 1                                                                                  
# framerate debug init                                                                     
counter = 0                                                                                
overflow = 1                                                                               
start = timer()                                                                            

x = np.linspace(0, 2 * np.pi, 400)                                                         
y = np.linspace(0, 2 * np.pi, 400).reshape(-1, 1)                                          

while True:                                                                                
    x += np.pi / 15.                                                                       
    y += np.pi / 20.                                                                       

    cv2.imshow("animation", f(x, y))                                                       

    if cv2.waitKey(delay) & 0xFF in quitkeys:                                              
        cv2.destroyAllWindows()                                                            
        break                                                                              

    counter += 1                                                                           
    elapsed = timer() - start                                                              
    if elapsed > overflow:                                                                 
        print("FPS: {:.01f}".format(counter / elapsed))                                    
        counter = 0                                                                        
        start = timer()                                                                                                
filippo
  • 5,197
  • 2
  • 21
  • 44
  • Yes, it's seems to be a simple, fast solution. in my test yet 2 time slower than pygame. probably not sharing the memory. I am always dreaming of this grall ! Thanks again for contribution. – B. M. May 16 '18 at 08:11