Procedural Plants in Python — Part 2
In the previous part of this article we looked at the background to L-Systems, and how they could be used for describing self-similar biological systems. In this part we’ll look at a sample implementation of a very basic 2D L-System in Python, together with a basic PNG renderer using PyCairo.
This is an implementation of a deterministic, context-less L-System, which given the same initial conditions will always produce the same results. Whilst this won’t produce very naturalistic results for our plant system, it’s a useful first step to produce results like this:

First, we’ll define some 2D operations:
- F: Move forward LINE_LENGTH, drawing a line.
- +: Rotate clockwise THETA degrees.
- -: Rotate anti-clockwise THETA degrees.
- [: Push the current position and rotation onto a stack
- ]: Pop the current position and rotation off the stack
PyCairo is a great library for vector drawing operations and very well suited to the drawing operations we’ll be using. To use PyCairo, you simply declare a drawing surface (in this case a cairo.ImageSurface), and pass that to a context. All drawing operations will then happen on this context.
This sample implementation is also available on pastebin.
# this code is an improved version of the l-system
# sample code originally distributed with pycairo.
# originally Copyright 2003 Jesse Andrews (jdandr2 at uky.edu)
# this version Copyright 2009 Seb Potter (iamseb at iamseb.com)
# licensed under GPL
import logging
import cairo
# setup logging to just print to stdout.
# python's logging module is significantly
# better than using print for debugging
LOG_FILENAME = '/dev/stdout'
logging.basicConfig(filename=LOG_FILENAME,level=logging.DEBUG)
class Lindenmayer:
"""
A class to represent L-Systems and render them using cairo
"""
def __init__( self ):
self.width = self.height = 500 # use a 500 pixel square image
self.prod = {'[':'[','F':'F',']':']','+':'+','-':'-'} # the identity products
self.start_pos = (self.width*0.5, self.height) # we translate to the middle bottom of the image to start
self.start_angle = 180 # we rotate through 180 degrees to draw upwards - cairo's origin is top-left
self.theta = 90 # the rotation angle in degrees
self.stack = [] # this will be the stack for storing translation and orientation tuples
self.str = 'f' # the starting string, or 'axiom'
self.line_length = 5 # how far we move forward on each step
self.line_width = 2 # just controls the rendered width of the line
self.logger = logging.getLogger("flower.draw.lindenmayer") # use logging to print debugging
self.logger.setLevel(logging.DEBUG)
def addProd(self, let, prod):
"""
Add a production to the ordered list of productions to apply to the current string.
"""
self.prod[let]=prod
def iterate(self, iterations=1):
"""
Iterate over the list of productions and apply them to the string, in a loop.
The end result is the final transformed string.
"""
for i in xrange(iterations):
self.str = ''.join([ self.prod[l] for l in self.str])
self.logger.info("String is: %s" % self.str)
def line(self, ctx, len):
ctx.rel_line_to( 0, len )
def rotate(self, ctx, deg):
ctx.rotate( 2*3.141592653589793*deg/360.0 )
def draw(self, colour):
"""Render the string representation of the L-System"""
self.logger.info(colour)
# create a cairo drawing context from the provided surface.
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, self.width, self.height)
ctx = cairo.Context(surface)
# first we'll create a nice white rectangle as our background
ctx.rectangle(0, 0, self.width, self.height)
ctx.set_source_rgb(1, 1, 1)
ctx.fill()
# set the line colour, width, and make sure that cairo knows how close
# lines should be to join them
ctx.set_source_rgb(*colour)
ctx.set_line_width(self.line_width)
ctx.set_tolerance(0.1)
ctx.set_line_join(cairo.LINE_JOIN_BEVEL)
# start drawing a path, move to our start point, and rotate to
# the starting angle
ctx.new_path()
ctx.move_to(*self.start_pos)
self.logger.debug("Initial position: %s, %s" % ctx.get_current_point())
self.rotate(ctx, self.start_angle)
# this is the very simple way we iterate over the final string
# and perform a drawing operation for each symbol in the string
for c in self.str:
if c == 'F':
# move forward
self.logger.debug("f: draw a line of %s" % self.line_length)
self.line(ctx, self.line_length)
if c == '+':
# rotate clockwise
self.logger.debug("+: rotate %s" % self.theta)
self.rotate(ctx, self.theta)
if c == '-':
# rotate anti-clockwise
self.logger.debug("-: rotate -%s" % self.theta)
self.rotate(ctx, -self.theta)
if c == '[':
# push the transform and orientation onto the stack
m = ctx.get_matrix()
p = ctx.get_current_point()
self.logger.debug("[: push the matrix %s onto the stack" % m)
self.stack.append((p, m))
if c == ']':
# restore the transform and orientation from the stack
p, m = self.stack.pop()
self.logger.debug("]: pop the matrix %s off the stack" % m)
ctx.set_matrix(m)
ctx.move_to(*p)
# now draw the path created as a stroke on the context
ctx.stroke()
# write this to a png
surface.write_to_png("test.png")
self.logger.info("Wrote test.png")
def main():
colour = (0, 0.3, 0)
# setup the initial parameters for this l-system
lin = Lindenmayer()
lin.start_angle = 205
lin.start_pos = (lin.width*0.25, lin.height)
lin.str = 'X'
lin.addProd('X', 'F[+X]F[-X]+X')
lin.addProd('F', 'FF')
lin.theta = 20
# generate the final string
lin.iterate(iterations=5)
# render the string using pycairo
lin.draw(colour)
if __name__ == '__main__':
main()
In the third part of this series we’ll look at more realistic approaches to describing plant systems, including introduction of randomness through Stochastic L-Systems, and ways to create more plant-like features including leaves and flowers.