Changing the palette of an image

(0 comments)

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

Mona Lisas

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: 1

Comments

Comments are pre-moderated. Please be patient and your comment will appear soon.

There are currently no comments

New Comment

required

required (not published)

optional

required