hzeller / rpi-rgb-led-matrix

Controlling up to three chains of 64x64, 32x32, 16x32 or similar RGB LED displays using Raspberry Pi GPIO
GNU General Public License v2.0
3.7k stars 1.17k forks source link

A better way to load a large amount of GIFs in Python (100+) #1652

Open ThisIs0xBC opened 6 months ago

ThisIs0xBC commented 6 months ago

Hi, I've created a GIF slideshow application in Python that takes GIFs uploaded to a website (web server is made with Flask and runs completely separately to the led matrix code), and will play them 5 times, then cycle on to the next GIF. The user can upload their own GIFs to the server and select which ones they want in the display queue.

This is the code I used to render the GIFs:

for i in range(5): #Plays GIF 5 times
    for frameNumber in range(len(gifCanvases[0])): #gifCanvases[0] is a list of FrameCanvases returned by "canvas = self.matrix.CreateFrameCanvas()" which make up each frame of the GIF
        self.matrix.SwapOnVSync(gifCanvases[0][frameNumber], framerate_fraction=framerateFraction)

Anyways, the way I render them is basically having a dictionary of lists, each dictionary key is a GIF filename, and each key has a value of a list containing: A list of FrameCanvas objects containing each frame of the GIF A number representing the GIF "duration"

The first time a GIF is rendered, I preprocess each frame into a 64x64 BMP file, with this code:

gif = Image.open(f"{self.webappRootDir}/basic-flask-app/static/gifs/{gifName}")

for frame_index in range(0, gif.n_frames):
    gif.seek(frame_index)
    # must copy the frame out of the gif, since thumbnail() modifies the image in-place
    frame = gif.copy()
    frame.thumbnail((self.matrix.width, self.matrix.height), Image.Resampling.LANCZOS)
    canvas = self.matrix.CreateFrameCanvas()

    frameImageData = frame.convert("RGB")
    frameImageData.save(f"{self.matrixRootDir}/saved-gifs/{gifName}/frame_{frame_index}.bmp")

    canvas.SetImage(frameImageData)
    canvases.append(canvas)

    #Store new GIF information in canvases dictionary
    self.gifFileCanvases[gifName] = [canvases, gif.info['duration']]

    #Close the gif file to save memory now that we have copied out all of the frames
    gif.close()
    print("Processed GIF from original file and saved data to file")

Once the GIF has been processed into its individual 64x64 frames, the next time I run the code or need to load that GIF it just loads directly from the BMP files, so I can skip the scaling down to 64x64 (which saves a BUNCH of loading time, its basically instant loading this way)

However, I still need to call:

canvas = self.matrix.CreateFrameCanvas()
canvas.SetImage(Image.open(f"{self.matrixRootDir}/saved-gifs/{gifName}/frame_{frameFileNumber}.bmp"))
canvases.append(canvas)

for EVERY frame of EVERY GIF! Here is the full code of just loading a GIF from the 64x64 BMP files:

gif = Image.open(f"{self.webappRootDir}/basic-flask-app/static/gifs/{gifName}")

directory = os.fsencode(f"{self.matrixRootDir}/saved-gifs/{gifName}")
numberOfFrames = len([f for f in os.listdir(directory)if os.path.isfile(os.path.join(directory, f))])

for frameFileNumber in range(numberOfFrames):
    canvas = self.matrix.CreateFrameCanvas()
    canvas.SetImage(Image.open(f"{self.matrixRootDir}/saved-gifs/{gifName}/frame_{frameFileNumber}.bmp"))
    canvases.append(canvas)

    self.gifFileCanvases[gifName] = [canvases, gif.info['duration']]

gif.close()

print(f"Loaded GIF Data from File: {gifName}")

The above code is essentially a modified version of https://github.com/hzeller/rpi-rgb-led-matrix/blob/master/bindings/python/samples/gif-viewer.py

This creates an issue, as the canvas objects generated when calling CreateFrameCanvas() don't appear to be deleted until the program is killed. After 500 and 1000 calls to it, it shows this warning:

CreateFrameCanvas() called 500 times; Usually you only want to call it once (or at most a few times) for double-buffering. These frames will not be freed until the end of the program.

Typical reasons:
 * Accidentally called CreateFrameCanvas() inside your inner loop (move outside the loop. Create offscreen-canvas once, then re-use. See SwapOnVSync() examples).
 * Used to pre-compute many frames (use led_matrix::StreamWriter instead for such use-case. See e.g. led-image-viewer)

And it then shows this again for every consecutive 500th call to the function.

The led_matrix::StreamWriter functionality is not exposed/doesn't have a Python binding, so is there a more optimal way to achieve what I'm trying to do without thousands of calls to CreateFrameCanvas?

I tried serialising the canvas objects via pickle, but it tells me the object is not able to be serialised (I assume because its a C type object with non standard fields?), any suggestions?

Thanks in advance.

ThisIs0xBC commented 6 months ago

Just had an idea, would it be possible to just create a sinuglar frame canvas instead, and just utilize PILs "SetImage" functionality like this:

gif = Image.open(f"{self.webappRootDir}/basic-flask-app/static/gifs/{gifName}")

directory = os.fsencode(f"{self.matrixRootDir}/saved-gifs/{gifName}")
numberOfFrames = len([f for f in os.listdir(directory)if os.path.isfile(os.path.join(directory, f))])

canvas = self.matrix.CreateFrameCanvas()

for frameFileNumber in range(numberOfFrames):

    canvas.SetImage(Image.open(f"{self.matrixRootDir}/saved-gifs/{gifName}/frame_{frameFileNumber}.bmp"))
    canvases.append(canvas)

    self.gifFileCanvases[gifName] = [canvases, gif.info['duration']]

gif.close()

print(f"Loaded GIF Data from File: {gifName}")

Then when rendering the GIFS I can just iterate each frame and run:

for frame in canvases:
    matrix.SetImage(frame)

Where "frame" is a "FrameCanvas" object we created when processing the GIF before?

ThisIs0xBC commented 6 months ago

Ok, update. I tried the above, and it partially works.

I now create just a sinuglar canvas object with matrix.CreateFrameCanvas(), and then save the bitmap files as usual (like shown above), and then when rendering each GIF, I first load all the BMP with Image.open(), passing each BMP file (each frame of the GIF) as an argument, then saving all the results of Image.open() into a list. So I end up with a list of Image objects (each object represents a frame of the GIF)

Then, when rendering, I iterate that list of Image objects and run:

self.canvas.SetImage(listOfImages[frameNumber])
self.matrix.SwapOnVSync(self.canvas, framerate_fraction=framerateFraction)

This works, but produces noticeable flickering when the GIF restarts (as I play them 5 times each).

Any ideas? I just have the rendering code wrapped in a for i in range (5) loop for playing them 5 times.