Producing 2D perlin noise with numpy

Question:

I’m trying to produce 2D perlin noise using numpy, but instead of something smooth I get this :

my broken perlin noise, with ugly squares everywhere

For sure, I’m mixing up my dimensions somewhere, probably when I combine the four gradients … But I can’t find it and my brain is melting right now. Anyone can help me pinpoint the problem ?

Anyway, here is the code:

%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt

def perlin(x,y,seed=0):
    # permutation table
    np.random.seed(seed)
    p = np.arange(256,dtype=int)
    np.random.shuffle(p)
    p = np.stack([p,p]).flatten()
    # coordinates of the first corner
    xi = x.astype(int)
    yi = y.astype(int)
    # internal coordinates
    xf = x - xi
    yf = y - yi
    # fade factors
    u = fade(xf)
    v = fade(yf)
    # noise components
    n00 = gradient(p[p[xi]+yi],xf,yf)
    n01 = gradient(p[p[xi]+yi+1],xf,yf-1)
    n11 = gradient(p[p[xi+1]+yi+1],xf-1,yf-1)
    n10 = gradient(p[p[xi+1]+yi],xf-1,yf)
    # combine noises
    x1 = lerp(n00,n10,u)
    x2 = lerp(n10,n11,u)
    return lerp(x2,x1,v)

def lerp(a,b,x):
    "linear interpolation"
    return a + x * (b-a)

def fade(t):
    "6t^5 - 15t^4 + 10t^3"
    return 6 * t**5 - 15 * t**4 + 10 * t**3

def gradient(h,x,y):
    "grad converts h to the right gradient vector and return the dot product with (x,y)"
    vectors = np.array([[0,1],[0,-1],[1,0],[-1,0]])
    g = vectors[h%4]
    return g[:,:,0] * x + g[:,:,1] * y

lin = np.linspace(0,5,100,endpoint=False)
y,x = np.meshgrid(lin,lin)

plt.imshow(perlin(x,y,seed=0))
Asked By: tgirod

||

Answers:

Thanks to Paul Panzer and a good night of sleep it works now …

import numpy as np
import matplotlib.pyplot as plt

def perlin(x, y, seed=0):
    # permutation table
    np.random.seed(seed)
    p = np.arange(256, dtype=int)
    np.random.shuffle(p)
    p = np.stack([p, p]).flatten()
    # coordinates of the top-left
    xi, yi = x.astype(int), y.astype(int)
    # internal coordinates
    xf, yf = x - xi, y - yi
    # fade factors
    u, v = fade(xf), fade(yf)
    # noise components
    n00 = gradient(p[p[xi] + yi], xf, yf)
    n01 = gradient(p[p[xi] + yi + 1], xf, yf - 1)
    n11 = gradient(p[p[xi + 1] + yi + 1], xf - 1, yf - 1)
    n10 = gradient(p[p[xi + 1] + yi], xf - 1, yf)
    # combine noises
    x1 = lerp(n00, n10, u)
    x2 = lerp(n01, n11, u)  # FIX1: I was using n10 instead of n01
    return lerp(x1, x2, v)  # FIX2: I also had to reverse x1 and x2 here

def lerp(a, b, x):
    "linear interpolation"
    return a + x * (b - a)

def fade(t):
    "6t^5 - 15t^4 + 10t^3"
    return 6 * t**5 - 15 * t**4 + 10 * t**3

def gradient(h, x, y):
    "grad converts h to the right gradient vector and return the dot product with (x,y)"
    vectors = np.array([[0, 1], [0, -1], [1, 0], [-1, 0]])
    g = vectors[h % 4]
    return g[:, :, 0] * x + g[:, :, 1] * y

# EDIT : generating noise at multiple frequencies and adding them up
p = np.zeros((100,100))
for i in range(4):
    freq = 2**i
    lin = np.linspace(0, freq, 100, endpoint=False)
    x, y = np.meshgrid(lin, lin)  # FIX3: I thought I had to invert x and y here but it was a mistake
    p = perlin(x, y, seed=87) / freq + p

plt.imshow(p, origin='upper')

EDIT(2023): this post seems to be popular, so I revisited it a bit. Before, the code was generating noise at one frequency, with a given seed.

In this new version, I’m adding noises with different frequencies and amplitudes. Here, I’m using frequencies [1,2,4,8], and the amplitude is the inverse of the frequency. That way, low frequency defines the overall shape while higher frequencies add details.

Answered By: tgirod

Seeing how you made a recent edit to your post this month, I thought I’d share a version of your code that I refactored to better understand the fundamentals of fractal noise generation:
I’ve modified the code to remove any explicit 2D related code, such that it is capable of generating noise for any dimensionality. Furthermore, I’ve also added visualizations for 1D and 3D slices of the noise, as well as a profiler for further optimization of the code. Using the profiler I added some tweaks to slightly speed up the noise generation function. However the biggest improvement could be made in not re-computing the nested permutation table lookup for the same grid position multiple times, but so far I have not found an elegant solution to this problem.

To answer the question of @Tisklon, this version of the allows you to define any gradient vectors you desire, by just altering the get_gradients function to return any gradient vectors you like. By default it returns all diagonal vectors in the chosen dimensionality.

#****************************************************************************************************
#                                               Imports                                              
#****************************************************************************************************

#--- Built-Ins ---
import itertools
from functools import cache

#--- Mathematics ---
import numpy as np

#****************************************************************************************************
#                                          Noise Generation                                          
#****************************************************************************************************

