vitor.fran commited on
Commit
175eeb9
·
1 Parent(s): 9e39a20

fix: smooth lineart, detect eyebrows on tilted faces, thinner eyebrows

Browse files
Files changed (2) hide show
  1. app.py +6 -12
  2. faceless_processing.py +83 -28
app.py CHANGED
@@ -33,18 +33,11 @@ def process_faceless(input_image, prompt="", negative_prompt=""):
33
  else:
34
  return lineart_path, None
35
  except Exception as e:
36
- import traceback
37
- err_str = "".join(traceback.format_exception(type(e), e, e.__traceback__))
38
- print(f"[ERRO] process_faceless: {err_str}")
39
-
40
- from PIL import ImageDraw
41
- err_img = Image.new("RGBA", (1024, 1024), (40, 0, 0, 255))
42
- draw = ImageDraw.Draw(err_img)
43
- draw.text((10, 10), err_str, fill=(255, 255, 255, 255))
44
-
45
- err_path = os.path.join(tempfile.gettempdir(), f"err_{os.getpid()}_{os.urandom(4).hex()}.png")
46
- err_img.save(err_path, "PNG")
47
- return err_path, None
48
 
49
 
50
  # Interface Gradio
@@ -63,5 +56,6 @@ demo = gr.Interface(
63
  description="Gera contornos e linearts minimalistas (Faceless) para bordados a partir de retratos.\n\n**Nota:** O rosto é completamente apagado e apenas a silhueta externa é desenhada."
64
  )
65
 
 
66
  if __name__ == "__main__":
67
  demo.launch()
 
33
  else:
34
  return lineart_path, None
35
  except Exception as e:
36
+ print(f"[ERRO] process_faceless: {e}")
37
+ # Save original to temp file to be safe
38
+ orig_path = os.path.join(tempfile.gettempdir(), f"orig_{os.getpid()}_{os.urandom(4).hex()}.png")
39
+ input_image.save(orig_path, "PNG")
40
+ return orig_path, None
 
 
 
 
 
 
 
41
 
42
 
43
  # Interface Gradio
 
56
  description="Gera contornos e linearts minimalistas (Faceless) para bordados a partir de retratos.\n\n**Nota:** O rosto é completamente apagado e apenas a silhueta externa é desenhada."
57
  )
58
 
59
+
60
  if __name__ == "__main__":
61
  demo.launch()
faceless_processing.py CHANGED
@@ -77,7 +77,7 @@ class LineartConfig:
77
  min_silhouette_area: float = 200.0 # drop tiny isolated blobs from the silhouette (px²)
78
 
79
  # Silhouette outline
80
- silhouette_line_thickness: int = 4 # outer contour stroke width (px); match interior for uniform look
81
 
82
  # Edge detection
83
  canny_low: int = 40
@@ -203,7 +203,7 @@ def build_silhouette(subject_mask: np.ndarray, cfg: LineartConfig) -> np.ndarray
203
  for cnt in contours:
204
  if cv2.contourArea(cnt) >= cfg.min_silhouette_area:
205
  # Smooth the polygon points slightly to reduce digitisation noise
206
- eps = 0.0015 * cv2.arcLength(cnt, True)
207
  smooth = cv2.approxPolyDP(cnt, eps, True)
208
  cv2.drawContours(silhouette, [smooth], -1, 255, cfg.silhouette_line_thickness)
209
  return silhouette
