Source code for noisemaker.masks

"""Value masks for Noisemaker. Used when generating value noise or glyph maps."""

from __future__ import annotations

import json
import os
import re
import string
from typing import Any, Callable, cast

import numpy as np

import noisemaker.rng as rng
from noisemaker.constants import ValueMask

# Load masks
_SHARE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "share"))
_MASKS_FILE = os.path.join(_SHARE_DIR, "masks.json")

with open(_MASKS_FILE) as f:
    _MASKS_DATA = json.load(f)["Masks"]

#: Hard-coded masks
Masks = {}
for name, value in _MASKS_DATA.items():
    Masks[ValueMask[name]] = value

# Procedural masks, corresponding to keys in constants.ValueMask

# Procedural mask shapes

_mask_shapes: dict[ValueMask, Any] = {}


# Masks wanting to use this decorator should first be added to the ValueMask enum in constants.py
[docs] def mask(*args: Any) -> Callable: """ Function decorator for procedural masks. Args: *args: Optional mask shape specification Returns: Decorator function for registering mask functions """ def decorator_fn(mask_fn: Callable) -> Callable: mask = ValueMask[mask_fn.__name__] if args: _mask_shapes[mask] = args[0] Masks[mask] = mask_fn return mask_fn return decorator_fn
[docs] def mask_shape(mask: ValueMask) -> list[int]: """ Return the shape for the received ValueMask. Args: mask: ValueMask to get shape for Returns: Shape as [height, width, channels] """ if ValueMask.is_procedural(mask): shape = _mask_shapes[mask] if callable(shape): shape = shape() # Ensure shape is a list for type checking shape = list(shape) if not isinstance(shape, list) else shape else: mask_data: Any = Masks[mask] height = len(mask_data) width = len(mask_data[0]) if isinstance(mask_data[0][0], list): channels = len(mask_data[0][0]) else: channels = 1 shape = [height, width, channels] return cast(list[int], shape)
[docs] def get_atlas(mask: ValueMask) -> Any: """ Get the glyph atlas for a procedural mask. Args: mask: ValueMask to get atlas for Returns: Atlas array or None if not applicable """ atlas = None if mask == ValueMask.truetype: from noisemaker.glyphs import load_glyphs atlas = load_glyphs([15, 15, 1]) elif ValueMask.is_procedural(mask): base_name = re.sub(r"_[a-z]+$", "", mask.name) if mask.name.endswith("_binary"): atlas = [Masks[ValueMask[f"{base_name}_0"]], Masks[ValueMask[f"{base_name}_1"]]] elif mask.name.endswith("_numeric"): atlas = [Masks[ValueMask[f"{base_name}_{i}"]] for i in string.digits] elif mask.name.endswith("_hex"): atlas = [Masks[g] for g in Masks if re.match(f"^{base_name}_[0-9a-f]$", g.name)] else: atlas = [Masks[g] for g in Masks if g.name.startswith(f"{mask.name}_") and not callable(Masks[g])] return atlas
[docs] def mask_values( mask: ValueMask, glyph_shape: list[int] | None = None, uv_noise: np.ndarray | None = None, atlas: np.ndarray | None = None, inverse: bool = False, time: float = 0.0, speed: float = 1.0, ) -> tuple[list[list[Any]], float]: """ Return pixel values for the received ValueMask. Args: mask: ValueMask to render glyph_shape: Shape of the mask being rendered [height, width, channels] uv_noise: Per-pixel noise values, shaped like glyph_shape atlas: Pre-rendered atlas of images (e.g., TrueType letters) inverse: Return the inverse of the mask time: Time parameter for animation speed: Animation speed multiplier Returns: Mask value array """ shape = mask_shape(mask) if glyph_shape is None: glyph_shape = shape if len(shape) == 3: glyph_shape[2] = shape[2] mask_values = [] uv_shape = [int(glyph_shape[0] / shape[0]) or 1, int(glyph_shape[1] / shape[1]) or 1] if uv_noise is None: from noisemaker.simplex import simplex uv_noise = simplex(uv_shape, time=time, seed=rng.random_int(1, 65536), speed=speed, as_np=True) # normalize() but it's numpy # floor = np.amin(uv_noise) # ceil = np.amax(uv_noise) # uv_noise = (uv_noise - floor) / (ceil - floor) total = 0 for y in range(glyph_shape[0]): uv_y = int((y / glyph_shape[0]) * uv_shape[0]) mask_row: list[Any] = [] mask_values.append(mask_row) for x in range(glyph_shape[1]): uv_x = int((x / glyph_shape[1]) * uv_shape[1]) mask_entry: Any = Masks[mask] if callable(mask_entry): pixel = mask_entry( x=x, y=y, row=mask_row, shape=shape, uv_x=uv_x, uv_y=uv_y, uv_noise=uv_noise, uv_shape=uv_shape, atlas=atlas, glyph_shape=glyph_shape ) else: pixel = mask_entry[y % shape[0]][x % shape[1]] if not isinstance(pixel, list): pixel = [pixel] pixel = [float(i) for i in pixel] if inverse: pixel = [1.0 - i for i in pixel] mask_row.append(pixel) total += sum(pixel) return mask_values, total
[docs] def uv_random(uv_noise: np.ndarray, uv_x: int, uv_y: int) -> float: return float((uv_noise[uv_y][uv_x] + rng.random()) % 1.0)
[docs] def square_masks() -> list[ValueMask]: """ Return a list of square ValueMasks. Returns: List of ValueMasks with square dimensions """ square = [] for mask in ValueMask: if callable(_mask_shapes.get(mask)): # No dynamic shapes continue shape = mask_shape(mask) if shape and shape[0] == shape[1]: square.append(mask) return square
def _glyph_from_atlas_range(x: int, y: int, shape: list[int], uv_x: int, uv_y: int, uv_noise: np.ndarray, atlas: np.ndarray, **kwargs: Any) -> Any: glyph_index = int(uv_noise[uv_y][uv_x] * (len(atlas))) glyph_index = min(max(glyph_index, 0), len(atlas) - 1) return atlas[glyph_index][y % shape[0]][x % shape[1]]
[docs] @mask([10, 10, 1]) def dropout(uv_noise: np.ndarray, uv_x: int, uv_y: int, **kwargs: Any) -> Any: return uv_random(uv_noise, uv_x, uv_y) < 0.25
[docs] @mask([10, 10, 1]) def sparse(uv_noise: np.ndarray, uv_x: int, uv_y: int, **kwargs: Any) -> Any: return uv_random(uv_noise, uv_x, uv_y) < 0.15
[docs] @mask([10, 10, 1]) def sparser(uv_noise: np.ndarray, uv_x: int, uv_y: int, **kwargs: Any) -> Any: return uv_random(uv_noise, uv_x, uv_y) < 0.05
[docs] @mask([10, 10, 1]) def sparsest(uv_noise: np.ndarray, uv_x: int, uv_y: int, **kwargs: Any) -> Any: return uv_random(uv_noise, uv_x, uv_y) < 0.0125
[docs] @mask(lambda: [rng.random_int(5, 7), rng.random_int(6, 12), 1]) def invaders(**kwargs: Any) -> Any: return _invaders(**kwargs)
[docs] @mask([18, 18, 1]) def invaders_large(**kwargs: Any) -> Any: return _invaders(**kwargs)
[docs] @mask([6, 6, 1]) def invaders_square(**kwargs: Any) -> Any: return _invaders(**kwargs)
[docs] @mask([4, 4, 1]) def white_bear(**kwargs: Any) -> Any: return _invaders(**kwargs)
def _invaders(x: int, y: int, row: list[Any], shape: list[int], uv_noise: np.ndarray, uv_x: int, uv_y: int, **kwargs: Any) -> Any: # Inspired by http://www.complexification.net/gallery/machines/invaderfractal/ height = shape[0] width = shape[1] if y % height == 0 or x % width == 0: return 0 elif x % width > width / 2: return row[x - int(((x % width) - width / 2) * 2)] else: return uv_random(uv_noise, uv_x, uv_y) < 0.5
[docs] @mask([6, 4, 1]) def matrix(x: int, y: int, row: int, shape: list[int], uv_noise: np.ndarray, uv_x: int, uv_y: int, **kwargs: Any) -> Any: height = shape[0] width = shape[1] if y % height == 0 or x % width == 0: return 0 return uv_random(uv_noise, uv_x, uv_y) < 0.5
[docs] @mask(lambda: [rng.random_int(3, 4) * 2 + 1, rng.random_int(3, 4) * 2 + 1, 1]) def letters(x: int, y: int, row: int, shape: list[int], uv_noise: np.ndarray, uv_x: int, uv_y: int, **kwargs: Any) -> Any: # Inspired by https://www.shadertoy.com/view/4lscz8 height = shape[0] width = shape[1] if any(n == 0 for n in (x % width, y % height)): return 0 if any(n == 1 for n in (width - (x % width), height - (y % height))): return 0 if all(n % 2 == 0 for n in (x % width, y % height)): return 0 if x % 2 == 0 or y % 2 == 0: return uv_random(uv_noise, uv_x, uv_y) > 0.25 return uv_random(uv_noise, uv_x, uv_y) > 0.75
[docs] @mask([14, 8, 1]) def iching(x: int, y: int, row: list[Any], shape: list[int], uv_noise: np.ndarray, uv_x: int, uv_y: int, **kwargs: Any) -> Any: height = shape[0] width = shape[1] if any(n == 0 for n in (x % width, y % height)): return 0 if any(n == 1 for n in (width - (x % width), height - (y % height))): return 0 if y % 2 == 0: return 0 if x % 2 == 1 and x % width not in (3, 4): return 1 if x % 2 == 0: return row[x - 1] return uv_random(uv_noise, uv_x, uv_y) < 0.5
[docs] @mask(lambda: [rng.random_int(4, 6) * 2] * 2 + [1]) def ideogram(x: int, y: int, row: int, shape: list[int], uv_noise: np.ndarray, uv_x: int, uv_y: int, **kwargs: Any) -> Any: height = shape[0] width = shape[1] if any(n == 0 for n in (x % width, y % height)): return 0 if any(n == 1 for n in (width - (x % width), height - (y % height))): return 0 if all(n % 2 == 1 for n in (x % width, y % height)): return 0 return uv_random(uv_noise, uv_x, uv_y) > 0.5
[docs] @mask(lambda: [rng.random_int(7, 9), rng.random_int(12, 24), 1]) def script(x: int, y: int, row: list[Any], shape: list[int], uv_noise: np.ndarray, uv_y: int, uv_x: int, **kwargs: Any) -> Any: height = shape[0] width = shape[1] x_step = x % width y_step = y % height if x > 0 and (x + y) % 2 == 1: return row[x - 1] if y_step == 0: return 0 if y_step in (1, 3, 6): return uv_random(uv_noise, uv_x, uv_y) > 0.25 if y_step in (2, 4, 5): return uv_random(uv_noise, uv_x, uv_y) > 0.9 if x_step == 0: return 0 if any(n == 0 for n in (width - x_step, height - y_step)): return 0 if all(n % 2 == 0 for n in (x_step, y_step)): return 0 if y_step == height - 1: return 0 return uv_random(uv_noise, uv_x, uv_y) > 0.5
[docs] @mask([4, 4, 1]) def tromino( x: int, y: int, row: int, shape: list[int], uv_x: int, uv_y: int, uv_noise: np.ndarray, uv_shape: list[int], atlas: np.ndarray, **kwargs: Any ) -> Any: tex_x = x % shape[1] tex_y = y % shape[0] uv_value = uv_noise[uv_y][uv_x] * (len(atlas) - 1) uv_floor = int(uv_value) uv_fract = uv_value - uv_floor float2 = uv_noise[(uv_y + int(shape[0] * 0.5)) % uv_shape[0]][uv_x] float3 = uv_noise[uv_y][(uv_x + int(shape[1] * 0.5)) % uv_shape[1]] if uv_fract < 0.5: _x = tex_x tex_x = tex_y tex_y = _x if float2 < 0.5: tex_x = shape[1] - tex_x - 1 if float3 < 0.5: tex_y = shape[0] - tex_y - 1 return atlas[uv_floor][tex_x][tex_y]
[docs] @mask([6, 6, 1]) def alphanum_binary(**kwargs: Any) -> Any: return _glyph_from_atlas_range(**kwargs)
[docs] @mask([6, 6, 1]) def alphanum_numeric(**kwargs: Any) -> Any: return _glyph_from_atlas_range(**kwargs)
[docs] @mask([6, 6, 1]) def alphanum_hex(**kwargs: Any) -> Any: return _glyph_from_atlas_range(**kwargs)
[docs] @mask([15, 15, 1]) def truetype(x: int, y: int, row: int, shape: list[int], uv_x: int, uv_y: int, uv_noise: np.ndarray, atlas: np.ndarray, **kwargs: Any) -> Any: value = max(0, min(1, uv_noise[uv_y][uv_x])) glyph = atlas[int(value * (len(atlas) - 1))] return glyph[y % shape[0]][x % shape[1]]
[docs] @mask([4, 4, 1]) def halftone(**kwargs: Any) -> Any: return _glyph_from_atlas_range(**kwargs)
[docs] @mask([8, 5, 1]) def lcd(**kwargs: Any) -> Any: return _glyph_from_atlas_range(**kwargs)
[docs] @mask([8, 5, 1]) def lcd_binary(**kwargs: Any) -> Any: return _glyph_from_atlas_range(**kwargs)
[docs] @mask([10, 10, 1]) def fat_lcd(**kwargs: Any) -> Any: return _glyph_from_atlas_range(**kwargs)
[docs] @mask([10, 10, 1]) def fat_lcd_binary(**kwargs: Any) -> Any: return _glyph_from_atlas_range(**kwargs)
[docs] @mask([10, 10, 1]) def fat_lcd_numeric(**kwargs: Any) -> Any: return _glyph_from_atlas_range(**kwargs)
[docs] @mask([10, 10, 1]) def fat_lcd_hex(**kwargs: Any) -> Any: return _glyph_from_atlas_range(**kwargs)
[docs] @mask([6, 3, 1]) def arecibo_num(x: int, y: int, row: int, shape: list[int], uv_noise: np.ndarray, uv_x: int, uv_y: int, **kwargs: Any) -> Any: tex_x = x % shape[1] tex_y = y % shape[0] if tex_y == 0 or tex_y == shape[0] - 1 or tex_x == 0: return 0 if tex_y == shape[0] - 2: return 1 if tex_x == 1 else 0 return uv_random(uv_noise, uv_x, uv_y) < 0.5
[docs] @mask([6, 5, 1]) def arecibo_bignum(*args: Any, **kwargs: Any) -> Any: return arecibo_num(*args, **kwargs)
[docs] @mask([6, 6, 1]) def arecibo_nucleotide(x: int, y: int, row: int, shape: list[int], uv_noise: np.ndarray, uv_x: int, uv_y: int, **kwargs: Any) -> Any: tex_x = x % shape[1] tex_y = y % shape[0] if tex_y == 0 or tex_y == shape[0] - 1 or tex_x == 0: return 0 if tex_y == shape[0] - 2: return 1 if tex_x < shape[1] else 0 if tex_y < shape[0] - 3 and tex_x > shape[1] - 2: return 0 return uv_random(uv_noise, uv_x, uv_y) < 0.5
_ARECIBO_DNA_TEMPLATE = [ [0, 1, 0, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, 0, 1, 0], [0, 0, 1, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, 1, 0, 0], [0, 0, 0, 1, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, 1, 0, 0, 0], [0, 0, 0, 0, 1, 1, 0, 0, -1, 0, 0, 0, 1, 1, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 1, 1, 0, 0, 0, -1, 0, 0, 1, 1, 0, 0, 0, 0], [0, 0, 0, 1, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, 1, 0, 0, 0], [0, 0, 1, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, 1, 0, 0], [0, 1, 0, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, 0, 1, 0], ]
[docs] @mask([11, 17, 1]) def arecibo_dna(x: int, y: int, row: int, shape: list[int], uv_noise: np.ndarray, uv_x: int, uv_y: int, **kwargs: Any) -> Any: tex_x = x % shape[1] tex_y = y % shape[0] value = _ARECIBO_DNA_TEMPLATE[tex_y][tex_x] return uv_random(uv_noise, uv_x, uv_y) < 0.5 if value == -1 else value
[docs] @mask(lambda: [64, 64, 1]) def arecibo(x: int, y: int, row: int, shape: list[int], uv_x: int, uv_y: int, uv_noise: np.ndarray, glyph_shape: list[int], **kwargs: Any) -> Any: third_height = glyph_shape[0] / 3 half_width = glyph_shape[1] / 2 dna_half_width = mask_shape(ValueMask.arecibo_dna)[1] * 0.5 if x > half_width - dna_half_width and x < half_width + dna_half_width: dna_x = int(x - half_width - dna_half_width) return arecibo_dna(dna_x, y, row, mask_shape(ValueMask.arecibo_dna), uv_noise, uv_x, uv_y, **kwargs) if x > half_width - (dna_half_width + 2) and x < half_width + dna_half_width + 1: return 0 if y < third_height: return arecibo_num(x, y, row, mask_shape(ValueMask.arecibo_num), uv_noise, uv_x, uv_y, **kwargs) if y < third_height * 2: return arecibo_nucleotide(x, y, row, mask_shape(ValueMask.arecibo_nucleotide), uv_noise, uv_x, uv_y, **kwargs) return arecibo_bignum(x, y, row, mask_shape(ValueMask.arecibo_bignum), uv_noise, uv_x, uv_y, **kwargs)
[docs] @mask([6, 6, 1]) def truchet_lines(**kwargs: Any) -> Any: return _glyph_from_atlas_range(**kwargs)
[docs] @mask([6, 6, 1]) def truchet_curves(**kwargs: Any) -> Any: return _glyph_from_atlas_range(**kwargs)
[docs] @mask([6, 6, 1]) def truchet_tile(**kwargs: Any) -> Any: return _glyph_from_atlas_range(**kwargs)
[docs] @mask([8, 8, 1]) def mcpaint(**kwargs: Any) -> Any: return _glyph_from_atlas_range(**kwargs)
[docs] @mask([13, 13, 1]) def emoji(**kwargs: Any) -> Any: return _glyph_from_atlas_range(**kwargs)
[docs] @mask([24, 1, 1]) def bar_code(x: int, y: int, row: int, shape: list[int], uv_x: int, uv_y: int, uv_noise: np.ndarray, **kwargs: Any) -> Any: return uv_noise[0][uv_x] < 0.5
[docs] @mask([10, 1, 1]) def bar_code_short(*args: Any, **kwargs: Any) -> Any: return bar_code(*args, **kwargs)
[docs] @mask([8, 7, 1]) def bank_ocr(**kwargs: Any) -> Any: return _glyph_from_atlas_range(**kwargs)
[docs] @mask(lambda: [rng.random_int(25, 50)] * 2 + [1]) def fake_qr(x: int, y: int, row: int, shape: list[int], uv_noise: np.ndarray, uv_x: int, uv_y: int, **kwargs: Any) -> Any: x = x % shape[1] y = y % shape[1] if ( (x == 0 or y == 0 or x == shape[1] - 1 or y == shape[0] - 1) or (y in (8, shape[0] - 9) and x < 9) or (y == 8 and x > shape[1] - 10) or (x in (8, shape[1] - 9) and y < 9) or (x == 8 and y > shape[0] - 10) or (y in (2, 6) and (x in range(2, 7) or x in range(shape[1] - 7, shape[1] - 2))) or (y in (shape[1] - 3, shape[1] - 7) and x in range(2, 7)) or (x in (2, 6) and (y in range(2, 7) or y in range(shape[0] - 7, shape[0] - 2))) or (x in (shape[0] - 3, shape[0] - 7) and y in range(2, 7)) or (x in (shape[0] - 7, shape[0] - 9) and y in range(shape[0] - 9, shape[0] - 6)) or (y in (shape[1] - 7, shape[1] - 9) and x in range(shape[1] - 9, shape[1] - 6)) ): return 1 elif ( (x == shape[1] - 8 and y == shape[0] - 8) or (x in (shape[0] - 6, shape[0] - 10) and y in range(shape[0] - 10, shape[0] - 5)) or (y in (shape[1] - 6, shape[1] - 10) and x in range(shape[1] - 10, shape[1] - 5)) ): return 0 elif (x > 8 and x < shape[1] - 8) or (y > 8 and y < shape[0] - 8) or (x >= shape[1] - 8 and y >= shape[0] - 8): return uv_random(uv_noise, uv_x, uv_y) < 0.5 return 0