Psychedelic Art Using Python

This blog post follows the idea presented by Jeremy Kun in his Math and Programming Blog, "Random (psychedelic) art, and a pinch of Python." Psychedelic art has long been associated with altered states of consciousness, often characterized by vibrant colors and surreal, highly distorted visuals. While these artworks have been linked to various therapeutic and psychological explorations, this blog will not delve into those aspects. Instead, we will explore the fascinating intersection of programming and digital art.

In this post, we’ll demonstrate how Python—a versatile and widely-used programming language—can be used to create captivating psychedelic images. Through simple programming concepts and Python libraries, we can generate visuals that echo the complex patterns and vivid colors of psychedelic art.

Getting Started: Why Use Python for Digital Art?

Python is an excellent choice for digital art due to its simplicity and the rich ecosystem of libraries that support image manipulation, graphics, and data visualization. We will be using libraries such as math, Pillow, and random for creating complex patterns and effects with relatively little code (promise!).

Before you begin, I recommend you read about how an image is made and how pixels and colors are defined. This foundational knowledge will help you better understand how the code works to generate psychedelic visuals.

The art generator

The script defines several classes like SinPi, CosPi, Times, Sum, Minus, and Expo that represent various mathematical operators and functions. These classes have two critical methods:

  • __str__: Provides a string representation of the function for easy understanding and debugging.
  • eval: Computes the value of the function given the x and y coordinates.

Here's the Python code for these classes:

from random import random, choice, seed
from math import sin, cos, pi, exp
from PIL import Image
import datetime

seed()

class X:
    def eval(self, x, y):
        return x
    def __str__(self) -> str:
        return "x"

class Y:
    def eval(self, x, y):
        return y
    def __str__(self) -> str:
        return "y"

class SinPi:
    def __init__(self, prob) -> None:
        self.arg = buildExpr(prob * prob)

    def __str__(self) -> str:
        return "sin(pi*" + str(self.arg) + ")"
    
    def eval(self, x, y):
        return sin(pi * self.arg.eval(x,y))

class CosPi:
    def __init__(self, prob) -> None:
        self.arg = buildExpr(prob * prob)

    def __str__(self) -> str:
        return "cos(pi*" + str(self.arg) + ")"
    
    def eval(self, x, y):
        return cos(pi * self.arg.eval(x,y))

class Times:
    def __init__(self, prob) -> None:
        self.lhs = buildExpr(prob * prob)
        self.rhs = buildExpr(prob * prob)

    def __str__(self) -> str:
        return str(self.lhs) + "*" + str(self.rhs)
    
    def eval(self, x, y):
        return self.lhs.eval(x,y) * self.rhs.eval(x,y)
    
class Sum:
    def __init__(self, prob) -> None:
        self.lhs = buildExpr(prob * prob)
        self.rhs = buildExpr(prob * prob)
    
    def __str__(self) -> str:
        return str(self.lhs) + "+" + str(self.rhs)
    
    def eval(self, x, y): 
        return self.lhs.eval(x,y) + self.rhs.eval(x,y)

class Minus:
    def __init__(self, prob) -> None:
        self.lhs = buildExpr(prob * prob)
        self.rhs = buildExpr(prob * prob)
    
    def __str__(self) -> str:
        return str(self.lhs) + "-" + str(self.rhs)
    
    def eval(self, x, y): 
        return self.lhs.eval(x,y) - self.rhs.eval(x,y)

class Expo:
    def __init__(self, prob) -> None:
        self.arg = buildExpr(prob * prob)
    
    def __str__(self) -> str:
        return "e^(" + str(self.arg) + ")"
    
    def eval(self, x, y):
        return exp(self.arg.eval(x,y))

The buildExpr Function

The buildExpr function recursively creates a complex mathematical expression tree by randomly selecting a math function. It uses a probability (prob) that decreases with each recursion to prevent infinite loops.

def buildExpr(prob = 0.99):
    if random() < prob :
        return choice([SinPi, CosPi, Times, Sum, Minus])(prob)
    else:
        return choice([X,Y])()

