According to ffmpy
documentation, it seems like the most relevant option is using using-pipe-protocol.
Instead of using PIL for reading the images, we may read the PNG images as binary data into BytesIO
(reading all images to in-memory file-like object):
# List of input image files (assume all images are in the same resolution, and the same "pixel format").
images = ['frame 1.png', 'frame 2.png', 'frame 3.png', 'frame 4.png', 'frame 5.png', 'frame 6.png', 'frame 7.png', 'frame 8.png']
# Read PNG images from files, and write to BytesIO object in memory (read images as binary data without decoding).
images_in_memory = io.BytesIO()
for png_file_name in images:
with open(png_file_name, 'rb') as f:
images_in_memory.write(f.read())
Run ffmpy.FFmpeg
using pipe protocol.
Pass images_in_memory.getbuffer()
as input_data
argument to ff.run
:
ff = ffmpy.FFmpeg(
inputs={'pipe:0': '-y -f image2pipe -r 1'},
outputs={'output.gif': None},
executable='\\ffmpeg\\bin\\ffmpeg.exe')
# Write the entire buffer of encoded PNG images to the "pipe".
ff.run(input_data=images_in_memory.getbuffer(), stdout=subprocess.PIPE)
The above solution seems a bit awkward, but it's the best solution I could find using ffmpy
.
There are other FFmpeg to Python binding like ffmpeg-python, that supports writing the images one by one in a loop.
Using ffmpy, we have to read all the images into memory from advance.
The above solution keeps the PNG images in their encoded (binary form).
Instead of decoding the images with PIL (for example), FFmpeg is going to decode the PNG images.
Letting FFmpeg decode the images is more efficient, and saves memory.
The limitation is that all the images must have the same resolution.
The images also must have the same "pixel format" (all RGB or all RGBA but not a mix).
In case images have different resolution or pixels format, we have to decode the images (and maybe resize the images) using Python, and write images as "raw video".
For testing we may create PNG images using FFmpeg CLI:
ffmpeg -f lavfi -i testsrc=size=192x108:rate=1:duration=8 "frame %d.png"
.
Complete code sample:
import ffmpy
import io
import subprocess
#Building sample images using FFmpeg CLI for testing: ffmpeg -f lavfi -i testsrc=size=192x108:rate=1:duration=8 "frame %d.png"
# List of input image files (assume all images are in the same resolution, and the same "pixel format").
images = ['frame 1.png', 'frame 2.png', 'frame 3.png', 'frame 4.png', 'frame 5.png', 'frame 6.png', 'frame 7.png', 'frame 8.png']
# Read PNG images from files, and write to BytesIO object in memory (read images as binary data without decoding).
images_in_memory = io.BytesIO()
for png_file_name in images:
with open(png_file_name, 'rb') as f:
images_in_memory.write(f.read())
# Use pipe protocol: https://ffmpy.readthedocs.io/en/latest/examples.html#using-pipe-protocol
ff = ffmpy.FFmpeg(
inputs={'pipe:0': '-y -f image2pipe -r 1'},
outputs={'output.gif': None},
executable='\\ffmpeg\\bin\\ffmpeg.exe') # Note: I have ffmpeg.exe is in C:\ffmpeg\bin folder
ff.run(input_data=images_in_memory.getbuffer(), stdout=subprocess.PIPE)
Sample output output.gif
:

Update:
Same solution using images from Pillow:
The above solution also works if we save the images from Pillow to BytesIO in PNG format.
Example:
import ffmpy
import io
import subprocess
from PIL import Image as Img
#Building sample images using FFmpeg CLI for testing: ffmpeg -f lavfi -i testsrc=size=192x108:rate=1:duration=8 "frame %d.png"
# List of input image files (assume all images are in the same resolution, and the same "pixel format").
images = ['frame 1.png', 'frame 2.png', 'frame 3.png', 'frame 4.png', 'frame 5.png', 'frame 6.png', 'frame 7.png', 'frame 8.png']
# Read PNG images from files, and write to BytesIO object in memory (read images as binary data without decoding).
images_in_memory = io.BytesIO()
for png_file_name in images:
img = Img.open(png_file_name)
# Modify the images using PIL...
img.save(images_in_memory, format="png")
# Use pipe protocol: https://ffmpy.readthedocs.io/en/latest/examples.html#using-pipe-protocol
ff = ffmpy.FFmpeg(
inputs={'pipe:0': '-y -f image2pipe -r 1'},
outputs={'output.gif': None},
executable='\\ffmpeg\\bin\\ffmpeg.exe')
ff.run(input_data=images_in_memory.getbuffer(), stdout=subprocess.PIPE)
Encoding the images as PNG in memory is not most efficient in terms of execution time, but it saves memory space.