vitordigitizing commited on
Commit
19aedb6
·
verified ·
1 Parent(s): 3933a36

Update faceless_processing.py

Browse files
Files changed (1) hide show
  1. faceless_processing.py +105 -155
faceless_processing.py CHANGED
@@ -1,6 +1,6 @@
1
  import cv2
2
  import numpy as np
3
- from PIL import Image, ImageFilter, ImageOps
4
  from dataclasses import dataclass
5
  from typing import Tuple, Optional
6
  import io
@@ -34,6 +34,8 @@ _LINEART_DETECTOR = None
34
  _LINEART_AVAILABLE = True
35
 
36
 
 
 
37
  def get_lineart_detector():
38
  """
39
  Lazy-load the controlnet_aux LineartDetector (informative-drawings model
@@ -73,8 +75,7 @@ def _download_assets_globally():
73
  os.makedirs(cascade_dir, exist_ok=True)
74
  xml_urls = {
75
  'haarcascade_frontalface_default.xml': 'https://raw.githubusercontent.com/opencv/opencv/master/data/haarcascades/haarcascade_frontalface_default.xml',
76
- 'haarcascade_frontalface_alt2.xml': 'https://raw.githubusercontent.com/opencv/opencv/master/data/haarcascades/haarcascade_frontalface_alt2.xml',
77
- 'haarcascade_profileface.xml': 'https://raw.githubusercontent.com/opencv/opencv/master/data/haarcascades/haarcascade_profileface.xml'
78
  }
79
  for name, url in xml_urls.items():
80
  path = os.path.join(cascade_dir, name)
@@ -104,6 +105,7 @@ _download_assets_globally()
104
  get_lineart_detector()
105
 
106
 
 
107
  @dataclass
108
  class LineartConfig:
109
  # Backward compatibility fields
@@ -127,36 +129,39 @@ class LineartConfig:
127
  mask_smooth_threshold: int = 128
128
  min_silhouette_area: float = 200.0
129
 
130
- # Silhouette outline — clean, slightly bold for embroidery visibility
 
 
 
131
  silhouette_line_thickness: int = 3
132
 
133
- # --- Neural line-art parameters (replace the old Canny block) ---
134
- # Resolution the detector runs at. Higher = more/finer detail. 768-1024
135
- # gives the clean structural lines (lapels, folds, hair) seen in the ref.
136
- lineart_detect_resolution: int = 1024
137
- lineart_image_resolution: int = 1024
138
- # coarse=False => detailed model (sk_model2). True => simpler/cleaner lines.
139
- lineart_coarse: bool = False
140
- # Strength threshold: only keep line pixels stronger than this (0-255).
141
- # Lower = keep more faint lines (more detail like face shape / hair strands).
142
- lineart_threshold: int = 14
143
- # Minimum contour length (px) to keep — removes speckle noise.
144
- # Low value preserves short strokes (eyebrows, hair locks, jaw line).
145
- lineart_min_perimeter: float = 8.0
146
- # How much to erode the subject mask before keeping interior lines.
147
- # Smaller = keeps lines closer to the silhouette (face outline, hair edge).
148
- interior_erode_size: int = 3
149
- # Final interior stroke thickness.
150
  interior_line_thickness: int = 2
151
 
152
- # ---- Canny fallback params (only used if controlnet_aux is missing) ----
153
- canny_low: int = 40
154
- canny_high: int = 130
 
 
155
  bilateral_d: int = 11
156
  bilateral_sigma_color: int = 110
157
  bilateral_sigma_space: int = 110
158
  gaussian_ksize: int = 11
159
- min_contour_perimeter: float = 65.0
160
  min_embroidery_stitch_px: int = 25
161
  enable_detail_pass: bool = False
162
  detail_canny_low: int = 15
@@ -174,28 +179,31 @@ class LineartConfig:
174
  face_nms_iou_thresh: float = 0.35
175
  face_max_count: int = 6
176
 
177
- # Face erase ellipse — covers ONLY eyes/nose/mouth zone (keeps eyebrows/chin)
178
  face_cx_ratio: float = 0.50
179
- face_cy_ratio: float = 0.46
180
- face_rx_ratio: float = 0.27
181
- face_ry_ratio: float = 0.19
182
  face_erase_blur: int = 5
183
  face_erase_threshold: int = 90
184
 
185
  # MediaPipe
186
  mediapipe_num_faces: int = 6
187
 
188
- # Eyebrow drawing — subtle (per user request)
189
- eyebrow_thickness: int = 3
190
- eyebrow_dilate: int = 1
 
 
191
 
192
- # Face shape (head/jaw outline). Draws the MediaPipe face oval so the
193
- # "formato do rosto" is visible while the centre stays faceless.
194
  draw_face_oval: bool = True
195
- face_oval_thickness: int = 2
196
- # Skip the very top of the oval (forehead) so the line doesn't cut across
197
- # the hair. 0.0 = draw full oval, 0.25 = skip top 25% (recommended).
198
- face_oval_skip_top_ratio: float = 0.18
 
 
199
 
200
 
201
  def _nms_faces(faces: list, iou_thresh: float) -> list:
@@ -220,57 +228,37 @@ def _nms_faces(faces: list, iou_thresh: float) -> list:
220
  return [faces[i] for i in keep]
221
 
222
 
223
- def _load_cascade(cascade_dir: str, name: str) -> Optional[cv2.CascadeClassifier]:
224
- """Load a Haar cascade from the local dir, falling back to OpenCV's builtin."""
225
- local = os.path.join(cascade_dir, name)
226
- if os.path.exists(local):
227
- c = cv2.CascadeClassifier(local)
228
- if not c.empty():
229
- return c
230
- builtin = cv2.CascadeClassifier(cv2.data.haarcascades + name)
231
- return builtin if not builtin.empty() else None
232
-
233
-
234
  def detect_faces(rgb: np.ndarray, cfg: LineartConfig) -> list:
