RedXIII | April 14, 2021, 11:11 a.m.
Piet Mondrian was one of the most influential artists of the 20th century. The Dutch painter, known for his strikingly abstract art, was the founder of the De Stijl movement, a style that favored economy and symbolism.
De Stijl and Mondrian gained international recognition through the German Bauhaus, a school of art that became famous for its principles of design.
Paintings like Broadway Boogie Woogie and Composition C are prime examples of Mondrian’s style. He often used a minimal color palette, and relied on the concept of the grid.
Mondrian was a devoted artist. He’d spend hours in his studio meticulously painting until he developed blisters on his fingers, or made himself sick from exhaustion.
With Python, and a little math, we can generate our own geometric abstract art.
By observing some of the rules present in Mondrian’s work, we can create an infinite number of paintings. No blisters required!
In order to paint with Python, we need to install a graphics library. We’ll be using PyCairo for this demonstration.
PyCairo is a free graphics library that you can use to draw shapes using Python code. Hopefully, you already have Python 3 installed on your computer.
It’s easiest to install PyCairo from the command line, like so:
pip install pycairo
Once the library is installed, we can use it in our Python programs.
Before we write any code, we’ll need to define some rules for our painting program. We need to break down the Piet Mondrian paintings into fundamental rules.
For starters, Mondrian only uses primary colors like red, yellow, and blue. These are concentrated in squares and rectangles.
Connecting the shapes are straight black lines. These lines extend from the edges of the rectangle shapes, forming their borders.
The background canvas is always white.
With these rules, we can start to imagine the algorithm for generating a Piet Mondrian painting:
We can start writing code now that we have something concrete in mind.
Let’s start with importing PyCario and drawing a white canvas. Use the import keyword to import a library in Python. We always import libraries at the top of the file.
We’ll need to import the random library too. This one comes standard with Python, so there’s no need to install it like we had to do with PyCairo.
Let’s start by drawing a white canvas that’s 500 pixels wide and 500 pixels tall.
import random
import cairoIMAGE_WIDTH = 500
IMAGE_HEIGHT = 500surface = cairo.ImageSurface(cairo.FORMAT_RGB24, IMAGE_WIDTH, IMAGE_HEIGHT)ctx = cairo.Context(surface)
ctx.rectangle(0,0,IMAGE_WIDTH,IMAGE_HEIGHT)
ctx.set_source_rgb(1,1,1)
ctx.fill()surface.write_to_png('painting.png')
Piet Mondrian relied on the concept of the grid, and so will we. By building our painting on a grid, we can ensure that everything lines up just like in Mondrian’s work.
How do we do that in Python? Well, we could try drawing some random lines and rectangles on the canvas with PyCario, but that wouldn’t give us the result we want.
To get something like a Mondrian painting, we’ll use a grid of tiles. Each tile will be a single unit in the grid.
By changing the color of these tiles, we can draw lines and rectangles.
The information for our tiles is going to be stored in a dictionary. We’ll use x and y coordinates as the key for the color information of each tile.
# mondrian.pyimport cairo
import random# the size of the image
IMAGE_WIDTH = 500
IMAGE_HEIGHT = 500# the size of the tile grid
MAP_WIDTH = 50
MAP_HEIGHT = 50# the size of each tile
TILE_SIZE = 10MAX_LINES = 15
MIN_LINES = 6
MAX_RECTS = 5
MIN_RECTS = 1tiles = {}#colors (0,white),(1,black),(2,red),(3,yellow)(4,blue)
def generate_tiles():
# build tile map
for x in range(MAP_WIDTH):
for y in range(MAP_HEIGHT):
# set every tile to white
tiles[x,y] = 0
draw_lines()
Drawing the Map
Each of our tiles is going to be a square 10x10 pixels wide. The color information will be an integer. We’ll need a function to draw the tiles to the canvas once we’ve finished generating them.
def draw_map():
# draw tile map using pycairo
surface = cairo.ImageSurface(cairo.FORMAT_RGB24, IMAGE_WIDTH, IMAGE_HEIGHT)
ctx = cairo.Context(surface)
for x in range(MAP_WIDTH):
for y in range(MAP_HEIGHT):
size = TILE_SIZE
ctx.rectangle(x*size,y*size,x+size,y+size)
if tiles[x,y] == 0:
ctx.set_source_rgb(1,1,1)
elif tiles[x,y] == 1:
ctx.set_source_rgb(0,0,0)
elif tiles[x,y] == 2:
ctx.set_source_rgb(1,0,0)
elif tiles[x,y] == 3:
ctx.set_source_rgb(1,1,0)
else:
ctx.set_source_rgb(0,0,1) ctx.fill() surface.write_to_png('mondrian.png')
Drawing Lines
We need both horizontal and vertical lines to achieve the effect we’re after. We can do this by randomly choosing pixels along the edge of the grid.
If the we want a horizontal line, we need to fill in the grid along the x axis. For the vertical, we want the y axis.
def draw_lines():
total_lines = random.randint(MIN_LINES,MAX_LINES)
print(total_lines)
for h in range(int(total_lines/2)):
y = random.randint(0,MAP_HEIGHT)
for x in range(MAP_WIDTH):
tiles[x,y] = 1
for v in range(int(total_lines/2)):
x = random.randint(0,MAP_WIDTH)
for y in range(MAP_HEIGHT):
tiles[x,y] = 1
fill_rects()
Filling the rectangles
Filling the rectangles is the most complicated part of this tutorial. We’re going to handle it in two parts.
First, we’ll deal with our fill_rect() method. This method will search the map for white pixels. If it finds one, it will fill it with a random primary color.
def fill_rects():
total_rects = random.randint(MIN_RECTS,MAX_RECTS)
max_iters = 5
for i in range(max_iters):
for r in range(total_rects):
x = random.randint(0,MAP_WIDTH-1)
y = random.randint(0,MAP_HEIGHT-1)
if tiles[x,y] == 0:
color = random.randint(2,4)
flood_recursion(x,y,0,color)
Using flood fill
The flood fill algorithm works like the “bucket” tool on any paint application. In our case, it will fill some of the white squares created by the draw_lines() method with color.
Flood recursion will spread out from the starting pixel and change any white pixels it encounters to the new color.
The flood fill method is recursive, meaning it calls on itself as it moves from tile to tile. This allows it to check each neighbor of every tile it encounters. The flood fill stops when it runs into the border of the map, or a black tile.
def flood_recursion(x,y,start_color,update_color):
width = MAP_WIDTH
height = MAP_HEIGHT
if tiles[x,y] != start_color:
return
elif tiles[x,y] == update_color:
return
else:
tiles[x,y] = update_color
neighbors = [(x-1,y),(x+1,y),(x-1,y-1),(x+1,y+1),(x-1,y+1),(x+1,y-1),(x,y-1),(x,y+1)]
for n in neighbors:
if 0 <= n[0] <= width-1 and 0 <= n[1] <= height-1:
flood_recursion(n[0],n[1],start_color,update_color)
With the flood fill complete, we have everything necessary to generate a Mondrian style painting.
You’ll find the final version of the program below.
Summary
I’ve included some variables that we can play with to vary the results. By changing the number of lines and rectangles the program allows, we can easily change the final result.
Hopefully you’ve enjoyed this lesson about Piet Mondrian, and learned something about Python in the process.
The Code
# mondrian.pyimport cairo
import random# the size of the image
IMAGE_WIDTH = 500
IMAGE_HEIGHT = 500# the size of the tile grid
MAP_WIDTH = 50
MAP_HEIGHT = 50# the size of each tile
TILE_SIZE = 10MAX_LINES = 15
MIN_LINES = 6
MAX_RECTS = 5
MIN_RECTS = 1tiles = {}
#colors (0,white),(1,black),(2,red),(3,yellow)(4,blue)
def generate_tiles():
# build tile map
for x in range(MAP_WIDTH):
for y in range(MAP_HEIGHT):
# set every tile to white
tiles[x,y] = 0
draw_lines()
def draw_lines():
total_lines = random.randint(MIN_LINES,MAX_LINES)
# print(total_lines)
for h in range(int(total_lines/2)):
y = random.randint(0,MAP_HEIGHT)
for x in range(MAP_WIDTH):
tiles[x,y] = 1
for v in range(int(total_lines/2)):
x = random.randint(0,MAP_WIDTH)
for y in range(MAP_HEIGHT):
tiles[x,y] = 1
fill_rects()
def fill_rects():
total_rects = random.randint(MIN_RECTS,MAX_RECTS)
max_iters = 5
for i in range(max_iters):
for r in range(total_rects):
x = random.randint(0,MAP_WIDTH-1)
y = random.randint(0,MAP_HEIGHT-1)
if tiles[x,y] == 0:
color = random.randint(2,4)
flood_recursion(x,y,0,color)
def flood_recursion(x,y,start_color,update_color):
width = MAP_WIDTH
height = MAP_HEIGHT
if tiles[x,y] != start_color:
return
elif tiles[x,y] == update_color:
return
else:
tiles[x,y] = update_color
neighbors = [(x-1,y),(x+1,y),(x-1,y-1),(x+1,y+1),(x-1,y+1),(x+1,y-1),(x,y-1),(x,y+1)]
for n in neighbors:
if 0 <= n[0] <= width-1 and 0 <= n[1] <= height-1:
flood_recursion(n[0],n[1],start_color,update_color)
def draw_map():
# draw tile map using pycairo
surface = cairo.ImageSurface(cairo.FORMAT_RGB24, IMAGE_WIDTH, IMAGE_HEIGHT)
ctx = cairo.Context(surface)
for x in range(MAP_WIDTH):
for y in range(MAP_HEIGHT):
size = TILE_SIZE
ctx.rectangle(x*size,y*size,x+size,y+size)
if tiles[x,y] == 0:
ctx.set_source_rgb(1,1,1)
elif tiles[x,y] == 1:
ctx.set_source_rgb(0,0,0)
elif tiles[x,y] == 2:
ctx.set_source_rgb(1,0,0)
elif tiles[x,y] == 3:
ctx.set_source_rgb(1,1,0)
else:
ctx.set_source_rgb(0,0,1) ctx.fill() surface.write_to_png('mondrian.png')
generate_tiles()
draw_map()