Conway’s Game of Life on images, using python

I remember playing the game of life program on Windows 95, and thinking “This isn’t a game?!” Years later, and this zero-player game is in my all-time geek hall of fame. If you’re not familiar, Life takes place on a 2d grid. At step zero, cells on the grid are determined as living or dead. In each subsequent step, new cells are defined as alive if they have 2 or 3 living neighbours, and dead if they have more or less than this. What’s amazing is that taking only two rules, endless possibilities emerge. If you haven’t seen Life in Life, do it now!SOOO META!!!

A few years back, I saw this neat little post on Hacker News showing an application of Life to images. It was in Java, so I tried to reimplement it in python. So here it is. Conway’s Game of Life on images. And if you get to the end, there’s another Meta moment… Conway’s Game of Life on Conway’s face!

title

Note: To keep the filesize down, I haven’t generated many frames of the animations here, but the code is ready and waiting.

from matplotlib import rc
from IPython.display import HTML
from matplotlib import animation
import matplotlib.pyplot as plt
import numpy as np
%matplotlib inline

## this line usually comes with a health warning, but in this case supresses an annoying deprecation 
## warning whose solution doesn't add up. See release notes to numpy 1.15 regarding sums of generators

np.warnings.filterwarnings('ignore')  
### Game of Life implementation from
## https://jakevdp.github.io/blog/2013/08/07/conways-game-of-life/



def life_step(X):
    """Game of life step using generator expressions"""
    nbrs_count = sum(np.roll(np.roll(X, i, 0), j, 1)
                     for i in (-1, 0, 1) for j in (-1, 0, 1)
                     if (i != 0 or j != 0))
    return (nbrs_count == 3) | (X & (nbrs_count == 2))

### Animating the Game of Life (from jakevdp)


from matplotlib import animation

def life_animation(X, figsize=(5,5), frames=10, interval=300, mode='loop'):
    """Produce a Game of Life Animation
    
    Parameters
    ----------
    X : array_like
        a two-dimensional numpy array showing the game board
    dpi : integer
        the number of dots per inch in the resulting animation.
        This controls the size of the game board on the screen
    frames : integer
        The number of frames to compute for the animation
    interval : float
        The time interval (in milliseconds) between frames
    mode : string
        The default mode of the animation.  Options are ['loop'|'once'|'reflect']
    """
    X = np.asarray(X)
    assert X.ndim == 2
    X = X.astype(bool)
    
    X_blank = np.zeros_like(X)
#     figsize = (X.shape[1] * 1. / dpi, X.shape[0] * 1. / dpi)

    fig = plt.figure(figsize=figsize)
    ax = fig.add_axes([0, 0, 1, 1], xticks=[], yticks=[], frameon=False)
    im = ax.imshow(X, cmap=plt.cm.binary, interpolation='nearest')
    im.set_clim(-0.05, 1)  # Make background gray

    # initialization function: plot the background of each frame
    def init():
        im.set_data(X_blank)
        return (im,)

    # animation function.  This is called sequentially
    def animate(i):
        im.set_data(animate.X)
        animate.X = life_step(animate.X)
        return (im,)
    animate.X = X

    anim = animation.FuncAnimation(fig, animate, init_func=init,
                                   frames=frames, interval=interval)
    
    return anim


np.random.seed(0)
X = np.zeros((30, 40), dtype=bool)
r = np.random.random((10, 20))
X[10:20, 10:30] = (r > 0.75)
anim = life_animation(X, frames=40, mode='once')
plt.close()        ## No matter what I tried, unless I add this line in I get an extra phantom canvas appearing!

HTML(anim.to_jshtml())


</input>
Once </input> Loop </input> Reflect </input>
## Starting with a straight line

np.random.seed(0)
X = np.zeros((60, 61), dtype=bool)
X[30, 10:52] = 1 
anim = life_animation(X, frames=10, mode='once')
plt.close()

HTML(anim.to_jshtml())

</input>
Once </input> Loop </input> Reflect </input>

How about we try some more interesting patterns, care of the amazing conwaylife.com

