Spaces:
Running
Running
| """ | |
| 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() | |