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>
This commit is contained in:
170
effects/displacement.py
Normal file
170
effects/displacement.py
Normal file
@@ -0,0 +1,170 @@
|
||||
# /// script
|
||||
# requires-python = ">=3.10"
|
||||
# dependencies = ["numpy", "opencv-python"]
|
||||
# ///
|
||||
"""
|
||||
@effect displacement
|
||||
@version 1.0.0
|
||||
@author artdag
|
||||
|
||||
@description
|
||||
Displacement effect. Warps the image based on a pattern (sine waves,
|
||||
noise, or radial). Creates flowing, liquid-like distortions.
|
||||
|
||||
@param amount float
|
||||
@range 0 100
|
||||
@default 20
|
||||
Displacement strength in pixels.
|
||||
|
||||
@param pattern string
|
||||
@enum sine noise radial turbulence
|
||||
@default sine
|
||||
Displacement pattern type:
|
||||
- sine: smooth sine wave
|
||||
- noise: random displacement
|
||||
- radial: swirl from center
|
||||
- turbulence: multi-scale noise
|
||||
|
||||
@param frequency float
|
||||
@range 1 50
|
||||
@default 10
|
||||
Pattern frequency (waves per frame width).
|
||||
|
||||
@param speed float
|
||||
@range 0 10
|
||||
@default 1
|
||||
Animation speed.
|
||||
|
||||
@param direction string
|
||||
@enum horizontal vertical both
|
||||
@default both
|
||||
Displacement direction.
|
||||
|
||||
@param seed int
|
||||
@default 42
|
||||
Random seed for noise patterns.
|
||||
|
||||
@state rng DeterministicRNG
|
||||
Random number generator.
|
||||
|
||||
@example
|
||||
(effect displacement :amount 30 :pattern "sine" :frequency 5)
|
||||
|
||||
@example
|
||||
;; Reactive turbulence
|
||||
(effect displacement :amount (bind energy :range [10 50]) :pattern "turbulence")
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import cv2
|
||||
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 displacement 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)
|
||||
"""
|
||||
amount = params.get("amount", 20)
|
||||
pattern = params.get("pattern", "sine")
|
||||
frequency = max(1, params.get("frequency", 10))
|
||||
speed = params.get("speed", 1)
|
||||
direction = params.get("direction", "both")
|
||||
seed = int(params.get("seed", 42))
|
||||
t = params.get("_time", 0)
|
||||
|
||||
if state is None:
|
||||
state = {}
|
||||
|
||||
if amount == 0:
|
||||
return frame, state
|
||||
|
||||
# Initialize RNG
|
||||
if "rng" not in state:
|
||||
state["rng"] = DeterministicRNG(seed)
|
||||
|
||||
h, w = frame.shape[:2]
|
||||
|
||||
# Create base coordinate maps
|
||||
map_x = np.tile(np.arange(w, dtype=np.float32), (h, 1))
|
||||
map_y = np.tile(np.arange(h, dtype=np.float32).reshape(-1, 1), (1, w))
|
||||
|
||||
# Generate displacement based on pattern
|
||||
if pattern == "sine":
|
||||
# Sine wave displacement
|
||||
phase = t * speed * 2 * np.pi
|
||||
if direction in ["horizontal", "both"]:
|
||||
map_x = map_x + amount * np.sin(2 * np.pi * map_y / h * frequency + phase)
|
||||
if direction in ["vertical", "both"]:
|
||||
map_y = map_y + amount * np.sin(2 * np.pi * map_x / w * frequency + phase)
|
||||
|
||||
elif pattern == "noise":
|
||||
# Generate noise displacement
|
||||
rng = state["rng"]
|
||||
if "noise_x" not in state or state.get("noise_size") != (h, w):
|
||||
state["noise_x"] = np.array([[rng.uniform(-1, 1) for _ in range(w)] for _ in range(h)], dtype=np.float32)
|
||||
state["noise_y"] = np.array([[rng.uniform(-1, 1) for _ in range(w)] for _ in range(h)], dtype=np.float32)
|
||||
state["noise_size"] = (h, w)
|
||||
|
||||
if direction in ["horizontal", "both"]:
|
||||
map_x = map_x + amount * state["noise_x"]
|
||||
if direction in ["vertical", "both"]:
|
||||
map_y = map_y + amount * state["noise_y"]
|
||||
|
||||
elif pattern == "radial":
|
||||
# Radial/swirl displacement
|
||||
cx, cy = w / 2, h / 2
|
||||
y_coords, x_coords = np.mgrid[0:h, 0:w].astype(np.float32)
|
||||
dx = x_coords - cx
|
||||
dy = y_coords - cy
|
||||
dist = np.sqrt(dx**2 + dy**2) + 1e-6
|
||||
angle = np.arctan2(dy, dx)
|
||||
|
||||
# Swirl amount varies with distance and time
|
||||
swirl = amount * 0.01 * np.sin(dist / (w / frequency) + t * speed * 2 * np.pi)
|
||||
|
||||
new_angle = angle + swirl
|
||||
if direction in ["horizontal", "both"]:
|
||||
map_x = cx + dist * np.cos(new_angle)
|
||||
if direction in ["vertical", "both"]:
|
||||
map_y = cy + dist * np.sin(new_angle)
|
||||
|
||||
elif pattern == "turbulence":
|
||||
# Multi-scale noise
|
||||
rng = state["rng"]
|
||||
disp_x = np.zeros((h, w), dtype=np.float32)
|
||||
disp_y = np.zeros((h, w), dtype=np.float32)
|
||||
|
||||
for scale in [1, 2, 4]:
|
||||
sh, sw = h // scale, w // scale
|
||||
noise_x = np.array([[rng.uniform(-1, 1) for _ in range(sw)] for _ in range(sh)], dtype=np.float32)
|
||||
noise_y = np.array([[rng.uniform(-1, 1) for _ in range(sw)] for _ in range(sh)], dtype=np.float32)
|
||||
if scale > 1:
|
||||
noise_x = cv2.resize(noise_x, (w, h))
|
||||
noise_y = cv2.resize(noise_y, (w, h))
|
||||
disp_x += noise_x / scale
|
||||
disp_y += noise_y / scale
|
||||
|
||||
if direction in ["horizontal", "both"]:
|
||||
map_x = map_x + amount * disp_x
|
||||
if direction in ["vertical", "both"]:
|
||||
map_y = map_y + amount * disp_y
|
||||
|
||||
# Apply remapping
|
||||
result = cv2.remap(frame, map_x, map_y, cv2.INTER_LINEAR, borderMode=cv2.BORDER_REFLECT)
|
||||
|
||||
return result, state
|
||||
Reference in New Issue
Block a user