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

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

Open
BootsManOut opened this issue Dec 27, 2022 · 23 comments
Open

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

BootsManOut opened this issue Dec 27, 2022 · 23 comments
Labels

Comments

@BootsManOut
Copy link

BootsManOut commented Dec 27, 2022

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 radarhere added the GIF label Dec 28, 2022
@radarhere
Copy link
Member

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
Copy link
Member

radarhere commented Dec 28, 2022

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?

@radarhere radarhere changed the title Color and Quality Loss in 2 cases: 1.) similar colors 2.) When adding alpha channel Color and Quality Loss with similar colors or from RGBA Dec 28, 2022
@radarhere radarhere changed the title Color and Quality Loss with similar colors or from RGBA Color and quality Loss with similar colors or from RGBA Dec 28, 2022
@radarhere radarhere changed the title Color and quality Loss with similar colors or from RGBA Color and quality loss with similar colors or from RGBA Dec 28, 2022
@BootsManOut
Copy link
Author

BootsManOut commented Dec 28, 2022

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
Copy link
Member

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
Copy link
Author

BootsManOut commented Dec 29, 2022

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
Copy link
Member

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
Copy link
Author

BootsManOut commented Dec 30, 2022

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
Copy link
Member

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

@BootsManOut
Copy link
Author

BootsManOut commented Dec 30, 2022

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
Copy link
Member

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}

@radarhere radarhere reopened this Dec 30, 2022
@BootsManOut
Copy link
Author

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
Copy link
Member

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
Copy link
Author

BootsManOut commented Jan 23, 2023

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
Copy link

yaustar commented Oct 3, 2023

@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
Copy link

Nydeyas commented Nov 22, 2023

@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
Copy link
Member

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
Copy link
Author

@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
Copy link

Nydeyas commented Nov 23, 2023

@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
Copy link
Member

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
Copy link
Member

#7568 has now been merged.

@BootsManOut
Copy link
Author

BootsManOut commented Jan 2, 2024

From which PIL version on is this improvement included?

@hugovk
Copy link
Member

hugovk commented Jan 2, 2024

It's in 10.2.0, released today!

@BootsManOut
Copy link
Author

Awesome, thank you!

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

5 participants