Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
ThisIs0xBC opened this issue May 9, 2024 · 2 comments
Open

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

ThisIs0xBC opened this issue May 9, 2024 · 2 comments

Comments

@ThisIs0xBC
Copy link

ThisIs0xBC commented May 9, 2024

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
Copy link
Author

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
Copy link
Author

ThisIs0xBC commented May 10, 2024

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant