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

GIF's n_frames is (about 60x) slower than it should be #6105

Closed
FirefoxMetzger opened this issue Mar 2, 2022 · 3 comments · Fixed by #6077
Closed

GIF's n_frames is (about 60x) slower than it should be #6105

FirefoxMetzger opened this issue Mar 2, 2022 · 3 comments · Fixed by #6077
Labels

Comments

@FirefoxMetzger
Copy link
Contributor

While idling around on SO and answering a question I noticed that n_frames is surprisingly slow for GIF:

# setup code based on ImageIO 
# because it's short and we can avoid measuring disk IO
import imageio.v3 as iio
from PIL import Image
import io

img = iio.imread("imageio:newtonscradle.gif", index=None)
img_bytes = iio.imwrite("<bytes>", img, format="GIF")

The desired way to do things:

%%timeit

with Image.open(io.BytesIO(img_bytes)) as file:
    n_frames = file.n_frames

Timing for this approach:

13.2 ms ± 94.3 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

This is much more time than it should take to check a series of block headers. So I checked, and - indeed - n_frames parses pixel data, which (in pillow9) involves a conversion to RGB(A) which is what makes this slow. Here is an alternative approach that only reads headers and is - more or less - a drop-in replacement:

%%timeit
# based on SO: https://stackoverflow.com/a/7506880/6753182

with Image.open(io.BytesIO(img_bytes)) as file:
    def skip_color_table(flags):
        if flags & 0x80: 
            file.fp.seek(3 << ((flags & 7) + 1), 1)

    current_fp = file.fp.tell()
    total_frames = file.tell()  # start counting from the current frame

    # seek to beginning of next block
    buffer_start = file.tile[0][2]
    file.fp.seek(buffer_start)
    while True:
        size = file.fp.read(1)
        if size and size[0]:
            file.fp.seek(size[0], 1)
        else:
            break
    
    # count the remaining blocks
    while True:
        block = file.fp.read(1)
        if block == b';': 
            break
        if block == b'!': 
            file.fp.seek(1, 1)
        elif block == b',':
            total_frames += 1
            file.fp.seek(8, 1)
            skip_color_table(ord(file.fp.read(1)))
            file.fp.seek(1, 1)
        else: raise RuntimeError("unknown block type")
        
        # skip to next block instead of loading pixels
        while True:
            l = ord(file.fp.read(1))
            if not l: break
            file.fp.seek(l, 1)

    file.fp.seek(current_fp)

The timings:

211 µs ± 529 ns per loop (mean ± std. dev. of 7 runs, 1000 loops each) 

So if we only check block headers we can be about 60x faster than we are now. If desired, and there is somebody willing to review, I can submit a PR that adds this some time next week :)

(tests were done on Windows 11 with a AMD Ryzen 7 5800X 8-Core Processor and 2x Kingston KHX3200C16D4/32GX 32GB)

@radarhere radarhere added the GIF label Mar 2, 2022
@radarhere
Copy link
Member

Actually, there's already a PR in the pipeline for this - see what you think of #6077

@FirefoxMetzger
Copy link
Contributor Author

@radarhere I build that branch and re-ran the timings. It is indeed a major improvement and gets close to the alternative I proposed above:

  • img.n_frames: 673 µs ± 18.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
  • manual parsing: 201 µs ± 548 ns per loop (mean ± std. dev. of 7 runs, 1000 loops each)

So about 3x slower than it could be but much improved compared to main. If you want, I can leave a review in the next couple of days once I have a bit more time on my hands and see if I spot areas where we could improve further. At the same time, this gets close enough, so I am also happy if it gets merged as is.

@radarhere
Copy link
Member

Sure, feel free to look it over and review.

I would be reluctant to add a lot of code for a minor speed gain, but if there are simple changes to make it faster, then sure.

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

Successfully merging a pull request may close this issue.

2 participants