1

I'm creating a simple GIF animation using PIL:

from PIL import Image, ImageDraw, ImageFont

images = []

for x, i in enumerate(range(10)):
    image = Image.new(mode="RGB", size=(320, 60), color="orange")

    draw = ImageDraw.Draw(image)
    fnt = ImageFont.truetype('font.ttf', size=10)
    draw.text((10, 10), ("%s" % x), fill=(0, 0, 255), font=fnt)
    images.append(image)

images[0].save("result/pil.gif", save_all=True, append_images=images[1:], duration=1000, loop=0, format="GIF")

The problem is that whenever I use Draw.text, image's background is getting some kind of white noze:

enter image description here

I found some info that I have to use getpalette from the first frame and putpalette for all the other frames like this:

for x, i in enumerate(range(10)):
    image = Image.new(mode="RGB", size=(320, 60), color="orange")

    if x == 0:
        palette = image.getpalette()
    else:
        image.putpalette(palette)

But it just gives me: ValueError: illegal image mode.

What's the reason of the noizy background and how can I fix it?

UPD I was able to fix the background by changing image mode to "P", but in this case my fonts became unreadable. These are examples with RGB mode (fonts are well) and P mode (fonts are awful):

enter image description here enter image description here

Why am I getting either nice background or nice fonts but not both? Is there a workaround?

radarhere
  • 929
  • 8
  • 23
ozahorulia
  • 9,798
  • 8
  • 48
  • 72

2 Answers2

6

This is dithering that happens, because gif can contain only colors from palette of size 256. Most likely PIL uses very basic algorithm to convert from RGB format to indexed format, which is required by gif. As your image contains colors #ff9900 and #ffcc00, then palette presumably consists of hex values 00, 33, 66, 99, cc, ff for each byte and has size 6x6x6 = 216, which fits nicely into 256 possible values. 'orange' has value of #ffa500 and can't be represented by such palette, so the background is filled by nearest available colors.

You can try to use color '#ff9900' instead of 'orange'. Hopefully this color can be represented by palette, as it is present in your noisy image.

You can also try to convert from RGB to indexed format using your own palette as an argument in quantize method, as suggested in this answer. Adding the following line results in nice solid background:

image = image.quantize(method=Image.MEDIANCUT)

enter image description here

Or you can just save RGB image with PNG format. In this case it will be saved as APNG image.

images[0].save("pil.png", save_all=True, append_images=images[0:],duration=1000, loop=0, format="PNG")

enter image description here

fdermishin
  • 3,519
  • 3
  • 24
  • 45
  • `image = image.quantize(method=Image.MEDIANCUT)` - This works perfectly for me! It slightly increases the execution time but it's still acceptable. There will never be too much colors in my gifs, but they are always unpredictable (user-entered data). That's why quantize is the best option for me. Thank you! – ozahorulia Nov 23 '20 at 18:17
1

user13044086 has given a generalised version of the problem, the specifics are that a gif is a palletised format, to convert your original RGB to a palette pillow needs to restrict it from "true color" to just 256 colors.

To do that, it will convert the image to mode L, and as you can see in the documentation if no transformation matrix is given that's just an alias for calling quantize with the default parameters. The relevant default parameter here is dither, meaning if the image has more than 256 colors try to "emulate" missing ones by having individual dots of nearby colors.

Now you might complain that you don't have more than 256 colors, but you probably do due to font antialiasing, and algorithms don't necessarily have "taste" so instead of dithering in the letter, it dithers very visibly in the background.

You can mitigate this issue by explicitly quantizing providing an explicit method as suggested, or just disabling dithering (which will probably yield lower-quality results but will certainly be faster):

images.append(image.quantize(dither=Image.NONE))

Manually crafting your own palette and passing that to quantize could also work.

Masklinn
  • 34,759
  • 3
  • 38
  • 57