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>
This commit is contained in:
137
effects/kaleidoscope.py
Normal file
137
effects/kaleidoscope.py
Normal file
@@ -0,0 +1,137 @@
|
||||
# /// script
|
||||
# requires-python = ">=3.10"
|
||||
# dependencies = ["numpy", "opencv-python"]
|
||||
# ///
|
||||
"""
|
||||
@effect kaleidoscope
|
||||
@version 1.0.0
|
||||
@author artdag
|
||||
|
||||
@description
|
||||
Kaleidoscope effect. Creates mesmerizing mandala-like patterns by
|
||||
dividing the frame into pie-slice segments and reflecting them.
|
||||
Great for psychedelic visuals.
|
||||
|
||||
@param segments int
|
||||
@range 3 16
|
||||
@default 6
|
||||
Number of symmetry segments.
|
||||
|
||||
@param rotation float
|
||||
@range 0 360
|
||||
@default 0
|
||||
Base rotation angle in degrees.
|
||||
|
||||
@param rotation_speed float
|
||||
@range -180 180
|
||||
@default 0
|
||||
Continuous rotation speed in degrees/second.
|
||||
|
||||
@param center_x float
|
||||
@range 0 1
|
||||
@default 0.5
|
||||
Center X position (0-1).
|
||||
|
||||
@param center_y float
|
||||
@range 0 1
|
||||
@default 0.5
|
||||
Center Y position (0-1).
|
||||
|
||||
@param zoom float
|
||||
@range 0.5 3.0
|
||||
@default 1.0
|
||||
Zoom factor for the source region.
|
||||
|
||||
@state cumulative_rotation float
|
||||
Tracks rotation over time.
|
||||
|
||||
@example
|
||||
(effect kaleidoscope :segments 8 :rotation_speed 30)
|
||||
|
||||
@example
|
||||
;; Beat-reactive segments
|
||||
(effect kaleidoscope :segments (bind bass :range [4 12]) :zoom 1.5)
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import cv2
|
||||
|
||||
|
||||
def process_frame(frame: np.ndarray, params: dict, state: dict) -> tuple:
|
||||
"""
|
||||
Apply kaleidoscope effect to a video frame.
|
||||
|
||||
Args:
|
||||
frame: Input frame as numpy array (H, W, 3) RGB uint8
|
||||
params: Effect parameters
|
||||
- segments: number of segments 3-16 (default 6)
|
||||
- rotation: base rotation degrees (default 0)
|
||||
- rotation_speed: degrees per second (default 0)
|
||||
- center_x: center X 0-1 (default 0.5)
|
||||
- center_y: center Y 0-1 (default 0.5)
|
||||
- zoom: zoom factor 0.5-3 (default 1.0)
|
||||
state: Persistent state dict
|
||||
|
||||
Returns:
|
||||
Tuple of (processed_frame, new_state)
|
||||
"""
|
||||
segments = max(3, min(int(params.get("segments", 6)), 16))
|
||||
rotation = params.get("rotation", 0)
|
||||
rotation_speed = params.get("rotation_speed", 0)
|
||||
center_x = params.get("center_x", 0.5)
|
||||
center_y = params.get("center_y", 0.5)
|
||||
zoom = max(0.5, min(params.get("zoom", 1.0), 3.0))
|
||||
|
||||
# Get time for animation
|
||||
t = params.get("_time", 0)
|
||||
|
||||
if state is None:
|
||||
state = {}
|
||||
|
||||
h, w = frame.shape[:2]
|
||||
|
||||
# Calculate center in pixels
|
||||
cx = int(w * center_x)
|
||||
cy = int(h * center_y)
|
||||
|
||||
# Total rotation including time-based animation
|
||||
total_rotation = rotation + rotation_speed * t
|
||||
|
||||
# Calculate the angle per segment
|
||||
segment_angle = 2 * np.pi / segments
|
||||
|
||||
# Create coordinate maps
|
||||
y_coords, x_coords = np.mgrid[0:h, 0:w].astype(np.float32)
|
||||
|
||||
# Translate to center
|
||||
x_centered = x_coords - cx
|
||||
y_centered = y_coords - cy
|
||||
|
||||
# Convert to polar coordinates
|
||||
r = np.sqrt(x_centered**2 + y_centered**2)
|
||||
theta = np.arctan2(y_centered, x_centered)
|
||||
|
||||
# Apply rotation
|
||||
theta = theta - np.deg2rad(total_rotation)
|
||||
|
||||
# Fold angle into first segment and mirror
|
||||
theta_normalized = theta % (2 * np.pi)
|
||||
segment_idx = (theta_normalized / segment_angle).astype(int)
|
||||
theta_in_segment = theta_normalized - segment_idx * segment_angle
|
||||
|
||||
# Mirror alternating segments
|
||||
mirror_mask = (segment_idx % 2) == 1
|
||||
theta_in_segment = np.where(mirror_mask, segment_angle - theta_in_segment, theta_in_segment)
|
||||
|
||||
# Apply zoom
|
||||
r = r / zoom
|
||||
|
||||
# Convert back to Cartesian (source coordinates)
|
||||
src_x = (r * np.cos(theta_in_segment) + cx).astype(np.float32)
|
||||
src_y = (r * np.sin(theta_in_segment) + cy).astype(np.float32)
|
||||
|
||||
# Remap
|
||||
result = cv2.remap(frame, src_x, src_y,
|
||||
cv2.INTER_LINEAR, borderMode=cv2.BORDER_REFLECT)
|
||||
|
||||
return result, state
|
||||
Reference in New Issue
Block a user