235
- h, w = rgb.shape[:2]
236
  gray = cv2.cvtColor(rgb, cv2.COLOR_RGB2GRAY)
237
  clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
238
  gray_eq = clahe.apply(gray)
239
- gray_flip = cv2.flip(gray_eq, 1) # mirror, for the other-facing profile
240
  faces = []
241
  try:
242
  cascade_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "cascades")
243
- frontal = [c for c in (
244
- _load_cascade(cascade_dir, 'haarcascade_frontalface_default.xml'),
245
- _load_cascade(cascade_dir, 'haarcascade_frontalface_alt2.xml'),
246
- ) if c is not None]
247
- profile = _load_cascade(cascade_dir, 'haarcascade_profileface.xml')
248
-
249
- # --- Frontal faces (raw + CLAHE) ---
250
- for cascade in frontal:
251
- for img_gray in (gray, gray_eq):
252
- detected = cascade.detectMultiScale(
253
- img_gray,
254
- scaleFactor=cfg.face_scale_factor,
255
- minNeighbors=cfg.face_min_neighbors,
256
- minSize=cfg.face_min_size
257
- )
258
- if len(detected) > 0:
259
- faces.extend(detected.tolist())
260
-
261
- # --- Profile faces (side view). The cascade only detects faces looking
262
- # one direction, so run it on the image AND its mirror, remapping x. ---
263
- if profile is not None and not profile.empty():
264
- # left-facing profiles
265
- for (fx, fy, fw, fh) in profile.detectMultiScale(
266
- gray_eq, scaleFactor=cfg.face_scale_factor,
267
- minNeighbors=cfg.face_min_neighbors, minSize=cfg.face_min_size):
268
- faces.append([int(fx), int(fy), int(fw), int(fh)])
269
- # right-facing profiles (detected on the mirror, x flipped back)
270
- for (fx, fy, fw, fh) in profile.detectMultiScale(
271
- gray_flip, scaleFactor=cfg.face_scale_factor,
272
- minNeighbors=cfg.face_min_neighbors, minSize=cfg.face_min_size):
273
- faces.append([int(w - fx - fw), int(fy), int(fw), int(fh)])
274
  except Exception as e:
275
  print(f"[AI] Face detection failed or cascades unavailable: {e}")
276
 
@@ -281,6 +269,7 @@ def detect_faces(rgb: np.ndarray, cfg: LineartConfig) -> list:
281
  faces = sorted(faces, key=lambda f: f[2] * f[3], reverse=True)[:max_faces]
