Search code examples
pythonopencvpython-imaging-library

How to recreate PIL palette image using OpenCV? (confusion with P mode)


I am trying to replace part of the code originally written with PIL by OpenCV. Ideally, I would like to eliminate PIL or at least make that input (first_frame) is OpenCV array.

Original (PIL) code:

from PIL import Image
import numpy as np
import cv2

first_frame_path = "00000.png"
image2 = Image.open(first_frame_path)
print(image2.mode) # out: P <---

image2_p = image2.convert("P")
image2_pil = np.array(image2_p)
print(image2_pil.mean()) # out: 0.039107 <---

OpenCV code:

from PIL import Image
import numpy as np
import cv2

first_frame_path = "00000.png"

image = cv2.imread(first_frame_path, cv2.IMREAD_COLOR)

image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
image_from_array = Image.fromarray(image)
print(image_from_array.mode) # out: RGB <---

image_p = image_from_array.convert("P")
image_pil = np.array(image_p)
print(image_pil.mean()) # out: 0.48973 <---

How should I adapt my OpenCV code to make that image_pil has the same values as image2_pil?

I computed the mean() only to present that these two arrays are different, and my goal is to obtain same results.

I am working with simple mask: enter image description here

I understand that the difference comes from the fact that in original code mask is directly loaded as P, contradicting to OpenCV code, where it is RGB. Unfortunately, I don't have an idea how to fix it. I tried to specify:

Image.fromarray(image, mode="P")

However, then I am getting error:

Exception has occurred: ValueError Too many dimensions: 3 > 2.

Could you please tell me what can I do to obtain the same NumPy array as in original code?


Solution

  • This does what you want. It relies on allowing PIL to convert the image to RGB and thence back to palette-mode creating (or recreating) the palette in its own (hopefully deterministic) way each time.

    In general, what you are doing is really rather ill-advised...

    #!/usr/bin/env python3
    
    import cv2 as cv
    import numpy as np
    from PIL import Image
    
    # Load original image
    im = Image.open('26dQH.png')
    
    # Convert to RGB, then back to palette so that **PIL decides the palette**
    im = im.convert('RGB').convert('P', palette=Image.Palette.ADAPTIVE)
    
    # Get its colours
    print(f'im.getcolors(): {im.getcolors()}')
    # prints im.getcolors(): [(5367, 0), (5297, 1), (399256, 2)]
    
    # Print first 3 palette entries
    print(np.array(im.getpalette()).reshape((-1,3))[:3])
    
    # Prints:
    # [[  0 128   0]
    #  [128   0   0]
    #  [  0   0   0]]
    
    # Read same image using OpenCV and convert to palette-mode PIL Image
    na = cv.imread('26dQH.png', cv.IMREAD_COLOR)
    pi = Image.fromarray(na).convert('P', palette=Image.Palette.ADAPTIVE)
    
    # Get its colours
    print(f'pi.getcolors(): {pi.getcolors()}') 
    # prints pi.getcolors(): [(5367, 0), (5297, 1), (399256, 2)]
    
    # Print first 3 palette entries
    print(np.array(im.getpalette()).reshape((-1,3))[:3])
    # Prints:
    # [[  0 128   0]
    #  [128   0   0]
    #  [  0   0   0]]
    

    Note that you can check the palette of a PNG with pngcheck like this:

    pngcheck -p  26dQH.png
    zlib warning:  different version (expected 1.2.11, using 1.2.12)
    
    File: 26dQH.png (2063 bytes)
      PLTE chunk: 256 palette entries
          0:  (  0,  0,  0) = (0x00,0x00,0x00)
          1:  (128,  0,  0) = (0x80,0x00,0x00)
          2:  (  0,128,  0) = (0x00,0x80,0x00)
          3:  (128,128,  0) = (0x80,0x80,0x00)
          4:  (  0,  0,128) = (0x00,0x00,0x80)
          5:  (128,  0,128) = (0x80,0x00,0x80)
        ...
        ...
        ...
        253:  (224, 96,192) = (0xe0,0x60,0xc0)
        254:  ( 96,224,192) = (0x60,0xe0,0xc0)
        255:  (224,224,192) = (0xe0,0xe0,0xc0)
    OK: 26dQH.png (854x480, 8-bit palette, non-interlaced, 99.5%).
    

    Or you can use ImageMagick - which very usefully gets you the frequencies/counts of each palette entry (in the first column) too:

    magick 26dQH.png -format %c histogram:info: 
    
        399256: (0,0,0) #000000 black
          5367: (0,128,0) #008000 green
          5297: (128,0,0) #800000 maroon
    

    Or with exiftool like this:

    exiftool -b -palette  26dQH.png | xxd -g 1 -c 3    
    00000000: 00 00 00  ...   # entry 0
    00000003: 80 00 00  ...   # entry 1
    00000006: 00 80 00  ...   # entry 2
    00000009: 80 80 00  ...
    ...
    ...