@@ -235,8 +235,12 @@ def build_interior_edges(rgb: np.ndarray, subject_mask: np.ndarray, cfg: Lineart
235
  contours, _ = cv2.findContours(edges, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
236
  filtered = np.zeros_like(edges)
237
  for cnt in contours:
238
- if cv2.arcLength(cnt, False) > cfg.min_contour_perimeter:
239
- cv2.drawContours(filtered, [cnt], -1, 255, cfg.interior_line_thickness)
 
 
 
 
240
 
241
  # Mild dilation for uniform, solid strokes
242
  kernel_dilate = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
@@ -244,8 +248,8 @@ def build_interior_edges(rgb: np.ndarray, subject_mask: np.ndarray, cfg: Lineart
244
  return filtered
245
 
246
 
247
- def draw_eyebrows(rgb: np.ndarray, cfg: LineartConfig) -> np.ndarray:
248
- """Detect and draw eyebrows landmarks using MediaPipe Task API."""
249
  h, w, _ = rgb.shape
250
  eyebrows_layer = np.zeros((h, w), dtype=np.uint8)
251
 
@@ -266,38 +270,89 @@ def draw_eyebrows(rgb: np.ndarray, cfg: LineartConfig) -> np.ndarray:
266
  urllib.request.urlretrieve(model_url, model_path)
267
  print("[AI] Download complete!")
268
 
269
- # Create mediapipe image
270
- mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=rgb)
271
-
272
- # Initialize landmarker
273
  base_options = python.BaseOptions(model_asset_path=model_path)
274
  options = vision.FaceLandmarkerOptions(
275
  base_options=base_options,
276
  output_face_blendshapes=False,
277
  output_facial_transformation_matrixes=False,
278
- num_faces=5
279
  )
280
 
281
  LEFT_EYEBROW = [70, 63, 105, 66, 107, 55, 65, 52, 53, 46]
282
  RIGHT_EYEBROW = [336, 296, 334, 293, 300, 285, 295, 282, 283, 276]
 
 
 
 
 
 
 
 
 
 
 
 
 
283
 
284
  with vision.FaceLandmarker.create_from_options(options) as landmarker:
285
- result = landmarker.detect(mp_image)
286
- if result.face_landmarks:
287
- print(f"[eyebrows] Detected {len(result.face_landmarks)} faces with eyebrows")
288
- for face_landmarks in result.face_landmarks:
289
- for indices in [LEFT_EYEBROW, RIGHT_EYEBROW]:
290
- points = []
291
- for idx in indices:
292
- landmark = face_landmarks[idx]
293
- x = int(landmark.x * w)
294
- y = int(landmark.y * h)
295
- points.append([x, y])
296
-
297
- pts = np.array(points, np.int32)
298
- pts = pts.reshape((-1, 1, 2))
299
- # Draw standard eyebrow contours
300
- cv2.polylines(eyebrows_layer, [pts], isClosed=False, color=255, thickness=cfg.interior_line_thickness)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
301
  except Exception as e:
302
  print(f"[AI] Eyebrow extraction error or library missing: {e}")
303
 
@@ -354,7 +409,7 @@ def make_faceless_lineart(
354
 
355
  # 6.5. Optional eyebrow detection overlay
356
  if cfg.detect_eyebrows:
357
- eyebrows = draw_eyebrows(rgb, cfg)
358
  final_lines = cv2.add(final_lines, eyebrows)
359
 
360
  # Avoid drawing rectangular boxes/borders at the image edges
 
77
  min_silhouette_area: float = 200.0 # drop tiny isolated blobs from the silhouette (px²)
78
 
79
  # Silhouette outline
80
+ silhouette_line_thickness: int = 3 # outer contour stroke width (px); match interior for uniform look
81
 
82
  # Edge detection
83
  canny_low: int = 40
 
203
  for cnt in contours:
204
  if cv2.contourArea(cnt) >= cfg.min_silhouette_area:
205
  # Smooth the polygon points slightly to reduce digitisation noise
206
+ eps = 0.0045 * cv2.arcLength(cnt, True)
207
  smooth = cv2.approxPolyDP(cnt, eps, True)
208
  cv2.drawContours(silhouette, [smooth], -1, 255, cfg.silhouette_line_thickness)
209
  return silhouette
 
235
  contours, _ = cv2.findContours(edges, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
236
  filtered = np.zeros_like(edges)
237
  for cnt in contours:
238
+ perimeter = cv2.arcLength(cnt, False)
239
+ if perimeter > cfg.min_contour_perimeter:
240
+ # Simplify interior edges to 0.004 to remove Canny wobbles
241
+ eps = 0.004 * perimeter
242
+ smooth = cv2.approxPolyDP(cnt, eps, False)
243
+ cv2.drawContours(filtered, [smooth], -1, 255, cfg.interior_line_thickness)
244
 
245
  # Mild dilation for uniform, solid strokes
246
  kernel_dilate = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
 
248
  return filtered
249
 
250
 
251
+ def draw_eyebrows(rgb: np.ndarray, cfg: LineartConfig, faces: Optional[list] = None) -> np.ndarray:
252
+ """Detect and draw eyebrows landmarks using MediaPipe Task API with hybrid global + crop detection."""
253
  h, w, _ = rgb.shape
254
  eyebrows_layer = np.zeros((h, w), dtype=np.uint8)
255
 
 
270
  urllib.request.urlretrieve(model_url, model_path)
271
  print("[AI] Download complete!")
272
 
273
+ # Initialize landmarker with num_faces=1 for high sensitivity on local crops
 
 
 
274
  base_options = python.BaseOptions(model_asset_path=model_path)
275
  options = vision.FaceLandmarkerOptions(
276
  base_options=base_options,
277
  output_face_blendshapes=False,
278
  output_facial_transformation_matrixes=False,
279
+ num_faces=1
280
  )
281
 
282
  LEFT_EYEBROW = [70, 63, 105, 66, 107, 55, 65, 52, 53, 46]
283
  RIGHT_EYEBROW = [336, 296, 334, 293, 300, 285, 295, 282, 283, 276]
284
+
285
+ eyebrow_thickness = max(1, cfg.interior_line_thickness - 1)
286
+ processed_centers = []
287
+
288
+ def is_duplicate(cx, cy):
289
+ for (pcx, pcy) in processed_centers:
290
+ dist = np.sqrt((cx - pcx)**2 + (cy - pcy)**2)
291
+ if dist < 0.08:
292
+ return True
293
+ return False
294
+
295
+ if faces is None:
296
+ faces = detect_faces(rgb, cfg)
297
 
298
  with vision.FaceLandmarker.create_from_options(options) as landmarker:
299
+ # Phase 1: Global detection (upright / clear faces)
300
+ mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=rgb)
301
+ global_result = landmarker.detect(mp_image)
302
+ if global_result.face_landmarks:
303
+ for face_landmarks in global_result.face_landmarks:
304
+ xs = [lm.x for lm in face_landmarks]
305
+ ys = [lm.y for lm in face_landmarks]
306
+ cx, cy = np.mean(xs), np.mean(ys)
307
+ if not is_duplicate(cx, cy):
308
+ processed_centers.append((cx, cy))
309
+ for indices in [LEFT_EYEBROW, RIGHT_EYEBROW]:
310
+ points = []
311
+ for idx in indices:
312
+ lm = face_landmarks[idx]
313
+ points.append([int(lm.x * w), int(lm.y * h)])
314
+ pts = np.array(points, np.int32).reshape((-1, 1, 2))
315
+ cv2.polylines(eyebrows_layer, [pts], isClosed=False, color=255, thickness=eyebrow_thickness)
316
+
317
+ # Phase 2: OpenCV crops with 45% margin (for tilted / overlapping faces)
318
+ for i, (fx, fy, fw, fh) in enumerate(faces):
319
+ margin_x = int(fw * 0.45)
320
+ margin_y = int(fh * 0.45)
321
+ x1 = max(0, fx - margin_x)
322
+ y1 = max(0, fy - margin_y)
323
+ x2 = min(w, fx + fw + margin_x)
324
+ y2 = min(h, fy + fh + margin_y)
325
+
326
+ crop_w = x2 - x1
327
+ crop_h = y2 - y1
328
+ if crop_w <= 0 or crop_h <= 0:
329
+ continue
330
+
331
+ crop_cx = (x1 + crop_w / 2) / w
332
+ crop_cy = (y1 + crop_h / 2) / h
333
+ if is_duplicate(crop_cx, crop_cy):
334
+ continue
335
+
336
+ crop = rgb[y1:y2, x1:x2]
337
+ crop_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=crop)
338
+ crop_result = landmarker.detect(crop_image)
339
+
340
+ if crop_result.face_landmarks:
341
+ face_landmarks = crop_result.face_landmarks[0]
342
+ xs_global = [(x1 + lm.x * crop_w) / w for lm in face_landmarks]
343
+ ys_global = [(y1 + lm.y * crop_h) / h for lm in face_landmarks]
344
+ cx, cy = np.mean(xs_global), np.mean(ys_global)
345
+ if not is_duplicate(cx, cy):
346
+ processed_centers.append((cx, cy))
347
+ for indices in [LEFT_EYEBROW, RIGHT_EYEBROW]:
348
+ points = []
349
+ for idx in indices:
350
+ lm = face_landmarks[idx]
351
+ gx = int(x1 + lm.x * crop_w)
352
+ gy = int(y1 + lm.y * crop_h)
353
+ points.append([gx, gy])
354
+ pts = np.array(points, np.int32).reshape((-1, 1, 2))
355
+ cv2.polylines(eyebrows_layer, [pts], isClosed=False, color=255, thickness=eyebrow_thickness)
356
  except Exception as e:
357
  print(f"[AI] Eyebrow extraction error or library missing: {e}")
358
 
 
409
 
410
  # 6.5. Optional eyebrow detection overlay
411
  if cfg.detect_eyebrows:
412
+ eyebrows = draw_eyebrows(rgb, cfg, faces=faces)
413
  final_lines = cv2.add(final_lines, eyebrows)
414
 
415
  # Avoid drawing rectangular boxes/borders at the image edges