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>
116 lines
3.5 KiB
Python
116 lines
3.5 KiB
Python
# /// script
|
|
# requires-python = ">=3.10"
|
|
# dependencies = ["numpy", "opencv-python"]
|
|
# ///
|
|
"""
|
|
@effect chromatic
|
|
@version 1.0.0
|
|
@author artdag
|
|
|
|
@description
|
|
Chromatic aberration effect. Creates color fringing by offsetting
|
|
RGB channels radially from the center (lens distortion simulation).
|
|
|
|
@param strength float
|
|
@range 0 50
|
|
@default 10
|
|
Aberration strength. Bind to bass for reactive effect.
|
|
|
|
@param center_x float
|
|
@range 0 1
|
|
@default 0.5
|
|
Aberration center X.
|
|
|
|
@param center_y float
|
|
@range 0 1
|
|
@default 0.5
|
|
Aberration center Y.
|
|
|
|
@param radial bool
|
|
@default true
|
|
If true, aberration increases from center to edges.
|
|
|
|
@example
|
|
(effect chromatic :strength 20)
|
|
|
|
@example
|
|
;; Beat-reactive chromatic aberration
|
|
(effect chromatic :strength (bind bass :range [0 30]))
|
|
"""
|
|
|
|
import numpy as np
|
|
import cv2
|
|
|
|
|
|
def process_frame(frame: np.ndarray, params: dict, state: dict) -> tuple:
|
|
"""
|
|
Apply chromatic aberration effect to a video frame.
|
|
|
|
Args:
|
|
frame: Input frame as numpy array (H, W, 3) RGB uint8
|
|
params: Effect parameters
|
|
- strength: aberration amount (default 10)
|
|
- center_x: center X 0-1 (default 0.5)
|
|
- center_y: center Y 0-1 (default 0.5)
|
|
- radial: increase from center (default True)
|
|
state: Persistent state dict
|
|
|
|
Returns:
|
|
Tuple of (processed_frame, new_state)
|
|
"""
|
|
strength = params.get("strength", 10)
|
|
center_x = params.get("center_x", 0.5)
|
|
center_y = params.get("center_y", 0.5)
|
|
radial = params.get("radial", True)
|
|
|
|
if state is None:
|
|
state = {}
|
|
|
|
if strength == 0:
|
|
return frame, state
|
|
|
|
h, w = frame.shape[:2]
|
|
r, g, b = frame[:, :, 0], frame[:, :, 1], frame[:, :, 2]
|
|
|
|
if radial:
|
|
# Create distance-from-center map
|
|
y_coords, x_coords = np.ogrid[:h, :w]
|
|
cx, cy = w * center_x, h * center_y
|
|
dist = np.sqrt((x_coords - cx)**2 + (y_coords - cy)**2)
|
|
max_dist = np.sqrt(cx**2 + cy**2)
|
|
dist_normalized = (dist / max_dist).astype(np.float32)
|
|
|
|
# Create coordinate maps for remapping
|
|
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))
|
|
|
|
# Direction from center
|
|
dx = (map_x - cx) / (dist + 1e-6)
|
|
dy = (map_y - cy) / (dist + 1e-6)
|
|
|
|
# Apply radial offset to red channel (outward)
|
|
r_offset = strength * dist_normalized
|
|
r_map_x = (map_x + dx * r_offset).astype(np.float32)
|
|
r_map_y = (map_y + dy * r_offset).astype(np.float32)
|
|
r_shifted = cv2.remap(r, r_map_x, r_map_y,
|
|
cv2.INTER_LINEAR, borderMode=cv2.BORDER_REPLICATE)
|
|
|
|
# Apply radial offset to blue channel (inward)
|
|
b_offset = -strength * dist_normalized
|
|
b_map_x = (map_x + dx * b_offset).astype(np.float32)
|
|
b_map_y = (map_y + dy * b_offset).astype(np.float32)
|
|
b_shifted = cv2.remap(b, b_map_x, b_map_y,
|
|
cv2.INTER_LINEAR, borderMode=cv2.BORDER_REPLICATE)
|
|
|
|
return np.stack([r_shifted, g, b_shifted], axis=-1).astype(np.uint8), state
|
|
else:
|
|
# Simple uniform offset
|
|
offset = int(strength)
|
|
M_r = np.float32([[1, 0, offset], [0, 1, 0]])
|
|
M_b = np.float32([[1, 0, -offset], [0, 1, 0]])
|
|
|
|
r_shifted = cv2.warpAffine(r, M_r, (w, h), borderMode=cv2.BORDER_REPLICATE)
|
|
b_shifted = cv2.warpAffine(b, M_b, (w, h), borderMode=cv2.BORDER_REPLICATE)
|
|
|
|
return np.stack([r_shifted, g, b_shifted], axis=-1).astype(np.uint8), state
|