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:
116
effects/scanlines.py
Normal file
116
effects/scanlines.py
Normal file
@@ -0,0 +1,116 @@
|
||||
# /// script
|
||||
# requires-python = ">=3.10"
|
||||
# dependencies = ["numpy"]
|
||||
# ///
|
||||
"""
|
||||
@effect scanlines
|
||||
@version 1.0.0
|
||||
@author artdag
|
||||
|
||||
@description
|
||||
VHS/CRT-style scan line shift. Horizontally displaces alternating lines
|
||||
to create analog video distortion. Great for retro/glitch aesthetics.
|
||||
|
||||
@param amplitude float
|
||||
@range 0 100
|
||||
@default 10
|
||||
Maximum shift amount in pixels. Bind to bass for reactive glitch.
|
||||
|
||||
@param frequency float
|
||||
@range 1 100
|
||||
@default 10
|
||||
Lines per cycle (affects pattern density).
|
||||
|
||||
@param randomness float
|
||||
@range 0 1
|
||||
@default 0.5
|
||||
0 = regular sine pattern, 1 = fully random shifts.
|
||||
|
||||
@param line_gap int
|
||||
@range 1 20
|
||||
@default 1
|
||||
Only shift every Nth line (1 = all lines).
|
||||
|
||||
@param seed int
|
||||
@default 42
|
||||
Random seed for deterministic patterns.
|
||||
|
||||
@state rng DeterministicRNG
|
||||
Random number generator for reproducible results.
|
||||
|
||||
@example
|
||||
(effect scanlines :amplitude 20)
|
||||
|
||||
@example
|
||||
;; Heavy glitch on bass
|
||||
(effect scanlines :amplitude (bind bass :range [0 50]) :randomness 0.8)
|
||||
|
||||
@example
|
||||
;; Reproducible scanlines
|
||||
(effect scanlines :amplitude 30 :randomness 0.7 :seed 999)
|
||||
"""
|
||||
|
||||
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 scan line shift to a video frame.
|
||||
|
||||
Args:
|
||||
frame: Input frame as numpy array (H, W, 3) RGB uint8
|
||||
params: Effect parameters
|
||||
- amplitude: max shift in pixels (default 10)
|
||||
- frequency: lines per cycle (default 10)
|
||||
- randomness: 0-1 random vs sine (default 0.5)
|
||||
- line_gap: shift every Nth line (default 1)
|
||||
- seed: random seed (default 42)
|
||||
state: Persistent state dict
|
||||
- rng: DeterministicRNG instance
|
||||
|
||||
Returns:
|
||||
Tuple of (processed_frame, new_state)
|
||||
"""
|
||||
amplitude = params.get("amplitude", 10)
|
||||
frequency = params.get("frequency", 10) or 1
|
||||
randomness = params.get("randomness", 0.5)
|
||||
line_gap = max(1, int(params.get("line_gap", 1)))
|
||||
seed = int(params.get("seed", 42))
|
||||
|
||||
if state is None:
|
||||
state = {}
|
||||
|
||||
# Initialize RNG
|
||||
if "rng" not in state:
|
||||
state["rng"] = DeterministicRNG(seed)
|
||||
rng = state["rng"]
|
||||
|
||||
if amplitude == 0:
|
||||
return frame, state
|
||||
|
||||
h, w = frame.shape[:2]
|
||||
result = frame.copy()
|
||||
|
||||
for y in range(0, h, line_gap):
|
||||
# Calculate shift amount
|
||||
if randomness >= 1.0:
|
||||
shift = int(rng.uniform(-amplitude, amplitude))
|
||||
elif randomness <= 0:
|
||||
shift = int(amplitude * np.sin(2 * np.pi * y / frequency))
|
||||
else:
|
||||
sine_shift = amplitude * np.sin(2 * np.pi * y / frequency)
|
||||
rand_shift = rng.uniform(-amplitude, amplitude)
|
||||
shift = int(sine_shift * (1 - randomness) + rand_shift * randomness)
|
||||
|
||||
if shift != 0:
|
||||
result[y] = np.roll(result[y], shift, axis=0)
|
||||
|
||||
return result, state
|
||||
Reference in New Issue
Block a user