Vector Field Generative Art with Perlin noise

Hello world

import numpy as np
import pandas as pd

import matplotlib.pyplot as plt
import seaborn as sns

from noise import pnoise2

octaves = 5
factor = 3.0

num_lines = 600
num_steps = 50
step_sizes = 0.5 * (np.arange(10) + 1.0)

seed = 42  # set random seed for reproducibility
random_state = np.random.RandomState(seed)

# resolution
h, w = 200, 200

y, x = np.ogrid[:h, :w]
X, Y = np.broadcast_arrays(x, y)

The function pnoise2 has range [-1, 1]. However, the outputs tend mostly to be centered around 0. Let’s blow up the range by some factor and squash it through the \(\tanh\) function so that it is still in the desired range.

Z = np.tanh(factor * np.vectorize(pnoise2)(x/w, y/h, octaves=octaves))  # range [-1, 1]
theta = np.pi * Z  # range [-pi, pi]

Sparsify the grid so we can later draw intelligible quiver plots.

w_factor = h_factor = 10

x_sparse = x[..., ::w_factor]
y_sparse = y[::h_factor]

X_sparse = X[::h_factor, ::w_factor]
Y_sparse = Y[::h_factor, ::w_factor]

theta_sparse = theta[::w_factor, ::w_factor]

We use use Perlin noise at every grid point as the angle (in radians).

fig, ax = plt.subplots(figsize=(10, 8))

ax.set_title(r"angle $\theta$ (rad)")

contours = ax.pcolormesh(X, Y, theta, cmap="twilight")
fig.colorbar(contours, ax=ax)

ax.set_xlabel(r"$x$")
ax.set_ylabel(r"$y$")

plt.show()
angle $\theta$ (rad)

Out:

/usr/src/app/examples/misc/plot_perlin_noise.py:60: MatplotlibDeprecationWarning: shading='flat' when X and Y have the same dimensions as C is deprecated since 3.3.  Either specify the corners of the quadrilaterals with X and Y, or pass shading='auto', 'nearest' or 'gouraud', or set rcParams['pcolor.shading'].  This will become an error two minor releases later.
  contours = ax.pcolormesh(X, Y, theta, cmap="twilight")

For sanity-check, we view the numerical values of the angles normalized by \(\pi\) on the heatmap.

data = pd.DataFrame(theta_sparse / np.pi,
                    index=y_sparse.squeeze(axis=1),
                    columns=x_sparse.squeeze(axis=0))
fig, ax = plt.subplots(figsize=(10, 8))

ax.set_title(r"angle $\theta / \pi$ (rad)")

sns.heatmap(data, annot=True, fmt=".2f", cmap="twilight", ax=ax)
ax.invert_yaxis()

ax.set_xlabel(r"$x$")
ax.set_ylabel(r"$y$")

plt.show()
angle $\theta / \pi$ (rad)

Flow field

dx = np.cos(theta_sparse)
dy = np.sin(theta_sparse)
fig, ax = plt.subplots(figsize=(10, 8))

ax.quiver(x_sparse, y_sparse, dx, dy)

ax.set_xlabel(r"$x$")
ax.set_ylabel(r"$y$")

plt.show()
plot perlin noise

Path

# TODO: `w`, `h` arguments
def line(x, y, x_lim, y_lim, step_fn, num_steps, step_size=1.0):

    x_min, x_max = x_lim
    y_min, y_max = y_lim

    if not (num_steps and x_min <= x < x_max and y_min <= y < y_max):
        return [], []

    dx, dy = step_fn(x, y, step_size=step_size)

    xs, ys = line(x=x+dx, y=y+dy, x_lim=x_lim, y_lim=y_lim, step_fn=step_fn,
                  num_steps=num_steps-1, step_size=step_size)
    xs.append(x)
    ys.append(y)

    return xs, ys
def step(x, y, step_size):

    j, i = int(x), int(y)

    dx = step_size * np.cos(theta[i, j])
    dy = step_size * np.sin(theta[i, j])

    return dx, dy
x = y = 25

xs, ys = line(x, y, x_lim=(0, w), y_lim=(0, h), step_fn=step,
              num_steps=num_steps, step_size=5.0)
_xs = np.asarray(xs[::-1])
_ys = np.asarray(ys[::-1])
fig, ax = plt.subplots(figsize=(10, 8))

ax.quiver(_xs[:-1], _ys[:-1], _xs[1:] - _xs[:-1], _ys[1:] - _ys[:-1],
          scale_units='xy', angles='xy', scale=1.0, width=3e-3, color='r')
ax.quiver(x_sparse, y_sparse, dx, dy)

ax.set_xlabel(r"$x$")
ax.set_ylabel(r"$y$")

plt.show()
plot perlin noise
dfs = []

for step_size in step_sizes:

    for lineno in range(num_lines):

        # sample starting point uniformly at random
        x = w * random_state.rand()
        y = h * random_state.rand()

        xs, ys = line(x, y, x_lim=(0, w), y_lim=(0, h), step_fn=step,
                      num_steps=num_steps, step_size=step_size)

        df = pd.DataFrame(dict(lineno=lineno, stepsize=step_size, x=xs, y=ys))
        dfs.append(df)
data = pd.concat(dfs, axis="index", sort=True)
data
lineno stepsize x y
0 0 0.5 99.311017 188.221356
1 0 0.5 98.811904 188.251132
2 0 0.5 98.312791 188.280909
3 0 0.5 97.813378 188.305127
4 0 0.5 97.313965 188.329345
... ... ... ... ...
5 599 5.0 183.639697 44.095758
6 599 5.0 183.508302 49.094031
7 599 5.0 184.229680 54.041719
8 599 5.0 185.655844 58.834010
9 599 5.0 187.717772 63.389058

215921 rows × 4 columns



fig, ax = plt.subplots(figsize=(10, 8))

sns.lineplot(x='x', y='y', hue='stepsize', units='lineno', estimator=None,
             sort=False, palette='Spectral', legend=None, linewidth=1.0,
             alpha=0.4, data=data, ax=ax)
ax.set_xlabel(r"$x$")
ax.set_ylabel(r"$y$")

plt.show()
plot perlin noise

Total running time of the script: ( 0 minutes 38.293 seconds)

Gallery generated by Sphinx-Gallery