# /// script # requires-python = ">=3.10" # dependencies = ["numpy", "opencv-python"] # /// """ @effect color_grade @version 1.0.0 @author artdag @description Color grading effect. Applies cinematic color adjustments including shadows/midtones/highlights tinting, lift/gamma/gain, and temperature. @param shadows list @default [0, 0, 0] RGB tint for dark areas. @param midtones list @default [0, 0, 0] RGB tint for middle tones. @param highlights list @default [0, 0, 0] RGB tint for bright areas. @param lift float @range -0.5 0.5 @default 0 Raise/lower shadow levels. @param gamma float @range 0.5 2 @default 1 Midtone brightness curve. @param gain float @range 0.5 2 @default 1 Highlight intensity. @param temperature float @range -100 100 @default 0 Color temperature (-100 = cool/blue, +100 = warm/orange). @param tint float @range -100 100 @default 0 Green/magenta tint (-100 = green, +100 = magenta). @example (effect color_grade :temperature 30 :shadows [0 0 20]) @example ;; Cinematic teal-orange look (effect color_grade :shadows [0 10 20] :highlights [20 10 0]) """ import numpy as np import cv2 def process_frame(frame: np.ndarray, params: dict, state: dict) -> tuple: """ Apply color grading to a video frame. Args: frame: Input frame as numpy array (H, W, 3) RGB uint8 params: Effect parameters state: Persistent state dict Returns: Tuple of (processed_frame, new_state) """ shadows = params.get("shadows", [0, 0, 0]) midtones = params.get("midtones", [0, 0, 0]) highlights = params.get("highlights", [0, 0, 0]) lift = params.get("lift", 0) gamma = max(0.5, min(params.get("gamma", 1), 2)) gain = max(0.5, min(params.get("gain", 1), 2)) temperature = params.get("temperature", 0) tint = params.get("tint", 0) if state is None: state = {} result = frame.astype(np.float32) / 255.0 # Apply lift (shadows) result = result + lift # Apply gamma (midtones) result = np.power(np.clip(result, 0.001, 1), 1 / gamma) # Apply gain (highlights) result = result * gain # Convert tints to float if isinstance(shadows, (list, tuple)) and len(shadows) >= 3: shadows = np.array(shadows[:3], dtype=np.float32) / 255.0 else: shadows = np.zeros(3, dtype=np.float32) if isinstance(midtones, (list, tuple)) and len(midtones) >= 3: midtones = np.array(midtones[:3], dtype=np.float32) / 255.0 else: midtones = np.zeros(3, dtype=np.float32) if isinstance(highlights, (list, tuple)) and len(highlights) >= 3: highlights = np.array(highlights[:3], dtype=np.float32) / 255.0 else: highlights = np.zeros(3, dtype=np.float32) # Calculate luminance for zone-based grading lum = 0.299 * result[:, :, 0] + 0.587 * result[:, :, 1] + 0.114 * result[:, :, 2] # Create zone masks shadow_mask = np.clip(1 - lum * 3, 0, 1)[:, :, np.newaxis] highlight_mask = np.clip((lum - 0.67) * 3, 0, 1)[:, :, np.newaxis] midtone_mask = 1 - shadow_mask - highlight_mask # Apply zone tints for c in range(3): result[:, :, c] += shadows[c] * shadow_mask[:, :, 0] result[:, :, c] += midtones[c] * midtone_mask[:, :, 0] result[:, :, c] += highlights[c] * highlight_mask[:, :, 0] # Apply temperature (blue <-> orange) if temperature != 0: temp_shift = temperature / 100.0 result[:, :, 0] += temp_shift * 0.3 # Red result[:, :, 2] -= temp_shift * 0.3 # Blue # Apply tint (green <-> magenta) if tint != 0: tint_shift = tint / 100.0 result[:, :, 1] -= tint_shift * 0.2 # Green return (np.clip(result, 0, 1) * 255).astype(np.uint8), state