vitordigitizing commited on
Commit
73ed5e6
·
verified ·
1 Parent(s): 43b4d1c

Update faceless_processing.py

Browse files
Files changed (1) hide show
  1. faceless_processing.py +24 -60
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 só 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,17 +180,15 @@ 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
197
  draw_face_oval: bool = True
198
  face_oval_thickness: int = 1
199
- face_oval_skip_top_ratio: float = 0.50
200
-
201
-
202
 
203
 
204
  def _nms_faces(faces: list, iou_thresh: float) -> list:
@@ -264,7 +254,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 +281,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 +325,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 +354,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 +364,32 @@ 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 +436,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 +463,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 +481,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 +492,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 +501,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 +590,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 +653,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 +676,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 +685,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
+ # AUMENTADO: Força bruta para ignorar detalhes fracos
138
+ lineart_threshold: int = 120
139
 
140
+ # AUMENTADO: Faca implacável para traços curtos
141
+ lineart_min_perimeter: float = 90.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
187
  draw_face_oval: bool = True
188
  face_oval_thickness: int = 1
189
+
190
+ # AJUSTE FINO: Remove 45% do topo do oval (o que causava aquele erro bizarro)
191
+ face_oval_skip_top_ratio: float = 0.45
192
 
193
 
194
  def _nms_faces(faces: list, iou_thresh: float) -> list:
 
254
  faces = sorted(faces, key=lambda f: f[2] * f[3], reverse=True)[:max_faces]
255
  return faces
256
 
 
257
 
258
  def build_subject_mask(rgb: np.ndarray, cfg: LineartConfig, alpha_channel: Optional[np.ndarray] = None) -> np.ndarray:
259
  if alpha_channel is not None:
 
281
  spline_pts = np.stack([x_new, y_new], axis=1).astype(np.int32)
282
  return spline_pts.reshape((-1, 1, 2))
283
  except Exception:
 
 
284
  return None
285
 
286
 
 
325
 
326
 
327
  def _line_strength_from_detector(pil_lineart: Image.Image, target_hw: Tuple[int, int]) -> np.ndarray:
 
 
 
 
 
328
  h, w = target_hw
329
  gray = np.array(pil_lineart.convert("L"))
 
330
  corners = [gray[0, 0], gray[0, -1], gray[-1, 0], gray[-1, -1]]
331
  bg = float(np.median(corners))
332
  if bg > 127:
333
+ strength = 255 - gray
334
  else:
335
+ strength = gray
336
  if strength.shape[:2] != (h, w):
337
  strength = cv2.resize(strength, (w, h), interpolation=cv2.INTER_LINEAR)
338
  return strength.astype(np.uint8)
339
 
340
 
 
341
  def build_interior_edges(rgb: np.ndarray, subject_mask: np.ndarray, cfg: LineartConfig) -> np.ndarray:
 
 
 
 
 
342
  h, w = rgb.shape[:2]
343
  detector = get_lineart_detector()
344
 
 
354
  )
355
  strength = _line_strength_from_detector(pil_out, (h, w))
356
 
 
357
  _, edges = cv2.threshold(strength, cfg.lineart_threshold, 255, cv2.THRESH_BINARY)
358
 
 
 
 
359
  erode_size = getattr(cfg, 'interior_erode_size', 3)
360
  erode_size = max(1, erode_size)
361
  if erode_size % 2 == 0:
 
364
  eroded_mask = cv2.erode(subject_mask, kernel_erode, iterations=1)
365
  edges = cv2.bitwise_and(edges, eroded_mask)
366
 
367
+ # Aspirador de Sujeiras
368
+ kernel_clean = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
369
+ edges = cv2.morphologyEx(edges, cv2.MORPH_OPEN, kernel_clean, iterations=1)
370
+
371
  contours, _ = cv2.findContours(edges, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
372
  filtered = np.zeros_like(edges)
373
  for cnt in contours:
374
+ perimeter = cv2.arcLength(cnt, False)
375
+ area = cv2.contourArea(cnt)
376
+
377
+ # BARREIRA EXTREMA: Elimina pontos isolados e traços curtos
378
+ if perimeter < cfg.lineart_min_perimeter or area < 40.0:
379
  continue
380
+
381
  cv2.drawContours(filtered, [cnt], -1, 255, cfg.interior_line_thickness)
382
 
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
+ _, filtered = cv2.threshold(filtered, 127, 255, cv2.THRESH_BINARY)
388
+
389
  return filtered
390
  except Exception as e:
391
  print(f"[AI] Neural lineart failed, falling back to Canny: {e}")
392
 
 
393
  return _build_interior_edges_canny(rgb, subject_mask, cfg)
394
 
395
 
 
436
  output_face_blendshapes=False,
437
  output_facial_transformation_matrixes=False,
438
  num_faces=num_faces,
 
 
439
  min_face_detection_confidence=0.2,
440
  min_face_presence_confidence=0.2,
441
  min_tracking_confidence=0.2,
 
463
 
464
  LEFT_EYEBROW = [70, 63, 105, 66, 107, 55, 65, 52, 53, 46]
465
  RIGHT_EYEBROW = [336, 296, 334, 293, 300, 285, 295, 282, 283, 276]
 
466
  FACE_OVAL = [10, 338, 297, 332, 284, 251, 389, 356, 454, 323, 361, 288,
467
  397, 365, 379, 378, 400, 377, 152, 148, 176, 149, 150, 136,
468
  172, 58, 132, 93, 234, 127, 162, 21, 54, 103, 67, 109]
 
481
  return False
482
 
483
  def draw_eyebrow_landmarks(face_lms, origin_x, origin_y, scale_x, scale_y):
 
484
  for indices in [LEFT_EYEBROW, RIGHT_EYEBROW]:
485
  pts = []
486
  for idx in indices:
 
492
  cv2.polylines(eyebrows_layer, [pts_np], isClosed=False, color=255,
493
  thickness=eyebrow_thickness, lineType=cv2.LINE_AA)
494
 
 
495
  if draw_oval:
496
  oval_pts = []
497
  for idx in FACE_OVAL:
 
501
  oval_pts.append([px, py])
502
  oval_np = np.array(oval_pts, np.int32)
503
  if len(oval_np) >= 4:
 
 
504
  if oval_skip_top > 0:
505
  y_min = oval_np[:, 1].min()
506
  y_max = oval_np[:, 1].max()
507
  cutoff = y_min + (y_max - y_min) * oval_skip_top
508
  kept = oval_np[oval_np[:, 1] >= cutoff]
509
  if len(kept) >= 4:
 
 
510
  arc = kept.reshape((-1, 1, 2))
511
  cv2.polylines(eyebrows_layer, [arc], isClosed=False,
512
  color=255, thickness=oval_thickness,
 
590
  rgb = img_np[:, :, :3]
591
  h, w = rgb.shape[:2]
592
 
 
 
593
  if alpha_channel is None and _SESSION_ISNET is not None:
594
  try:
595
  cut = remove(Image.fromarray(rgb).convert("RGBA"), session=_SESSION_ISNET, only_mask=True)
 
653
  return total > 0 and (visible / total) >= 0.03
654
 
655
 
 
 
 
656
  def remove_background_subject(img: Image.Image) -> Image.Image:
657
  try:
658
  rgba = img.convert("RGBA")
 
676
  return remove_background_subject(img)
677
 
678
 
 
679
  def build_faceless_embroidery_assets(img: Image.Image, cfg: Optional[LineartConfig] = None, **kwargs) -> Tuple[Image.Image, Image.Image]:
680
  if cfg is None:
681
  cfg = LineartConfig()
 
685
  cfg.negative_prompt = kwargs["negative_prompt"]
686
  lineart = make_faceless_lineart(img, cfg)
687
  cutout = make_faceless_cutout(img)
688
+ return lineart, cutout