Creating Fractals in Python

-

An image I took earlier this yr on a very bleak day, even for England.

Fractals are infinitely complex patterns which might be self-similar across different scales. For instance, a tree trunk splits into smaller branches. These in turn split into even smaller branches, and so forth.

By generating fractals programmatically, we will turn easy shapes into complicated repeating patterns.

In this text I might be exploring how we will construct impressive fractals in Python using some basic A-Level geometry and a bit of programming know-how.

Fractals play a very important role in data science. For instance, in fractal evaluation the fractal characteristics of datasets are evaluated to assist understand the structure of underlying processes. As well as, the recurring algorithm on the centre of fractal generation might be applied to a big selection of knowledge problems, from the binary search algorithm to recurrent neural networks.

I would like to put in writing a program that may draw an equilateral triangle. On all sides of the triangle it must then find a way to attract a rather smaller outward facing triangle. It should find a way to repeat this process as persistently as I would love, hopefully creating some interesting patterns.

This rough sketch illustrates the form of pattern I would like to generate.

I might be representing a picture as a two dimensional array of pixels. Each cell within the pixel array will represent the color (RGB) of that pixel.

To realize this, we will use the libraries NumPy to generate the pixel array and Pillow to show it into a picture that we will save.

The blue pixel has an x value of three and y value of 4 and could possibly be accessed from a second array like pixels[4][3]

Now it’s time to get coding!

Firstly, I would like a function that may take two sets of coordinates and draw a line between them.

The code below works by interpolating between two points, adding recent pixels to the pixel array with each step. You may consider this process like colouring in a line pixel by pixel.

I even have used the continuation character ‘’ in each code snippet to assist fit some longer lines of code in.

import numpy as np
from PIL import Image
import math

def plot_line(from_coordinates, to_coordinates, thickness, color, pixels):

# Work out the boundaries of our pixel array
max_x_coordinate = len(pixels[0])
max_y_coordinate = len(pixels)

# The distances along the x and y axis between the two points
horizontal_distance = to_coordinates[1] - from_coordinates[1]
vertical_distance = to_coordinates[0] - from_coordinates[0]

# The full distance between the 2 points
distance = math.sqrt((to_coordinates[1] - from_coordinates[1])**2
+ (to_coordinates[0] - from_coordinates[0])**2)

# How far we'll step forwards every time we color in a recent pixel
horizontal_step = horizontal_distance/distance
vertical_step = vertical_distance/distance

# At this point, we enter the loop to attract the road in our pixel array
# Each iteration of the loop will add a recent point along our line
for i in range(round(distance)):

# These 2 coordinates are those at the middle of our line
current_x_coordinate = round(from_coordinates[1] + (horizontal_step*i))
current_y_coordinate = round(from_coordinates[0] + (vertical_step*i))

# Once we've got the coordinates of our point,
# we draw across the coordinates of size 'thickness'
for x in range (-thickness, thickness):
for y in range (-thickness, thickness):
x_value = current_x_coordinate + x
y_value = current_y_coordinate + y

if (x_value > 0 and x_value < max_x_coordinate and
y_value > 0 and y_value < max_y_coordinate):
pixels[y_value][x_value] = color

# Define the dimensions of our image
pixels = np.zeros( (500,500,3), dtype=np.uint8 )

# Draw a line
plot_line([0,0], [499,499], 1, [255,200,0], pixels)

# Turn our pixel array right into a real picture
img = Image.fromarray(pixels)

# Show our picture, and put it aside
img.show()
img.save('Line.png')

The result after I ask this function to attract a yellow line between each corner of the pixel array

Now I even have a function which might draw a line between two points, it’s time to attract the primary equilateral triangle.

Given the centre point and side length of a triangle, we will work out the peak using the handy formula: h = ½(√3a).

Now using that height, centre point and side length, I can work out where each corner of the triangle needs to be. Using the plot_line function I made earlier, I can draw a line between each corner.

def draw_triangle(center, side_length, thickness, color, pixels):

# The peak of an equilateral triangle is, h = ½(√3a)
# where 'a' is the side length
triangle_height = round(side_length * math.sqrt(3)/2)

# The highest corner
top = [center[0] - triangle_height/2, center[1]]

# Bottom left corner
bottom_left = [center[0] + triangle_height/2, center[1] - side_length/2]

# Bottom right corner
bottom_right = [center[0] + triangle_height/2, center[1] + side_length/2]

# Draw a line between each corner to finish the triangle
plot_line(top, bottom_left, thickness, color, pixels)
plot_line(top, bottom_right, thickness, color, pixels)
plot_line(bottom_left, bottom_right, thickness, color, pixels)

The result once we draw a triangle within the centre of a 500×500 pixel PNG

The stage is about. Almost the whole lot I would like is able to create my first fractal in Python. How exciting!

Nonetheless, this final step is arguably the trickiest. I would like our triangle function to call itself for all sides it has. For this to work, I would like to find a way to calculate the centre point of every of the brand new smaller triangles, and to rotate them appropriately in order that they are pointing perpendicular to the side they’re attached to.

By subtracting the offset of our centre point from the coordinates I want to rotate, after which applying the formula to rotate a pair of coordinates, we will use this function to rotate each corner of the triangle.

def rotate(coordinate, center_point, degrees):
# Subtract the purpose we're rotating around from our coordinate
x = (coordinate[0] - center_point[0])
y = (coordinate[1] - center_point[1])

