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

Inconsistent GIF result after resizing #6085

Closed
Kusefiru opened this issue Feb 22, 2022 · 7 comments
Closed

Inconsistent GIF result after resizing #6085

Kusefiru opened this issue Feb 22, 2022 · 7 comments
Labels

Comments

@Kusefiru
Copy link

I am trying to resize a .gif file. For that, I open my file with Pillow, then for each frame, resize that frame, and save the resulting frames as a new .gif file.

Here is the sample code:

from PIL import Image

image = Image.open("test.gif")

frames = []
for i in range(image.n_frames):
    image.seek(i)
    frames.append(image.resize((512, 512), resample=Image.BICUBIC))

frames[0].save("result.gif", save_all=True, append_images=frames[1:], duration=100, loop=0)

Here is the used test.gif file:
test.gif

As the resize is done using the Image.BICUBIC resampling method, I expect the resulting file to have a blurred appearance.

On Pillow 9.0.1, the first frame appears pixel crisp:
result

On my previous install, Pillow 8.4.0, all the frames are pixel crisp:
result_8 4 0

What could explain such behaviour (is there a missing option in my save call)?

@radarhere radarhere added the GIF label Feb 22, 2022
@FirefoxMetzger
Copy link
Contributor

This is actually unrelated to doing a resize or the save call. Instead, it happens during the loading of the GIF. The lack of crisp-ness, aka, the graininess of each frame is a typical dithering artifact, in this case Floyd–Steinberg dithering. It currently happens under the hood once you seek beyond the first frame of a GIF:

if "transparency" in self.info:
self.mode = "RGBA"
self.im.putpalettealpha(self.info["transparency"], 0)
self.im = self.im.convert("RGBA", Image.Dither.FLOYDSTEINBERG)
del self.info["transparency"]
else:
self.mode = "RGB"
self.im = self.im.convert("RGB", Image.Dither.FLOYDSTEINBERG)

I don't think there is much you can do in pillow 9.0.1 considering that the value was hard-coded. It might be possible to expose the dithering method as a kwarg instead of hardcoding it (which could then be set to None), but I don't know the internal plugin API well enough to know where such a kwarg would live. seek seems natural, but might be the wrong place to do so. We'd have to wait for @radarhere to tell what's feasible.

@radarhere
Copy link
Member

Hi. Unfortunately, I need to disagree with @FirefoxMetzger. Dithering during loading is not causing a problem here.

from PIL import Image

image = Image.open("test.gif")
image.save("firstframe.png")

image.seek(1)
image.save("secondframe.png")
firstframe.png secondframe.png
firstframe secondframe

It's hard to tell visually on this page, but if I zoom in on macOS Preview, both display similarly.

from PIL import Image

image = Image.open("test.gif")

print(image.mode)  # P
image.resize((512, 512), resample=Image.BICUBIC).save("firstframe_resized.png")

image.seek(1)
print(image.mode)  # RGB
image.resize((512, 512), resample=Image.BICUBIC).save("secondframe_resized.png")
firstframe_resized.png secondframe_resized.png
firstframe_resized secondframe_resized

The difference is only apparent once you try and resize the images. This would be because the first frame is P, and the second frame is RGB.

As the resize is done using the Image.BICUBIC resampling method, I expect the resulting file to have a blurred appearance.

So your request is that all frames be blurred? If so, this can be achieved by just converting the first frame to RGB.

from PIL import Image

image = Image.open("test.gif")

frames = []
for i in range(image.n_frames):
    image.seek(i)
    frame = image
    if i == 0:
    	frame = frame.convert("RGB")
    frames.append(frame.resize((512, 512), resample=Image.BICUBIC))

frames[0].save("result.gif", save_all=True, append_images=frames[1:], duration=100, loop=0)

result

@FirefoxMetzger
Copy link
Contributor

FirefoxMetzger commented Feb 24, 2022

So your request is that all frames be blurred? If so, this can be achieved by just converting the first frame to RGB.

Ah right; that's another way to read the issue. I understood "all the frames are pixel crisp" as a reference to the dither, not to the fact that the first frame is blurred differently. Sorry if I jumped to conclusions.


Interestingly though, automatic dithering did get introduced in pillow9. If I run the original code in pillow8.4 vs pillow9 I get the following result:

from PIL import Image

image = Image.open("test.gif")

frames = []
for i in range(image.n_frames):
    image.seek(i)
    frame = image
    frames.append(frame.resize((256, 256), resample=Image.BICUBIC))

frames[0].save("result.gif", save_all=True, append_images=frames[1:], duration=100, loop=0)
Pillow8.4 Pillow9
result result

However if I add an explicit conversion to RGB (as in your suggestion) then both results look the same and they match the default behavior in pillow9:

from PIL import Image

image = Image.open("test.gif")

frames = []
for i in range(image.n_frames):
    image.seek(i)
    frame = image
    frame = frame.convert("RGB")
    frames.append(frame.resize((256, 256), resample=Image.BICUBIC))

frames[0].save("result.gif", save_all=True, append_images=frames[1:], duration=100, loop=0)
Pillow8.4 Pillow9
result result

@Kusefiru
Copy link
Author

Okay, I understand that the issue comes from the conversion of the first frame being different. I can modify my program as suggested to fix the issue.

But why is it that the first frame is not converted as RGB automatically when the others are ? Is it because of the seek method ?

@radarhere
Copy link
Member

Before Pillow 9, P mode was used for all frames in a non-grayscale GIF. While this worked fine for your image, some images were broken because of this.

https://pillow.readthedocs.io/en/stable/releasenotes/9.0.0.html#convert-subsequent-gif-frames-to-rgb-or-rgba

Since each frame of a GIF can have up to 256 colors, after the first frame it is possible for there to be too many colors to fit in a P mode image. To allow for this, seeking to any subsequent GIF frame will now convert the image to RGB or RGBA, depending on whether or not the first frame had transparency.

Why was the first frame not changed to RGB as well? I presume there is a contingent of Pillow users who just read the first frame of images, and don't bother seeking past that. This change in Pillow 9 (#5857) had no effect on those users, and so did not disrupt their experience at all.

I think the first image is a P mode image - it has only up to 256 colors. It is represented by a palette in the original GIF.
While your image may happen to also use the same colors for subsequent frames, it is entirely possible for GIFs to have more than 256 colors by the second frame, meaning that RGB is used.

@radarhere
Copy link
Member

@Kusefiru did that answer your question? Is there anything else we can do for you?

@Kusefiru
Copy link
Author

Yes I think it's fine for me, thanks.

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

No branches or pull requests

3 participants