Files
test/effects/vignette.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

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