# Python's cos and sin functions take radians as a substitute of degrees
radians = math.radians(degrees)

# Calculate our rotated points
new_x = (x * math.cos(radians)) - (y * math.sin(radians))
new_y = (y * math.cos(radians)) + (x * math.sin(radians))

# Add back our offset we subtracted at the start to our rotated points
return [new_x + center_point[0], new_y + center_point[1]]

A triangle where we’ve got rotated each coordinate by 35 degrees

Now I can rotate a triangle, I have to switch my focus to drawing a recent smaller triangle on all sides of the primary triangle.

To realize this, I prolonged the draw_triangle function to calculate, for every edge, the rotation and centre point of a recent triangle with a side length reduced by the parameter shrink_side_by.

Once it has calculated the centre point and rotation of the brand new triangle it calls draw_triangle (itself) to attract the brand new, smaller triangle out from the centre of the present line. This may then in turn hit the identical block of code that calculates one other set of centre points and rotations for an excellent smaller triangle.

This known as a recurring algorithm, as our draw_triangle function will now call itself until it reaches the max_depth of triangles we wish to attract. It’s necessary to have this escape clause, because otherwise the function would theoretically proceed recurring ceaselessly (but in practice the decision stack will get too large, leading to a stack overflow error)!

def draw_triangle(center, side_length, degrees_rotate, thickness, color, 
pixels, shrink_side_by, iteration, max_depth):

# The peak of an equilateral triangle is, h = ½(√3a)
# where 'a' is the side length
triangle_height = side_length * math.sqrt(3)/2

# The highest corner
top = [center[0] - triangle_height/2, center[1]]

# Bottom left corner
bottom_left = [center[0] + triangle_height/2, center[1] - side_length/2]

# Bottom right corner
bottom_right = [center[0] + triangle_height/2, center[1] + side_length/2]

if (degrees_rotate != 0):
top = rotate(top, center, degrees_rotate)
bottom_left = rotate(bottom_left, center, degrees_rotate)
bottom_right = rotate(bottom_right, center, degrees_rotate)

# Coordinates between each fringe of the triangle
lines = [[top, bottom_left],[top, bottom_right],[bottom_left, bottom_right]]

line_number = 0

# Draw a line between each corner to finish the triangle
for line in lines:
line_number += 1

plot_line(line[0], line[1], thickness, color, pixels)

# If we've not reached max_depth, draw some recent triangles
if (iteration < max_depth and (iteration < 1 or line_number < 3)):
gradient = (line[1][0] - line[0][0]) / (line[1][1] - line[0][1])

new_side_length = side_length*shrink_side_by

# Center of the road of the traingle we're drawing
center_of_line = [(line[0][0] + line[1][0]) / 2,
(line[0][1] + line[1][1]) / 2]

new_center = []
new_rotation = degrees_rotate

# Amount we want to rotate the traingle by
if (line_number == 1):
new_rotation += 60
elif (line_number == 2):
new_rotation -= 60
else:
new_rotation += 180

# In a perfect world this could be gradient == 0,
# but because of floating point division we cannot
# make sure that it will all the time be the case
if (gradient < 0.0001 and gradient > -0.0001):
if (center_of_line[0] - center[0] > 0):
new_center = [center_of_line[0] + triangle_height *
(shrink_side_by/2), center_of_line[1]]
else:
new_center = [center_of_line[0] - triangle_height *
(shrink_side_by/2), center_of_line[1]]

else:

# Calculate the conventional to the gradient of the road
difference_from_center = -1/gradient

# Calculate the space from the middle of the road
# to the middle of our recent traingle
distance_from_center = triangle_height * (shrink_side_by/2)

# Calculate the length within the x direction,
# from the middle of our line to the middle of our recent triangle
x_length = math.sqrt((distance_from_center**2)/
(1 + difference_from_center**2))

# Work out which way across the x direction must go
if (center_of_line[1] < center[1] and x_length > 0):
x_length *= -1

# Now calculate the length within the y direction
y_length = x_length * difference_from_center

# Offset the middle of the road with our recent x and y values
new_center = [center_of_line[0] + y_length,
center_of_line[1] + x_length]

draw_triangle(new_center, new_side_length, new_rotation,
thickness, color, pixels, shrink_side_by,
iteration+1, max_depth)

Triangular fractal with shrink_side_by = 1/2 and max_depth = 2

Below are some examples of various images we will generate by modifying the shrink_side_by and max_depth values input to our draw_triangle function.

It’s interesting how these large repeating patterns often create more complex shapes, similar to hexagons, but with a mesmerising symmetry.

Increasingly complex shapes begin to emerge within the symmetry of the repeating triangles.
A snowflake like fractal, using a modified version of the draw_triangle function that also drew a triangle facing inwards.
One other creation, using a smaller decrease in size with each iteration

All images unless otherwise noted are by the creator.

Fractals are great fun to mess around with and may create beautiful patterns. Using a number of easy concepts and a splash of creativity, we will generate very impressive structures.

In understanding the core properties of our fractals, and applying the recurring algorithm, we’ve created a solid foundation which can assist us understand more complex fractal problems in data science.

Be at liberty to read and download the total code here. Let me know in the event you find ways to enhance or extend it!

I ponder what you possibly can create with a distinct shape?

ASK DUKE

What are your thoughts on this topic?
Let us know in the comments below.

2 COMMENTS

0 0 votes
Article Rating
guest
2 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments

Share this article

Recent posts

2
0
Would love your thoughts, please comment.x
()
x