What I want to achieve is to programmatically create a two-dimensional color ramp represented by a 256x256 matrix of color values. The expected result can be seen in the att
Here are 3 ways to do this bilinear interpolation. The first version does all the arithmetic in pure Python, the second uses PIL image composition, the third uses Numpy to do the arithmetic. As expected, the pure Python is significantly slower than the other approaches. The Numpy version (which was derived from code written by Andras Deak) is almost as fast as the PIL version for small images, but for larger images the PIL version is noticeably faster.
I also tried using jadsq's scaling technique in PIL but the results were not good - I suspect that PIL's interpolation code is a little buggy.
If you wanted to create lots of these bilinear gradient images of the same size, the PIL technique has another advantage: once you've created the composition masks you don't need to rebuild them for every image.
#!/usr/bin/env python3
''' Simple bilinear interpolation
Written by PM 2Ring 2016.09.14
'''
from PIL import Image
from math import floor
import numpy as np
def color_square0(colors, size):
tl, tr, bl, br = colors
m = size - 1
r = range(size)
def interp_2D(tl, tr, bl, br, x, y):
u0, v0 = x / m, y / m
u1, v1 = 1 - u0, 1 - v0
return floor(0.5 + u1*v1*tl + u0*v1*tr + u1*v0*bl + u0*v0*br)
data = bytes(interp_2D(tl[i], tr[i], bl[i], br[i], x, y)
for y in r for x in r for i in (0, 1, 2))
return Image.frombytes('RGB', (size, size), data)
# Fastest
def color_square1(colors, size):
#Make an Image of each corner color
tl, tr, bl, br = [Image.new('RGB', (size, size), color=c) for c in colors]
#Make the composition mask
mask = Image.new('L', (size, size))
m = 255.0 / (size - 1)
mask.putdata([int(m * x) for x in range(size)] * size)
imgt = Image.composite(tr, tl, mask)
imgb = Image.composite(br, bl, mask)
return Image.composite(imgb, imgt, mask.transpose(Image.TRANSPOSE))
# This function was derived from code written by Andras Deak
def color_square2(colors, size):
tl, tr, bl, br = map(np.array, colors)
m = size - 1
x, y = np.mgrid[0:size, 0:size]
x = x[..., None] / m
y = y[..., None] / m
data = np.floor(x*y*br + (1-x)*y*tr + x*(1-y)*bl + (1-x)*(1-y)*tl + 0.5)
return Image.fromarray(np.array(data, dtype = 'uint8'), 'RGB')
color_square = color_square1
#tl = (255, 0, 0)
#tr = (255, 255, 0)
#bl = (0, 0, 255)
#br = (0, 255, 0)
tl = (108, 115, 111)
tr = (239, 239, 192)
bl = (124, 137, 129)
br = (192, 192, 175)
colors = (tl, tr, bl, br)
size = 256
img = color_square(colors, size)
img.show()
#img.save('test.png')
output
Just for fun, here's a simple GUI program using Tkinter which can be used to generate these gradients.
#!/usr/bin/env python3
''' Simple bilinear colour interpolation
using PIL, in a Tkinter GUI
Inspired by https://stackoverflow.com/q/39485178/4014959
Written by PM 2Ring 2016.09.15
'''
import tkinter as tk
from tkinter.colorchooser import askcolor
from tkinter.filedialog import asksaveasfilename
from PIL import Image, ImageTk
DEFCOLOR = '#d9d9d9'
SIZE = 256
#Make the composition masks
mask = Image.new('L', (SIZE, SIZE))
m = 255.0 / (SIZE - 1)
mask.putdata([int(m * x) for x in range(SIZE)] * SIZE)
maskt = mask.transpose(Image.TRANSPOSE)
def do_gradient():
imgt = Image.composite(tr.img, tl.img, mask)
imgb = Image.composite(br.img, bl.img, mask)
img = Image.composite(imgb, imgt, maskt)
ilabel.img = img
photo = ImageTk.PhotoImage(img)
ilabel.config(image=photo)
ilabel.photo = photo
def set_color(w, c):
w.color = c
w.config(background=c, activebackground=c)
w.img = Image.new('RGB', (SIZE, SIZE), color=c)
def show_color(w):
c = w.color
newc = askcolor(c)[1]
if newc is not None and newc != c:
set_color(w, newc)
do_gradient()
def color_button(row, column, initcolor=DEFCOLOR):
b = tk.Button(root)
b.config(command=lambda w=b:show_color(w))
set_color(b, initcolor)
b.grid(row=row, column=column)
return b
def save_image():
filetypes = [('All files', '.*'), ('PNG files', '.png')]
fname = asksaveasfilename(title="Save Image",filetypes=filetypes)
if fname:
ilabel.img.save(fname)
print('Saved image as %r' % fname)
else:
print('Cancelled')
root = tk.Tk()
root.title("Color interpolation")
coords = ((0, 0), (0, 2), (2, 0), (2, 2))
tl, tr, bl, br = [color_button(r, c) for r,c in coords]
ilabel = tk.Label(root, relief=tk.SUNKEN)
do_gradient()
ilabel.grid(row=1, column=1)
b = tk.Button(root, text="Save", command=save_image)
b.grid(row=3, column=1)
root.mainloop()