python-pillow / python-pillow/Pillow

Inconsistent use of dithering in `convert` and `quantize`

Open
#5,836 0 comments 21 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
13.8k
Forks
2.5k
Avg merge
2d 8h
Merged PRs (30d)
89

Description

What did you do?

Let's play around with image dithering, starting with this nice, low-res picture of Theresa May:

image

According to the docs calling convert in "P" mode defaults to the WEB palette with dithering:

orig.convert("P")

image

Nice, that looks good! And you can pass Image.NONE to disable the dithering.

What if we want to use less colors? Maybe we cut down the bit depth and dither to make up for it, for a retro effect.
Checking the docs again, it claims that we can use an ADAPTIVE palette and pass in some number of colors; let's use 16.
The docs say that dithering is used when converting from "RGB" to "P" and that it defaults to FLOYDSTEINBERG, but let's pass in dither just to make sure:

dither_lesscol_broken = orig.convert(
    "P", palette=Image.ADAPTIVE, colors=16, dither=Image.FLOYDSTEINBERG
)

image

Doesn't look dithered - test it on a gradient image and we get severe banding:

image

image

Is there a workaround? Well, the docs for quantize claim to apply a dither.
Let's try it:

orig.quantize(16, dither=Image.FLOYDSTEINBERG)

image

No dice, exactly the same as the previous. Looking at the code, we see that quantize calls ImagingCore.convert with a dither - but ONLY if you pass in a palette!
Let's try that:

# Call .quantize once to get the palette
pal = orig.quantize(16)
# And again using that palette
dither_lesscol = orig.quantize(16, palette=pal, dither=Image.FLOYDSTEINBERG)

image

There we go, that's what I expected.

What did you expect to happen?

Image.convert should be able to simultanously handle an ADAPTIVE palette and apply dithering (or not) based on the dither argument.

Additionally, Image.quantize should apply dithering (or not) based on the dither argument.

What actually happened?

orig.convert("P", palette=Image.ADAPTIVE, colors=16, dither=Image.FLOYDSTEINBERG) produces a non-dithered image.

orig.quantize(16, dither=Image.FLOYDSTEINBERG) also produces a non-dithered image.

What are your OS, Python and Pillow versions?
  • OS: Linux Mint 20.1
  • Python: 3.8.10
  • Pillow: 7.0.0

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in PIL/Image.py at Image.convert and Image.quantize, then inspect the referenced ImagingCore.convert path to compare how adaptive palettes and dither arguments are handled. Done means both APIs honor the requested dithering behavior for the examples described, including adaptive palettes and quantization without a supplied palette.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
computer-vision
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.