282
  return faces
283
 
 
284
 
285
  def build_subject_mask(rgb: np.ndarray, cfg: LineartConfig, alpha_channel: Optional[np.ndarray] = None) -> np.ndarray:
286
  if alpha_channel is not None:
@@ -308,6 +297,8 @@ def _smooth_contour_spline(cnt: np.ndarray, n_points: int = 400) -> Optional[np.
308
  spline_pts = np.stack([x_new, y_new], axis=1).astype(np.int32)
309
  return spline_pts.reshape((-1, 1, 2))
310
  except Exception:
 
 
311
  return None
312
 
313
 
@@ -371,6 +362,7 @@ def _line_strength_from_detector(pil_lineart: Image.Image, target_hw: Tuple[int,
371
  return strength.astype(np.uint8)
372
 
373
 
 
374
  def build_interior_edges(rgb: np.ndarray, subject_mask: np.ndarray, cfg: LineartConfig) -> np.ndarray:
375
  """
376
  Detect structural lines (clothing folds, lapels, collars, cuffs, hair) using
@@ -415,8 +407,10 @@ def build_interior_edges(rgb: np.ndarray, subject_mask: np.ndarray, cfg: Lineart
415
  cv2.drawContours(filtered, [cnt], -1, 255, cfg.interior_line_thickness)
416
 
417
  # Gentle dilation for solid, embroiderable strokes
418
- kernel_dilate = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
419
  filtered = cv2.dilate(filtered, kernel_dilate, iterations=1)
 
 
420
  return filtered
421
  except Exception as e:
422
  print(f"[AI] Neural lineart failed, falling back to Canny: {e}")
@@ -479,41 +473,6 @@ def get_landmarker(model_path, num_faces: int = 6):
479
  return _LANDMARKER_CACHE
480
 
481
 
482
- def _draw_eyebrows_geometric(eyebrows_layer: np.ndarray, faces: list, cfg: LineartConfig) -> np.ndarray:
483
- """
484
- Fallback used when MediaPipe (OpenGL) fails to load on the Space.
485
- Draws approximate eyebrows + a jaw/chin arc from the Haar cascade bounding
486
- box using standard facial proportions, so these details are NEVER missing.
487
- """
488
- eb_th = max(2, getattr(cfg, 'eyebrow_thickness', 2))
489
- oval_th = max(1, getattr(cfg, 'face_oval_thickness', 1))
490
- draw_oval = getattr(cfg, 'draw_face_oval', True)
491
- count = 0
492
- for (fx, fy, fw, fh) in faces:
493
- # Eyebrows: horizontal band around 30% of face height, slight upward arc
494
- eb_y = int(fy + fh * 0.30)
495
- lift = int(fh * 0.04)
496
- left = (int(fx + fw * 0.18), int(fx + fw * 0.44))
497
- right = (int(fx + fw * 0.56), int(fx + fw * 0.82))
498
- for (xa, xb) in (left, right):
499
- mx = (xa + xb) // 2
500
- pts = np.array([[xa, eb_y], [mx, eb_y - lift], [xb, eb_y]], np.int32)
501
- cv2.polylines(eyebrows_layer, [pts.reshape((-1, 1, 2))],
502
- isClosed=False, color=255, thickness=eb_th,
503
- lineType=cv2.LINE_AA)
504
- if draw_oval:
505
- # Jaw / chin arc only (lower half of the face) so hair stays free
506
- cx = int(fx + fw * 0.50)
507
- cy = int(fy + fh * 0.52)
508
- rx = int(fw * 0.46)
509
- ry = int(fh * 0.50)
510
- cv2.ellipse(eyebrows_layer, (cx, cy), (rx, ry), 0, 30, 150,
511
- 255, oval_th, lineType=cv2.LINE_AA)
512
- count += 1
513
- print(f"[eyebrows] Geometric fallback drew {count} face(s)")
514
- return eyebrows_layer
515
-
516
-
517
  def draw_eyebrows(rgb: np.ndarray, cfg: LineartConfig, faces: Optional[list] = None) -> np.ndarray:
518
  h, w, _ = rgb.shape
519
  eyebrows_layer = np.zeros((h, w), dtype=np.uint8)
@@ -538,7 +497,7 @@ def draw_eyebrows(rgb: np.ndarray, cfg: LineartConfig, faces: Optional[list] = N
538
  172, 58, 132, 93, 234, 127, 162, 21, 54, 103, 67, 109]
539
 
540
  eyebrow_thickness = getattr(cfg, 'eyebrow_thickness', cfg.interior_line_thickness)
541
- draw_oval = getattr(cfg, 'draw_face_oval', True)
542
  oval_thickness = getattr(cfg, 'face_oval_thickness', 2)
543
  oval_skip_top = getattr(cfg, 'face_oval_skip_top_ratio', 0.18)
544
  drawn_centers_px: list = []
@@ -564,29 +523,34 @@ def draw_eyebrows(rgb: np.ndarray, cfg: LineartConfig, faces: Optional[list] = N
564
  thickness=eyebrow_thickness, lineType=cv2.LINE_AA)
565
 
566
  # Face oval (head / jaw outline) — gives the "formato do rosto"
567
- if draw_oval:
568
- oval_pts = []
569
  for idx in FACE_OVAL:
570
  lm = face_lms[idx]
571
  px = int(origin_x + lm.x * scale_x)
572
  py = int(origin_y + lm.y * scale_y)
573
  oval_pts.append([px, py])
574
  oval_np = np.array(oval_pts, np.int32)
 
575
  if len(oval_np) >= 4:
576
- # Optionally drop the top (forehead) points so the outline
577
- # doesn't cut a hard line across the hair.
578
- if oval_skip_top > 0:
579
- y_min = oval_np[:, 1].min()
580
- y_max = oval_np[:, 1].max()
581
- cutoff = y_min + (y_max - y_min) * oval_skip_top
582
- kept = oval_np[oval_np[:, 1] >= cutoff]
583
- if len(kept) >= 4:
584
- # open arc (jaw + cheeks), not closed, to leave the
585
- # crown of the head to the hair/silhouette lines
586
- arc = kept.reshape((-1, 1, 2))
587
- cv2.polylines(eyebrows_layer, [arc], isClosed=False,
588
- color=255, thickness=oval_thickness,
589
- lineType=cv2.LINE_AA)
 
 
 
 
590
  else:
591
  cv2.polylines(eyebrows_layer, [oval_np.reshape((-1, 1, 2))],
592
  isClosed=True, color=255,
@@ -636,22 +600,8 @@ def draw_eyebrows(rgb: np.ndarray, cfg: LineartConfig, faces: Optional[list] = N
636
  draw_eyebrow_landmarks(face_lms, x1, y1, crop_w, crop_h)
637
 
638
  print(f"[eyebrows] Drew eyebrows for {len(drawn_centers_px)} face(s)")
639
-
640
- # If MediaPipe ran but found no faces, use the geometric fallback so
641
- # eyebrows / face shape are still present.
642
- if not drawn_centers_px:
643
- if faces is None:
644
- faces = detect_faces(rgb, cfg)
645
- if faces:
646
- eyebrows_layer = _draw_eyebrows_geometric(eyebrows_layer, faces, cfg)
647
  except Exception as e:
648
  print(f"[AI] Eyebrow extraction error or library missing: {e}")
649
- # MediaPipe failed to load (e.g. libGLESv2 missing). Fall back to the
650
- # geometric approximation from the Haar cascade boxes.
651
- if faces is None:
652
- faces = detect_faces(rgb, cfg)
653
- if faces:
654
- eyebrows_layer = _draw_eyebrows_geometric(eyebrows_layer, faces, cfg)
655
 
656
  if eyebrows_layer.any():
657
  dil = getattr(cfg, 'eyebrow_dilate', 1)
@@ -664,10 +614,6 @@ def draw_eyebrows(rgb: np.ndarray, cfg: LineartConfig, faces: Optional[list] = N
664
  def make_faceless_lineart(img: Image.Image, cfg: LineartConfig = None) -> Image.Image:
665
  cfg = cfg or LineartConfig()
666
 
667
- # Honour EXIF orientation (phone photos). Without this, photos with an
668
- # orientation tag are processed sideways and end up rotated/cropped.
669
- img = ImageOps.exif_transpose(img)
670
-
671
  alpha_channel: Optional[np.ndarray] = None
672
  if img.mode in ("RGBA", "LA") or (img.mode == "P" and "transparency" in img.info):
673
  rgba_img = img.convert("RGBA")
@@ -749,10 +695,13 @@ def _has_real_transparency(img: Image.Image) -> bool:
749
  return total > 0 and (visible / total) >= 0.03
750
 
751
 
 
 
 
 
 
752
  def remove_background_subject(img: Image.Image) -> Image.Image:
753
  try:
754
- # Match the lineart orientation so the cutout never ends up rotated/cropped.
755
- img = ImageOps.exif_transpose(img)
756
  rgba = img.convert("RGBA")
757
  if _has_real_transparency(rgba):
758
  return rgba
@@ -774,6 +723,7 @@ def make_faceless_cutout(img: Image.Image) -> Image.Image:
774
  return remove_background_subject(img)
775
 
776
 
 
777
  def build_faceless_embroidery_assets(img: Image.Image, cfg: Optional[LineartConfig] = None, **kwargs) -> Tuple[Image.Image, Image.Image]:
778
  if cfg is None:
779
  cfg = LineartConfig()
 
1
  import cv2
2
  import numpy as np
3
+ from PIL import Image, ImageFilter
4
  from dataclasses import dataclass
5
  from typing import Tuple, Optional
6
  import io
 
34
  _LINEART_AVAILABLE = True
35
 
36
 
37
+
38
+
39
  def get_lineart_detector():
40
  """
41
  Lazy-load the controlnet_aux LineartDetector (informative-drawings model
 
75
  os.makedirs(cascade_dir, exist_ok=True)
76
  xml_urls = {
77
  'haarcascade_frontalface_default.xml': 'https://raw.githubusercontent.com/opencv/opencv/master/data/haarcascades/haarcascade_frontalface_default.xml',
78
+ 'haarcascade_frontalface_alt2.xml': 'https://raw.githubusercontent.com/opencv/opencv/master/data/haarcascades/haarcascade_frontalface_alt2.xml'
 
79
  }
80
  for name, url in xml_urls.items():
81
  path = os.path.join(cascade_dir, name)
 
105
  get_lineart_detector()
106
 
107
 
108
+
109
  @dataclass
110
  class LineartConfig:
111
  # Backward compatibility fields
 
129
  mask_smooth_threshold: int = 128
130
  min_silhouette_area: float = 200.0
131
 
132
+ # Silhouette outline
133
+
134
+ silhouette_smooth_ksize: int = 5
135
+
136
  silhouette_line_thickness: int = 3
137
 
138
+ # --- Neural line-art parameters ---
139
+ lineart_detect_resolution: int = 2048
140
+ lineart_image_resolution: int = 2048
141
+ lineart_coarse: bool = True
142
+
143
+ # AUMENTADO: Força a ignorar dobras de roupa e focar só na estrutura
144
+ lineart_threshold: int = 100
145
+
146
+ # AJUSTE DE CABELO E CORPO: Diminuído para capturar fios e dobras de roupa.
147
+ lineart_min_perimeter: float = 70.0
148
+
149
+ # Diminuído para manter os traços mais próximos da borda.
150
+ interior_erode_size: int = 1
151
+
152
+ # AJUSTE GERAL: Afinado para traços internos mais delicados.
 
 
153
  interior_line_thickness: int = 2
154
 
155
+
156
+
157
+ # ---- Canny fallback parametros ----
158
+ canny_low: int = 35
159
+ canny_high: int = 75
160
  bilateral_d: int = 11
161
  bilateral_sigma_color: int = 110
162
  bilateral_sigma_space: int = 110
163
  gaussian_ksize: int = 11
164
+ min_contour_perimeter: float = 15.0
165
  min_embroidery_stitch_px: int = 25
166
  enable_detail_pass: bool = False
167
  detail_canny_low: int = 15
 
179
  face_nms_iou_thresh: float = 0.35
180
  face_max_count: int = 6
181
 
182
+ # Face erase ellipse
183
  face_cx_ratio: float = 0.50
184
+ face_cy_ratio: float = 0.55
185
+ face_rx_ratio: float = 0.48
186
+ face_ry_ratio: float = 0.50
187
  face_erase_blur: int = 5
188
  face_erase_threshold: int = 90
189
 
190
  # MediaPipe
191
  mediapipe_num_faces: int = 6
192
 
193
+ # --- AJUSTE DE SOBRANCELHA ---
194
+ # Reduzido drasticamente para um traço fino e sutil.
195
+ eyebrow_thickness: int = 2
196
+ # Zerado (era 1). Isso remove o "borrão" que deixava a sobrancelha grossa demais.
197
+ eyebrow_dilate: int = 0
198
 
199
+ # Face shape
 
200
  draw_face_oval: bool = True
201
+ face_oval_thickness: int = 1
202
+ face_oval_skip_top_ratio: float = 0.50
203
+
204
+
205
+
206
+
207
 
208
 
209
  def _nms_faces(faces: list, iou_thresh: float) -> list:
 
228
  return [faces[i] for i in keep]
229
 
230
 
 
 
 
 
 
 
 
 
 
 
 
231
  def detect_faces(rgb: np.ndarray, cfg: LineartConfig) -> list:
232
+ h = rgb.shape[0]
233
  gray = cv2.cvtColor(rgb, cv2.COLOR_RGB2GRAY)
234
  clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
235
  gray_eq = clahe.apply(gray)
 
236
  faces = []
237
  try:
238
  cascade_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "cascades")
239
+ path_default = os.path.join(cascade_dir, 'haarcascade_frontalface_default.xml')
240
+ path_alt = os.path.join(cascade_dir, 'haarcascade_frontalface_alt2.xml')
241
+ classifiers = []
242
+ if os.path.exists(path_default):
243
+ classifiers.append(cv2.CascadeClassifier(path_default))
244
+ if os.path.exists(path_alt):
245
+ classifiers.append(cv2.CascadeClassifier(path_alt))
246
+ if not classifiers or all(c.empty() for c in classifiers):
247
+ classifiers = [
248
+ cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml'),
249
+ cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_alt2.xml')
250
+ ]
251
+ for cascade in classifiers:
252
+ if cascade and not cascade.empty():
253
+ for img_gray in [gray, gray_eq]:
254
+ detected = cascade.detectMultiScale(
255
+ img_gray,
256
+ scaleFactor=cfg.face_scale_factor,
257
+ minNeighbors=cfg.face_min_neighbors,
258
+ minSize=cfg.face_min_size
259
+ )
260
+ if len(detected) > 0:
261
+ faces.extend(detected.tolist())
 
 
 
 
 
 
 
 
262
  except Exception as e:
263
  print(f"[AI] Face detection failed or cascades unavailable: {e}")
264
 
 
269
  faces = sorted(faces, key=lambda f: f[2] * f[3], reverse=True)[:max_faces]
270
  return faces
271
 
272
+
273
 
274
  def build_subject_mask(rgb: np.ndarray, cfg: LineartConfig, alpha_channel: Optional[np.ndarray] = None) -> np.ndarray:
275
  if alpha_channel is not None:
 
297
  spline_pts = np.stack([x_new, y_new], axis=1).astype(np.int32)
298
  return spline_pts.reshape((-1, 1, 2))
299
  except Exception:
300
+
301
+
302
  return None
303
 
304
 
 
362
  return strength.astype(np.uint8)
363
 
364
 
365
+
366
  def build_interior_edges(rgb: np.ndarray, subject_mask: np.ndarray, cfg: LineartConfig) -> np.ndarray:
367
  """
368
  Detect structural lines (clothing folds, lapels, collars, cuffs, hair) using
 
407
  cv2.drawContours(filtered, [cnt], -1, 255, cfg.interior_line_thickness)
408
 
409
  # Gentle dilation for solid, embroiderable strokes
410
+ ernel_dilate = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
411
  filtered = cv2.dilate(filtered, kernel_dilate, iterations=1)
412
+ filtered = cv2.GaussianBlur(filtered, (3, 3), 0)
413
+ filtered = cv2.threshold(filtered, 127, 255, cv2.THRESH_BINARY)
414
  return filtered
415
  except Exception as e:
416
  print(f"[AI] Neural lineart failed, falling back to Canny: {e}")
 
473
  return _LANDMARKER_CACHE
474
 
475
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
476
  def draw_eyebrows(rgb: np.ndarray, cfg: LineartConfig, faces: Optional[list] = None) -> np.ndarray:
477
  h, w, _ = rgb.shape
478
  eyebrows_layer = np.zeros((h, w), dtype=np.uint8)
 
497
  172, 58, 132, 93, 234, 127, 162, 21, 54, 103, 67, 109]
498
 
499
  eyebrow_thickness = getattr(cfg, 'eyebrow_thickness', cfg.interior_line_thickness)
500
+ draw_oval = True
501
  oval_thickness = getattr(cfg, 'face_oval_thickness', 2)
502
  oval_skip_top = getattr(cfg, 'face_oval_skip_top_ratio', 0.18)
503
  drawn_centers_px: list = []
 
523
  thickness=eyebrow_thickness, lineType=cv2.LINE_AA)
524
 
525
  # Face oval (head / jaw outline) — gives the "formato do rosto"
526
+ if draw_oval:
527
+ oval_pts = []
528
  for idx in FACE_OVAL:
529
  lm = face_lms[idx]
530
  px = int(origin_x + lm.x * scale_x)
531
  py = int(origin_y + lm.y * scale_y)
532
  oval_pts.append([px, py])
533
  oval_np = np.array(oval_pts, np.int32)
534
+
535
  if len(oval_np) >= 4:
536
+ # LÓGICA REVISADA: Focar apenas no terço inferior (queixo/maxilar)
537
+ y_min = oval_np[:, 1].min()
538
+ y_max = oval_np[:, 1].max()
539
+
540
+ # Corta os 60% superiores do rosto (mantém apenas o "U" inferior)
541
+ cutoff = y_min + (y_max - y_min) * 0.60
542
+
543
+ kept = oval_np[oval_np[:, 1] >= cutoff]
544
+
545
+ if len(kept) >= 4:
546
+ # Ordena os pontos da esquerda para a direita para evitar o polígono maluco
547
+ kept = kept[np.argsort(kept[:, 0])]
548
+ arc = kept.reshape((-1, 1, 2))
549
+
550
+ # Desenha o maxilar sem fechar a forma
551
+ cv2.polylines(eyebrows_layer, [arc], isClosed=False,
552
+ color=255, thickness=oval_thickness,
553
+ lineType=cv2.LINE_AA)
554
  else:
555
  cv2.polylines(eyebrows_layer, [oval_np.reshape((-1, 1, 2))],
556
  isClosed=True, color=255,
 
600
  draw_eyebrow_landmarks(face_lms, x1, y1, crop_w, crop_h)
601
 
602
  print(f"[eyebrows] Drew eyebrows for {len(drawn_centers_px)} face(s)")
 
 
 
 
 
 
 
 
603
  except Exception as e:
604
  print(f"[AI] Eyebrow extraction error or library missing: {e}")
 
 
 
 
 
 
605
 
606
  if eyebrows_layer.any():
607
  dil = getattr(cfg, 'eyebrow_dilate', 1)
 
614
  def make_faceless_lineart(img: Image.Image, cfg: LineartConfig = None) -> Image.Image:
615
  cfg = cfg or LineartConfig()
616
 
 
 
 
 
617
  alpha_channel: Optional[np.ndarray] = None
618
  if img.mode in ("RGBA", "LA") or (img.mode == "P" and "transparency" in img.info):
619
  rgba_img = img.convert("RGBA")
 
695
  return total > 0 and (visible / total) >= 0.03
696
 
697
 
698
+
699
+
700
+
701
+
702
+
703
  def remove_background_subject(img: Image.Image) -> Image.Image:
704
  try:
 
 
705
  rgba = img.convert("RGBA")
706
  if _has_real_transparency(rgba):
707
  return rgba
 
723
  return remove_background_subject(img)
724
 
725
 
726
+
727
  def build_faceless_embroidery_assets(img: Image.Image, cfg: Optional[LineartConfig] = None, **kwargs) -> Tuple[Image.Image, Image.Image]:
728
  if cfg is None:
729
  cfg = LineartConfig()