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>
117 lines
2.9 KiB
Python
117 lines
2.9 KiB
Python
# /// script
|
|
# requires-python = ">=3.10"
|
|
# dependencies = ["numpy"]
|
|
# ///
|
|
"""
|
|
@effect vignette
|
|
@version 1.0.0
|
|
@author artdag
|
|
|
|
@description
|
|
Vignette effect. Darkens the corners of the frame, drawing focus
|
|
to the center. Classic cinematic look.
|
|
|
|
@param strength float
|
|
@range 0 1
|
|
@default 0.5
|
|
How dark the corners get (0 = none, 1 = black corners).
|
|
|
|
@param radius float
|
|
@range 0.5 2
|
|
@default 1.0
|
|
Size of the bright center area. Smaller = more vignette.
|
|
|
|
@param softness float
|
|
@range 0.1 1
|
|
@default 0.5
|
|
How gradual the falloff is.
|
|
|
|
@param center_x float
|
|
@range 0 1
|
|
@default 0.5
|
|
Center X position.
|
|
|
|
@param center_y float
|
|
@range 0 1
|
|
@default 0.5
|
|
Center Y position.
|
|
|
|
@param color list
|
|
@default [0, 0, 0]
|
|
Vignette color (default black).
|
|
|
|
@example
|
|
(effect vignette :strength 0.6)
|
|
|
|
@example
|
|
;; Off-center vignette
|
|
(effect vignette :center_x 0.3 :center_y 0.3 :strength 0.7)
|
|
"""
|
|
|
|
import numpy as np
|
|
|
|
|
|
def process_frame(frame: np.ndarray, params: dict, state: dict) -> tuple:
|
|
"""
|
|
Apply vignette effect to a video frame.
|
|
|
|
Args:
|
|
frame: Input frame as numpy array (H, W, 3) RGB uint8
|
|
params: Effect parameters
|
|
- strength: darkness 0-1 (default 0.5)
|
|
- radius: center size 0.5-2 (default 1.0)
|
|
- softness: falloff gradient (default 0.5)
|
|
- center_x: center X 0-1 (default 0.5)
|
|
- center_y: center Y 0-1 (default 0.5)
|
|
- color: RGB tuple (default [0,0,0])
|
|
state: Persistent state dict
|
|
|
|
Returns:
|
|
Tuple of (processed_frame, new_state)
|
|
"""
|
|
strength = np.clip(params.get("strength", 0.5), 0, 1)
|
|
radius = max(0.5, min(params.get("radius", 1.0), 2))
|
|
softness = max(0.1, min(params.get("softness", 0.5), 1))
|
|
center_x = params.get("center_x", 0.5)
|
|
center_y = params.get("center_y", 0.5)
|
|
color = params.get("color", [0, 0, 0])
|
|
|
|
if state is None:
|
|
state = {}
|
|
|
|
if strength <= 0:
|
|
return frame, state
|
|
|
|
h, w = frame.shape[:2]
|
|
|
|
# Calculate center in pixels
|
|
cx = w * center_x
|
|
cy = h * center_y
|
|
|
|
# Create distance map from center
|
|
y_coords, x_coords = np.ogrid[:h, :w]
|
|
dist = np.sqrt((x_coords - cx)**2 + (y_coords - cy)**2)
|
|
|
|
# Normalize distance
|
|
max_dist = np.sqrt(cx**2 + cy**2) * radius
|
|
|
|
# Create vignette mask
|
|
normalized_dist = dist / max_dist
|
|
|
|
# Apply softness to the falloff
|
|
vignette_mask = 1 - np.clip((normalized_dist - (1 - softness)) / softness, 0, 1) * strength
|
|
|
|
# Apply vignette
|
|
if isinstance(color, (list, tuple)) and len(color) >= 3:
|
|
vignette_color = np.array(color[:3], dtype=np.float32)
|
|
else:
|
|
vignette_color = np.array([0, 0, 0], dtype=np.float32)
|
|
|
|
result = frame.astype(np.float32)
|
|
|
|
# Blend toward vignette color based on mask
|
|
for c in range(3):
|
|
result[:, :, c] = result[:, :, c] * vignette_mask + vignette_color[c] * (1 - vignette_mask)
|
|
|
|
return np.clip(result, 0, 255).astype(np.uint8), state
|