python-pillow / Pillow

Python Imaging Library (Fork)
https://python-pillow.org
Other
11.88k stars 2.2k forks source link

Color and quality loss with similar colors or from RGBA #6832

Open BootsManOut opened 1 year ago

BootsManOut commented 1 year ago

Hello,

I have found 2 cases where PIL reduces color quality, when I import a gif animation as PIL images, and then save it again as a gif animation.

Case 1: Importing a gif animation with similar background colors removes differentiated background:

This is the gif animation with a checkered background: GIF before import

After importing it as PIL format images, and then exporting it again as a gif, the colors get reduced, and you cannot see the checkered pattern of the background anymore: GIF after export

The code used is this:

from PIL import Image as Img

#Convert Gif Frames into PIL images:
tempimage = Img.open("GIF before import.gif")
ExportFrames = []
durationFrame=None
for i in range(0, tempimage.n_frames):
    tempimage.seek(i)
    ExportFrames.append(tempimage.copy())

    if durationFrame == None:
        durationFrame = tempimage.info['duration']

ExportFrames[0].save("GIF after export.gif", disposal=2, save_all=True,
                                     append_images=ExportFrames[1:], loop=0,
                                     duration=durationFrame, optimize=False, lossless=True)

Case 2: Adding Alpha Channel to PIL images, before saving as gif:

I added this additional step of pasting each frame unto a transparent alpha PIL image, because for some GIFs, when I imported them and then saved them, the first main frame of the GIF would not have a transparent background. This would be fixed by adding this step. However with it this quality loss occurs: The original GIF image that I import as PIL formatted images: GIF before import After saving, there is an extreme quality loss: GIF after export

This quality loss does only occur when adding the step. However, even with the step added, there is no quality loss when exporting the frames as single png images instead as a gif animation:

GIF before import 44

This suggests that error happens during GIF export.

The code used for this is the following:

from PIL import Image as Img

#Correct the first frame of gif animations:
def CreateFrameWithAlphaValue(CurrentImage,size):
    width,height=size
    edited_frame = Img.new('RGBA', (width, height))
    edited_frame.putalpha(0)
    edited_frame.paste(CurrentImage, (0, 0))

    return edited_frame

#Convert Gif Frames into PIL images:
tempimage = Img.open("GIF before import.gif")
ExportFrames = []
durationFrame=None
for i in range(0, tempimage.n_frames):
    tempimage.seek(i)
    ExportFrames.append(tempimage.copy())

    if durationFrame == None:
        durationFrame = tempimage.info['duration']

#Add Alpha Channel to all gifs in order to avoid erroneous first frames:
for x in range(0,len(ExportFrames)):
    ExportFrames[x]=CreateFrameWithAlphaValue(ExportFrames[x],ExportFrames[0].size).copy()

ExportFrames[0].save("GIF after export.gif", disposal=2, save_all=True,
                                     append_images=ExportFrames[1:], loop=0,
                                     duration=durationFrame, optimize=False, lossless=True)

I also attach the sample images and python scripts above here so anyone can recreate the issue: Sample files.zip

Running convert('P') on every PIL frame image did not solve the issue.

How can I stop and control the quality loss in both of these cases? Why is this happening? Is this a potential bug?

radarhere commented 1 year ago

For your first example, the problem would be that after your first frame, Pillow changes the mode of the image to RGB. This is because there may be more colours than can be contained in a single palette. Then, the image is converted back to a palette image when saving.

However, in the case of your image, there are not more colours than can be contained. So inserting

from PIL import GifImagePlugin
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY

at the start of your code resolves the matter.

Let us know if that isn't sufficient for your situation.

radarhere commented 1 year ago

For your second example, When converting from RGBA to P, quantize is used. It isn't when converting from RGB to P.

I added this additional step of pasting each frame unto a transparent alpha PIL image, because for some GIFs, when I imported them and then saved them, the first main frame of the GIF would not have a transparent background.

