Spaces:
Sleeping
Sleeping
Update faceless_processing.py
Browse files- faceless_processing.py +115 -13
faceless_processing.py
CHANGED
|
@@ -202,6 +202,12 @@ class LineartConfig:
|
|
| 202 |
# AJUSTE FINO: Remove 50% do topo do oval (desenha apenas a mandíbula inferior)
|
| 203 |
face_oval_skip_top_ratio: float = 0.50
|
| 204 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
|
| 206 |
def _nms_faces(faces: list, iou_thresh: float) -> list:
|
| 207 |
if not faces:
|
|
@@ -501,6 +507,54 @@ def _line_strength_from_detector(pil_lineart: Image.Image, target_hw: Tuple[int,
|
|
| 501 |
return strength.astype(np.uint8)
|
| 502 |
|
| 503 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 504 |
def build_interior_edges(rgb: np.ndarray, subject_mask: np.ndarray, cfg: LineartConfig) -> np.ndarray:
|
| 505 |
h, w = rgb.shape[:2]
|
| 506 |
detector = get_lineart_detector()
|
|
@@ -534,19 +588,19 @@ def build_interior_edges(rgb: np.ndarray, subject_mask: np.ndarray, cfg: Lineart
|
|
| 534 |
edges = cv2.morphologyEx(edges, cv2.MORPH_OPEN, kernel_clean, iterations=1)
|
| 535 |
|
| 536 |
contours, _ = cv2.findContours(edges, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
|
| 537 |
-
filtered = np.zeros_like(edges)
|
| 538 |
-
|
| 539 |
min_area = getattr(cfg, 'lineart_min_area', 40.0)
|
|
|
|
|
|
|
| 540 |
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
|
| 551 |
kernel_dilate = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
|
| 552 |
filtered = cv2.dilate(filtered, kernel_dilate, iterations=1)
|
|
@@ -575,19 +629,34 @@ def _build_interior_edges_canny(rgb: np.ndarray, subject_mask: np.ndarray, cfg:
|
|
| 575 |
edges = cv2.bitwise_and(edges, eroded_mask)
|
| 576 |
|
| 577 |
contours, _ = cv2.findContours(edges, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
|
| 578 |
-
|
|
|
|
| 579 |
for cnt in contours:
|
| 580 |
perimeter = cv2.arcLength(cnt, False)
|
| 581 |
if perimeter < cfg.min_contour_perimeter:
|
| 582 |
continue
|
| 583 |
eps = 0.0012 * perimeter
|
| 584 |
smooth = cv2.approxPolyDP(cnt, eps, False)
|
| 585 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 586 |
kernel_dilate = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
|
| 587 |
filtered = cv2.dilate(filtered, kernel_dilate, iterations=1)
|
| 588 |
return filtered
|
| 589 |
|
| 590 |
|
|
|
|
| 591 |
_LANDMARKER_LOCK = threading.Lock()
|
| 592 |
_LANDMARKER_CACHE = None
|
| 593 |
_LANDMARKER_NUM_FACES = None
|
|
@@ -748,6 +817,7 @@ def draw_eyebrows(rgb: np.ndarray, cfg: LineartConfig, faces: Optional[list] = N
|
|
| 748 |
return eyebrows_layer
|
| 749 |
|
| 750 |
|
|
|
|
| 751 |
def make_faceless_lineart(img: Image.Image, cfg: LineartConfig = None) -> Image.Image:
|
| 752 |
cfg = cfg or LineartConfig()
|
| 753 |
|
|
@@ -781,6 +851,38 @@ def make_faceless_lineart(img: Image.Image, cfg: LineartConfig = None) -> Image.
|
|
| 781 |
faces = detect_faces(rgb, cfg)
|
| 782 |
print(f"[lineart] Detected {len(faces)} face(s)")
|
| 783 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 784 |
silhouette = build_silhouette(subject_mask, cfg)
|
| 785 |
interior = build_interior_edges(rgb, subject_mask, cfg)
|
| 786 |
|
|
|
|
| 202 |
# AJUSTE FINO: Remove 50% do topo do oval (desenha apenas a mandíbula inferior)
|
| 203 |
face_oval_skip_top_ratio: float = 0.50
|
| 204 |
|
| 205 |
+
# --- Parâmetros de limpeza e bordabilidade ---
|
| 206 |
+
min_line_distance: int = 8
|
| 207 |
+
line_overlap_threshold: float = 0.35
|
| 208 |
+
custom_tuned_for_embroidery: bool = False
|
| 209 |
+
|
| 210 |
+
|
| 211 |
|
| 212 |
def _nms_faces(faces: list, iou_thresh: float) -> list:
|
| 213 |
if not faces:
|
|
|
|
| 507 |
return strength.astype(np.uint8)
|
| 508 |
|
| 509 |
|
| 510 |
+
def _filter_dense_contours(contours: list, min_dist_px: int, overlap_threshold: float, min_perimeter: float, thickness: int, shape: Tuple[int, int], min_area: float = 0.0) -> np.ndarray:
|
| 511 |
+
"""
|
| 512 |
+
Filters contours to ensure that no two lines are too close to each other,
|
| 513 |
+
which makes the lineart highly suitable for embroidery (prevents dense/overlapping stitches).
|
| 514 |
+
"""
|
| 515 |
+
sorted_cnts = sorted(contours, key=lambda c: cv2.arcLength(c, False), reverse=True)
|
| 516 |
+
|
| 517 |
+
filtered_mask = np.zeros(shape, dtype=np.uint8)
|
| 518 |
+
dilated_mask = np.zeros(shape, dtype=np.uint8)
|
| 519 |
+
|
| 520 |
+
if min_dist_px > 0:
|
| 521 |
+
kernel_dist = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (min_dist_px * 2 + 1, min_dist_px * 2 + 1))
|
| 522 |
+
else:
|
| 523 |
+
kernel_dist = None
|
| 524 |
+
|
| 525 |
+
for cnt in sorted_cnts:
|
| 526 |
+
perimeter = cv2.arcLength(cnt, False)
|
| 527 |
+
# contourArea requires at least 3 points
|
| 528 |
+
area = cv2.contourArea(cnt) if len(cnt) >= 3 else 0.0
|
| 529 |
+
|
| 530 |
+
if perimeter < min_perimeter:
|
| 531 |
+
continue
|
| 532 |
+
if min_area > 0 and area < min_area:
|
| 533 |
+
continue
|
| 534 |
+
|
| 535 |
+
cnt_mask = np.zeros(shape, dtype=np.uint8)
|
| 536 |
+
cv2.drawContours(cnt_mask, [cnt], -1, 255, thickness)
|
| 537 |
+
|
| 538 |
+
if kernel_dist is not None and np.any(dilated_mask > 0):
|
| 539 |
+
intersection = cv2.bitwise_and(cnt_mask, dilated_mask)
|
| 540 |
+
num_intersect = cv2.countNonZero(intersection)
|
| 541 |
+
num_total = cv2.countNonZero(cnt_mask)
|
| 542 |
+
|
| 543 |
+
if num_total > 0 and (num_intersect / num_total) > overlap_threshold:
|
| 544 |
+
# Too close to existing lines
|
| 545 |
+
continue
|
| 546 |
+
|
| 547 |
+
cv2.drawContours(filtered_mask, [cnt], -1, 255, thickness)
|
| 548 |
+
|
| 549 |
+
if kernel_dist is not None:
|
| 550 |
+
cnt_draw = np.zeros(shape, dtype=np.uint8)
|
| 551 |
+
cv2.drawContours(cnt_draw, [cnt], -1, 255, thickness)
|
| 552 |
+
cnt_dilated = cv2.dilate(cnt_draw, kernel_dist, iterations=1)
|
| 553 |
+
dilated_mask = cv2.bitwise_or(dilated_mask, cnt_dilated)
|
| 554 |
+
|
| 555 |
+
return filtered_mask
|
| 556 |
+
|
| 557 |
+
|
| 558 |
def build_interior_edges(rgb: np.ndarray, subject_mask: np.ndarray, cfg: LineartConfig) -> np.ndarray:
|
| 559 |
h, w = rgb.shape[:2]
|
| 560 |
detector = get_lineart_detector()
|
|
|
|
| 588 |
edges = cv2.morphologyEx(edges, cv2.MORPH_OPEN, kernel_clean, iterations=1)
|
| 589 |
|
| 590 |
contours, _ = cv2.findContours(edges, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
|
|
|
|
|
|
|
| 591 |
min_area = getattr(cfg, 'lineart_min_area', 40.0)
|
| 592 |
+
min_dist = getattr(cfg, 'min_line_distance', 8)
|
| 593 |
+
overlap_thresh = getattr(cfg, 'line_overlap_threshold', 0.35)
|
| 594 |
|
| 595 |
+
filtered = _filter_dense_contours(
|
| 596 |
+
contours=contours,
|
| 597 |
+
min_dist_px=min_dist,
|
| 598 |
+
overlap_threshold=overlap_thresh,
|
| 599 |
+
min_perimeter=cfg.lineart_min_perimeter,
|
| 600 |
+
thickness=cfg.interior_line_thickness,
|
| 601 |
+
shape=edges.shape,
|
| 602 |
+
min_area=min_area
|
| 603 |
+
)
|
| 604 |
|
| 605 |
kernel_dilate = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
|
| 606 |
filtered = cv2.dilate(filtered, kernel_dilate, iterations=1)
|
|
|
|
| 629 |
edges = cv2.bitwise_and(edges, eroded_mask)
|
| 630 |
|
| 631 |
contours, _ = cv2.findContours(edges, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
|
| 632 |
+
|
| 633 |
+
smoothed_contours = []
|
| 634 |
for cnt in contours:
|
| 635 |
perimeter = cv2.arcLength(cnt, False)
|
| 636 |
if perimeter < cfg.min_contour_perimeter:
|
| 637 |
continue
|
| 638 |
eps = 0.0012 * perimeter
|
| 639 |
smooth = cv2.approxPolyDP(cnt, eps, False)
|
| 640 |
+
smoothed_contours.append(smooth)
|
| 641 |
+
|
| 642 |
+
min_dist = getattr(cfg, 'min_line_distance', 8)
|
| 643 |
+
overlap_thresh = getattr(cfg, 'line_overlap_threshold', 0.35)
|
| 644 |
+
|
| 645 |
+
filtered = _filter_dense_contours(
|
| 646 |
+
contours=smoothed_contours,
|
| 647 |
+
min_dist_px=min_dist,
|
| 648 |
+
overlap_threshold=overlap_thresh,
|
| 649 |
+
min_perimeter=cfg.min_contour_perimeter,
|
| 650 |
+
thickness=cfg.interior_line_thickness,
|
| 651 |
+
shape=edges.shape
|
| 652 |
+
)
|
| 653 |
+
|
| 654 |
kernel_dilate = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
|
| 655 |
filtered = cv2.dilate(filtered, kernel_dilate, iterations=1)
|
| 656 |
return filtered
|
| 657 |
|
| 658 |
|
| 659 |
+
|
| 660 |
_LANDMARKER_LOCK = threading.Lock()
|
| 661 |
_LANDMARKER_CACHE = None
|
| 662 |
_LANDMARKER_NUM_FACES = None
|
|
|
|
| 817 |
return eyebrows_layer
|
| 818 |
|
| 819 |
|
| 820 |
+
|
| 821 |
def make_faceless_lineart(img: Image.Image, cfg: LineartConfig = None) -> Image.Image:
|
| 822 |
cfg = cfg or LineartConfig()
|
| 823 |
|
|
|
|
| 851 |
faces = detect_faces(rgb, cfg)
|
| 852 |
print(f"[lineart] Detected {len(faces)} face(s)")
|
| 853 |
|
| 854 |
+
# Se NÃO detectou rosto, entra em modo de contingência ("cartoon-clean" automático)
|
| 855 |
+
is_cartoon_fallback = getattr(cfg, 'is_cartoon_fallback', False) or (len(faces) == 0)
|
| 856 |
+
|
| 857 |
+
if is_cartoon_fallback and not getattr(cfg, 'custom_tuned_for_embroidery', False):
|
| 858 |
+
print("[lineart] No face detected or cartoon fallback active. Tuning parameters for clean structured lineart.")
|
| 859 |
+
cfg.erase_faces = False
|
| 860 |
+
cfg.detect_eyebrows = False
|
| 861 |
+
cfg.erase_borders = False
|
| 862 |
+
|
| 863 |
+
# Parâmetros equilibrados para traços nítidos e bordáveis
|
| 864 |
+
cfg.lineart_threshold = 95
|
| 865 |
+
cfg.lineart_min_perimeter = 45.0
|
| 866 |
+
cfg.lineart_min_area = 20.0
|
| 867 |
+
cfg.lineart_morph_open = True
|
| 868 |
+
|
| 869 |
+
cfg.canny_low = 50
|
| 870 |
+
cfg.canny_high = 110
|
| 871 |
+
cfg.min_contour_perimeter = 35.0
|
| 872 |
+
|
| 873 |
+
# Suavização para remover pelos sem perder formas principais
|
| 874 |
+
cfg.bilateral_d = 7
|
| 875 |
+
cfg.bilateral_sigma_color = 90
|
| 876 |
+
cfg.bilateral_sigma_space = 90
|
| 877 |
+
cfg.gaussian_ksize = 5
|
| 878 |
+
|
| 879 |
+
# Filtro de proximidade entre linhas
|
| 880 |
+
cfg.min_line_distance = 8
|
| 881 |
+
cfg.line_overlap_threshold = 0.35
|
| 882 |
+
|
| 883 |
+
cfg.custom_tuned_for_embroidery = True
|
| 884 |
+
|
| 885 |
+
subject_mask = build_subject_mask(rgb, cfg, alpha_channel)
|
| 886 |
silhouette = build_silhouette(subject_mask, cfg)
|
| 887 |
interior = build_interior_edges(rgb, subject_mask, cfg)
|
| 888 |
|