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>
135 lines
3.7 KiB
Python
135 lines
3.7 KiB
Python
# /// script
|
|
# requires-python = ">=3.10"
|
|
# dependencies = ["numpy", "opencv-python"]
|
|
# ///
|
|
"""
|
|
@effect tile_grid
|
|
@version 1.0.0
|
|
@author artdag
|
|
|
|
@description
|
|
Tile Grid effect. Repeats the frame in a grid pattern creating
|
|
a mosaic by tiling scaled-down copies. Great for psychedelic visuals.
|
|
|
|
@param rows int
|
|
@range 1 10
|
|
@default 2
|
|
Number of rows in grid.
|
|
|
|
@param cols int
|
|
@range 1 10
|
|
@default 2
|
|
Number of columns in grid.
|
|
|
|
@param gap int
|
|
@range 0 50
|
|
@default 0
|
|
Gap between tiles in pixels.
|
|
|
|
@param gap_color list
|
|
@default [0, 0, 0]
|
|
RGB color for gaps.
|
|
|
|
@param rotation_per_tile float
|
|
@range -180 180
|
|
@default 0
|
|
Rotation increment per tile in degrees.
|
|
|
|
@param alternate_flip bool
|
|
@default false
|
|
Flip alternating tiles horizontally.
|
|
|
|
@example
|
|
(effect tile_grid :rows 3 :cols 3)
|
|
|
|
@example
|
|
;; Rotating tiles
|
|
(effect tile_grid :rows 2 :cols 2 :rotation_per_tile 90)
|
|
|
|
@example
|
|
;; Beat-reactive grid
|
|
(effect tile_grid :rows (bind bass :range [2 6]) :cols (bind bass :range [2 6]))
|
|
"""
|
|
|
|
import numpy as np
|
|
import cv2
|
|
|
|
|
|
def process_frame(frame: np.ndarray, params: dict, state: dict) -> tuple:
|
|
"""
|
|
Apply tile grid effect to a video frame.
|
|
|
|
Args:
|
|
frame: Input frame as numpy array (H, W, 3) RGB uint8
|
|
params: Effect parameters
|
|
- rows: number of rows (default 2)
|
|
- cols: number of columns (default 2)
|
|
- gap: gap between tiles (default 0)
|
|
- gap_color: RGB tuple (default [0,0,0])
|
|
- rotation_per_tile: rotation increment (default 0)
|
|
- alternate_flip: flip alternating tiles (default False)
|
|
state: Persistent state dict
|
|
|
|
Returns:
|
|
Tuple of (processed_frame, new_state)
|
|
"""
|
|
rows = max(1, min(int(params.get("rows", 2)), 10))
|
|
cols = max(1, min(int(params.get("cols", 2)), 10))
|
|
gap = max(0, int(params.get("gap", 0)))
|
|
gap_color = params.get("gap_color", [0, 0, 0])
|
|
rotation_per_tile = params.get("rotation_per_tile", 0)
|
|
alternate_flip = params.get("alternate_flip", False)
|
|
|
|
if state is None:
|
|
state = {}
|
|
|
|
h, w = frame.shape[:2]
|
|
|
|
# Calculate tile size
|
|
tile_w = (w - gap * (cols - 1)) // cols
|
|
tile_h = (h - gap * (rows - 1)) // rows
|
|
|
|
if tile_w <= 0 or tile_h <= 0:
|
|
return frame, state
|
|
|
|
# Scale down the original frame to tile size
|
|
tile = cv2.resize(frame, (tile_w, tile_h), interpolation=cv2.INTER_LINEAR)
|
|
|
|
# Create result with gap color
|
|
if isinstance(gap_color, (list, tuple)) and len(gap_color) >= 3:
|
|
result = np.full((h, w, 3), gap_color[:3], dtype=np.uint8)
|
|
else:
|
|
result = np.zeros((h, w, 3), dtype=np.uint8)
|
|
|
|
# Place tiles
|
|
tile_idx = 0
|
|
for row in range(rows):
|
|
for col in range(cols):
|
|
y = row * (tile_h + gap)
|
|
x = col * (tile_w + gap)
|
|
|
|
current_tile = tile.copy()
|
|
|
|
# Apply rotation if specified
|
|
if rotation_per_tile != 0:
|
|
angle = rotation_per_tile * tile_idx
|
|
center = (tile_w // 2, tile_h // 2)
|
|
M = cv2.getRotationMatrix2D(center, angle, 1.0)
|
|
current_tile = cv2.warpAffine(current_tile, M, (tile_w, tile_h),
|
|
borderMode=cv2.BORDER_REFLECT)
|
|
|
|
# Apply flip for alternating tiles
|
|
if alternate_flip and (row + col) % 2 == 1:
|
|
current_tile = cv2.flip(current_tile, 1)
|
|
|
|
# Place tile
|
|
y_end = min(y + tile_h, h)
|
|
x_end = min(x + tile_w, w)
|
|
tile_crop_h = y_end - y
|
|
tile_crop_w = x_end - x
|
|
result[y:y_end, x:x_end] = current_tile[:tile_crop_h, :tile_crop_w]
|
|
|
|
tile_idx += 1
|
|
|
|
return result, state
|