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:
152
effects/pixelsort.py
Normal file
152
effects/pixelsort.py
Normal file
@@ -0,0 +1,152 @@
|
||||
# /// script
|
||||
# requires-python = ">=3.10"
|
||||
# dependencies = ["numpy", "opencv-python"]
|
||||
# ///
|
||||
"""
|
||||
@effect pixelsort
|
||||
@version 1.0.0
|
||||
@author artdag
|
||||
|
||||
@description
|
||||
Pixel sorting glitch art effect. Sorts pixels within rows by brightness,
|
||||
hue, or other properties. Creates distinctive streaked/melted aesthetics.
|
||||
|
||||
@param sort_by string
|
||||
@enum lightness hue saturation red green blue
|
||||
@default lightness
|
||||
Property to sort pixels by.
|
||||
|
||||
@param threshold_low float
|
||||
@range 0 255
|
||||
@default 50
|
||||
Pixels darker than this are not sorted.
|
||||
|
||||
@param threshold_high float
|
||||
@range 0 255
|
||||
@default 200
|
||||
Pixels brighter than this are not sorted.
|
||||
|
||||
@param angle float
|
||||
@range 0 180
|
||||
@default 0
|
||||
Sort direction: 0 = horizontal, 90 = vertical.
|
||||
|
||||
@param reverse bool
|
||||
@default false
|
||||
Reverse the sort order.
|
||||
|
||||
@example
|
||||
(effect pixelsort)
|
||||
|
||||
@example
|
||||
;; Vertical pixel sort
|
||||
(effect pixelsort :angle 90)
|
||||
|
||||
@example
|
||||
;; Sort by hue for rainbow streaks
|
||||
(effect pixelsort :sort_by "hue" :threshold_low 20 :threshold_high 240)
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import cv2
|
||||
|
||||
|
||||
def process_frame(frame: np.ndarray, params: dict, state: dict) -> tuple:
|
||||
"""
|
||||
Apply pixel sorting to a video frame.
|
||||
|
||||
Args:
|
||||
frame: Input frame as numpy array (H, W, 3) RGB uint8
|
||||
params: Effect parameters
|
||||
- sort_by: property to sort by (default "lightness")
|
||||
- threshold_low: min brightness to sort (default 50)
|
||||
- threshold_high: max brightness to sort (default 200)
|
||||
- angle: 0 = horizontal, 90 = vertical (default 0)
|
||||
- reverse: reverse sort order (default False)
|
||||
state: Persistent state dict (unused)
|
||||
|
||||
Returns:
|
||||
Tuple of (processed_frame, new_state)
|
||||
"""
|
||||
sort_by = params.get("sort_by", "lightness")
|
||||
threshold_low = params.get("threshold_low", 50)
|
||||
threshold_high = params.get("threshold_high", 200)
|
||||
angle = params.get("angle", 0)
|
||||
reverse = params.get("reverse", False)
|
||||
|
||||
h, w = frame.shape[:2]
|
||||
|
||||
# Rotate for non-horizontal sorting
|
||||
if 45 <= (angle % 180) <= 135:
|
||||
frame = np.transpose(frame, (1, 0, 2))
|
||||
h, w = frame.shape[:2]
|
||||
rotated = True
|
||||
else:
|
||||
rotated = False
|
||||
|
||||
result = frame.copy()
|
||||
|
||||
# Get sort values
|
||||
sort_values = _get_sort_values(frame, sort_by)
|
||||
|
||||
# Create mask of pixels to sort
|
||||
mask = (sort_values >= threshold_low) & (sort_values <= threshold_high)
|
||||
|
||||
# Sort each row
|
||||
for y in range(h):
|
||||
row = result[y].copy()
|
||||
row_mask = mask[y]
|
||||
row_values = sort_values[y]
|
||||
|
||||
# Find contiguous segments to sort
|
||||
segments = _find_segments(row_mask)
|
||||
|
||||
for start, end in segments:
|
||||
if end - start > 1:
|
||||
segment_values = row_values[start:end]
|
||||
sort_indices = np.argsort(segment_values)
|
||||
if reverse:
|
||||
sort_indices = sort_indices[::-1]
|
||||
row[start:end] = row[start:end][sort_indices]
|
||||
|
||||
result[y] = row
|
||||
|
||||
# Rotate back if needed
|
||||
if rotated:
|
||||
result = np.transpose(result, (1, 0, 2))
|
||||
|
||||
return np.ascontiguousarray(result), state
|
||||
|
||||
|
||||
def _get_sort_values(frame, sort_by):
|
||||
"""Get values to sort pixels by."""
|
||||
if sort_by == "lightness":
|
||||
return cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY).astype(np.float32)
|
||||
elif sort_by == "hue":
|
||||
hsv = cv2.cvtColor(frame, cv2.COLOR_RGB2HSV)
|
||||
return hsv[:, :, 0].astype(np.float32)
|
||||
elif sort_by == "saturation":
|
||||
hsv = cv2.cvtColor(frame, cv2.COLOR_RGB2HSV)
|
||||
return hsv[:, :, 1].astype(np.float32)
|
||||
elif sort_by == "red":
|
||||
return frame[:, :, 0].astype(np.float32)
|
||||
elif sort_by == "green":
|
||||
return frame[:, :, 1].astype(np.float32)
|
||||
elif sort_by == "blue":
|
||||
return frame[:, :, 2].astype(np.float32)
|
||||
return cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY).astype(np.float32)
|
||||
|
||||
|
||||
def _find_segments(mask):
|
||||
"""Find contiguous True segments in mask."""
|
||||
segments = []
|
||||
start = None
|
||||
for i, val in enumerate(mask):
|
||||
if val and start is None:
|
||||
start = i
|
||||
elif not val and start is not None:
|
||||
segments.append((start, i))
|
||||
start = None
|
||||
if start is not None:
|
||||
segments.append((start, len(mask)))
|
||||
return segments
|
||||
Reference in New Issue
Block a user