# Changing the palette of an image

A short script to reduce the palette of an image and replace it with random colours, using the image mona_lisa_400.jpg.

import numpy as np
from PIL import Image

def get_new_val(old_val, nc):
"""
Get the "closest" colour to old_val in the range [0,1] per channel divided
into nc values.

"""

return np.round(old_val * (nc - 1)) / (nc - 1)

# For RGB images, the following might give better colour-matching.
#p = np.linspace(0, 1, nc)
#p = np.array(list(product(p,p,p)))
#def get_new_val(old_val):
#    idx = np.argmin(np.sum((old_val[None,:] - p)**2, axis=1))
#    return p[idx]

def fs_dither(img, nc):
"""
Floyd-Steinberg dither the image img into a palette with nc colours per
channel.

"""

arr = np.array(img, dtype=float) / 255
height, width = arr.shape[:2]

for ir in range(height):
for ic in range(width):
# NB need to copy here for RGB arrays otherwise err will be (0,0,0)!
old_val = arr[ir, ic].copy()
new_val = get_new_val(old_val, nc)
arr[ir, ic] = new_val
err = old_val - new_val
# In this simple example, we will just ignore the border pixels.
if ic < width - 1:
arr[ir, ic+1] += err * 7/16
if ir < height - 1:
if ic > 0:
arr[ir+1, ic-1] += err * 3/16
arr[ir+1, ic] += err * 5/16
if ic < width - 1:
arr[ir+1, ic+1] += err / 16

carr = np.array(arr/np.max(arr, axis=(0,1)) * 255, dtype=np.uint8)
return Image.fromarray(carr)

def palette_reduce(img, nc):
"""Simple palette reduction without dithering."""
arr = np.array(img, dtype=float) / 255
arr = get_new_val(arr, nc)

carr = np.array(arr/np.max(arr) * 255, dtype=np.uint8)
return Image.fromarray(carr)

def get_image(img_name, new_width=400, GREYSCALE=False):

# Read in the image, convert to greyscale.
img = Image.open(img_name)
if GREYSCALE:
img = img.convert('L')

width, height = img.size
new_height = int(height * new_width / width)
img = img.resize((new_width, new_height), Image.ANTIALIAS)
return img

colours = ['581845', '900c3f', 'c70039', 'ff5733', 'ffc30f']
colours = ['7d6608', '9d7d0a', 'b7950b', 'd4ac0d', 'f1c40f', 'f4d03f', 'f7dc6f', 'f9e79f']
def hex_to_tuple(s):
return int(s[:2], 16), int(s[2:4], 16), int(s[4:], 16)
new_palette = [hex_to_tuple(s) for s in colours]

def mess_with_palette_dict(palette_dict):
n = len(new_palette)
for i, k in enumerate(palette_dict.keys()):
palette_dict[k] = new_palette[i % n]

def random_mess_with_palette_dict(palette_dict):
n = len(new_palette)
for i, k in enumerate(palette_dict.keys()):
palette_dict[k] = np.random.randint(0, 255, dtype=np.uint8, size=3)

def crazy_img(rimg, j):
arr = np.array(rimg)
height, width = arr.shape[:2]
rarr = arr.reshape((width*height, 3))
palette = set((tuple(t) for t in rarr))
palette_arr = np.array(list(palette))
offset = np.random.randint(0, 256, palette_arr.shape, dtype=np.uint8)
new_palette_arr = palette_arr
palette_len = len(palette)
palette_dict = dict((tuple(palette_arr[i]), new_palette_arr[i]) for i in range(palette_len))
random_mess_with_palette_dict(palette_dict)
new_rarr = np.zeros_like(rarr)
for i in range(width*height):
new_rarr[i] = palette_dict[tuple(rarr[i])]
new_rarr = new_rarr.reshape((height, width, 3))
new_rimg = Image.fromarray(new_rarr)
new_rimg.save(f'img/img{j}.png')

img_name = 'mona_lisa_400.jpg'
img = get_image(img_name, GREYSCALE=False)
#r_img = fs_dither(img, 2)
r_img = palette_reduce(img, 4)

n = 24
for j in range(n):
print(f'{j+1}/{n}')
crazy_img(r_img, j+1)

Current rating: 2