diff --git a/deepface/DeepFace.py b/deepface/DeepFace.py index 2f20a13..3b3fce3 100644 --- a/deepface/DeepFace.py +++ b/deepface/DeepFace.py @@ -102,7 +102,7 @@ def verify( 'centerface' or 'skip' (default is opencv). distance_metric (string): Metric for measuring similarity. Options: 'cosine', - 'euclidean', 'euclidean_l2' (default is cosine). + 'euclidean', 'euclidean_l2', 'angular' (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). @@ -194,7 +194,7 @@ def analyze( 'centerface' or 'skip' (default is opencv). distance_metric (string): Metric for measuring similarity. Options: 'cosine', - 'euclidean', 'euclidean_l2' (default is cosine). + 'euclidean', 'euclidean_l2', 'angular' (default is cosine). align (boolean): Perform alignment based on the eye positions (default is True). @@ -299,7 +299,7 @@ def find( OpenFace, DeepFace, DeepID, Dlib, ArcFace, SFace and GhostFaceNet (default is VGG-Face). distance_metric (string): Metric for measuring similarity. Options: 'cosine', - 'euclidean', 'euclidean_l2' (default is cosine). + 'euclidean', 'euclidean_l2', 'angular' (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). @@ -479,7 +479,7 @@ def stream( 'centerface' or 'skip' (default is opencv). distance_metric (string): Metric for measuring similarity. Options: 'cosine', - 'euclidean', 'euclidean_l2' (default is cosine). + 'euclidean', 'euclidean_l2', 'angular' (default is cosine). enable_face_analysis (bool): Flag to enable face analysis (default is True). diff --git a/deepface/modules/demography.py b/deepface/modules/demography.py index 218d541..7611b33 100644 --- a/deepface/modules/demography.py +++ b/deepface/modules/demography.py @@ -39,7 +39,7 @@ def analyze( 'centerface' or 'skip' (default is opencv). distance_metric (string): Metric for measuring similarity. Options: 'cosine', - 'euclidean', 'euclidean_l2' (default is cosine). + 'euclidean', 'euclidean_l2', 'angular' (default is cosine). align (boolean): Perform alignment based on the eye positions (default is True). diff --git a/deepface/modules/recognition.py b/deepface/modules/recognition.py index 4002b47..2a29944 100644 --- a/deepface/modules/recognition.py +++ b/deepface/modules/recognition.py @@ -48,7 +48,7 @@ def find( OpenFace, DeepFace, DeepID, Dlib, ArcFace, SFace and GhostFaceNet (default is VGG-Face). distance_metric (string): Metric for measuring similarity. Options: 'cosine', - 'euclidean', 'euclidean_l2'. + 'euclidean', 'euclidean_l2', 'angular'. enforce_detection (boolean): If no face is detected in an image, raise an exception. Default is True. Set to False to avoid the exception for low-resolution images. @@ -481,7 +481,7 @@ def find_batched( OpenFace, DeepFace, DeepID, Dlib, ArcFace, SFace and GhostFaceNet (default is VGG-Face). distance_metric (string): Metric for measuring similarity. Options: 'cosine', - 'euclidean', 'euclidean_l2'. + 'euclidean', 'euclidean_l2', 'angular'. enforce_detection (boolean): If no face is detected in an image, raise an exception. Default is True. Set to False to avoid the exception for low-resolution images. diff --git a/deepface/modules/streaming.py b/deepface/modules/streaming.py index e461e56..75e543b 100644 --- a/deepface/modules/streaming.py +++ b/deepface/modules/streaming.py @@ -51,7 +51,7 @@ def analysis( 'centerface' or 'skip' (default is opencv). distance_metric (string): Metric for measuring similarity. Options: 'cosine', - 'euclidean', 'euclidean_l2' (default is cosine). + 'euclidean', 'euclidean_l2', 'angular' (default is cosine). enable_face_analysis (bool): Flag to enable face analysis (default is True). @@ -223,7 +223,7 @@ def search_identity( 'mtcnn', 'ssd', 'dlib', 'mediapipe', 'yolov8', 'yolov11n', 'yolov11s', 'yolov11m', 'centerface' or 'skip' (default is opencv). distance_metric (string): Metric for measuring similarity. Options: 'cosine', - 'euclidean', 'euclidean_l2' (default is cosine). + 'euclidean', 'euclidean_l2', angular, (default is cosine). Returns: result (tuple): result consisting of following objects identified image path (str) @@ -474,7 +474,7 @@ def perform_facial_recognition( 'mtcnn', 'ssd', 'dlib', 'mediapipe', 'yolov8', 'yolov11n', 'yolov11s', 'yolov11m', 'centerface' or 'skip' (default is opencv). distance_metric (string): Metric for measuring similarity. Options: 'cosine', - 'euclidean', 'euclidean_l2' (default is cosine). + 'euclidean', 'euclidean_l2', angular (default is cosine). model_name (str): Model for face recognition. Options: VGG-Face, Facenet, Facenet512, OpenFace, DeepFace, DeepID, Dlib, ArcFace, SFace and GhostFaceNet (default is VGG-Face). Returns: diff --git a/deepface/modules/verification.py b/deepface/modules/verification.py index 2dc11aa..c5d1d53 100644 --- a/deepface/modules/verification.py +++ b/deepface/modules/verification.py @@ -51,7 +51,7 @@ def verify( 'centerface' or 'skip' (default is opencv) distance_metric (string): Metric for measuring similarity. Options: 'cosine', - 'euclidean', 'euclidean_l2' (default is cosine). + 'euclidean', 'euclidean_l2', angular (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). @@ -297,6 +297,45 @@ def find_cosine_distance( ) return distances +def find_angular_distance( + source_representation: Union[np.ndarray, list], test_representation: Union[np.ndarray, list] +) -> Union[np.float64, np.ndarray]: + """ + Find angular distance between two vectors or batches of vectors. + + Args: + source_representation (np.ndarray or list): 1st vector or batch of vectors. + test_representation (np.ndarray or list): 2nd vector or batch of vectors. + + Returns: + np.float64 or np.ndarray: angular distance(s). + Returns a np.float64 for single embeddings and np.ndarray for batch embeddings. + """ + + # calculate cosine similarity first + # then convert to angular distance + source_representation = np.asarray(source_representation) + test_representation = np.asarray(test_representation) + + if source_representation.ndim == 1 and test_representation.ndim == 1: + # single embedding + dot_product = np.dot(source_representation, test_representation) + source_norm = np.linalg.norm(source_representation) + test_norm = np.linalg.norm(test_representation) + similarity = dot_product / (source_norm * test_norm) + distances = np.arccos(similarity) / np.pi + elif source_representation.ndim == 2 and test_representation.ndim == 2: + # list of embeddings (batch) + source_normed = l2_normalize(source_representation, axis=1) # (N, D) + test_normed = l2_normalize(test_representation, axis=1) # (M, D) + similarity = np.dot(test_normed, source_normed.T) # (M, N) + distances = np.arccos(similarity) / np.pi + else: + raise ValueError( + f"Embeddings must be 1D or 2D, but received " + f"source shape: {source_representation.shape}, test shape: {test_representation.shape}" + ) + return distances def find_euclidean_distance( source_representation: Union[np.ndarray, list], test_representation: Union[np.ndarray, list] @@ -362,7 +401,7 @@ def find_distance( alpha_embedding (np.ndarray or list): 1st vector or batch of vectors. beta_embedding (np.ndarray or list): 2nd vector or batch of vectors. distance_metric (str): The type of distance to compute - ('cosine', 'euclidean', or 'euclidean_l2'). + ('cosine', 'euclidean', 'euclidean_l2', or 'angular'). Returns: np.float64 or np.ndarray: The calculated distance(s). @@ -380,6 +419,8 @@ def find_distance( if distance_metric == "cosine": distance = find_cosine_distance(alpha_embedding, beta_embedding) + elif distance_metric == "angular": + distance = find_angular_distance(alpha_embedding, beta_embedding) elif distance_metric == "euclidean": distance = find_euclidean_distance(alpha_embedding, beta_embedding) elif distance_metric == "euclidean_l2": @@ -399,31 +440,32 @@ def find_threshold(model_name: str, distance_metric: str) -> float: 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. + euclidean_l2 and angular. 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} + base_threshold = {"cosine": 0.40, "euclidean": 0.55, "euclidean_l2": 0.75, "angular": 0.37} thresholds = { - # "VGG-Face": {"cosine": 0.40, "euclidean": 0.60, "euclidean_l2": 0.86}, # 2622d + # "VGG-Face": {"cosine": 0.40, "euclidean": 0.60, "euclidean_l2": 0.86, "angular": 0.37}, # 2622d "VGG-Face": { "cosine": 0.68, "euclidean": 1.17, "euclidean_l2": 1.17, + "angular": 0.43, }, # 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}, - "Buffalo_L": {"cosine": 0.55, "euclidean": 0.6, "euclidean_l2": 1.1}, + "Facenet": {"cosine": 0.40, "euclidean": 10, "euclidean_l2": 0.80, "angular": 0.47}, + "Facenet512": {"cosine": 0.30, "euclidean": 23.56, "euclidean_l2": 1.04, "angular": 0.49}, + "ArcFace": {"cosine": 0.68, "euclidean": 4.15, "euclidean_l2": 1.13, "angular": 0.43}, + "Dlib": {"cosine": 0.07, "euclidean": 0.6, "euclidean_l2": 0.4, "angular": 0.50}, + "SFace": {"cosine": 0.593, "euclidean": 10.734, "euclidean_l2": 1.055, "angular": 0.445}, + "OpenFace": {"cosine": 0.10, "euclidean": 0.55, "euclidean_l2": 0.55, "angular": 0.50}, + "DeepFace": {"cosine": 0.23, "euclidean": 64, "euclidean_l2": 0.64, "angular": 0.49}, + "DeepID": {"cosine": 0.015, "euclidean": 45, "euclidean_l2": 0.17, "angular": 0.50}, + "GhostFaceNet": {"cosine": 0.65, "euclidean": 35.71, "euclidean_l2": 1.10, "angular": 0.43}, + "Buffalo_L": {"cosine": 0.55, "euclidean": 0.6, "euclidean_l2": 1.1, "angular": 0.45}, } threshold = thresholds.get(model_name, base_threshold).get(distance_metric, 0.4) diff --git a/tests/test_verify.py b/tests/test_verify.py index 2a6951b..f542da1 100644 --- a/tests/test_verify.py +++ b/tests/test_verify.py @@ -9,7 +9,7 @@ from deepface.commons.logger import Logger logger = Logger() models = ["VGG-Face", "Facenet", "Facenet512", "ArcFace", "GhostFaceNet"] -metrics = ["cosine", "euclidean", "euclidean_l2"] +metrics = ["cosine", "euclidean", "euclidean_l2", "angular"] detectors = ["opencv", "mtcnn"]