Would you consider it more helpful to talk about this instead? If you wanted to post an image and example code?

BootsManOut commented 1 year ago
from PIL import GifImagePlugin
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY

at the start of your code resolves the matter.

Let us know if that isn't sufficient for your situation.

Hello radarhere,

Thank you for the tips so far.

Adding this loading strategy in the beginning of the sample code snippet I created worked to solve the first sample issue. However I was not able to make it work in my main project (where Main Code, import functions, and export functions are in separate scripts), no matter if I added the line to all the scripts or only the import script.

Would you consider if more helpful to talk about this instead? If you wanted to post an image and example code?

Unfortunately I'm not able to recreate the issue, it didn't happen with all gifs, and it's been a few months back, when I worked on this project and solved it this way.

Overall, I will need to convert all frames into RGBA, in no matter what case, even if I don't paste unto an empty image. Now, if I convert to RGBA, and set GifImagePlugin.LOADING_STRATEGY as you mentioned in the sample code, then sample 2 does NOT lose quality and get quantized, however sample 1 DOES. (I tried to convert from RGBA to RGB before saving the GIF file but it didn't change anything).

Here is the code as example:

from PIL import Image as Img
from PIL import GifImagePlugin as GifPl
GifPl.LOADING_STRATEGY = GifPl.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY
#Convert Gif Frames into PIL images:
tempimage = Img.open("GIF before import.gif")
ExportFrames = []
durationFrame=None
for i in range(0, tempimage.n_frames):
    tempimage.seek(i)
    ExportFrames.append(tempimage.copy().convert('RGBA'))#CONVERTING TO RGBA

    if durationFrame == None:
        durationFrame = tempimage.info['duration']

for frame in ExportFrames:
    frame.convert('RGB')

ExportFrames[0].save("GIF after export.gif", disposal=2, save_all=True,
                                     append_images=ExportFrames[1:], loop=0,
                                     duration=durationFrame, optimize=False, lossless=True)

Resulting in all the frames having reduced quality: GIF after export

But sample 2 now doesn't have quality loss after saving with this code: GIF after export

I do modifications on the frames before saving them back as GIF, so generally I need to convert them to RGBA (for gifs with transparent backgrounds). So I still need help with this, I wasn't able to figure it out so far and understand why the loss occurs in different situations.

radarhere commented 1 year ago

Ah, ok. The point of

from PIL import GifImagePlugin
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY

was precisely to stop the image frames from becoming RGB. So the fact that you're manually converting them to RGBA works against my suggestion.

Try this code instead.

from PIL import Image as Img

def convertToP(im):
    if im.getcolors() is not None:
        # There are 256 colors or less in this image
        p = Img.new("P", im.size)
        transparent_pixels = []
        for x in range(im.width):
            for y in range(im.height):
                pixel = im.getpixel((x, y))
                if pixel[3] == 0:
                    transparent_pixels.append((x, y))
                else:
                    color = p.palette.getcolor(pixel[:3])
                    p.putpixel((x, y), color)
        if transparent_pixels and len(p.palette.colors) < 256:
            color = (0, 0, 0)
            while color in p.palette.colors:
                if color[0] < 255:
                    color = (color[0] + 1, color[1], color[2])
                else:
                    color = (color[0], color[1] + 1, color[2])
            transparency = p.palette.getcolor(color)
            p.info["transparency"] = transparency
            for x, y in transparent_pixels:
                p.putpixel((x, y), transparency)
        return p
    return im.convert("P")

#Convert Gif Frames into PIL images:
tempimage = Img.open("GIF before import.gif")
ExportFrames = []
durationFrame=None
for i in range(0, tempimage.n_frames):
    tempimage.seek(i)
    rgba_image = tempimage.convert("RGBA")
    # Perform operations with RGBA images
    # ...
    ExportFrames.append(convertToP(rgba_image))

    if durationFrame == None:
        durationFrame = tempimage.info['duration']

