Stylique commited on
Commit
fea08e3
·
verified ·
1 Parent(s): 6d863be

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +68 -7
  2. requirements.txt +1 -0
app.py CHANGED
@@ -7,6 +7,12 @@ import gradio as gr
7
  import numpy as np
8
  from PIL import Image
9
 
 
 
 
 
 
 
10
 
11
  def _ensure_rgb_uint8(image: np.ndarray) -> np.ndarray:
12
  """Convert an input image array to RGB uint8 format.
@@ -41,6 +47,47 @@ def _central_crop_bbox(width: int, height: int, frac: float = 0.6) -> Tuple[int,
41
  return x1, y1, x2, y2
42
 
43
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  def _binary_open_close(mask: np.ndarray, kernel_size: int = 5, iterations: int = 1) -> np.ndarray:
45
  """Apply morphological open then close to clean the binary mask."""
46
  kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (kernel_size, kernel_size))
@@ -129,7 +176,7 @@ def _solid_color_image(color_rgb: np.ndarray, size: Tuple[int, int] = (160, 160)
129
  return swatch
130
 
131
 
132
- def detect_skin_tone(image: np.ndarray, center_focus: bool = True) -> Tuple[str, np.ndarray, np.ndarray]:
133
  """Main pipeline: returns (hex_code, color_swatch_image, debug_mask_overlay).
134
 
135
  - image: input image as numpy array (H, W, 3) RGB uint8
@@ -138,8 +185,15 @@ def detect_skin_tone(image: np.ndarray, center_focus: bool = True) -> Tuple[str,
138
  rgb = _ensure_rgb_uint8(image)
139
  height, width = rgb.shape[:2]
140
 
141
- # Optionally restrict to central crop to avoid background
142
- if center_focus:
 
 
 
 
 
 
 
143
  x1, y1, x2, y2 = _central_crop_bbox(width, height, frac=0.7)
144
  central_rgb = rgb[y1:y2, x1:x2]
145
  else:
@@ -218,21 +272,28 @@ with gr.Blocks(title="Skin Tone Detector") as demo:
218
  height=360,
219
  )
220
  center_focus = gr.Checkbox(value=True, label="Center focus (ignore edges)")
 
221
  run_btn = gr.Button("Detect Skin Tone", variant="primary")
222
 
223
  with gr.Column():
224
  hex_output = gr.HTML(label="HEX Color")
225
  swatch_output = gr.Image(label="Color Swatch", type="numpy")
226
  debug_output = gr.Image(label="Mask Overlay", type="numpy")
 
 
227
 
228
- def _run(image: Optional[np.ndarray], center_focus: bool):
229
  if image is None:
230
  return _hex_html("#000000"), np.zeros((160, 160, 3), dtype=np.uint8), None
231
- hex_code, swatch, debug = detect_skin_tone(image, center_focus=center_focus)
 
 
 
 
232
  return _hex_html(hex_code), swatch, debug
233
 
234
- run_btn.click(_run, inputs=[input_image, center_focus], outputs=[hex_output, swatch_output, debug_output])
235
- input_image.change(_run, inputs=[input_image, center_focus], outputs=[hex_output, swatch_output, debug_output])
236
 
237
 
238
  if __name__ == "__main__":
 
7
  import numpy as np
8
  from PIL import Image
9
 
10
+ try:
11
+ import mediapipe as mp # type: ignore
12
+ HAS_MEDIAPIPE = True
13
+ except Exception: # pragma: no cover - optional dependency
14
+ HAS_MEDIAPIPE = False
15
+
16
 
17
  def _ensure_rgb_uint8(image: np.ndarray) -> np.ndarray:
