overview#

This is a short overview of enjoyn, geared towards new users.

why enjoyn#

The primary goal of enjoyn is to facilitate joining images to form animations.

Indubitably, there are plenty of other Python libraries that already accomplish this.

However, many of these Python libraries either:

  • only utilize a single thread for rendering.

  • requires the images for the animation ready on the file system.

In contrast, enjoyn features:

  • utilizing multiple threads and/or processes for rendering.

  • generating input images for the animation on the fly.

short demo#

To demonstrate, let’s first generate a large number of images to animate.

[1]:
from enjoyn.example import RandomWalkExample

example = RandomWalkExample(length=1000)
with example.time_run():
    outputs = example.output_images()

print(f"Length: {len(outputs)}, Example: {outputs[0]}")
Runtime: 45.973551041 seconds
Length: 1000, Example: /var/folders/l8/rxc2dv157_9dmx0sjlq7nwc00000gn/T/enjoyn_bardrjv5/f07433b50ba14ee1abc1688e52a25d71.png

using imageio#

To animate those images, let’s use imageio to:

  1. serialize the output into numpy arrays using iio.imread.

  2. join the arrays to form a GIF using iio.imwrite.

[2]:
import imageio.v3 as iio

with example.time_run():
    imageio_uri = "assets/imageio_random_walk.gif"
    iio.imwrite(imageio_uri, [iio.imread(output) for output in outputs], loop=0)
Runtime: 9.815273082999994 seconds

using enjoyn#

Now let’s use enjoyn to do the same.

[3]:
from enjoyn import GifAnimator

with example.time_run():
    enjoyn_uri = "assets/enjoyn_random_walk.gif"
    GifAnimator(items=outputs, output_path=enjoyn_uri).compute()
[########################################] | 100% Completed |  4.2s
Runtime: 4.240391915999993 seconds

both outputs#

Here are the rendered GIFs; imageio is shown above and enjoyn is shown below.

imageio enjoyn

Here are the file sizes.

[4]:
example.size_of("assets/imageio_random_walk.gif")
example.size_of("assets/enjoyn_random_walk.gif")
File size of imageio_random_walk.gif: 3.73 MBs
File size of enjoyn_random_walk.gif: 1.27 MBs

Notice, although the renders look identical, both runtime and file size are halved with enjoyn!

inner workings#

Internally, enjoyn uses imageio as described above.

However, on top of that, enjoyn leverages dask to scale and gifsicle to optimize:

  • dask partitions the items across workers, returning partitioned animations.

  • gifsicle concatenates the partitioned animations and applies compression.

For a deeper dive on the implementation, see design notes.

Here’s what the dask dashboard would have looked like if a distributed.Client was provided!

dask

using preprocessor#

Before cleaning up, let’s see how enjoyn can generate input images on the fly.

To accomplish this:

  1. serialize a Preprocessor with the desired plotting function, func, and keywords, kwds.

  2. update items so that when it’s mapped, each item becomes the first positional argument of func.

Note enjoyn can accept both files and file-objects, as exemplified by setting to_bytes_io = True here.

[5]:
from enjoyn import GifAnimator, Preprocessor

with example.time_run():
    example.to_bytes_io = True
    preprocessor = Preprocessor(func=example.plot_image)

    data = example.load_data()
    items = [data[:i] for i in range(1, len(data))]

    output_path = "assets/enjoyn_random_walk_on_the_fly.gif"

    GifAnimator(
        preprocessor=preprocessor, items=items, output_path=output_path
    ).compute()
[########################################] | 100% Completed | 24.8s
Runtime: 25.475539207999987 seconds

The runtime is now way over 10 seconds.

However, that’s because it’s now performing two jobs:

  • generating the images.

  • rendering the animation.

Overall, enjoyn is still able to halve the total runtime because these jobs are executed in parallel.

[6]:
example.cleanup_images()

next steps#

If this guide intrigued you, why not install enjoyn or star the repo?

It’s inspiring to see others enjoy enjoyn!