import requests
max_url = 'http://www.conwaylife.com/patterns/max.cells'
r = requests.get(max_url)

max_pattern = r.text.split('\r\n')[4:-1]
max_pattern
['..................O',
 '.................OOO',
 '............OOO....OO',
 '...........O..OOO..O.OO',
 '..........O...O.O..O.O',
 '..........O....O.O.O.O.OO',
 '............O....O.O...OO',
 'OOOO.....O.O....O...O.OOO',
 'O...OO.O.OOO.OO.........OO',
 'O.....OO.....O',
 '.O..OO.O..O..O.OO',
 '.......O.O.O.O.O.O.....OOOO',
 '.O..OO.O..O..O..OO.O.OO...O',
 'O.....OO...O.O.O...OO.....O',
 'O...OO.O.OO..O..O..O.OO..O',
 'OOOO.....O.O.O.O.O.O',
 '..........OO.O..O..O.OO..O',
 '.............O.....OO.....O',
 '.OO.........OO.OOO.O.OO...O',
 '..OOO.O...O....O.O.....OOOO',
 '..OO...O.O....O',
 '..OO.O.O.O.O....O',
 '.....O.O..O.O...O',
 '....OO.O..OOO..O',
 '......OO....OOO',
 '.......OOO',
 '........O']
def text_to_life(text_pattern):
    pattern = []
    max_width = 0
    
    for line in text_pattern:
        row = []
        
        for position in line:
            if position == '.':
                row.append(False)
            else:
                row.append(True)
        
        # Establish the maximum width of the array
        
        if len(row) > max_width:
            max_width = len(row)
            
        pattern.append(row)
        
        # Pad out any shorter rows back to the max length
        
    for row in pattern:
        if len(row) < max_width:
            pad_length = max_width - len(row)
            row += [0] * (pad_length)
    pattern = np.array(pattern)
    
    return pattern

max_pattern_array = text_to_life(max_pattern)

Max filler is SO cool! It seems to cover the plane in an almost pinstripe pattern, but because we’re using a wrap function when the life hits the edge of the canvas the pattern kind of peaks and then falls back into the kind of entropy that we’ve all come to expect at the heat death of our ever expanding universe…!

np.random.seed(0)
X = np.zeros((500, 500), dtype=bool)
X[200:227, 200:227] = max_pattern_array

anim = life_animation(X, frames=50, mode='once', interval=100)
plt.close()

HTML(anim.to_jshtml())

</input>
Once </input> Loop </input> Reflect </input>

Time to try it on images

Following the original inspiration from zx, we’ll kick things off with a grid of colours.

def colour_grid(size, density):
    '''
    docstring
    '''
    
    data = np.random.random(size)
    grid = np.zeros([s * density for s in size])
    
    # fill out each square in the grid with the same colour, but a finer density 
    # to enable the life cells to be more graduated
    
    for x_square in range(size[0]):
        for y_square in range(size[1]):
            local_x = density * x_square
            local_y = density * y_square
            
            for x in range(local_x, local_x + density):
                for y in range(local_y, local_y + density):
                    grid[x, y] = data[x_square, y_square]
    return grid

plt.imshow(colour_grid((10,10),50), cmap='plasma')
                    
    
    
<matplotlib.image.AxesImage at 0x7f3eb9556128>

png

size = (10,10)
density = 10

start_image = colour_grid(size, density)
# set which cells are alive at step 0
start_grid = start_image > 0.7
# set the behaviour for the life_effect 
wrap = size[0] * density
# set the number of frames you want
frames = 100


def life_effect(image, step, wrap):
    '''
    docstring
    '''
    x_cells, y_cells = np.where(step)
    new_cells = np.mod(x_cells[0] -1, wrap)
    image[step] = image[new_cells, y_cells]
    return image


def data_gen():
    cnt = 0
    step = start_grid
    image = start_image
    while cnt < frames:
        cnt+=1
        if cnt > 1:
            step = life_step(step)
            image = life_effect(image, step, wrap)
        yield step, image

