Dataset-Maker / src /noise.py
arittrabag's picture
Deploy Dataset-Maker: torn-page non-overlapping dataset generator
a8784d9 verified
"""Vectorized value-noise field for domain warping.
We do NOT pull in a Perlin dependency: a tileable value-noise (random lattice +
bilinear upsample, summed across octaves) is enough to warp Voronoi boundaries
into organic "torn" edges, and it is pure NumPy.
Complexity: building an (H, W) field over `octaves` is
Theta(H * W * octaves) time,
Theta(H * W) space.
"""
from __future__ import annotations
import numpy as np
def _bilinear_upsample(grid: np.ndarray, out_h: int, out_w: int) -> np.ndarray:
"""Upsample a small lattice to (out_h, out_w) with bilinear interpolation."""
gh, gw = grid.shape
# Sample positions in lattice space.
ys = np.linspace(0, gh - 1, out_h)
xs = np.linspace(0, gw - 1, out_w)
y0 = np.floor(ys).astype(np.int64)
x0 = np.floor(xs).astype(np.int64)
y1 = np.minimum(y0 + 1, gh - 1)
x1 = np.minimum(x0 + 1, gw - 1)
wy = (ys - y0)[:, None]
wx = (xs - x0)[None, :]
top = grid[y0][:, x0] * (1 - wx) + grid[y0][:, x1] * wx
bot = grid[y1][:, x0] * (1 - wx) + grid[y1][:, x1] * wx
return top * (1 - wy) + bot * wy
def value_noise(
h: int,
w: int,
scale: float,
rng: np.random.Generator,
octaves: int = 3,
persistence: float = 0.5,
) -> np.ndarray:
"""Return an (h, w) float field in roughly [-1, 1].
`scale` is the wavelength in pixels of the base octave: larger -> smoother.
Octaves add finer detail (fractal/fBm), persistence weights their amplitude.
"""
field = np.zeros((h, w), dtype=np.float32)
amplitude = 1.0
total_amp = 0.0
freq_scale = max(scale, 2.0)
for _ in range(max(1, octaves)):
gh = max(2, int(np.ceil(h / freq_scale)) + 1)
gw = max(2, int(np.ceil(w / freq_scale)) + 1)
lattice = rng.uniform(-1.0, 1.0, size=(gh, gw)).astype(np.float32)
field += amplitude * _bilinear_upsample(lattice, h, w)
total_amp += amplitude
amplitude *= persistence
freq_scale = max(freq_scale * 0.5, 2.0)
return field / total_amp