def test_invalid_image(): model_loc = "./models" ob = FaceDetectorDlib(model_loc=model_loc, model_type="hog") img = np.zeros((100,100,5), dtype='float32') with pytest.raises(InvalidImage): ob.detect_faces(img)
def test_incorrect_model_path(): """ Test object init with the incorrect model path """ ob = None incorrect_model_loc = "./wrong_models" with pytest.raises(ModelFileMissing): ob = FaceDetectorDlib(model_loc=incorrect_model_loc, model_type="mmod")
def __init__( self, model_loc: str = "./models", persistent_data_loc="data/facial_data.json", face_detection_threshold: int = 0.99, face_detector: str = "dlib", ) -> None: """Constructor Args: model_loc (str, optional): Path where model files are saved. Defaults to "./models". persistent_data_loc (str, optional): Path to save the persistence storage file. Defaults to 'data/facial_data.json'. face_detection_threshold (int, optional): Threshold facial model confidence to consider a detection. Defaults to 0.99. face_detector (str, optional): Type of face detector to use. Options: Dlib-HOG and MMOD, MTCNN, OpenCV CNN. Defaults to 'dlib'. Raises: ModelFileMissing: Raised when model file is not found """ keypoints_model_path = os.path.join( model_loc, FaceRecognition.keypoints_model_path ) face_recog_model_path = os.path.join( model_loc, FaceRecognition.face_recog_model_path ) if not ( path_exists(keypoints_model_path) or path_exists(face_recog_model_path) ): raise ModelFileMissing if face_detector == "opencv": self.face_detector = FaceDetectorOpenCV( model_loc=model_loc, crop_forehead=True, shrink_ratio=0.2 ) elif face_detector == "mtcnn": self.face_detector = FaceDetectorMTCNN(crop_forehead=True, shrink_ratio=0.2) else: self.face_detector = FaceDetectorDlib() self.face_detection_threshold = face_detection_threshold self.keypoints_detector = dlib.shape_predictor(keypoints_model_path) self.face_recognizor = dlib.face_recognition_model_v1(face_recog_model_path) self.datastore = FaceDataStore(persistent_data_loc=persistent_data_loc)
def test_correct_model_path(): """ Test object init with the correct model path """ ob = None model_loc = "./models" try: ob = FaceDetectorDlib(model_loc=model_loc, model_type="mmod") except Exception: pass finally: assert isinstance(ob, FaceDetectorDlib)
def __init__( self, face_detector: str = "dlib", model_loc: str = "models", persistent_db_path: str = "data/facial_data.json", face_detection_threshold: float = 0.8, ) -> None: self.face_recognizer = FaceRecognition( model_loc=model_loc, persistent_data_loc=persistent_db_path, face_detection_threshold=face_detection_threshold, face_detector=face_detector, ) if face_detector == "opencv": self.face_detector = FaceDetectorOpenCV( model_loc=model_loc, crop_forehead=True, shrink_ratio=0.2 ) elif face_detector == "mtcnn": self.face_detector = FaceDetectorMTCNN(crop_forehead=True, shrink_ratio=0.2) elif face_detector == "dlib": self.face_detector = FaceDetectorDlib()
class FaceRecognition: """Class for Face Recognition related methods. Main operations: Register and Recognize face. Raises: ModelFileMissing: [description] NoNameProvided: [description] NoFaceDetected: [description] FaceMissing: [description] """ keypoints_model_path = "shape_predictor_5_face_landmarks.dat" face_recog_model_path = "dlib_face_recognition_resnet_model_v1.dat" def __init__( self, model_loc: str = "./models", persistent_data_loc="data/facial_data.json", face_detection_threshold: int = 0.99, face_detector: str = "dlib", ) -> None: """Constructor Args: model_loc (str, optional): Path where model files are saved. Defaults to "./models". persistent_data_loc (str, optional): Path to save the persistence storage file. Defaults to 'data/facial_data.json'. face_detection_threshold (int, optional): Threshold facial model confidence to consider a detection. Defaults to 0.99. face_detector (str, optional): Type of face detector to use. Options: Dlib-HOG and MMOD, MTCNN, OpenCV CNN. Defaults to 'dlib'. Raises: ModelFileMissing: Raised when model file is not found """ keypoints_model_path = os.path.join( model_loc, FaceRecognition.keypoints_model_path) face_recog_model_path = os.path.join( model_loc, FaceRecognition.face_recog_model_path) if not (path_exists(keypoints_model_path) or path_exists(face_recog_model_path)): raise ModelFileMissing if face_detector == "opencv": self.face_detector = FaceDetectorOpenCV(model_loc=model_loc, crop_forehead=True, shrink_ratio=0.2) elif face_detector == "mtcnn": self.face_detector = FaceDetectorMTCNN(crop_forehead=True, shrink_ratio=0.2) else: self.face_detector = FaceDetectorDlib() self.face_detection_threshold = face_detection_threshold self.keypoints_detector = dlib.shape_predictor(keypoints_model_path) self.face_recognizor = dlib.face_recognition_model_v1( face_recog_model_path) self.datastore = FaceDataStore(persistent_data_loc=persistent_data_loc) def register_face(self, image=None, name: str = None, bbox: List[int] = None): """Method to register a face via the facial encoding. Siamese neural network is used to generate 128 numbers for a given facial region. These encodings can be used to identify a facial ROI for identification later. Args: image (numpy array, optional): Defaults to None. name (str, optional): Name to associate with the face. Defaults to None. bbox (List[int], optional): Facial ROI bounding box. Defaults to None. Raises: NoNameProvided: NoFaceDetected: Returns: Dict: Facial encodings along with an unique identifier and name """ if not is_valid_img(image) or name is None: raise NoNameProvided if name is None else InvalidImage image = image.copy() face_encoding = None try: if bbox is None: bboxes = self.face_detector.detect_faces(image=image) if len(bboxes) == 0: raise NoFaceDetected bbox = bboxes[0] face_encoding = self.get_facial_fingerprint(image, bbox) # Convert the numpy array to normal python float list # to make json serialization simpler facial_data = { "id": str(uuid.uuid4()), "encoding": tuple(face_encoding.tolist()), "name": name, } # save the encoding with the name self.save_facial_data(facial_data) logger.info("Face registered with name: {}".format(name)) except Exception as exc: raise exc return facial_data def save_facial_data(self, facial_data: Dict = None) -> bool: """Saves facial data to cache and persistent storage Args: facial_data (Dict, optional): [description]. Defaults to None. Returns: bool: status of saving """ if facial_data is not None: self.datastore.add_facial_data(facial_data=facial_data) return True return False def get_registered_faces(self) -> List[Dict]: """Returns the list of all facial data of all registered users Returns: List[Dict]: List of facial data """ return self.datastore.get_all_facial_data() def recognize_faces(self, image, threshold: float = 0.6, bboxes: List[List[int]] = None): """Finds matching registered users for the face(s) in the input image. The input image should be cropped to contain only one face and then sent to this method. Args: image (numpy array): [description] threshold (float, optional): Max threshold euclidean distance to consider two people to be a match. Defaults to 0.6. bboxes (List[List[int]], optional): List of facial ROI bounding box. If this is None, then face detection is performed on the image and facial recognition is run for all the detected faces, otherwise if a bounding box is sent, then facial recognition is only done for that bounding box. Defaults to None. Raises: NoFaceDetected: [description] Returns: List[Tuple]: List of information of matching """ if image is None: return InvalidImage image = image.copy() if bboxes is None: bboxes = self.face_detector.detect_faces(image=image) if len(bboxes) == 0: raise NoFaceDetected # Load the data of existing registered faces # compare using the metric the closest match all_facial_data = self.datastore.get_all_facial_data() matches = [] for bbox in bboxes: face_encoding = self.get_facial_fingerprint(image, bbox) match, min_dist = None, 10000000 for face_data in all_facial_data: dist = self.euclidean_distance(face_encoding, face_data["encoding"]) if dist <= threshold and dist < min_dist: match = face_data min_dist = dist # bound box, matched face details, dist from closest match matches.append((bbox, match, min_dist)) return matches def get_facial_fingerprint(self, image, bbox: List[int] = None) -> List[float]: """Driver method for generating the facial encoding for an input image. Input image bbox -> facial keypoints detection -> keypoints used for face alignment -> Siamese NN -> Encoding Args: image (numpy array): [description] bbox (List[int], optional): List of facial ROI bounding box. Defaults to None. Raises: FaceMissing: [description] Returns: List[float]: Facial Encoding """ if bbox is None: raise FaceMissing # Convert to dlib format rectangle bbox = convert_to_dlib_rectangle(bbox) # Get the facial landmark coordinates face_keypoints = self.keypoints_detector(image, bbox) # Compute the 128D vector that describes the face in an img identified by # shape. In general, if two face descriptor vectors have a Euclidean # distance between them less than 0.6 then they are from the same # person, otherwise they are from different people. face_encoding = self.get_face_encoding(image, face_keypoints) return face_encoding def get_face_encoding(self, image, face_keypoints: List): """Method for generating the facial encoding for a face in an input image. Args: image (numpy array): [description] face_keypoints (List): [description] Returns: [type]: [description] """ encoding = self.face_recognizor.compute_face_descriptor( image, face_keypoints, 1) return np.array(encoding) def euclidean_distance(self, vector1: Tuple, vector2: Tuple): """Computes Euclidean distance between two vectors Args: vector1 (Tuple): [description] vector2 (Tuple): [description] Returns: [type]: [description] """ return np.linalg.norm(np.array(vector1) - np.array(vector2))
def test_detect_face_mmod(img2_data, img2_facebox_dlib_mmod): model_loc = "./models" ob = FaceDetectorDlib(model_loc=model_loc, model_type="mmod") assert img2_facebox_dlib_mmod == ob.detect_faces(img2_data)
def test_detect_face_hog(img2_data, img2_facebox_dlib_hog): model_loc = "./models" ob = FaceDetectorDlib(model_loc=model_loc, model_type="hog") assert img2_facebox_dlib_hog == ob.detect_faces(img2_data)
class FaceRecognitionVideo: """Class with methods to do facial recognition on video or webcam feed.""" def __init__( self, face_detector: str = "dlib", model_loc: str = "models", persistent_db_path: str = "data/facial_data.json", face_detection_threshold: float = 0.8, ) -> None: self.face_recognizer = FaceRecognition( model_loc=model_loc, persistent_data_loc=persistent_db_path, face_detection_threshold=face_detection_threshold, face_detector=face_detector, ) if face_detector == "opencv": self.face_detector = FaceDetectorOpenCV( model_loc=model_loc, crop_forehead=True, shrink_ratio=0.2 ) elif face_detector == "mtcnn": self.face_detector = FaceDetectorMTCNN(crop_forehead=True, shrink_ratio=0.2) elif face_detector == "dlib": self.face_detector = FaceDetectorDlib() def recognize_face_video( self, video_path: str = None, detection_interval: int = 15, save_output: bool = False, preview: bool = False, output_path: str = "data/output.mp4", resize_scale: float = 0.5, verbose: bool = True, ) -> None: if video_path is None: # If no video source is given, try # switching to webcam video_path = 0 elif not path_exists(video_path): raise FileNotFoundError cap, video_writer = None, None try: cap = cv2.VideoCapture(video_path) # To save the video file, get the opencv video writer video_writer = get_video_writer(cap, output_path) frame_num = 1 matches, name, match_dist = [], None, None t1 = time.time() logger.info("Enter q to exit...") while True: status, frame = cap.read() if not status: break try: # Flip webcam feed so that it looks mirrored if video_path == 0: frame = cv2.flip(frame, 2) if frame_num % detection_interval == 0: # Scale down the image to increase model # inference time. smaller_frame = convert_to_rgb( cv2.resize(frame, (0, 0), fx=resize_scale, fy=resize_scale) ) # Detect faces matches = self.face_recognizer.recognize_faces( image=smaller_frame, threshold=0.6, bboxes=None ) if verbose: self.annotate_facial_data(matches, frame, resize_scale) if save_output: video_writer.write(frame) if preview: cv2.imshow("Preview", cv2.resize(frame, (680, 480))) key = cv2.waitKey(1) & 0xFF if key == ord("q"): break except Exception: pass frame_num += 1 t2 = time.time() logger.info("Time:{}".format((t2 - t1) / 60)) logger.info("Total frames: {}".format(frame_num)) logger.info("Time per frame: {}".format((t2 - t1) / frame_num)) except Exception as exc: raise exc finally: cv2.destroyAllWindows() cap.release() video_writer.release() def register_face_webcam( self, name: str = None, detection_interval: int = 5 ) -> bool: if name is None: raise NoNameProvided cap = None try: cap = cv2.VideoCapture(0) frame_num = 0 while True: status, frame = cap.read() if not status: break if frame_num % detection_interval == 0: # detect faces bboxes = self.face_detector.detect_faces(image=frame) try: if len(bboxes) == 1: facial_data = self.face_recognizer.register_face( image=frame, name=name, bbox=bboxes[0] ) if facial_data: draw_bounding_box(frame, bboxes[0]) cv2.imshow("Registered Face", frame) cv2.waitKey(0) logger.info("Press any key to continue......") break except Exception as exc: traceback.print_exc(file=sys.stdout) frame_num += 1 except Exception as exc: raise exc finally: cv2.destroyAllWindows() cap.release() def register_face_path(self, img_path: str, name: str) -> None: if not path_exists(img_path): raise PathNotFound try: img = cv2.imread(img_path) facial_data = self.face_recognizer.register_face( image=convert_to_rgb(img), name=name ) if facial_data: logger.info("Face registered...") return True return False except Exception as exc: raise exc def annotate_facial_data( self, matches: List[Dict], image, resize_scale: float ) -> None: for face_bbox, match, dist in matches: name = match["name"] if match is not None else "Unknown" # match_dist = '{:.2f}'.format(dist) if dist < 1000 else 'INF' # name = name + ', Dist: {}'.format(match_dist) # draw face labels draw_annotation(image, name, int(1 / resize_scale) * np.array(face_bbox))