python-pillow / python-pillow/Pillow
Inconsistent use of dithering in `convert` and `quantize`
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:

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

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
)

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


Is there a workaround? Well, the docs for quantize claim to apply a dither.
Let's try it:
orig.quantize(16, dither=Image.FLOYDSTEINBERG)

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)

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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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