ExportFrames[0].save("GIF after export.gif", disposal=2, save_all=True,
                                     append_images=ExportFrames[1:], loop=0,
                                     duration=durationFrame, optimize=False, lossless=True)
BootsManOut commented 1 year ago

Thank you for the custom function! It works in the sample code snippet, but when I divide it into 2 separate processes, as I need it in my main project (first store the RGBA images, then convert them later on adding your custom function and saving with my SaveAnimation function), it does not work anymore. (It must be the same reason why the LoadingStrategy did not work in my main project but worked in the sample code snippet, which I mentioned in a comment above.)

I shortened the "SaveAnimation" function down to only the bug related parts. So this is the code to reproduce it, where it does not work anymore:

#Showcase Bug:
from PIL import Image as Img

#Convert To "P" Gif 255 color Pallette mode:
def convertToP(im):
    if im.getcolors() is not None:
        # There are 256 colors or less in this image
        p = Img.new("P", im.size)
        transparent_pixels = []
        for x in range(im.width):
            for y in range(im.height):
                pixel = im.getpixel((x, y))
                if pixel[3] == 0:
                    transparent_pixels.append((x, y))
                else:
                    color = p.palette.getcolor(pixel[:3])
                    p.putpixel((x, y), color)
        if transparent_pixels and len(p.palette.colors) < 256:
            color = (0, 0, 0)
            while color in p.palette.colors:
                print("happening")
                if color[0] < 255:
                    color = (color[0] + 1, color[1], color[2])
                else:
                    color = (color[0], color[1] + 1, color[2])
            transparency = p.palette.getcolor(color)
            p.info["transparency"] = transparency
            for x, y in transparent_pixels:
                p.putpixel((x, y), transparency)
        return p
    return im.convert("P")

#Export and Save Gif Function:
def SaveAnimationFunction(ExportFrames,newfilepathname, formatname, extension, disposalID,FPS,savepath):
    durationFrame = 1000 / FPS
    if extension == ".gif":
        for frame in ExportFrames:
            print(ExportFrames.index(frame))
            frame=convertToP(frame)
    ExportFrames[0].save(newfilepathname + formatname + extension, disposal=disposalID, save_all=True,
                             append_images=ExportFrames[1:], loop=0,
                             duration=durationFrame, optimize=False, lossless=True)

#Convert Gif Frames into modifiable RGBA PIL images:
tempimage = Img.open("GIF before import.gif")
ExportFrames = []
durationFrame=None
for i in range(0, tempimage.n_frames):
    tempimage.seek(i)
    rgba_image = tempimage.convert("RGBA")
    print("created rgba frame",i)
    ExportFrames.append(rgba_image)

#Modifications can happen here.

#Set Up Export Settings:
newfilepathname = "GIF before import "
formatname ="animation"
extension = ".gif"
disposalID = 2
FPS = 30
savepath=('GIF before import.gif',)

#Export/ Save:
SaveAnimationFunction(ExportFrames,newfilepathname, formatname, extension, disposalID,FPS,savepath)

Even though I run through every frame stored in "ExportFrames" and convert it to palette mode using your custom function, it still creates the previous result with reduced colors. Why is it not working anymore in the 2 step process shown above?

In case it helps, here's the script as py file together with the sample gif file (The new exported file will be saved as "Gif before import animation.gif", using this script).

Script and Gif File.zip

radarhere commented 1 year ago

You're running convertToP(), but not using the result.

Replace

for frame in ExportFrames:
    print(ExportFrames.index(frame))
    frame=convertToP(frame)

with

for i, frame in enumerate(ExportFrames):
    print(ExportFrames.index(frame))
    ExportFrames[i]=convertToP(frame)
BootsManOut commented 1 year ago

Amazing, thank you very much for your help! Indeed, I need to call the list directly in order to modify it's containing items, my bad!

