Fceless / faceless_processing.py
vitordigitizing's picture
Update faceless_processing.py
73ed5e6 verified
Raw
History Blame Contribute Delete
29.1 kB
import cv2
import numpy as np
from PIL import Image, ImageFilter
from dataclasses import dataclass
from typing import Tuple, Optional
import io
import base64
import os
import urllib.request
from rembg import remove, new_session
import threading
try:
from scipy.interpolate import splprep, splev
_SCIPY_AVAILABLE = True
except ImportError:
_SCIPY_AVAILABLE = False
print("[AI] scipy not available — falling back to approxPolyDP for silhouette smoothing")
try:
_SESSION_ISNET = new_session("isnet-general-use")
except Exception as e:
print(f"[AI] Failed to load rembg session: {e}")
_SESSION_ISNET = None
# ---------------------------------------------------------------------------
# Neural line-art detector (controlnet_aux / informative-drawings)
# This is what produces the clean, continuous "illustrator" strokes that a
# Canny edge detector can never achieve.
# ---------------------------------------------------------------------------
_LINEART_LOCK = threading.Lock()
_LINEART_DETECTOR = None
_LINEART_AVAILABLE = True
def get_lineart_detector():
"""
Lazy-load the controlnet_aux LineartDetector (informative-drawings model
from lllyasviel/Annotators). Loaded once and cached. Moves to GPU if torch
+ CUDA are available, otherwise runs on CPU.
"""
global _LINEART_DETECTOR, _LINEART_AVAILABLE
if _LINEART_DETECTOR is not None or not _LINEART_AVAILABLE:
return _LINEART_DETECTOR
with _LINEART_LOCK:
if _LINEART_DETECTOR is not None:
return _LINEART_DETECTOR
try:
from controlnet_aux import LineartDetector
detector = LineartDetector.from_pretrained("lllyasviel/Annotators")
try:
import torch
if torch.cuda.is_available():
detector = detector.to("cuda")
print("[AI] LineartDetector loaded on CUDA")
else:
print("[AI] LineartDetector loaded on CPU")
except Exception:
print("[AI] LineartDetector loaded (torch device check skipped)")
_LINEART_DETECTOR = detector
except Exception as e:
print(f"[AI] controlnet_aux LineartDetector unavailable, falling back to Canny: {e}")
_LINEART_AVAILABLE = False
_LINEART_DETECTOR = None
return _LINEART_DETECTOR
# Globally download cascades and models at startup if missing
def _download_assets_globally():
try:
cascade_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "cascades")
os.makedirs(cascade_dir, exist_ok=True)
xml_urls = {
'haarcascade_frontalface_default.xml': 'https://raw.githubusercontent.com/opencv/opencv/master/data/haarcascades/haarcascade_frontalface_default.xml',
'haarcascade_frontalface_alt2.xml': 'https://raw.githubusercontent.com/opencv/opencv/master/data/haarcascades/haarcascade_frontalface_alt2.xml'
}
for name, url in xml_urls.items():
path = os.path.join(cascade_dir, name)
if not os.path.exists(path) or os.path.getsize(path) < 1000:
print(f"[AI] Downloading cascade {name} globally from OpenCV GitHub...")
try:
urllib.request.urlretrieve(url, path)
except Exception as dl_err:
print(f"[AI] Failed to download {name} globally: {dl_err}")
model_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "models")
os.makedirs(model_dir, exist_ok=True)
model_path = os.path.join(model_dir, "face_landmarker.task")
if not os.path.exists(model_path) or os.path.getsize(model_path) < 1000000:
print("[AI] Downloading face_landmarker.task globally...")
try:
model_url = "https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/1/face_landmarker.task"
urllib.request.urlretrieve(model_url, model_path)
print("[AI] face_landmarker.task download complete!")
except Exception as dl_err:
print(f"[AI] Failed to download face_landmarker.task globally: {dl_err}")
except Exception as e:
print(f"[AI] Global asset download manager failed: {e}")
_download_assets_globally()
# Warm up the neural detector at import time (download weights once)
get_lineart_detector()
@dataclass
class LineartConfig:
# Backward compatibility fields
prompt: str = ""
negative_prompt: str = ""
stable_diffusion_model_id: str = ""
controlnet_model_id: str = ""
num_inference_steps: int = 10
controlnet_conditioning_scale: float = 0.9
face_margin_ratio: float = 0.035
mediapipe_model_path: str = ""
rembg_model: str = ""
transparency_threshold: float = 0.03
detect_eyebrows: bool = True
# Subject mask
white_bg_threshold: int = 245
mask_close_iterations: int = 4
mask_open_iterations: int = 1
mask_smooth_ksize: int = 5
mask_smooth_threshold: int = 128
min_silhouette_area: float = 200.0
# Silhouette outline
silhouette_line_thickness: int = 3
# --- Neural line-art parameters ---
lineart_detect_resolution: int = 1024
lineart_image_resolution: int = 1024
lineart_coarse: bool = True
# AUMENTADO: Força bruta para ignorar detalhes fracos
lineart_threshold: int = 120
# AUMENTADO: Faca implacável para traços curtos
lineart_min_perimeter: float = 90.0
interior_erode_size: int = 1
interior_line_thickness: int = 2
# ---- Canny fallback parametros ----
canny_low: int = 35
canny_high: int = 75
bilateral_d: int = 11
bilateral_sigma_color: int = 110
bilateral_sigma_space: int = 110
gaussian_ksize: int = 11
min_contour_perimeter: float = 15.0
min_embroidery_stitch_px: int = 25
enable_detail_pass: bool = False
detail_canny_low: int = 15
detail_canny_high: int = 55
detail_gaussian_ksize: int = 5
detail_min_perimeter: float = 30.0
detail_bilateral_d: int = 7
detail_bilateral_sigma: int = 60
# Face detection
face_scale_factor: float = 1.05
face_min_neighbors: int = 3
face_min_size: Tuple[int, int] = (20, 20)
face_max_y_ratio: float = 0.65
face_nms_iou_thresh: float = 0.35
face_max_count: int = 6
# Face erase ellipse
face_cx_ratio: float = 0.50
face_cy_ratio: float = 0.55
face_rx_ratio: float = 0.35
face_ry_ratio: float = 0.40
face_erase_blur: int = 5
face_erase_threshold: int = 90
# MediaPipe
mediapipe_num_faces: int = 6
# --- AJUSTE DE SOBRANCELHA ---
eyebrow_thickness: int = 2
eyebrow_dilate: int = 0
# Face shape
draw_face_oval: bool = True
face_oval_thickness: int = 1
# AJUSTE FINO: Remove 45% do topo do oval (o que causava aquele erro bizarro)
face_oval_skip_top_ratio: float = 0.45
def _nms_faces(faces: list, iou_thresh: float) -> list:
if not faces:
return []
boxes = np.array(faces, dtype=float)
x1, y1 = boxes[:, 0], boxes[:, 1]
x2, y2 = boxes[:, 0] + boxes[:, 2], boxes[:, 1] + boxes[:, 3]
areas = (x2 - x1) * (y2 - y1)
order = areas.argsort()[::-1]
keep = []
while len(order) > 0:
i = order[0]
keep.append(i)
xx1 = np.maximum(x1[i], x1[order[1:]])
yy1 = np.maximum(y1[i], y1[order[1:]])
xx2 = np.minimum(x2[i], x2[order[1:]])
yy2 = np.minimum(y2[i], y2[order[1:]])
inter = np.maximum(0, xx2 - xx1) * np.maximum(0, yy2 - yy1)
iou = inter / (areas[i] + areas[order[1:]] - inter)
order = order[1:][iou < iou_thresh]
return [faces[i] for i in keep]
def detect_faces(rgb: np.ndarray, cfg: LineartConfig) -> list:
h = rgb.shape[0]
gray = cv2.cvtColor(rgb, cv2.COLOR_RGB2GRAY)
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
gray_eq = clahe.apply(gray)
faces = []
try:
cascade_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "cascades")
path_default = os.path.join(cascade_dir, 'haarcascade_frontalface_default.xml')
path_alt = os.path.join(cascade_dir, 'haarcascade_frontalface_alt2.xml')
classifiers = []
if os.path.exists(path_default):
classifiers.append(cv2.CascadeClassifier(path_default))
if os.path.exists(path_alt):
classifiers.append(cv2.CascadeClassifier(path_alt))
if not classifiers or all(c.empty() for c in classifiers):
classifiers = [
cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml'),
cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_alt2.xml')
]
for cascade in classifiers:
if cascade and not cascade.empty():
for img_gray in [gray, gray_eq]:
detected = cascade.detectMultiScale(
img_gray,
scaleFactor=cfg.face_scale_factor,
minNeighbors=cfg.face_min_neighbors,
minSize=cfg.face_min_size
)
if len(detected) > 0:
faces.extend(detected.tolist())
except Exception as e:
print(f"[AI] Face detection failed or cascades unavailable: {e}")
faces = [f for f in faces if f[1] < h * cfg.face_max_y_ratio]
faces = _nms_faces(faces, cfg.face_nms_iou_thresh)
max_faces = getattr(cfg, 'face_max_count', 6)
if len(faces) > max_faces:
faces = sorted(faces, key=lambda f: f[2] * f[3], reverse=True)[:max_faces]
return faces
def build_subject_mask(rgb: np.ndarray, cfg: LineartConfig, alpha_channel: Optional[np.ndarray] = None) -> np.ndarray:
if alpha_channel is not None:
mask = (alpha_channel > 30).astype(np.uint8) * 255
else:
white_mask = np.all(rgb > cfg.white_bg_threshold, axis=2)
mask = (~white_mask).astype(np.uint8) * 255
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7, 7))
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations=cfg.mask_close_iterations)
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel, iterations=cfg.mask_open_iterations)
return mask
def _smooth_contour_spline(cnt: np.ndarray, n_points: int = 400) -> Optional[np.ndarray]:
if not _SCIPY_AVAILABLE:
return None
pts = cnt[:, 0, :]
if len(pts) < 4:
return None
try:
tck, _ = splprep([pts[:, 0].astype(float), pts[:, 1].astype(float)],
s=len(pts) * 0.5, per=True, quiet=True)
u_new = np.linspace(0, 1, n_points)
x_new, y_new = splev(u_new, tck)
spline_pts = np.stack([x_new, y_new], axis=1).astype(np.int32)
return spline_pts.reshape((-1, 1, 2))
except Exception:
return None
def build_silhouette(subject_mask: np.ndarray, cfg: LineartConfig) -> np.ndarray:
mask = subject_mask.copy()
if cfg.mask_smooth_ksize > 1:
k = max(21, cfg.mask_smooth_ksize if cfg.mask_smooth_ksize % 2 == 1 else cfg.mask_smooth_ksize + 1)
mask = cv2.GaussianBlur(mask, (k, k), 0)
_, mask = cv2.threshold(mask, cfg.mask_smooth_threshold, 255, cv2.THRESH_BINARY)
k_close = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (9, 9))
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, k_close, iterations=2)
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
silhouette = np.zeros_like(subject_mask)
for cnt in contours:
if cv2.contourArea(cnt) < cfg.min_silhouette_area:
continue
smooth = _smooth_contour_spline(cnt)
if smooth is not None:
cv2.polylines(silhouette, [smooth], isClosed=True,
color=255, thickness=cfg.silhouette_line_thickness,
lineType=cv2.LINE_AA)
else:
eps = 0.0003 * cv2.arcLength(cnt, True)
approx = cv2.approxPolyDP(cnt, eps, True)
cv2.drawContours(silhouette, [approx], -1, 255,
cfg.silhouette_line_thickness, lineType=cv2.LINE_AA)
return silhouette
def build_face_erase_mask(faces: list, shape: Tuple[int, int], cfg: LineartConfig) -> np.ndarray:
h, w = shape
mask = np.zeros((h, w), dtype=np.uint8)
for (fx, fy, fw, fh) in faces:
cx = int(fx + fw * cfg.face_cx_ratio)
cy = int(fy + fh * cfg.face_cy_ratio)
rx = int(fw * cfg.face_rx_ratio)
ry = int(fh * cfg.face_ry_ratio)
cv2.ellipse(mask, (cx, cy), (rx, ry), 0, 0, 360, 255, -1)
soft = np.array(Image.fromarray(mask).filter(ImageFilter.GaussianBlur(cfg.face_erase_blur)))
return (soft > cfg.face_erase_threshold).astype(np.uint8) * 255
def _line_strength_from_detector(pil_lineart: Image.Image, target_hw: Tuple[int, int]) -> np.ndarray:
h, w = target_hw
gray = np.array(pil_lineart.convert("L"))
corners = [gray[0, 0], gray[0, -1], gray[-1, 0], gray[-1, -1]]
bg = float(np.median(corners))
if bg > 127:
strength = 255 - gray
else:
strength = gray
if strength.shape[:2] != (h, w):
strength = cv2.resize(strength, (w, h), interpolation=cv2.INTER_LINEAR)
return strength.astype(np.uint8)
def build_interior_edges(rgb: np.ndarray, subject_mask: np.ndarray, cfg: LineartConfig) -> np.ndarray:
h, w = rgb.shape[:2]
detector = get_lineart_detector()
if detector is not None:
try:
pil_in = Image.fromarray(rgb)
with _LINEART_LOCK:
pil_out = detector(
pil_in,
detect_resolution=cfg.lineart_detect_resolution,
image_resolution=cfg.lineart_image_resolution,
coarse=cfg.lineart_coarse,
)
strength = _line_strength_from_detector(pil_out, (h, w))
_, edges = cv2.threshold(strength, cfg.lineart_threshold, 255, cv2.THRESH_BINARY)
erode_size = getattr(cfg, 'interior_erode_size', 3)
erode_size = max(1, erode_size)
if erode_size % 2 == 0:
erode_size += 1
kernel_erode = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (erode_size, erode_size))
eroded_mask = cv2.erode(subject_mask, kernel_erode, iterations=1)
edges = cv2.bitwise_and(edges, eroded_mask)
# Aspirador de Sujeiras
kernel_clean = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
edges = cv2.morphologyEx(edges, cv2.MORPH_OPEN, kernel_clean, iterations=1)
contours, _ = cv2.findContours(edges, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
filtered = np.zeros_like(edges)
for cnt in contours:
perimeter = cv2.arcLength(cnt, False)
area = cv2.contourArea(cnt)
# BARREIRA EXTREMA: Elimina pontos isolados e traços curtos
if perimeter < cfg.lineart_min_perimeter or area < 40.0:
continue
cv2.drawContours(filtered, [cnt], -1, 255, cfg.interior_line_thickness)
kernel_dilate = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
filtered = cv2.dilate(filtered, kernel_dilate, iterations=1)
filtered = cv2.GaussianBlur(filtered, (3, 3), 0)
_, filtered = cv2.threshold(filtered, 127, 255, cv2.THRESH_BINARY)
return filtered
except Exception as e:
print(f"[AI] Neural lineart failed, falling back to Canny: {e}")
return _build_interior_edges_canny(rgb, subject_mask, cfg)
def _build_interior_edges_canny(rgb: np.ndarray, subject_mask: np.ndarray, cfg: LineartConfig) -> np.ndarray:
gray = cv2.cvtColor(rgb, cv2.COLOR_RGB2GRAY)
blurred = cv2.bilateralFilter(gray, cfg.bilateral_d, cfg.bilateral_sigma_color, cfg.bilateral_sigma_space)
blurred = cv2.GaussianBlur(blurred, (cfg.gaussian_ksize, cfg.gaussian_ksize), 0)
edges = cv2.Canny(blurred, cfg.canny_low, cfg.canny_high)
erode_size = max(5, cfg.silhouette_line_thickness * 2 + 3)
if erode_size % 2 == 0:
erode_size += 1
kernel_erode = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (erode_size, erode_size))
eroded_mask = cv2.erode(subject_mask, kernel_erode, iterations=1)
edges = cv2.bitwise_and(edges, eroded_mask)
contours, _ = cv2.findContours(edges, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
filtered = np.zeros_like(edges)
for cnt in contours:
perimeter = cv2.arcLength(cnt, False)
if perimeter < cfg.min_contour_perimeter:
continue
eps = 0.0012 * perimeter
smooth = cv2.approxPolyDP(cnt, eps, False)
cv2.drawContours(filtered, [smooth], -1, 255, cfg.interior_line_thickness)
kernel_dilate = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
filtered = cv2.dilate(filtered, kernel_dilate, iterations=1)
return filtered
_LANDMARKER_LOCK = threading.Lock()
_LANDMARKER_CACHE = None
_LANDMARKER_NUM_FACES = None
def get_landmarker(model_path, num_faces: int = 6):
global _LANDMARKER_CACHE, _LANDMARKER_NUM_FACES
if _LANDMARKER_CACHE is None or _LANDMARKER_NUM_FACES != num_faces:
import mediapipe as mp
from mediapipe.tasks import python
from mediapipe.tasks.python import vision
base_options = python.BaseOptions(model_asset_path=model_path)
options = vision.FaceLandmarkerOptions(
base_options=base_options,
output_face_blendshapes=False,
output_facial_transformation_matrixes=False,
num_faces=num_faces,
min_face_detection_confidence=0.2,
min_face_presence_confidence=0.2,
min_tracking_confidence=0.2,
)
_LANDMARKER_CACHE = vision.FaceLandmarker.create_from_options(options)
_LANDMARKER_NUM_FACES = num_faces
return _LANDMARKER_CACHE
def draw_eyebrows(rgb: np.ndarray, cfg: LineartConfig, faces: Optional[list] = None) -> np.ndarray:
h, w, _ = rgb.shape
eyebrows_layer = np.zeros((h, w), dtype=np.uint8)
try:
import mediapipe as mp
model_path = cfg.mediapipe_model_path
if not model_path or not os.path.exists(model_path):
model_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "models")
os.makedirs(model_dir, exist_ok=True)
model_path = os.path.join(model_dir, "face_landmarker.task")
if not os.path.exists(model_path) or os.path.getsize(model_path) < 1000000:
print("[AI] Downloading face_landmarker.task for eyebrows...")
model_url = "https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/1/face_landmarker.task"
urllib.request.urlretrieve(model_url, model_path)
LEFT_EYEBROW = [70, 63, 105, 66, 107, 55, 65, 52, 53, 46]
RIGHT_EYEBROW = [336, 296, 334, 293, 300, 285, 295, 282, 283, 276]
FACE_OVAL = [10, 338, 297, 332, 284, 251, 389, 356, 454, 323, 361, 288,
397, 365, 379, 378, 400, 377, 152, 148, 176, 149, 150, 136,
172, 58, 132, 93, 234, 127, 162, 21, 54, 103, 67, 109]
eyebrow_thickness = getattr(cfg, 'eyebrow_thickness', cfg.interior_line_thickness)
draw_oval = True
oval_thickness = getattr(cfg, 'face_oval_thickness', 2)
oval_skip_top = getattr(cfg, 'face_oval_skip_top_ratio', 0.18)
drawn_centers_px: list = []
def is_duplicate_px(cx_px: float, cy_px: float) -> bool:
min_dist = min(w, h) * 0.06
for (pcx, pcy) in drawn_centers_px:
if np.hypot(cx_px - pcx, cy_px - pcy) < min_dist:
return True
return False
def draw_eyebrow_landmarks(face_lms, origin_x, origin_y, scale_x, scale_y):
for indices in [LEFT_EYEBROW, RIGHT_EYEBROW]:
pts = []
for idx in indices:
lm = face_lms[idx]
px = int(origin_x + lm.x * scale_x)
py = int(origin_y + lm.y * scale_y)
pts.append([px, py])
pts_np = np.array(pts, np.int32).reshape((-1, 1, 2))
cv2.polylines(eyebrows_layer, [pts_np], isClosed=False, color=255,
thickness=eyebrow_thickness, lineType=cv2.LINE_AA)
if draw_oval:
oval_pts = []
for idx in FACE_OVAL:
lm = face_lms[idx]
px = int(origin_x + lm.x * scale_x)
py = int(origin_y + lm.y * scale_y)
oval_pts.append([px, py])
oval_np = np.array(oval_pts, np.int32)
if len(oval_np) >= 4:
if oval_skip_top > 0:
y_min = oval_np[:, 1].min()
y_max = oval_np[:, 1].max()
cutoff = y_min + (y_max - y_min) * oval_skip_top
kept = oval_np[oval_np[:, 1] >= cutoff]
if len(kept) >= 4:
arc = kept.reshape((-1, 1, 2))
cv2.polylines(eyebrows_layer, [arc], isClosed=False,
color=255, thickness=oval_thickness,
lineType=cv2.LINE_AA)
else:
cv2.polylines(eyebrows_layer, [oval_np.reshape((-1, 1, 2))],
isClosed=True, color=255,
thickness=oval_thickness, lineType=cv2.LINE_AA)
if faces is None:
faces = detect_faces(rgb, cfg)
with _LANDMARKER_LOCK:
num_faces = getattr(cfg, 'mediapipe_num_faces', 6)
landmarker = get_landmarker(model_path, num_faces=num_faces)
mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=rgb)
global_result = landmarker.detect(mp_image)
if global_result.face_landmarks:
for face_lms in global_result.face_landmarks:
cx_px = np.mean([lm.x for lm in face_lms]) * w
cy_px = np.mean([lm.y for lm in face_lms]) * h
if not is_duplicate_px(cx_px, cy_px):
drawn_centers_px.append((cx_px, cy_px))
draw_eyebrow_landmarks(face_lms, 0, 0, w, h)
for (fx, fy, fw, fh) in faces:
mx = int(fw * 0.50); my = int(fh * 0.50)
x1 = max(0, fx - mx); y1 = max(0, fy - my)
x2 = min(w, fx + fw + mx); y2 = min(h, fy + fh + my)
crop_w, crop_h = x2 - x1, y2 - y1
if crop_w < 20 or crop_h < 20:
continue
face_cx_px = (x1 + x2) / 2; face_cy_px = (y1 + y2) / 2
if is_duplicate_px(face_cx_px, face_cy_px):
continue
crop = rgb[y1:y2, x1:x2]
target = max(512, crop_w, crop_h)
scale = target / max(crop_w, crop_h)
cw_scaled = int(crop_w * scale); ch_scaled = int(crop_h * scale)
crop_resized = cv2.resize(crop, (cw_scaled, ch_scaled), interpolation=cv2.INTER_CUBIC)
crop_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=crop_resized)
crop_result = landmarker.detect(crop_image)
if crop_result.face_landmarks:
face_lms = crop_result.face_landmarks[0]
cx_px2 = x1 + np.mean([lm.x for lm in face_lms]) * crop_w
cy_px2 = y1 + np.mean([lm.y for lm in face_lms]) * crop_h
if is_duplicate_px(cx_px2, cy_px2):
continue
drawn_centers_px.append((cx_px2, cy_px2))
draw_eyebrow_landmarks(face_lms, x1, y1, crop_w, crop_h)
print(f"[eyebrows] Drew eyebrows for {len(drawn_centers_px)} face(s)")
except Exception as e:
print(f"[AI] Eyebrow extraction error or library missing: {e}")
if eyebrows_layer.any():
dil = getattr(cfg, 'eyebrow_dilate', 1)
if dil > 0:
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
eyebrows_layer = cv2.dilate(eyebrows_layer, kernel, iterations=dil)
return eyebrows_layer
def make_faceless_lineart(img: Image.Image, cfg: LineartConfig = None) -> Image.Image:
cfg = cfg or LineartConfig()
alpha_channel: Optional[np.ndarray] = None
if img.mode in ("RGBA", "LA") or (img.mode == "P" and "transparency" in img.info):
rgba_img = img.convert("RGBA")
alpha_np = np.array(rgba_img)[:, :, 3]
if np.any(alpha_np < 200):
alpha_channel = alpha_np
background = Image.new("RGBA", img.size, (255, 255, 255, 255))
rgba = Image.alpha_composite(background, rgba_img)
img = rgba.convert("RGB")
else:
img = img.convert("RGB")
img_np = np.array(img)
rgb = img_np[:, :, :3]
h, w = rgb.shape[:2]
if alpha_channel is None and _SESSION_ISNET is not None:
try:
cut = remove(Image.fromarray(rgb).convert("RGBA"), session=_SESSION_ISNET, only_mask=True)
cut_l = cut.convert("L") if isinstance(cut, Image.Image) else Image.open(io.BytesIO(cut)).convert("L")
if cut_l.size != (w, h):
cut_l = cut_l.resize((w, h), Image.LANCZOS)
alpha_channel = np.array(cut_l)
except Exception as e:
print(f"[AI] rembg mask for lineart failed: {e}")
subject_mask = build_subject_mask(rgb, cfg, alpha_channel)
faces = detect_faces(rgb, cfg)
print(f"[lineart] Detected {len(faces)} face(s)")
silhouette = build_silhouette(subject_mask, cfg)
interior = build_interior_edges(rgb, subject_mask, cfg)
face_erase = build_face_erase_mask(faces, (h, w), cfg)
interior_clean = cv2.bitwise_and(interior, cv2.bitwise_not(face_erase))
final_lines = cv2.add(silhouette, interior_clean)
if cfg.detect_eyebrows:
eyebrows = draw_eyebrows(rgb, cfg, faces=faces)
final_lines = cv2.add(final_lines, eyebrows)
border_erase_width = max(cfg.silhouette_line_thickness, cfg.interior_line_thickness) + 2
if border_erase_width > 0:
final_lines[0:border_erase_width, :] = 0
final_lines[-border_erase_width:, :] = 0
final_lines[:, 0:border_erase_width] = 0
final_lines[:, -border_erase_width:] = 0
alpha = Image.fromarray(final_lines)
transparent = Image.new("RGBA", img.size, (0, 0, 0, 0))
transparent.putalpha(alpha)
return transparent
def decode_base64_image(image_data: str) -> Image.Image:
payload = image_data.split(",", 1)[1] if "," in image_data else image_data
return Image.open(io.BytesIO(base64.b64decode(payload))).convert("RGBA")
def encode_png_data_url(image: Image.Image) -> str:
buf = io.BytesIO()
image.save(buf, format="PNG")
return f"data:image/png;base64,{base64.b64encode(buf.getvalue()).decode()}"
def _has_real_transparency(img: Image.Image) -> bool:
rgba = img.convert("RGBA")
alpha = rgba.getchannel("A")
sample = list(alpha.getdata())
total = 0
visible = 0
for index in range(0, len(sample), 12):
total += 1
if sample[index] < 245:
visible += 1
return total > 0 and (visible / total) >= 0.03
def remove_background_subject(img: Image.Image) -> Image.Image:
try:
rgba = img.convert("RGBA")
if _has_real_transparency(rgba):
return rgba
if _SESSION_ISNET is None:
return rgba
mask = remove(rgba, session=_SESSION_ISNET, only_mask=True)
alpha = mask.convert("L") if isinstance(mask, Image.Image) else Image.open(io.BytesIO(mask)).convert("L")
if alpha.size != rgba.size:
alpha = alpha.resize(rgba.size, Image.LANCZOS)
alpha = alpha.filter(ImageFilter.GaussianBlur(radius=0.8))
r, g, b, _a = rgba.split()
return Image.merge("RGBA", (r, g, b, alpha))
except Exception as exc:
print(f"[AI] Background removal failed: {exc}")
return img.convert("RGBA")
def make_faceless_cutout(img: Image.Image) -> Image.Image:
return remove_background_subject(img)
def build_faceless_embroidery_assets(img: Image.Image, cfg: Optional[LineartConfig] = None, **kwargs) -> Tuple[Image.Image, Image.Image]:
if cfg is None:
cfg = LineartConfig()
if "prompt" in kwargs and kwargs["prompt"] is not None:
cfg.prompt = kwargs["prompt"]
if "negative_prompt" in kwargs and kwargs["negative_prompt"] is not None:
cfg.negative_prompt = kwargs["negative_prompt"]
lineart = make_faceless_lineart(img, cfg)
cutout = make_faceless_cutout(img)
return lineart, cutout