mrpink925 commited on
Commit
df4d2da
·
verified ·
1 Parent(s): 4519188

Upload 4 files

Browse files

initial commit of files

Files changed (4) hide show
  1. Stencil.py +326 -0
  2. StencilCV.py +369 -0
  3. app.py +389 -0
  4. requirements.txt +18 -0
Stencil.py ADDED
@@ -0,0 +1,326 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Stencil Image Generator using Stable Diffusion
3
+
4
+ This module provides a simple interface to generate drawing stencil images
5
+ using pretrained Stable Diffusion models with prompt engineering.
6
+ """
7
+
8
+ import torch
9
+ from diffusers import StableDiffusionPipeline, DPMSolverMultistepScheduler
10
+ from PIL import Image, ImageOps, ImageEnhance, ImageFilter
11
+ from typing import Optional, List, Union
12
+ import os
13
+ import numpy as np
14
+ from scipy import ndimage
15
+
16
+
17
+ def _patch_clip_init():
18
+ """
19
+ Monkey-patch CLIPTextModel.__init__ to ignore offload_state_dict parameter.
20
+ This fixes compatibility issues between mismatched transformers versions.
21
+ """
22
+ try:
23
+ from transformers import CLIPTextModel
24
+ original_init = CLIPTextModel.__init__
25
+
26
+ def patched_init(self, config, *args, **kwargs):
27
+ # Remove the offload_state_dict parameter if it exists
28
+ kwargs.pop('offload_state_dict', None)
29
+ return original_init(self, config, *args, **kwargs)
30
+
31
+ CLIPTextModel.__init__ = patched_init
32
+ except ImportError:
33
+ pass # transformers not installed yet
34
+
35
+
36
+ class StencilGenerator:
37
+ """
38
+ A class to generate drawing stencil images using Stable Diffusion.
39
+
40
+ This generator automatically appends stencil-specific prompt decorations
41
+ to guide the model toward producing black and white stencil-style images.
42
+ """
43
+
44
+ def __init__(
45
+ self,
46
+ model_id: str = "stabilityai/stable-diffusion-2-1-base",
47
+ device: Optional[str] = None,
48
+ use_fp16: bool = True
49
+ ):
50
+ """
51
+ Initialize the Stencil Generator.
52
+
53
+ Args:
54
+ model_id: HuggingFace model ID for Stable Diffusion model
55
+ device: Device to run on ('cuda', 'cpu', or None for auto-detect)
56
+ use_fp16: Whether to use half precision (FP16) for faster inference
57
+ """
58
+ self.model_id = model_id
59
+ self.device = device or ("cuda" if torch.cuda.is_available() else "cpu")
60
+ self.use_fp16 = use_fp16 and self.device == "cuda"
61
+
62
+ # Apply monkey-patch to fix transformers version compatibility
63
+ _patch_clip_init()
64
+
65
+ print(f"Loading model {model_id} on {self.device}...")
66
+
67
+ # Load the pipeline with version-compatible parameters
68
+ dtype = torch.float16 if self.use_fp16 else torch.float32
69
+
70
+ self.pipe = StableDiffusionPipeline.from_pretrained(
71
+ model_id,
72
+ torch_dtype=dtype,
73
+ safety_checker=None, # Disable for faster loading
74
+ )
75
+
76
+ # Use DPM-Solver for faster generation
77
+ self.pipe.scheduler = DPMSolverMultistepScheduler.from_config(
78
+ self.pipe.scheduler.config
79
+ )
80
+
81
+ self.pipe = self.pipe.to(self.device)
82
+
83
+ # Enable memory optimizations
84
+ if self.device == "cuda":
85
+ self.pipe.enable_attention_slicing()
86
+ # Uncomment if you have limited VRAM
87
+ # self.pipe.enable_vae_slicing()
88
+
89
+ print("Model loaded successfully!")
90
+
91
+ # Default stencil prompt suffix - simplified since post-processing does the heavy lifting
92
+ self.stencil_suffix = (
93
+ "black silhouette, high contrast, simple stencil design, "
94
+ "centered in frame, complete object visible, isolated subject"
95
+ )
96
+
97
+ # Default negative prompt to avoid unwanted features
98
+ # self.default_negative_prompt = (
99
+ # "color, colorful, photograph, realistic, detailed, complex, "
100
+ # "blurry, low quality, watermark, text, cropped, cut off, "
101
+ # "partial, multiple subjects, duplicate"
102
+ # )
103
+
104
+ # Simpler stencil prompt suffix (seems to work better) - simplified since post-processing does the heavy lifting
105
+ # self.stencil_suffix = (
106
+ # "black silhouette, high contrast, sketch line drawing, simple, simple stencil design, white background, "
107
+ # # "centered in frame, complete object visible, isolated subject"
108
+ # )
109
+
110
+ # Simpler negative prompt (seems to work better) to avoid unwanted features
111
+ self.default_negative_prompt = (
112
+ "color, colorful, photograph, realistic, detailed, complex, "
113
+ # "blurry, low quality, watermark, text, cropped, cut off, "
114
+ # "partial, multiple subjects, duplicate"
115
+ )
116
+
117
+ def _clean_stencil_image(
118
+ self,
119
+ image: Image.Image,
120
+ binary_threshold: int = 128,
121
+ invert_if_needed: bool = True,
122
+ remove_small_objects: bool = True,
123
+ min_object_size: int = 100
124
+ ) -> Image.Image:
125
+ """
126
+ Aggressively convert any image to a clean binary stencil.
127
+ This uses Otsu's method and morphological operations to force
128
+ a clean black silhouette on pure white background, regardless
129
+ of what the model generated.
130
+
131
+ Args:
132
+ image: Input PIL Image
133
+ binary_threshold: Threshold for binarization (0-255), 128 = middle
134
+ invert_if_needed: Auto-detect if we need to invert (black on white vs white on black)
135
+ remove_small_objects: Remove small noise/artifacts
136
+ min_object_size: Minimum pixel area to keep (removes noise)
137
+
138
+ Returns:
139
+ Pure black and white stencil image
140
+ """
141
+ # Convert to grayscale first
142
+ if image.mode != 'L':
143
+ image = image.convert('L')
144
+
145
+ # Convert to numpy array
146
+ img_array = np.array(image)
147
+
148
+ # Apply Otsu's method for automatic threshold detection
149
+ # This finds the optimal threshold to separate foreground/background
150
+ try:
151
+ from skimage.filters import threshold_otsu
152
+ binary_threshold = threshold_otsu(img_array)
153
+ except ImportError:
154
+ # Fall back to simple threshold if skimage not available
155
+ binary_threshold = 128
156
+
157
+ # Apply binary threshold - create stark black and white
158
+ binary = img_array > binary_threshold
159
+
160
+ # Decide if we need to invert (we want black subject on white background)
161
+ if invert_if_needed:
162
+ # Count pixels - if more white than black, we likely have black subject on white (correct)
163
+ # If more black than white, we have white subject on black (need to invert)
164
+ white_pixels = np.sum(binary)
165
+ total_pixels = binary.size
166
+ if white_pixels < total_pixels / 2:
167
+ # More black than white - invert
168
+ binary = ~binary
169
+
170
+ # Remove small objects (noise/artifacts)
171
+ if remove_small_objects:
172
+ try:
173
+ from scipy.ndimage import label, sum as ndi_sum
174
+ # Label connected components
175
+ labeled_array, num_features = label(~binary) # Invert for labeling dark regions
176
+
177
+ # Calculate size of each component
178
+ component_sizes = ndi_sum(~binary, labeled_array, range(num_features + 1))
179
+
180
+ # Remove small components
181
+ mask_size = component_sizes < min_object_size
182
+ remove_pixel = mask_size[labeled_array]
183
+ binary[remove_pixel] = True # Set to white (background)
184
+ except ImportError:
185
+ pass # Skip if scipy not available
186
+
187
+ # Apply slight morphological closing to fill small holes in the subject
188
+ try:
189
+ from scipy.ndimage import binary_closing
190
+ binary = binary_closing(binary, structure=np.ones((3, 3)))
191
+ except ImportError:
192
+ pass
193
+
194
+ # Convert boolean array to uint8 (True->255, False->0)
195
+ result = (binary * 255).astype(np.uint8)
196
+
197
+ # Convert back to PIL Image
198
+ cleaned_image = Image.fromarray(result, mode='L').convert('RGB')
199
+
200
+ return cleaned_image
201
+
202
+
203
+
204
+ def generate(
205
+ self,
206
+ prompt: str,
207
+ num_images: int = 1,
208
+ negative_prompt: Optional[str] = None,
209
+ num_inference_steps: int = 25,
210
+ guidance_scale: float = 7.5,
211
+ width: int = 512,
212
+ height: int = 512,
213
+ seed: Optional[int] = None,
214
+ add_stencil_suffix: bool = True,
215
+ clean_background: bool = True,
216
+ ) -> Union[Image.Image, List[Image.Image]]:
217
+ """
218
+ Generate stencil images based on the prompt.
219
+
220
+ Args:
221
+ prompt: Base text prompt describing what to draw
222
+ negative_prompt: Things to avoid in the generation
223
+ num_images: Number of images to generate
224
+ num_inference_steps: Number of denoising steps (higher = better quality, slower)
225
+ guidance_scale: How strongly to follow the prompt (7-8 recommended)
226
+ width: Image width in pixels (must be divisible by 8)
227
+ height: Image height in pixels (must be divisible by 8)
228
+ seed: Random seed for reproducibility (None for random)
229
+ add_stencil_suffix: Whether to automatically add stencil styling to prompt
230
+ clean_background: Whether to post-process into pure binary stencil (highly recommended)
231
+
232
+ Returns:
233
+ Single PIL Image if num_images=1, otherwise list of PIL Images
234
+ """
235
+
236
+
237
+ # Construct full prompt
238
+ full_prompt = prompt
239
+ if add_stencil_suffix:
240
+ full_prompt = f"{prompt}, {self.stencil_suffix}"
241
+
242
+ # Use default negative prompt if none provided
243
+ full_negative_prompt = negative_prompt or self.default_negative_prompt
244
+
245
+ # Set seed if provided
246
+ generator = None
247
+ if seed is not None:
248
+ generator = torch.Generator(device=self.device).manual_seed(seed)
249
+
250
+ print(f"Generating {num_images} stencil image(s)...")
251
+ print(f"Prompt: {full_prompt}")
252
+
253
+ # Generate images
254
+ with torch.autocast(self.device) if self.use_fp16 else torch.no_grad():
255
+ result = self.pipe(
256
+ prompt=full_prompt,
257
+ num_images_per_prompt=num_images,
258
+ negative_prompt=full_negative_prompt,
259
+ num_inference_steps=num_inference_steps,
260
+ guidance_scale=guidance_scale,
261
+ width=width,
262
+ height=height,
263
+ generator=generator,
264
+ )
265
+
266
+ images = result.images
267
+
268
+ # Apply post-processing to clean background if enabled
269
+ if clean_background:
270
+ print("Cleaning background...")
271
+ images = [self._clean_stencil_image(img) for img in images]
272
+
273
+ print("Generation complete!")
274
+
275
+ # Return single image or list
276
+ return images[0] if num_images == 1 else images
277
+
278
+ def save_image(
279
+ self,
280
+ image: Image.Image,
281
+ output_path: str,
282
+ create_dirs: bool = True
283
+ ):
284
+ """
285
+ Save a generated image to disk.
286
+
287
+ Args:
288
+ image: PIL Image to save
289
+ output_path: Path where to save the image
290
+ create_dirs: Whether to create parent directories if they don't exist
291
+ """
292
+ if create_dirs:
293
+ os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True)
294
+
295
+ image.save(output_path)
296
+ print(f"Image saved to: {output_path}")
297
+
298
+ def generate_and_save(
299
+ self,
300
+ prompt: str,
301
+ output_path: str,
302
+ num_images: int = 1,
303
+ **kwargs
304
+ ) -> Image.Image:
305
+ """
306
+ Generate a stencil image and save it to disk in one call.
307
+
308
+ Args:
309
+ prompt: Base text prompt describing what to draw
310
+ output_path: Path where to save the image
311
+ **kwargs: Additional arguments passed to generate()
312
+
313
+ Returns:
314
+ The generated PIL Image
315
+ """
316
+ image = self.generate(prompt, num_images, **kwargs)
317
+ # Save single or multiple images
318
+ # if numb images is 1, save directly, else save with index suffix
319
+ if num_images == 1:
320
+ self.save_image(image, output_path)
321
+ else:
322
+ for idx, img in enumerate(image):
323
+ path = output_path.replace(".png", f"_{idx+1}.png")
324
+ self.save_image(img, path)
325
+ return image
326
+
StencilCV.py ADDED
@@ -0,0 +1,369 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ StencilCV - Traditional Computer Vision Approach to Stencil Generation
3
+
4
+ This module uses classical computer vision techniques (edge detection, contours,
5
+ thresholding) to convert images into clean stencils. No AI required - fast,
6
+ deterministic, and reliable.
7
+
8
+ Approaches:
9
+ 1. Edge detection (Canny) - creates outline stencils
10
+ 2. Contour extraction - creates filled silhouette stencils
11
+ 3. Threshold-based - converts to binary black/white
12
+ """
13
+
14
+ import cv2
15
+ import numpy as np
16
+ from PIL import Image
17
+ from typing import Union, Literal
18
+ import os
19
+
20
+
21
+ class StencilCV:
22
+ """
23
+ Computer vision-based stencil generator.
24
+
25
+ Converts any input image (photo, drawing, etc.) into a clean stencil
26
+ using traditional image processing techniques.
27
+ """
28
+
29
+ def __init__(self):
30
+ """Initialize the StencilCV processor."""
31
+ pass
32
+
33
+ def load_image(self, image_path: str) -> np.ndarray:
34
+ """
35
+ Load an image from file.
36
+
37
+ Args:
38
+ image_path: Path to input image
39
+
40
+ Returns:
41
+ Image as numpy array (BGR format)
42
+ """
43
+ img = cv2.imread(image_path)
44
+ if img is None:
45
+ raise ValueError(f"Could not load image from {image_path}")
46
+ return img
47
+
48
+ def from_pil(self, pil_image: Image.Image) -> np.ndarray:
49
+ """
50
+ Convert PIL Image to OpenCV format.
51
+
52
+ Args:
53
+ pil_image: PIL Image object
54
+
55
+ Returns:
56
+ Image as numpy array (BGR format)
57
+ """
58
+ # Convert PIL (RGB) to OpenCV (BGR)
59
+ return cv2.cvtColor(np.array(pil_image), cv2.COLOR_RGB2BGR)
60
+
61
+ def to_pil(self, cv_image: np.ndarray) -> Image.Image:
62
+ """
63
+ Convert OpenCV image to PIL Image.
64
+
65
+ Args:
66
+ cv_image: OpenCV image (BGR or grayscale)
67
+
68
+ Returns:
69
+ PIL Image object
70
+ """
71
+ if len(cv_image.shape) == 2: # Grayscale
72
+ return Image.fromarray(cv_image)
73
+ else: # BGR to RGB
74
+ return Image.fromarray(cv2.cvtColor(cv_image, cv2.COLOR_BGR2RGB))
75
+
76
+ def edge_stencil(
77
+ self,
78
+ image: Union[str, np.ndarray, Image.Image],
79
+ blur_kernel: int = 5,
80
+ canny_low: int = 50,
81
+ canny_high: int = 150,
82
+ line_thickness: int = 2,
83
+ invert: bool = False
84
+ ) -> Image.Image:
85
+ """
86
+ Create outline stencil using Canny edge detection.
87
+ Perfect for line art, coloring book style stencils.
88
+
89
+ Args:
90
+ image: Input image (path, numpy array, or PIL Image)
91
+ blur_kernel: Gaussian blur kernel size (reduces noise)
92
+ canny_low: Canny edge detection low threshold
93
+ canny_high: Canny edge detection high threshold
94
+ line_thickness: Thickness of edge lines
95
+ invert: If True, white lines on black; if False, black lines on white
96
+
97
+ Returns:
98
+ PIL Image with edge-based stencil
99
+ """
100
+ # Load image
101
+ if isinstance(image, str):
102
+ img = self.load_image(image)
103
+ elif isinstance(image, Image.Image):
104
+ img = self.from_pil(image)
105
+ else:
106
+ img = image.copy()
107
+
108
+ # Convert to grayscale
109
+ gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
110
+
111
+ # Apply Gaussian blur to reduce noise
112
+ blurred = cv2.GaussianBlur(gray, (blur_kernel, blur_kernel), 0)
113
+
114
+ # Apply Canny edge detection
115
+ edges = cv2.Canny(blurred, canny_low, canny_high)
116
+
117
+ # Dilate edges to make them thicker if requested
118
+ if line_thickness > 1:
119
+ kernel = np.ones((line_thickness, line_thickness), np.uint8)
120
+ edges = cv2.dilate(edges, kernel, iterations=1)
121
+
122
+ # Invert if needed (black on white by default)
123
+ if not invert:
124
+ edges = cv2.bitwise_not(edges)
125
+
126
+ # Convert to RGB
127
+ result = cv2.cvtColor(edges, cv2.COLOR_GRAY2RGB)
128
+
129
+ return self.to_pil(result)
130
+
131
+ def silhouette_stencil(
132
+ self,
133
+ image: Union[str, np.ndarray, Image.Image],
134
+ threshold_method: Literal['otsu', 'adaptive', 'simple'] = 'otsu',
135
+ threshold_value: int = 127,
136
+ blur_kernel: int = 5,
137
+ fill_holes: bool = True,
138
+ remove_small_objects: int = 500,
139
+ smooth_edges: bool = True
140
+ ) -> Image.Image:
141
+ """
142
+ Create filled silhouette stencil using thresholding and contours.
143
+ Perfect for solid stencils, like spray paint templates.
144
+
145
+ Args:
146
+ image: Input image (path, numpy array, or PIL Image)
147
+ threshold_method: Method for thresholding ('otsu', 'adaptive', 'simple')
148
+ threshold_value: Threshold value for 'simple' method (0-255)
149
+ blur_kernel: Gaussian blur kernel size
150
+ fill_holes: Fill holes in the silhouette
151
+ remove_small_objects: Remove objects smaller than this (pixels)
152
+ smooth_edges: Apply morphological smoothing to edges
153
+
154
+ Returns:
155
+ PIL Image with silhouette stencil
156
+ """
157
+ # Load image
158
+ if isinstance(image, str):
159
+ img = self.load_image(image)
160
+ elif isinstance(image, Image.Image):
161
+ img = self.from_pil(image)
162
+ else:
163
+ img = image.copy()
164
+
165
+ # Convert to grayscale
166
+ gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
167
+
168
+ # Apply Gaussian blur to reduce noise
169
+ blurred = cv2.GaussianBlur(gray, (blur_kernel, blur_kernel), 0)
170
+
171
+ # Apply thresholding based on method
172
+ if threshold_method == 'otsu':
173
+ # Otsu's method automatically finds optimal threshold
174
+ _, binary = cv2.threshold(blurred, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
175
+ elif threshold_method == 'adaptive':
176
+ # Adaptive threshold - good for varying lighting
177
+ binary = cv2.adaptiveThreshold(
178
+ blurred, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
179
+ cv2.THRESH_BINARY, 11, 2
180
+ )
181
+ else: # simple
182
+ _, binary = cv2.threshold(blurred, threshold_value, 255, cv2.THRESH_BINARY)
183
+
184
+ # Ensure we have black subject on white background
185
+ # (Check if more white than black, invert if needed)
186
+ if np.mean(binary) < 127:
187
+ binary = cv2.bitwise_not(binary)
188
+
189
+ # Remove small objects (noise)
190
+ if remove_small_objects > 0:
191
+ # Find contours
192
+ contours, _ = cv2.findContours(
193
+ cv2.bitwise_not(binary),
194
+ cv2.RETR_EXTERNAL,
195
+ cv2.CHAIN_APPROX_SIMPLE
196
+ )
197
+
198
+ # Filter contours by area and redraw
199
+ mask = np.ones_like(binary) * 255
200
+ for contour in contours:
201
+ area = cv2.contourArea(contour)
202
+ if area >= remove_small_objects:
203
+ cv2.drawContours(mask, [contour], -1, 0, -1)
204
+
205
+ binary = mask
206
+
207
+ # Fill holes in the silhouette
208
+ if fill_holes:
209
+ # Find contours
210
+ contours, hierarchy = cv2.findContours(
211
+ cv2.bitwise_not(binary),
212
+ cv2.RETR_CCOMP,
213
+ cv2.CHAIN_APPROX_SIMPLE
214
+ )
215
+
216
+ # Fill all contours (including holes)
217
+ mask = np.ones_like(binary) * 255
218
+ for i, contour in enumerate(contours):
219
+ # Only draw external contours (fill holes)
220
+ if hierarchy[0][i][3] == -1: # External contour
221
+ cv2.drawContours(mask, [contour], -1, 0, -1)
222
+
223
+ binary = mask
224
+
225
+ # Smooth edges using morphological operations
226
+ if smooth_edges:
227
+ kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
228
+ binary = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)
229
+ binary = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel)
230
+
231
+ # Convert to RGB
232
+ result = cv2.cvtColor(binary, cv2.COLOR_GRAY2RGB)
233
+
234
+ return self.to_pil(result)
235
+
236
+ def hybrid_stencil(
237
+ self,
238
+ image: Union[str, np.ndarray, Image.Image],
239
+ show_edges: bool = True,
240
+ show_fill: bool = True,
241
+ edge_thickness: int = 2
242
+ ) -> Image.Image:
243
+ """
244
+ Create hybrid stencil with both edges and filled regions.
245
+ Combines edge detection with silhouette for detailed stencils.
246
+
247
+ Args:
248
+ image: Input image (path, numpy array, or PIL Image)
249
+ show_edges: Include edge lines
250
+ show_fill: Include filled silhouette
251
+ edge_thickness: Thickness of edge lines
252
+
253
+ Returns:
254
+ PIL Image with hybrid stencil
255
+ """
256
+ # Load image
257
+ if isinstance(image, str):
258
+ img = self.load_image(image)
259
+ elif isinstance(image, Image.Image):
260
+ img = self.from_pil(image)
261
+ else:
262
+ img = image.copy()
263
+
264
+ # Start with white canvas
265
+ result = np.ones_like(cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)) * 255
266
+
267
+ # Add filled silhouette if requested
268
+ if show_fill:
269
+ silhouette = self.silhouette_stencil(img)
270
+ silhouette_cv = self.from_pil(silhouette)
271
+ silhouette_gray = cv2.cvtColor(silhouette_cv, cv2.COLOR_BGR2GRAY)
272
+ result = cv2.bitwise_and(result, silhouette_gray)
273
+
274
+ # Add edges if requested
275
+ if show_edges:
276
+ edges = self.edge_stencil(img, line_thickness=edge_thickness, invert=True)
277
+ edges_cv = self.from_pil(edges)
278
+ edges_gray = cv2.cvtColor(edges_cv, cv2.COLOR_BGR2GRAY)
279
+ result = cv2.bitwise_and(result, edges_gray)
280
+
281
+ # Convert to RGB
282
+ result_rgb = cv2.cvtColor(result, cv2.COLOR_GRAY2RGB)
283
+
284
+ return self.to_pil(result_rgb)
285
+
286
+ def auto_stencil(
287
+ self,
288
+ image: Union[str, np.ndarray, Image.Image],
289
+ style: Literal['outline', 'filled', 'hybrid'] = 'filled'
290
+ ) -> Image.Image:
291
+ """
292
+ Automatically create a stencil with sensible defaults.
293
+
294
+ Args:
295
+ image: Input image (path, numpy array, or PIL Image)
296
+ style: Style of stencil ('outline', 'filled', 'hybrid')
297
+
298
+ Returns:
299
+ PIL Image with stencil
300
+ """
301
+ if style == 'outline':
302
+ return self.edge_stencil(image)
303
+ elif style == 'filled':
304
+ return self.silhouette_stencil(image)
305
+ else: # hybrid
306
+ return self.hybrid_stencil(image)
307
+
308
+ def save(self, image: Image.Image, output_path: str):
309
+ """
310
+ Save stencil image to file.
311
+
312
+ Args:
313
+ image: PIL Image to save
314
+ output_path: Path to save image
315
+ """
316
+ os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True)
317
+ image.save(output_path)
318
+ print(f"Saved stencil to: {output_path}")
319
+
320
+
321
+ def main():
322
+ """Example usage demonstrating different stencil styles."""
323
+
324
+ print("StencilCV - Computer Vision Stencil Generator")
325
+ print("=" * 60)
326
+
327
+ # Note: You'll need input images to test this
328
+ # For now, let's create a simple example
329
+
330
+ # Create a sample image (circle)
331
+ print("\nCreating sample image...")
332
+ sample = np.ones((512, 512, 3), dtype=np.uint8) * 255
333
+ cv2.circle(sample, (256, 256), 100, (0, 0, 0), -1)
334
+ sample_path = "sample_input.png"
335
+ cv2.imwrite(sample_path, sample)
336
+ print(f"Created sample image: {sample_path}")
337
+
338
+ # Initialize processor
339
+ processor = StencilCV()
340
+
341
+ # Create output directory
342
+ output_dir = "output_cv_stencils"
343
+ os.makedirs(output_dir, exist_ok=True)
344
+
345
+ # Example 1: Edge/Outline stencil
346
+ print("\n1. Creating outline stencil...")
347
+ outline = processor.edge_stencil(sample_path)
348
+ processor.save(outline, f"{output_dir}/outline_stencil.png")
349
+
350
+ # Example 2: Filled silhouette stencil
351
+ print("2. Creating filled silhouette stencil...")
352
+ filled = processor.silhouette_stencil(sample_path)
353
+ processor.save(filled, f"{output_dir}/filled_stencil.png")
354
+
355
+ # Example 3: Hybrid stencil
356
+ print("3. Creating hybrid stencil...")
357
+ hybrid = processor.hybrid_stencil(sample_path)
358
+ processor.save(hybrid, f"{output_dir}/hybrid_stencil.png")
359
+
360
+ print(f"\n{'=' * 60}")
361
+ print(f"All stencils saved to: {output_dir}/")
362
+ print("\nTo use with your own images:")
363
+ print(" processor = StencilCV()")
364
+ print(" stencil = processor.auto_stencil('your_image.jpg', style='filled')")
365
+ print(" processor.save(stencil, 'output.png')")
366
+
367
+
368
+ if __name__ == "__main__":
369
+ main()
app.py ADDED
@@ -0,0 +1,389 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Gradio Web Interface for Stencil Image Generator
3
+
4
+ This module provides a web-based UI for the Stencil Generator using Gradio.
5
+ Run this file to launch the interactive web interface.
6
+ """
7
+
8
+ import gradio as gr
9
+ from Stencil import StencilGenerator
10
+ from StencilCV import StencilCV
11
+ import torch
12
+ from typing import Optional
13
+ import numpy as np
14
+
15
+ MAX_IMAGES = 4
16
+
17
+ class StencilApp:
18
+ """Wrapper class for the Gradio application."""
19
+
20
+ def __init__(self):
21
+ """Initialize the Stencil Generator."""
22
+ self.generator = None
23
+ self.original_images = [] # Store original images for toggling
24
+ self.outlined_status = [] # Track which images have outline applied
25
+
26
+ def load_model(self):
27
+ """Lazy load the model when first needed."""
28
+ if self.generator is None:
29
+ print("Initializing Stencil Generator...")
30
+ self.generator = StencilGenerator(
31
+ model_id="stabilityai/stable-diffusion-2-1-base",
32
+ use_fp16=torch.cuda.is_available()
33
+ )
34
+ return self.generator
35
+
36
+ def generate_stencil(
37
+ self,
38
+ prompt: str,
39
+ negative_prompt: Optional[str],
40
+ num_images: int,
41
+ num_inference_steps: int,
42
+ guidance_scale: float,
43
+ width: int,
44
+ height: int,
45
+ seed: int,
46
+ use_seed: bool,
47
+ add_stencil_suffix: bool,
48
+ clean_background: bool
49
+ ):
50
+ """
51
+ Generate stencil images based on user inputs.
52
+
53
+ This is the main function called by the Gradio interface.
54
+ """
55
+ if not prompt or prompt.strip() == "":
56
+ return [], "Please enter a prompt!"
57
+
58
+ try:
59
+ # Load model if not already loaded
60
+ generator = self.load_model()
61
+
62
+ # Generate the image(s)
63
+ images = generator.generate(
64
+ prompt=prompt,
65
+ negative_prompt=negative_prompt if negative_prompt else None,
66
+ num_images=num_images,
67
+ num_inference_steps=num_inference_steps,
68
+ guidance_scale=guidance_scale,
69
+ width=width,
70
+ height=height,
71
+ seed=seed if use_seed else None,
72
+ add_stencil_suffix=add_stencil_suffix,
73
+ clean_background=clean_background
74
+ )
75
+
76
+ # Ensure images is a list
77
+ if not isinstance(images, list):
78
+ images = [images]
79
+
80
+ # Store original images and reset outlined status
81
+ self.original_images = [img.copy() for img in images]
82
+ self.outlined_status = [False] * len(images)
83
+
84
+ return images, f"Generation successful! Created {len(images)} image(s)."
85
+
86
+ except Exception as e:
87
+ return [], f"Error: {str(e)}"
88
+
89
+ def apply_outline(self, gallery_data, selected_index):
90
+ """
91
+ Toggle outline processing on a selected image using StencilCV.
92
+ If the image has outline applied, revert to original. Otherwise, apply outline.
93
+
94
+ Args:
95
+ gallery_data: Gallery data from Gradio (list of images or tuples)
96
+ selected_index: Index of the selected image (from gr.Gallery select event)
97
+
98
+ Returns:
99
+ Updated gallery and status message
100
+ """
101
+ # print(f"DEBUG: apply_outline called")
102
+ # print(f"DEBUG: gallery_data type: {type(gallery_data)}")
103
+ # print(f"DEBUG: gallery_data length: {len(gallery_data) if gallery_data else 0}")
104
+ # print(f"DEBUG: selected_index: {selected_index}")
105
+
106
+ if not gallery_data:
107
+ return gallery_data, "No images to process!"
108
+
109
+ if selected_index is None:
110
+ return gallery_data, "Please select an image first by clicking on it!"
111
+
112
+ if selected_index >= len(self.original_images):
113
+ return gallery_data, "Error: Image index out of range!"
114
+
115
+ try:
116
+ # Create a copy of the gallery data
117
+ updated_gallery = list(gallery_data)
118
+
119
+ # Check if this image already has outline applied
120
+ if self.outlined_status[selected_index]:
121
+ # Revert to original
122
+ # print(f"DEBUG: Reverting image {selected_index} to original")
123
+ updated_gallery[selected_index] = self.original_images[selected_index].copy()
124
+ self.outlined_status[selected_index] = False
125
+ return updated_gallery, f"Reverted image {selected_index + 1} to original."
126
+ else:
127
+ # Apply outline
128
+ # print(f"DEBUG: Applying outline to image {selected_index}")
129
+
130
+ # Initialize StencilCV processor
131
+ processor = StencilCV()
132
+
133
+ # Get the original image (not the gallery one, to ensure consistency)
134
+ original_img = self.original_images[selected_index]
135
+
136
+ # print(f"DEBUG: Applying edge_stencil...")
137
+ # Apply outline to the original image
138
+ outlined = processor.edge_stencil(original_img)
139
+ # print(f"DEBUG: Outline applied successfully!")
140
+
141
+ # Update gallery with outlined version
142
+ updated_gallery[selected_index] = outlined
143
+ self.outlined_status[selected_index] = True
144
+
145
+ return updated_gallery, f"Applied outline to image {selected_index + 1}. Click again to revert."
146
+
147
+ except Exception as e:
148
+ import traceback
149
+ print("DEBUG: Exception occurred:")
150
+ traceback.print_exc()
151
+ return gallery_data, f"Error applying outline: {str(e)}"
152
+
153
+
154
+ def create_interface():
155
+ """Create and configure the Gradio interface."""
156
+
157
+ app = StencilApp()
158
+
159
+ # Define the interface
160
+ with gr.Blocks(title="Stencil Image Generator", theme=gr.themes.Soft()) as interface:
161
+
162
+ gr.Markdown(
163
+ """
164
+ # 🎨 Stencil Image Generator
165
+
166
+ Generate black and white stencil-style images using AI. Perfect for creating
167
+ cutting templates, vector art, and silhouette designs.
168
+ """
169
+ )
170
+
171
+ with gr.Row():
172
+ with gr.Column(scale=1):
173
+ # Input controls
174
+ prompt = gr.Textbox(
175
+ label="Prompt",
176
+ placeholder="e.g., a cat sitting, a tree with spreading branches...",
177
+ lines=3
178
+ )
179
+
180
+ num_images = gr.Slider(
181
+ minimum=1,
182
+ maximum=MAX_IMAGES,
183
+ value=1,
184
+ step=1,
185
+ label="Number of Images",
186
+ info="Generate multiple variations to choose from"
187
+ )
188
+
189
+ with gr.Accordion("Advanced Settings", open=False):
190
+ negative_prompt = gr.Textbox(
191
+ label="Negative Prompt (optional)",
192
+ placeholder="Things to avoid in the generation...",
193
+ lines=2
194
+ )
195
+
196
+ add_stencil_suffix = gr.Checkbox(
197
+ label="Add stencil styling suffix(recommended)",
198
+ value=True,
199
+ info="Automatically adds stencil-specific styling to your prompt (prompt decorations)"
200
+ )
201
+
202
+ clean_background = gr.Checkbox(
203
+ label="Clean white background (recommended)",
204
+ value=True,
205
+ info="Post-process to ensure pure white background and remove artifacts"
206
+ )
207
+
208
+ num_inference_steps = gr.Slider(
209
+ minimum=10,
210
+ maximum=50,
211
+ value=25,
212
+ step=5,
213
+ label="Inference Steps",
214
+ info="Higher = better quality but slower"
215
+ )
216
+
217
+ guidance_scale = gr.Slider(
218
+ minimum=1,
219
+ maximum=15,
220
+ value=7.5,
221
+ step=0.5,
222
+ label="Guidance Scale",
223
+ info="How closely to follow the prompt (7-8 recommended)"
224
+ )
225
+
226
+ with gr.Row():
227
+ width = gr.Slider(
228
+ minimum=256,
229
+ maximum=1024,
230
+ value=512,
231
+ step=64,
232
+ label="Width"
233
+ )
234
+ height = gr.Slider(
235
+ minimum=256,
236
+ maximum=1024,
237
+ value=512,
238
+ step=64,
239
+ label="Height"
240
+ )
241
+
242
+ with gr.Row():
243
+ use_seed = gr.Checkbox(
244
+ label="Use fixed seed",
245
+ value=False,
246
+ info="Enable for reproducible results"
247
+ )
248
+ seed = gr.Number(
249
+ label="Seed",
250
+ value=42,
251
+ precision=0
252
+ )
253
+
254
+ generate_btn = gr.Button("Generate Stencil", variant="primary", size="lg")
255
+
256
+ # Example prompts
257
+ gr.Examples(
258
+ examples=[
259
+ ["a cat sitting"],
260
+ ["a tree with spreading branches"],
261
+ ["a bicycle"],
262
+ ["a coffee cup"],
263
+ ["a bird in flight"],
264
+ ["a deer with antlers"],
265
+ ["a mountain landscape"],
266
+ ["a lighthouse by the sea"]
267
+ ],
268
+ inputs=prompt,
269
+ label="Example Prompts"
270
+ )
271
+
272
+ with gr.Column(scale=1):
273
+ # Output - Gallery for multiple images
274
+ output_gallery = gr.Gallery(
275
+ label="Generated Stencils (click to select)",
276
+ show_label=True,
277
+ columns=2,
278
+ rows=2,
279
+ height="auto",
280
+ object_fit="contain"
281
+ )
282
+ status_text = gr.Textbox(
283
+ label="Status",
284
+ interactive=False,
285
+ lines=1
286
+ )
287
+
288
+ # Hidden state to track selected image
289
+ selected_image_index = gr.State(value=None)
290
+
291
+ # Post-processing section
292
+ with gr.Accordion("Post-Processing Options", open=False):
293
+ gr.Markdown(
294
+ """
295
+ **Outline Generation**: Click an image above to select it, then click the button below
296
+ to toggle outline processing. This creates a line-art effect and works best on images
297
+ with clear subjects. Click the button again to revert to the original.
298
+
299
+ You can toggle outline on/off for each image independently to compare styles.
300
+ """
301
+ )
302
+ apply_outline_btn = gr.Button("Toggle Outline on Selected Image", variant="secondary")
303
+
304
+ gr.Markdown(
305
+ """
306
+ ### Tips for Best Results:
307
+ - Keep prompts simple and descriptive
308
+ - Generate multiple images to see variations
309
+ - The AI automatically adds stencil styling
310
+ - Use negative prompts to avoid unwanted features
311
+ - Try the outline option after generation for different styles
312
+ - Higher inference steps = better quality (but slower)
313
+ """
314
+ )
315
+
316
+ # Connect the generate button
317
+ generate_btn.click(
318
+ fn=app.generate_stencil,
319
+ inputs=[
320
+ prompt,
321
+ negative_prompt,
322
+ num_images,
323
+ num_inference_steps,
324
+ guidance_scale,
325
+ width,
326
+ height,
327
+ seed,
328
+ use_seed,
329
+ add_stencil_suffix,
330
+ clean_background
331
+ ],
332
+ outputs=[output_gallery, status_text]
333
+ )
334
+
335
+ # Track when user selects an image in the gallery
336
+ def update_selection(evt: gr.SelectData):
337
+ print(f"DEBUG: Gallery selection event - index: {evt.index}")
338
+ return evt.index
339
+
340
+ output_gallery.select(
341
+ fn=update_selection,
342
+ outputs=selected_image_index
343
+ )
344
+
345
+ # Connect the outline button
346
+ apply_outline_btn.click(
347
+ fn=app.apply_outline,
348
+ inputs=[output_gallery, selected_image_index],
349
+ outputs=[output_gallery, status_text]
350
+ )
351
+
352
+ gr.Markdown(
353
+ """
354
+ ---
355
+ Built with [Stable Diffusion](https://stability.ai/) and [Gradio](https://gradio.app/)
356
+ """
357
+ )
358
+
359
+ return interface
360
+
361
+
362
+ def launch(
363
+ share: bool = False,
364
+ server_name: str = "0.0.0.0",
365
+ server_port: int = 7860,
366
+ **kwargs
367
+ ):
368
+ """
369
+ Launch the Gradio interface.
370
+
371
+ Args:
372
+ share: Whether to create a public shareable link
373
+ server_name: Server host (0.0.0.0 for public access)
374
+ server_port: Port to run the server on
375
+ **kwargs: Additional arguments passed to gradio.launch()
376
+ """
377
+ interface = create_interface()
378
+ interface.launch(
379
+ share=share,
380
+ server_name=server_name,
381
+ server_port=server_port,
382
+ **kwargs
383
+ )
384
+
385
+
386
+ if __name__ == "__main__":
387
+ # Launch with default settings
388
+ # Set share=True to create a public link
389
+ launch(share=True, pwa=True)
requirements.txt ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ torch>=2.0.0
2
+ diffusers>=0.21.0
3
+ transformers>=4.30.0
4
+ accelerate>=0.20.0
5
+ safetensors>=0.3.0
6
+ gradio>=4.0.0
7
+ numpy>=1.24.0
8
+ Pillow>=9.0.0
9
+ scipy>=1.10.0
10
+ scikit-image>=0.20.0
11
+ opencv-python>=4.8.0
12
+
13
+ # Note: Pillow, numpy, scipy, scikit-image required for AI-based post-processing
14
+ # opencv-python required for StencilCV (traditional computer vision approach)
15
+
16
+ # Tested with:
17
+ # torch==2.4.1, diffusers==0.30.3, transformers==4.44.2
18
+ # accelerate==1.1.1, safetensors==0.4.5, gradio==4.44.0