Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does PIL's Image.convert() function work with mode 'P'

I have a set of 24-bit png files and I want to transform them into 8-bit png files. I used PIL's Image.convert() method for solving this problem. However, after using mode 'P' as the argument, I found out that pixels with same RGB values can be converted differently.

I transferred an example image into a numpy array and the original 24-bit png file has values like this:

RGB array

   ..., 
   [204, 102, 119],
   [204, 102, 119],
   [204, 102, 119],
   [204, 102, 119],
   [204, 102, 119],
   [204, 102, 119],
   [204, 102, 119],
   [204, 102, 119],
   [204, 102, 119],
   [204, 102, 119],
   [204, 102, 119], 
   ...

After using the convert function with mode 'P', the images value became like this:

8-bit array

   ..., 98, 98, 134, 98, 98, 98, 134, 98, 98, 98, 134, ...

Code Example:

from PIL import Image
import numpy as np
img = Image.open("path-to-file.png")
p_img = img.convert("P")

I expect that pixels with same RGB values are converted in the same way. I know pixels are converted into palette index, but this still doesn't make sense to me. I'm not familiar with the PIL library. Can someone please explain why this happens? Thanks in advance.


Implemented a little something following Mark's examples

import numpy as np
from PIL import Image
#from improc import GenerateNColourImage

# Set image height and width
N    = 6
h, w = 100, 100

# Generate repeatable random Numpy image with N^3 unique colours at most
n = np.random.randint(N, size=(h, w, 3), dtype=np.uint8)
# Work out indices of diagonal elements
diags = np.diag_indices(h)

# Intentionally set all diagonal elements same shade of blue
n[diags] = [10,20,200]
# Make Numpy image into PIL Image, palettise, convert back to Numpy array and check diagonals
a0 = Image.fromarray(n)

unique_colors = np.unique(n.reshape(-1, n.shape[2]), axis=0).shape
print(unique_colors)   #e.g. print (217, 3)
a1 = a0.convert('P')
a2 = np.array(a1)

# Look at diagonals - should all be the same
print(a2[diags])
print(' %s %d' % ("Number of unique colors:  ", np.unique(a2).shape[0]))

Diagonal pixels' values printed

... 154 154 154 154 154 154 124 154 160 160 160 154 160 ...

The 8-bit image in mode 'P' contains 125 unique palette indexes. It seems PIL will perform dithering no matter what.

like image 709
VicXue Avatar asked Jul 10 '19 03:07

VicXue


People also ask

What is image mode P?

If you have a P mode image, that means it is palettised. That means there is a palette with up to 256 different colours in it, and instead of storing 3 bytes for R, G and B for each pixel, you store 1 byte which is the index into the palette. This confers both advantages and disadvantages.

What does PIL image return?

The Image module provides a class with the same name which is used to represent a PIL image. The module also provides a number of factory functions, including functions to load images from files, and to create new images. Image. convert() Returns a converted copy of this image.

How do you convert an image to grayscale in Python?

Convert an Image to Grayscale in Python Using the Conversion Formula and the Matplotlib Library. We can also convert an image to grayscale using the standard RGB to grayscale conversion formula that is imgGray = 0.2989 * R + 0.5870 * G + 0.1140 * B .


2 Answers

This is a normal behavior shown, when we convert a Image into P color mode. The way Palette mode works is it creates a mapping table, which corresponds a index (in range 0 - 255) to a discrete color in larger color space (like RGB). For example, RGB color value (0, 0, 255) (Pure Blue) in an image gets an index 1 (just an hypothetical example). This same process goes through each unique pixel value in the original image (but the table size should not exceed 256, in the process of mapping). So, the numpy array (or a regular list) having values like this:-

   ..., 98, 98, 134, 98, 98, 98, 134, 98, 98, 98, 134, ...

corresponds to the index, in the mapping table, rather then the actual color value itself. So, you may interpret them as a index, which upon reading an image gets converted to the actual color value stored in that index.

But these pixel values, need not always mean the image is of color mode P. For example, if you view the pixel data of a Greyscale image (L), the values would look the same like in the case of paletted mode, but would actually correspond to true color values (or shades of grey), rather then a index.

like image 106
Vasu Deo.S Avatar answered Oct 05 '22 23:10

Vasu Deo.S


The issue is that PIL/Pillow is "dithering". Basically, if you have more than the 256 colours (maximum a palette can hold) in your image, there are necessarily colours in the image that do not occur in the palette. So, PIL accumulates the errors (difference between original colour and palettised colour) and every now and then inserts a pixel of a slightly different colour so that the image looks more or less correct from a distance. It's basically "error diffusion". So, if your colour gets caught up in that, it will sometimes come out differently.

One way you can avoid that, is to quantise the image down to less than 256 colours, then there will be no errors to diffuse.

# Quantise to 256 colours
im256c = = image.quantize(colors=256, method=2)

Note that this does not mean your shade of blue will always map to the same palette index in every image, it just means all pixels with your shade of blue in any one given image will all have the same palette index.

Here is an example:

#!/usr/bin/env python3

import numpy as np
from PIL import Image
from improc import GenerateNColourImage

# Set image height and width
h, w = 200, 200
N    = 1000

# Generate repeatable random Numpy image with N unique colours
np.random.seed(42)
n = GenerateNColourImage(h,w,N) 

# Work out indices of diagonal elements
diags = np.diag_indices(h)

# Intentionally set all diagonal elements same shade of blue
n[diags] = [10,20,200]

# Make Numpy image into PIL Image, palettise, convert back to Numpy array and check diagonals
a0 = Image.fromarray(n)
a1 = a0.convert('P')
a2 = np.array(a1)

# Look at diagonals - should all be the same
print(a2[diags])

Output

[154 154 154 154 154 154 154 154 160 154 160 154 154 154 154 154 160 154
 154 154 160 154 154 154 160 160 154 154 154 160 154 154 154 154 154 154
 154 154 160 154 154 154 154 154 154 154 154 160 160 154 154 154 154 154
 154 154 154 154 154 154 154 154 154 160 154 154 154 154 154 154 154 160
 154 160 154 154 154 154 154 154 154 154 154 154 154 160 154 160 160 154
 154 160 154 154 154 160 154 154 154 154 154 160 154 154 154 154 155 154
 154 160 154 154 154 154 154 154 154 154 154 160 154 154 154 160 154 154
 154 154 160 154 154 154 154 154 154 154 154 154 154 154 154 154 154 160
 154 160 154 160 154 160 154 160 160 154 154 154 154 154 154 154 154 154
 154 154 154 154 161 154 154 154 154 154 154 154 154 154 154 160 154 160
 118 154 160 154 154 154 154 154 154 154 154 154 160 154 154 160 154 154
 154 154]

Ooops, there are values of 154, 118 and 160 in there...


Now do those last 4 lines again, with the same Numpy Array, but using quantise():

# Make Numpy image into PIL Image, quantise, convert back to Numpy array and check diagonals
b0 = Image.fromarray(n)
b1 = b0.quantize(colors=256,method=2)
b2 = np.array(b1)

# Look at diagonals - should all be the same
print(b2[diags])

Output

[64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64
 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64
 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64
 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64
 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64
 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64
 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64
 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64
 64 64 64 64 64 64 64 64]

That's better - all the same!

I should add, that if you save an image as PNG with 256 colours or less, PIL/Pillow will automatically save a palette image.

like image 29
Mark Setchell Avatar answered Oct 06 '22 00:10

Mark Setchell