Image Generation

Grayscale and Color Plotting

  • plotIntensity: This function generates a grayscale image by evaluating the mathematical expression at each point on a grid. The intensity of each pixel is determined by the result of the expression.
def plotIntensity(expr, pixelsPerUnit = 150):
    canvasWidth = 2 * pixelsPerUnit + 1
    canvas = Image.new("L", (canvasWidth, canvasWidth))
    for py in range(canvasWidth):
        for px in range(canvasWidth):
            x = float(px - pixelsPerUnit) / pixelsPerUnit
            y = -float(py - pixelsPerUnit) / pixelsPerUnit
            z = expr.eval(x,y)
            intensity = int(z * 127.5 + 127.5)
            canvas.putpixel((px,py), intensity)
    return canvas
  • plotColor: This function creates a color image by generating three separate grayscale images (for the red, green, and blue channels) and merging them.
def plotColor(redExpr, blueExpr, greenExpr, pixelsPerUnit = 150):
    redPlane = plotIntensity(redExpr, pixelsPerUnit)
    bluePlabe = plotIntensity(blueExpr, pixelsPerUnit)
    greenPlane = plotIntensity(greenExpr, pixelsPerUnit)
    return Image.merge("RGB", (redPlane, bluePlabe, greenPlane))

Generating and Saving Art

The makeImage function generates a single psychedelic image using random expressions for the red, green, and blue channels. The create_art function then saves this image to the current directory with a unique timestamp filename.

def makeImage(pixelsPerUnit = 150):
    redExpr = buildExpr()
    blueExpr = buildExpr()
    greenExpr = buildExpr()
    log = f'red = {str(redExpr)}\n\ngreen = {str(greenExpr)}\n\nblue = {str(blueExpr)}\n'
    image = plotColor(redExpr, blueExpr, greenExpr, pixelsPerUnit)
    return (log, image)

def create_art() :
    (log, img) = makeImage(300)
    print(log)
    #img.show()
    fname = datetime.datetime.now().strftime("%d-%m-%YT%H-%M-%S") + '.png'
    img.save(fp=fname)

Creating a Collage

The create_collage function creates a collage of n x n psychedelic images. It generates individual images, arranges them in a grid, and saves the collage as a single image file.

# n x n Collage
def create_collage(width=900, height=900, n=3):
    cols = n
    rows = n
    print("generating images ......")
    list_of_img = []
    for i in range(int(n*n)) :
        _, img = makeImage(300)
        list_of_img.append(img)
        print(i+1, " image(s) generated")
    collage = Image.new("RGB", (width, height), "white")
    i = 0
    x = 0
    y = 0
    for col in range(cols):
        for row in range(rows):
            print(i, x, y)
            collage.paste(list_of_img[i], (x,y))
            i += 1
            y += height // rows
        x += width // cols
        y = 0
    
    fname = "collage_" + datetime.datetime.now().strftime("%d-%m-%YT%H-%M-%S") + '.png'
    collage.save(fp=fname)

Below are a few examples of images created using the code above:

Complete Source Code

from random import random, choice, seed
from math import sin, cos, pi, exp
from PIL import Image
import datetime

seed()

class X:
    def eval(self, x, y):
        return x
    def __str__(self) -> str:
        return "x"

class Y:
    def eval(self, x, y):
        return y
    def __str__(self) -> str:
        return "y"

class SinPi:
    def __init__(self, prob) -> None:
        self.arg = buildExpr(prob * prob)

    def __str__(self) -> str:
        return "sin(pi*" + str(self.arg) + ")"
    
    def eval(self, x, y):
        return sin(pi * self.arg.eval(x,y))

class CosPi:
    def __init__(self, prob) -> None:
        self.arg = buildExpr(prob * prob)

    def __str__(self) -> str:
        return "cos(pi*" + str(self.arg) + ")"
    
    def eval(self, x, y):
        return cos(pi * self.arg.eval(x,y))

