Source code for noisemaker.effects

"""Low-level effects library for Noisemaker"""

from __future__ import annotations

import math
from typing import Any, cast

import numpy as np
import tensorflow as tf

import noisemaker.masks as masks
import noisemaker.rng as rng
import noisemaker.simplex as simplex
import noisemaker.util as util
import noisemaker.value as value
from noisemaker.constants import (
    DistanceMetric,
    InterpolationType,
    PointDistribution,
    ValueDistribution,
    ValueMask,
    VoronoiDiagramType,
    WormBehavior,
)
from noisemaker.effects_registry import effect
from noisemaker.glyphs import load_glyphs
from noisemaker.palettes import PALETTES as palettes
from noisemaker.points import point_cloud


def _conform_kernel_to_tensor(kernel: ValueMask, tensor: tf.Tensor, shape: list[int]) -> tf.Tensor:
    """
    Re-shape a convolution kernel to match the given tensor's color dimensions.

    Args:
        kernel: Convolution kernel mask
        tensor: Input tensor to process
        shape: Shape of the tensor [height, width, channels]

    Returns:
        Processed tensor
    """

    values, _ = masks.mask_values(kernel)

    length = len(values)

    channels = shape[-1]

    temp = np.repeat(values, channels)

    temp = tf.reshape(temp, (length, length, channels, 1))

    temp = tf.cast(temp, tf.float32)

    temp /= tf.maximum(tf.reduce_max(temp), tf.reduce_min(temp) * -1)

    return temp


