Files
test/effects/datamosh.py
gilesb 406cc7c0c7 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>
2026-01-19 12:34:45 +00:00

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