-
Notifications
You must be signed in to change notification settings - Fork 0
/
face.py
114 lines (84 loc) · 3.27 KB
/
face.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
import cv2
import dlib
from landmarks import LandmarksMap
detector = dlib.get_frontal_face_detector()
predictor = dlib.shape_predictor('data/dlib_face_landmarks.dat')
FAST_FACE_WIDTH = 120 # px, dlib can detect min 80 px width
class Face:
def __init__(self, img):
self._img = img
self._det = None
self.landmarks_map = None
self.fast_detected = False
@classmethod
def from_frame(cls, img, prev_face=None):
face = cls(img)
if prev_face:
# if we have previous face detection, lets reuse it and do faster method
face._det = face._detect_face_fast(img, prev_face)
if not face._det:
# if we did not detect a face fast (or at all before)
face._det = face._detect_face(img)
face._detect_landmarks()
return face
def _detect_face(self, img):
dets = detector(img, 0)
if len(dets) > 1:
raise ValueError(f'Detected {len(dets)} faces instead of 1')
if not dets:
return None
return dets[0]
def _detect_face_fast(self, img, prev_face):
# we want to use prev face detection to crop the image
# and speed up processing
if not prev_face.det:
return None
img = img.copy()
# pad a bit so we leave some wiggle room
pad_horiz = int(prev_face.det.width() * 0.2)
pad_vert = int(prev_face.det.height() * 0.2)
x1 = max(prev_face.det.left() - pad_horiz, 0)
y1 = max(prev_face.det.top() - pad_vert, 0)
x2 = min(prev_face.det.right() + pad_horiz, img.shape[1])
y2 = min(prev_face.det.bottom() + pad_vert, img.shape[0])
# crop the face out of image using previous bounding box
img = img[y1:y2, x1:x2]
# resize to be smaller and faster to process
scale = FAST_FACE_WIDTH / img.shape[1]
width = int(img.shape[1] * scale)
height = int(img.shape[0] * scale)
img = cv2.resize(img, (width, height), interpolation=cv2.INTER_AREA)
# now we can invoke regular face detection on a smaller cropped image
det = self._detect_face(img)
if not det:
return None
# need to map bounding box coordinates back to original frame
orig_left = x1 + int(det.left() / scale)
orig_top = y1 + int(det.top() / scale)
orig_right = orig_left + int(det.width() / scale)
orig_bottom = orig_top + int(det.height() / scale)
det = dlib.rectangle(orig_left, orig_top, orig_right, orig_bottom)
self.fast_detected = True
return det
def _detect_landmarks(self):
if not self._det:
return None
shape = predictor(self._img, self._det)
self.landmarks_map = LandmarksMap.from_dlib(shape)
def debug_draw(self, img=None):
if img is None:
img = self._img
if self._det:
# face rectangle
x1 = self._det.left()
y1 = self._det.top()
x2 = self._det.right()
y2 = self._det.bottom()
cv2.rectangle(img, (x1, y1), (x2, y2), (255, 0, 0), 2)
self.landmarks_map.debug_draw(img)
return img
@property
def det(self):
if not self._det:
return None
return self._det