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>
143 lines
3.9 KiB
Python
143 lines
3.9 KiB
Python
# /// script
|
|
# requires-python = ">=3.10"
|
|
# dependencies = ["numpy", "scipy"]
|
|
# ///
|
|
"""
|
|
@effect scatter
|
|
@version 1.0.0
|
|
@author artdag
|
|
|
|
@description
|
|
Scatter effect. Randomly redistributes pixels in local area creating
|
|
a dissolving, dispersed look.
|
|
|
|
@param amount float
|
|
@range 0 100
|
|
@default 10
|
|
Scatter radius in pixels. Bind to energy for reactive dissolve.
|
|
|
|
@param randomize_per_frame bool
|
|
@default false
|
|
Different scatter pattern each frame (vs. static pattern).
|
|
|
|
@param grain float
|
|
@range 0 1
|
|
@default 0
|
|
Add film grain to scattered result.
|
|
|
|
@param seed int
|
|
@default 42
|
|
Random seed for reproducible patterns.
|
|
|
|
@state rng DeterministicRNG
|
|
Random number generator for displacement.
|
|
|
|
@state displacement_map ndarray
|
|
Cached displacement map for static mode.
|
|
|
|
@example
|
|
(effect scatter :amount 20)
|
|
|
|
@example
|
|
;; Reactive dissolve
|
|
(effect scatter :amount (bind energy :range [5 50]) :randomize_per_frame true)
|
|
"""
|
|
|
|
import numpy as np
|
|
from scipy import ndimage
|
|
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 scatter effect to a video frame.
|
|
|
|
Args:
|
|
frame: Input frame as numpy array (H, W, 3) RGB uint8
|
|
params: Effect parameters
|
|
- amount: scatter radius (default 10)
|
|
- randomize_per_frame: animate pattern (default False)
|
|
- grain: add film grain (default 0)
|
|
- seed: random seed (default 42)
|
|
state: Persistent state dict
|
|
|
|
Returns:
|
|
Tuple of (processed_frame, new_state)
|
|
"""
|
|
amount = int(np.clip(params.get("amount", 10), 0, 100))
|
|
randomize = params.get("randomize_per_frame", False)
|
|
grain = params.get("grain", 0)
|
|
seed = int(params.get("seed", 42))
|
|
|
|
if state is None:
|
|
state = {}
|
|
|
|
if amount < 1:
|
|
return frame, state
|
|
|
|
# Initialize RNG
|
|
if "rng" not in state:
|
|
state["rng"] = DeterministicRNG(seed)
|
|
rng = state["rng"]
|
|
|
|
h, w = frame.shape[:2]
|
|
|
|
# Generate or reuse displacement map
|
|
last_size = state.get("last_size")
|
|
if randomize or "displacement_map" not in state or last_size != (h, w):
|
|
# Generate new displacement map
|
|
displacement = np.zeros((h, w, 2), dtype=np.float32)
|
|
for y in range(h):
|
|
for x in range(w):
|
|
displacement[y, x, 0] = rng.uniform(-amount, amount)
|
|
displacement[y, x, 1] = rng.uniform(-amount, amount)
|
|
state["displacement_map"] = displacement
|
|
state["last_size"] = (h, w)
|
|
|
|
displacement_map = state["displacement_map"]
|
|
|
|
# Create coordinate grids
|
|
y_coords, x_coords = np.mgrid[0:h, 0:w].astype(np.float32)
|
|
|
|
# Apply displacement
|
|
new_y = y_coords + displacement_map[:, :, 0]
|
|
new_x = x_coords + displacement_map[:, :, 1]
|
|
|
|
# Clip to valid range
|
|
new_y = np.clip(new_y, 0, h - 1)
|
|
new_x = np.clip(new_x, 0, w - 1)
|
|
|
|
# Sample from displaced positions
|
|
result = np.zeros_like(frame)
|
|
for c in range(frame.shape[2] if len(frame.shape) > 2 else 1):
|
|
if len(frame.shape) > 2:
|
|
result[:, :, c] = ndimage.map_coordinates(
|
|
frame[:, :, c],
|
|
[new_y, new_x],
|
|
order=1,
|
|
mode='reflect'
|
|
)
|
|
else:
|
|
result = ndimage.map_coordinates(
|
|
frame,
|
|
[new_y, new_x],
|
|
order=1,
|
|
mode='reflect'
|
|
)
|
|
|
|
# Add grain
|
|
if grain > 0:
|
|
noise = np.array([[[rng.uniform(-grain * 20, grain * 20) for _ in range(3)]
|
|
for _ in range(w)]
|
|
for _ in range(h)])
|
|
result = np.clip(result.astype(np.float32) + noise, 0, 255).astype(np.uint8)
|
|
|
|
return result, state
|