18
  """Convert an input image array to RGB uint8 format.
 
47
  return x1, y1, x2, y2
48
 
49
 
50
+ def _detect_face_bbox_mediapipe(image_rgb: np.ndarray) -> Optional[Tuple[int, int, int, int]]:
51
+ """Detect a face bounding box using MediaPipe Face Detection and return (x1, y1, x2, y2).
52
+
53
+ Returns None if detection fails or mediapipe is unavailable.
54
+ """
55
+ if not HAS_MEDIAPIPE:
56
+ return None
57
+ height, width = image_rgb.shape[:2]
58
+ try:
59
+ with mp.solutions.face_detection.FaceDetection(model_selection=1, min_detection_confidence=0.5) as detector:
60
+ results = detector.process(image_rgb)
61
+ detections = results.detections or []
62
+ if not detections:
63
+ return None
64
+ # Pick the largest bbox
65
+ def bbox_area(det):
66
+ bbox = det.location_data.relative_bounding_box
67
+ return max(0.0, bbox.width) * max(0.0, bbox.height)
68
+
69
+ best = max(detections, key=bbox_area)
70
+ rb = best.location_data.relative_bounding_box
71
+ x1 = int(np.clip(rb.xmin * width, 0, width - 1))
72
+ y1 = int(np.clip(rb.ymin * height, 0, height - 1))
73
+ x2 = int(np.clip((rb.xmin + rb.width) * width, 0, width))
74
+ y2 = int(np.clip((rb.ymin + rb.height) * height, 0, height))
75
+
76
+ # Expand a bit to include cheeks/forehead
77
+ pad_x = int(0.08 * width)
78
+ pad_y = int(0.12 * height)
79
+ x1 = int(np.clip(x1 - pad_x, 0, width - 1))
80
+ y1 = int(np.clip(y1 - pad_y, 0, height - 1))
81
+ x2 = int(np.clip(x2 + pad_x, 0, width))
82
+ y2 = int(np.clip(y2 + pad_y, 0, height))
83
+
84
+ if x2 - x1 < 10 or y2 - y1 < 10:
85
+ return None
86
+ return x1, y1, x2, y2
87
+ except Exception:
88
+ return None
89
+
90
+
91
  def _binary_open_close(mask: np.ndarray, kernel_size: int = 5, iterations: int = 1) -> np.ndarray:
92
  """Apply morphological open then close to clean the binary mask."""
93
  kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (kernel_size, kernel_size))
 
176
  return swatch
177
 
178
 
179
+ def detect_skin_tone(image: np.ndarray, center_focus: bool = True, use_face_detector: bool = False) -> Tuple[str, np.ndarray, np.ndarray]:
180
  """Main pipeline: returns (hex_code, color_swatch_image, debug_mask_overlay).
181
 
182
  - image: input image as numpy array (H, W, 3) RGB uint8
 
185
  rgb = _ensure_rgb_uint8(image)
186
  height, width = rgb.shape[:2]
187
 
188
+ # Optionally restrict to detected face, else center crop, else full image
189
+ face_bbox: Optional[Tuple[int, int, int, int]] = None
190
+ if use_face_detector:
191
+ face_bbox = _detect_face_bbox_mediapipe(rgb)
192
+
193
+ if face_bbox is not None:
194
+ x1, y1, x2, y2 = face_bbox
195
+ central_rgb = rgb[y1:y2, x1:x2]
196
+ elif center_focus:
197
  x1, y1, x2, y2 = _central_crop_bbox(width, height, frac=0.7)
198
  central_rgb = rgb[y1:y2, x1:x2]
199
  else:
 
272
  height=360,
273
  )
274
  center_focus = gr.Checkbox(value=True, label="Center focus (ignore edges)")
275
+ use_face_det = gr.Checkbox(value=True if HAS_MEDIAPIPE else False, label="Use face detection (MediaPipe)")
276
  run_btn = gr.Button("Detect Skin Tone", variant="primary")
277
 
278
  with gr.Column():
279
  hex_output = gr.HTML(label="HEX Color")
280
  swatch_output = gr.Image(label="Color Swatch", type="numpy")
281
  debug_output = gr.Image(label="Mask Overlay", type="numpy")
282
+ if not HAS_MEDIAPIPE:
283
+ gr.Markdown("MediaPipe not installed or unavailable. Face detection toggle will be ignored.")
284
 
285
+ def _run(image: Optional[np.ndarray], center_focus: bool, use_face_det_flag: bool):
286
  if image is None:
287
  return _hex_html("#000000"), np.zeros((160, 160, 3), dtype=np.uint8), None
288
+ hex_code, swatch, debug = detect_skin_tone(
289
+ image,
290
+ center_focus=center_focus,
291
+ use_face_detector=(use_face_det_flag and HAS_MEDIAPIPE),
292
+ )
293
  return _hex_html(hex_code), swatch, debug
294
 
295
+ run_btn.click(_run, inputs=[input_image, center_focus, use_face_det], outputs=[hex_output, swatch_output, debug_output])
296
+ input_image.change(_run, inputs=[input_image, center_focus, use_face_det], outputs=[hex_output, swatch_output, debug_output])
297
 
298
 
299
  if __name__ == "__main__":
requirements.txt CHANGED
@@ -2,4 +2,5 @@ gradio>=4.44.0
2
  opencv-python-headless>=4.10.0.84
3
  numpy>=1.26.0
4
  Pillow>=10.3.0
 
5
 
 
2
  opencv-python-headless>=4.10.0.84
3
  numpy>=1.26.0
4
  Pillow>=10.3.0
5
+ mediapipe>=0.10.14
6