HealthPoliticsEconomics | Quant Analytics | Numerics

An accelerated generator of pastel canvas

We have found in this inspiring blog post a lean engine that generates smart pastel canvas.

The algorithm starts at the left boundary where the first column is initiated with a color that oscillates randomly around a certain value. The algorithm marches now through the canvas to the right. Each pixel within a new column is computed as the mean value of the three nearest neighbouring pixels of the left column - and a certain amount of random noise is added at this step.

The results are images with a lot of atmospheric flair.

Vectorized code

We have modified the original code and replaced the two outer for-loops by vectorized forms. We use Python's slice-forms which allow for a lean writing of the array's indices and still give a fast access to the elements of the array. We have tested that earlier here.

Further we have split the initialisation into a lower and an upper part. So we get a landscape/atmosphere impression.

import sys
import numpy as np
from PIL import Image
import matplotlib.pylab as plt
import matplotlib.colors as mclr
np.set_printoptions(linewidth=200, precision=3)

def draw_pastell(nx=900, ny=1600, CL=180, rshift=3):
    mid = nx//2
    dCL = 50
    #---- start the coloring ----------
    A = np.ones((nx,ny,nz)) *CL           # initialize the image matrix
    #np.random.seed(1234)                  # initialize RNG

    #---- initialize the lower part ----
    ix = slice(0,mid-1);   iz = slice(0,nz)  # color the left boundary
    A[ix,0,iz] =  CL + np.cumsum(np.random.randint(-rshift, rshift+1, size=(mid-1,nz)),axis=0 )

    #---- initialize the upper part ----
    ix = slice(mid,nx);   iz = slice(0,nz)  # color the left boundary
    A[ix,0,iz] =  CL-dCL + np.cumsum(np.random.randint(-rshift, rshift+1, size=(nx-mid,nz)),axis=0 )

    #---- march to the right boundary -------------
    ix = slice(1,nx-1); ixm = slice(0,nx-2); ixp = slice(2,nx)
    for jy in range(1,ny):                # smear the color to the right boundary
        A[ix,jy,iz] = 0.3333*(A[ixm,jy-1,iz] + A[ix,jy-1,iz] + A[ixp,jy-1,iz]) + np.random.randint(-rshift, rshift+1, size=(nx-2,nz))

    #---- show&save grafics ---------
    im = Image.fromarray(A.astype(np.uint8)).convert('RGBA')
    fig, ax = plt.subplots(figsize=(20,14));
    fileName = 'pic_pastell_B_{}_{}.png'.format(CL,rshift)
    plt.axis('off'); plt.imshow(im)

   # nx, ny :   size of image (x,y)
   # CL     :   color level
   # rshift :   spread of random numbers
draw_pastell(nx=900, ny=1600, CL=181, rshift=3)


draw_pastell(nx=900, ny=1600, CL=151, rshift=3)


draw_pastell(nx=900, ny=1600, CL=183, rshift=3)


Code with for-loops

def draw_pastell_FOR(width=1600, height=900, CL=180, rshift=3):
    arr = np.ones((height, width,3)) * CL

    for y in range(1,height):
        arr[y,0] = arr[y-1,0] + np.random.randint(-rshift, rshift+1, size=3)

    for x in range(1, width):
        for y in range(1,height-1):
            arr[y,x] = ((arr[y-1, x-1] + arr[y,x-1] + arr[y+1,x-1])/3 ) + np.random.randint(-rshift, rshift+1, size=3)

    im = Image.fromarray(arr.astype(np.uint8)).convert('RGBA')
    filename = 'pic_pastell_FOR.png'
    fig, ax = plt.subplots(figsize=(20,20));
    plt.axis('off'); plt.imshow(im)

draw_pastell_FOR(width=1600, height=900, CL=120, rshift=3)


Comparing the speed

%timeit draw_pastell(width=1600, hight=900, CL=180, rshift=3)
1.8 s ± 23.5 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

%timeit draw_pastell_FOR(width=1600, hight=900, CL=180, rshift=3)
22.6 s ± 202 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

The difference of the runtime is about a factor 12.