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
Animated, transparent GIFs not calculating diff correctly #3665
Comments
Can you make a pull request to fix that? |
--- a/src/PIL/GifImagePlugin.py
+++ b/src/PIL/GifImagePlugin.py
@@ -443,7 +443,10 @@ def _write_multiple_frames(im, fp, palette):
if im_frames:
# delta frame
previous = im_frames[-1]
- if _get_palette_bytes(im_frame) == \
+ if "disposal" in encoderinfo and encoderinfo["disposal"] == 2:
+ # If diposing whole frame, treat full new frame as delta
+ delta = im_frame
+ elif _get_palette_bytes(im_frame) == \
_get_palette_bytes(previous['im']):
delta = ImageChops.subtract_modulo(im_frame,
previous['im']) Tried this out in a fork, but the travisCI build failed on the test_save_dispose test, it seems like it's causing it to drop a frame somewhere, although I honestly cannot tell why. Open to ideas. |
I haven't dev'd on this project before, so please save me from myself, but I think the following snippet works. I didn't fork and pull request because the code needs a little love. It, for instance, creates a new background image on every iteration of the loop. That should probably be moved out of the loop, but then it would not have access to Importantly, this code does not just use the whole frame as the delta. It does continue to try to optimize the frames as much as possible by comparing against the background color. In any case, I think this code works, but please help me optimize the number of calls to if im_frames:
# delta frame
previous = im_frames[-1]
if ("disposal" in im.encoderinfo and im.encoderinfo["disposal"] == 2):
base_image = Image.new("P", im_frame.size, 0)
else:
base_image = previous["im"]
if _get_palette_bytes(im_frame) == \
_get_palette_bytes(base_image):
delta = ImageChops.subtract_modulo(im_frame,
base_image)
else:
delta = ImageChops.subtract_modulo(
im_frame.convert('RGB'), base_image.convert('RGB'))
bbox = delta.getbbox()
if not bbox and not ("disposal" in im.encoderinfo and im.encoderinfo["disposal"] == 2):
# This frame is identical to the previous frame
if duration:
previous['encoderinfo']['duration'] += \
encoderinfo['duration']
continue |
Is there sample code that demonstrates this problem? |
import requests
from PIL import Image
response = requests.get('https://66.media.tumblr.com/tumblr_lzkp40b0Sl1qhwcy0.gif')
im = Image.open(BytesIO(response.content))
im.save('1.png', 'PNG')
im.seek(1)
im.load()
im.save('2.png', 'PNG') The |
@Artheau With further testing, this introduces a new problem - each frame generates a diff compared to a I've tested and it seems like if every colour that appears in any frame appears in the first frame, it all works great. Trying to work out a solution where a palette that includes every colour from every frame can be generated and used for every frame. |
Yeah, it seems like that could come up for some cases. So, the code seems to already compare the palettes: I think an elegant solution would probably try to intelligently compare the palettes and find out what the palette position is in the new palette, defaulting to using the whole image as the diff if the color is not in the new palette...somehow this seems like overkill, because the existing code doesn't do that at all. If we riff off of the existing code, the corresponding |
Unfortunately, even if we use the whole frame as the diff, if it doesn't have the same palette as the first frame the colour mapping will be wrong. We will need to enforce the same palette onto every frame I think. |
What does the code do presently if images are presented with different palettes but disposal mode 2 is not activated? Does it work correctly? If so, I think we should just create an RGB background image to compare against instead of comparing against the paletted version. |
im_list = [
Image.new('RGB', (100, 100), '#fff'),
Image.new('RGB', (100, 100), '#999'),
Image.new('RGB', (100, 100), '#000'),
]
for img in im_list:
d = ImageDraw.Draw(img)
d.ellipse([(0,0),(100,100)], fill='#f00')
# d.ellipse([(0,0),(1,1)], fill='#fff')
# d.ellipse([(2,2),(3,3)], fill='#999')
# d.ellipse([(4,4),(5,5)], fill='#000')
im_list[0].save("test0.png")
im_list[1].save("test1.png")
im_list[2].save("test2.png")
# check per frame disposal
im_list[0].save(
out,
save_all=True,
append_images=im_list[1:],
disposal=0
) This is my test case - a red circle over a background changing white to gray to black. Right now, regardless of disposal mode, the output frames are: red circle on white (correct), red circle on white (incorrect) and white circle on red (very incorrect). My assumption is that the colours of the first frame are being mapped to the other frames (though that could be incorrect) |
Okay, I've made a discovery which might totally shift how I do this: Each frame is capable of storing it's own palette locally, so we don't have to worry about all frames sharing a palette In order to include that local colour table header, the frame info must include The formula for the colour table size |
@radarhere It looks like the color_table_size calculation was written by you, I'm wondering if you have any insight? It looks like a frame with the colours Where does the -1 come from? Would it be appropriate to just force a >3 len palette for disposal=2 frames? |
That calculation was actually added in a466b3e, not written by me. That said, https://www.w3.org/Graphics/GIF/spec-gif89a.txt states that
So |
That's on me for not reading the history close enough, I just saw the last modification and assumed, I apologize for that! Okay so that confirms that the color table basically has a minimum palette size of 3 colors (or two if we remove the statement that considers 0 invalid) and that padding out the palette if it's too small is probably the way to go! Thank you! |
The sample code provided by @burnoo is not actually solved by #3708 - that's okay though, because it is solved by #3434. I'm going to suggest that any discussion of that problem is moved to #3153, which looks like the same issue. |
Here are some thoughts. The disposal method determines how a GIF frame is removed. A value of 2 means that in the next frame after the frame when it is added, it should be replaced by the background colour. See one of Pillow's test images to demonstrate - When writing a GIF image, because GIFs are effectively made up of different images that are pasted on top of each other as they move along, each subsequent image can be made smaller by cropping it to only include the section of the image that has changed compared to the previous image. This cropped image is also referred to in the code as a delta, and referred to in this issue as a diff. So the delta controls how an image is added, and disposal controls how an image is removed, meaning that the two are not related when writing out a file. Without code to demonstrate something wrong, I don't see a problem here. Yes, there is currently a problem within Pillow where the palettes are read incorrectly on subsequent frames, but that's a reading bug, not a writing bug. Feel free to disagree, or to add code showing a problem. |
Disposal and Delta are inherently linked, because you can't layer a delta on top of a frame that's been discarded underneath - you need to calculate the delta in a totally different way (compared to the background of the gif) The problem with the current setup is the delta is calculated based on the frame that is disposed so anything that didn't change is erased. Here is a clear example (which I will turn into a unit test) out = 'temp.gif'
# 4 frames: red/blue, red/red, blue/blue, red/blue
circles = [
((255, 0, 0, 255),(0, 0, 255, 255)),
((255, 0, 0, 255),(255, 0, 0, 255)),
((0, 0, 255, 255),(0, 0, 255, 255)),
((255, 0, 0, 255),(0, 0, 255, 255))
]
im_list = []
for i in range(len(circles)):
img = Image.new('RGBA', (100, 100), (255,255,255,0))
# Red circle in center of each frame
d = ImageDraw.Draw(img)
d.ellipse([(0, 30), (40, 70)], fill=circles[i][0])
d.ellipse([(60, 30), (100, 70)], fill=circles[i][1])
im_list.append(img)
im_list[0].save(
out,
save_all=True,
append_images=im_list[1:],
disposal=2,
transparency=0
) In any frame where a circle is supposed to be the same colour as the frame before, it doesn't show up at all |
function
_write_multiple_frames
inGifImagePlugin.py
has an error on line 445 (line 445 in present repo, line 443 in released version 5.4.1).Specifically, when the disposal_method is set to '2', it should calculate the diff against the background color, but currently it is calculating the diff against the previous frame
previous = im_frames[-1]
,and this produces substantial artifacts in the rendering whenever there are still portions of the image in adjacent frames.
The text was updated successfully, but these errors were encountered: