Update faceless_processing.py

#1
Files changed (1) hide show
  1. faceless_processing.py +24 -59
faceless_processing.py CHANGED
@@ -34,8 +34,6 @@ _LINEART_DETECTOR = None
34
  _LINEART_AVAILABLE = True
35
 
36
 
37
-
38
-
39
  def get_lineart_detector():
40
  """
41
  Lazy-load the controlnet_aux LineartDetector (informative-drawings model
@@ -105,7 +103,6 @@ _download_assets_globally()
105
  get_lineart_detector()
106
 
107
 
108
-
109
  @dataclass
110
  class LineartConfig:
111
  # Backward compatibility fields
@@ -137,20 +134,15 @@ class LineartConfig:
137
  lineart_image_resolution: int = 1024
138
  lineart_coarse: bool = True
139
 
140
- # AUMENTADO: Força a ignorar dobras de roupa e focar na estrutura
141
- lineart_threshold: int = 100
142
 
143
- # AJUSTE DE CABELO E CORPO: Diminuído para capturar fios e dobras de roupa.
144
- lineart_min_perimeter: float = 70.0
145
 
146
- # Diminuído para manter os traços mais próximos da borda.
147
  interior_erode_size: int = 1
148
-
149
- # AJUSTE GERAL: Afinado para traços internos mais delicados.
150
  interior_line_thickness: int = 2
151
 
152
-
153
-
154
  # ---- Canny fallback parametros ----
155
  canny_low: int = 35
156
  canny_high: int = 75
@@ -188,9 +180,7 @@ class LineartConfig:
188
  mediapipe_num_faces: int = 6
189
 
190
  # --- AJUSTE DE SOBRANCELHA ---
191
- # Reduzido drasticamente para um traço fino e sutil.
192
  eyebrow_thickness: int = 2
193
- # Zerado (era 1). Isso remove o "borrão" que deixava a sobrancelha grossa demais.
194
  eyebrow_dilate: int = 0
195
 
196
  # Face shape
@@ -199,8 +189,6 @@ class LineartConfig:
199
  face_oval_skip_top_ratio: float = 0.50
200
 
201
 
202
-
203
-
204
  def _nms_faces(faces: list, iou_thresh: float) -> list:
205
  if not faces:
206
  return []
@@ -264,7 +252,6 @@ def detect_faces(rgb: np.ndarray, cfg: LineartConfig) -> list:
264
  faces = sorted(faces, key=lambda f: f[2] * f[3], reverse=True)[:max_faces]
265
  return faces
266
 
267
-
268
 
269
  def build_subject_mask(rgb: np.ndarray, cfg: LineartConfig, alpha_channel: Optional[np.ndarray] = None) -> np.ndarray:
270
  if alpha_channel is not None:
@@ -292,8 +279,6 @@ def _smooth_contour_spline(cnt: np.ndarray, n_points: int = 400) -> Optional[np.
292
  spline_pts = np.stack([x_new, y_new], axis=1).astype(np.int32)
293
  return spline_pts.reshape((-1, 1, 2))
294
  except Exception:
295
-
296
-
297
  return None
298
 
299
 
@@ -338,32 +323,20 @@ def build_face_erase_mask(faces: list, shape: Tuple[int, int], cfg: LineartConfi
338
 
339
 
340
  def _line_strength_from_detector(pil_lineart: Image.Image, target_hw: Tuple[int, int]) -> np.ndarray:
341
- """
342
- Convert the detector output into a polarity-normalised "line strength" map
343
- where lines = bright (255) on a black (0) background, resized to target.
344
- Handles either polarity (black-on-white or white-on-black).
345
- """
346
  h, w = target_hw
347
  gray = np.array(pil_lineart.convert("L"))
348
- # Determine background from the image corners
349
  corners = [gray[0, 0], gray[0, -1], gray[-1, 0], gray[-1, -1]]
350
  bg = float(np.median(corners))
351
  if bg > 127:
352
- strength = 255 - gray # dark lines on light bg -> invert
353
  else:
354
- strength = gray # already light lines on dark bg
355
  if strength.shape[:2] != (h, w):
356
  strength = cv2.resize(strength, (w, h), interpolation=cv2.INTER_LINEAR)
357
  return strength.astype(np.uint8)
358
 
359
 
360
-
361
  def build_interior_edges(rgb: np.ndarray, subject_mask: np.ndarray, cfg: LineartConfig) -> np.ndarray:
362
- """
363
- Detect structural lines (clothing folds, lapels, collars, cuffs, hair) using
364
- the neural LineartDetector. Produces continuous, illustrator-style strokes.
365
- Falls back to the legacy Canny pipeline if controlnet_aux is unavailable.
366
- """
367
  h, w = rgb.shape[:2]
368
  detector = get_lineart_detector()
369
 
@@ -379,12 +352,8 @@ def build_interior_edges(rgb: np.ndarray, subject_mask: np.ndarray, cfg: Lineart
379
  )
380
  strength = _line_strength_from_detector(pil_out, (h, w))
381
 
382
- # Threshold to a binary line map
383
  _, edges = cv2.threshold(strength, cfg.lineart_threshold, 255, cv2.THRESH_BINARY)
384
 
385
- # Keep lines only inside the subject. Use a SMALL erosion so that
386
- # face-shape, hairline and jaw lines (which sit close to the
387
- # silhouette) are preserved instead of being eaten away.
388
  erode_size = getattr(cfg, 'interior_erode_size', 3)
389
  erode_size = max(1, erode_size)
390
  if erode_size % 2 == 0:
@@ -393,24 +362,35 @@ def build_interior_edges(rgb: np.ndarray, subject_mask: np.ndarray, cfg: Lineart
393
  eroded_mask = cv2.erode(subject_mask, kernel_erode, iterations=1)
394
  edges = cv2.bitwise_and(edges, eroded_mask)
395
 
396
- # Remove speckle: drop tiny contours
 
 
 
 
397
  contours, _ = cv2.findContours(edges, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
398
  filtered = np.zeros_like(edges)
399
  for cnt in contours:
400
- if cv2.arcLength(cnt, False) < cfg.lineart_min_perimeter:
 
 
 
 
401
  continue
 
402
  cv2.drawContours(filtered, [cnt], -1, 255, cfg.interior_line_thickness)
403
 
404
- # Gentle dilation for solid, embroiderable strokes
405
- ernel_dilate = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
406
  filtered = cv2.dilate(filtered, kernel_dilate, iterations=1)
407
  filtered = cv2.GaussianBlur(filtered, (3, 3), 0)
408
- filtered = cv2.threshold(filtered, 127, 255, cv2.THRESH_BINARY)
 
 
 
409
  return filtered
410
  except Exception as e:
411
  print(f"[AI] Neural lineart failed, falling back to Canny: {e}")
412
 
413
- # ---------------- Legacy Canny fallback ----------------
414
  return _build_interior_edges_canny(rgb, subject_mask, cfg)
415
 
416
 
@@ -457,8 +437,6 @@ def get_landmarker(model_path, num_faces: int = 6):
457
  output_face_blendshapes=False,
458
  output_facial_transformation_matrixes=False,
459
  num_faces=num_faces,
460
- # Lower confidence thresholds so smiling / tilted / smaller faces
461
- # are still detected and their eyebrows get drawn.
462
  min_face_detection_confidence=0.2,
463
  min_face_presence_confidence=0.2,
464
  min_tracking_confidence=0.2,
@@ -486,7 +464,6 @@ def draw_eyebrows(rgb: np.ndarray, cfg: LineartConfig, faces: Optional[list] = N
486
 
487
  LEFT_EYEBROW = [70, 63, 105, 66, 107, 55, 65, 52, 53, 46]
488
  RIGHT_EYEBROW = [336, 296, 334, 293, 300, 285, 295, 282, 283, 276]
489
- # MediaPipe face oval (head/jaw outline), ordered around the perimeter.
490
  FACE_OVAL = [10, 338, 297, 332, 284, 251, 389, 356, 454, 323, 361, 288,
491
  397, 365, 379, 378, 400, 377, 152, 148, 176, 149, 150, 136,
492
  172, 58, 132, 93, 234, 127, 162, 21, 54, 103, 67, 109]
@@ -505,7 +482,6 @@ def draw_eyebrows(rgb: np.ndarray, cfg: LineartConfig, faces: Optional[list] = N
505
  return False
506
 
507
  def draw_eyebrow_landmarks(face_lms, origin_x, origin_y, scale_x, scale_y):
508
- # Eyebrows
509
  for indices in [LEFT_EYEBROW, RIGHT_EYEBROW]:
510
  pts = []
511
  for idx in indices:
@@ -517,7 +493,6 @@ def draw_eyebrows(rgb: np.ndarray, cfg: LineartConfig, faces: Optional[list] = N
517
  cv2.polylines(eyebrows_layer, [pts_np], isClosed=False, color=255,
518
  thickness=eyebrow_thickness, lineType=cv2.LINE_AA)
519
 
520
- # Face oval (head / jaw outline) — gives the "formato do rosto"
521
  if draw_oval:
522
  oval_pts = []
523
  for idx in FACE_OVAL:
@@ -527,16 +502,12 @@ def draw_eyebrows(rgb: np.ndarray, cfg: LineartConfig, faces: Optional[list] = N
527
  oval_pts.append([px, py])
528
  oval_np = np.array(oval_pts, np.int32)
529
  if len(oval_np) >= 4:
530
- # Optionally drop the top (forehead) points so the outline
531
- # doesn't cut a hard line across the hair.
532
  if oval_skip_top > 0:
533
  y_min = oval_np[:, 1].min()
534
  y_max = oval_np[:, 1].max()
535
  cutoff = y_min + (y_max - y_min) * oval_skip_top
536
  kept = oval_np[oval_np[:, 1] >= cutoff]
537
  if len(kept) >= 4:
538
- # open arc (jaw + cheeks), not closed, to leave the
539
- # crown of the head to the hair/silhouette lines
540
  arc = kept.reshape((-1, 1, 2))
541
  cv2.polylines(eyebrows_layer, [arc], isClosed=False,
542
  color=255, thickness=oval_thickness,
@@ -620,8 +591,6 @@ def make_faceless_lineart(img: Image.Image, cfg: LineartConfig = None) -> Image.
620
  rgb = img_np[:, :, :3]
621
  h, w = rgb.shape[:2]
622
 
623
- # If no alpha was provided, derive a subject mask with rembg for a clean
624
- # silhouette and to confine interior lines to the subject.
625
  if alpha_channel is None and _SESSION_ISNET is not None:
626
  try:
627
  cut = remove(Image.fromarray(rgb).convert("RGBA"), session=_SESSION_ISNET, only_mask=True)
@@ -685,9 +654,6 @@ def _has_real_transparency(img: Image.Image) -> bool:
685
  return total > 0 and (visible / total) >= 0.03
686
 
687
 
688
-
689
-
690
-
691
  def remove_background_subject(img: Image.Image) -> Image.Image:
692
  try:
693
  rgba = img.convert("RGBA")
@@ -711,7 +677,6 @@ def make_faceless_cutout(img: Image.Image) -> Image.Image:
711
  return remove_background_subject(img)
712
 
713
 
714
-
715
  def build_faceless_embroidery_assets(img: Image.Image, cfg: Optional[LineartConfig] = None, **kwargs) -> Tuple[Image.Image, Image.Image]:
716
  if cfg is None:
717
  cfg = LineartConfig()
@@ -721,4 +686,4 @@ def build_faceless_embroidery_assets(img: Image.Image, cfg: Optional[LineartConf
721
  cfg.negative_prompt = kwargs["negative_prompt"]
722
  lineart = make_faceless_lineart(img, cfg)
723
  cutout = make_faceless_cutout(img)
724
- return lineart, cutout
 
34
  _LINEART_AVAILABLE = True
35
 
36
 
 
 
37
  def get_lineart_detector():
38
  """
39
  Lazy-load the controlnet_aux LineartDetector (informative-drawings model
 
103
  get_lineart_detector()
104
 
105
 
 
106
  @dataclass
107
  class LineartConfig:
108
  # Backward compatibility fields
 
134
  lineart_image_resolution: int = 1024
135
  lineart_coarse: bool = True
136
 
137
+ # EQUILÍBRIO: 80 é forte o suficiente para ignorar sombra, mas não apaga o rosto.
138
+ lineart_threshold: int = 80
139
 
140
+ # EQUILÍBRIO: 60 pixels corta fios curtos, preservando linhas estruturais.
141
+ lineart_min_perimeter: float = 60.0
142
 
 
143
  interior_erode_size: int = 1
 
 
144
  interior_line_thickness: int = 2
145
 
 
 
146
  # ---- Canny fallback parametros ----
147
  canny_low: int = 35
148
  canny_high: int = 75
 
180
  mediapipe_num_faces: int = 6
181
 
182
  # --- AJUSTE DE SOBRANCELHA ---
 
183
  eyebrow_thickness: int = 2
 
184
  eyebrow_dilate: int = 0
185
 
186
  # Face shape
 
189
  face_oval_skip_top_ratio: float = 0.50
190
 
191
 
 
 
192
  def _nms_faces(faces: list, iou_thresh: float) -> list:
193
  if not faces:
194
  return []
 
252
  faces = sorted(faces, key=lambda f: f[2] * f[3], reverse=True)[:max_faces]
253
  return faces
254
 
 
255
 
256
  def build_subject_mask(rgb: np.ndarray, cfg: LineartConfig, alpha_channel: Optional[np.ndarray] = None) -> np.ndarray:
257
  if alpha_channel is not None:
 
279
  spline_pts = np.stack([x_new, y_new], axis=1).astype(np.int32)
280
  return spline_pts.reshape((-1, 1, 2))
281
  except Exception:
 
 
282
  return None
283
 
284
 
 
323
 
324
 
325
  def _line_strength_from_detector(pil_lineart: Image.Image, target_hw: Tuple[int, int]) -> np.ndarray:
 
 
 
 
 
326
  h, w = target_hw
327
  gray = np.array(pil_lineart.convert("L"))
 
328
  corners = [gray[0, 0], gray[0, -1], gray[-1, 0], gray[-1, -1]]
329
  bg = float(np.median(corners))
330
  if bg > 127:
331
+ strength = 255 - gray
332
  else:
333
+ strength = gray
334
  if strength.shape[:2] != (h, w):
335
  strength = cv2.resize(strength, (w, h), interpolation=cv2.INTER_LINEAR)
336
  return strength.astype(np.uint8)
337
 
338
 
 
339
  def build_interior_edges(rgb: np.ndarray, subject_mask: np.ndarray, cfg: LineartConfig) -> np.ndarray:
 
 
 
 
 
340
  h, w = rgb.shape[:2]
341
  detector = get_lineart_detector()
342
 
 
352
  )
353
  strength = _line_strength_from_detector(pil_out, (h, w))
354
 
 
355
  _, edges = cv2.threshold(strength, cfg.lineart_threshold, 255, cv2.THRESH_BINARY)
356
 
 
 
 
357
  erode_size = getattr(cfg, 'interior_erode_size', 3)
358
  erode_size = max(1, erode_size)
359
  if erode_size % 2 == 0:
 
362
  eroded_mask = cv2.erode(subject_mask, kernel_erode, iterations=1)
363
  edges = cv2.bitwise_and(edges, eroded_mask)
364
 
365
+ # --- O "ASPIRADOR DE PÓ" MATEMÁTICO ---
366
+ # Remove pontos solitários grossos antes da medição
367
+ kernel_clean = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
368
+ edges = cv2.morphologyEx(edges, cv2.MORPH_OPEN, kernel_clean, iterations=1)
369
+
370
  contours, _ = cv2.findContours(edges, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
371
  filtered = np.zeros_like(edges)
372
  for cnt in contours:
373
+ perimeter = cv2.arcLength(cnt, False)
374
+ area = cv2.contourArea(cnt)
375
+
376
+ # Barreira dupla: Remove traços menores que 60px E bolinhas grossas.
377
+ if perimeter < cfg.lineart_min_perimeter or (perimeter < 150 and area < 10.0):
378
  continue
379
+
380
  cv2.drawContours(filtered, [cnt], -1, 255, cfg.interior_line_thickness)
381
 
382
+ # Suavização final para cantos realistas de linha de costura
383
+ kernel_dilate = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
384
  filtered = cv2.dilate(filtered, kernel_dilate, iterations=1)
385
  filtered = cv2.GaussianBlur(filtered, (3, 3), 0)
386
+
387
+ # CORREÇÃO: cv2.threshold retorna uma tupla (_, imagem_resultante)
388
+ _, filtered = cv2.threshold(filtered, 127, 255, cv2.THRESH_BINARY)
389
+
390
  return filtered
391
  except Exception as e:
392
  print(f"[AI] Neural lineart failed, falling back to Canny: {e}")
393
 
 
394
  return _build_interior_edges_canny(rgb, subject_mask, cfg)
395
 
396
 
 
437
  output_face_blendshapes=False,
438
  output_facial_transformation_matrixes=False,
439
  num_faces=num_faces,
 
 
440
  min_face_detection_confidence=0.2,
441
  min_face_presence_confidence=0.2,
442
  min_tracking_confidence=0.2,
 
464
 
465
  LEFT_EYEBROW = [70, 63, 105, 66, 107, 55, 65, 52, 53, 46]
466
  RIGHT_EYEBROW = [336, 296, 334, 293, 300, 285, 295, 282, 283, 276]
 
467
  FACE_OVAL = [10, 338, 297, 332, 284, 251, 389, 356, 454, 323, 361, 288,
468
  397, 365, 379, 378, 400, 377, 152, 148, 176, 149, 150, 136,
469
  172, 58, 132, 93, 234, 127, 162, 21, 54, 103, 67, 109]
 
482
  return False
483
 
484
  def draw_eyebrow_landmarks(face_lms, origin_x, origin_y, scale_x, scale_y):
 
485
  for indices in [LEFT_EYEBROW, RIGHT_EYEBROW]:
486
  pts = []
487
  for idx in indices:
 
493
  cv2.polylines(eyebrows_layer, [pts_np], isClosed=False, color=255,
494
  thickness=eyebrow_thickness, lineType=cv2.LINE_AA)
495
 
 
496
  if draw_oval:
497
  oval_pts = []
498
  for idx in FACE_OVAL:
 
502
  oval_pts.append([px, py])
503
  oval_np = np.array(oval_pts, np.int32)
504
  if len(oval_np) >= 4:
 
 
505
  if oval_skip_top > 0:
506
  y_min = oval_np[:, 1].min()
507
  y_max = oval_np[:, 1].max()
508
  cutoff = y_min + (y_max - y_min) * oval_skip_top
509
  kept = oval_np[oval_np[:, 1] >= cutoff]
510
  if len(kept) >= 4:
 
 
511
  arc = kept.reshape((-1, 1, 2))
512
  cv2.polylines(eyebrows_layer, [arc], isClosed=False,
513
  color=255, thickness=oval_thickness,
 
591
  rgb = img_np[:, :, :3]
592
  h, w = rgb.shape[:2]
593
 
 
 
594
  if alpha_channel is None and _SESSION_ISNET is not None:
595
  try:
596
  cut = remove(Image.fromarray(rgb).convert("RGBA"), session=_SESSION_ISNET, only_mask=True)
 
654
  return total > 0 and (visible / total) >= 0.03
655
 
656
 
 
 
 
657
  def remove_background_subject(img: Image.Image) -> Image.Image:
658
  try:
659
  rgba = img.convert("RGBA")
 
677
  return remove_background_subject(img)
678
 
679
 
 
680
  def build_faceless_embroidery_assets(img: Image.Image, cfg: Optional[LineartConfig] = None, **kwargs) -> Tuple[Image.Image, Image.Image]:
681
  if cfg is None:
682
  cfg = LineartConfig()
 
686
  cfg.negative_prompt = kwargs["negative_prompt"]
687
  lineart = make_faceless_lineart(img, cfg)
688
  cutout = make_faceless_cutout(img)
689
+ return lineart, cutout