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:

Test render of a D0 L-System

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.

Leave a Reply:

Gravatar Image