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:
148
effects/film_grain.py
Normal file
148
effects/film_grain.py
Normal file
@@ -0,0 +1,148 @@
|
||||
# /// script
|
||||
# requires-python = ">=3.10"
|
||||
# dependencies = ["numpy", "opencv-python"]
|
||||
# ///
|
||||
"""
|
||||
@effect film_grain
|
||||
@version 1.0.0
|
||||
@author artdag
|
||||
|
||||
@description
|
||||
Film grain / Noise effect. Adds realistic film grain texture.
|
||||
Great for vintage aesthetics and subtle texture.
|
||||
|
||||
@param intensity float
|
||||
@range 0 1
|
||||
@default 0.2
|
||||
Noise intensity. Bind to energy for reactive grain.
|
||||
|
||||
@param grain_size float
|
||||
@range 0.5 5
|
||||
@default 1.0
|
||||
Size of grain particles. Larger = coarser grain.
|
||||
|
||||
@param colored bool
|
||||
@default false
|
||||
Use colored noise instead of monochrome.
|
||||
|
||||
@param temporal_variation float
|
||||
@range 0 1
|
||||
@default 1.0
|
||||
How much grain changes frame-to-frame. 0 = static, 1 = full animation.
|
||||
|
||||
@param seed int
|
||||
@default 42
|
||||
Random seed for reproducible grain.
|
||||
|
||||
@state rng DeterministicRNG
|
||||
Random number generator for consistent grain.
|
||||
|
||||
@example
|
||||
(effect film_grain :intensity 0.3)
|
||||
|
||||
@example
|
||||
;; Colored grain, reactive to energy
|
||||
(effect film_grain :intensity (bind energy :range [0.1 0.5]) :colored true)
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import cv2
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
# Import DeterministicRNG from same directory
|
||||
_effects_dir = Path(__file__).parent
|
||||
if str(_effects_dir) not in sys.path:
|
||||
sys.path.insert(0, str(_effects_dir))
|
||||
from random import DeterministicRNG
|
||||
|
||||
|
||||
def process_frame(frame: np.ndarray, params: dict, state: dict) -> tuple:
|
||||
"""
|
||||
Apply film grain effect to a video frame.
|
||||
|
||||
Args:
|
||||
frame: Input frame as numpy array (H, W, 3) RGB uint8
|
||||
params: Effect parameters
|
||||
- intensity: noise intensity 0-1 (default 0.2)
|
||||
- grain_size: grain particle size (default 1.0)
|
||||
- colored: use colored noise (default False)
|
||||
- temporal_variation: grain animation 0-1 (default 1.0)
|
||||
- seed: random seed (default 42)
|
||||
state: Persistent state dict
|
||||
- rng: DeterministicRNG instance
|
||||
- frame_count: frame counter
|
||||
|
||||
Returns:
|
||||
Tuple of (processed_frame, new_state)
|
||||
"""
|
||||
intensity = params.get("intensity", 0.2)
|
||||
grain_size = max(0.5, params.get("grain_size", 1.0))
|
||||
colored = params.get("colored", False)
|
||||
temporal_var = params.get("temporal_variation", 1.0)
|
||||
seed = int(params.get("seed", 42))
|
||||
|
||||
if state is None:
|
||||
state = {}
|
||||
|
||||
if intensity <= 0:
|
||||
return frame, state
|
||||
|
||||
# Initialize RNG
|
||||
if "rng" not in state:
|
||||
state["rng"] = DeterministicRNG(seed)
|
||||
state["frame_count"] = 0
|
||||
state["static_noise"] = None
|
||||
|
||||
rng = state["rng"]
|
||||
frame_count = state["frame_count"]
|
||||
|
||||
h, w = frame.shape[:2]
|
||||
|
||||
# Calculate noise dimensions
|
||||
if grain_size > 1:
|
||||
noise_h = max(1, int(h / grain_size))
|
||||
noise_w = max(1, int(w / grain_size))
|
||||
else:
|
||||
noise_h, noise_w = h, w
|
||||
|
||||
# Generate noise based on temporal variation
|
||||
if temporal_var >= 1.0 or state.get("static_noise") is None:
|
||||
# Generate new noise
|
||||
if colored:
|
||||
noise = np.array([[[rng.gaussian(0, 1) for _ in range(3)]
|
||||
for _ in range(noise_w)]
|
||||
for _ in range(noise_h)]) * intensity * 50
|
||||
else:
|
||||
noise_2d = np.array([[rng.gaussian(0, 1)
|
||||
for _ in range(noise_w)]
|
||||
for _ in range(noise_h)]) * intensity * 50
|
||||
noise = np.stack([noise_2d, noise_2d, noise_2d], axis=-1)
|
||||
|
||||
if temporal_var < 1.0:
|
||||
state["static_noise"] = noise
|
||||
else:
|
||||
# Blend static and new noise
|
||||
static = state["static_noise"]
|
||||
if colored:
|
||||
new_noise = np.array([[[rng.gaussian(0, 1) for _ in range(3)]
|
||||
for _ in range(noise_w)]
|
||||
for _ in range(noise_h)]) * intensity * 50
|
||||
else:
|
||||
noise_2d = np.array([[rng.gaussian(0, 1)
|
||||
for _ in range(noise_w)]
|
||||
for _ in range(noise_h)]) * intensity * 50
|
||||
new_noise = np.stack([noise_2d, noise_2d, noise_2d], axis=-1)
|
||||
|
||||
noise = static * (1 - temporal_var) + new_noise * temporal_var
|
||||
|
||||
# Scale noise up if using larger grain
|
||||
if grain_size > 1:
|
||||
noise = cv2.resize(noise.astype(np.float32), (w, h), interpolation=cv2.INTER_LINEAR)
|
||||
|
||||
# Add noise to frame
|
||||
result = frame.astype(np.float32) + noise
|
||||
|
||||
state["frame_count"] = frame_count + 1
|
||||
|
||||
return np.clip(result, 0, 255).astype(np.uint8), state
|
||||
Reference in New Issue
Block a user