# built-in dependencies import time from typing import Any, Dict, Union, List, Tuple # 3rd party dependencies import numpy as np # project dependencies from deepface.modules import representation, detection, modeling from deepface.models.FacialRecognition import FacialRecognition from deepface.commons import logger as log logger = log.get_singletonish_logger() def verify( img1_path: Union[str, np.ndarray, List[float]], img2_path: Union[str, np.ndarray, List[float]], model_name: str = "VGG-Face", detector_backend: str = "opencv", distance_metric: str = "cosine", enforce_detection: bool = True, align: bool = True, expand_percentage: int = 0, normalization: str = "base", silent: bool = False, ) -> Dict[str, Any]: """ Verify if an image pair represents the same person or different persons. The verification function converts facial images to vectors and calculates the similarity between those vectors. Vectors of images of the same person should exhibit higher similarity (or lower distance) than vectors of images of different persons. Args: img1_path (str or np.ndarray or List[float]): Path to the first image. Accepts exact image path as a string, numpy array (BGR), base64 encoded images or pre-calculated embeddings. img2_path (str or np.ndarray or or List[float]): Path to the second image. Accepts exact image path as a string, numpy array (BGR), base64 encoded images or pre-calculated embeddings. model_name (str): Model for face recognition. Options: VGG-Face, Facenet, Facenet512, OpenFace, DeepFace, DeepID, Dlib, ArcFace, SFace and GhostFaceNet (default is VGG-Face). detector_backend (string): face detector backend. Options: 'opencv', 'retinaface', 'mtcnn', 'ssd', 'dlib', 'mediapipe', 'yolov8', 'centerface' or 'skip' (default is opencv) distance_metric (string): Metric for measuring similarity. Options: 'cosine', 'euclidean', 'euclidean_l2' (default is cosine). enforce_detection (boolean): If no face is detected in an image, raise an exception. Set to False to avoid the exception for low-resolution images (default is True). align (bool): Flag to enable face alignment (default is True). expand_percentage (int): expand detected facial area with a percentage (default is 0). normalization (string): Normalize the input image before feeding it to the model. Options: base, raw, Facenet, Facenet2018, VGGFace, VGGFace2, ArcFace (default is base) silent (boolean): Suppress or allow some log messages for a quieter analysis process (default is False). Returns: result (dict): A dictionary containing verification results. - 'verified' (bool): Indicates whether the images represent the same person (True) or different persons (False). - 'distance' (float): The distance measure between the face vectors. A lower distance indicates higher similarity. - 'max_threshold_to_verify' (float): The maximum threshold used for verification. If the distance is below this threshold, the images are considered a match. - 'model' (str): The chosen face recognition model. - 'similarity_metric' (str): The chosen similarity metric for measuring distances. - 'facial_areas' (dict): Rectangular regions of interest for faces in both images. - 'img1': {'x': int, 'y': int, 'w': int, 'h': int} Region of interest for the first image. - 'img2': {'x': int, 'y': int, 'w': int, 'h': int} Region of interest for the second image. - 'time' (float): Time taken for the verification process in seconds. """ tic = time.time() model: FacialRecognition = modeling.build_model(model_name) dims = model.output_shape # extract faces from img1 if isinstance(img1_path, list): # given image is already pre-calculated embedding if not all(isinstance(dim, float) for dim in img1_path): raise ValueError( "When passing img1_path as a list, ensure that all its items are of type float." ) if silent is False: logger.warn( "You passed 1st image as pre-calculated embeddings." f"Please ensure that embeddings have been calculated for the {model_name} model." ) if len(img1_path) != dims: raise ValueError( f"embeddings of {model_name} should have {dims} dimensions," f" but it has {len(img1_path)} dimensions input" ) img1_embeddings = [img1_path] img1_facial_areas = [None] else: try: img1_embeddings, img1_facial_areas = __extract_faces_and_embeddings( img_path=img1_path, model_name=model_name, detector_backend=detector_backend, enforce_detection=enforce_detection, align=align, expand_percentage=expand_percentage, normalization=normalization, ) except ValueError as err: raise ValueError("Exception while processing img1_path") from err # extract faces from img2 if isinstance(img2_path, list): # given image is already pre-calculated embedding if not all(isinstance(dim, float) for dim in img2_path): raise ValueError( "When passing img2_path as a list, ensure that all its items are of type float." ) if silent is False: logger.warn( "You passed 2nd image as pre-calculated embeddings." f"Please ensure that embeddings have been calculated for the {model_name} model." ) if len(img2_path) != dims: raise ValueError( f"embeddings of {model_name} should have {dims} dimensions," f" but it has {len(img2_path)} dimensions input" ) img2_embeddings = [img2_path] img2_facial_areas = [None] else: try: img2_embeddings, img2_facial_areas = __extract_faces_and_embeddings( img_path=img2_path, model_name=model_name, detector_backend=detector_backend, enforce_detection=enforce_detection, align=align, expand_percentage=expand_percentage, normalization=normalization, ) except ValueError as err: raise ValueError("Exception while processing img2_path") from err no_facial_area = { "x": None, "y": None, "w": None, "h": None, "left_eye": None, "right_eye": None, } distances = [] facial_areas = [] for idx, img1_embedding in enumerate(img1_embeddings): for idy, img2_embedding in enumerate(img2_embeddings): distance = find_distance(img1_embedding, img2_embedding, distance_metric) distances.append(distance) facial_areas.append( (img1_facial_areas[idx] or no_facial_area, img2_facial_areas[idy] or no_facial_area) ) # find the face pair with minimum distance threshold = find_threshold(model_name, distance_metric) distance = float(min(distances)) # best distance facial_areas = facial_areas[np.argmin(distances)] toc = time.time() resp_obj = { "verified": distance <= threshold, "distance": distance, "threshold": threshold, "model": model_name, "detector_backend": detector_backend, "similarity_metric": distance_metric, "facial_areas": {"img1": facial_areas[0], "img2": facial_areas[1]}, "time": round(toc - tic, 2), } return resp_obj def __extract_faces_and_embeddings( img_path: Union[str, np.ndarray], model_name: str = "VGG-Face", detector_backend: str = "opencv", enforce_detection: bool = True, align: bool = True, expand_percentage: int = 0, normalization: str = "base", ) -> Tuple[List[List[float]], List[dict]]: """ Extract facial areas and find corresponding embeddings for given image Returns: embeddings (List[float]) facial areas (List[dict]) """ embeddings = [] facial_areas = [] img_objs = detection.extract_faces( img_path=img_path, detector_backend=detector_backend, grayscale=False, enforce_detection=enforce_detection, align=align, expand_percentage=expand_percentage, ) # find embeddings for each face for img_obj in img_objs: img_embedding_obj = representation.represent( img_path=img_obj["face"], model_name=model_name, enforce_detection=enforce_detection, detector_backend="skip", align=align, normalization=normalization, ) # already extracted face given, safe to access its 1st item img_embedding = img_embedding_obj[0]["embedding"] embeddings.append(img_embedding) facial_areas.append(img_obj["facial_area"]) return embeddings, facial_areas def find_cosine_distance( source_representation: Union[np.ndarray, list], test_representation: Union[np.ndarray, list] ) -> np.float64: """ Find cosine distance between two given vectors Args: source_representation (np.ndarray or list): 1st vector test_representation (np.ndarray or list): 2nd vector Returns distance (np.float64): calculated cosine distance """ if isinstance(source_representation, list): source_representation = np.array(source_representation) if isinstance(test_representation, list): test_representation = np.array(test_representation) a = np.matmul(np.transpose(source_representation), test_representation) b = np.sum(np.multiply(source_representation, source_representation)) c = np.sum(np.multiply(test_representation, test_representation)) return 1 - (a / (np.sqrt(b) * np.sqrt(c))) def find_euclidean_distance( source_representation: Union[np.ndarray, list], test_representation: Union[np.ndarray, list] ) -> np.float64: """ Find euclidean distance between two given vectors Args: source_representation (np.ndarray or list): 1st vector test_representation (np.ndarray or list): 2nd vector Returns distance (np.float64): calculated euclidean distance """ if isinstance(source_representation, list): source_representation = np.array(source_representation) if isinstance(test_representation, list): test_representation = np.array(test_representation) euclidean_distance = source_representation - test_representation euclidean_distance = np.sum(np.multiply(euclidean_distance, euclidean_distance)) euclidean_distance = np.sqrt(euclidean_distance) return euclidean_distance def l2_normalize(x: Union[np.ndarray, list]) -> np.ndarray: """ Normalize input vector with l2 Args: x (np.ndarray or list): given vector Returns: y (np.ndarray): l2 normalized vector """ if isinstance(x, list): x = np.array(x) return x / np.sqrt(np.sum(np.multiply(x, x))) def find_distance( alpha_embedding: Union[np.ndarray, list], beta_embedding: Union[np.ndarray, list], distance_metric: str, ) -> np.float64: """ Wrapper to find distance between vectors according to the given distance metric Args: source_representation (np.ndarray or list): 1st vector test_representation (np.ndarray or list): 2nd vector Returns distance (np.float64): calculated cosine distance """ if distance_metric == "cosine": distance = find_cosine_distance(alpha_embedding, beta_embedding) elif distance_metric == "euclidean": distance = find_euclidean_distance(alpha_embedding, beta_embedding) elif distance_metric == "euclidean_l2": distance = find_euclidean_distance( l2_normalize(alpha_embedding), l2_normalize(beta_embedding) ) else: raise ValueError("Invalid distance_metric passed - ", distance_metric) return distance def find_threshold(model_name: str, distance_metric: str) -> float: """ Retrieve pre-tuned threshold values for a model and distance metric pair Args: model_name (str): Model for face recognition. Options: VGG-Face, Facenet, Facenet512, OpenFace, DeepFace, DeepID, Dlib, ArcFace, SFace and GhostFaceNet (default is VGG-Face). distance_metric (str): distance metric name. Options are cosine, euclidean and euclidean_l2. Returns: threshold (float): threshold value for that model name and distance metric pair. Distances less than this threshold will be classified same person. """ base_threshold = {"cosine": 0.40, "euclidean": 0.55, "euclidean_l2": 0.75} thresholds = { # "VGG-Face": {"cosine": 0.40, "euclidean": 0.60, "euclidean_l2": 0.86}, # 2622d "VGG-Face": { "cosine": 0.68, "euclidean": 1.17, "euclidean_l2": 1.17, }, # 4096d - tuned with LFW "Facenet": {"cosine": 0.40, "euclidean": 10, "euclidean_l2": 0.80}, "Facenet512": {"cosine": 0.30, "euclidean": 23.56, "euclidean_l2": 1.04}, "ArcFace": {"cosine": 0.68, "euclidean": 4.15, "euclidean_l2": 1.13}, "Dlib": {"cosine": 0.07, "euclidean": 0.6, "euclidean_l2": 0.4}, "SFace": {"cosine": 0.593, "euclidean": 10.734, "euclidean_l2": 1.055}, "OpenFace": {"cosine": 0.10, "euclidean": 0.55, "euclidean_l2": 0.55}, "DeepFace": {"cosine": 0.23, "euclidean": 64, "euclidean_l2": 0.64}, "DeepID": {"cosine": 0.015, "euclidean": 45, "euclidean_l2": 0.17}, "GhostFaceNet": {"cosine": 0.65, "euclidean": 35.71, "euclidean_l2": 1.10}, } threshold = thresholds.get(model_name, base_threshold).get(distance_metric, 0.4) return threshold