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>
138 lines
3.8 KiB
Python
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
|