fig, (ax1, ax2) = plt.subplots(1,2,figsize=(10,10))
plt.tight_layout()

# intialize image objects (one in each axes)
img = ax1.imshow(start_image, cmap='plasma')
life = ax2.imshow(start_grid, cmap='plasma')


images = [img, life]

def run(data):
    # update the data
    step, image = data

    # update the images with the new data
    images[0].set_data(step)
    images[1].set_data(image)
    return images


ani = animation.FuncAnimation(fig, run, data_gen, blit=True, interval=300,
    repeat=False)
plt.close()
HTML(ani.to_jshtml())



</input>
Once </input> Loop </input> Reflect </input>
\[Chaos + Life \to Magic\]

This time, let’s use the max_filler pattern kick things off, and change the life effect function. Instead of drawing the colour from one of our neighbours, we’ll just flick the colour somehow. What better way than the old favourite \(rx(1 - x)\), which displays chaotic behaviour when r is between 3 and 4.

def life_effect(x, r):
    return r * x * (1 - x)

life_effect = np.vectorize(life_effect)

r = 3.9

sequence = [0.1]

for i in range(99):
    sequence.append(life_effect(sequence[-1], r))
    
plt.plot(np.arange(100), sequence)
[<matplotlib.lines.Line2D at 0x7f3eb93b8780>]

png

np.shape(life_effect(start_image, r))
(100, 100)
size = (10,10)
density = 10

start_image = colour_grid(size, density)
# set which cells are alive at step 0
start_grid = np.zeros((size[0] * density, size[1] * density), dtype='bool')
start_grid[36:63, 36:63] = max_pattern_array
# set the 'chaos factor' for the life_step
r = 3.5

# set the behaviour for the life_effect 
wrap = size[0] * density
# set the number of frames you want
frames = 1000

def data_gen():
    cnt = 0
    step = start_grid
    image = start_image
    while cnt < frames:
        cnt+=1
        if cnt > 1:
            step = life_step(step)
            image[step] = life_effect(image[step], r)
        yield step, image

fig, (ax1, ax2) = plt.subplots(1,2,figsize=(10,10))
plt.tight_layout()

# intialize image objects (one in each axes)
img = ax1.imshow(start_image)
life = ax2.imshow(start_grid)


images = [img, life]

def run(data):
    # update the data
    step, image = data

    # update the images with the new data
    images[0].set_data(step)
    images[1].set_data(image)
    return images


ani = animation.FuncAnimation(fig, run, data_gen, blit=True, interval=10,
    repeat=False)
plt.close()
HTML(ani.to_jshtml())



</input>
Once </input> Loop </input> Reflect </input>

Working with photos

Matplotlib did the job for me, although in a previous version of this I’d used opencv.

import matplotlib.pyplot as plt
import matplotlib.image as mpimg
image = mpimg.imread("tree.jpg")
plt.imshow(image)
plt.show()

png

Interestingly, if you do go down the road of opencv, it seems to reverse the colours in the rgb matrix… so plt.imshow(image) interprets it as a …bgr matrix. Anyway, I digress!

import cv2
tree = cv2.imread("tree.jpg")
plt.imshow(tree, aspect='equal')


<matplotlib.image.AxesImage at 0x7f3eb80f55c0>

png

red = tree[:,:,0]
green = tree[:,:,1]
blue = tree[:,:,2]
plt.imshow(np.dstack([blue, green, red]))
<matplotlib.image.AxesImage at 0x7f3eb804ada0>

png

We’ll apply life to each of the RGB layers, and then join in the results

Views vs copies

At this point, it’s important to cast your eye over views and copies of numpy arrays. They’re different, and they’re tricky. I found this pretty helpful.

red = np.copy(image[:,:,0])
green = np.copy(image[:,:,1])
blue = np.copy(image[:,:,2])
plt.imshow(np.dstack([red, green, blue]))
<matplotlib.image.AxesImage at 0x7f3eb0035668>

png

# check we've done our copying correctly
# If we have, this should not return anything!
red.base
# Determine which cells are alive at the initial point
red_step = red < 130
green_step = green < 130
blue_step = blue < 130