class NoiseGenerator():

    #================================================================================
    # Initialization
    #================================================================================

    def __init__(self,seed=0,nr_dimensions=2):
        self.SEED          = int(seed)
        self.NR_DIMENSIONS = int(nr_dimensions)

        self.fade_f = smootherstep

        self.compute_constants()

    def compute_constants(self):
        self.PERMUTATION_TABLE = get_permutation_table(self.SEED)
        self.CORNERS           = get_corners  (self.NR_DIMENSIONS)
        self.GRADIENTS         = get_gradients(self.NR_DIMENSIONS)

        # Extend memory, to avoid '%' operation when retrieving gradient indices!
        self.NR_GRADIENTS       = self.GRADIENTS.shape[0]
        GRADIENT_MULTIPLIER     = int(np.ceil(self.PERMUTATION_TABLE.shape[0]/self.NR_GRADIENTS))
        self.GRADIENTS_EXTENDED = np.vstack([self.GRADIENTS]*GRADIENT_MULTIPLIER)

    #================================================================================
    # Generation
    #================================================================================
    
    def fractal_noise(self,pos,octaves=8):
        noise = np.zeros(pos.shape[:-1])
        for i in range(octaves):
            freq = 2**i
            amp  = 1/freq
            noise+= self.perlin_noise(pos*freq) * amp
        return noise

    def perlin_noise(self,pos):
        pos_i     = pos.astype(int)                                               # Grid coordinates
        pos_f     = pos - pos_i                                                   # Local fractional coordinates
        gradients = {tuple(c):self.get_gradients (pos_i+c) for c in self.CORNERS} # Grid gradients               # ToDo: Remove duplicate computation!
        n         = [self.dot(gradients[tuple(c)],pos_f-c) for c in self.CORNERS] # Noise components
        pos_ff    = self.fade_f(pos_f)                                            # Fade positions
        for i in range(self.NR_DIMENSIONS):                                       # Interpolate noise
            n     = [lerp(n1,n2, pos_ff[self.filter_axis(i)]) for n1,n2 in zip(n[:len(n)//2],n[len(n)//2:])]
        return n[0]
    
    #================================================================================
    # Support Functions
    #================================================================================
    
    def get_pos_grid(self,dim=512):
        return np.moveaxis(np.mgrid[[slice(0,dim)]*self.NR_DIMENSIONS],0,self.NR_DIMENSIONS)/dim

    def get_gradients(self,pos):
        return self.GRADIENTS_EXTENDED[self.get_gradients_idx(pos)]

    def get_gradients_idx(self,pos):
        gradient_idx = pos[self.filter_axis(0)]
        for i in range(1,self.NR_DIMENSIONS):
            gradient_idx = self.PERMUTATION_TABLE[gradient_idx+pos[self.filter_axis(i)]]
        return gradient_idx

    def dot(self,a,b):
        return np.sum([a[self.filter_axis(i)]*b[self.filter_axis(i)] for i in range(self.NR_DIMENSIONS)],axis=0)

    def filter_axis(self,axis):
        SLICE_ALL = [slice(None)]*self.NR_DIMENSIONS
        return tuple(SLICE_ALL+[axis])

#================================================================================
# Support functions
#================================================================================

#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# Constants
#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

@cache
def get_permutation_table(seed=0,N=512):
    np.random.seed(seed)
    p = np.arange(N//2, dtype=int)
    np.random.shuffle(p)
    p = np.stack([p]*2).flatten()
    return p

def _get_combinations(nr_dimensions,vs):
    return np.array(list(itertools.product(*zip(*[[v]*nr_dimensions for v in vs]))))

@cache
def get_corners(nr_dimensions):
    return _get_combinations(nr_dimensions,[0,1])

@cache
def get_gradients(nr_dimensions):
    return _get_combinations(nr_dimensions,[-1,+1])

#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# Transitions
#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

def lerp(a, b, r):
    return a + r * (b - a)

def smootherstep(t):
    t2 = t**2 # Pre-compute square, for faster computation
    return t2*t * (6*t2 - 15*t + 10)

#****************************************************************************************************
#                                              Test Code                                             
#****************************************************************************************************

if __name__=="__main__":

    #--- Imports ---
    import matplotlib.pyplot as plt
    import time

    #--- Settings ---
    PROFILE       = False
    NR_DIMENSIONS = 3
    DIM           = 2**(9-NR_DIMENSIONS)

    #--- Computation ---
    t0 = time.time()

    ng  = NoiseGenerator(nr_dimensions=NR_DIMENSIONS)
    pos = ng.get_pos_grid(DIM)
    if PROFILE:
        import cProfile
        import pstats
        cProfile.run("ng.fractal_noise(pos)","pstats.ps")
        p = pstats.Stats("pstats.ps")
        p.sort_stats(pstats.SortKey.TIME).print_stats(10)
        quit()
    else:
        noise = ng.fractal_noise(pos)

    dt = time.time()-t0
    print(f"Noise generated in {dt:.1f} s")

    #--- Visualization ---
    def nd_slice(nd):
        return tuple([slice(None)]*nd+[0]*(NR_DIMENSIONS-nd))

    if NR_DIMENSIONS>=1:
        plt.figure("1D")
        plt.plot(np.arange(noise.shape[0]),noise[nd_slice(1)],color="gray")
    if NR_DIMENSIONS>=2:
        plt.figure("2D")
        plt.imshow(noise[nd_slice(2)],cmap="gray")
    if NR_DIMENSIONS>=3:
        n = noise[nd_slice(3)]
        n_norm = (n-np.min(n))/(np.max(n)-np.min(n))
        colors = np.zeros(n.shape+(4,))
        for i in range(4): colors[:,:,:,i] = n_norm
        ax = plt.figure("3D").add_subplot(projection="3d")
        ax.set(xlabel="x", ylabel="y", zlabel="z")
        ax.voxels(
            *np.indices(np.array(n.shape)+1), n,
            facecolors=colors,
            linewidth=0.5
        )
    plt.show()
Answered By: H.Basien
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.