With that little correction in mind, I tried to simply convert each frame to 'RGB' before saving as GIF, and that solved the issue for both cases! (both if I first convert the images to 'RGBA' or paste them unto a transparent PIL image as I mentioned in the beginning) Since the pixel-by-pixel conversion obviously takes significantly longer, converting to 'RGB' is the preferred solution.

If anyone might have a related issue, this simple sample script here demonstrates the solution:

#Showcase Solution:

tempimage = Img.open("GIF before import.gif")
ExportFrames = []

for i in range(0, tempimage.n_frames):
    tempimage.seek(i)
    rgba_image = tempimage.convert("RGBA")

    # Perform operations with RGBA images
    # ...

    #------SOLUTION------:
    ExportFrames.append(rgba_image.convert("RGB"))

    if durationFrame == None:
        durationFrame = tempimage.info['duration']
    ExportFrames[0].save(newfilepathname + formatname + extension, disposal=disposalID, save_all=True,
                             append_images=ExportFrames[1:], loop=0,
                             duration=durationFrame, optimize=False, lossless=True)

Thank you very much for your time, the cool conversion function, and your help. Much appreciated.

radarhere commented 1 year ago

A final note - when I wrote im.convert("P") above, im.convert("P", palette=Image.Palette.ADAPTIVE) would have been better.

BootsManOut commented 1 year ago

Hi again,

Why is Image.Palette.ADAPTIVE better than Image.Palette.WEB? What would be the difference between the two?

I realize that since I will also work with transparent GIFs, I will have to eventually use your custom function as well! I will create an option in my project to switch to the pixel-by-pixel export if necessary.

