# /// script # requires-python = ">=3.10" # dependencies = ["numpy", "scipy"] # /// """ @effect cartoon @version 1.0.0 @author artdag @description Cartoon / Cel-shaded effect. Simplifies colors into flat regions and adds dark edge outlines for a hand-drawn cartoon appearance. @param detail float @range 0.1 1.0 @default 0.5 Edge detection sensitivity. Higher = more edges detected. @param edge_thickness int @range 1 5 @default 1 Outline thickness in pixels. @param color_levels int @range 2 32 @default 6 Number of color levels per channel. @param edge_color list @default [0, 0, 0] RGB color for edges (default black). @param blur_size int @range 0 10 @default 2 Pre-blur for smoother color regions. @example (effect cartoon :detail 0.6 :color_levels 4) @example ;; Thick outlines, fewer colors (effect cartoon :edge_thickness 3 :color_levels 3 :blur_size 4) """ import numpy as np from scipy import ndimage def process_frame(frame: np.ndarray, params: dict, state: dict) -> tuple: """ Apply cartoon effect to a video frame. Args: frame: Input frame as numpy array (H, W, 3) RGB uint8 params: Effect parameters - detail: edge sensitivity 0.1-1.0 (default 0.5) - edge_thickness: outline thickness (default 1) - color_levels: posterization levels (default 6) - edge_color: RGB tuple (default [0,0,0]) - blur_size: pre-blur amount (default 2) state: Persistent state dict Returns: Tuple of (processed_frame, new_state) """ detail = np.clip(params.get("detail", 0.5), 0.1, 1.0) edge_thickness = max(1, min(int(params.get("edge_thickness", 1)), 5)) color_levels = max(2, min(int(params.get("color_levels", 6)), 32)) edge_color = params.get("edge_color", [0, 0, 0]) blur_size = max(0, int(params.get("blur_size", 2))) if state is None: state = {} h, w = frame.shape[:2] result = frame.copy().astype(np.float32) # Step 1: Blur to reduce noise and create smoother regions if blur_size > 0: for c in range(3): result[:, :, c] = ndimage.uniform_filter(result[:, :, c], size=blur_size) # Step 2: Posterize colors (reduce to N levels) step = 256 / color_levels result = (np.floor(result / step) * step).astype(np.uint8) # Step 3: Detect edges using Sobel gray = np.mean(frame, axis=2).astype(np.float32) sobel_x = ndimage.sobel(gray, axis=1) sobel_y = ndimage.sobel(gray, axis=0) edges = np.sqrt(sobel_x**2 + sobel_y**2) # Normalize and threshold edge_max = edges.max() if edge_max > 0: edges = edges / edge_max edge_threshold = 1.0 - detail edge_mask = edges > edge_threshold # Dilate edges for thickness if edge_thickness > 1: struct = ndimage.generate_binary_structure(2, 1) for _ in range(edge_thickness - 1): edge_mask = ndimage.binary_dilation(edge_mask, structure=struct) # Step 4: Apply edge color if isinstance(edge_color, (list, tuple)) and len(edge_color) >= 3: color = np.array(edge_color[:3], dtype=np.uint8) else: color = np.array([0, 0, 0], dtype=np.uint8) result[edge_mask] = color return result, state