Spaces:
Sleeping
Sleeping
Update faceless_processing.py
Browse files- faceless_processing.py +27 -45
faceless_processing.py
CHANGED
|
@@ -12,7 +12,7 @@ from rembg import remove, new_session
|
|
| 12 |
try:
|
| 13 |
_SESSION_ISNET = new_session("isnet-general-use")
|
| 14 |
except Exception as e:
|
| 15 |
-
print(f"[AI] Failed to load rembg
|
| 16 |
_SESSION_ISNET = None
|
| 17 |
|
| 18 |
# Globally download cascades at startup if missing (runs on main container with internet access)
|
|
@@ -53,12 +53,9 @@ class LineartConfig:
|
|
| 53 |
transparency_threshold: float = 0.03
|
| 54 |
|
| 55 |
# Subject mask
|
| 56 |
-
white_bg_threshold: int =
|
| 57 |
mask_close_iterations: int = 4
|
| 58 |
mask_open_iterations: int = 1
|
| 59 |
-
mask_smooth_ksize: int = 5 # GaussianBlur kernel before silhouette (must be odd; 0 = disabled)
|
| 60 |
-
mask_smooth_threshold: int = 128 # re-binarize after blur
|
| 61 |
-
min_silhouette_area: float = 200.0 # drop tiny isolated blobs from the silhouette (px²)
|
| 62 |
|
| 63 |
# Silhouette outline
|
| 64 |
silhouette_line_thickness: int = 4 # outer contour stroke width (px); match interior for uniform look
|
|
@@ -80,13 +77,13 @@ class LineartConfig:
|
|
| 80 |
face_max_y_ratio: float = 0.6 # ignore faces below 60% image height
|
| 81 |
face_nms_iou_thresh: float = 0.4
|
| 82 |
|
| 83 |
-
# Face erase ellipse (ratios relative to face bbox)
|
| 84 |
face_cx_ratio: float = 0.5
|
| 85 |
-
face_cy_ratio: float = 0.
|
| 86 |
-
face_rx_ratio: float = 0.
|
| 87 |
-
face_ry_ratio: float = 0.
|
| 88 |
-
face_erase_blur: int =
|
| 89 |
-
face_erase_threshold: int =
|
| 90 |
|
| 91 |
|
| 92 |
def _nms_faces(faces: list, iou_thresh: float) -> list:
|
|
@@ -158,14 +155,10 @@ def detect_faces(rgb: np.ndarray, cfg: LineartConfig) -> list:
|
|
| 158 |
return faces
|
| 159 |
|
| 160 |
|
| 161 |
-
def build_subject_mask(rgb: np.ndarray, cfg: LineartConfig
|
| 162 |
-
"""
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
mask = (alpha_channel > 30).astype(np.uint8) * 255
|
| 166 |
-
else:
|
| 167 |
-
white_mask = np.all(rgb > cfg.white_bg_threshold, axis=2)
|
| 168 |
-
mask = (~white_mask).astype(np.uint8) * 255
|
| 169 |
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7, 7))
|
| 170 |
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations=cfg.mask_close_iterations)
|
| 171 |
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel, iterations=cfg.mask_open_iterations)
|
|
@@ -173,23 +166,10 @@ def build_subject_mask(rgb: np.ndarray, cfg: LineartConfig, alpha_channel: Optio
|
|
| 173 |
|
| 174 |
|
| 175 |
def build_silhouette(subject_mask: np.ndarray, cfg: LineartConfig) -> np.ndarray:
|
| 176 |
-
"""Draw the subject silhouette as a
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
# Smooth the mask boundary to eliminate staircase jaggies before tracing
|
| 180 |
-
if cfg.mask_smooth_ksize > 1:
|
| 181 |
-
k = cfg.mask_smooth_ksize if cfg.mask_smooth_ksize % 2 == 1 else cfg.mask_smooth_ksize + 1
|
| 182 |
-
mask = cv2.GaussianBlur(mask, (k, k), 0)
|
| 183 |
-
_, mask = cv2.threshold(mask, cfg.mask_smooth_threshold, 255, cv2.THRESH_BINARY)
|
| 184 |
-
|
| 185 |
-
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_TC89_KCOS)
|
| 186 |
silhouette = np.zeros_like(subject_mask)
|
| 187 |
-
|
| 188 |
-
if cv2.contourArea(cnt) >= cfg.min_silhouette_area:
|
| 189 |
-
# Smooth the polygon points slightly to reduce digitisation noise
|
| 190 |
-
eps = 0.0015 * cv2.arcLength(cnt, True)
|
| 191 |
-
smooth = cv2.approxPolyDP(cnt, eps, True)
|
| 192 |
-
cv2.drawContours(silhouette, [smooth], -1, 255, cfg.silhouette_line_thickness)
|
| 193 |
return silhouette
|
| 194 |
|
| 195 |
|
|
@@ -237,16 +217,10 @@ def make_faceless_lineart(
|
|
| 237 |
"""
|
| 238 |
cfg = cfg or LineartConfig()
|
| 239 |
|
| 240 |
-
#
|
| 241 |
-
alpha_channel: Optional[np.ndarray] = None
|
| 242 |
if img.mode in ("RGBA", "LA") or (img.mode == "P" and "transparency" in img.info):
|
| 243 |
-
rgba_img = img.convert("RGBA")
|
| 244 |
-
alpha_np = np.array(rgba_img)[:, :, 3]
|
| 245 |
-
# Only use alpha mask if it contains meaningful transparency
|
| 246 |
-
if np.any(alpha_np < 200):
|
| 247 |
-
alpha_channel = alpha_np
|
| 248 |
background = Image.new("RGBA", img.size, (255, 255, 255, 255))
|
| 249 |
-
rgba = Image.alpha_composite(background,
|
| 250 |
img = rgba.convert("RGB")
|
| 251 |
else:
|
| 252 |
img = img.convert("RGB")
|
|
@@ -255,8 +229,8 @@ def make_faceless_lineart(
|
|
| 255 |
rgb = img_np[:, :, :3]
|
| 256 |
h, w = rgb.shape[:2]
|
| 257 |
|
| 258 |
-
# 1. Subject mask
|
| 259 |
-
subject_mask = build_subject_mask(rgb, cfg
|
| 260 |
|
| 261 |
# 2. Detect faces
|
| 262 |
faces = detect_faces(rgb, cfg)
|
|
@@ -275,6 +249,14 @@ def make_faceless_lineart(
|
|
| 275 |
# 6. Combine
|
| 276 |
final_lines = cv2.add(silhouette, interior_clean)
|
| 277 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 278 |
# 7. Build output image
|
| 279 |
alpha = Image.fromarray(final_lines)
|
| 280 |
size = img.size
|
|
|
|
| 12 |
try:
|
| 13 |
_SESSION_ISNET = new_session("isnet-general-use")
|
| 14 |
except Exception as e:
|
| 15 |
+
print(f"[AI] Failed to load rembg session1 {e}")
|
| 16 |
_SESSION_ISNET = None
|
| 17 |
|
| 18 |
# Globally download cascades at startup if missing (runs on main container with internet access)
|
|
|
|
| 53 |
transparency_threshold: float = 0.03
|
| 54 |
|
| 55 |
# Subject mask
|
| 56 |
+
white_bg_threshold: int = 235 # pixels above this = background
|
| 57 |
mask_close_iterations: int = 4
|
| 58 |
mask_open_iterations: int = 1
|
|
|
|
|
|
|
|
|
|
| 59 |
|
| 60 |
# Silhouette outline
|
| 61 |
silhouette_line_thickness: int = 4 # outer contour stroke width (px); match interior for uniform look
|
|
|
|
| 77 |
face_max_y_ratio: float = 0.6 # ignore faces below 60% image height
|
| 78 |
face_nms_iou_thresh: float = 0.4
|
| 79 |
|
| 80 |
+
# Face erase ellipse (ratios relative to face bbox)
|
| 81 |
face_cx_ratio: float = 0.5
|
| 82 |
+
face_cy_ratio: float = 0.48
|
| 83 |
+
face_rx_ratio: float = 0.40
|
| 84 |
+
face_ry_ratio: float = 0.42
|
| 85 |
+
face_erase_blur: int = 10
|
| 86 |
+
face_erase_threshold: int = 100
|
| 87 |
|
| 88 |
|
| 89 |
def _nms_faces(faces: list, iou_thresh: float) -> list:
|
|
|
|
| 155 |
return faces
|
| 156 |
|
| 157 |
|
| 158 |
+
def build_subject_mask(rgb: np.ndarray, cfg: LineartConfig) -> np.ndarray:
|
| 159 |
+
"""Threshold out white background and clean up mask."""
|
| 160 |
+
white_mask = np.all(rgb > cfg.white_bg_threshold, axis=2)
|
| 161 |
+
mask = (~white_mask).astype(np.uint8) * 255
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7, 7))
|
| 163 |
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations=cfg.mask_close_iterations)
|
| 164 |
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel, iterations=cfg.mask_open_iterations)
|
|
|
|
| 166 |
|
| 167 |
|
| 168 |
def build_silhouette(subject_mask: np.ndarray, cfg: LineartConfig) -> np.ndarray:
|
| 169 |
+
"""Draw the subject silhouette as a uniform-width contour stroke."""
|
| 170 |
+
contours, _ = cv2.findContours(subject_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
silhouette = np.zeros_like(subject_mask)
|
| 172 |
+
cv2.drawContours(silhouette, contours, -1, 255, cfg.silhouette_line_thickness)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
return silhouette
|
| 174 |
|
| 175 |
|
|
|
|
| 217 |
"""
|
| 218 |
cfg = cfg or LineartConfig()
|
| 219 |
|
| 220 |
+
# Force transparent background to white background for processing mask correctly
|
|
|
|
| 221 |
if img.mode in ("RGBA", "LA") or (img.mode == "P" and "transparency" in img.info):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
background = Image.new("RGBA", img.size, (255, 255, 255, 255))
|
| 223 |
+
rgba = Image.alpha_composite(background, img.convert("RGBA"))
|
| 224 |
img = rgba.convert("RGB")
|
| 225 |
else:
|
| 226 |
img = img.convert("RGB")
|
|
|
|
| 229 |
rgb = img_np[:, :, :3]
|
| 230 |
h, w = rgb.shape[:2]
|
| 231 |
|
| 232 |
+
# 1. Subject mask
|
| 233 |
+
subject_mask = build_subject_mask(rgb, cfg)
|
| 234 |
|
| 235 |
# 2. Detect faces
|
| 236 |
faces = detect_faces(rgb, cfg)
|
|
|
|
| 249 |
# 6. Combine
|
| 250 |
final_lines = cv2.add(silhouette, interior_clean)
|
| 251 |
|
| 252 |
+
# Avoid drawing rectangular boxes/borders at the image edges
|
| 253 |
+
border_erase_width = max(cfg.silhouette_line_thickness, cfg.interior_line_thickness) + 2
|
| 254 |
+
if border_erase_width > 0:
|
| 255 |
+
final_lines[0:border_erase_width, :] = 0
|
| 256 |
+
final_lines[-border_erase_width:, :] = 0
|
| 257 |
+
final_lines[:, 0:border_erase_width] = 0
|
| 258 |
+
final_lines[:, -border_erase_width:] = 0
|
| 259 |
+
|
| 260 |
# 7. Build output image
|
| 261 |
alpha = Image.fromarray(final_lines)
|
| 262 |
size = img.size
|