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>
142 lines
4.5 KiB
Python
142 lines
4.5 KiB
Python
# /// script
|
|
# requires-python = ">=3.10"
|
|
# dependencies = ["numpy"]
|
|
# ///
|
|
"""
|
|
@effect datamosh
|
|
@version 1.0.0
|
|
@author artdag
|
|
|
|
@description
|
|
Digital corruption / glitch block effect. Randomly corrupts rectangular
|
|
blocks by shifting, swapping, or duplicating from previous frames.
|
|
Simulates video compression artifacts.
|
|
|
|
@param block_size int
|
|
@range 8 128
|
|
@default 32
|
|
Size of corruption blocks in pixels.
|
|
|
|
@param corruption float
|
|
@range 0 1
|
|
@default 0.3
|
|
Probability of corrupting each block. Bind to energy for reactive glitch.
|
|
|
|
@param max_offset int
|
|
@range 0 200
|
|
@default 50
|
|
Maximum pixel offset when shifting blocks.
|
|
|
|
@param color_corrupt bool
|
|
@default true
|
|
Also apply color channel shifts to blocks.
|
|
|
|
@param seed int
|
|
@default 42
|
|
Random seed for deterministic glitch patterns.
|
|
|
|
@state previous_frame ndarray
|
|
Stores previous frame for frame-blending corruption.
|
|
|
|
@state rng DeterministicRNG
|
|
Random number generator for reproducible results.
|
|
|
|
@example
|
|
(effect datamosh :corruption 0.4)
|
|
|
|
@example
|
|
;; Heavy glitch on energy peaks
|
|
(effect datamosh :corruption (bind energy :range [0 0.8]) :block_size 16)
|
|
|
|
@example
|
|
;; Reproducible glitch with seed
|
|
(effect datamosh :corruption 0.5 :seed 12345)
|
|
"""
|
|
|
|
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 datamosh/glitch block effect to a video frame.
|
|
|
|
Args:
|
|
frame: Input frame as numpy array (H, W, 3) RGB uint8
|
|
params: Effect parameters
|
|
- block_size: corruption block size (default 32)
|
|
- corruption: probability 0-1 (default 0.3)
|
|
- max_offset: max shift in pixels (default 50)
|
|
- color_corrupt: apply color shifts (default True)
|
|
state: Persistent state dict
|
|
- previous_frame: last frame for duplication effect
|
|
|
|
Returns:
|
|
Tuple of (processed_frame, new_state)
|
|
"""
|
|
block_size = max(8, min(int(params.get("block_size", 32)), 128))
|
|
corruption = max(0, min(params.get("corruption", 0.3), 1))
|
|
max_offset = int(params.get("max_offset", 50))
|
|
color_corrupt = params.get("color_corrupt", True)
|
|
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 corruption == 0:
|
|
state["previous_frame"] = frame.copy()
|
|
return frame, state
|
|
|
|
h, w = frame.shape[:2]
|
|
result = frame.copy()
|
|
prev_frame = state.get("previous_frame")
|
|
|
|
# Process blocks
|
|
for by in range(0, h, block_size):
|
|
for bx in range(0, w, block_size):
|
|
bh = min(block_size, h - by)
|
|
bw = min(block_size, w - bx)
|
|
|
|
if rng.uniform() < corruption:
|
|
corruption_type = rng.choice(["shift", "duplicate", "color", "swap"])
|
|
|
|
if corruption_type == "shift" and max_offset > 0:
|
|
ox = rng.randint(-max_offset, max_offset)
|
|
oy = rng.randint(-max_offset, max_offset)
|
|
src_x = max(0, min(bx + ox, w - bw))
|
|
src_y = max(0, min(by + oy, h - bh))
|
|
result[by:by+bh, bx:bx+bw] = frame[src_y:src_y+bh, src_x:src_x+bw]
|
|
|
|
elif corruption_type == "duplicate" and prev_frame is not None:
|
|
if prev_frame.shape == frame.shape:
|
|
result[by:by+bh, bx:bx+bw] = prev_frame[by:by+bh, bx:bx+bw]
|
|
|
|
elif corruption_type == "color" and color_corrupt:
|
|
block = result[by:by+bh, bx:bx+bw].copy()
|
|
shift = rng.randint(1, 3)
|
|
channel = rng.randint(0, 2)
|
|
block[:, :, channel] = np.roll(block[:, :, channel], shift, axis=0)
|
|
result[by:by+bh, bx:bx+bw] = block
|
|
|
|
elif corruption_type == "swap":
|
|
other_bx = rng.randint(0, max(0, w - bw - 1))
|
|
other_by = rng.randint(0, max(0, h - bh - 1))
|
|
temp = result[by:by+bh, bx:bx+bw].copy()
|
|
result[by:by+bh, bx:bx+bw] = frame[other_by:other_by+bh, other_bx:other_bx+bw]
|
|
result[other_by:other_by+bh, other_bx:other_bx+bw] = temp
|
|
|
|
state["previous_frame"] = frame.copy()
|
|
return result, state
|