Skip to content

Add numba support to perlin2d noise generator #9

Open

Description

@andrekv17

Numba support decrease the computation time up to nearly 5 times. No much changes are required

import numpy as np
from numba import njit

@njit
def interpolant(t: np.ndarray):
    return t*t*t*(t*(t*6 - 15) + 10)

@njit
def generate_perlin_noise_2d(
        shape, res, tileable=(False, False), interpolant=interpolant
):
    """Generate a 2D numpy array of perlin noise.
    Args:
        shape: The shape of the generated array (tuple of two ints).
            This must be a multple of res.
        res: The number of periods of noise to generate along each
            axis (tuple of two ints). Note shape must be a multiple of
            res.
        tileable: If the noise should be tileable along each axis
            (tuple of two bools). Defaults to (False, False).
        interpolant: The interpolation function, defaults to
            t*t*t*(t*(t*6 - 15) + 10).
    Returns:
        A numpy array of shape shape with the generated noise.
    Raises:
        ValueError: If shape is not a multiple of res.
    """
    delta = (res[0] / shape[0], res[1] / shape[1])
    d = (shape[0] // res[0], shape[1] // res[1])
    xvals = np.arange(0,res[0], delta[0])
    yvals = np.arange(0,res[1], delta[1])
    grid = np.empty((2, len(xvals), len(yvals)))
    for j, y in enumerate(yvals):
        for i, x in enumerate(xvals):
            grid[0][i, j] = x
    yy = np.empty((len(xvals), len(yvals)))
    for i, x in enumerate(xvals):
        grid[1][i, :] = yvals
    grid = grid.transpose(1, 2, 0) % 1
    # Gradients
    angles = 2*np.pi*np.random.rand(res[0]+1, res[1]+1)
    gradients = np.dstack((np.cos(angles), np.sin(angles)))
    if tileable[0]:
        gradients[-1,:] = gradients[0,:]
    if tileable[1]:
        gradients[:,-1] = gradients[:,0]
    grad_matrix = np.empty((d[0] * gradients.shape[0], d[1] * gradients.shape[1], 2))
    for i in range(gradients.shape[0]):
        for j in range(gradients.shape[1]):
            grad_matrix[i * d[0] : (i+1) * d[0], j * d[1] : (j+1) * d[1]] = gradients[i, j]
    gradients = grad_matrix
        
    g00 = gradients[    :-d[0],    :-d[1]]
    g10 = gradients[d[0]:     ,    :-d[1]]
    g01 = gradients[    :-d[0],d[1]:     ]
    g11 = gradients[d[0]:     ,d[1]:     ]
    # Ramps
    n00 = np.sum(np.dstack((grid[:,:,0]  , grid[:,:,1]  )) * g00, 2)
    n10 = np.sum(np.dstack((grid[:,:,0]-1, grid[:,:,1]  )) * g10, 2)
    n01 = np.sum(np.dstack((grid[:,:,0]  , grid[:,:,1]-1)) * g01, 2)
    n11 = np.sum(np.dstack((grid[:,:,0]-1, grid[:,:,1]-1)) * g11, 2)
    # Interpolation
    t = interpolant(grid)
    n0 = n00*(1-t[:,:,0]) + t[:,:,0]*n10
    n1 = n01*(1-t[:,:,0]) + t[:,:,0]*n11
    t1 = (1-t[:,:,1])*n0
    t2 = t[:,:,1]*n1
    sum_t = t1 + t2
    mult_s2 = np.sqrt(2)*sum_t
    return mult_s2

@njit
def generate_fractal_noise_2d(
        shape, res, octaves=1, persistence=0.5,
        lacunarity=2, tileable=(False, False),
        interpolant=interpolant
):
    """Generate a 2D numpy array of fractal noise.
    Args:
        shape: The shape of the generated array (tuple of two ints).
            This must be a multiple of lacunarity**(octaves-1)*res.
        res: The number of periods of noise to generate along each
            axis (tuple of two ints). Note shape must be a multiple of
            (lacunarity**(octaves-1)*res).
        octaves: The number of octaves in the noise. Defaults to 1.
        persistence: The scaling factor between two octaves.
        lacunarity: The frequency factor between two octaves.
        tileable: If the noise should be tileable along each axis
            (tuple of two bools). Defaults to (False, False).
        interpolant: The, interpolation function, defaults to
            t*t*t*(t*(t*6 - 15) + 10).
    Returns:
        A numpy array of fractal noise and of shape shape generated by
        combining several octaves of perlin noise.
    Raises:
        ValueError: If shape is not a multiple of
            (lacunarity**(octaves-1)*res).
    """
    noise = np.zeros(shape)
    frequency = 1
    amplitude = 1
    for _ in range(octaves):
        noise += amplitude * generate_perlin_noise_2d(
            shape, (frequency*res[0], frequency*res[1]), tileable, interpolant
        )
        frequency *= lacunarity
        amplitude *= persistence
    return noise

Works for numba==0.53.1 and numpy==1.21.1

Activity

  1. davidsvy commented on Jul 23, 2022

    @davidsvy

    Here is the numba implementation for the 3d case:
    link

  2. pvigier commented on Jul 23, 2022

    @pvigier
    Owner

    Thank you for sharing! What speedup do you get with numba?

  3. davidsvy commented on Jul 23, 2022

    @davidsvy

    The numba code runs roughly 3 times faster on google colab compared to numpy.
    Everything is already vectorized, so:

    1. Numba cannot do much.
    2. A cuda implementation is suitable. One could consider pytorch, numba-cuda, taichi etc for more significant speedup.
  4. pvigier commented on Jul 24, 2022

    @pvigier
    Owner

    OK, that's already nice! I will add a link to this issue in the README.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions