@@ -198,185 +198,145 @@ def _enhance_with_opencv(
198198 Advanced OpenCV enhancement pipeline
199199
200200 Pipeline stages:
201- 1. Multi-scale edge detection
202- 2. Morphological line connection
203- 3. Style-specific processing
204- 4. Post-processing (gamma, sharpening)
201+ 1. Adaptive solid stroke extraction (avoids Canny double-edges)
202+ 2. Style-specific processing
203+ 3. Post-processing (gamma, sharpening)
205204 """
206205
207206 print ("Running OpenCV enhancement pipeline..." )
208207
209- # Stage 1: Multi-scale edge detection
210- edges = self ._detect_edges_multiscale (img )
208+ # Stage 1: Extract clean solid strokes
209+ strokes = self ._extract_solid_strokes (img )
211210
212- # Stage 2: Connect broken strokes
213- edges = self ._connect_strokes (edges )
214-
215- # Stage 3: Apply style-specific processing
211+ # Stage 2: Apply style-specific processing
216212 if style == "professional" :
217- result = self ._apply_professional_style (img , edges )
213+ result = self ._apply_professional_style (img , strokes )
218214 elif style == "artistic" :
219- result = self ._apply_artistic_style (img , edges )
215+ result = self ._apply_artistic_style (img , strokes )
220216 elif style == "clean" :
221- result = self ._apply_clean_style (img , edges )
217+ result = self ._apply_clean_style (img , strokes )
222218 elif style == "minimal" :
223- result = self ._apply_minimal_style (img , edges )
219+ result = self ._apply_minimal_style (img , strokes )
224220 else :
225- result = self ._apply_professional_style (img , edges )
221+ result = self ._apply_professional_style (img , strokes )
226222
227- # Stage 4 : Post-processing
223+ # Stage 3 : Post-processing
228224 result = self ._post_process (result , style )
229225
230226 print ("[OK] OpenCV enhancement complete" )
231227
232228 return result
233229
234- def _detect_edges_multiscale (self , img : np .ndarray ) -> np .ndarray :
230+ def _extract_solid_strokes (self , img : np .ndarray ) -> np .ndarray :
235231 """
236- Multi-scale edge detection - combines three Canny passes
237- Captures both strong and subtle edges
232+ Extract solid strokes from drawing using adaptive thresholding
233+ This completely avoids the double-edge outlines caused by Canny.
238234 """
239235 gray = cv2 .cvtColor (img , cv2 .COLOR_RGB2GRAY )
240236
241- # Three scales with different thresholds
242- edges_fine = cv2 .Canny (gray , 30 , 100 ) # Captures subtle details
243- edges_mid = cv2 .Canny (gray , 50 , 150 ) # Main strokes
244- edges_strong = cv2 .Canny (gray , 70 , 200 ) # Strong features only
245-
246- # Weighted combination (prioritize main strokes)
247- edges = np .maximum (
248- edges_strong ,
249- np .maximum (edges_mid * 0.7 , edges_fine * 0.4 )
250- ).astype (np .uint8 )
251-
252- return edges
253-
254- def _connect_strokes (self , edges : np .ndarray ) -> np .ndarray :
255- """
256- Connect broken strokes using morphological operations
257- """
258- # Dilation to connect nearby edges
259- kernel_connect = cv2 .getStructuringElement (cv2 .MORPH_ELLIPSE , (3 , 3 ))
260- connected = cv2 .dilate (edges , kernel_connect , iterations = 1 )
237+ # Bilateral filter removes paper texture noise while preserving sharp stroke boundaries
238+ smoothed = cv2 .bilateralFilter (gray , 9 , 75 , 75 )
239+
240+ # Adaptive threshold extracts local dark drawing strokes on light background
241+ thresh = cv2 .adaptiveThreshold (
242+ smoothed ,
243+ 255 ,
244+ cv2 .ADAPTIVE_THRESH_GAUSSIAN_C ,
245+ cv2 .THRESH_BINARY_INV ,
246+ 15 , # local window size
247+ 8 # constant offset
248+ )
261249
262- # Thinning to restore line width
263- kernel_thin = cv2 .getStructuringElement (cv2 .MORPH_ELLIPSE , (2 , 2 ))
264- connected = cv2 .erode ( connected , kernel_thin , iterations = 1 )
250+ # Clean small isolated noise specks
251+ kernel_clean = cv2 .getStructuringElement (cv2 .MORPH_RECT , (2 , 2 ))
252+ cleaned = cv2 .morphologyEx ( thresh , cv2 . MORPH_OPEN , kernel_clean )
265253
266- # Remove small isolated noise
267- kernel_denoise = np . ones (( 2 , 2 ), np . uint8 )
268- connected = cv2 .morphologyEx (connected , cv2 .MORPH_OPEN , kernel_denoise )
254+ # Connect nearby stroke gaps
255+ kernel_smooth = cv2 . getStructuringElement ( cv2 . MORPH_ELLIPSE , ( 3 , 3 ) )
256+ smoothed_strokes = cv2 .morphologyEx (cleaned , cv2 .MORPH_CLOSE , kernel_smooth )
269257
270- return connected
258+ return smoothed_strokes
271259
272260 def _apply_professional_style (
273261 self ,
274262 img : np .ndarray ,
275- edges : np .ndarray
263+ strokes : np .ndarray
276264 ) -> np .ndarray :
277265 """
278266 Professional technical drawing style
279267 - Clean white background
280- - Pure black lines
281- - Slight anti-aliasing
268+ - Solid, clean, anti-aliased black strokes
282269 """
283- # Create white background
284270 result = np .ones_like (img ) * 255
285271
286- # Apply edges in pure black
287- result [edges > 0 ] = [0 , 0 , 0 ]
288-
289- # Slight Gaussian blur for anti-aliasing
290- result = cv2 .GaussianBlur (result , (3 , 3 ), 0.5 )
272+ # Create premium anti-aliased borders
273+ mask = cv2 .GaussianBlur (strokes , (3 , 3 ), 0.5 )
291274
275+ # Apply anti-aliased black strokes
276+ for c in range (3 ):
277+ result [:, :, c ] = 255 - mask
278+
292279 return result
293280
294281 def _apply_artistic_style (
295282 self ,
296283 img : np .ndarray ,
297- edges : np .ndarray
284+ strokes : np .ndarray
298285 ) -> np .ndarray :
299286 """
300287 Artistic pencil sketch style
301- - Dodge blend technique
302- - Textured appearance
303- - Maintains some grayscale variation
288+ - Retains beautiful textured pencil graphite details
289+ - Completely purifies the paper background to clean white
304290 """
305291 gray = cv2 .cvtColor (img , cv2 .COLOR_RGB2GRAY )
306292
307- # Invert grayscale
308- inv = 255 - gray
309-
310- # Blur inverted image
311- blur = cv2 .GaussianBlur (inv , (21 , 21 ), 0 )
312-
313- # Dodge blend (divide)
293+ # Bleach background to white using a dodge blend
294+ inv_gray = 255 - gray
295+ blur = cv2 .GaussianBlur (inv_gray , (21 , 21 ), 0 )
314296 sketch = cv2 .divide (gray , 255 - blur , scale = 256 )
315297
316- # Overlay edges for definition
317- sketch [edges > 127 ] = 0
318-
319- # Convert back to RGB
320- result = cv2 .cvtColor (sketch , cv2 .COLOR_GRAY2RGB )
298+ # Enhance strokes specifically with the solid mask for clean outline contrast
299+ enhanced_sketch = cv2 .multiply (sketch , 255 - (strokes // 3 ), scale = 1.0 / 255 )
321300
301+ result = cv2 .cvtColor (enhanced_sketch .astype (np .uint8 ), cv2 .COLOR_GRAY2RGB )
322302 return result
323303
324304 def _apply_clean_style (
325305 self ,
326306 img : np .ndarray ,
327- edges : np .ndarray
307+ strokes : np .ndarray
328308 ) -> np .ndarray :
329309 """
330310 Clean minimal style
331- - High contrast binary
332- - No grayscale variation
333- - Sharp clean lines
311+ - Sharp, high-contrast, pure solid black lines
334312 """
335- gray = cv2 .cvtColor (img , cv2 .COLOR_RGB2GRAY )
336-
337- # Adaptive threshold for uneven lighting
338- binary = cv2 .adaptiveThreshold (
339- gray , 255 , cv2 .ADAPTIVE_THRESH_GAUSSIAN_C ,
340- cv2 .THRESH_BINARY , 11 , 2
341- )
342-
343- # Combine with edges
344- binary [edges > 0 ] = 0
345-
346- # Noise removal
347- kernel = np .ones ((2 , 2 ), np .uint8 )
348- cleaned = cv2 .morphologyEx (binary , cv2 .MORPH_CLOSE , kernel )
349-
350- # Convert to RGB
351- result = cv2 .cvtColor (cleaned , cv2 .COLOR_GRAY2RGB )
352-
313+ result = np .ones_like (img ) * 255
314+ result [strokes > 0 ] = [0 , 0 , 0 ]
353315 return result
354316
355317 def _apply_minimal_style (
356318 self ,
357319 img : np .ndarray ,
358- edges : np .ndarray
320+ strokes : np .ndarray
359321 ) -> np .ndarray :
360322 """
361- Ultra-minimal line drawing
362- - Only strongest edges
363- - Thin lines
364- - Maximum simplicity
323+ Ultra-minimal thin line drawing
324+ - Elegantly thinned strokes
325+ - Fine, high-quality contours
365326 """
366- gray = cv2 .cvtColor (img , cv2 .COLOR_RGB2GRAY )
367-
368- # Only keep strongest edges
369- strong_edges = cv2 .Canny (gray , 100 , 200 )
327+ # Erode the stroke mask to uniform thin centerlines
328+ kernel = cv2 .getStructuringElement (cv2 .MORPH_RECT , (2 , 2 ))
329+ thinned = cv2 .erode (strokes , kernel , iterations = 1 )
370330
371- # Thin lines
372- kernel = np .ones ((2 , 2 ), np .uint8 )
373- thinned = cv2 .erode (strong_edges , kernel , iterations = 1 )
331+ # Anti-alias the fine strokes
332+ mask = cv2 .GaussianBlur (thinned , (3 , 3 ), 0.5 )
374333
375- # White background
376334 result = np .ones_like (img ) * 255
377- result [thinned > 0 ] = [0 , 0 , 0 ]
378-
335+ for c in range (3 ):
336+ result [:, :, c ] = 255 - mask
337+
379338 return result
339+
380340
381341 def _post_process (
382342 self ,
0 commit comments