from PIL import Image as im
import numpy as np
import seaborn as sns
import random
= ["rocket", "mako", "flare", "crest", "viridis", "plasma", "inferno", "magma", "cividis"]
CMPS = [sns.color_palette(cmap, as_cmap=True) for cmap in CMPS] # I found this on the Seaborn website
CMPS 1] CMPS[
3] CMPS[
Mako “makes me feel” similarly to the way the photography of the Isfahan tileset make me feel. But how can I create a tiling?
I believe the intent of بهاء الدين محمد بن حسين العاملي and other artisans is beyond my ability to grasp, so I will not place firm intent behind placing my tiles. Rather, I will place and color them randomly, then tune the process to evoke a similar feeling.
I will create an image of a fixed size, break it into tiles, color tiles with “mako” and perhaps other maps, and see how they make me feel.
= 400
siz # A way to create a new array of size s
= lambda s : [[0 for i in range(s)] for j in range(s)]
new # A way to see an array as an image
= lambda a : im.fromarray(np.array(a).astype(np.uint8))
see see(new(siz))
It would be a simple matter to break this into rectangles, but there seems a much more complex geometry at play in Isfahan. I will instead: * Select some number n of random locations * Computer which random location each pixel is closest too * Color each tile randomly. That is two randoms for those following along at home. Let’s begin
# A way to get a random point given some size s
= lambda s : (random.randint(0, s - 1), random.randint(0, s - 1))
rpt # A way to measure distance between two points. We want integers here.
= lambda a, b : int(((a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2) ** 0.5) dst
We need to test this code, so we’ll pick a random point and color the entire grid based on proximity.
Test it a few times to make sure it changes.
= 400
siz = rpt(siz)
pnt = [[dst((i,j),pnt) for i in range(siz)] for j in range(siz)]
img see(img)
It is non-obvious how to apply a color map. Here is one example. We create a grid that increases in one direction, and rather than plotting in grayscale, we plot the cmap as a function over the pixel location.
= CMPS[1]
mako 100) mako(
(0.22698081, 0.35544612, 0.60642528, 1.0)
Here’s an interesting thing - those numbers are not between 0 and 255. They are between 0 and 1. So we need to multiple by 255 to be able to see much of anything at all.
= lambda x : [255 * i for i in CMPS[1](x)]
mako 100) mako(
[57.88010655, 90.6387606, 154.6384464, 255.0]
Well, really we should be able to do this for all cmaps, so, we make a double lambda - it takes a cmap function that return 0-1 scale colors, and returns a new function that returns 0-255 scale colors.
= lambda c : lambda x : [255 * i for i in CMPS[c](x)]
scl = scl(1)
mako 100) mako(
[57.88010655, 90.6387606, 154.6384464, 255.0]
= 400
siz = [[i for i in range(siz)] for j in range(siz)]
img see(img)
= 400
siz = [[mako(i) for i in range(siz)] for j in range(siz)]
img see(img)
We can test distance.
= 400
siz = rpt(siz)
pnt = [[mako(dst((i,j),pnt)) for i in range(siz)] for j in range(siz)]
img see(img)
Let’s try adding a few points, and coloring randomly based on which is closer.
To do this, we introduce a new “thing” - a dictionary. Dictionaries relate “keys” to “values” - like words to definitions. Ours will relate random points to random colors. Not too bad, I hope.
The notation is:
d = { key_0 : val_0, key_1 : val_1 }
We can look up a value given a key:
print(d[key_0]) # prints 'val_0'
Keys can be pretty much anything except a list or another dictionary. Values can be anything at all.
We can look at only keys or only values via d.keys()
and d.values()
.
= 400
siz = [rpt(siz) for i in range(10)]
rps = {i : mako(random.randint(0, 255)) for i in rps}
rps rps.keys(), rps.values()
(dict_keys([(304, 206), (72, 206), (344, 325), (218, 264), (94, 348), (315, 272), (228, 204), (283, 189), (112, 368), (367, 31)]),
dict_values([[64.342263, 69.5368323, 137.91755325, 255.0], [67.84041675, 187.58910015, 173.1808581, 255.0], [55.4014326, 167.58291705, 171.8255025, 255.0], [53.71616055, 110.54076855000001, 160.0290852, 255.0], [53.6094303, 111.6754344, 160.26311145, 255.0], [64.7269917, 66.41437260000001, 133.33145475, 255.0], [127.40400269999999, 215.13171645, 175.39901895, 255.0], [144.83702159999999, 219.26912715, 179.69406555, 255.0], [18.89536485, 8.5637262, 14.87830905, 255.0], [55.31160885, 40.41372345, 80.82060525, 255.0]]))
We can loop over the dictionary keys the way we loop over anything else…
= [print(i) for i in rps] _
(304, 206)
(72, 206)
(344, 325)
(218, 264)
(94, 348)
(315, 272)
(228, 204)
(283, 189)
(112, 368)
(367, 31)
However, using indices is quite different. The points are indices, and the keys are the values at the index. It’s quite odd to look at this, so I grab a valid key by looping over the keys and taking the first key, then plugging that into the dictionary.
= [i for i in rps][0]
ky0
rps[ky0]# Or in a single line...
for i in rps][0]] rps[[i
[61.12383405, 80.9751225, 149.487936, 255.0]
Okay - so now we have colors associated with areas. How do we associate a pixel with an area? Well, for each pixel, we need to know the closest point. We just loop over points and find the closest.
I do this by looping over the points in the dictionary, calculating distances, and taking the minimum. But I use one trick: Besides the distance, I also save the color associated with the point. So whatever the minimum is, that’s the color I want, because it’s the color associated with the closest point (and therefore my tile)!
This checks random points. See how each finds a different color!
min([(dst(rpt(400),i), rps[i]) for i in rps])
(70, [54.12077925, 107.11620525000001, 159.3030747, 255.0])
We’ll now define a few new things - a lambda that returns a tiling given (a) a cmap, (b) a size and (c) a number of colors, and a lambda that returns a color given a tiling and a point.
= lambda c, s, n : {rpt(s) : scl(c)(random.randint(0,255)) for i in range(n)}
til 1, 400, 10) til(
{(304, 380): [103.6181178, 209.07908745, 172.59984825, 255.0],
(383, 1): [56.447590500000004, 42.003378149999996, 84.25564905, 255.0],
(365, 8): [53.1669849, 117.3129897, 161.4556164, 255.0],
(191, 307): [18.89536485, 8.5637262, 14.87830905, 255.0],
(297, 326): [52.1962458, 36.5139498, 72.31764555, 255.0],
(171, 231): [20.942772599999998, 10.221241500000001, 17.7756981, 255.0],
(59, 165): [64.0022613, 57.9121932, 117.8709246, 255.0],
(194, 158): [54.12077925, 107.11620525000001, 159.3030747, 255.0],
(162, 173): [52.76633145, 122.91452369999999, 162.7222728, 255.0],
(80, 32): [55.31160885, 40.41372345, 80.82060525, 255.0]}
= lambda t, p : min([(dst(p,i), t[i]) for i in t])[1]
col = til(1, 400, 10)
t 100,100)), col(t, (100,101)), col(t, (300,300)) col(t, (
([63.228252149999996, 55.29738495, 112.45724399999999, 255.0],
[63.228252149999996, 55.29738495, 112.45724399999999, 255.0],
[45.6050211, 29.7484887, 57.3639075, 255.0])
We put it all together to tile an image. We just need a tiling and a size. We could do without the size in fact, but it’s a bit scuffed.
= 400
siz = lambda t, s : [[col(t, (i,j)) for i in range(s)] for j in range(s)]
art = til(1, siz, 10)
t see(art(t, siz))
With 400x400 = 16000 pixels, I probably need more than 10 tiles. Let’s try 160. It took me 33 seconds, which is slow but hey, art amirite.
1, siz, 160), siz)) see(art(til(
I don’t know why I made this so hard to write:
see(art(til(1, siz, 160), siz))
Let’s make an easier function to let you test faster.
By the way, how long it takes to see something is basically height time width times number of tiles, just so you know what will take a long time.
Try different ‘c’ for color changes, and change ‘s’ and ‘n’ just in order to see something you like without waiting forever!