diff --git a/deepface/DeepFace.py b/deepface/DeepFace.py index 7d7e81f1..af5245fd 100644 --- a/deepface/DeepFace.py +++ b/deepface/DeepFace.py @@ -323,18 +323,18 @@ def find( anti_spoofing (boolean): Flag to enable anti spoofing (default is False). Returns: - results (List[pd.DataFrame] or List[List[Dict[str, Any]]]): + results (List[pd.DataFrame] or List[List[Dict[str, Any]]]): A list of pandas dataframes (if `batched=False`) or a list of dicts (if `batched=True`). Each dataframe or dict corresponds to the identity information for an individual detected in the source image. Note: If you have a large database and/or a source photo with many faces, - use `batched=True`, as it is optimized for large batch processing. - Please pay attention that when using `batched=True`, the function returns + use `batched=True`, as it is optimized for large batch processing. + Please pay attention that when using `batched=True`, the function returns a list of dicts (not a list of DataFrames), but with the same keys as the columns in the DataFrame. - + The DataFrame columns or dict keys include: - 'identity': Identity label of the detected individual. @@ -364,7 +364,7 @@ def find( silent=silent, refresh_database=refresh_database, anti_spoofing=anti_spoofing, - batched=batched + batched=batched, ) diff --git a/deepface/commons/weight_utils.py b/deepface/commons/weight_utils.py index e7944051..d6770c08 100644 --- a/deepface/commons/weight_utils.py +++ b/deepface/commons/weight_utils.py @@ -105,7 +105,7 @@ def download_all_models_in_one_shot() -> None: Download all model weights in one shot """ - # weight urls as variables + # import model weights from module here to avoid circular import issue from deepface.models.facial_recognition.VGGFace import WEIGHTS_URL as VGGFACE_WEIGHTS from deepface.models.facial_recognition.Facenet import FACENET128_WEIGHTS, FACENET512_WEIGHTS from deepface.models.facial_recognition.OpenFace import WEIGHTS_URL as OPENFACE_WEIGHTS diff --git a/deepface/modules/recognition.py b/deepface/modules/recognition.py index a4848eb0..df7068dc 100644 --- a/deepface/modules/recognition.py +++ b/deepface/modules/recognition.py @@ -78,18 +78,18 @@ def find( Returns: - results (List[pd.DataFrame] or List[List[Dict[str, Any]]]): + results (List[pd.DataFrame] or List[List[Dict[str, Any]]]): A list of pandas dataframes (if `batched=False`) or a list of dicts (if `batched=True`). Each dataframe or dict corresponds to the identity information for an individual detected in the source image. Note: If you have a large database and/or a source photo with many faces, - use `batched=True`, as it is optimized for large batch processing. - Please pay attention that when using `batched=True`, the function returns + use `batched=True`, as it is optimized for large batch processing. + Please pay attention that when using `batched=True`, the function returns a list of dicts (not a list of DataFrames), but with the same keys as the columns in the DataFrame. - + The DataFrame columns or dict keys include: - 'identity': Identity label of the detected individual. @@ -266,7 +266,7 @@ def find( align, threshold, normalization, - anti_spoofing + anti_spoofing, ) df = pd.DataFrame(representations) @@ -441,6 +441,7 @@ def __find_bulk_embeddings( return representations + def find_batched( representations: List[Dict[str, Any]], source_objs: List[Dict[str, Any]], @@ -459,11 +460,11 @@ def find_batched( The function uses batch processing for efficient computation of distances. Args: - representations (List[Dict[str, Any]]): - A list of dictionaries containing precomputed target embeddings and associated metadata. + representations (List[Dict[str, Any]]): + A list of dictionaries containing precomputed target embeddings and associated metadata. Each dictionary should have at least the key `embedding`. - - source_objs (List[Dict[str, Any]]): + + source_objs (List[Dict[str, Any]]): A list of dictionaries representing the source images to compare against the target embeddings. Each dictionary should contain: - `face`: The image data or path to the source face image. @@ -471,7 +472,7 @@ def find_batched( indicating the facial region. - Optionally, `is_real`: A boolean indicating if the face is real (used for anti-spoofing). - + model_name (str): Model for face recognition. Options: VGG-Face, Facenet, Facenet512, OpenFace, DeepFace, DeepID, Dlib, ArcFace, SFace and GhostFaceNet (default is VGG-Face). @@ -499,7 +500,7 @@ def find_batched( anti_spoofing (boolean): Flag to enable anti spoofing (default is False). Returns: - List[List[Dict[str, Any]]]: + List[List[Dict[str, Any]]]: A list where each element corresponds to a source face and contains a list of dictionaries with matching faces. """ @@ -508,27 +509,24 @@ def find_batched( metadata = set() for item in representations: - emb = item.get('embedding') + emb = item.get("embedding") if emb is not None: embeddings_list.append(emb) valid_mask.append(True) else: - embeddings_list.append(np.zeros_like(representations[0]['embedding'])) + embeddings_list.append(np.zeros_like(representations[0]["embedding"])) valid_mask.append(False) metadata.update(item.keys()) # remove embedding key from other keys - metadata.discard('embedding') + metadata.discard("embedding") metadata = list(metadata) - embeddings = np.array(embeddings_list) # (N, D) - valid_mask = np.array(valid_mask) # (N,) + embeddings = np.array(embeddings_list) # (N, D) + valid_mask = np.array(valid_mask) # (N,) - data = { - key: np.array([item.get(key, None) for item in representations]) - for key in metadata - } + data = {key: np.array([item.get(key, None) for item in representations]) for key in metadata} target_embeddings = [] source_regions = [] @@ -558,101 +556,46 @@ def find_batched( target_threshold = threshold or verification.find_threshold(model_name, distance_metric) target_thresholds.append(target_threshold) - target_embeddings = np.array(target_embeddings) # (M, D) - target_thresholds = np.array(target_thresholds) # (M,) + target_embeddings = np.array(target_embeddings) # (M, D) + target_thresholds = np.array(target_thresholds) # (M,) source_regions_arr = { - 'source_x': np.array([region['x'] for region in source_regions]), - 'source_y': np.array([region['y'] for region in source_regions]), - 'source_w': np.array([region['w'] for region in source_regions]), - 'source_h': np.array([region['h'] for region in source_regions]), + "source_x": np.array([region["x"] for region in source_regions]), + "source_y": np.array([region["y"] for region in source_regions]), + "source_w": np.array([region["w"] for region in source_regions]), + "source_h": np.array([region["h"] for region in source_regions]), } - def find_cosine_distance_batch( - embeddings: np.ndarray, target_embeddings: np.ndarray - ) -> np.ndarray: - """ - Find the cosine distances between batches of embeddings - Args: - embeddings (np.ndarray): array of shape (N, D) - target_embeddings (np.ndarray): array of shape (M, D) - Returns: - np.ndarray: distance matrix of shape (M, N) - """ - embeddings_norm = verification.l2_normalize(embeddings, axis=1) - target_embeddings_norm = verification.l2_normalize(target_embeddings, axis=1) - cosine_similarities = np.dot(target_embeddings_norm, embeddings_norm.T) - cosine_distances = 1 - cosine_similarities - return cosine_distances - - def find_euclidean_distance_batch( - embeddings: np.ndarray, target_embeddings: np.ndarray - ) -> np.ndarray: - """ - Find the Euclidean distances between batches of embeddings - Args: - embeddings (np.ndarray): array of shape (N, D) - target_embeddings (np.ndarray): array of shape (M, D) - Returns: - np.ndarray: distance matrix of shape (M, N) - """ - diff = embeddings[None, :, :] - target_embeddings[:, None, :] # (M, N, D) - distances = np.linalg.norm(diff, axis=2) # (M, N) - return distances - - def find_distance_batch( - embeddings: np.ndarray, target_embeddings: np.ndarray, distance_metric: str, - ) -> np.ndarray: - """ - Find pairwise distances between batches of embeddings using the specified distance metric - Args: - embeddings (np.ndarray): array of shape (N, D) - target_embeddings (np.ndarray): array of shape (M, D) - distance_metric (str): distance metric ('cosine', 'euclidean', 'euclidean_l2') - Returns: - np.ndarray: distance matrix of shape (M, N) - """ - if distance_metric == "cosine": - distances = find_cosine_distance_batch(embeddings, target_embeddings) - elif distance_metric == "euclidean": - distances = find_euclidean_distance_batch(embeddings, target_embeddings) - elif distance_metric == "euclidean_l2": - embeddings_norm = verification.l2_normalize(embeddings, axis=1) - target_embeddings_norm = verification.l2_normalize(target_embeddings, axis=1) - distances = find_euclidean_distance_batch(embeddings_norm, target_embeddings_norm) - else: - raise ValueError("Invalid distance_metric passed - ", distance_metric) - return np.round(distances, 6) - - distances = find_distance_batch(embeddings, target_embeddings, distance_metric) # (M, N) + distances = verification.find_distance(embeddings, target_embeddings, distance_metric) # (M, N) distances[:, ~valid_mask] = np.inf resp_obj = [] for i in range(len(target_embeddings)): - target_distances = distances[i] # (N,) + target_distances = distances[i] # (N,) target_threshold = target_thresholds[i] N = embeddings.shape[0] result_data = dict(data) - result_data.update({ - 'source_x': np.full(N, source_regions_arr['source_x'][i]), - 'source_y': np.full(N, source_regions_arr['source_y'][i]), - 'source_w': np.full(N, source_regions_arr['source_w'][i]), - 'source_h': np.full(N, source_regions_arr['source_h'][i]), - 'threshold': np.full(N, target_threshold), - 'distance': target_distances, - }) + result_data.update( + { + "source_x": np.full(N, source_regions_arr["source_x"][i]), + "source_y": np.full(N, source_regions_arr["source_y"][i]), + "source_w": np.full(N, source_regions_arr["source_w"][i]), + "source_h": np.full(N, source_regions_arr["source_h"][i]), + "threshold": np.full(N, target_threshold), + "distance": target_distances, + } + ) mask = target_distances <= target_threshold filtered_data = {key: value[mask] for key, value in result_data.items()} - sorted_indices = np.argsort(filtered_data['distance']) + sorted_indices = np.argsort(filtered_data["distance"]) sorted_data = {key: value[sorted_indices] for key, value in filtered_data.items()} - num_results = len(sorted_data['distance']) + num_results = len(sorted_data["distance"]) result_dicts = [ - {key: sorted_data[key][i] for key in sorted_data} - for i in range(num_results) + {key: sorted_data[key][i] for key in sorted_data} for i in range(num_results) ] resp_obj.append(result_dicts) return resp_obj diff --git a/deepface/modules/verification.py b/deepface/modules/verification.py index 3756db72..540b63bf 100644 --- a/deepface/modules/verification.py +++ b/deepface/modules/verification.py @@ -263,45 +263,73 @@ def __extract_faces_and_embeddings( def find_cosine_distance( source_representation: Union[np.ndarray, list], test_representation: Union[np.ndarray, list] -) -> np.float64: +) -> Union[np.float64, np.ndarray]: """ - Find cosine distance between two given vectors + Find cosine distance between two given vectors or batches of vectors. Args: - source_representation (np.ndarray or list): 1st vector - test_representation (np.ndarray or list): 2nd vector + 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 - distance (np.float64): calculated cosine distance + np.float64 or np.ndarray: Calculated cosine distance(s). + It returns a np.float64 for single embeddings and np.ndarray for batch embeddings. """ - if isinstance(source_representation, list): - source_representation = np.array(source_representation) - - if isinstance(test_representation, list): - test_representation = np.array(test_representation) - - a = np.dot(source_representation, test_representation) - b = np.linalg.norm(source_representation) - c = np.linalg.norm(test_representation) - return 1 - a / (b * c) + # Convert inputs to numpy arrays if necessary + 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) + distances = 1 - dot_product / (source_norm * test_norm) + 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) + cosine_similarities = np.dot(test_normed, source_normed.T) # (M, N) + distances = 1 - cosine_similarities + 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] -) -> 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 +) -> Union[np.float64, np.ndarray]: """ - if isinstance(source_representation, list): - source_representation = np.array(source_representation) + Find Euclidean distance between two vectors or batches of vectors. - if isinstance(test_representation, list): - test_representation = np.array(test_representation) + 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. - return np.linalg.norm(source_representation - test_representation) + Returns: + np.float64 or np.ndarray: Euclidean distance(s). + Returns a np.float64 for single embeddings and np.ndarray for batch embeddings. + """ + # Convert inputs to numpy arrays if necessary + source_representation = np.asarray(source_representation) + test_representation = np.asarray(test_representation) + + # Single embedding case (1D arrays) + if source_representation.ndim == 1 and test_representation.ndim == 1: + distances = np.linalg.norm(source_representation - test_representation) + # Batch embeddings case (2D arrays) + elif source_representation.ndim == 2 and test_representation.ndim == 2: + diff = ( + source_representation[None, :, :] - test_representation[:, None, :] + ) # (N, D) - (M, D) = (M, N, D) + distances = np.linalg.norm(diff, axis=2) # (M, N) + 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 l2_normalize( @@ -315,8 +343,8 @@ def l2_normalize( Returns: np.ndarray: l2 normalized vector """ - if isinstance(x, list): - x = np.array(x) + # Convert inputs to numpy arrays if necessary + x = np.asarray(x) norm = np.linalg.norm(x, axis=axis, keepdims=True) return x / (norm + epsilon) @@ -325,23 +353,39 @@ def find_distance( alpha_embedding: Union[np.ndarray, list], beta_embedding: Union[np.ndarray, list], distance_metric: str, -) -> np.float64: +) -> Union[np.float64, np.ndarray]: """ - Wrapper to find distance between vectors according to the given distance metric + Wrapper to find the distance between vectors based on the specified 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 + 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'). + + Returns: + np.float64 or np.ndarray: The calculated distance(s). """ + # Convert inputs to numpy arrays if necessary + alpha_embedding = np.asarray(alpha_embedding) + beta_embedding = np.asarray(beta_embedding) + + # Ensure that both embeddings are either 1D or 2D + if alpha_embedding.ndim != beta_embedding.ndim or alpha_embedding.ndim not in (1, 2): + raise ValueError( + f"Both embeddings must be either 1D or 2D, but received " + f"alpha shape: {alpha_embedding.shape}, beta shape: {beta_embedding.shape}" + ) + 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) - ) + axis = None if alpha_embedding.ndim == 1 else 1 + normalized_alpha = l2_normalize(alpha_embedding, axis=axis) + normalized_beta = l2_normalize(beta_embedding, axis=axis) + distance = find_euclidean_distance(normalized_alpha, normalized_beta) else: raise ValueError("Invalid distance_metric passed - ", distance_metric) return np.round(distance, 6)