Files
test/effects/noise.py
gilesb 406cc7c0c7 Initial commit: video effects processing system
Add S-expression based video effects pipeline with modular effect
definitions, constructs, and recipe files.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 12:34:45 +00:00

138 lines
3.8 KiB
Python

# /// script
# requires-python = ">=3.10"
# dependencies = ["numpy"]
# ///
"""
@effect noise
@version 1.0.0
@author artdag
@description
Noise effect. Adds various types of noise to the image including
static, gaussian, salt & pepper, and more.
@param intensity float
@range 0 1
@default 0.2
Noise intensity.
@param mode string
@enum gaussian uniform salt_pepper scanline
@default gaussian
Type of noise:
- gaussian: smooth normal distribution
- uniform: flat random noise
- salt_pepper: random black/white pixels
- scanline: horizontal line noise
@param colored bool
@default false
Use colored noise instead of monochrome.
@param animate bool
@default true
Different noise each frame.
@param seed int
@default 42
Random seed for reproducible noise.
@state rng DeterministicRNG
Random number generator.
@example
(effect noise :intensity 0.3 :mode "gaussian")
@example
;; Static TV noise
(effect noise :intensity 0.5 :mode "uniform" :animate true)
"""
import numpy as np
from pathlib import Path
import sys
# Import DeterministicRNG from same directory
_effects_dir = Path(__file__).parent
if str(_effects_dir) not in sys.path:
sys.path.insert(0, str(_effects_dir))
from random import DeterministicRNG
def process_frame(frame: np.ndarray, params: dict, state: dict) -> tuple:
"""
Apply noise effect to a video frame.
Args:
frame: Input frame as numpy array (H, W, 3) RGB uint8
params: Effect parameters
state: Persistent state dict
Returns:
Tuple of (processed_frame, new_state)
"""
intensity = params.get("intensity", 0.2)
mode = params.get("mode", "gaussian")
colored = params.get("colored", False)
animate = params.get("animate", True)
seed = int(params.get("seed", 42))
if state is None:
state = {}
if intensity <= 0:
return frame, state
# Initialize RNG
if "rng" not in state:
state["rng"] = DeterministicRNG(seed)
rng = state["rng"]
h, w = frame.shape[:2]
result = frame.astype(np.float32)
if mode == "gaussian":
# Gaussian noise
if colored:
noise = np.array([[[rng.gaussian(0, intensity * 50) for _ in range(3)]
for _ in range(w)]
for _ in range(h)])
else:
noise_2d = np.array([[rng.gaussian(0, intensity * 50)
for _ in range(w)]
for _ in range(h)])
noise = np.stack([noise_2d, noise_2d, noise_2d], axis=-1)
result = result + noise
elif mode == "uniform":
# Uniform random noise
if colored:
noise = np.array([[[rng.uniform(-intensity * 100, intensity * 100) for _ in range(3)]
for _ in range(w)]
for _ in range(h)])
else:
noise_2d = np.array([[rng.uniform(-intensity * 100, intensity * 100)
for _ in range(w)]
for _ in range(h)])
noise = np.stack([noise_2d, noise_2d, noise_2d], axis=-1)
result = result + noise
elif mode == "salt_pepper":
# Salt and pepper noise
for y in range(h):
for x in range(w):
if rng.uniform() < intensity * 0.1:
if rng.uniform() < 0.5:
result[y, x] = [0, 0, 0]
else:
result[y, x] = [255, 255, 255]
elif mode == "scanline":
# Horizontal scanline noise
for y in range(h):
if rng.uniform() < intensity * 0.2:
noise_val = rng.uniform(-intensity * 100, intensity * 100)
result[y] = result[y] + noise_val
return np.clip(result, 0, 255).astype(np.uint8), state