2

I have an image, I want to change all the colors in the image from a color map eg. {(10,20,212) : (60,40,112)...}

Currently, I am reading the image OpenCV and then iterating over the image array and changing each pixel, but this is very slow.

Is there any way I can do it faster?

Ankit Agrawal
  • 101
  • 1
  • 7
  • What is the size of your hashmap? – ZdaR Jul 24 '19 at 07:44
  • It's a segmentation mask, and size of my hashmap is around 100 – Ankit Agrawal Jul 24 '19 at 07:51
  • What do you mean *"you want to change all pixels in the image from a colormap"*? Change them from what into what? Please show your image. And your colourmap. – Mark Setchell Jul 24 '19 at 08:04
  • I want to change the pixels of an image from a dictionary like {(10,20,212) : (60,40,112)...} with BGR values, I have to iterate over every pixel in the image and change it by using the color_map dictionary. I can't show the image due to confidentiality concerns. – Ankit Agrawal Jul 24 '19 at 08:25
  • You mean you want to change pixels with values `rgb(10,20,212)` into `rgb(60,40,112)`? If so, how many such colours do you want to remap? – Mark Setchell Jul 24 '19 at 08:53
  • The entire image, which contains around 100 colors – Ankit Agrawal Jul 25 '19 at 11:17

3 Answers3

8

I am providing two answers to this question. This answer is more based in PIL/Pillow and the other is more based in OpenCV. Read this answer in conjunction with my other answer and potentially mix and match.

You can do it using the palette. In case you are unfamiliar with palettised images, rather than having an RGB value at each pixel location, you have a simple 8-bit index into a palette of up to 256 colours.

So, what we can do, is load your image as a PIL Image, and quantise it to the set of input colours you have. Then each pixel will have the index of the colour in your map. Then just replace the palette with the colours you want to map to.

#!/usr/bin/env python3

import numpy as np
from PIL import Image

def QuantizeToGivenPalette(im, palette):
    """Quantize image to a given palette.

    The input image is expected to be a PIL Image.
    The palette is expected to be a list of no more than 256 R,G,B values."""

    e = len(palette)
    assert e>0,    "Palette unexpectedly short"
    assert e<=768, "Palette unexpectedly long"
    assert e%3==0, "Palette not multiple of 3, so not RGB"

    # Make tiny, 1x1 new palette image
    p = Image.new("P", (1,1))

    # Zero-pad the palette to 256 RGB colours, i.e. 768 values and apply to image
    palette += (768-e)*[0]
    p.putpalette(palette)

    # Now quantize input image to the same palette as our little image
    return im.convert("RGB").quantize(palette=p)

# Open input image and palettise to "inPalette" so each pixel is replaced by palette index
# ... so all black pixels become 0, all red pixels become 1, all green pixels become 2...
im = Image.open('image.png').convert('RGB')

inPalette = [
    0,0,0,    # black
    255,0,0,  # red
    0,255,0,  # green
    0,0,255,  # blue
    255,255,255 # white
    ]
r = QuantizeToGivenPalette(im,inPalette)

# Now simply replace the palette leaving the indices unchanged
newPalette = [
    255,255,255,  # white
    0,255,255,    # cyan
    255,0,255,    # magenta
    255,255,0,    # yellow
    0,0,0         # black
    ]

# Zero-pad the palette to 256 RGB colours, i.e. 768 values
newPalette += (768-len(newPalette))*[0]

# And finally replace the palette with the new one
r.putpalette(newPalette)

# Save result
r.save('result.png')

Input Image

enter image description here

Output Image

enter image description here

So, to do specifically what you asked with a dictionary that maps old colour values to new ones, you will want to initialise oldPalette to the keys of your dictionary and newPalette to the values of your dictionary.

Keywords: Python, PIL, Pillow, image, image processing, quantise, quantize, specific palette, given palette, specified palette, known palette, remap, re-map, colormap, map.

There are some hopefully useful words about palettised images here, and here.

Mark Setchell
  • 191,897
  • 31
  • 273
  • 432
8

I am providing two answers to this question. This answer is more based in OpenCV and the other is more based in PIL/Pillow. Read this answer in conjunction with my other answer and potentially mix and match.

You can use Numpy's linalg.norm() to find the distances between colours and then argmin() to choose the nearest. You can then use a LUT "Look Up Table" to look up a new value based on the existing values in an image.

#!/usr/bin/env python3

import numpy as np
import cv2

def QuantizeToGivenPalette(im, palette):
    """Quantize image to a given palette.
    
    The input image is expected to be a Numpy array.
    The palette is expected to be a list of R,G,B values."""

    # Calculate the distance to each palette entry from each pixel
    distance = np.linalg.norm(im[:,:,None] - palette[None,None,:], axis=3)

    # Now choose whichever one of the palette colours is nearest for each pixel
    palettised = np.argmin(distance, axis=2).astype(np.uint8)

    return palettised

# Open input image and palettise to "inPalette" so each pixel is replaced by palette index
# ... so all black pixels become 0, all red pixels become 1, all green pixels become 2...
im=cv2.imread("image.png",cv2.IMREAD_COLOR)

inPalette = np.array([
   [0,0,0],             # black
   [0,0,255],           # red
   [0,255,0],           # green
   [255,0,0],           # blue
   [255,255,255]],      # white
   )

r = QuantizeToGivenPalette(im,inPalette)

# Now make LUT (Look Up Table) with the 5 new colours
LUT = np.zeros((5,3),dtype=np.uint8)
LUT[0]=[255,255,255]  # white
LUT[1]=[255,255,0]    # cyan
LUT[2]=[255,0,255]    # magenta
LUT[3]=[0,255,255]    # yellow
LUT[4]=[0,0,0]        # black

# Look up each pixel in the LUT
result = LUT[r]

# Save result
cv2.imwrite('result.png', result)

Input Image

enter image description here

Output Image

enter image description here

Keywords: Python, PIL, Pillow, image, image processing, quantise, quantize, specific palette, given palette, specified palette, known palette, remap, re-map, colormap, map, LUT, linalg.norm.

Martin
  • 3,333
  • 2
  • 18
  • 39
Mark Setchell
  • 191,897
  • 31
  • 273
  • 432
  • great solution. However, I seem to be getting wrong results (probably due to uint8 overflow, during subtracting). Shouldnt inPalette be different type than uint8? – Martin Jan 23 '22 at 16:38
  • In the inPallete array, shouldn't the RGB corresponding to the row commented 'red' be [255, 0, 0]? – caffeinemachine Aug 04 '23 at 14:49
  • @caffeinemachine No, I am using OpenCV, which uses BGR ordering rather than RGB. – Mark Setchell Aug 04 '23 at 15:15
0

I think you might find using the built in LUT function of opencv helpful, as documented here.

There is already a python binding for the function, and it takes as input the original matrix and a LUT, and returns the new matrix as an output.

There isn't a tutorial for using it in python, but there is one for using it in C++ which I imagine will be useful, found here. That tutorial lists this method as the fastest one for this sort of problem.

TheBarrometer
  • 356
  • 1
  • 7