diff --git a/deepface/DeepFace.py b/deepface/DeepFace.py index aafb297..e5e16cd 100644 --- a/deepface/DeepFace.py +++ b/deepface/DeepFace.py @@ -62,6 +62,7 @@ def verify( 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. @@ -91,6 +92,9 @@ def verify( 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 with following keys. @@ -126,6 +130,7 @@ def verify( align=align, expand_percentage=expand_percentage, normalization=normalization, + silent=silent, ) diff --git a/deepface/modules/verification.py b/deepface/modules/verification.py index dc66713..6ab5531 100644 --- a/deepface/modules/verification.py +++ b/deepface/modules/verification.py @@ -1,6 +1,6 @@ # built-in dependencies import time -from typing import Any, Dict, Union +from typing import Any, Dict, Union, List, Tuple # 3rd party dependencies import numpy as np @@ -8,11 +8,14 @@ import numpy as np # project dependencies from deepface.modules import representation, detection, modeling from deepface.models.FacialRecognition import FacialRecognition +from deepface.commons.logger import Logger + +logger = Logger(module="deepface/modules/verification.py") def verify( - img1_path: Union[str, np.ndarray], - img2_path: Union[str, np.ndarray], + 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", @@ -20,6 +23,7 @@ def verify( 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. @@ -30,10 +34,10 @@ def verify( Args: img1_path (str or np.ndarray): Path to the first image. Accepts exact image path - as a string, numpy array (BGR), or base64 encoded images. + as a string, numpy array (BGR), base64 encoded images or pre-calculated embeddings. img2_path (str or np.ndarray): Path to the second image. Accepts exact image path - as a string, numpy array (BGR), or base64 encoded images. + 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 and SFace (default is VGG-Face). @@ -54,6 +58,9 @@ def verify( 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. @@ -81,74 +88,96 @@ def verify( tic = time.time() - # -------------------------------- model: FacialRecognition = modeling.build_model(model_name) - target_size = model.input_shape + dims = model.output_shape - try: - img1_objs = detection.extract_faces( + 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: + img1_embeddings, img1_facial_areas = __extract_faces_and_embeddings( img_path=img1_path, - target_size=target_size, + model_name=model_name, detector_backend=detector_backend, - grayscale=False, 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 - try: - img2_objs = detection.extract_faces( + 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: + img2_embeddings, img2_facial_areas = __extract_faces_and_embeddings( img_path=img2_path, - target_size=target_size, + model_name=model_name, detector_backend=detector_backend, - grayscale=False, enforce_detection=enforce_detection, align=align, expand_percentage=expand_percentage, - ) - except ValueError as err: - raise ValueError("Exception while processing img2_path") from err - - img1_embeddings = [] - for img1_obj in img1_objs: - img1_embedding_obj = representation.represent( - img_path=img1_obj["face"], - model_name=model_name, - enforce_detection=enforce_detection, - detector_backend="skip", - align=align, normalization=normalization, ) - img1_embedding = img1_embedding_obj[0]["embedding"] - img1_embeddings.append(img1_embedding) - img2_embeddings = [] - for img2_obj in img2_objs: - img2_embedding_obj = representation.represent( - img_path=img2_obj["face"], - model_name=model_name, - enforce_detection=enforce_detection, - detector_backend="skip", - align=align, - normalization=normalization, - ) - img2_embedding = img2_embedding_obj[0]["embedding"] - img2_embeddings.append(img2_embedding) + no_facial_area = { + "x": None, + "y": None, + "w": None, + "h": None, + "left_eye": None, + "right_eye": None, + } distances = [] - regions = [] + 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) - regions.append((img1_objs[idx]["facial_area"], img2_objs[idy]["facial_area"])) + 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 = regions[np.argmin(distances)] + facial_areas = facial_areas[np.argmin(distances)] toc = time.time() @@ -166,6 +195,58 @@ def verify( 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 = [] + + model: FacialRecognition = modeling.build_model(model_name) + target_size = model.input_shape + + try: + img_objs = detection.extract_faces( + img_path=img_path, + target_size=target_size, + detector_backend=detector_backend, + grayscale=False, + enforce_detection=enforce_detection, + align=align, + expand_percentage=expand_percentage, + ) + except ValueError as err: + raise ValueError("Exception while processing img1_path") from err + + # 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: diff --git a/tests/test_verify.py b/tests/test_verify.py index 5d13541..ff1f9fd 100644 --- a/tests/test_verify.py +++ b/tests/test_verify.py @@ -1,3 +1,4 @@ +import pytest import cv2 from deepface import DeepFace from deepface.commons.logger import Logger @@ -100,3 +101,53 @@ def test_verify_for_preloaded_image(): res = DeepFace.verify(img1, img2) assert res["verified"] is True logger.info("✅ test verify for pre-loaded image done") + + +def test_verify_for_precalculated_embeddings(): + model_name = "Facenet" + + img1_path = "dataset/img1.jpg" + img2_path = "dataset/img2.jpg" + + img1_embedding = DeepFace.represent(img_path=img1_path, model_name=model_name)[0]["embedding"] + img2_embedding = DeepFace.represent(img_path=img2_path, model_name=model_name)[0]["embedding"] + + result = DeepFace.verify( + img1_path=img1_embedding, img2_path=img2_embedding, model_name=model_name, silent=True + ) + + assert result["verified"] is True + assert result["distance"] < result["threshold"] + assert result["model"] == model_name + + logger.info("✅ test verify for pre-calculated embeddings done") + + +def test_verify_with_precalculated_embeddings_for_incorrect_model(): + # generate embeddings with VGG (default) + img1_path = "dataset/img1.jpg" + img2_path = "dataset/img2.jpg" + img1_embedding = DeepFace.represent(img_path=img1_path)[0]["embedding"] + img2_embedding = DeepFace.represent(img_path=img2_path)[0]["embedding"] + + with pytest.raises( + ValueError, + match="embeddings of Facenet should have 128 dimensions, but it has 4096 dimensions input", + ): + _ = DeepFace.verify( + img1_path=img1_embedding, img2_path=img2_embedding, model_name="Facenet", silent=True + ) + + logger.info("✅ test verify with pre-calculated embeddings for incorrect model done") + + +def test_verify_for_broken_embeddings(): + img1_embeddings = ["a", "b", "c"] + img2_embeddings = [1, 2, 3] + + with pytest.raises( + ValueError, + match="When passing img1_path as a list, ensure that all its items are of type float.", + ): + _ = DeepFace.verify(img1_path=img1_embeddings, img2_path=img2_embeddings) + logger.info("✅ test verify for broken embeddings content is done")