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>
109 lines
2.8 KiB
Python
109 lines
2.8 KiB
Python
# /// script
|
|
# requires-python = ">=3.10"
|
|
# dependencies = ["numpy", "opencv-python"]
|
|
# ///
|
|
"""
|
|
@effect bloom
|
|
@version 1.0.0
|
|
@author artdag
|
|
|
|
@description
|
|
Bloom effect. Creates a soft glow around bright areas, simulating
|
|
camera lens bloom. Great for dreamy or ethereal looks.
|
|
|
|
@param intensity float
|
|
@range 0 2
|
|
@default 0.5
|
|
Bloom brightness.
|
|
|
|
@param threshold int
|
|
@range 0 255
|
|
@default 200
|
|
Brightness threshold for bloom (pixels above this glow).
|
|
|
|
@param radius int
|
|
@range 1 50
|
|
@default 15
|
|
Blur radius for the glow.
|
|
|
|
@param soft_threshold bool
|
|
@default true
|
|
Use soft threshold (gradual) vs hard threshold.
|
|
|
|
@param color_tint list
|
|
@default [255, 255, 255]
|
|
Tint color for the bloom.
|
|
|
|
@example
|
|
(effect bloom :intensity 0.7 :threshold 180)
|
|
|
|
@example
|
|
;; Warm bloom
|
|
(effect bloom :intensity 0.6 :color_tint [255 200 150])
|
|
"""
|
|
|
|
import numpy as np
|
|
import cv2
|
|
|
|
|
|
def process_frame(frame: np.ndarray, params: dict, state: dict) -> tuple:
|
|
"""
|
|
Apply bloom effect to a video frame.
|
|
|
|
Args:
|
|
frame: Input frame as numpy array (H, W, 3) RGB uint8
|
|
params: Effect parameters
|
|
- intensity: bloom brightness (default 0.5)
|
|
- threshold: brightness cutoff (default 200)
|
|
- radius: blur radius (default 15)
|
|
- soft_threshold: gradual vs hard (default True)
|
|
- color_tint: RGB tint (default white)
|
|
state: Persistent state dict
|
|
|
|
Returns:
|
|
Tuple of (processed_frame, new_state)
|
|
"""
|
|
intensity = params.get("intensity", 0.5)
|
|
threshold = int(params.get("threshold", 200))
|
|
radius = max(1, int(params.get("radius", 15)))
|
|
soft_threshold = params.get("soft_threshold", True)
|
|
color_tint = params.get("color_tint", [255, 255, 255])
|
|
|
|
if state is None:
|
|
state = {}
|
|
|
|
if intensity <= 0:
|
|
return frame, state
|
|
|
|
# Convert to float
|
|
result = frame.astype(np.float32)
|
|
|
|
# Get brightness (luminance)
|
|
lum = 0.299 * result[:, :, 0] + 0.587 * result[:, :, 1] + 0.114 * result[:, :, 2]
|
|
|
|
# Create bloom mask
|
|
if soft_threshold:
|
|
# Soft threshold - gradual falloff
|
|
bloom_mask = np.clip((lum - threshold) / (255 - threshold + 1e-6), 0, 1)
|
|
else:
|
|
# Hard threshold
|
|
bloom_mask = (lum > threshold).astype(np.float32)
|
|
|
|
# Extract bright areas
|
|
bloom = result * bloom_mask[:, :, np.newaxis]
|
|
|
|
# Apply blur to create glow
|
|
ksize = radius * 2 + 1
|
|
bloom = cv2.GaussianBlur(bloom, (ksize, ksize), 0)
|
|
|
|
# Apply color tint
|
|
if isinstance(color_tint, (list, tuple)) and len(color_tint) >= 3:
|
|
tint = np.array(color_tint[:3], dtype=np.float32) / 255.0
|
|
for c in range(3):
|
|
bloom[:, :, c] *= tint[c]
|
|
|
|
# Add bloom to original (screen blend)
|
|
result = result + bloom * intensity
|
|
|
|
return np.clip(result, 0, 255).astype(np.uint8), state
|