[docs] @effect() def erosion_worms( tensor: tf.Tensor, shape: list[int], density: float = 50, iterations: int = 50, contraction: float = 1.0, quantize: bool = False, alpha: float = 0.25, inverse: bool = False, xy_blend: bool = False, time: float = 0.0, speed: float = 1.0, ) -> tf.Tensor: """ WIP hydraulic erosion effect. .. noisemaker-live:: :effect: erosion-worms :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] density: Feature density iterations: Number of iterations to perform contraction: Contraction amount quantize: Quantize output colors alpha: Blending alpha value (0.0-1.0) inverse: Invert the effect xy_blend: XY blend amount time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ # This will never be as good as # https://www.dropbox.com/s/kqv8b3w7o8ucbyi/Beyer%20-%20implementation%20of%20a%20methode%20for%20hydraulic%20erosion.pdf?dl=0 height, width, channels = shape count = int(math.sqrt(height * width) * density) x = rng.uniform([count]) * (width - 1) y = rng.uniform([count]) * (height - 1) x_dir = rng.normal([count]) y_dir = rng.normal([count]) length = tf.sqrt(x_dir * x_dir + y_dir * y_dir) x_dir /= length y_dir /= length inertia = rng.normal([count], mean=0.75, stddev=0.25) out = tf.zeros(shape, dtype=tf.float32) # colors = tf.gather_nd(tensor, tf.cast(tf.stack([y, x], 1), tf.int32)) values = value.value_map(value.convolve(kernel=ValueMask.conv2d_blur, tensor=tensor, shape=shape), shape, keepdims=True) x_index = tf.cast(x, tf.int32) y_index = tf.cast(y, tf.int32) index = tf.stack([y_index, x_index], 1) starting_colors = tf.gather_nd(tensor, index) for i in range(iterations): x_index = tf.cast(x, tf.int32) % width y_index = tf.cast(y, tf.int32) % height index = tf.stack([y_index, x_index], 1) exposure = 1 - abs(1 - i / (iterations - 1) * 2) # Makes linear gradient [ 0 .. 1 .. 0 ] out += tf.scatter_nd(index, starting_colors * exposure, shape) x1_index = (x_index + 1) % width y1_index = (y_index + 1) % height x1_values = tf.squeeze(tf.gather_nd(values, tf.stack([y1_index, x1_index], 1))) y1_values = tf.squeeze(tf.gather_nd(values, tf.stack([y1_index, x_index], 1))) x1_y1_values = tf.squeeze(tf.gather_nd(values, tf.stack([y1_index, x1_index], 1))) u = x - tf.floor(x) v = y - tf.floor(y) sparse_values = tf.squeeze(tf.gather_nd(values, index)) g_x = value.blend(y1_values - sparse_values, x1_y1_values - x1_values, u) g_y = value.blend(x1_values - sparse_values, x1_y1_values - y1_values, v) if quantize: g_x = tf.floor(g_x) g_y = tf.floor(g_y) length = value.distance(g_x, g_y, DistanceMetric.euclidean) * contraction x_dir = value.blend(x_dir, g_x / length, inertia) y_dir = value.blend(y_dir, g_y / length, inertia) # step x = (x + x_dir) % width y = (y + y_dir) % height out = value.clamp01(out) if inverse: out = 1.0 - out if xy_blend: tensor = value.blend(shadow(tensor, shape), reindex(tensor, shape, 1), xy_blend * values) return value.blend(tensor, out, alpha)
[docs] @effect() def reindex(tensor: tf.Tensor, shape: list[int], displacement: float = 0.5, time: float = 0.0, speed: float = 1.0) -> tf.Tensor: """ Re-color the given tensor, by sampling along one axis at a specified frequency. .. noisemaker-live:: :effect: reindex :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] displacement: Displacement amount time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Processed tensor """ height, width, channels = shape reference = value.value_map(tensor, shape) mod = min(height, width) x_offset = tf.cast((reference * displacement * mod + reference) % width, tf.int32) y_offset = tf.cast((reference * displacement * mod + reference) % height, tf.int32) tensor = tf.gather_nd(tensor, tf.stack([y_offset, x_offset], 2)) return tensor
[docs] @effect() def ripple( tensor: tf.Tensor, shape: list[int], freq: int | list[int] = 2, displacement: float = 1.0, kink: float = 1.0, reference: tf.Tensor | None = None, spline_order: int | InterpolationType = InterpolationType.bicubic, time: float = 0.0, speed: float = 1.0, ) -> tf.Tensor: """ Apply displacement from pixel radian values. .. noisemaker-live:: :effect: ripple :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] freq: Noise frequency displacement: Displacement amount kink: Displacement kink amount reference: Reference tensor for comparison spline_order: Interpolation type for resampling time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ height, width, channels = shape x0_index = value.row_index(shape) y0_index = value.column_index(shape) value_shape = value.value_shape(shape) if reference is None: reference = value.values(freq=freq, shape=value_shape, spline_order=spline_order) # Twist index, borrowed from worms. TODO refactor me? index = value.value_map(reference, shape, with_normalize=False) * math.tau * kink * simplex.random(time, speed=speed) reference_x = (tf.cos(index) * displacement * width) % width reference_y = (tf.sin(index) * displacement * height) % height # Bilinear interpolation of midpoints, borrowed from refract(). TODO refactor me? x0_offsets = (tf.cast(reference_x, tf.int32) + x0_index) % width x1_offsets = (x0_offsets + 1) % width y0_offsets = (tf.cast(reference_y, tf.int32) + y0_index) % height y1_offsets = (y0_offsets + 1) % height x0_y0 = tf.gather_nd(tensor, tf.stack([y0_offsets, x0_offsets], 2)) x1_y0 = tf.gather_nd(tensor, tf.stack([y0_offsets, x1_offsets], 2)) x0_y1 = tf.gather_nd(tensor, tf.stack([y1_offsets, x0_offsets], 2)) x1_y1 = tf.gather_nd(tensor, tf.stack([y1_offsets, x1_offsets], 2)) x_fract = tf.reshape(reference_x - tf.floor(reference_x), [height, width, 1]) y_fract = tf.reshape(reference_y - tf.floor(reference_y), [height, width, 1]) x_y0 = value.blend(x0_y0, x1_y0, x_fract) x_y1 = value.blend(x0_y1, x1_y1, x_fract) return value.blend(x_y0, x_y1, y_fract)
[docs] @effect() def color_map( tensor: tf.Tensor, shape: list[int], clut: tf.Tensor | str | None = None, horizontal: bool = False, displacement: float = 0.5, time: float = 0.0, speed: float = 1.0, ) -> tf.Tensor: """ Apply a color map to an image tensor. The color map can be a photo or whatever else. .. noisemaker-live:: :effect: color-map :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] clut: Color lookup table horizontal: Apply horizontally (vs vertically) displacement: Displacement amount time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ if isinstance(clut, str): clut = util.load(clut) height, width, channels = shape reference = value.value_map(tensor, shape) * displacement x_index = (value.row_index(shape) + tf.cast(reference * (width - 1), tf.int32)) % width if horizontal: y_index = value.column_index(shape) else: y_index = (value.column_index(shape) + tf.cast(reference * (height - 1), tf.int32)) % height index = tf.stack([y_index, x_index], 2) clut = value.resample(tf.image.convert_image_dtype(clut, tf.float32, saturate=True), shape) output = tf.gather_nd(clut, index) return output
[docs] @effect() def worms( tensor: tf.Tensor, shape: list[int], behavior: int | WormBehavior = 1, density: float = 4.0, duration: float = 4.0, stride: float = 1.0, stride_deviation: float = 0.05, alpha: float = 0.5, kink: float = 1.0, drunkenness: float = 0.0, quantize: bool = False, colors: tf.Tensor | None = None, time: float = 0.0, speed: float = 1.0, ) -> tf.Tensor: """ Make a furry patch of worms which follow field flow rules. .. noisemaker-live:: :effect: worms :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] behavior: Worm behavior mode density: Feature density duration: Effect duration stride: Movement stride length stride_deviation: Stride randomization amount alpha: Blending alpha value (0.0-1.0) kink: Displacement kink amount drunkenness: Random walk amount quantize: Quantize output colors colors: Optional color tensor for rendering time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ behavior = value.coerce_enum(behavior, WormBehavior) height, width, channels = shape count = int(max(width, height) * density) worms_y = rng.uniform([count]) * (height - 1) # RNG[1] worms_x = rng.uniform([count]) * (width - 1) # RNG[2] worms_stride = rng.normal([count], mean=stride, stddev=stride_deviation) * (max(width, height) / 1024.0) # RNG[3] color_source = colors if colors is not None else tensor colors = tf.gather_nd(color_source, tf.cast(tf.stack([worms_y, worms_x], 1), tf.int32)) quarter_count = int(count * 0.25) rots: dict[WormBehavior, Any] = {} rots = { WormBehavior.obedient: lambda n: tf.ones([n], dtype=tf.float32) * rng.random() * math.tau, # RNG[4] WormBehavior.crosshatch: lambda n: rots[WormBehavior.obedient](n) + (tf.floor(rng.uniform([n]) * 100) % 4) * math.radians(90), # RNG[5] WormBehavior.unruly: lambda n: rots[WormBehavior.obedient](n) + rng.uniform([n]) * 0.25 - 0.125, # RNG[6] WormBehavior.chaotic: lambda n: rng.uniform([n]) * math.tau, # RNG[7] WormBehavior.random: lambda _: tf.reshape( tf.stack( [ rots[WormBehavior.obedient](quarter_count), rots[WormBehavior.crosshatch](quarter_count), rots[WormBehavior.unruly](quarter_count), rots[WormBehavior.chaotic](quarter_count), ] ), [count], ), # Chaotic, changing over time WormBehavior.meandering: lambda n: value.periodic_value(time * speed, rng.uniform([count])), # RNG[8] } # Ensure behavior is WormBehavior enum if isinstance(behavior, int): behavior = WormBehavior(behavior) worms_rot = rots[behavior](count) index = value.value_map(tensor, shape) * math.tau * kink iterations = int(math.sqrt(min(width, height)) * duration) out = tf.zeros(shape, dtype=tf.float32) scatter_shape = tf.shape(tensor) # Might be different than `shape` due to clut # Make worms! for i in range(iterations): if drunkenness: start = int(min(shape[0], shape[1]) * time * speed + i * speed * 10) worms_rot += (value.periodic_value(start, rng.uniform([count])) * 2.0 - 1.0) * drunkenness * math.pi worm_positions = tf.cast(tf.stack([worms_y % height, worms_x % width], 1), tf.int32) exposure = 1 - abs(1 - i / (iterations - 1) * 2) # Makes linear gradient [ 0 .. 1 .. 0 ] out += tf.scatter_nd(worm_positions, colors * exposure, scatter_shape) next_position = tf.gather_nd(index, worm_positions) + worms_rot if quantize: next_position = tf.math.round(next_position) worms_y = (worms_y + tf.cos(next_position) * worms_stride) % height worms_x = (worms_x + tf.sin(next_position) * worms_stride) % width out = tf.image.convert_image_dtype(out, tf.float32, saturate=True) return value.blend(tensor, tf.sqrt(value.normalize(out)), alpha)
[docs] @effect() def wormhole( tensor: tf.Tensor, shape: list[int], kink: float = 1.0, input_stride: float = 1.0, alpha: float = 1.0, time: float = 0.0, speed: float = 1.0 ) -> tf.Tensor: """ Apply per-pixel field flow. Non-iterative. .. noisemaker-live:: :effect: wormhole :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] kink: Displacement kink amount input_stride: Input sampling stride alpha: Blending alpha value (0.0-1.0) time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ height, width, channels = shape values = value.value_map(tensor, shape, with_normalize=False) degrees = values * math.tau * kink stride = 1024 * input_stride x_index = tf.cast(value.row_index(shape), tf.float32) y_index = tf.cast(value.column_index(shape), tf.float32) x_offset = (tf.cos(degrees) + 1) * stride y_offset = (tf.sin(degrees) + 1) * stride x = tf.cast(x_index + x_offset, tf.int32) % width y = tf.cast(y_index + y_offset, tf.int32) % height luminosity = tf.square(tf.reshape(values, [height, width, 1])) out = value.normalize(tf.scatter_nd(offset_index(y, height, x, width), tensor * luminosity, tf.shape(tensor))) return value.blend(tensor, tf.sqrt(out), alpha)
[docs] @effect() def derivative( tensor: tf.Tensor, shape: list[int], dist_metric: int | DistanceMetric = DistanceMetric.euclidean, with_normalize: bool = True, alpha: float = 1.0, time: float = 0.0, speed: float = 1.0, ) -> tf.Tensor: """ Extract a derivative from the given noise. .. noisemaker-live:: :effect: derivative :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] dist_metric: Distance metric to use with_normalize: Normalize the output alpha: Blending alpha value (0.0-1.0) time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ x = value.convolve(kernel=ValueMask.conv2d_deriv_x, tensor=tensor, shape=shape, with_normalize=False) y = value.convolve(kernel=ValueMask.conv2d_deriv_y, tensor=tensor, shape=shape, with_normalize=False) out = value.distance(x, y, dist_metric) if with_normalize: out = value.normalize(out) if alpha == 1.0: return out return value.blend(tensor, out, alpha)
[docs] @effect("sobel") def sobel_operator( tensor: tf.Tensor, shape: list[int], dist_metric: int | DistanceMetric = DistanceMetric.euclidean, time: float = 0.0, speed: float = 1.0 ) -> tf.Tensor: """ Apply a sobel operator. .. noisemaker-live:: :effect: sobel :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] dist_metric: Distance metric to use time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ tensor = value.convolve(kernel=ValueMask.conv2d_blur, tensor=tensor, shape=shape) x = value.convolve(kernel=ValueMask.conv2d_sobel_x, tensor=tensor, shape=shape, with_normalize=False) y = value.convolve(kernel=ValueMask.conv2d_sobel_y, tensor=tensor, shape=shape, with_normalize=False) out = tf.abs(value.normalize(value.distance(x, y, dist_metric)) * 2 - 1) fudge = -1 out = value.offset(out, shape, x=fudge, y=fudge) return out
[docs] @effect() def normal_map(tensor: tf.Tensor, shape: list[int], time: float = 0.0, speed: float = 1.0) -> tf.Tensor: """ Generate a tangent-space normal map. .. noisemaker-live:: :effect: normal-map :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ reference = value.value_map(tensor, shape, keepdims=True) value_shape = value.value_shape(shape) x = value.normalize(1 - value.convolve(kernel=ValueMask.conv2d_sobel_x, tensor=reference, shape=value_shape)) y = value.normalize(value.convolve(kernel=ValueMask.conv2d_sobel_y, tensor=reference, shape=value_shape)) z = 1 - tf.abs(value.normalize(tf.sqrt(x * x + y * y)) * 2 - 1) * 0.5 + 0.5 return tf.stack([x[:, :, 0], y[:, :, 0], z[:, :, 0]], 2)
[docs] @effect() def density_map(tensor: tf.Tensor, shape: list[int], time: float = 0.0, speed: float = 1.0) -> tf.Tensor: """ Create a binned pixel value density map. .. noisemaker-live:: :effect: density-map :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ height, width, channels = shape bins = max(height, width) # values = value.value_map(tensor, shape, keepdims=True) # values = tf.minimum(tf.maximum(tensor, 0.0), 1.0) # TODO: Get this to work with HDR data values = value.normalize(tensor) # https://stackoverflow.com/a/34143927 binned_values = tf.cast(tf.reshape(values * (bins - 1), [-1]), tf.int32) ones = tf.ones_like(binned_values, dtype=tf.int32) counts = tf.math.unsorted_segment_sum(ones, binned_values, bins) out = tf.gather(counts, tf.cast(values[:, :] * (bins - 1), tf.int32)) return tf.ones(shape, dtype=tf.float32) * value.normalize(tf.cast(out, tf.float32))
[docs] @effect() def jpeg_decimate(tensor: tf.Tensor, shape: list[int], iterations: int = 25, time: float = 0.0, speed: float = 1.0) -> tf.Tensor: """ Destroy an image with the power of JPEG .. noisemaker-live:: :effect: jpeg-decimate :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] iterations: Number of iterations to perform time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ jpegged = tensor for i in range(iterations): jpegged = tf.image.convert_image_dtype(jpegged, tf.uint8) data = tf.image.encode_jpeg(jpegged, quality=rng.random_int(5, 50), x_density=rng.random_int(50, 500), y_density=rng.random_int(50, 500)) jpegged = tf.image.decode_jpeg(data) jpegged = tf.image.convert_image_dtype(jpegged, tf.float32, saturate=True) return jpegged
[docs] @effect() def conv_feedback(tensor: tf.Tensor, shape: list[int], iterations: int = 50, alpha: float = 0.5, time: float = 0.0, speed: float = 1.0) -> tf.Tensor: """ Conv2d feedback loop .. noisemaker-live:: :effect: conv-feedback :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] iterations: Number of iterations to perform alpha: Blending alpha value (0.0-1.0) time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ iterations = 100 half_shape = [int(shape[0] * 0.5), int(shape[1] * 0.5), shape[2]] convolved = value.proportional_downsample(tensor, shape, half_shape) for i in range(iterations): convolved = value.convolve(kernel=ValueMask.conv2d_blur, tensor=convolved, shape=half_shape) convolved = value.convolve(kernel=ValueMask.conv2d_sharpen, tensor=convolved, shape=half_shape) convolved = value.normalize(convolved) up = tf.maximum((convolved - 0.5) * 2, 0.0) down = tf.minimum(convolved * 2, 1.0) return value.blend(tensor, value.resample(up + (1.0 - down), shape), alpha)
[docs] def blend_layers(control: list[tuple[tf.Tensor, float]], shape: list[int], feather: float = 1.0, *layers: Any) -> tf.Tensor: """ Blend multiple image layers based on a control tensor. Args: control: Control tensor for blending shape: Shape of the tensor [height, width, channels] feather: Feathering amount for blending transitions *layers: Variable number of layer tensors to blend Returns: Modified tensor """ layer_count = len(layers) control = value.normalize(control) control *= layer_count control_floor = tf.cast(control, tf.int32) x_index = value.row_index(shape) y_index = value.column_index(shape) layers = tf.stack(list(layers) + [layers[-1]]) layer_count += 1 floor_values = control_floor[:, :, 0] # I'm not sure why the mod operation is needed, but tensorflow-cpu explodes without it. combined_layer_0 = tf.gather_nd(layers, tf.stack([floor_values % layer_count, y_index, x_index], 2)) combined_layer_1 = tf.gather_nd(layers, tf.stack([(floor_values + 1) % layer_count, y_index, x_index], 2)) control_floor_fract = control - tf.floor(control) control_floor_fract = tf.minimum(tf.maximum(control_floor_fract - (1.0 - feather), 0.0) / feather, 1.0) return value.blend(combined_layer_0, combined_layer_1, control_floor_fract)
[docs] def center_mask( center: tf.Tensor, edges: tf.Tensor, shape: list[int], dist_metric: int | DistanceMetric = DistanceMetric.chebyshev, power: float = 2 ) -> tf.Tensor: """ Blend two image tensors from the center to the edges. Args: center: Center point coordinates edges: Edge handling mode shape: Shape of the tensor [height, width, channels] dist_metric: Distance metric to use power: Power curve exponent Returns: Processed tensor """ mask = tf.pow(value.singularity(None, shape, dist_metric=dist_metric), power) return value.blend(center, edges, mask)
[docs] @effect() def posterize(tensor: tf.Tensor, shape: list[int], levels: int = 9, time: float = 0.0, speed: float = 1.0) -> tf.Tensor: """ Reduce the number of color levels per channel. .. noisemaker-live:: :effect: posterize :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] levels: Number of posterization levels time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ if levels == 0: return tensor if shape[-1] == 3: tensor = util.from_srgb(tensor) tensor *= levels tensor += (1 / levels) * 0.5 tensor = tf.floor(tensor) tensor /= levels if shape[-1] == 3: tensor = util.from_linear_rgb(tensor) return tensor
[docs] def inner_tile(tensor: tf.Tensor, shape: list[int], freq: int | list[int]) -> tf.Tensor: """ Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] freq: Noise frequency Returns: Modified tensor """ if isinstance(freq, int): freq = value.freq_for_shape(freq, shape) # At this point freq is definitely list[int] assert isinstance(freq, list) freq_list: list[int] = freq small_shape = [int(shape[0] / freq_list[0]), int(shape[1] / freq_list[1]), shape[2]] y_index = tf.tile(value.column_index(small_shape) * freq_list[0], [freq_list[0], freq_list[0]]) x_index = tf.tile(value.row_index(small_shape) * freq_list[1], [freq_list[0], freq_list[0]]) tiled = tf.gather_nd(tensor, tf.stack([y_index, x_index], 2)) tiled = value.resample(tiled, shape, spline_order=InterpolationType.linear) return tiled
[docs] def expand_tile(tensor: tf.Tensor, input_shape: list[int], output_shape: list[int], with_offset: bool = True) -> tf.Tensor: """ Args: tensor: Input tensor to process input_shape: Shape of input tensor output_shape: Shape of output tensor with_offset: Apply offset to output Returns: Modified tensor """ input_width = input_shape[1] input_height = input_shape[0] if with_offset: x_offset = tf.cast(input_shape[1] / 2, tf.int32) y_offset = tf.cast(input_shape[0] / 2, tf.int32) else: x_offset = 0 y_offset = 0 x_index = (x_offset + value.row_index(output_shape)) % input_width y_index = (y_offset + value.column_index(output_shape)) % input_height return tf.gather_nd(tensor, tf.stack([y_index, x_index], 2))
[docs] def offset_index(y_index: tf.Tensor, height: int, x_index: tf.Tensor, width: int) -> tf.Tensor: """ Offset X and Y displacement channels from each other, to help with diagonal banding. Returns a combined Tensor with shape [height, width, 2] Args: y_index: Y coordinate indices height: Height dimension x_index: X coordinate indices width: Width dimension Returns: Processed tensor """ index = tf.stack( [ (y_index + int(height * 0.5 + rng.random() * height * 0.5)) % height, (x_index + int(rng.random() * width * 0.5)) % width, ], 2, ) return tf.cast(index, tf.int32)
[docs] @effect() def warp( tensor: tf.Tensor, shape: list[int], freq: int | list[int] = 2, octaves: int = 5, displacement: float = 1, spline_order: int | InterpolationType = InterpolationType.bicubic, warp_map: tf.Tensor | None = None, signed_range: bool = True, time: float = 0.0, speed: float = 1.0, ) -> tf.Tensor: """ Multi-octave warp effect .. noisemaker-live:: :effect: warp :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] freq: Noise frequency octaves: Number of octave layers displacement: Displacement amount spline_order: Interpolation type for resampling warp_map: Optional warp displacement map signed_range: Use signed range (-1 to 1) time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ if isinstance(freq, int): freq = value.freq_for_shape(freq, shape) for octave in range(1, octaves + 1): multiplier = 2**octave freq_list = freq if isinstance(freq, list) else [freq, freq] base_freq = [int(f * 0.5 * multiplier) for f in freq_list] if base_freq[0] >= shape[0] or base_freq[1] >= shape[1]: break kwargs = {} if warp_map is not None: if isinstance(warp_map, str): warp_map = tf.image.convert_image_dtype(util.load(warp_map), tf.float32) kwargs["reference_x"] = warp_map else: kwargs["warp_freq"] = base_freq tensor = value.refract( tensor, shape, displacement=displacement / multiplier, spline_order=spline_order, signed_range=signed_range, time=time, speed=speed, **kwargs ) return tensor
[docs] def sobel(tensor: tf.Tensor, shape: list[int], dist_metric: int | DistanceMetric = 1, rgb: bool = False) -> tf.Tensor: """ Colorized sobel edges. Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] dist_metric: Distance metric to use rgb: Treat as RGB (vs grayscale) Returns: Modified tensor """ if rgb: return sobel_operator(tensor, shape, dist_metric) else: return outline(tensor, shape, dist_metric, True)
[docs] @effect() def outline( tensor: tf.Tensor, shape: list[int], sobel_metric: int | DistanceMetric = 1, invert: bool = False, time: float = 0.0, speed: float = 1.0 ) -> tf.Tensor: """ Superimpose sobel operator results (cartoon edges) .. noisemaker-live:: :effect: outline :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] sobel_metric: Distance metric for Sobel operator invert: Invert the effect time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ height, width, channels = shape value_shape = value.value_shape(shape) values = value.value_map(tensor, shape, keepdims=True) edges = sobel_operator(values, value_shape, dist_metric=sobel_metric) if invert: edges = 1.0 - edges return edges * tensor
[docs] @effect() def glowing_edges( tensor: tf.Tensor, shape: list[int], sobel_metric: int | DistanceMetric = 2, alpha: float = 1.0, time: float = 0.0, speed: float = 1.0 ) -> tf.Tensor: """ .. noisemaker-live:: :effect: glowing-edges :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] sobel_metric: Distance metric for Sobel operator alpha: Blending alpha value (0.0-1.0) time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ height, width, channels = shape value_shape = value.value_shape(shape) edges = value.value_map(tensor, shape, keepdims=True) edges = posterize(edges, value_shape, rng.random_int(3, 5)) edges = 1.0 - sobel_operator(edges, value_shape, dist_metric=sobel_metric) edges = tf.minimum(edges * 8, 1.0) * tf.minimum(tensor * 1.25, 1.0) edges = bloom(edges, shape, alpha=0.5) edges = value.normalize(edges + value.convolve(kernel=ValueMask.conv2d_blur, tensor=edges, shape=shape)) return value.blend(tensor, 1.0 - ((1.0 - edges) * (1.0 - tensor)), alpha)
[docs] @effect() def vortex(tensor: tf.Tensor, shape: list[int], displacement: float = 64.0, time: float = 0.0, speed: float = 1.0) -> tf.Tensor: """ Vortex tiling effect .. noisemaker-live:: :effect: vortex :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] displacement: Displacement amount time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ value_shape = value.value_shape(shape) displacement_map = value.singularity(None, value_shape) displacement_map = value.normalize(displacement_map) x = value.convolve(kernel=ValueMask.conv2d_deriv_x, tensor=displacement_map, shape=value_shape, with_normalize=False) y = value.convolve(kernel=ValueMask.conv2d_deriv_y, tensor=displacement_map, shape=value_shape, with_normalize=False) fader = value.singularity(None, value_shape, dist_metric=DistanceMetric.chebyshev, inverse=True) fader = value.normalize(fader) x *= fader y *= fader warped = value.refract(tensor, shape, displacement=simplex.random(time, speed=speed) * 100 * displacement, reference_x=x, reference_y=y, signed_range=False) return warped
[docs] @effect() def aberration(tensor: tf.Tensor, shape: list[int], displacement: float = 0.005, time: float = 0.0, speed: float = 1.0) -> tf.Tensor: """ Chromatic aberration .. noisemaker-live:: :effect: aberration :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] displacement: Displacement amount time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ height, width, channels = shape if channels != 3: # aye cannit doit return tensor x_index = value.row_index(shape) y_index = value.column_index(shape) x_index_float = tf.cast(x_index, tf.float32) separated = [] displacement_pixels = int(width * displacement * simplex.random(time, speed=speed)) mask = tf.pow(tf.squeeze(value.singularity(None, [shape[0], shape[1], 1])), 3) gradient = value.normalize(x_index_float) shift = rng.random() * 0.1 - 0.05 tensor = tf.image.adjust_hue(tensor, shift) for i in range(channels): # Left and right neighbor pixels if i == 0: # Left (red) offset_x_index = tf.minimum(x_index + displacement_pixels, width - 1) elif i == 1: # Center (green) offset_x_index = x_index elif i == 2: # Right (blue) offset_x_index = tf.maximum(x_index - displacement_pixels, 0) # return tf.expand_dims(offset_x_index, axis=2) offset_x_index = tf.cast(offset_x_index, tf.float32) # Left and right image sides if i == 0: # Left (red) offset_x_index = value.blend(offset_x_index, x_index_float, gradient) elif i == 2: # Right (blue) offset_x_index = value.blend(x_index_float, offset_x_index, gradient) # Fade effect towards center offset_x_index = tf.cast(value.blend_cosine(x_index_float, offset_x_index, mask), tf.int32) separated.append(tf.gather_nd(tensor[:, :, i], tf.stack([y_index, offset_x_index], 2))) tensor = tf.stack(separated, 2) # Restore original colors return tf.image.adjust_hue(tensor, -shift)
[docs] @effect() def bloom(tensor: tf.Tensor, shape: list[int], alpha: float = 0.5, time: float = 0.0, speed: float = 1.0) -> tf.Tensor: """ Bloom effect Input image must currently be square (sorry). .. noisemaker-live:: :effect: bloom :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] alpha: Blending alpha value (0.0-1.0) time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ height, width, channels = shape blurred = value.clamp01(tensor * 2.0 - 1.0) blurred = value.proportional_downsample(blurred, shape, [max(int(height / 100), 1), max(int(width / 100), 1), channels]) * 4.0 blurred = value.resample(blurred, shape) blurred = value.offset(blurred, shape, x=int(tf.cast(width, tf.float32) * -0.05), y=int(tf.cast(shape[0], tf.float32) * -0.05)) # Mirror the JavaScript bloom implementation exactly: brightness is a straight # addition followed by clamping to ``[-1, 1]`` before the contrast stretch is # applied. Using the TensorFlow helpers here introduces small numerical # differences, so we implement the arithmetic directly to stay bit-for-bit in # sync with the reference. blurred = tf.clip_by_value(blurred + 0.25, -1.0, 1.0) mean = tf.reduce_mean(blurred, axis=[0, 1], keepdims=True) blurred = (blurred - mean) * 1.5 + mean blurred = value.clamp01(blurred) return value.blend(value.clamp01(tensor), value.clamp01((tensor + blurred) * 0.5), alpha)
[docs] @effect() def dla( tensor: tf.Tensor, shape: list[int], padding: int = 2, seed_density: float = 0.01, density: float = 0.125, xy: tuple[tf.Tensor, tf.Tensor, int] | None = None, alpha: float = 1.0, time: float = 0.0, speed: float = 1.0, ) -> tf.Tensor: """ Diffusion-limited aggregation. Renders with respect to the `time` param (0..1) .. noisemaker-live:: :effect: dla :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] padding: Edge padding amount seed_density: Density of seed points density: Feature density xy: Optional XY coordinates for point cloud alpha: Blending alpha value (0.0-1.0) time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ height, width, channels = shape # Nearest-neighbor map for affixed nodes, lets us miss with one lookup instead of eight neighborhoods = set() # Nearest-neighbor map of neighbor map, lets us skip nodes which are too far away to matter expanded_neighborhoods = set() # Actual affixed nodes clustered = [] # Not-affixed nodes walkers = [] scale = 1 / padding half_width = int(width * scale) half_height = int(height * scale) if xy is None: seed_count = int(math.sqrt(int(half_height * seed_density) or 1)) result = point_cloud(seed_count, distrib=PointDistribution.random, shape=shape, time=time, speed=speed) if result is None: raise ValueError("point_cloud returned None") x, y = result else: x, y, seed_count = xy walkers_count = half_height * half_width * density walkers_per_seed = int(walkers_count / seed_count) offsets = [-1, 0, 1] expanded_range = 8 expanded_offsets = range(-expanded_range, expanded_range + 1) for i in range(seed_count): node = (int(y[i] * scale), int(x[i] * scale)) clustered.append(node) for x_offset in offsets: for y_offset in offsets: neighborhoods.add((node[0] + y_offset, node[1] + x_offset)) for x_offset in expanded_offsets: for y_offset in expanded_offsets: expanded_neighborhoods.add((node[0] + y_offset, node[1] + x_offset)) for i in range(walkers_per_seed): walkers.append((int(rng.random() * half_height), int(rng.random() * half_width))) iterations = int(math.sqrt(walkers_count) * time * time) for i in range(iterations): remove_walkers = set() for walker in walkers: if walker in neighborhoods: remove_walkers.add(walker) # Remove all occurrences walkers = [walker for walker in walkers if walker not in remove_walkers] for walker in remove_walkers: for x_offset in offsets: for y_offset in offsets: # tensorflowification - use a conv2d here neighborhoods.add(((walker[0] + y_offset) % half_height, (walker[1] + x_offset) % half_width)) for x_offset in expanded_offsets: for y_offset in expanded_offsets: expanded_neighborhoods.add(((walker[0] + y_offset) % half_height, (walker[1] + x_offset) % half_width)) clustered.append(walker) if not walkers: break for w in range(len(walkers)): walker = walkers[w] if walker in expanded_neighborhoods: walkers[w] = ( (walker[0] + offsets[rng.random_int(0, len(offsets) - 1)]) % half_height, (walker[1] + offsets[rng.random_int(0, len(offsets) - 1)]) % half_width, ) else: walkers[w] = ( (walker[0] + expanded_offsets[rng.random_int(0, len(expanded_offsets) - 1)]) % half_height, (walker[1] + expanded_offsets[rng.random_int(0, len(expanded_offsets) - 1)]) % half_width, ) seen = set() unique = [] for c in clustered: if c in seen: continue seen.add(c) unique.append(c) count = len(unique) # hot = tf.ones([count, channels]) hot = tf.ones([count, channels], dtype=tf.float32) * tf.cast(tf.reshape(tf.stack(list(reversed(range(count)))), [count, 1]), tf.float32) out = value.convolve(kernel=ValueMask.conv2d_blur, tensor=tf.scatter_nd(tf.stack(unique) * int(1 / scale), hot, [height, width, channels]), shape=shape) return value.blend(tensor, out * tensor, alpha)
[docs] @effect() def wobble(tensor: tf.Tensor, shape: list[int], time: float = 0.0, speed: float = 1.0) -> tf.Tensor: """ Move the entire image around .. noisemaker-live:: :effect: wobble :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ x_offset = tf.cast(simplex.random(time=time, speed=speed * 0.5) * shape[1], tf.int32) y_offset = tf.cast(simplex.random(time=time, speed=speed * 0.5) * shape[0], tf.int32) return value.offset(tensor, shape, x=x_offset, y=y_offset)
[docs] @effect() def reverb(tensor: tf.Tensor, shape: list[int], octaves: int = 2, iterations: int = 1, ridges: bool = True, time: float = 0.0, speed: float = 1.0) -> tf.Tensor: """ Multi-octave "reverberation" of input image tensor .. noisemaker-live:: :effect: reverb :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] octaves: Number of octave layers iterations: Number of iterations to perform ridges: Apply ridge transformation time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ if not octaves: return tensor height, width, channels = shape if ridges: reference = 1.0 - tf.abs(tensor * 2 - 1) else: reference = tensor out = reference for i in range(iterations): for octave in range(1, octaves + 1): multiplier = 2**octave octave_shape = [int(height / multiplier) or 1, int(width / multiplier) or 1, channels] if not all(octave_shape): break layer = value.proportional_downsample(reference, shape, octave_shape) out += expand_tile(layer, octave_shape, shape) / multiplier return value.normalize(out)
[docs] @effect() def light_leak(tensor: tf.Tensor, shape: list[int], alpha: float = 0.25, time: float = 0.0, speed: float = 1.0) -> tf.Tensor: """ .. noisemaker-live:: :effect: light-leak :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] alpha: Blending alpha value (0.0-1.0) time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ result = point_cloud( 6, distrib=PointDistribution.grid_members()[rng.random_int(0, len(PointDistribution.grid_members()) - 1)], drift=0.05, shape=shape, time=time, speed=speed, ) if result is None: raise ValueError("point_cloud returned None") x, y = result leak = value.voronoi(tensor, shape, diagram_type=VoronoiDiagramType.color_regions, xy=(x, y, len(x))) leak = wormhole(leak, shape, kink=1.0, input_stride=0.25) leak = bloom(leak, shape, 1.0) leak = 1 - ((1 - tensor) * (1 - leak)) leak = center_mask(tensor, leak, shape, 4) return vaseline(value.blend(tensor, leak, alpha), shape, alpha)
[docs] @effect() def vignette(tensor: tf.Tensor, shape: list[int], brightness: float = 0.0, alpha: float = 1.0, time: float = 0.0, speed: float = 1.0) -> tf.Tensor: """ .. noisemaker-live:: :effect: vignette :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] brightness: Brightness adjustment alpha: Blending alpha value (0.0-1.0) time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ tensor = value.normalize(tensor) edges = center_mask(tensor, tf.ones(shape, dtype=tf.float32) * brightness, shape, dist_metric=DistanceMetric.euclidean) return value.blend(tensor, edges, alpha)
[docs] @effect() def vaseline(tensor: tf.Tensor, shape: list[int], alpha: float = 1.0, time: float = 0.0, speed: float = 1.0) -> tf.Tensor: """ .. noisemaker-live:: :effect: vaseline :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] alpha: Blending alpha value (0.0-1.0) time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ return value.blend(tensor, center_mask(tensor, bloom(tensor, shape, 1.0), shape), alpha)
[docs] @effect() def shadow(tensor: tf.Tensor, shape: list[int], alpha: float = 1.0, reference: tf.Tensor | None = None, time: float = 0.0, speed: float = 1.0) -> tf.Tensor: """ Convolution-based self-shadowing effect. .. noisemaker-live:: :effect: shadow :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] alpha: Blending alpha value (0.0-1.0) reference: Reference tensor for comparison time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ height, width, channels = shape if reference is None: reference = tensor reference = value.value_map(reference, shape, keepdims=True) value_shape = value.value_shape(shape) x = value.convolve(kernel=ValueMask.conv2d_sobel_x, tensor=reference, shape=value_shape) y = value.convolve(kernel=ValueMask.conv2d_sobel_y, tensor=reference, shape=value_shape) shade = value.normalize(value.distance(x, y, DistanceMetric.euclidean)) shade = value.convolve(kernel=ValueMask.conv2d_sharpen, tensor=shade, shape=value_shape, alpha=0.5) # Ramp values to not be so imposing visually highlight = tf.math.square(shade) # Darken and brighten original pixel values shade = (1.0 - ((1.0 - tensor) * (1.0 - highlight))) * shade if channels == 1: tensor = value.blend(tensor, shade, alpha) elif channels == 2: tensor = tf.stack([value.blend(tensor[:, :, 0], shade, alpha), tensor[:, :, 1]], 2) elif channels in (3, 4): if channels == 4: a = tensor[:, :, 0] tensor = tf.stack([tensor[:, :, 0], tensor[:, :, 1], tensor[:, :, 2]], 2) # Limit effect to just the brightness channel tensor = tf.image.rgb_to_hsv([tensor])[0] tensor = tf.stack([tensor[:, :, 0], tensor[:, :, 1], value.blend(tensor[:, :, 2], tf.image.rgb_to_hsv([shade])[0][:, :, 2], alpha)], 2) tensor = tf.image.hsv_to_rgb([tensor])[0] if channels == 4: tensor = tf.stack([tensor[:, :, 0], tensor[:, :, 1], tensor[:, :, 2], a], 2) return tensor
[docs] @effect() def glyph_map( tensor: tf.Tensor, shape: list[int], mask: ValueMask | None = None, colorize: bool = True, zoom: int = 1, alpha: float = 1.0, spline_order: InterpolationType = InterpolationType.constant, time: float = 0.0, speed: float = 1.0, ) -> tf.Tensor: """ .. noisemaker-live:: :effect: glyph-map :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] mask: Value mask to apply colorize: Apply colorization zoom: Zoom factor alpha: Blending alpha value (0.0-1.0) spline_order: Interpolation type for resampling time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ if mask is None: mask = ValueMask.truetype mask = cast(ValueMask, value.coerce_enum(mask, ValueMask)) if mask == ValueMask.truetype: glyph_shape = masks.mask_shape(ValueMask.truetype) glyphs = load_glyphs(glyph_shape) else: glyph_shape = masks.mask_shape(mask) glyphs = [] sums = [] levels = 100 for i in range(levels): # Generate some glyphs. glyph, brightness = masks.mask_values(mask, glyph_shape, uv_noise=np.ones(glyph_shape) * i / levels, atlas=masks.get_atlas(mask)) glyphs.append(glyph) sums.append(brightness) glyphs = [g for _, g in sorted(zip(sums, glyphs))] in_shape = [int(shape[0] / zoom), int(shape[1] / zoom), shape[2]] height, width, channels = in_shape # Figure out how many glyphs it will take approximately to cover the image uv_shape = [int(in_shape[0] / glyph_shape[0]) or 1, int(in_shape[1] / glyph_shape[1] or 1), 1] # Generate a value map, multiply by len(glyphs) to create glyph index offsets value_shape = value.value_shape(shape) uv_noise = value.proportional_downsample(value.value_map(tensor, in_shape, keepdims=True), value_shape, uv_shape) approx_shape = [glyph_shape[0] * uv_shape[0], glyph_shape[1] * uv_shape[1], 1] uv_noise = value.resample(uv_noise, approx_shape, spline_order=spline_order) x_index = value.row_index(approx_shape) % glyph_shape[1] y_index = value.column_index(approx_shape) % glyph_shape[0] glyph_count = len(glyphs) z_index = tf.cast(uv_noise[:, :, 0] * glyph_count, tf.int32) % glyph_count spline_order = InterpolationType.cosine if mask == ValueMask.truetype else spline_order out = value.resample(tf.gather_nd(glyphs, tf.stack([z_index, y_index, x_index], 2)), [shape[0], shape[1], 1], spline_order=spline_order) if not colorize: return out * tf.ones(shape, dtype=tf.float32) out *= value.resample(value.proportional_downsample(tensor, shape, [uv_shape[0], uv_shape[1], channels]), shape, spline_order=spline_order) if alpha == 1.0: return out return value.blend(tensor, out, alpha)
[docs] @effect() def pixel_sort(tensor: tf.Tensor, shape: list[int], angled: bool | float = False, darkest: bool = False, time: float = 0.0, speed: float = 1.0) -> tf.Tensor: """ Pixel sort effect .. noisemaker-live:: :effect: pixel-sort :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] angled: Use angled sorting darkest: Sort from darkest pixels time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ if angled: angle = rng.random() * 360.0 if isinstance(angled, bool) else angled else: angle = False tensor = _pixel_sort(tensor, shape, angle, darkest) return tensor
def _pixel_sort(tensor: tf.Tensor, shape: list[int], angle: float | None, darkest: bool) -> tf.Tensor: """ Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] angle: Rotation angle in radians darkest: Sort from darkest pixels Returns: Processed tensor """ height, width, channels = shape if darkest: tensor = 1.0 - tensor # Handle None angle if angle is None: angle = 0.0 want_length = max(height, width) * 2 padded_shape = [want_length, want_length, channels] padded = tf.image.resize_with_crop_or_pad(tensor, want_length, want_length) rotated = rotate2D(padded, padded_shape, math.radians(angle)) # Find index of brightest pixel x_index = tf.expand_dims(tf.argmax(value.value_map(rotated, padded_shape), axis=1, output_type=tf.int32), -1) # Add offset index to row index x_index = (value.row_index(padded_shape) - tf.tile(x_index, [1, padded_shape[1]])) % padded_shape[1] # Sort pixels sorted_channels = [tf.nn.top_k(rotated[:, :, c], padded_shape[1])[0] for c in range(padded_shape[2])] # Apply offset sorted_channels = tf.gather_nd(tf.stack(sorted_channels, 2), tf.stack([value.column_index(padded_shape), x_index], 2)) # Rotate back to original orientation sorted_channels = rotate2D(sorted_channels, padded_shape, math.radians(-angle)) # Crop to original size sorted_channels = tf.image.resize_with_crop_or_pad(sorted_channels, height, width) # Blend with source image tensor = tf.maximum(tensor, sorted_channels) if darkest: tensor = 1.0 - tensor return tensor
[docs] @effect() def rotate(tensor: tf.Tensor, shape: list[int], angle: float | None = None, time: float = 0.0, speed: float = 1.0) -> tf.Tensor: """ Rotate the image. This breaks seamless edges. .. noisemaker-live:: :effect: rotate :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] angle: Rotation angle in radians time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ height, width, channels = shape if angle is None: angle = rng.random() * 360.0 want_length = max(height, width) * 2 padded_shape = [want_length, want_length, channels] padded = expand_tile(tensor, shape, padded_shape) rotated = rotate2D(padded, padded_shape, math.radians(angle)) return tf.image.resize_with_crop_or_pad(rotated, height, width)
[docs] def rotate2D(tensor: tf.Tensor, shape: list[int], angle: float | None) -> tf.Tensor: """ Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] angle: Rotation angle in radians Returns: Modified tensor """ x_index = tf.cast(value.row_index(shape), tf.float32) / shape[1] - 0.5 y_index = tf.cast(value.column_index(shape), tf.float32) / shape[0] - 0.5 _x_index = tf.cos(angle) * x_index + tf.sin(angle) * y_index + 0.5 _y_index = -tf.sin(angle) * x_index + tf.cos(angle) * y_index + 0.5 x_index = tf.cast(_x_index * shape[1], tf.int32) % shape[1] y_index = tf.cast(_y_index * shape[0], tf.int32) % shape[0] return tf.gather_nd(tensor, tf.stack([y_index, x_index], 2))
[docs] @effect() def sketch(tensor: tf.Tensor, shape: list[int], time: float = 0.0, speed: float = 1.0) -> tf.Tensor: """ Pencil sketch effect .. noisemaker-live:: :effect: sketch :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ value_shape = value.value_shape(shape) values = value.value_map(tensor, value_shape, keepdims=True) values = tf.image.adjust_contrast(values, 2.0) values = value.clamp01(values) outline = 1.0 - derivative(values, value_shape) outline = tf.minimum(outline, 1.0 - derivative(1.0 - values, value_shape)) outline = tf.image.adjust_contrast(outline, 0.25) outline = value.normalize(outline) values = vignette(values, value_shape, 1.0, 0.875) crosshatch = 1.0 - worms(1.0 - values, value_shape, behavior=2, density=125, duration=0.5, stride=1, stride_deviation=0.25, alpha=1.0) crosshatch = value.normalize(crosshatch) combined = value.blend(crosshatch, outline, 0.75) combined = warp(combined, value_shape, [int(shape[0] * 0.125) or 1, int(shape[1] * 0.125) or 1], octaves=1, displacement=0.0025, time=time, speed=speed) combined *= combined return combined * tf.ones(shape, dtype=tf.float32)
[docs] @effect() def simple_frame(tensor: tf.Tensor, shape: list[int], brightness: float = 0.0, time: float = 0.0, speed: float = 1.0) -> tf.Tensor: """ .. noisemaker-live:: :effect: simple-frame :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] brightness: Brightness adjustment time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ border = value.singularity(None, shape, dist_metric=DistanceMetric.chebyshev) border = value.blend(tf.zeros(shape, dtype=tf.float32), border, 0.55) border = posterize(border, shape, 1) return value.blend(tensor, tf.ones(shape, dtype=tf.float32) * brightness, border)
[docs] @effect() def lowpoly( tensor: tf.Tensor, shape: list[int], distrib: int | PointDistribution | ValueDistribution = PointDistribution.random, freq: int | list[int] = 10, time: float = 0.0, speed: float = 1.0, dist_metric: int | DistanceMetric = DistanceMetric.euclidean, ) -> tf.Tensor: """ Low-poly art style effect .. noisemaker-live:: :effect: lowpoly :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] distrib: Distrib freq: Noise frequency time: Time value for animation (0.0-1.0) speed: Animation speed multiplier dist_metric: Distance metric to use Returns: Modified tensor """ # Convert freq to int if it's a list if isinstance(freq, list): freq = freq[0] xy = point_cloud(freq, distrib=distrib, shape=shape, drift=1.0, time=time, speed=speed) # type: ignore[arg-type] distance = value.voronoi(tensor, shape, nth=1, xy=xy, dist_metric=dist_metric) color = value.voronoi(tensor, shape, diagram_type=VoronoiDiagramType.color_regions, xy=xy, dist_metric=dist_metric) return value.normalize(value.blend(distance, color, 0.5))
[docs] def square_crop_and_resize(tensor: tf.Tensor, shape: list[int], length: int = 1024) -> tf.Tensor: """ Crop and resize an image Tensor into a square with desired side length. Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] length: Length Returns: Modified tensor """ height, width, channels = shape have_length = min(height, width) if height != width: tensor = tf.image.resize_with_crop_or_pad(tensor, have_length, have_length) if length != have_length: tensor = value.resample(tensor, [length, length, channels]) return tensor
[docs] @effect() def kaleido( tensor: tf.Tensor, shape: list[int], sides: int = 6, sdf_sides: int = 5, xy: tf.Tensor | None = None, blend_edges: bool = True, time: float = 0.0, speed: float = 1.0, point_freq: int = 1, point_generations: int = 1, point_distrib: PointDistribution = PointDistribution.random, point_drift: float = 0.0, point_corners: bool = False, ) -> tf.Tensor: """ .. noisemaker-live:: :effect: kaleido :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] sides: Number of polygon sides sdf_sides: SDF polygon sides for distance metric xy: Optional XY coordinates for point cloud blend_edges: Blend with original edges time: Time value for animation (0.0-1.0) speed: Animation speed multiplier point_freq: Point frequency for Voronoi point_generations: Point generation count point_distrib: Point distribution method point_drift: Point drift amount point_corners: Include corner points Returns: Modified tensor """ height, width, channels = shape x_identity = tf.cast(value.row_index(shape), tf.float32) y_identity = tf.cast(value.column_index(shape), tf.float32) # indices offset to center x_index = value.normalize(tf.cast(x_identity, tf.float32)) - 0.5 y_index = value.normalize(tf.cast(y_identity, tf.float32)) - 0.5 value_shape = value.value_shape(shape) if sdf_sides < 3: dist_metric = DistanceMetric.euclidean else: dist_metric = DistanceMetric.sdf # distance from any pixel to center r = value.voronoi( None, value_shape, dist_metric=dist_metric, sdf_sides=sdf_sides, xy=xy, point_freq=point_freq, point_generations=point_generations, point_distrib=point_distrib, point_drift=point_drift, point_corners=point_corners, ) r = tf.squeeze(r) # cartesian to polar coordinates a = tf.math.atan2(y_index, x_index) # repeat side according to angle # rotate by 90 degrees because vertical symmetry is more pleasing to me ma = tf.math.floormod(a + math.radians(90), math.tau / sides) ma = tf.math.abs(ma - math.pi / sides) # polar to cartesian coordinates x_index = r * width * tf.math.sin(ma) y_index = r * height * tf.math.cos(ma) if blend_edges: # fade to original image edges fader = value.normalize(value.singularity(None, value_shape, dist_metric=DistanceMetric.chebyshev)) fader = tf.squeeze(fader) # conform to index shape fader = tf.math.pow(fader, 5) x_index = value.blend(x_index, x_identity, fader) y_index = value.blend(y_index, y_identity, fader) x_index = tf.cast(x_index, tf.int32) y_index = tf.cast(y_index, tf.int32) return tf.gather_nd(tensor, tf.stack([y_index % height, x_index % width], 2))
[docs] @effect() def palette(tensor: tf.Tensor, shape: list[int], name: str | None = None, alpha: float = 1.0, time: float = 0.0, speed: float = 1.0) -> tf.Tensor: """ Another approach to image coloration https://iquilezles.org/www/articles/palettes/palettes.htm .. noisemaker-live:: :effect: palette :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] name: Name alpha: Blending alpha value (0.0-1.0) time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ if not name: return tensor # Can't apply if mode is grayscale if shape[2] in (1, 2): return tensor # Preserve the alpha channel alpha_channel = None if shape[2] == 4: alpha_channel = tensor[:, :, 3] tensor = tf.stack([tensor[:, :, 0], tensor[:, :, 1], tensor[:, :, 2]], 2) rgb_shape = [shape[0], shape[1], 3] p: Any = palettes[name] offset = p["offset"] * tf.ones(rgb_shape, dtype=tf.float32) amp = p["amp"] * tf.ones(rgb_shape, dtype=tf.float32) freq = p["freq"] * tf.ones(rgb_shape, dtype=tf.float32) phase = p["phase"] * tf.ones(rgb_shape, dtype=tf.float32) + time # Multiply value_map's result x .875, in case the image is just black and white (0 == 1, we don't want a solid color image) colored = offset + amp * tf.math.cos(math.tau * (freq * value.value_map(tensor, shape, keepdims=True, with_normalize=False) * 0.875 + 0.0625 + phase)) tensor = value.blend_cosine(tensor, colored, alpha) # Re-insert the alpha channel if shape[2] == 4: tensor = tf.stack([tensor[:, :, 0], tensor[:, :, 1], tensor[:, :, 2], alpha_channel], 2) return tensor
[docs] @effect() @effect() def vhs(tensor: tf.Tensor, shape: list[int], time: float = 0.0, speed: float = 1.0) -> tf.Tensor: """ Apply a bad VHS tracking effect. .. noisemaker-live:: :effect: vhs :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ height, width, channels = shape # Generate scan noise scan_noise = value.values( freq=int(height * 0.5) + 1, shape=[height, width, 1], time=time, speed=speed * 100, ) # Create horizontal offsets grad = value.values( freq=[5, 1], shape=[height, width, 1], time=time, speed=speed, ) grad = tf.maximum(grad - 0.5, 0) grad = tf.minimum(grad * 2, 1) x_index = value.row_index(shape) x_index -= tf.squeeze(tf.cast(scan_noise * width * tf.square(grad), tf.int32)) x_index = x_index % width tensor = value.blend(tensor, scan_noise, grad) identity = tf.stack([value.column_index(shape), x_index], 2) tensor = tf.gather_nd(tensor, identity) return tensor
[docs] @effect() def lens_warp(tensor: tf.Tensor, shape: list[int], displacement: float = 0.0625, time: float = 0.0, speed: float = 1.0) -> tf.Tensor: """ .. noisemaker-live:: :effect: lens-warp :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] displacement: Displacement amount time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ value_shape = value.value_shape(shape) # Fake CRT lens shape mask = tf.pow(value.singularity(None, value_shape), 5) # obscure center pinch # Displacement values multiplied by mask to make it wavy towards the edges distortion_x = (value.values(2, value_shape, time=time, speed=speed, spline_order=2) * 2.0 - 1.0) * mask return value.refract(tensor, shape, displacement, reference_x=distortion_x)
[docs] @effect() def lens_distortion(tensor: tf.Tensor, shape: list[int], displacement: float = 1.0, time: float = 0.0, speed: float = 1.0) -> tf.Tensor: """ .. noisemaker-live:: :effect: lens-distortion :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] displacement: Displacement amount time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ x_index = tf.cast(value.row_index(shape), tf.float32) / shape[1] y_index = tf.cast(value.column_index(shape), tf.float32) / shape[0] x_dist = x_index - 0.5 y_dist = y_index - 0.5 center_dist = 1.0 - value.normalize(value.distance(x_dist, y_dist)) if displacement < 0.0: zoom = displacement * -0.25 else: zoom = 0.0 x_offset = tf.cast(((x_index - x_dist * zoom) - x_dist * center_dist * center_dist * displacement) * shape[1], tf.int32) % shape[1] y_offset = tf.cast(((y_index - y_dist * zoom) - y_dist * center_dist * center_dist * displacement) * shape[0], tf.int32) % shape[0] return tf.gather_nd(tensor, tf.stack([y_offset, x_offset], 2))
[docs] @effect() def degauss(tensor: tf.Tensor, shape: list[int], displacement: float = 0.0625, time: float = 0.0, speed: float = 1.0) -> tf.Tensor: """ .. noisemaker-live:: :effect: degauss :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] displacement: Displacement amount time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ channel_shape = [shape[0], shape[1], 1] red = lens_warp(tf.expand_dims(tensor[:, :, 0], -1), channel_shape, displacement=displacement, time=time, speed=speed) green = lens_warp(tf.expand_dims(tensor[:, :, 1], -1), channel_shape, displacement=displacement, time=time, speed=speed) blue = lens_warp(tf.expand_dims(tensor[:, :, 2], -1), channel_shape, displacement=displacement, time=time, speed=speed) return tf.stack([tf.squeeze(red), tf.squeeze(green), tf.squeeze(blue)], 2)
[docs] @effect() def crt(tensor: tf.Tensor, shape: list[int], time: float = 0.0, speed: float = 1.0) -> tf.Tensor: """ Apply vintage CRT scanlines. .. noisemaker-live:: :effect: crt :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ height, width, channels = shape value_shape = value.value_shape(shape) # Horizontal scanlines scan_noise = value.normalize(value.values(freq=[2, 1], shape=[2, 1, 1], time=time, speed=speed * 0.1, spline_order=0)) tile_h = max(1, int(height * 0.125)) scan_noise = expand_tile(scan_noise, [2, 1, 1], [tile_h * 2, width, 1], with_offset=False) scan_noise = value.resample(scan_noise, value_shape) scan_noise = lens_warp(scan_noise, value_shape, time=time, speed=speed) tensor = value.clamp01(value.blend(tensor, (tensor + scan_noise) * scan_noise, 0.05)) if channels == 3: tensor = aberration(tensor, shape, 0.0125 + rng.random() * 0.00625) tensor = adjust_hue(tensor, shape, rng.random() * 0.25 - 0.125) tensor = tf.image.adjust_saturation(tensor, 1.125) tensor = vignette(tensor, shape, brightness=0, alpha=rng.random() * 0.175) mean = tf.reduce_mean(tensor) tensor = (tensor - mean) * 1.25 + mean return tensor
[docs] @effect() def scanline_error(tensor: tf.Tensor, shape: list[int], time: float = 0.0, speed: float = 1.0) -> tf.Tensor: """ .. noisemaker-live:: :effect: scanline-error :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ height, width, channels = shape value_shape = value.value_shape(shape) error_freq = [int(value_shape[0] * 0.5) or 1, int(value_shape[1] * 0.5) or 1] error_line = tf.maximum(value.values(freq=error_freq, shape=value_shape, time=time, speed=speed * 10, distrib=ValueDistribution.exp) - 0.5, 0) error_swerve = tf.maximum(value.values(freq=[int(height * 0.01), 1], shape=value_shape, time=time, speed=speed, distrib=ValueDistribution.exp) - 0.5, 0) error_line *= error_swerve error_swerve *= 2 white_noise = value.values(freq=error_freq, shape=value_shape, time=time, speed=speed * 100) white_noise = value.blend(0, white_noise, error_swerve) error = error_line + white_noise y_index = value.column_index(shape) x_index = (value.row_index(shape) - tf.cast(value.value_map(error, value_shape) * width * 0.025, tf.int32)) % width return tf.minimum(tf.gather_nd(tensor, tf.stack([y_index, x_index], 2)) + error_line * white_noise * 4, 1)
[docs] @effect() def snow(tensor: tf.Tensor, shape: list[int], alpha: float = 0.25, time: float = 0.0, speed: float = 1.0) -> tf.Tensor: """ .. noisemaker-live:: :effect: snow :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] alpha: Blending alpha value (0.0-1.0) time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ height, width, channels = shape value_shape = value.value_shape(shape) static = value.values(freq=[height, width], shape=value_shape, time=time, speed=speed * 100, spline_order=0) static_limiter = value.values(freq=[height, width], shape=value_shape, time=time, speed=speed * 100, distrib=ValueDistribution.exp, spline_order=0) * alpha return value.blend(tensor, static, static_limiter)
[docs] @effect() def grain(tensor: tf.Tensor, shape: list[int], alpha: float = 0.25, time: float = 0.0, speed: float = 1.0) -> tf.Tensor: """ .. noisemaker-live:: :effect: grain :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] alpha: Blending alpha value (0.0-1.0) time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ height, width, channels = shape white_noise = value.values(freq=[height, width], shape=[height, width, 1], time=time, speed=speed * 100) return value.blend(tensor, white_noise, alpha)
[docs] @effect() def false_color(tensor: tf.Tensor, shape: list[int], horizontal: bool = False, displacement: float = 0.5, time: float = 0.0, speed: float = 1.0) -> tf.Tensor: """ .. noisemaker-live:: :effect: false-color :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] horizontal: Apply horizontally (vs vertically) displacement: Displacement amount time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ clut = value.values(freq=2, shape=shape, time=time, speed=speed) return value.normalize(color_map(tensor, shape, clut=clut, horizontal=horizontal, displacement=displacement))
[docs] @effect() def fibers(tensor: tf.Tensor, shape: list[int], time: float = 0.0, speed: float = 1.0) -> tf.Tensor: """ .. noisemaker-live:: :effect: fibers :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ value_shape = value.value_shape(shape) for i in range(4): mask = value.values(freq=4, shape=value_shape, time=time, speed=speed) mask = worms( mask, shape, behavior=WormBehavior.chaotic, alpha=1, density=0.05 + rng.random() * 0.00125, duration=1, kink=rng.random_int(5, 10), stride=0.75, stride_deviation=0.125, time=time, speed=speed, ) brightness = value.values(freq=128, shape=shape, time=time, speed=speed) tensor = value.blend(tensor, brightness, mask * 0.5) return tensor
[docs] @effect() def scratches(tensor: tf.Tensor, shape: list[int], time: float = 0.0, speed: float = 1.0) -> tf.Tensor: """ .. noisemaker-live:: :effect: scratches :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ value_shape = value.value_shape(shape) for i in range(4): mask = value.values(freq=rng.random_int(2, 4), shape=value_shape, time=time, speed=speed) mask = worms( mask, value_shape, behavior=[1, 3][rng.random_int(0, 1)], alpha=1, density=0.25 + rng.random() * 0.25, duration=2 + rng.random() * 2, kink=0.125 + rng.random() * 0.125, stride=0.75, stride_deviation=0.5, time=time, speed=speed, ) mask -= value.values(freq=rng.random_int(2, 4), shape=value_shape, time=time, speed=speed) * 2.0 mask = tf.maximum(mask, 0.0) tensor = tf.maximum(tensor, mask * 8.0) tensor = tf.minimum(tensor, 1.0) return tensor
[docs] @effect() def stray_hair(tensor: tf.Tensor, shape: list[int], time: float = 0.0, speed: float = 1.0) -> tf.Tensor: """ .. noisemaker-live:: :effect: stray-hair :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ value_shape = value.value_shape(shape) mask = value.values(4, value_shape, time=time, speed=speed) mask = worms( mask, value_shape, behavior=WormBehavior.unruly, alpha=1, density=0.0025 + rng.random() * 0.00125, duration=rng.random_int(8, 16), kink=rng.random_int(5, 50), stride=0.5, stride_deviation=0.25, ) brightness = value.values(freq=32, shape=value_shape, time=time, speed=speed) return value.blend(tensor, brightness * 0.333, mask * 0.666)
[docs] @effect() def grime(tensor: tf.Tensor, shape: list[int], time: float = 0.0, speed: float = 1.0) -> tf.Tensor: """ .. noisemaker-live:: :effect: grime :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ value_shape = value.value_shape(shape) mask = value.simple_multires(freq=5, shape=value_shape, time=time, speed=speed, octaves=8) mask = value.refract(mask, value_shape, 1.0, y_from_offset=True) mask = derivative(mask, value_shape, DistanceMetric.chebyshev, alpha=0.125) dusty = value.blend(tensor, 0.25, tf.square(mask) * 0.075) specks = value.values( freq=[int(shape[0] * 0.25), int(shape[1] * 0.25)], shape=value_shape, time=time, mask=ValueMask.dropout, speed=speed, distrib=ValueDistribution.exp ) specks = value.refract(specks, value_shape, 0.25) specks = 1.0 - tf.sqrt(value.normalize(tf.maximum(specks - 0.625, 0.0))) dusty = ( value.blend( dusty, value.values(freq=[shape[0], shape[1]], shape=value_shape, mask=ValueMask.sparse, time=time, speed=speed, distrib=ValueDistribution.exp), 0.075, ) * specks ) return value.blend(tensor, dusty, mask * 0.75)
[docs] @effect() def frame(tensor: tf.Tensor, shape: list[int], time: float = 0.0, speed: float = 1.0) -> tf.Tensor: """ .. noisemaker-live:: :effect: frame :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ half_shape = [int(shape[0] * 0.5), int(shape[1] * 0.5), shape[2]] half_value_shape = value.value_shape(half_shape) noise = value.simple_multires(64, half_value_shape, time=time, speed=speed, octaves=8) black = tf.zeros(half_value_shape, dtype=tf.float32) white = tf.ones(half_value_shape, dtype=tf.float32) mask = value.singularity(None, half_value_shape, VoronoiDiagramType.range, dist_metric=DistanceMetric.chebyshev, inverse=True) mask = value.normalize(mask + noise * 0.005) mask = blend_layers(tf.sqrt(mask), half_value_shape, 0.0125, white, black, black, black) faded = value.proportional_downsample(tensor, shape, half_shape) faded = tf.image.adjust_brightness(faded, 0.1) faded = tf.image.adjust_contrast(faded, 0.75) faded = light_leak(faded, half_shape, 0.125) faded = vignette(faded, half_shape, 0.05, 0.75) edge_texture = white * 0.9 + shadow(noise, half_value_shape, alpha=1.0) * 0.1 out = value.blend(faded, edge_texture, mask) out = aberration(out, half_shape, 0.00666) out = grime(out, half_shape) out = tf.image.adjust_saturation(out, 0.5) out = tf.image.random_hue(out, 0.05, seed=rng.random_int(0, 0xFFFFFFFF)) out = value.resample(out, shape) out = scratches(out, shape) out = stray_hair(out, shape) return out
[docs] @effect() def texture(tensor: tf.Tensor, shape: list[int], time: float = 0.0, speed: float = 1.0) -> tf.Tensor: """ .. noisemaker-live:: :effect: texture :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ value_shape = value.value_shape(shape) noise = value.simple_multires(64, value_shape, time=time, speed=speed, octaves=8, ridges=True) return tensor * (tf.ones(value_shape, dtype=tf.float32) * 0.9 + shadow(noise, value_shape, 1.0) * 0.1)
[docs] @effect() def spooky_ticker(tensor: tf.Tensor, shape: list[int], time: float = 0.0, speed: float = 1.0) -> tf.Tensor: """ .. noisemaker-live:: :effect: spooky-ticker :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ if rng.random() > 0.75: tensor = on_screen_display(tensor, shape, time=time, speed=speed) _masks = [ ValueMask.arecibo_nucleotide, ValueMask.arecibo_num, ValueMask.bank_ocr, ValueMask.bar_code, ValueMask.bar_code_short, ValueMask.emoji, ValueMask.fat_lcd_hex, ValueMask.alphanum_hex, ValueMask.iching, ValueMask.ideogram, ValueMask.invaders, ValueMask.lcd, ValueMask.letters, ValueMask.matrix, ValueMask.alphanum_numeric, ValueMask.script, ValueMask.white_bear, ] bottom_padding = 2 rendered_mask = tf.zeros(shape, dtype=tf.float32) for _ in range(rng.random_int(1, 3)): mask = _masks[rng.random_int(0, len(_masks) - 1)] mask_shape = masks.mask_shape(mask) multiplier = 1 if mask != ValueMask.script and (mask_shape[1] == 1 or mask_shape[1] >= 10) else 2 width = int(shape[1] / multiplier) or 1 width = mask_shape[1] * int(width / mask_shape[1]) # Make sure the mask divides evenly into width freq = [mask_shape[0], width] row_shape = [mask_shape[0], width, 1] row_mask = value.values(freq=freq, shape=row_shape, corners=True, spline_order=0, distrib=ValueDistribution.ones, mask=mask, time=time, speed=speed) if time != 0.0: # Make the ticker tick! row_mask = value.offset(row_mask, row_shape, int(time * width), 0) row_mask = value.resample(row_mask, [mask_shape[0] * multiplier, shape[1]], spline_order=1) rendered_mask += tf.pad(row_mask, tf.stack([[shape[0] - mask_shape[0] * multiplier - bottom_padding, bottom_padding], [0, 0], [0, 0]])) bottom_padding += mask_shape[0] * multiplier + 2 alpha = 0.5 + rng.random() * 0.25 # shadow tensor = value.blend(tensor, tensor * 1.0 - value.offset(rendered_mask, shape, -1, -1), alpha * 0.333) return value.blend(tensor, tf.maximum(rendered_mask, tensor), alpha)
[docs] @effect() def on_screen_display(tensor: tf.Tensor, shape: list[int], time: float = 0.0, speed: float = 1.0) -> tf.Tensor: """ .. noisemaker-live:: :effect: on-screen-display :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ glyph_count = rng.random_int(3, 6) _masks = [ ValueMask.bank_ocr, ValueMask.alphanum_hex, ValueMask.alphanum_numeric, ] mask = _masks[rng.random_int(0, len(_masks) - 1)] mask_shape = masks.mask_shape(mask) width = int(shape[1] / 24) width = mask_shape[1] * int(width / mask_shape[1]) # Make sure the mask divides evenly height = mask_shape[0] * int(width / mask_shape[1]) width *= glyph_count freq = [mask_shape[0], mask_shape[1] * glyph_count] row_mask = value.values( freq=freq, shape=[height, width, shape[2]], corners=True, spline_order=0, distrib=ValueDistribution.ones, mask=mask, time=time, speed=speed ) rendered_mask = tf.pad(row_mask, tf.stack([[25, shape[0] - height - 25], [shape[1] - width - 25, 25], [0, 0]])) alpha = 0.5 + rng.random() * 0.25 return value.blend(tensor, tf.maximum(rendered_mask, tensor), alpha)
[docs] @effect() def nebula(tensor: tf.Tensor, shape: list[int], time: float = 0.0, speed: float = 1.0) -> tf.Tensor: """ .. noisemaker-live:: :effect: nebula :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ value_shape = value.value_shape(shape) overlay = value.simple_multires([rng.random_int(3, 4), 1], value_shape, time=time, speed=speed, distrib=ValueDistribution.exp, ridges=True, octaves=6) overlay -= value.simple_multires([rng.random_int(2, 4), 1], value_shape, time=time, speed=speed, ridges=True, octaves=4) overlay *= 0.125 overlay = rotate(overlay, value_shape, angle=rng.random_int(-15, 15), time=time, speed=speed) tensor *= 1.0 - overlay tensor += tint( tf.maximum(overlay * tf.ones(shape, dtype=tf.float32), 0), shape, alpha=1.0, time=time, speed=1.0, ) return tensor
[docs] @effect() def spatter(tensor: tf.Tensor, shape: list[int], color: bool = True, time: float = 0.0, speed: float = 1.0) -> tf.Tensor: """ .. noisemaker-live:: :effect: spatter :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] color: Color time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ value_shape = value.value_shape(shape) # Generate a smear smear = value.simple_multires(rng.random_int(3, 6), value_shape, time=time, speed=speed, distrib=ValueDistribution.exp, octaves=6, spline_order=3) smear = warp( smear, value_shape, [rng.random_int(2, 3), rng.random_int(1, 3)], octaves=rng.random_int(1, 2), displacement=1.0 + rng.random(), spline_order=3, time=time, speed=speed, ) # Add spatter dots spatter = value.simple_multires( rng.random_int(32, 64), value_shape, time=time, speed=speed, distrib=ValueDistribution.exp, octaves=4, spline_order=InterpolationType.linear ) spatter = adjust_brightness(spatter, shape, -1.0) spatter = adjust_contrast(spatter, shape, 4.0) smear = tf.maximum(smear, spatter) spatter = value.simple_multires( rng.random_int(150, 200), value_shape, time=time, speed=speed, distrib=ValueDistribution.exp, octaves=4, spline_order=InterpolationType.linear ) spatter = adjust_brightness(spatter, shape, -1.25) spatter = adjust_contrast(spatter, shape, 4.0) smear = tf.maximum(smear, spatter) # Remove some of it smear = tf.maximum( 0.0, smear - value.simple_multires( rng.random_int(2, 3), value_shape, time=time, speed=speed, distrib=ValueDistribution.exp, ridges=True, octaves=3, spline_order=2 ), ) # if color and shape[2] == 3: if color is True: splash = tf.image.random_hue(tf.ones(shape, dtype=tf.float32) * tf.stack([0.875, 0.125, 0.125]), 0.5, seed=rng.random_int(0, 0xFFFFFFFF)) else: # Pass in [r, g, b] splash = tf.ones(shape, dtype=tf.float32) * tf.stack(color) else: splash = tf.zeros(shape, dtype=tf.float32) return blend_layers(value.normalize(smear), shape, 0.005, tensor, splash * tensor)
[docs] @effect() def clouds(tensor: tf.Tensor, shape: list[int], time: float = 0.0, speed: float = 1.0) -> tf.Tensor: """ Top-down cloud cover effect .. noisemaker-live:: :effect: clouds :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ pre_shape = [int(shape[0] * 0.25) or 1, int(shape[1] * 0.25) or 1, 1] control = value.simple_multires(freq=rng.random_int(2, 4), shape=pre_shape, octaves=8, ridges=True, time=time, speed=speed) control = warp(control, pre_shape, freq=3, displacement=0.125, octaves=2) layer_0 = tf.ones(pre_shape, dtype=tf.float32) layer_1 = tf.zeros(pre_shape, dtype=tf.float32) combined = blend_layers(control, pre_shape, 1.0, layer_0, layer_1) shaded = value.offset(combined, pre_shape, rng.random_int(-15, 15), rng.random_int(-15, 15)) shaded = tf.minimum(shaded * 2.5, 1.0) for _ in range(3): shaded = value.convolve(kernel=ValueMask.conv2d_blur, tensor=shaded, shape=pre_shape) post_shape = [shape[0], shape[1], 1] shaded = value.resample(shaded, post_shape) combined = value.resample(combined, post_shape) tensor = value.blend(tensor, tf.zeros(shape, dtype=tf.float32), shaded * 0.75) tensor = value.blend(tensor, tf.ones(shape, dtype=tf.float32), combined) tensor = shadow(tensor, shape, alpha=0.5) return tensor
[docs] @effect() def tint(tensor: tf.Tensor, shape: list[int], time: float = 0.0, speed: float = 1.0, alpha: float = 0.5) -> tf.Tensor: """ .. noisemaker-live:: :effect: tint :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] time: Time value for animation (0.0-1.0) speed: Animation speed multiplier alpha: Blending alpha value (0.0-1.0) Returns: Modified tensor """ if shape[2] < 3: # Not a color image return tensor color = value.values(freq=3, shape=shape, time=time, speed=speed, corners=True) # Confine hue to a range color = tf.stack([(tensor[:, :, 0] * 0.333 + rng.random() * 0.333 + rng.random()) % 1.0, tensor[:, :, 1], tensor[:, :, 2]], 2) alpha_chan = None if shape[2] == 4: alpha_chan = tensor[:, :, 3] tensor = tf.stack([tensor[:, :, 0], tensor[:, :, 1], tensor[:, :, 2]], 2) colorized = tf.stack([color[:, :, 0], color[:, :, 1], tf.image.rgb_to_hsv([tensor])[0][:, :, 2]], 2) colorized = tf.image.hsv_to_rgb([colorized])[0] out = value.blend(tensor, colorized, alpha) if shape[2] == 4: out = tf.stack([out[:, :, 0], out[:, :, 1], out[:, :, 2], alpha_chan], 2) return out
[docs] @effect() def adjust_hue(tensor: tf.Tensor, shape: list[int], amount: float = 0.25, time: float = 0.0, speed: float = 1.0) -> tf.Tensor: """ .. noisemaker-live:: :effect: adjust-hue :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] amount: Amount time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ if amount not in (1.0, 0.0, None) and shape[2] == 3: tensor = tf.image.adjust_hue(tensor, amount) return tensor
[docs] @effect() def adjust_saturation(tensor: tf.Tensor, shape: list[int], amount: float = 0.75, time: float = 0.0, speed: float = 1.0) -> tf.Tensor: """ .. noisemaker-live:: :effect: adjust-saturation :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] amount: Amount time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ if shape[2] == 3: tensor = tf.image.adjust_saturation(tensor, amount) return tensor
[docs] @effect() def adjust_brightness(tensor: tf.Tensor, shape: list[int], amount: float = 0.125, time: float = 0.0, speed: float = 1.0) -> tf.Tensor: """ .. noisemaker-live:: :effect: adjust-brightness :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] amount: Amount time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ return tf.maximum(tf.minimum(tf.image.adjust_brightness(tensor, amount), 1.0), -1.0)
[docs] @effect() def adjust_contrast(tensor: tf.Tensor, shape: list[int], amount: float = 1.25, time: float = 0.0, speed: float = 1.0) -> tf.Tensor: """ .. noisemaker-live:: :effect: adjust-contrast :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] amount: Amount time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ return value.clamp01(tf.image.adjust_contrast(tensor, amount))
[docs] @effect() def normalize(tensor: tf.Tensor, shape: list[int], time: float = 0.0, speed: float = 1.0) -> tf.Tensor: """ .. noisemaker-live:: :effect: normalize :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ return value.normalize(tensor)
[docs] @effect() def ridge(tensor: tf.Tensor, shape: list[int], time: float = 0.0, speed: float = 1.0) -> tf.Tensor: """ .. noisemaker-live:: :effect: ridge :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ return value.ridge(tensor)
[docs] @effect() def sine(tensor: tf.Tensor, shape: list[int], amount: float = 1.0, time: float = 0.0, speed: float = 1.0, rgb: bool = False) -> tf.Tensor: """ .. noisemaker-live:: :effect: sine :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] amount: Amount time: Time value for animation (0.0-1.0) speed: Animation speed multiplier rgb: Treat as RGB (vs grayscale) Returns: Modified tensor """ channels = shape[2] if channels == 1: return value.normalized_sine(tensor * amount) elif channels == 2: return tf.stack([value.normalized_sine(tensor[:, :, 0] * amount), tensor[:, :, 1]], 2) elif channels == 3: if rgb: return value.normalized_sine(tensor * amount) return tf.stack([tensor[:, :, 0], tensor[:, :, 1], value.normalized_sine(tensor[:, :, 2] * amount)], 2) elif channels == 4: if rgb: temp = value.normalized_sine(tf.stack([tensor[:, :, 0], tensor[:, :, 1], tensor[:, :, 2]], 2) * amount) return tf.stack([temp[:, :, 0], temp[:, :, 1], temp[:, :, 2], tensor[:, :, 3]], 2) return tf.stack([tensor[:, :, 0], tensor[:, :, 1], value.normalized_sine(tensor[:, :, 2] * amount), tensor[:, :, 3]], 2)
[docs] @effect() def value_refract( tensor: tf.Tensor, shape: list[int], freq: int | list[int] = 4, distrib: int | PointDistribution | ValueDistribution = ValueDistribution.center_circle, displacement: float = 0.125, time: float = 0.0, speed: float = 1.0, ) -> tf.Tensor: """ .. noisemaker-live:: :effect: value-refract :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] freq: Noise frequency distrib: Distrib displacement: Displacement amount time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ blend_values = value.values(freq=freq, shape=value.value_shape(shape), distrib=distrib, time=time, speed=speed) # type: ignore[arg-type] return value.refract(tensor, shape, time=time, speed=speed, reference_x=blend_values, displacement=displacement)
[docs] @effect() def blur( tensor: tf.Tensor, shape: list[int], amount: float = 10.0, spline_order: int | InterpolationType = InterpolationType.bicubic, time: float = 0.0, speed: float = 1.0, ) -> tf.Tensor: """ .. noisemaker-live:: :effect: blur :input: basic :seed: 42 :width: 512 :height: 256 :lazy: Args: tensor: Input tensor to process shape: Shape of the tensor [height, width, channels] amount: Amount spline_order: Interpolation type for resampling time: Time value for animation (0.0-1.0) speed: Animation speed multiplier Returns: Modified tensor """ "" "" tensor = value.proportional_downsample(tensor, shape, [max(int(shape[0] / amount), 1), max(int(shape[1] / amount), 1), shape[2]]) * 4.0 tensor = value.resample(tensor, shape, spline_order=spline_order) return tensor