from __future__ import annotations
import typing
from typing import Union
import torch
import numpy as np
from scipy.spatial import cKDTree
from tinycio import MonoImage
from .util import *
from .smoothstep import Smoothstep
# SPATIAL-DOMAIN
[docs]
class SpatialNoise:
"""Spatial-domain procedural noise generators."""
err_hw_pot = "height and width must be power-of-two"
err_density_zero = "density cannot be zero"
#TODO: value noise
#https://iquilezles.org/articles/gradientnoise/
# simplex noise
#https://www.shadertoy.com/view/Msf3WH
# From: https://github.com/pvigier/perlin-numpy/blob/master/perlin_numpy/perlin2d.py
# Copyright (c) 2019 Pierre Vigier
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
@classmethod
def __perlin_np(cls,
shape:tuple,
res:tuple,
tileable:tuple=(True, True),
interpolant:str='quintic_polynomial'):
"""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])
grid = np.mgrid[0:res[0]:delta[0], 0:res[1]:delta[1]].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]
gradients = gradients.repeat(d[0], 0).repeat(d[1], 1)
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 = Smoothstep.interpolate(interpolant, grid)
n0 = n00*(1-t[:,:,0]) + t[:,:,0]*n10
n1 = n01*(1-t[:,:,0]) + t[:,:,0]*n11
return np.sqrt(2)*((1-t[:,:,1])*n0 + t[:,:,1]*n1)
[docs]
@classmethod
def perlin(cls,
shape:tuple,
density:float=5.,
tileable:tuple=(True, True),
interpolant:str='quintic_polynomial') -> torch.Tensor:
"""
Generate 2D Perlin noise.
:param shape: Output shape as (height, width).
:param density: Controls frequency of the noise pattern.
:param tileable: Whether noise should tile along each axis.
:param interpolant: Interpolation function name (e.g., 'linear', 'quintic_polynomial').
:return: Tensor of shape [1, H, W] with values in [0, 1].
"""
assert density > 0., cls.err_density_zero
assert is_pot(shape[0]) and is_pot(shape[1]), cls.err_hw_pot
res = (
closest_divisor(shape[0], np.ceil(shape[0]/256.*density)),
closest_divisor(shape[1], np.ceil(shape[1]/256.*density)))
out = cls.__perlin_np(shape, res, tileable, interpolant)
return torch.from_numpy(np.expand_dims(out, 0).astype(np.float32)*0.5+0.5).clamp(0., 1.)
# From: https://github.com/pvigier/perlin-numpy/blob/master/perlin_numpy/perlin2d.py
# Copyright (c) 2019 Pierre Vigier
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
@classmethod
def __fractal_np(cls,
shape:tuple,
res:tuple,
octaves:int=1,
persistence:float=0.5,
lacunarity:int=2,
tileable:tuple=(True, True),
interpolant:str='quintic_polynomial',
turbulence:bool=False) -> np.ndarray:
"""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):
perlin = cls.__perlin_np(
shape,
(min(frequency*res[0], shape[0]), min(frequency*res[1], shape[1])),
tileable,
interpolant
)
noise += amplitude * (np.abs(perlin) if turbulence else perlin)
frequency *= lacunarity
amplitude *= persistence
return noise
[docs]
@classmethod
def fractal(cls,
shape:tuple,
density:float=5.,
octaves:int=5,
persistence:float=0.5,
lacunarity:int=2,
tileable:tuple=(True, True),
interpolant:str='quintic_polynomial') -> torch.Tensor:
"""
Generate 2D fractal noise using layered Perlin noise.
:param shape: Output shape as (height, width).
:param density: Base frequency scale.
:param octaves: Number of noise layers.
:param persistence: Amplitude falloff per octave.
:param lacunarity: Frequency multiplier per octave.
:param tileable: Whether noise should tile along each axis.
:param interpolant: Interpolation function name.
:return: Tensor of shape [1, H, W] with values in [0, 1].
"""
assert density > 0., cls.err_density_zero
assert is_pot(shape[0]) and is_pot(shape[1]), cls.err_hw_pot
res = (
closest_divisor(shape[0], np.ceil(shape[0]/256.*density)),
closest_divisor(shape[1], np.ceil(shape[1]/256.*density)))
out = cls.__fractal_np(shape, res, octaves, persistence, lacunarity, tileable, interpolant, turbulence=False)
return torch.from_numpy(np.expand_dims(out, 0).astype(np.float32)*0.5+0.5).clamp(0., 1.)
[docs]
@classmethod
def turbulence(cls,
shape:tuple,
density:float=5.,
octaves:int=5,
persistence:float=0.5,
lacunarity:int=2,
tileable:tuple=(True, True),
interpolant:str='quintic_polynomial',
ridge:bool=False) -> torch.Tensor:
"""
Generate 2D turbulence noise (absolute layered Perlin).
:param shape: Output shape as (height, width).
:param density: Base frequency scale.
:param octaves: Number of noise layers.
:param persistence: Amplitude falloff per octave.
:param lacunarity: Frequency multiplier per octave.
:param tileable: Whether noise should tile along each axis.
:param interpolant: Interpolation function name.
:param ridge: If True, applies ridge-remapping for sharper features.
:return: Tensor of shape [1, H, W] with values in [0, 1].
"""
assert density > 0., cls.err_density_zero
assert is_pot(shape[0]) and is_pot(shape[1]), cls.err_hw_pot
res = (
closest_divisor(shape[0], np.ceil(shape[0]/256.*density)),
closest_divisor(shape[1], np.ceil(shape[1]/256.*density)))
out = cls.__fractal_np(shape, res, octaves, persistence, lacunarity, tileable, interpolant, turbulence=True)
if ridge:
out = 1. - out
out = out ** 2
return torch.from_numpy(np.expand_dims(out, 0).astype(np.float32)).clamp(0., 1.)
@classmethod
def __worley_np(cls,
shape:tuple,
density:float,
tileable:tuple=(True, True)) -> np.ndarray:
height, width = shape[0], shape[1]
points = []
density = int(density)
base = [[np.random.randint(0, height), np.random.randint(0, width)] for _ in range(density)]
for h in range(3):
if not tileable[0] and h != 1: continue
for w in range(3):
if not tileable[1] and w != 1: continue
for v in range(density):
h_offset = h * height
w_offset = w * width
points.append([base[v][0] + h_offset, base[v][1] + w_offset])
coord = np.dstack(np.mgrid[0:height*3, 0:width*3])
tree = cKDTree(points)
distances = tree.query(coord, workers=-1)[0].astype(np.float32)
return distances[height:height*2, width:width*2]
[docs]
@classmethod
def worley(cls,
shape:tuple,
density:float=5.,
intensity:float=1.,
tileable:tuple=(True, True)) -> torch.Tensor:
"""
Generate 2D Worley (cellular) noise.
:param shape: Output shape as (height, width).
:param density: Number of feature points per axis.
:param intensity: Multiplier for the distance field.
:param tileable: Whether noise should tile along each axis.
:return: Tensor of shape [1, H, W] with values in [0, 1].
"""
assert density > 0., cls.err_density_zero
assert is_pot(shape[0]) and is_pot(shape[1]), cls.err_hw_pot
density *= 10
intensity = 0.01 * intensity
out = cls.__worley_np(shape, density, tileable)
return torch.from_numpy(np.expand_dims(out*intensity, 0).astype(np.float32)).clamp(0., 1.)
# SPECTRAL-DOMAIN
[docs]
class SpectralNoise:
"""
Spectral-domain procedural noise generators.
"""
[docs]
@classmethod
def noise_psd_2d(cls, height:int, width:int, psd=lambda f: torch.ones_like(f)):
"""
Generate spectral 2D noise field. Shape (height, width) with a spectral shaping function psd.
:param height: Field height.
:param width: Field width.
:param psd: a function that accepts a tensor f of shape (height, width//2+1) of frequency magnitudes
and returns a tensor of the same shape.
"""
# Generate 2D white noise in the frequency domain.
X_white = torch.fft.rfft2(torch.randn(height, width))
# Create frequency grids for the rfft2 output.
# For the first dimension, use full FFT frequencies.
fy = torch.fft.fftfreq(height, d=1.0) # shape: (height,)
# For the second dimension, use rFFT frequencies.
fx = torch.fft.rfftfreq(width, d=1.0) # shape: (width//2 + 1,)
# Build 2D grids by broadcasting.
fy_grid = fy.view(height, 1) # shape: (height, 1)
fx_grid = fx.view(1, -1) # shape: (1, width//2+1)
f_grid = torch.sqrt(fx_grid**2 + fy_grid**2) # shape: (height, width//2+1)
# Compute the spectral shaping function S and normalize its mean-square.
S = psd(f_grid)
S[0, 0] = 0 # Prevent NaN at DC
S = S / torch.sqrt(torch.mean(S ** 2, dim=(-2, -1), keepdim=True) + 1e-8) # Avoid div by zero
# Shape the white noise spectrum.
X_shaped = X_white * S
# Inverse FFT to obtain a spatial-domain noise field.
return torch.fft.irfft2(X_shaped, s=(height, width))
[docs]
@classmethod
def white(cls, height: int, width: int) -> torch.Tensor:
"""
Generate 2D white noise (flat power spectrum).
:param height: Output height.
:param width: Output width.
:return: 2D tensor of white noise with shape (height, width).
"""
return cls.noise_psd_2d(height, width, psd=lambda f: torch.ones_like(f))
[docs]
@classmethod
def pink(cls, height: int, width: int) -> torch.Tensor:
"""
Generate 2D pink noise (1/f spectrum).
:param height: Output height.
:param width: Output width.
:return: 2D tensor of pink noise with shape (height, width).
"""
return cls.noise_psd_2d(height, width, psd=lambda f: f.pow(-1))
[docs]
@classmethod
def brownian(cls, height: int, width: int) -> torch.Tensor:
"""
Generate 2D brownian (red) noise (1/f² spectrum).
:param height: Output height.
:param width: Output width.
:return: 2D tensor of brownian noise with shape (height, width).
"""
return cls.noise_psd_2d(height, width, psd=lambda f: f.pow(-2))
[docs]
@classmethod
def blue(cls, height: int, width: int) -> torch.Tensor:
"""
Generate 2D blue noise (f spectrum).
:param height: Output height.
:param width: Output width.
:return: 2D tensor of blue noise with shape (height, width).
"""
return cls.noise_psd_2d(height, width, psd=lambda f: f)
[docs]
@classmethod
def violet(cls, height: int, width: int) -> torch.Tensor:
"""
Generate 2D violet noise (f² spectrum).
:param height: Output height.
:param width: Output width.
:return: 2D tensor of violet noise with shape (height, width).
"""
return cls.noise_psd_2d(height, width, psd=lambda f: f.pow(2))