class Times:
    def __init__(self, prob) -> None:
        self.lhs = buildExpr(prob * prob)
        self.rhs = buildExpr(prob * prob)

    def __str__(self) -> str:
        return str(self.lhs) + "*" + str(self.rhs)
    
    def eval(self, x, y):
        return self.lhs.eval(x,y) * self.rhs.eval(x,y)
    
class Sum:
    def __init__(self, prob) -> None:
        self.lhs = buildExpr(prob * prob)
        self.rhs = buildExpr(prob * prob)
    
    def __str__(self) -> str:
        return str(self.lhs) + "+" + str(self.rhs)
    
    def eval(self, x, y): 
        return self.lhs.eval(x,y) + self.rhs.eval(x,y)

class Minus:
    def __init__(self, prob) -> None:
        self.lhs = buildExpr(prob * prob)
        self.rhs = buildExpr(prob * prob)
    
    def __str__(self) -> str:
        return str(self.lhs) + "-" + str(self.rhs)
    
    def eval(self, x, y): 
        return self.lhs.eval(x,y) - self.rhs.eval(x,y)

class Expo:
    def __init__(self, prob) -> None:
        self.arg = buildExpr(prob * prob)
    
    def __str__(self) -> str:
        return "e^(" + str(self.arg) + ")"
    
    def eval(self, x, y):
        return exp(self.arg.eval(x,y))

def buildExpr(prob = 0.99):
    if random() < prob :
        return choice([SinPi, CosPi, Times, Sum, Minus])(prob)
    else:
        return choice([X,Y])()

def plotIntensity(expr, pixelsPerUnit = 150):
    canvasWidth = 2 * pixelsPerUnit + 1
    canvas = Image.new("L", (canvasWidth, canvasWidth))
    for py in range(canvasWidth):
        for px in range(canvasWidth):
            x = float(px - pixelsPerUnit) / pixelsPerUnit
            y = -float(py - pixelsPerUnit) / pixelsPerUnit
            z = expr.eval(x,y)
            intensity = int(z * 127.5 + 127.5)
            canvas.putpixel((px,py), intensity)
    return canvas

def plotColor(redExpr, blueExpr, greenExpr, pixelsPerUnit = 150):
    redPlane = plotIntensity(redExpr, pixelsPerUnit)
    bluePlabe = plotIntensity(blueExpr, pixelsPerUnit)
    greenPlane = plotIntensity(greenExpr, pixelsPerUnit)
    return Image.merge("RGB", (redPlane, bluePlabe, greenPlane))

def makeImage(pixelsPerUnit = 150):
    redExpr = buildExpr()
    blueExpr = buildExpr()
    greenExpr = buildExpr()
    log = f'red = {str(redExpr)}\n\ngreen = {str(greenExpr)}\n\nblue = {str(blueExpr)}\n'
    image = plotColor(redExpr, blueExpr, greenExpr, pixelsPerUnit)
    return (log, image)

def create_art() :
    (log, img) = makeImage(300)
    print(log)
    #img.show()
    fname = datetime.datetime.now().strftime("%d-%m-%YT%H-%M-%S") + '.png'
    img.save(fp=fname)

# n x n Collage
def create_collage(width=900, height=900, n=3):
    cols = n
    rows = n
    print("generating images ......")
    list_of_img = []
    for i in range(int(n*n)) :
        _, img = makeImage(300)
        list_of_img.append(img)
        print(i+1, " image(s) generated")
    collage = Image.new("RGB", (width, height), "white")
    i = 0
    x = 0
    y = 0
    for col in range(cols):
        for row in range(rows):
            print(i, x, y)
            collage.paste(list_of_img[i], (x,y))
            i += 1
            y += height // rows
        x += width // cols
        y = 0
    
    fname = "collage_" + datetime.datetime.now().strftime("%d-%m-%YT%H-%M-%S") + '.png'
    collage.save(fp=fname)

if __name__=='__main__':
    #create_art()
    #create_collage()
    for i in range(10):
        create_art()

The beauty of this Python art generator is in its simplicity and the random beauty it creates. Feel free to tweak the expressions or parameters to see what unique patterns you can generate. Share your psychedelic art creations with us!