temp_red = np.copy(red)
temp_green = np.copy(green)
temp_blue = np.copy(blue)
images = [image]


for i in range(100):

    red_step = life_step(red_step)
    green_step = life_step(green_step)
    blue_step = life_step(blue_step)
    red[red_step] = red[np.mod(np.where(red_step)[0] + 1,255),np.where(red_step)[1]]
    green[green_step] = green[np.mod(np.where(green_step)[0] + 1,255),np.where(green_step)[1]]
    blue[blue_step] = blue[np.mod(np.where(blue_step)[0] + 1,255),np.where(blue_step)[1]]
    
    update = np.dstack([np.copy(colour) for colour in [red, green, blue]])
    images.append(update)

fig = plt.figure(figsize=(6,6))

ims = [[plt.imshow(triplet,animated=True)] for triplet in images]

ani = animation.ArtistAnimation(fig, ims, interval=50, 
                                blit=True, repeat_delay=1000)
plt.close()
HTML(ani.to_jshtml())
Animation size has reached 21109840 bytes, exceeding the limit of 20971520.0. If you're sure you want a larger animation embedded, set the animation.embed_limit rc parameter to a larger value (in MB). This and further frames will be dropped.

</input>
Once </input> Loop </input> Reflect </input>

How about we look at 5000 iterations:

for i in range(5000):

    red_step = life_step(red_step)
    green_step = life_step(green_step)
    blue_step = life_step(blue_step)
    red[red_step] = red[np.mod(np.where(red_step)[0] + 1,255),np.where(red_step)[1]]
    green[green_step] = green[np.mod(np.where(green_step)[0] + 1,255),np.where(green_step)[1]]
    blue[blue_step] = blue[np.mod(np.where(blue_step)[0] + 1,255),np.where(blue_step)[1]]
    

update = np.dstack([np.copy(colour) for colour in [red, green, blue]])

fig = plt.figure(figsize=(10,10))
plt.imshow(update)
<matplotlib.image.AxesImage at 0x7f3eb93f3e80>

png

Conway’s Game of Life on John Conway’s face

import matplotlib.pyplot as plt
import matplotlib.image as mpimg
conway = mpimg.imread("conway.png")

fig = plt.figure(figsize=(10,10))
plt.imshow(conway)
plt.show()

png

red = np.copy(conway[:,:,0])
green = np.copy(conway[:,:,1])
blue = np.copy(conway[:,:,2])


# Determine which cells are alive at the initial point
red_step = red < 0.3
green_step = green < 0.3
blue_step = blue < 0.3

temp_red = np.copy(red)
temp_green = np.copy(green)
temp_blue = np.copy(blue)

images = [conway]
for i in range(100):

    red_step = life_step(red_step)
    green_step = life_step(green_step)
    blue_step = life_step(blue_step)
    red[red_step] = red[np.mod(np.where(red_step)[0] + 1,255),np.where(red_step)[1]]
    green[green_step] = green[np.mod(np.where(green_step)[0] + 1,255),np.where(green_step)[1]]
    blue[blue_step] = blue[np.mod(np.where(blue_step)[0] + 1,255),np.where(blue_step)[1]]
    
    update = np.dstack([np.copy(colour) for colour in [red, green, blue]])
    images.append(update)

fig = plt.figure(figsize=(10,10))
ims = [[plt.imshow(triplet,animated=True)] for i, triplet in enumerate(images) if i % 10 == 0]


ani = animation.ArtistAnimation(fig, ims, interval=50, 
                                blit=True, repeat_delay=1000)
plt.close()
HTML(ani.to_jshtml())

</input>
Once </input> Loop </input> Reflect </input>

Room for improvement

  • I can’t figure out how to control the number of frames included in the image animations. I really want to see what happens in longer term iterations, especially with max.filler

  • Deciding how to apply GoL to an image is a rabbit hole which I’d like to explore further. You can come up with a million rules about how the colour changes, whether the colour of a new cell is inherited from its parents, whether the life algorithm is applied to each of the RGB(A?) layers separately.