-
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
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
Comments
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. |
For your second example, When converting from RGBA to P,
Would you consider it more helpful to talk about this instead? If you wanted to post an image and example code? |
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) |
Thank you for the custom function! I shortened the "SaveAnimation" function down to only the bug related parts. #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. 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). |
You're running 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) |
Amazing, thank you very much for your help! 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) 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. |
A final note - when I wrote |
Hi again, Why is Image.Palette.ADAPTIVE better than Image.Palette.WEB? I realize that since I will also work with transparent GIFs, I will have to eventually use your custom function as well! 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) |
>>> 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} |
Okay, that makes it very clear what it means, thank you very much! |
The decision to use
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
I have to presume that
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.
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
|
Okay, thank you very much for all of the information. |
@radarhere Just wanted to say thanks for providing the convert to palette custom function as I was hitting similar issue where |
@Nydeyas |
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. Thank you for the help.
Thank you for the suggestion. I'll try it. |
#7568 has now been merged. |
From which PIL version on is this improvement included? |
It's in 10.2.0, released today! |
Awesome, thank you! |
In some cases, such as GIF output, Pillow must convert to P (palette) mode. Unfortunately, when done on RGBA images, this can lose some colour information (cf, python-pillow/Pillow#6832) But when the image starts in RGB mode, the conversion to P mode works better, so convert frames to RGB when they don't have any transparent pixels. Fixes matplotlib#29190
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:
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:
The code used is this:
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:
After saving, there is an extreme quality loss:
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:
This suggests that error happens during GIF export.
The code used for this is the following:
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?
The text was updated successfully, but these errors were encountered: