vitordigitizing commited on
Commit
9b1e7ab
·
verified ·
1 Parent(s): e34bf93

Update faceless_processing.py

Browse files
Files changed (1) hide show
  1. 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 session: {e}")
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 = 245 # pixels above this = background (raised to avoid erasing light subjects)
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) — smaller = more conservative
84
  face_cx_ratio: float = 0.5
85
- face_cy_ratio: float = 0.45
86
- face_rx_ratio: float = 0.35 # reduced from 0.40 to avoid erasing neck/ears
87
- face_ry_ratio: float = 0.38 # reduced from 0.42 to avoid erasing chin/neck
88
- face_erase_blur: int = 8
89
- face_erase_threshold: int = 130 # higher = sharper face mask edge, less spill
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, alpha_channel: Optional[np.ndarray] = None) -> np.ndarray:
162
- """Build subject mask. Uses alpha channel if available, otherwise thresholds white background."""
163
- if alpha_channel is not None:
164
- # Use rembg/RGBA alpha directly — much more accurate than color threshold
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 smooth, solid-stroke contour."""
177
- mask = subject_mask.copy()
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
- for cnt in contours:
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
- # Preserve alpha channel if image already has a transparent background (e.g. from rembg)
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, rgba_img)
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 — use alpha if available, else color threshold
259
- subject_mask = build_subject_mask(rgb, cfg, alpha_channel)
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