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>
105 lines
2.8 KiB
Python
105 lines
2.8 KiB
Python
# /// script
|
|
# requires-python = ">=3.10"
|
|
# dependencies = ["numpy", "opencv-python"]
|
|
# ///
|
|
"""
|
|
@effect color_cycle
|
|
@version 1.0.0
|
|
@author artdag
|
|
|
|
@description
|
|
Color cycle effect. Shifts all hues over time creating psychedelic
|
|
rainbow cycling. Great for trippy visuals.
|
|
|
|
@param speed float
|
|
@range 0 10
|
|
@default 1
|
|
Cycle speed (rotations per second).
|
|
|
|
@param offset float
|
|
@range 0 360
|
|
@default 0
|
|
Initial hue offset in degrees.
|
|
|
|
@param saturation_boost float
|
|
@range 0 2
|
|
@default 1
|
|
Saturation multiplier.
|
|
|
|
@param mode string
|
|
@enum all highlights shadows midtones
|
|
@default all
|
|
Which tones to affect.
|
|
|
|
@example
|
|
(effect color_cycle :speed 0.5)
|
|
|
|
@example
|
|
;; Beat-synced color shift
|
|
(effect color_cycle :offset (bind beat_position :range [0 360]))
|
|
"""
|
|
|
|
import numpy as np
|
|
import cv2
|
|
|
|
|
|
def process_frame(frame: np.ndarray, params: dict, state: dict) -> tuple:
|
|
"""
|
|
Apply color cycle effect to a video frame.
|
|
|
|
Args:
|
|
frame: Input frame as numpy array (H, W, 3) RGB uint8
|
|
params: Effect parameters
|
|
- speed: rotations per second (default 1)
|
|
- offset: initial hue offset (default 0)
|
|
- saturation_boost: saturation multiplier (default 1)
|
|
- mode: which tones to affect (default all)
|
|
state: Persistent state dict
|
|
|
|
Returns:
|
|
Tuple of (processed_frame, new_state)
|
|
"""
|
|
speed = params.get("speed", 1)
|
|
offset = params.get("offset", 0)
|
|
saturation_boost = max(0, min(params.get("saturation_boost", 1), 2))
|
|
mode = params.get("mode", "all")
|
|
t = params.get("_time", 0)
|
|
|
|
if state is None:
|
|
state = {}
|
|
|
|
# Calculate hue shift
|
|
hue_shift = int((offset + speed * t * 360) % 360)
|
|
|
|
# Convert to HSV (OpenCV uses BGR, our frame is RGB)
|
|
frame_bgr = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)
|
|
hsv = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2HSV).astype(np.float32)
|
|
|
|
if mode == "all":
|
|
# Shift all hues
|
|
hsv[:, :, 0] = (hsv[:, :, 0] + hue_shift / 2) % 180
|
|
hsv[:, :, 1] = np.clip(hsv[:, :, 1] * saturation_boost, 0, 255)
|
|
else:
|
|
# Calculate luminance mask
|
|
lum = hsv[:, :, 2] / 255.0
|
|
|
|
if mode == "highlights":
|
|
mask = np.clip((lum - 0.67) * 3, 0, 1)
|
|
elif mode == "shadows":
|
|
mask = np.clip(1 - lum * 3, 0, 1)
|
|
else: # midtones
|
|
shadow_mask = np.clip(1 - lum * 3, 0, 1)
|
|
highlight_mask = np.clip((lum - 0.67) * 3, 0, 1)
|
|
mask = 1 - shadow_mask - highlight_mask
|
|
|
|
# Apply selective hue shift
|
|
shifted_hue = (hsv[:, :, 0] + hue_shift / 2) % 180
|
|
hsv[:, :, 0] = hsv[:, :, 0] * (1 - mask) + shifted_hue * mask
|
|
|
|
# Convert back
|
|
hsv = np.clip(hsv, 0, 255).astype(np.uint8)
|
|
result_bgr = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)
|
|
result = cv2.cvtColor(result_bgr, cv2.COLOR_BGR2RGB)
|
|
|
|
return result, state
|