ItsFarros commited on
Commit
96019a4
Β·
verified Β·
1 Parent(s): 8be8916

Upload backend/face_detection.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. backend/face_detection.py +200 -0
backend/face_detection.py ADDED
@@ -0,0 +1,200 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import cv2
3
+ import numpy as np
4
+
5
+ MODEL_DIR = os.path.join(os.path.dirname(__file__), "models")
6
+ MODEL_PATH = os.path.join(MODEL_DIR, "blaze_face_short_range.tflite")
7
+ _detector = None
8
+
9
+
10
+ def _get_detector():
11
+ from mediapipe.tasks.python import BaseOptions
12
+ from mediapipe.tasks.python.vision.face_detector import FaceDetector, FaceDetectorOptions
13
+ global _detector
14
+ if _detector is None:
15
+ base = BaseOptions(model_asset_path=MODEL_PATH)
16
+ opts = FaceDetectorOptions(base_options=base, min_detection_confidence=0.5)
17
+ _detector = FaceDetector.create_from_options(opts)
18
+ return _detector
19
+
20
+
21
+ # ── Skin color fallback ────────────────────────────────────────────
22
+ def _detect_skin_region(image):
23
+ """Detect skin-colored region using HSV. Returns (x, y, w, h) or None."""
24
+ hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
25
+
26
+ lower1 = np.array([0, 20, 70], dtype=np.uint8)
27
+ upper1 = np.array([20, 255, 255], dtype=np.uint8)
28
+ lower2 = np.array([160, 20, 70], dtype=np.uint8)
29
+ upper2 = np.array([180, 255, 255], dtype=np.uint8)
30
+
31
+ mask1 = cv2.inRange(hsv, lower1, upper1)
32
+ mask2 = cv2.inRange(hsv, lower2, upper2)
33
+ mask = cv2.bitwise_or(mask1, mask2)
34
+
35
+ kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (15, 15))
36
+ mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
37
+ mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
38
+
39
+ contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
40
+ if not contours:
41
+ return None
42
+
43
+ largest = max(contours, key=cv2.contourArea)
44
+ img_area = image.shape[0] * image.shape[1]
45
+ if cv2.contourArea(largest) < img_area * 0.05:
46
+ return None
47
+
48
+ x, y, bw, bh = cv2.boundingRect(largest)
49
+ return (x, y, bw, bh)
50
+
51
+
52
+ # ── Helper ─────────────────────────────────────────────────────────
53
+ def _crop_region(image, x, y, bw, bh, padding=0.2, shift_up=0.0):
54
+ """Crop region with padding, clamped to image bounds.
55
+
56
+ shift_up: fraction of face height to shift crop upward
57
+ (keeps same crop size, moves window up)
58
+ """
59
+ h, w = image.shape[:2]
60
+ pad_x = int(bw * padding)
61
+ pad_y = int(bh * padding)
62
+ shift_px = int(bh * shift_up)
63
+
64
+ crop_h = bh + 2 * pad_y # desired crop height
65
+ crop_w = bw + 2 * pad_x
66
+
67
+ # Center on face, then shift up
68
+ cx = x + bw // 2
69
+ cy = y + bh // 2 - shift_px
70
+
71
+ x1 = max(0, cx - crop_w // 2)
72
+ y1 = max(0, cy - crop_h // 2)
73
+ x2 = min(w, x1 + crop_w)
74
+ y2 = min(h, y1 + crop_h)
75
+
76
+ # Re-adjust if clipped at top boundary
77
+ if y1 == 0 and y2 - y1 < crop_h:
78
+ y2 = min(h, crop_h)
79
+
80
+ return image[y1:y2, x1:x2]
81
+
82
+
83
+ # ── Orientation ────────────────────────────────────────────────────
84
+ def _classify_orientation(face_result, detection=None):
85
+ """Klasifikasi orientasi wajah: 'frontal' atau 'side_profile'.
86
+
87
+ Menggunakan keypoints MediaPipe (mata/telinga) jika tersedia,
88
+ fallback ke aspect ratio bounding box.
89
+ """
90
+ x, y, w, h = face_result["bounds"]
91
+ aspect = h / max(w, 1)
92
+
93
+ # Jika ada keypoints dari MediaPipe
94
+ if detection is not None and hasattr(detection, 'keypoints') and len(detection.keypoints) >= 6:
95
+ kp = detection.keypoints
96
+ # keypoints: 0=right_eye, 1=left_eye, 2=nose, 3=mouth, 4=right_ear, 5=left_ear
97
+ reye, leye = kp[0], kp[1]
98
+ rear, lear = kp[4], kp[5]
99
+ # Jarak horizontal mata (normalized 0-1)
100
+ eye_dist = abs(leye.x - reye.x)
101
+ # Frontal: kedua mata terpisah lebar, ear di sisi luar
102
+ if eye_dist > 0.12:
103
+ return "frontal"
104
+ # Side profile: mata berdekatan, atau ear mendekati center
105
+ if eye_dist < 0.08 or abs(rear.x - lear.x) < 0.03:
106
+ return "side_profile"
107
+
108
+ # Fallback: aspect ratio
109
+ # Frontal ~1.0-1.6, side profile ~1.6-2.5
110
+ return "side_profile" if aspect > 1.7 else "frontal"
111
+
112
+
113
+ # ── Public API ─────────────────────────────────────────────────────
114
+ def detect_face(image):
115
+ """Detect face in image and return info dict or None.
116
+
117
+ Returns:
118
+ dict with keys:
119
+ - bounds: (x, y, w, h) face bounding box on original image
120
+ - score: confidence score (0-1)
121
+ - method: "mediapipe" | "skin"
122
+ - orientation: "frontal" | "side_profile" (mediapipe only)
123
+ or None if no face found.
124
+ """
125
+ # Step 1: MediaPipe Face Detection
126
+ try:
127
+ from mediapipe.tasks.python.vision.core.image import Image, ImageFormat
128
+ detector = _get_detector()
129
+ rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
130
+ mp_img = Image(image_format=ImageFormat.SRGB, data=rgb)
131
+ result = detector.detect(mp_img)
132
+ if result.detections:
133
+ best = max(result.detections, key=lambda d: d.categories[0].score)
134
+ bb = best.bounding_box
135
+ x = int(bb.origin_x)
136
+ y = int(bb.origin_y)
137
+ fw = int(bb.width)
138
+ fh = int(bb.height)
139
+ face_result = {
140
+ "bounds": (x, y, fw, fh),
141
+ "score": float(best.categories[0].score),
142
+ "method": "mediapipe",
143
+ }
144
+ face_result["orientation"] = _classify_orientation(face_result, detection=best)
145
+ return face_result
146
+ except Exception as e:
147
+ print(f"[face_detection] MediaPipe failed: {e}")
148
+
149
+ # Step 2: Skin color fallback
150
+ skin = _detect_skin_region(image)
151
+ if skin is not None:
152
+ x, y, fw, fh = int(skin[0]), int(skin[1]), int(skin[2]), int(skin[3])
153
+ return {"bounds": (x, y, fw, fh), "score": 1.0, "method": "skin", "orientation": "frontal"}
154
+
155
+ return None
156
+
157
+
158
+ def draw_face_box(image, face_result, color=(0, 255, 0), thickness=3):
159
+ """Draw face bounding box on a copy of the image."""
160
+ result = image.copy()
161
+ if face_result is None:
162
+ return result
163
+ x, y, w, h = face_result["bounds"]
164
+ cv2.rectangle(result, (x, y), (x + w, y + h), color, thickness)
165
+
166
+ orientation = face_result.get("orientation", "?")
167
+ label = f"face ({face_result['method']}) {orientation} {face_result['score']:.2f}"
168
+ (tw, th), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1)
169
+ cv2.rectangle(result, (x, y - th - 6), (x + tw + 4, y), color, -1)
170
+ cv2.putText(result, label, (x + 2, y - 3),
171
+ cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1)
172
+
173
+ return result
174
+
175
+
176
+ def crop_face(image, padding=0.2, shift_up=0.15,
177
+ padding_side=0.30, shift_up_side=0.15):
178
+ """Detect face area and crop image with orientation-aware settings.
179
+
180
+ Args:
181
+ image: numpy array (BGR image from cv2.imread)
182
+ padding: face padding for frontal faces
183
+ shift_up: face shift for frontal faces
184
+ padding_side: face padding for side profiles
185
+ shift_up_side: face shift for side profiles
186
+
187
+ Returns:
188
+ Cropped face image, or original image if nothing detected.
189
+ """
190
+ face_result = detect_face(image)
191
+ if face_result is None:
192
+ return image
193
+
194
+ orientation = face_result.get("orientation", "frontal")
195
+ if orientation == "side_profile":
196
+ padding = padding_side
197
+ shift_up = shift_up_side
198
+
199
+ x, y, fw, fh = face_result["bounds"]
200
+ return _crop_region(image, x, y, fw, fh, padding, shift_up=shift_up)