""" StencilCV - Traditional Computer Vision Approach to Stencil Generation This module uses classical computer vision techniques (edge detection, contours, thresholding) to convert images into clean stencils. No AI required - fast, deterministic, and reliable. Approaches: 1. Edge detection (Canny) - creates outline stencils 2. Contour extraction - creates filled silhouette stencils 3. Threshold-based - converts to binary black/white """ import cv2 import numpy as np from PIL import Image from typing import Union, Literal import os class StencilCV: """ Computer vision-based stencil generator. Converts any input image (photo, drawing, etc.) into a clean stencil using traditional image processing techniques. """ def __init__(self): """Initialize the StencilCV processor.""" pass def load_image(self, image_path: str) -> np.ndarray: """ Load an image from file. Args: image_path: Path to input image Returns: Image as numpy array (BGR format) """ img = cv2.imread(image_path) if img is None: raise ValueError(f"Could not load image from {image_path}") return img def from_pil(self, pil_image: Image.Image) -> np.ndarray: """ Convert PIL Image to OpenCV format. Args: pil_image: PIL Image object Returns: Image as numpy array (BGR format) """ # Convert PIL (RGB) to OpenCV (BGR) return cv2.cvtColor(np.array(pil_image), cv2.COLOR_RGB2BGR) def to_pil(self, cv_image: np.ndarray) -> Image.Image: """ Convert OpenCV image to PIL Image. Args: cv_image: OpenCV image (BGR or grayscale) Returns: PIL Image object """ if len(cv_image.shape) == 2: # Grayscale return Image.fromarray(cv_image) else: # BGR to RGB return Image.fromarray(cv2.cvtColor(cv_image, cv2.COLOR_BGR2RGB)) def edge_stencil( self, image: Union[str, np.ndarray, Image.Image], blur_kernel: int = 5, canny_low: int = 50, canny_high: int = 150, line_thickness: int = 2, invert: bool = False ) -> Image.Image: """ Create outline stencil using Canny edge detection. Perfect for line art, coloring book style stencils. Args: image: Input image (path, numpy array, or PIL Image) blur_kernel: Gaussian blur kernel size (reduces noise) canny_low: Canny edge detection low threshold canny_high: Canny edge detection high threshold line_thickness: Thickness of edge lines invert: If True, white lines on black; if False, black lines on white Returns: PIL Image with edge-based stencil """ # Load image if isinstance(image, str): img = self.load_image(image) elif isinstance(image, Image.Image): img = self.from_pil(image) else: img = image.copy() # Convert to grayscale gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # Apply Gaussian blur to reduce noise blurred = cv2.GaussianBlur(gray, (blur_kernel, blur_kernel), 0) # Apply Canny edge detection edges = cv2.Canny(blurred, canny_low, canny_high) # Dilate edges to make them thicker if requested if line_thickness > 1: kernel = np.ones((line_thickness, line_thickness), np.uint8) edges = cv2.dilate(edges, kernel, iterations=1) # Invert if needed (black on white by default) if not invert: edges = cv2.bitwise_not(edges) # Convert to RGB result = cv2.cvtColor(edges, cv2.COLOR_GRAY2RGB) return self.to_pil(result) def silhouette_stencil( self, image: Union[str, np.ndarray, Image.Image], threshold_method: Literal['otsu', 'adaptive', 'simple'] = 'otsu', threshold_value: int = 127, blur_kernel: int = 5, fill_holes: bool = True, remove_small_objects: int = 500, smooth_edges: bool = True ) -> Image.Image: """ Create filled silhouette stencil using thresholding and contours. Perfect for solid stencils, like spray paint templates. Args: image: Input image (path, numpy array, or PIL Image) threshold_method: Method for thresholding ('otsu', 'adaptive', 'simple') threshold_value: Threshold value for 'simple' method (0-255) blur_kernel: Gaussian blur kernel size fill_holes: Fill holes in the silhouette remove_small_objects: Remove objects smaller than this (pixels) smooth_edges: Apply morphological smoothing to edges Returns: PIL Image with silhouette stencil """ # Load image if isinstance(image, str): img = self.load_image(image) elif isinstance(image, Image.Image): img = self.from_pil(image) else: img = image.copy() # Convert to grayscale gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # Apply Gaussian blur to reduce noise blurred = cv2.GaussianBlur(gray, (blur_kernel, blur_kernel), 0) # Apply thresholding based on method if threshold_method == 'otsu': # Otsu's method automatically finds optimal threshold _, binary = cv2.threshold(blurred, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) elif threshold_method == 'adaptive': # Adaptive threshold - good for varying lighting binary = cv2.adaptiveThreshold( blurred, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2 ) else: # simple _, binary = cv2.threshold(blurred, threshold_value, 255, cv2.THRESH_BINARY) # Ensure we have black subject on white background # (Check if more white than black, invert if needed) if np.mean(binary) < 127: binary = cv2.bitwise_not(binary) # Remove small objects (noise) if remove_small_objects > 0: # Find contours contours, _ = cv2.findContours( cv2.bitwise_not(binary), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE ) # Filter contours by area and redraw mask = np.ones_like(binary) * 255 for contour in contours: area = cv2.contourArea(contour) if area >= remove_small_objects: cv2.drawContours(mask, [contour], -1, 0, -1) binary = mask # Fill holes in the silhouette if fill_holes: # Find contours contours, hierarchy = cv2.findContours( cv2.bitwise_not(binary), cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE ) # Fill all contours (including holes) mask = np.ones_like(binary) * 255 for i, contour in enumerate(contours): # Only draw external contours (fill holes) if hierarchy[0][i][3] == -1: # External contour cv2.drawContours(mask, [contour], -1, 0, -1) binary = mask # Smooth edges using morphological operations if smooth_edges: kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) binary = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel) binary = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel) # Convert to RGB result = cv2.cvtColor(binary, cv2.COLOR_GRAY2RGB) return self.to_pil(result) def hybrid_stencil( self, image: Union[str, np.ndarray, Image.Image], show_edges: bool = True, show_fill: bool = True, edge_thickness: int = 2 ) -> Image.Image: """ Create hybrid stencil with both edges and filled regions. Combines edge detection with silhouette for detailed stencils. Args: image: Input image (path, numpy array, or PIL Image) show_edges: Include edge lines show_fill: Include filled silhouette edge_thickness: Thickness of edge lines Returns: PIL Image with hybrid stencil """ # Load image if isinstance(image, str): img = self.load_image(image) elif isinstance(image, Image.Image): img = self.from_pil(image) else: img = image.copy() # Start with white canvas result = np.ones_like(cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)) * 255 # Add filled silhouette if requested if show_fill: silhouette = self.silhouette_stencil(img) silhouette_cv = self.from_pil(silhouette) silhouette_gray = cv2.cvtColor(silhouette_cv, cv2.COLOR_BGR2GRAY) result = cv2.bitwise_and(result, silhouette_gray) # Add edges if requested if show_edges: edges = self.edge_stencil(img, line_thickness=edge_thickness, invert=True) edges_cv = self.from_pil(edges) edges_gray = cv2.cvtColor(edges_cv, cv2.COLOR_BGR2GRAY) result = cv2.bitwise_and(result, edges_gray) # Convert to RGB result_rgb = cv2.cvtColor(result, cv2.COLOR_GRAY2RGB) return self.to_pil(result_rgb) def auto_stencil( self, image: Union[str, np.ndarray, Image.Image], style: Literal['outline', 'filled', 'hybrid'] = 'filled' ) -> Image.Image: """ Automatically create a stencil with sensible defaults. Args: image: Input image (path, numpy array, or PIL Image) style: Style of stencil ('outline', 'filled', 'hybrid') Returns: PIL Image with stencil """ if style == 'outline': return self.edge_stencil(image) elif style == 'filled': return self.silhouette_stencil(image) else: # hybrid return self.hybrid_stencil(image) def save(self, image: Image.Image, output_path: str): """ Save stencil image to file. Args: image: PIL Image to save output_path: Path to save image """ os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True) image.save(output_path) print(f"Saved stencil to: {output_path}") def main(): """Example usage demonstrating different stencil styles.""" print("StencilCV - Computer Vision Stencil Generator") print("=" * 60) # Note: You'll need input images to test this # For now, let's create a simple example # Create a sample image (circle) print("\nCreating sample image...") sample = np.ones((512, 512, 3), dtype=np.uint8) * 255 cv2.circle(sample, (256, 256), 100, (0, 0, 0), -1) sample_path = "sample_input.png" cv2.imwrite(sample_path, sample) print(f"Created sample image: {sample_path}") # Initialize processor processor = StencilCV() # Create output directory output_dir = "output_cv_stencils" os.makedirs(output_dir, exist_ok=True) # Example 1: Edge/Outline stencil print("\n1. Creating outline stencil...") outline = processor.edge_stencil(sample_path) processor.save(outline, f"{output_dir}/outline_stencil.png") # Example 2: Filled silhouette stencil print("2. Creating filled silhouette stencil...") filled = processor.silhouette_stencil(sample_path) processor.save(filled, f"{output_dir}/filled_stencil.png") # Example 3: Hybrid stencil print("3. Creating hybrid stencil...") hybrid = processor.hybrid_stencil(sample_path) processor.save(hybrid, f"{output_dir}/hybrid_stencil.png") print(f"\n{'=' * 60}") print(f"All stencils saved to: {output_dir}/") print("\nTo use with your own images:") print(" processor = StencilCV()") print(" stencil = processor.auto_stencil('your_image.jpg', style='filled')") print(" processor.save(stencil, 'output.png')") if __name__ == "__main__": main()