Upload 2 files
Browse files- app.py +68 -7
- 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
|
| 142 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
|