But since the pixel-by-pixel export takes around 110 times longer, could this be implemented into the module, that you can save RGBA images to gif, without quality loss? (40 seconds to export a relatively simple gif animation is a lot) Can I influence the quality loss with the in-built saving by changing the Quantization method (Fast octree/ LIBIMAGEQUANT)? If yes, does the libimagequant method run on all computers (since the project I'm creating is meant to run on many Windows Pcs)?

radarhere commented 1 year ago

ADAPTIVE creates a palette from the image, in combination with the colors argument. From experience, WEB creates a static palette, regardless of the image.

>>> from PIL import Image
>>> im = Image.new("RGB", (1, 2))
>>> im.putpixel((0, 0), (255, 0, 0))
>>> im.putpixel((0, 1), (0, 255, 0))
>>> im.convert("P", palette=Image.Palette.ADAPTIVE).palette.colors
{(0, 255, 0): 0, (255, 0, 0): 1, (0, 0, 0): 2}
>>> im.convert("P", palette=Image.Palette.WEB).palette.colors
{(0, 1, 2): 0, (3, 4, 5): 1, (6, 7, 8): 2, (9, 10, 11): 3, (12, 13, 14): 4, (15, 16, 17): 5, (18, 19, 20): 6, (21, 22, 23): 7, (24, 25, 26): 8, (27, 28, 29): 9, (30, 31, 32): 10, (33, 34, 35): 11, (36, 37, 38): 12, (39, 40, 41): 13, (42, 43, 44): 14, (45, 46, 47): 15, (48, 49, 50): 16, (51, 52, 53): 17, (54, 55, 56): 18, (57, 58, 59): 19, (60, 61, 62): 20, (63, 64, 65): 21, (66, 67, 68): 22, (69, 70, 71): 23, (72, 73, 74): 24, (75, 76, 77): 25, (78, 79, 80): 26, (81, 82, 83): 27, (84, 85, 86): 28, (87, 88, 89): 29, (90, 91, 92): 30, (93, 94, 95): 31, (96, 97, 98): 32, (99, 100, 101): 33, (102, 103, 104): 34, (105, 106, 107): 35, (108, 109, 110): 36, (111, 112, 113): 37, (114, 115, 116): 38, (117, 118, 119): 39, (120, 121, 122): 40, (123, 124, 125): 41, (126, 127, 128): 42, (129, 130, 131): 43, (132, 133, 134): 44, (135, 136, 137): 45, (138, 139, 140): 46, (141, 142, 143): 47, (144, 145, 146): 48, (147, 148, 149): 49, (150, 151, 152): 50, (153, 154, 155): 51, (156, 157, 158): 52, (159, 160, 161): 53, (162, 163, 164): 54, (165, 166, 167): 55, (168, 169, 170): 56, (171, 172, 173): 57, (174, 175, 176): 58, (177, 178, 179): 59, (180, 181, 182): 60, (183, 184, 185): 61, (186, 187, 188): 62, (189, 190, 191): 63, (192, 193, 194): 64, (195, 196, 197): 65, (198, 199, 200): 66, (201, 202, 203): 67, (204, 205, 206): 68, (207, 208, 209): 69, (210, 211, 212): 70, (213, 214, 215): 71, (216, 217, 218): 72, (219, 220, 221): 73, (222, 223, 224): 74, (225, 226, 227): 75, (228, 229, 230): 76, (231, 232, 233): 77, (234, 235, 236): 78, (237, 238, 239): 79, (240, 241, 242): 80, (243, 244, 245): 81, (246, 247, 248): 82, (249, 250, 251): 83, (252, 253, 254): 84, (255, 0, 1): 85, (2, 3, 4): 86, (5, 6, 7): 87, (8, 9, 10): 88, (11, 12, 13): 89, (14, 15, 16): 90, (17, 18, 19): 91, (20, 21, 22): 92, (23, 24, 25): 93, (26, 27, 28): 94, (29, 30, 31): 95, (32, 33, 34): 96, (35, 36, 37): 97, (38, 39, 40): 98, (41, 42, 43): 99, (44, 45, 46): 100, (47, 48, 49): 101, (50, 51, 52): 102, (53, 54, 55): 103, (56, 57, 58): 104, (59, 60, 61): 105, (62, 63, 64): 106, (65, 66, 67): 107, (68, 69, 70): 108, (71, 72, 73): 109, (74, 75, 76): 110, (77, 78, 79): 111, (80, 81, 82): 112, (83, 84, 85): 113, (86, 87, 88): 114, (89, 90, 91): 115, (92, 93, 94): 116, (95, 96, 97): 117, (98, 99, 100): 118, (101, 102, 103): 119, (104, 105, 106): 120, (107, 108, 109): 121, (110, 111, 112): 122, (113, 114, 115): 123, (116, 117, 118): 124, (119, 120, 121): 125, (122, 123, 124): 126, (125, 126, 127): 127, (128, 129, 130): 128, (131, 132, 133): 129, (134, 135, 136): 130, (137, 138, 139): 131, (140, 141, 142): 132, (143, 144, 145): 133, (146, 147, 148): 134, (149, 150, 151): 135, (152, 153, 154): 136, (155, 156, 157): 137, (158, 159, 160): 138, (161, 162, 163): 139, (164, 165, 166): 140, (167, 168, 169): 141, (170, 171, 172): 142, (173, 174, 175): 143, (176, 177, 178): 144, (179, 180, 181): 145, (182, 183, 184): 146, (185, 186, 187): 147, (188, 189, 190): 148, (191, 192, 193): 149, (194, 195, 196): 150, (197, 198, 199): 151, (200, 201, 202): 152, (203, 204, 205): 153, (206, 207, 208): 154, (209, 210, 211): 155, (212, 213, 214): 156, (215, 216, 217): 157, (218, 219, 220): 158, (221, 222, 223): 159, (224, 225, 226): 160, (227, 228, 229): 161, (230, 231, 232): 162, (233, 234, 235): 163, (236, 237, 238): 164, (239, 240, 241): 165, (242, 243, 244): 166, (245, 246, 247): 167, (248, 249, 250): 168, (251, 252, 253): 169, (254, 255, 0): 170, (1, 2, 3): 171, (4, 5, 6): 172, (7, 8, 9): 173, (10, 11, 12): 174, (13, 14, 15): 175, (16, 17, 18): 176, (19, 20, 21): 177, (22, 23, 24): 178, (25, 26, 27): 179, (28, 29, 30): 180, (31, 32, 33): 181, (34, 35, 36): 182, (37, 38, 39): 183, (40, 41, 42): 184, (43, 44, 45): 185, (46, 47, 48): 186, (49, 50, 51): 187, (52, 53, 54): 188, (55, 56, 57): 189, (58, 59, 60): 190, (61, 62, 63): 191, (64, 65, 66): 192, (67, 68, 69): 193, (70, 71, 72): 194, (73, 74, 75): 195, (76, 77, 78): 196, (79, 80, 81): 197, (82, 83, 84): 198, (85, 86, 87): 199, (88, 89, 90): 200, (91, 92, 93): 201, (94, 95, 96): 202, (97, 98, 99): 203, (100, 101, 102): 204, (103, 104, 105): 205, (106, 107, 108): 206, (109, 110, 111): 207, (112, 113, 114): 208, (115, 116, 117): 209, (118, 119, 120): 210, (121, 122, 123): 211, (124, 125, 126): 212, (127, 128, 129): 213, (130, 131, 132): 214, (133, 134, 135): 215, (136, 137, 138): 216, (139, 140, 141): 217, (142, 143, 144): 218, (145, 146, 147): 219, (148, 149, 150): 220, (151, 152, 153): 221, (154, 155, 156): 222, (157, 158, 159): 223, (160, 161, 162): 224, (163, 164, 165): 225, (166, 167, 168): 226, (169, 170, 171): 227, (172, 173, 174): 228, (175, 176, 177): 229, (178, 179, 180): 230, (181, 182, 183): 231, (184, 185, 186): 232, (187, 188, 189): 233, (190, 191, 192): 234, (193, 194, 195): 235, (196, 197, 198): 236, (199, 200, 201): 237, (202, 203, 204): 238, (205, 206, 207): 239, (208, 209, 210): 240, (211, 212, 213): 241, (214, 215, 216): 242, (217, 218, 219): 243, (220, 221, 222): 244, (223, 224, 225): 245, (226, 227, 228): 246, (229, 230, 231): 247, (232, 233, 234): 248, (235, 236, 237): 249, (238, 239, 240): 250, (241, 242, 243): 251, (244, 245, 246): 252, (247, 248, 249): 253, (250, 251, 252): 254, (253, 254, 255): 255}
BootsManOut commented 1 year ago

Okay, that makes it very clear what it means, thank you very much! Do you plan an update for the PIL module for faster conversion from RGBA to P directly without color loss?

radarhere commented 1 year ago

The decision to use quantize for RGBA to P conversion was made in #574. Although I've realised that RGB to P conversion with an ADAPTIVE palette also quantizes, and that dates back to the PIL fork.

Do you plan an update for the PIL module for faster conversion from RGBA to P directly without color loss?

Pillow is a project maintained by people in their spare time. Tidelift graciously provides some support, but this is still a side project.

5204 talks about a more general form of this problem, where quantize is reducing to too few colours. So this has sort of been known about for a little while now.

But since the pixel-by-pixel export takes around 110 times longer, could this be implemented into the module, that you can save RGBA images to gif, without quality loss? (40 seconds to export a relatively simple gif animation is a lot)

I have to presume that quantize() is less accurate precisely in order to have better performance. If you're asking for more accuracy without a performance hit, I think that is a difficult request no matter what context you're in.

I added this additional step of pasting each frame unto a transparent alpha PIL image, because for some GIFs, when I imported them and then saved them, the first main frame of the GIF would not have a transparent background.

I think the best and quickest solution would be to look at a reproduction of this problem, so that you don't have to convert to RGBA in the first place.

Can I influence the quality loss with the in-built saving by changing the Quantization method (Fast octree/ LIBIMAGEQUANT)? If yes, does the libimagequant method run on all computers (since the project I'm creating is meant to run on many Windows Pcs)?

libimagequant will work if you have the dependency installed. However, this would require you to build Pillow from source, since https://pillow.readthedocs.io/en/stable/installation.html#external-libraries

Libimagequant is licensed GPLv3, which is more restrictive than the Pillow license, therefore we will not be distributing binaries with libimagequant support enabled.

BootsManOut commented 1 year ago

Okay, thank you very much for all of the information. I will have to convert to RGBA, these two images were just examples, but otherwise I will need RGBA. I will try the imageio library to do this very conversion step, imageio seems to have a good gif support. Thanks.

yaustar commented 9 months ago

@radarhere Just wanted to say thanks for providing the convert to palette custom function as I was hitting similar issue where convert was crush similar colors 👍

Nydeyas commented 7 months ago

@radarhere

I have a question related to this topic. Sorry if I shouldn't put it here. I'm trying to load a GIF file into PIL and save it.

This is the GIF I'm loading: before

This is the loading code:

import os
from PIL import Image, ImageSequence

def size(path: str) -> float:
    return round((os.stat(path).st_size)/1000000, 1)

path_load = "images/test_area/before.gif"
img = Image.open(path_load)
print(f"File size before: {size(path_load)}MB")

frames = [frame.copy() for frame in ImageSequence.Iterator(img)]

if frames:
    duration = frames[0].info['duration']
    if all(frame.info['duration'] == duration for frame in frames):
        print(f"Eeach loaded frame has the same duration ({duration}).")
    else:
        print("Loaded frames have irregular durations.")

    path_save = "images/test_area/after_pillow.gif"
    frames[0].save(path_save, save_all=True, append_images=frames[1:], 
                           loop=0,duration=duration, optimize=False)
    print(f"File size after: {size(path_save)}MB")

After saving, the size increases significantly: code1

Also the quality is worse.

When I add:

from PIL import GifImagePlugin as GifPl
GifPl.LOADING_STRATEGY = GifPl.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY

The size is even bigger: code2

But the GIF looks identical to the one loaded. It goes for every GIF I load this way.

Could you please tell me where this difference in size comes from and if there is any way to avoid that?

radarhere commented 7 months ago

For next time, I think a new issue would be better, as your primary question is around file size, which the rest of this issue is not concerned with.

The GIF format allows new frames to be encoded as just the difference between the new frame and the last one. So the last frame in your GIF file actually looks like this.

Pillow loads each of these potentially partial frames in and combines them to make full images that look correct. However, when saving, Pillow does not have a strategy that will save only these skeletal differences. It crops off the edges if they edges are the same, but it does not come up with the same strategy that your file originally used, and so saves most of the image for each frame. I expect that is the difference in file size.

BootsManOut commented 7 months ago

@Nydeyas You may try to use gifsicle in combination with PIL to compress and optimize the gif after modifying, as outlined in this article for example: https://medium.com/thedevproject/quick-and-easy-gif-creation-and-optimization-with-python-5223814861e2

Nydeyas commented 7 months ago

@radarhere

For next time, I think a new issue would be better, as your primary question is around file size, which the rest of this issue is not concerned with.

Understood. Sorry for that.

So PIL is using its own strategy to save gifs, that could be different from what was used to save the original file. In that case, unless using the same process, there is little that can be done.

Thank you for the help.

@BootsManOut

You may try to use gifsicle in combination with PIL to compress and optimize the gif after modifying, as outlined in this article for example: https://medium.com/thedevproject/quick-and-easy-gif-creation-and-optimization-with-python-5223814861e2

Thank you for the suggestion. I'll try it.

radarhere commented 7 months ago

I've created #7568 as a possible improvement to the file size concerns from @Nydeyas. With that PR, the file created is actually smaller than the original image.

radarhere commented 6 months ago

7568 has now been merged.

BootsManOut commented 6 months ago

From which PIL version on is this improvement included?

hugovk commented 6 months ago

It's in 10.2.0, released today!

BootsManOut commented 6 months ago

Awesome, thank you!