diff --git a/deepface/DeepFace.py b/deepface/DeepFace.py index 30d8910..1fc95b6 100644 --- a/deepface/DeepFace.py +++ b/deepface/DeepFace.py @@ -166,7 +166,7 @@ def verify( def analyze( - img_path: Union[str, np.ndarray, IO[bytes]], + img_path: Union[str, np.ndarray, IO[bytes], List[str], List[np.ndarray], List[IO[bytes]]], actions: Union[tuple, list] = ("emotion", "age", "gender", "race"), enforce_detection: bool = True, detector_backend: str = "opencv", @@ -178,7 +178,7 @@ def analyze( """ Analyze facial attributes such as age, gender, emotion, and race in the provided image. Args: - img_path (str or np.ndarray or IO[bytes]): The exact path to the image, a numpy array + img_path (str, np.ndarray, IO[bytes], list): The exact path to the image, a numpy array in BGR format, a file object that supports at least `.read` and is opened in binary mode, or a base64 encoded image. If the source image contains multiple faces, the result will include information for each detected face. diff --git a/deepface/models/FacialRecognition.py b/deepface/models/FacialRecognition.py index 410a033..9100923 100644 --- a/deepface/models/FacialRecognition.py +++ b/deepface/models/FacialRecognition.py @@ -11,6 +11,7 @@ else: # Notice that all facial recognition models must be inherited from this class + # pylint: disable=too-few-public-methods class FacialRecognition(ABC): model: Union[Model, Any] @@ -24,11 +25,24 @@ class FacialRecognition(ABC): "You must overwrite forward method if it is not a keras model," f"but {self.model_name} not overwritten!" ) - # model.predict causes memory issue when it is called in a for loop - # embedding = model.predict(img, verbose=0)[0].tolist() - if img.shape == 4 and img.shape[0] == 1: - img = img[0] - embeddings = self.model(img, training=False).numpy() + + # predict expexts e.g. (1, 224, 224, 3) shaped inputs + if img.ndim == 3: + img = np.expand_dims(img, axis=0) + + if img.ndim == 4 and img.shape[0] == 1: + # model.predict causes memory issue when it is called in a for loop + # embedding = model.predict(img, verbose=0)[0].tolist() + embeddings = self.model(img, training=False).numpy() + elif img.ndim == 4 and img.shape[0] > 1: + embeddings = self.model.predict_on_batch(img) + else: + raise ValueError(f"Input image must be (1, X, X, 3) shaped but it is {img.shape}") + + assert isinstance( + embeddings, np.ndarray + ), f"Embeddings must be numpy array but it is {type(embeddings)}" + if embeddings.shape[0] == 1: return embeddings[0].tolist() return embeddings.tolist() diff --git a/deepface/models/facial_recognition/VGGFace.py b/deepface/models/facial_recognition/VGGFace.py index bffd0d6..b8c21c2 100644 --- a/deepface/models/facial_recognition/VGGFace.py +++ b/deepface/models/facial_recognition/VGGFace.py @@ -42,6 +42,7 @@ WEIGHTS_URL = ( "https://github.com/serengil/deepface_models/releases/download/v1.0/vgg_face_weights.h5" ) + # pylint: disable=too-few-public-methods class VggFaceClient(FacialRecognition): """ @@ -70,10 +71,7 @@ class VggFaceClient(FacialRecognition): # having normalization layer in descriptor troubles for some gpu users (e.g. issue 957, 966) # instead we are now calculating it with traditional way not with keras backend embedding = super().forward(img) - if ( - isinstance(embedding, list) and - isinstance(embedding[0], list) - ): + if isinstance(embedding, list) and len(embedding) > 0 and isinstance(embedding[0], list): embedding = verification.l2_normalize(embedding, axis=1) else: embedding = verification.l2_normalize(embedding) diff --git a/deepface/modules/demography.py b/deepface/modules/demography.py index d3ce8e6..218d541 100644 --- a/deepface/modules/demography.py +++ b/deepface/modules/demography.py @@ -1,5 +1,5 @@ # built-in dependencies -from typing import Any, Dict, List, Union +from typing import Any, Dict, List, Union, IO # 3rd party dependencies import numpy as np @@ -11,7 +11,7 @@ from deepface.models.demography import Gender, Race, Emotion def analyze( - img_path: Union[str, np.ndarray], + img_path: Union[str, np.ndarray, IO[bytes], List[str], List[np.ndarray], List[IO[bytes]]], actions: Union[tuple, list] = ("emotion", "age", "gender", "race"), enforce_detection: bool = True, detector_backend: str = "opencv", @@ -19,14 +19,14 @@ def analyze( expand_percentage: int = 0, silent: bool = False, anti_spoofing: bool = False, -) -> List[Dict[str, Any]]: +) -> Union[List[Dict[str, Any]], List[List[Dict[str, Any]]]]: """ Analyze facial attributes such as age, gender, emotion, and race in the provided image. Args: - img_path (str or np.ndarray): The exact path to the image, a numpy array in BGR format, - or a base64 encoded image. If the source image contains multiple faces, the result will - include information for each detected face. + img_path (str, np.ndarray, IO[bytes], list): The exact path to the image, + a numpy array in BGR format, or a base64 encoded image. If the source image + contains multiple faces, the result will include information for each detected face. actions (tuple): Attributes to analyze. The default is ('age', 'gender', 'emotion', 'race'). You can exclude some of these attributes from the analysis if needed. @@ -100,28 +100,28 @@ def analyze( - 'white': Confidence score for White ethnicity. """ - if isinstance(img_path, np.ndarray) and len(img_path.shape) == 4: - # Received 4-D array, which means image batch. - # Check batch dimension and process each image separately. - if img_path.shape[0] > 1: - batch_resp_obj = [] - # Execute analysis for each image in the batch. - for single_img in img_path: - # Call the analyze function for each image in the batch. - resp_obj = analyze( - img_path=single_img, - actions=actions, - enforce_detection=enforce_detection, - detector_backend=detector_backend, - align=align, - expand_percentage=expand_percentage, - silent=silent, - anti_spoofing=anti_spoofing, - ) + # batch input + if (isinstance(img_path, np.ndarray) and img_path.ndim == 4 and img_path.shape[0] > 1) or ( + isinstance(img_path, list) + ): + batch_resp_obj = [] + # Execute analysis for each image in the batch. + for single_img in img_path: + # Call the analyze function for each image in the batch. + resp_obj = analyze( + img_path=single_img, + actions=actions, + enforce_detection=enforce_detection, + detector_backend=detector_backend, + align=align, + expand_percentage=expand_percentage, + silent=silent, + anti_spoofing=anti_spoofing, + ) - # Append the response object to the batch response list. - batch_resp_obj.append(resp_obj) - return batch_resp_obj + # Append the response object to the batch response list. + batch_resp_obj.append(resp_obj) + return batch_resp_obj # if actions is passed as tuple with single item, interestingly it becomes str here if isinstance(actions, str): diff --git a/deepface/modules/representation.py b/deepface/modules/representation.py index 2be4fec..2365605 100644 --- a/deepface/modules/representation.py +++ b/deepface/modules/representation.py @@ -1,5 +1,6 @@ # built-in dependencies from typing import Any, Dict, List, Union, Optional, Sequence, IO +from collections import defaultdict # 3rd party dependencies import numpy as np @@ -161,17 +162,16 @@ def represent( # Forward pass through the model for the entire batch embeddings = model.forward(batch_images) - for idx in range(0, len(images)): - resp_obj = [] - for idy, batch_index in enumerate(batch_indexes): - if idx == batch_index: - resp_obj.append( - { - "embedding": embeddings if len(batch_images) == 1 else embeddings[idy], - "facial_area": batch_regions[idy], - "face_confidence": batch_confidences[idy], - } - ) - resp_objs.append(resp_obj) + resp_objs_dict = defaultdict(list) + for idy, batch_index in enumerate(batch_indexes): + resp_objs_dict[batch_index].append( + { + "embedding": embeddings if len(batch_images) == 1 else embeddings[idy], + "facial_area": batch_regions[idy], + "face_confidence": batch_confidences[idy], + } + ) + + resp_objs = [resp_objs_dict[idx] for idx in range(len(images))] return resp_objs[0] if len(images) == 1 else resp_objs diff --git a/tests/test_analyze.py b/tests/test_analyze.py index 6f8c996..b9ec964 100644 --- a/tests/test_analyze.py +++ b/tests/test_analyze.py @@ -16,9 +16,13 @@ detectors = ["opencv", "mtcnn"] def test_standard_analyze(): img = "dataset/img4.jpg" demography_objs = DeepFace.analyze(img, silent=True) + + # return type should be list of dict for non batch input + assert isinstance(demography_objs, list) + for demography in demography_objs: + assert isinstance(demography, dict) logger.debug(demography) - assert type(demography) == dict assert demography["age"] > 20 and demography["age"] < 40 assert demography["dominant_gender"] == "Woman" logger.info("✅ test standard analyze done") @@ -99,9 +103,13 @@ def test_analyze_for_some_actions(): def test_analyze_for_preloaded_image(): img = cv2.imread("dataset/img1.jpg") resp_objs = DeepFace.analyze(img, silent=True) + + # return type should be list of dict for non batch input + assert isinstance(resp_objs, list) + for resp_obj in resp_objs: + assert isinstance(resp_obj, dict) logger.debug(resp_obj) - assert type(resp_obj) == dict assert resp_obj["age"] > 20 and resp_obj["age"] < 40 assert resp_obj["dominant_gender"] == "Woman" @@ -127,7 +135,10 @@ def test_analyze_for_different_detectors(): results = DeepFace.analyze( img_path, actions=("gender",), detector_backend=detector, enforce_detection=False ) + # return type should be list of dict for non batch input + assert isinstance(results, list) for result in results: + assert isinstance(result, dict) logger.debug(result) # validate keys @@ -138,13 +149,63 @@ def test_analyze_for_different_detectors(): ] # validate probabilities - assert type(result) == dict if result["dominant_gender"] == "Man": assert result["gender"]["Man"] > result["gender"]["Woman"] else: assert result["gender"]["Man"] < result["gender"]["Woman"] +def test_analyze_for_batched_image_as_list_of_string(): + img_paths = ["dataset/img1.jpg", "dataset/img2.jpg", "dataset/couple.jpg"] + expected_faces = [1, 1, 2] + + demography_batch = DeepFace.analyze(img_path=img_paths, silent=True) + # return type should be list of list of dict for batch input + assert isinstance(demography_batch, list) + + # 3 image in batch, so 3 demography objects + assert len(demography_batch) == len(img_paths) + + for idx, demography_objs in enumerate(demography_batch): + assert isinstance(demography_objs, list) + assert len(demography_objs) == expected_faces[idx] + for demography_obj in demography_objs: + assert isinstance(demography_obj, dict) + + assert demography_obj["age"] > 20 and demography_obj["age"] < 40 + assert demography_obj["dominant_gender"] in ["Woman", "Man"] + + logger.info("✅ test analyze for batched image as list of string done") + + +def test_analyze_for_batched_image_as_list_of_numpy(): + img_paths = ["dataset/img1.jpg", "dataset/img2.jpg", "dataset/couple.jpg"] + expected_faces = [1, 1, 2] + + imgs = [] + for img_path in img_paths: + img = cv2.imread(img_path) + imgs.append(img) + + demography_batch = DeepFace.analyze(img_path=imgs, silent=True) + # return type should be list of list of dict for batch input + assert isinstance(demography_batch, list) + + # 3 image in batch, so 3 demography objects + assert len(demography_batch) == len(img_paths) + + for idx, demography_objs in enumerate(demography_batch): + assert isinstance(demography_objs, list) + assert len(demography_objs) == expected_faces[idx] + for demography_obj in demography_objs: + assert isinstance(demography_obj, dict) + + assert demography_obj["age"] > 20 and demography_obj["age"] < 40 + assert demography_obj["dominant_gender"] in ["Woman", "Man"] + + logger.info("✅ test analyze for batched image as list of numpy done") + + def test_analyze_for_numpy_batched_image(): img1_path = "dataset/img4.jpg" img2_path = "dataset/couple.jpg" @@ -163,14 +224,20 @@ def test_analyze_for_numpy_batched_image(): assert img.shape[0] == 2 # Check batch size. demography_batch = DeepFace.analyze(img, silent=True) + # return type should be list of list of dict for batch input + + assert isinstance(demography_batch, list) + # 2 image in batch, so 2 demography objects. assert len(demography_batch) == 2 for i, demography_objs in enumerate(demography_batch): + assert isinstance(demography_objs, list) assert len(demography_objs) == expected_num_faces[i] for demography in demography_objs: # Iterate over faces - assert isinstance(demography, dict) # Check type + assert isinstance(demography, dict) + assert demography["age"] > 20 and demography["age"] < 40 assert demography["dominant_gender"] in ["Woman", "Man"] diff --git a/tests/test_represent.py b/tests/test_represent.py index e5a7eab..247a0d4 100644 --- a/tests/test_represent.py +++ b/tests/test_represent.py @@ -183,6 +183,8 @@ def test_batched_represent_for_list_input(model_name): assert len(single_embedding_objs) == len(batched_embedding_objs[idx]) for alpha, beta in zip(single_embedding_objs, batched_embedding_objs[idx]): + assert isinstance(alpha, dict) + assert isinstance(beta, dict) assert np.allclose( alpha["embedding"], beta["embedding"], rtol=1e-2, atol=1e-2 ), "Embeddings do not match within tolerance"