1

If I have two GIFs, GIF 1 being 10 seconds long and GIF 2 being 5 seconds long, is there a way to connect them so the final GIF is a total of 15 seconds long?

Would I have to loop through each frame of both the GIFs with imageio.mimread() and output, once all the frames are read in memory?

Or is there another way by knowing the start and end times and shifting it?

Edit: The solution presented by FirefoxMetzger is extremely Pythonic, ideal if you do not wish to install other software / packages like gifsicle.

import imageio.v3 as iio
import numpy as np

frames = np.vstack([
    iio.imread("imageio1.gif"),
    iio.imread("imageio2.gif"),
])

# get duration each frame is displayed
iio.imwrite("imageio_combined.gif", frames)

This completes in 15.6 seconds for two GIFs, each containing 100 frames.

However, if runtime is important, I recommend gifsicle:

gifsicle(
    sources=["imageio1.gif", "imageio2.gif"], # or just omit it and will use the first source provided.
    destination="imageio3.gif",
    options=["--optimize=2", "--threads=2", "--no-conserve-memory"]
)

This completes in 4.8 seconds, which is three times as fast.

Andrew
  • 507
  • 5
  • 16

3 Answers3

1

You already found a way to do it, but as you were referring to imageio let me add an answer using it. At least in the old imageio v2 API it was a pretty straightforward business too even if more verbose.

import imageio

gif_1 = imageio.get_reader(path_gif_1)
gif_2 = imageio.get_reader(path_gif_2)

combined_gif = imageio.get_writer('combined.gif')

for frame in gif_1:
    combined_gif.append_data(frame)

for frame in gif_2:
    combined_gif.append_data(frame)

gif_1.close()
gif_2.close()    
combined_gif.close()
Mr K.
  • 1,064
  • 3
  • 19
  • 22
1

The exact solution depends on whether your GIFs have the same FPS or not. If they do, you can simply concatenate them and are good to go.

import imageio.v3 as iio
import numpy as np

frames = np.vstack([
    iio.imread("my_first.gif"),
    iio.imread("my_second.gif"),
])

# get duration each frame is displayed
duration = iio.immeta("my_first.gif")["duration"]

iio.imwrite("combined.gif", frames, duration=duration)

If the GIFs do not have the same framerate things get a little bit more complicated. Here, you will first have to normalize the FPS of both GIFs before concatenating them. I'd recommend using ImageIO's pyav plugin for this, which allows delegating this procedure to FFMPEG:

import imageio.v3 as iio
import numpy as np

# make sure pyav is installed (pip install av)

frames = np.vstack([
    iio.imread(
        "my_first.gif",
        plugin="pyav",
        # normalize the GIF to 50 FPS
        filter_sequence=[("fps", "50")]
    ),
    iio.imread(
        "my_second.gif",
        plugin="pyav",
        # normalize the GIF to 50 FPS
        filter_sequence=[("fps", "50")]
    ),
])

iio.imwrite("combined.gif", frames, duration=20)
FirefoxMetzger
  • 2,880
  • 1
  • 18
  • 32
  • Thanks for the answer! Just tested this and I like how this is very Pythonic. I must admit that it's 3x slower than calling gifsicle in my testing for merging two GIFs containing 100 frames each. I edited my answer above. – Andrew Nov 09 '22 at 03:07
  • However, I do wonder if it's necessary to read both GIFs in memory, or if there's a way to read only the metadata and concatenate based on the metadata. Probably not since GIFs are binaries? – Andrew Nov 09 '22 at 03:14
  • 1
    @Andrew If you profiled this, I wouldn't be surprised if you found that the slowest part is `iio.imwrite`. GIF is paletted, so we have to spend a decent chunk of time figuring out how to decompose each RGB image into a 256-color palette and an index image. pygifsicle (which just does GIF) can assume that the input is paletted, too, and can thus save time by re-using this representation. It's a typical example of how a specialized implementation beats a general-purpose one performance-wise by making assumptions that the general one can't make. – FirefoxMetzger Nov 09 '22 at 07:09
  • With dask, it's possible to take advantage of multithreading: ``` ds = xr.tutorial.open_dataset("air_temperature") ds_list = np.array_split(ds["air"], 20) fns = (dask.delayed(plot)(ds_part) for ds_part in ds_list) images = [dask.delayed(iio.imread)(fn) for fn in fns] iio.imwrite("test.gif", np.vstack(dask.compute(images, scheduler="threads"))) ``` – Andrew Jan 02 '23 at 00:11
0

I think this works:

from pygifsicle import gifsicle

gifsicle(
    sources=["imageio_00.gif", "imageio_01.gif"], # or a single_file.gif
    destination="destination.gif", # or just omit it and will use the first source provided.
    optimize=True, # Whetever to add the optimize flag of not
    colors=256, # Number of colors t use
    options=["--verbose"] # Options to use.
)
Andrew
  • 507
  • 5
  • 16