From c42df046f14ea69c689764c129503ec7cf72262f Mon Sep 17 00:00:00 2001 From: Nat Lee Date: Thu, 5 Dec 2024 17:55:10 +0800 Subject: [PATCH 01/69] [update] add batch predicting for Age model --- deepface/models/demography/Age.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/deepface/models/demography/Age.py b/deepface/models/demography/Age.py index 67ab3ae..f3a21a1 100644 --- a/deepface/models/demography/Age.py +++ b/deepface/models/demography/Age.py @@ -1,3 +1,6 @@ +# stdlib dependencies +from typing import List + # 3rd party dependencies import numpy as np @@ -43,6 +46,27 @@ class ApparentAgeClient(Demography): age_predictions = self.model(img, training=False).numpy()[0, :] return find_apparent_age(age_predictions) + def predicts(self, imgs: List[np.ndarray]) -> np.ndarray: + """ + Predict apparent ages of multiple faces + Args: + imgs (List[np.ndarray]): (n, 224, 224, 3) + Returns: + apparent_ages (np.ndarray): (n,) + """ + # Convert list to numpy array + imgs_:np.ndarray = np.array(imgs) + # Remove batch dimension if exists + imgs_ = imgs_.squeeze() + # Check if the input is a single image + if len(imgs_.shape) == 3: + # Add batch dimension if not exists + imgs_ = np.expand_dims(imgs_, axis=0) + # Batch prediction + age_predictions = self.model.predict_on_batch(imgs_) + apparent_ages = np.array([find_apparent_age(age_prediction) for age_prediction in age_predictions]) + return apparent_ages + def load_model( url=WEIGHTS_URL, From a4b1b5d157704c2c75074c40ca24885ff6694bae Mon Sep 17 00:00:00 2001 From: Nat Lee Date: Thu, 5 Dec 2024 17:55:17 +0800 Subject: [PATCH 02/69] [update] add batch predicting for Gender model --- deepface/models/demography/Gender.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/deepface/models/demography/Gender.py b/deepface/models/demography/Gender.py index ad1c15e..77f0ec1 100644 --- a/deepface/models/demography/Gender.py +++ b/deepface/models/demography/Gender.py @@ -1,3 +1,6 @@ +# stdlib dependencies +from typing import List + # 3rd party dependencies import numpy as np @@ -42,6 +45,24 @@ class GenderClient(Demography): # return self.model.predict(img, verbose=0)[0, :] return self.model(img, training=False).numpy()[0, :] + def predicts(self, imgs: List[np.ndarray]) -> np.ndarray: + """ + Predict apparent ages of multiple faces + Args: + imgs (List[np.ndarray]): (n, 224, 224, 3) + Returns: + apparent_ages (np.ndarray): (n,) + """ + # Convert list to numpy array + imgs_:np.ndarray = np.array(imgs) + # Remove redundant dimensions + imgs_ = imgs_.squeeze() + # Check if the input is a single image + if len(imgs_.shape) == 3: + # Add batch dimension + imgs_ = np.expand_dims(imgs_, axis=0) + return self.model.predict_on_batch(imgs_) + def load_model( url=WEIGHTS_URL, From b55cb31e450cc960ecc31f03428b725cead7a27a Mon Sep 17 00:00:00 2001 From: Nat Lee Date: Fri, 6 Dec 2024 13:45:22 +0800 Subject: [PATCH 03/69] [fix] name of model attributes `inputs` --- deepface/models/demography/Age.py | 2 +- deepface/models/demography/Gender.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/deepface/models/demography/Age.py b/deepface/models/demography/Age.py index f3a21a1..d9d08fe 100644 --- a/deepface/models/demography/Age.py +++ b/deepface/models/demography/Age.py @@ -89,7 +89,7 @@ def load_model( # -------------------------- - age_model = Model(inputs=model.input, outputs=base_model_output) + age_model = Model(inputs=model.inputs, outputs=base_model_output) # -------------------------- diff --git a/deepface/models/demography/Gender.py b/deepface/models/demography/Gender.py index 77f0ec1..f55c571 100644 --- a/deepface/models/demography/Gender.py +++ b/deepface/models/demography/Gender.py @@ -85,7 +85,7 @@ def load_model( # -------------------------- - gender_model = Model(inputs=model.input, outputs=base_model_output) + gender_model = Model(inputs=model.inputs, outputs=base_model_output) # -------------------------- From 29c818d61e13eae06869652dd57af887eb0aa973 Mon Sep 17 00:00:00 2001 From: Nat Lee Date: Fri, 6 Dec 2024 13:55:16 +0800 Subject: [PATCH 04/69] [fix] line too long --- deepface/models/demography/Age.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/deepface/models/demography/Age.py b/deepface/models/demography/Age.py index d9d08fe..9f2052e 100644 --- a/deepface/models/demography/Age.py +++ b/deepface/models/demography/Age.py @@ -64,7 +64,9 @@ class ApparentAgeClient(Demography): imgs_ = np.expand_dims(imgs_, axis=0) # Batch prediction age_predictions = self.model.predict_on_batch(imgs_) - apparent_ages = np.array([find_apparent_age(age_prediction) for age_prediction in age_predictions]) + apparent_ages = np.array( + [find_apparent_age(age_prediction) for age_prediction in age_predictions] + ) return apparent_ages From 27e8fc9d5eddaf5fbc4ef91e364059468ffb8589 Mon Sep 17 00:00:00 2001 From: Nat Lee Date: Tue, 17 Dec 2024 13:44:01 +0800 Subject: [PATCH 05/69] [update] enhance predict methods to support single and batch inputs for Age and Gender models --- deepface/models/demography/Age.py | 50 ++++++++++++++++----------- deepface/models/demography/Gender.py | 51 +++++++++++++++++----------- 2 files changed, 63 insertions(+), 38 deletions(-) diff --git a/deepface/models/demography/Age.py b/deepface/models/demography/Age.py index 9f2052e..9c7ef3c 100644 --- a/deepface/models/demography/Age.py +++ b/deepface/models/demography/Age.py @@ -1,5 +1,5 @@ # stdlib dependencies -from typing import List +from typing import List, Union # 3rd party dependencies import numpy as np @@ -40,33 +40,45 @@ class ApparentAgeClient(Demography): self.model = load_model() self.model_name = "Age" - def predict(self, img: np.ndarray) -> np.float64: - # model.predict causes memory issue when it is called in a for loop - # age_predictions = self.model.predict(img, verbose=0)[0, :] - age_predictions = self.model(img, training=False).numpy()[0, :] - return find_apparent_age(age_predictions) - - def predicts(self, imgs: List[np.ndarray]) -> np.ndarray: + def predict(self, img: Union[np.ndarray, List[np.ndarray]]) -> Union[np.float64, np.ndarray]: """ - Predict apparent ages of multiple faces + Predict apparent age(s) for single or multiple faces Args: - imgs (List[np.ndarray]): (n, 224, 224, 3) + img: Single image as np.ndarray (224, 224, 3) or + List of images as List[np.ndarray] or + Batch of images as np.ndarray (n, 224, 224, 3) Returns: - apparent_ages (np.ndarray): (n,) + Single age as np.float64 or + Multiple ages as np.ndarray (n,) """ - # Convert list to numpy array - imgs_:np.ndarray = np.array(imgs) + # Convert to numpy array if input is list + if isinstance(img, list): + imgs = np.array(img) + else: + imgs = img + # Remove batch dimension if exists - imgs_ = imgs_.squeeze() - # Check if the input is a single image - if len(imgs_.shape) == 3: - # Add batch dimension if not exists - imgs_ = np.expand_dims(imgs_, axis=0) + imgs = imgs.squeeze() + + # Check input dimension + if len(imgs.shape) == 3: + # Single image - add batch dimension + imgs = np.expand_dims(imgs, axis=0) + is_single = True + else: + is_single = False + # Batch prediction - age_predictions = self.model.predict_on_batch(imgs_) + age_predictions = self.model.predict_on_batch(imgs) + + # Calculate apparent ages apparent_ages = np.array( [find_apparent_age(age_prediction) for age_prediction in age_predictions] ) + + # Return single value for single image + if is_single: + return apparent_ages[0] return apparent_ages diff --git a/deepface/models/demography/Gender.py b/deepface/models/demography/Gender.py index f55c571..ac8716a 100644 --- a/deepface/models/demography/Gender.py +++ b/deepface/models/demography/Gender.py @@ -1,5 +1,5 @@ # stdlib dependencies -from typing import List +from typing import List, Union # 3rd party dependencies import numpy as np @@ -40,28 +40,41 @@ class GenderClient(Demography): self.model = load_model() self.model_name = "Gender" - def predict(self, img: np.ndarray) -> np.ndarray: - # model.predict causes memory issue when it is called in a for loop - # return self.model.predict(img, verbose=0)[0, :] - return self.model(img, training=False).numpy()[0, :] - - def predicts(self, imgs: List[np.ndarray]) -> np.ndarray: + def predict(self, img: Union[np.ndarray, List[np.ndarray]]) -> Union[np.ndarray, np.ndarray]: """ - Predict apparent ages of multiple faces + Predict gender probabilities for single or multiple faces Args: - imgs (List[np.ndarray]): (n, 224, 224, 3) + img: Single image as np.ndarray (224, 224, 3) or + List of images as List[np.ndarray] or + Batch of images as np.ndarray (n, 224, 224, 3) Returns: - apparent_ages (np.ndarray): (n,) + Single prediction as np.ndarray (2,) [female_prob, male_prob] or + Multiple predictions as np.ndarray (n, 2) """ - # Convert list to numpy array - imgs_:np.ndarray = np.array(imgs) - # Remove redundant dimensions - imgs_ = imgs_.squeeze() - # Check if the input is a single image - if len(imgs_.shape) == 3: - # Add batch dimension - imgs_ = np.expand_dims(imgs_, axis=0) - return self.model.predict_on_batch(imgs_) + # Convert to numpy array if input is list + if isinstance(img, list): + imgs = np.array(img) + else: + imgs = img + + # Remove batch dimension if exists + imgs = imgs.squeeze() + + # Check input dimension + if len(imgs.shape) == 3: + # Single image - add batch dimension + imgs = np.expand_dims(imgs, axis=0) + is_single = True + else: + is_single = False + + # Batch prediction + predictions = self.model.predict_on_batch(imgs) + + # Return single prediction for single image + if is_single: + return predictions[0] + return predictions def load_model( From 38c06522a579f0e74c650444ca0083be79558bb9 Mon Sep 17 00:00:00 2001 From: Nat Lee Date: Tue, 17 Dec 2024 13:55:07 +0800 Subject: [PATCH 06/69] [update] enhance predict methods in Emotion and Race models to support single and batch inputs --- deepface/models/demography/Emotion.py | 62 ++++++++++++++++++++++++--- deepface/models/demography/Race.py | 43 +++++++++++++++++-- 2 files changed, 94 insertions(+), 11 deletions(-) diff --git a/deepface/models/demography/Emotion.py b/deepface/models/demography/Emotion.py index d2633b5..e6cb3d9 100644 --- a/deepface/models/demography/Emotion.py +++ b/deepface/models/demography/Emotion.py @@ -1,3 +1,6 @@ +# stdlib dependencies +from typing import List, Union + # 3rd party dependencies import numpy as np import cv2 @@ -43,16 +46,61 @@ class EmotionClient(Demography): self.model = load_model() self.model_name = "Emotion" - def predict(self, img: np.ndarray) -> np.ndarray: - img_gray = cv2.cvtColor(img[0], cv2.COLOR_BGR2GRAY) + def _preprocess_image(self, img: np.ndarray) -> np.ndarray: + """ + Preprocess single image for emotion detection + Args: + img: Input image (224, 224, 3) + Returns: + Preprocessed grayscale image (48, 48) + """ + img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) img_gray = cv2.resize(img_gray, (48, 48)) - img_gray = np.expand_dims(img_gray, axis=0) + return img_gray - # model.predict causes memory issue when it is called in a for loop - # emotion_predictions = self.model.predict(img_gray, verbose=0)[0, :] - emotion_predictions = self.model(img_gray, training=False).numpy()[0, :] + def predict(self, img: Union[np.ndarray, List[np.ndarray]]) -> Union[np.ndarray, np.ndarray]: + """ + Predict emotion probabilities for single or multiple faces + Args: + img: Single image as np.ndarray (224, 224, 3) or + List of images as List[np.ndarray] or + Batch of images as np.ndarray (n, 224, 224, 3) + Returns: + Single prediction as np.ndarray (n_emotions,) [emotion_probs] or + Multiple predictions as np.ndarray (n, n_emotions) + where n_emotions is the number of emotion categories + """ + # Convert to numpy array if input is list + if isinstance(img, list): + imgs = np.array(img) + else: + imgs = img + + # Remove batch dimension if exists + imgs = imgs.squeeze() + + # Check input dimension + if len(imgs.shape) == 3: + # Single image - add batch dimension + imgs = np.expand_dims(imgs, axis=0) + is_single = True + else: + is_single = False + + # Preprocess each image + processed_imgs = np.array([self._preprocess_image(img) for img in imgs]) + + # Add channel dimension for grayscale images + processed_imgs = np.expand_dims(processed_imgs, axis=-1) + + # Batch prediction + predictions = self.model.predict_on_batch(processed_imgs) + + # Return single prediction for single image + if is_single: + return predictions[0] + return predictions - return emotion_predictions def load_model( diff --git a/deepface/models/demography/Race.py b/deepface/models/demography/Race.py index 2334c8b..4537bed 100644 --- a/deepface/models/demography/Race.py +++ b/deepface/models/demography/Race.py @@ -1,3 +1,6 @@ +# stdlib dependencies +from typing import List, Union + # 3rd party dependencies import numpy as np @@ -37,10 +40,42 @@ class RaceClient(Demography): self.model = load_model() self.model_name = "Race" - def predict(self, img: np.ndarray) -> np.ndarray: - # model.predict causes memory issue when it is called in a for loop - # return self.model.predict(img, verbose=0)[0, :] - return self.model(img, training=False).numpy()[0, :] + def predict(self, img: Union[np.ndarray, List[np.ndarray]]) -> Union[np.ndarray, np.ndarray]: + """ + Predict race probabilities for single or multiple faces + Args: + img: Single image as np.ndarray (224, 224, 3) or + List of images as List[np.ndarray] or + Batch of images as np.ndarray (n, 224, 224, 3) + Returns: + Single prediction as np.ndarray (n_races,) [race_probs] or + Multiple predictions as np.ndarray (n, n_races) + where n_races is the number of race categories + """ + # Convert to numpy array if input is list + if isinstance(img, list): + imgs = np.array(img) + else: + imgs = img + + # Remove batch dimension if exists + imgs = imgs.squeeze() + + # Check input dimension + if len(imgs.shape) == 3: + # Single image - add batch dimension + imgs = np.expand_dims(imgs, axis=0) + is_single = True + else: + is_single = False + + # Batch prediction + predictions = self.model.predict_on_batch(imgs) + + # Return single prediction for single image + if is_single: + return predictions[0] + return predictions def load_model( From b9418eb46fa68f13d1e3be40a3156d1b030008d7 Mon Sep 17 00:00:00 2001 From: Nat Lee Date: Tue, 17 Dec 2024 14:02:30 +0800 Subject: [PATCH 07/69] [fix] `input` to `inputs` --- deepface/models/demography/Race.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deepface/models/demography/Race.py b/deepface/models/demography/Race.py index 4537bed..cec6aaa 100644 --- a/deepface/models/demography/Race.py +++ b/deepface/models/demography/Race.py @@ -97,7 +97,7 @@ def load_model( # -------------------------- - race_model = Model(inputs=model.input, outputs=base_model_output) + race_model = Model(inputs=model.inputs, outputs=base_model_output) # -------------------------- From d992428d65c894c9eb9a68ae41cf455893d62369 Mon Sep 17 00:00:00 2001 From: Nat Lee Date: Tue, 17 Dec 2024 14:41:18 +0800 Subject: [PATCH 08/69] [update] embed into deepface module --- deepface/models/demography/Age.py | 11 +- deepface/models/demography/Emotion.py | 12 +-- deepface/models/demography/Gender.py | 11 +- deepface/models/demography/Race.py | 11 +- deepface/modules/demography.py | 148 +++++++++++++++----------- 5 files changed, 94 insertions(+), 99 deletions(-) diff --git a/deepface/models/demography/Age.py b/deepface/models/demography/Age.py index 9c7ef3c..d470cff 100644 --- a/deepface/models/demography/Age.py +++ b/deepface/models/demography/Age.py @@ -40,7 +40,7 @@ class ApparentAgeClient(Demography): self.model = load_model() self.model_name = "Age" - def predict(self, img: Union[np.ndarray, List[np.ndarray]]) -> Union[np.float64, np.ndarray]: + def predict(self, img: Union[np.ndarray, List[np.ndarray]]) -> np.ndarray: """ Predict apparent age(s) for single or multiple faces Args: @@ -48,8 +48,7 @@ class ApparentAgeClient(Demography): List of images as List[np.ndarray] or Batch of images as np.ndarray (n, 224, 224, 3) Returns: - Single age as np.float64 or - Multiple ages as np.ndarray (n,) + np.ndarray (n,) """ # Convert to numpy array if input is list if isinstance(img, list): @@ -64,9 +63,6 @@ class ApparentAgeClient(Demography): if len(imgs.shape) == 3: # Single image - add batch dimension imgs = np.expand_dims(imgs, axis=0) - is_single = True - else: - is_single = False # Batch prediction age_predictions = self.model.predict_on_batch(imgs) @@ -76,9 +72,6 @@ class ApparentAgeClient(Demography): [find_apparent_age(age_prediction) for age_prediction in age_predictions] ) - # Return single value for single image - if is_single: - return apparent_ages[0] return apparent_ages diff --git a/deepface/models/demography/Emotion.py b/deepface/models/demography/Emotion.py index e6cb3d9..065795e 100644 --- a/deepface/models/demography/Emotion.py +++ b/deepface/models/demography/Emotion.py @@ -58,7 +58,7 @@ class EmotionClient(Demography): img_gray = cv2.resize(img_gray, (48, 48)) return img_gray - def predict(self, img: Union[np.ndarray, List[np.ndarray]]) -> Union[np.ndarray, np.ndarray]: + def predict(self, img: Union[np.ndarray, List[np.ndarray]]) -> np.ndarray: """ Predict emotion probabilities for single or multiple faces Args: @@ -66,8 +66,7 @@ class EmotionClient(Demography): List of images as List[np.ndarray] or Batch of images as np.ndarray (n, 224, 224, 3) Returns: - Single prediction as np.ndarray (n_emotions,) [emotion_probs] or - Multiple predictions as np.ndarray (n, n_emotions) + np.ndarray (n, n_emotions) where n_emotions is the number of emotion categories """ # Convert to numpy array if input is list @@ -83,9 +82,6 @@ class EmotionClient(Demography): if len(imgs.shape) == 3: # Single image - add batch dimension imgs = np.expand_dims(imgs, axis=0) - is_single = True - else: - is_single = False # Preprocess each image processed_imgs = np.array([self._preprocess_image(img) for img in imgs]) @@ -96,13 +92,9 @@ class EmotionClient(Demography): # Batch prediction predictions = self.model.predict_on_batch(processed_imgs) - # Return single prediction for single image - if is_single: - return predictions[0] return predictions - def load_model( url=WEIGHTS_URL, ) -> Sequential: diff --git a/deepface/models/demography/Gender.py b/deepface/models/demography/Gender.py index ac8716a..23fd69b 100644 --- a/deepface/models/demography/Gender.py +++ b/deepface/models/demography/Gender.py @@ -40,7 +40,7 @@ class GenderClient(Demography): self.model = load_model() self.model_name = "Gender" - def predict(self, img: Union[np.ndarray, List[np.ndarray]]) -> Union[np.ndarray, np.ndarray]: + def predict(self, img: Union[np.ndarray, List[np.ndarray]]) -> np.ndarray: """ Predict gender probabilities for single or multiple faces Args: @@ -48,8 +48,7 @@ class GenderClient(Demography): List of images as List[np.ndarray] or Batch of images as np.ndarray (n, 224, 224, 3) Returns: - Single prediction as np.ndarray (2,) [female_prob, male_prob] or - Multiple predictions as np.ndarray (n, 2) + np.ndarray (n, 2) """ # Convert to numpy array if input is list if isinstance(img, list): @@ -64,16 +63,10 @@ class GenderClient(Demography): if len(imgs.shape) == 3: # Single image - add batch dimension imgs = np.expand_dims(imgs, axis=0) - is_single = True - else: - is_single = False # Batch prediction predictions = self.model.predict_on_batch(imgs) - # Return single prediction for single image - if is_single: - return predictions[0] return predictions diff --git a/deepface/models/demography/Race.py b/deepface/models/demography/Race.py index cec6aaa..dc4a788 100644 --- a/deepface/models/demography/Race.py +++ b/deepface/models/demography/Race.py @@ -40,7 +40,7 @@ class RaceClient(Demography): self.model = load_model() self.model_name = "Race" - def predict(self, img: Union[np.ndarray, List[np.ndarray]]) -> Union[np.ndarray, np.ndarray]: + def predict(self, img: Union[np.ndarray, List[np.ndarray]]) -> np.ndarray: """ Predict race probabilities for single or multiple faces Args: @@ -48,8 +48,7 @@ class RaceClient(Demography): List of images as List[np.ndarray] or Batch of images as np.ndarray (n, 224, 224, 3) Returns: - Single prediction as np.ndarray (n_races,) [race_probs] or - Multiple predictions as np.ndarray (n, n_races) + np.ndarray (n, n_races) where n_races is the number of race categories """ # Convert to numpy array if input is list @@ -65,16 +64,10 @@ class RaceClient(Demography): if len(imgs.shape) == 3: # Single image - add batch dimension imgs = np.expand_dims(imgs, axis=0) - is_single = True - else: - is_single = False # Batch prediction predictions = self.model.predict_on_batch(imgs) - # Return single prediction for single image - if is_single: - return predictions[0] return predictions diff --git a/deepface/modules/demography.py b/deepface/modules/demography.py index b68314b..4c58314 100644 --- a/deepface/modules/demography.py +++ b/deepface/modules/demography.py @@ -9,7 +9,7 @@ from tqdm import tqdm from deepface.modules import modeling, detection, preprocessing from deepface.models.demography import Gender, Race, Emotion - +# pylint: disable=trailing-whitespace def analyze( img_path: Union[str, np.ndarray], actions: Union[tuple, list] = ("emotion", "age", "gender", "race"), @@ -130,83 +130,107 @@ def analyze( anti_spoofing=anti_spoofing, ) - for img_obj in img_objs: - if anti_spoofing is True and img_obj.get("is_real", True) is False: - raise ValueError("Spoof detected in the given image.") + # Anti-spoofing check + if anti_spoofing: + for img_obj in img_objs: + if img_obj.get("is_real", True) is False: + raise ValueError("Spoof detected in the given image.") + # Prepare the input for the model + valid_faces = [] + face_regions = [] + face_confidences = [] + + for img_obj in img_objs: + # Extract the face content img_content = img_obj["face"] - img_region = img_obj["facial_area"] - img_confidence = img_obj["confidence"] + # Check if the face content is empty if img_content.shape[0] == 0 or img_content.shape[1] == 0: continue - # rgb to bgr + # Convert the image to RGB format from BGR img_content = img_content[:, :, ::-1] - - # resize input image + # Resize the image to the target size for the model img_content = preprocessing.resize_image(img=img_content, target_size=(224, 224)) - obj = {} - # facial attribute analysis - pbar = tqdm( - range(0, len(actions)), - desc="Finding actions", - disable=silent if len(actions) > 1 else True, - ) - for index in pbar: - action = actions[index] - pbar.set_description(f"Action: {action}") + valid_faces.append(img_content) + face_regions.append(img_obj["facial_area"]) + face_confidences.append(img_obj["confidence"]) - if action == "emotion": - emotion_predictions = modeling.build_model( - task="facial_attribute", model_name="Emotion" - ).predict(img_content) - sum_of_predictions = emotion_predictions.sum() + # If no valid faces are found, return an empty list + if not valid_faces: + return [] - obj["emotion"] = {} + # Convert the list of valid faces to a numpy array + faces_array = np.array(valid_faces) + resp_objects = [{} for _ in range(len(valid_faces))] + + # For each action, predict the corresponding attribute + pbar = tqdm( + range(0, len(actions)), + desc="Finding actions", + disable=silent if len(actions) > 1 else True, + ) + + for index in pbar: + action = actions[index] + pbar.set_description(f"Action: {action}") + + if action == "emotion": + # Build the emotion model + model = modeling.build_model(task="facial_attribute", model_name="Emotion") + emotion_predictions = model.predict(faces_array) + + for idx, predictions in enumerate(emotion_predictions): + sum_of_predictions = predictions.sum() + resp_objects[idx]["emotion"] = {} + for i, emotion_label in enumerate(Emotion.labels): - emotion_prediction = 100 * emotion_predictions[i] / sum_of_predictions - obj["emotion"][emotion_label] = emotion_prediction + emotion_prediction = 100 * predictions[i] / sum_of_predictions + resp_objects[idx]["emotion"][emotion_label] = emotion_prediction + + resp_objects[idx]["dominant_emotion"] = Emotion.labels[np.argmax(predictions)] - obj["dominant_emotion"] = Emotion.labels[np.argmax(emotion_predictions)] + elif action == "age": + # Build the age model + model = modeling.build_model(task="facial_attribute", model_name="Age") + age_predictions = model.predict(faces_array) + + for idx, age in enumerate(age_predictions): + resp_objects[idx]["age"] = int(age) - elif action == "age": - apparent_age = modeling.build_model( - task="facial_attribute", model_name="Age" - ).predict(img_content) - # int cast is for exception - object of type 'float32' is not JSON serializable - obj["age"] = int(apparent_age) - - elif action == "gender": - gender_predictions = modeling.build_model( - task="facial_attribute", model_name="Gender" - ).predict(img_content) - obj["gender"] = {} + elif action == "gender": + # Build the gender model + model = modeling.build_model(task="facial_attribute", model_name="Gender") + gender_predictions = model.predict(faces_array) + + for idx, predictions in enumerate(gender_predictions): + resp_objects[idx]["gender"] = {} + for i, gender_label in enumerate(Gender.labels): - gender_prediction = 100 * gender_predictions[i] - obj["gender"][gender_label] = gender_prediction + gender_prediction = 100 * predictions[i] + resp_objects[idx]["gender"][gender_label] = gender_prediction + + resp_objects[idx]["dominant_gender"] = Gender.labels[np.argmax(predictions)] - obj["dominant_gender"] = Gender.labels[np.argmax(gender_predictions)] - - elif action == "race": - race_predictions = modeling.build_model( - task="facial_attribute", model_name="Race" - ).predict(img_content) - sum_of_predictions = race_predictions.sum() - - obj["race"] = {} + elif action == "race": + # Build the race model + model = modeling.build_model(task="facial_attribute", model_name="Race") + race_predictions = model.predict(faces_array) + + for idx, predictions in enumerate(race_predictions): + sum_of_predictions = predictions.sum() + resp_objects[idx]["race"] = {} + for i, race_label in enumerate(Race.labels): - race_prediction = 100 * race_predictions[i] / sum_of_predictions - obj["race"][race_label] = race_prediction + race_prediction = 100 * predictions[i] / sum_of_predictions + resp_objects[idx]["race"][race_label] = race_prediction + + resp_objects[idx]["dominant_race"] = Race.labels[np.argmax(predictions)] - obj["dominant_race"] = Race.labels[np.argmax(race_predictions)] - - # ----------------------------- - # mention facial areas - obj["region"] = img_region - # include image confidence - obj["face_confidence"] = img_confidence - - resp_objects.append(obj) + # Add the face region and confidence to the response objects + for idx, resp_obj in enumerate(resp_objects): + resp_obj["region"] = face_regions[idx] + resp_obj["face_confidence"] = face_confidences[idx] return resp_objects From e96ede3dedbb550f8302e63c23035cf093825102 Mon Sep 17 00:00:00 2001 From: Nat Lee Date: Tue, 17 Dec 2024 15:27:31 +0800 Subject: [PATCH 09/69] [update] add multiple faces testing --- tests/test_analyze.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/test_analyze.py b/tests/test_analyze.py index bad4426..976952b 100644 --- a/tests/test_analyze.py +++ b/tests/test_analyze.py @@ -135,3 +135,15 @@ def test_analyze_for_different_detectors(): assert result["gender"]["Man"] > result["gender"]["Woman"] else: assert result["gender"]["Man"] < result["gender"]["Woman"] + +def test_analyze_for_multiple_faces(): + img = "dataset/img4.jpg" + # Copy and combine the same image to create multiple faces + img = cv2.imread(img) + img = cv2.hconcat([img, img]) + demography_objs = DeepFace.analyze(img, silent=True) + for demography in demography_objs: + logger.debug(demography) + assert demography["age"] > 20 and demography["age"] < 40 + assert demography["dominant_gender"] == "Woman" + logger.info("✅ test analyze for multiple faces done") From ffbba7fe83c81c880ace6724d91b008428cb6b1f Mon Sep 17 00:00:00 2001 From: h-alice Date: Tue, 31 Dec 2024 14:06:33 +0800 Subject: [PATCH 10/69] Change base class's predict signature. --- deepface/models/Demography.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deepface/models/Demography.py b/deepface/models/Demography.py index ad93920..1dcef41 100644 --- a/deepface/models/Demography.py +++ b/deepface/models/Demography.py @@ -1,4 +1,4 @@ -from typing import Union +from typing import Union, List from abc import ABC, abstractmethod import numpy as np from deepface.commons import package_utils @@ -18,5 +18,5 @@ class Demography(ABC): model_name: str @abstractmethod - def predict(self, img: np.ndarray) -> Union[np.ndarray, np.float64]: + def predict(self, img: Union[np.ndarray, List[np.ndarray]]) -> Union[np.ndarray, np.float64]: pass From edcef02511d6c789734842693e787655951fbdb4 Mon Sep 17 00:00:00 2001 From: Nat Lee Date: Tue, 31 Dec 2024 17:30:28 +0800 Subject: [PATCH 11/69] [update] remove dummy functions --- deepface/models/demography/Age.py | 26 -------------------------- deepface/models/demography/Gender.py | 21 --------------------- 2 files changed, 47 deletions(-) diff --git a/deepface/models/demography/Age.py b/deepface/models/demography/Age.py index 236bbdf..57ffbcf 100644 --- a/deepface/models/demography/Age.py +++ b/deepface/models/demography/Age.py @@ -75,32 +75,6 @@ class ApparentAgeClient(Demography): return apparent_ages - - def predicts(self, imgs: List[np.ndarray]) -> np.ndarray: - """ - Predict apparent ages of multiple faces - Args: - imgs (List[np.ndarray]): (n, 224, 224, 3) - Returns: - apparent_ages (np.ndarray): (n,) - """ - # Convert list to numpy array - imgs_:np.ndarray = np.array(imgs) - # Remove batch dimension if exists - imgs_ = imgs_.squeeze() - # Check if the input is a single image - if len(imgs_.shape) == 3: - # Add batch dimension if not exists - imgs_ = np.expand_dims(imgs_, axis=0) - # Batch prediction - age_predictions = self.model.predict_on_batch(imgs_) - apparent_ages = np.array( - [find_apparent_age(age_prediction) for age_prediction in age_predictions] - ) - return apparent_ages - - - def load_model( url=WEIGHTS_URL, ) -> Model: diff --git a/deepface/models/demography/Gender.py b/deepface/models/demography/Gender.py index 2ef4cc2..1c06a76 100644 --- a/deepface/models/demography/Gender.py +++ b/deepface/models/demography/Gender.py @@ -70,27 +70,6 @@ class GenderClient(Demography): return predictions - - def predicts(self, imgs: List[np.ndarray]) -> np.ndarray: - """ - Predict apparent ages of multiple faces - Args: - imgs (List[np.ndarray]): (n, 224, 224, 3) - Returns: - apparent_ages (np.ndarray): (n,) - """ - # Convert list to numpy array - imgs_:np.ndarray = np.array(imgs) - # Remove redundant dimensions - imgs_ = imgs_.squeeze() - # Check if the input is a single image - if len(imgs_.shape) == 3: - # Add batch dimension - imgs_ = np.expand_dims(imgs_, axis=0) - return self.model.predict_on_batch(imgs_) - - - def load_model( url=WEIGHTS_URL, ) -> Model: From 472f146ecc1cbda4f0042997aeab82e91e5e7e58 Mon Sep 17 00:00:00 2001 From: h-alice Date: Fri, 3 Jan 2025 10:24:43 +0800 Subject: [PATCH 12/69] Avoid recreating `resp_objects`. As the following code review comment suggested: https://github.com/serengil/deepface/pull/1396#discussion_r1900015959 --- deepface/modules/demography.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/deepface/modules/demography.py b/deepface/modules/demography.py index c199cd5..b10a33f 100644 --- a/deepface/modules/demography.py +++ b/deepface/modules/demography.py @@ -163,7 +163,11 @@ def analyze( # Convert the list of valid faces to a numpy array faces_array = np.array(valid_faces) - resp_objects = [{} for _ in range(len(valid_faces))] + + # Create placeholder response objects for each face + for _ in range(len(valid_faces)): + resp_objects.append({}) + # For each action, predict the corresponding attribute pbar = tqdm( From b69dcfcca7f4cf58aa65fd9c40d00993be90aa4b Mon Sep 17 00:00:00 2001 From: h-alice Date: Fri, 3 Jan 2025 10:57:59 +0800 Subject: [PATCH 13/69] Engineering stuff, remove redundant code. As mentioned: https://github.com/serengil/deepface/pull/1396#discussion_r1900017766 --- deepface/models/Demography.py | 26 ++++++++++++++++++++++++++ deepface/models/demography/Age.py | 15 ++------------- deepface/models/demography/Emotion.py | 15 ++------------- deepface/models/demography/Gender.py | 15 ++------------- deepface/models/demography/Race.py | 15 ++------------- 5 files changed, 34 insertions(+), 52 deletions(-) diff --git a/deepface/models/Demography.py b/deepface/models/Demography.py index 1dcef41..e73fe65 100644 --- a/deepface/models/Demography.py +++ b/deepface/models/Demography.py @@ -20,3 +20,29 @@ class Demography(ABC): @abstractmethod def predict(self, img: Union[np.ndarray, List[np.ndarray]]) -> Union[np.ndarray, np.float64]: pass + + def _preprocess_batch_or_single_input(self, img: Union[np.ndarray, List[np.ndarray]]) -> np.ndarray: + + """ + Preprocess single or batch of images, return as 4-D numpy array. + Args: + img: Single image as np.ndarray (224, 224, 3) or + List of images as List[np.ndarray] or + Batch of images as np.ndarray (n, 224, 224, 3) + Returns: + Four-dimensional numpy array (n, 224, 224, 3) + """ + if isinstance(img, list): # Convert from list to image batch. + image_batch = np.array(img) + else: + image_batch = img + + # Remove batch dimension in advance if exists + image_batch = image_batch.squeeze() + + # Check input dimension + if len(image_batch.shape) == 3: + # Single image - add batch dimension + imgs = np.expand_dims(image_batch, axis=0) + + return image_batch diff --git a/deepface/models/demography/Age.py b/deepface/models/demography/Age.py index 57ffbcf..5bc409f 100644 --- a/deepface/models/demography/Age.py +++ b/deepface/models/demography/Age.py @@ -51,19 +51,8 @@ class ApparentAgeClient(Demography): Returns: np.ndarray (n,) """ - # Convert to numpy array if input is list - if isinstance(img, list): - imgs = np.array(img) - else: - imgs = img - - # Remove batch dimension if exists - imgs = imgs.squeeze() - - # Check input dimension - if len(imgs.shape) == 3: - # Single image - add batch dimension - imgs = np.expand_dims(imgs, axis=0) + # Preprocessing input image or image list. + imgs = self._preprocess_batch_or_single_input(img) # Batch prediction age_predictions = self.model.predict_on_batch(imgs) diff --git a/deepface/models/demography/Emotion.py b/deepface/models/demography/Emotion.py index 065795e..10e5115 100644 --- a/deepface/models/demography/Emotion.py +++ b/deepface/models/demography/Emotion.py @@ -69,19 +69,8 @@ class EmotionClient(Demography): np.ndarray (n, n_emotions) where n_emotions is the number of emotion categories """ - # Convert to numpy array if input is list - if isinstance(img, list): - imgs = np.array(img) - else: - imgs = img - - # Remove batch dimension if exists - imgs = imgs.squeeze() - - # Check input dimension - if len(imgs.shape) == 3: - # Single image - add batch dimension - imgs = np.expand_dims(imgs, axis=0) + # Preprocessing input image or image list. + imgs = self._preprocess_batch_or_single_input(img) # Preprocess each image processed_imgs = np.array([self._preprocess_image(img) for img in imgs]) diff --git a/deepface/models/demography/Gender.py b/deepface/models/demography/Gender.py index 1c06a76..f7e705e 100644 --- a/deepface/models/demography/Gender.py +++ b/deepface/models/demography/Gender.py @@ -51,19 +51,8 @@ class GenderClient(Demography): Returns: np.ndarray (n, 2) """ - # Convert to numpy array if input is list - if isinstance(img, list): - imgs = np.array(img) - else: - imgs = img - - # Remove batch dimension if exists - imgs = imgs.squeeze() - - # Check input dimension - if len(imgs.shape) == 3: - # Single image - add batch dimension - imgs = np.expand_dims(imgs, axis=0) + # Preprocessing input image or image list. + imgs = self._preprocess_batch_or_single_input(img) # Batch prediction predictions = self.model.predict_on_batch(imgs) diff --git a/deepface/models/demography/Race.py b/deepface/models/demography/Race.py index dc4a788..0c6a2f0 100644 --- a/deepface/models/demography/Race.py +++ b/deepface/models/demography/Race.py @@ -51,19 +51,8 @@ class RaceClient(Demography): np.ndarray (n, n_races) where n_races is the number of race categories """ - # Convert to numpy array if input is list - if isinstance(img, list): - imgs = np.array(img) - else: - imgs = img - - # Remove batch dimension if exists - imgs = imgs.squeeze() - - # Check input dimension - if len(imgs.shape) == 3: - # Single image - add batch dimension - imgs = np.expand_dims(imgs, axis=0) + # Preprocessing input image or image list. + imgs = self._preprocess_batch_or_single_input(img) # Batch prediction predictions = self.model.predict_on_batch(imgs) From 0f65a8765ee7353d17ec850c725e096993f86e33 Mon Sep 17 00:00:00 2001 From: h-alice Date: Fri, 3 Jan 2025 11:01:12 +0800 Subject: [PATCH 14/69] Add assertion to verify length of analyzed objects. As mentioned: https://github.com/serengil/deepface/pull/1396#discussion_r1900012703 --- tests/test_analyze.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_analyze.py b/tests/test_analyze.py index 976952b..27805f5 100644 --- a/tests/test_analyze.py +++ b/tests/test_analyze.py @@ -142,6 +142,7 @@ def test_analyze_for_multiple_faces(): img = cv2.imread(img) img = cv2.hconcat([img, img]) demography_objs = DeepFace.analyze(img, silent=True) + assert len(demography_objs) == 2 for demography in demography_objs: logger.debug(demography) assert demography["age"] > 20 and demography["age"] < 40 From bb820a7ef4ba5c146089f6086035474d3b1097ed Mon Sep 17 00:00:00 2001 From: Nat Lee Date: Fri, 3 Jan 2025 14:53:59 +0800 Subject: [PATCH 15/69] [update] one-line checking --- deepface/modules/demography.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/deepface/modules/demography.py b/deepface/modules/demography.py index b10a33f..6b6382f 100644 --- a/deepface/modules/demography.py +++ b/deepface/modules/demography.py @@ -131,10 +131,8 @@ def analyze( ) # Anti-spoofing check - if anti_spoofing: - for img_obj in img_objs: - if img_obj.get("is_real", True) is False: - raise ValueError("Spoof detected in the given image.") + if anti_spoofing and any(img_obj.get("is_real", True) is False for img_obj in img_objs): + raise ValueError("Spoof detected in the given image.") # Prepare the input for the model valid_faces = [] From e1822851a56dc9a8e5cf7e8264ad2b9e2ed801ee Mon Sep 17 00:00:00 2001 From: h-alice Date: Fri, 3 Jan 2025 16:22:55 +0800 Subject: [PATCH 16/69] Fix: Image batch dimension not expanded. --- deepface/models/Demography.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deepface/models/Demography.py b/deepface/models/Demography.py index e73fe65..d240f1e 100644 --- a/deepface/models/Demography.py +++ b/deepface/models/Demography.py @@ -43,6 +43,6 @@ class Demography(ABC): # Check input dimension if len(image_batch.shape) == 3: # Single image - add batch dimension - imgs = np.expand_dims(image_batch, axis=0) + image_batch = np.expand_dims(image_batch, axis=0) return image_batch From 5747d9648b731e23586db344e2dc863c012438df Mon Sep 17 00:00:00 2001 From: h-alice Date: Mon, 6 Jan 2025 11:37:19 +0800 Subject: [PATCH 17/69] Predictor. --- deepface/models/Demography.py | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/deepface/models/Demography.py b/deepface/models/Demography.py index d240f1e..bf4ea6c 100644 --- a/deepface/models/Demography.py +++ b/deepface/models/Demography.py @@ -21,14 +21,37 @@ class Demography(ABC): def predict(self, img: Union[np.ndarray, List[np.ndarray]]) -> Union[np.ndarray, np.float64]: pass + def _predict_internal(self, img_batch: np.ndarray) -> np.ndarray: + """ + Predict for single image or batched images. + This method uses legacy method while receiving single image as input. + And switch to batch prediction if receives batched images. + + Args: + img_batch: Batch of images as np.ndarray (n, 224, 224, 3), with n >= 1. + """ + if not self.model_name: # Check if called from derived class + raise NotImplementedError("virtual method must not be called directly") + + assert img_batch.ndim == 4, "expected 4-dimensional tensor input" + + if img_batch.shape[0] == 1: # Single image + img_batch = img_batch.squeeze(0) # Remove batch dimension + predict_result = self.model(img_batch, training=False).numpy()[0, :] + predict_result = np.expand_dims(predict_result, axis=0) # Add batch dimension + return predict_result + else: # Batch of images + return self.model.predict_on_batch(img_batch) + def _preprocess_batch_or_single_input(self, img: Union[np.ndarray, List[np.ndarray]]) -> np.ndarray: """ Preprocess single or batch of images, return as 4-D numpy array. Args: img: Single image as np.ndarray (224, 224, 3) or - List of images as List[np.ndarray] or - Batch of images as np.ndarray (n, 224, 224, 3) + List of images as List[np.ndarray] or + Batch of images as np.ndarray (n, 224, 224, 3) + NOTE: If the imput is grayscale, then there's no channel dimension. Returns: Four-dimensional numpy array (n, 224, 224, 3) """ From 5a1881492f17deb1f4b11136bd4651ed1ebcf194 Mon Sep 17 00:00:00 2001 From: h-alice Date: Mon, 6 Jan 2025 11:51:22 +0800 Subject: [PATCH 18/69] Add comment. --- deepface/models/Demography.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/deepface/models/Demography.py b/deepface/models/Demography.py index bf4ea6c..64e56e2 100644 --- a/deepface/models/Demography.py +++ b/deepface/models/Demography.py @@ -37,8 +37,7 @@ class Demography(ABC): if img_batch.shape[0] == 1: # Single image img_batch = img_batch.squeeze(0) # Remove batch dimension - predict_result = self.model(img_batch, training=False).numpy()[0, :] - predict_result = np.expand_dims(predict_result, axis=0) # Add batch dimension + predict_result = self.model(img_batch, training=False).numpy()[0, :] # Predict with legacy method. return predict_result else: # Batch of images return self.model.predict_on_batch(img_batch) From 72b94d11de1d40f23dc23d63690c4641ae5c33a8 Mon Sep 17 00:00:00 2001 From: h-alice Date: Mon, 6 Jan 2025 11:51:49 +0800 Subject: [PATCH 19/69] Add new predictor. --- deepface/models/demography/Age.py | 4 ++-- deepface/models/demography/Emotion.py | 4 ++-- deepface/models/demography/Gender.py | 4 ++-- deepface/models/demography/Race.py | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/deepface/models/demography/Age.py b/deepface/models/demography/Age.py index 5bc409f..7284aad 100644 --- a/deepface/models/demography/Age.py +++ b/deepface/models/demography/Age.py @@ -54,8 +54,8 @@ class ApparentAgeClient(Demography): # Preprocessing input image or image list. imgs = self._preprocess_batch_or_single_input(img) - # Batch prediction - age_predictions = self.model.predict_on_batch(imgs) + # Prediction + age_predictions = self._predict_internal(imgs) # Calculate apparent ages apparent_ages = np.array( diff --git a/deepface/models/demography/Emotion.py b/deepface/models/demography/Emotion.py index 10e5115..e6ebf5f 100644 --- a/deepface/models/demography/Emotion.py +++ b/deepface/models/demography/Emotion.py @@ -78,8 +78,8 @@ class EmotionClient(Demography): # Add channel dimension for grayscale images processed_imgs = np.expand_dims(processed_imgs, axis=-1) - # Batch prediction - predictions = self.model.predict_on_batch(processed_imgs) + # Prediction + predictions = self._predict_internal(processed_imgs) return predictions diff --git a/deepface/models/demography/Gender.py b/deepface/models/demography/Gender.py index f7e705e..b6a3ef1 100644 --- a/deepface/models/demography/Gender.py +++ b/deepface/models/demography/Gender.py @@ -54,8 +54,8 @@ class GenderClient(Demography): # Preprocessing input image or image list. imgs = self._preprocess_batch_or_single_input(img) - # Batch prediction - predictions = self.model.predict_on_batch(imgs) + # Prediction + predictions = self._predict_internal(imgs) return predictions diff --git a/deepface/models/demography/Race.py b/deepface/models/demography/Race.py index 0c6a2f0..eae5154 100644 --- a/deepface/models/demography/Race.py +++ b/deepface/models/demography/Race.py @@ -54,8 +54,8 @@ class RaceClient(Demography): # Preprocessing input image or image list. imgs = self._preprocess_batch_or_single_input(img) - # Batch prediction - predictions = self.model.predict_on_batch(imgs) + # Prediction + predictions = self._predict_internal(imgs) return predictions From 85e2d8d863abf11d2d3cb3bb98c1702857a7aeb2 Mon Sep 17 00:00:00 2001 From: NatLee Date: Tue, 7 Jan 2025 04:05:11 +0800 Subject: [PATCH 20/69] [update] modify comment for multi models --- deepface/models/Demography.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deepface/models/Demography.py b/deepface/models/Demography.py index 64e56e2..d0a00f1 100644 --- a/deepface/models/Demography.py +++ b/deepface/models/Demography.py @@ -28,7 +28,7 @@ class Demography(ABC): And switch to batch prediction if receives batched images. Args: - img_batch: Batch of images as np.ndarray (n, 224, 224, 3), with n >= 1. + img_batch: Batch of images as np.ndarray (n, x, y, c), with n >= 1, x = image width, y = image height, c = channel """ if not self.model_name: # Check if called from derived class raise NotImplementedError("virtual method must not be called directly") From ba0d0c5bb66587fd67b7407e7bfed5e5a879b98e Mon Sep 17 00:00:00 2001 From: NatLee Date: Tue, 7 Jan 2025 04:54:17 +0800 Subject: [PATCH 21/69] [update] make process to one-line --- deepface/models/demography/Emotion.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/deepface/models/demography/Emotion.py b/deepface/models/demography/Emotion.py index e6ebf5f..caf862b 100644 --- a/deepface/models/demography/Emotion.py +++ b/deepface/models/demography/Emotion.py @@ -72,13 +72,11 @@ class EmotionClient(Demography): # Preprocessing input image or image list. imgs = self._preprocess_batch_or_single_input(img) - # Preprocess each image - processed_imgs = np.array([self._preprocess_image(img) for img in imgs]) - - # Add channel dimension for grayscale images - processed_imgs = np.expand_dims(processed_imgs, axis=-1) + # Preprocess each image and add channel dimension for grayscale images + processed_imgs = np.expand_dims(np.array([self._preprocess_image(img) for img in imgs]), axis=0) # Prediction + # Emotion model input shape is (48, 48, 1, n), where n is the batch size predictions = self._predict_internal(processed_imgs) return predictions From 29141b3cd5293c88fca852b1e0a7a64a09f75304 Mon Sep 17 00:00:00 2001 From: NatLee Date: Tue, 7 Jan 2025 04:54:35 +0800 Subject: [PATCH 22/69] [update] add hint for the shape of input img --- deepface/models/demography/Age.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deepface/models/demography/Age.py b/deepface/models/demography/Age.py index 7284aad..ae53487 100644 --- a/deepface/models/demography/Age.py +++ b/deepface/models/demography/Age.py @@ -54,9 +54,9 @@ class ApparentAgeClient(Demography): # Preprocessing input image or image list. imgs = self._preprocess_batch_or_single_input(img) - # Prediction + # Prediction from 3 channels image age_predictions = self._predict_internal(imgs) - + # Calculate apparent ages apparent_ages = np.array( [find_apparent_age(age_prediction) for age_prediction in age_predictions] From 36fb512bec44284f0b02bed98f8c121f7bc54884 Mon Sep 17 00:00:00 2001 From: NatLee Date: Tue, 7 Jan 2025 04:55:14 +0800 Subject: [PATCH 23/69] [fix] handle between grayscale and RGB image for models --- deepface/models/Demography.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/deepface/models/Demography.py b/deepface/models/Demography.py index d0a00f1..7bba87f 100644 --- a/deepface/models/Demography.py +++ b/deepface/models/Demography.py @@ -36,7 +36,8 @@ class Demography(ABC): assert img_batch.ndim == 4, "expected 4-dimensional tensor input" if img_batch.shape[0] == 1: # Single image - img_batch = img_batch.squeeze(0) # Remove batch dimension + if img_batch.shape[-1] != 3: # Check if grayscale + img_batch = img_batch.squeeze(0) # Remove batch dimension predict_result = self.model(img_batch, training=False).numpy()[0, :] # Predict with legacy method. return predict_result else: # Batch of images From 431544ac523e6d1ebae573b0cc2efc99604a1784 Mon Sep 17 00:00:00 2001 From: NatLee Date: Tue, 7 Jan 2025 04:56:05 +0800 Subject: [PATCH 24/69] [update] add process for single and multiple image --- deepface/modules/demography.py | 40 +++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/deepface/modules/demography.py b/deepface/modules/demography.py index 6b6382f..36b8305 100644 --- a/deepface/modules/demography.py +++ b/deepface/modules/demography.py @@ -182,30 +182,49 @@ def analyze( # Build the emotion model model = modeling.build_model(task="facial_attribute", model_name="Emotion") emotion_predictions = model.predict(faces_array) - + + # Handle single vs multiple emotion predictions + if len(emotion_predictions.shape) == 1: + # Single face case - reshape predictions to 2D array for consistent handling + emotion_predictions = emotion_predictions.reshape(1, -1) + + # Process predictions for each face for idx, predictions in enumerate(emotion_predictions): sum_of_predictions = predictions.sum() resp_objects[idx]["emotion"] = {} - + + # Calculate emotion probabilities and store in response for i, emotion_label in enumerate(Emotion.labels): - emotion_prediction = 100 * predictions[i] / sum_of_predictions - resp_objects[idx]["emotion"][emotion_label] = emotion_prediction - - resp_objects[idx]["dominant_emotion"] = Emotion.labels[np.argmax(predictions)] + emotion_probability = 100 * predictions[i] / sum_of_predictions + resp_objects[idx]["emotion"][emotion_label] = emotion_probability + + # Store dominant emotion + resp_objects[idx]["dominant_emotion"] = Emotion.labels[np.argmax(predictions)] elif action == "age": # Build the age model model = modeling.build_model(task="facial_attribute", model_name="Age") age_predictions = model.predict(faces_array) + # Handle single vs multiple age predictions + if len(age_predictions.shape) == 1: + # Single face case - reshape predictions to 2D array for consistent handling + age_predictions = age_predictions.reshape(1, -1) + for idx, age in enumerate(age_predictions): - resp_objects[idx]["age"] = int(age) + resp_objects[idx]["age"] = np.argmax(age) elif action == "gender": # Build the gender model model = modeling.build_model(task="facial_attribute", model_name="Gender") gender_predictions = model.predict(faces_array) + + # Handle single vs multiple gender predictions + if len(gender_predictions.shape) == 1: + # Single face case - reshape predictions to 2D array for consistent handling + gender_predictions = gender_predictions.reshape(1, -1) + # Process predictions for each face for idx, predictions in enumerate(gender_predictions): resp_objects[idx]["gender"] = {} @@ -219,7 +238,12 @@ def analyze( # Build the race model model = modeling.build_model(task="facial_attribute", model_name="Race") race_predictions = model.predict(faces_array) - + + # Handle single vs multiple race predictions + if len(race_predictions.shape) == 1: + # Single face case - reshape predictions to 2D array for consistent handling + race_predictions = race_predictions.reshape(1, -1) + for idx, predictions in enumerate(race_predictions): sum_of_predictions = predictions.sum() resp_objects[idx]["race"] = {} From 041773232fc59c150995ad80ed4fbea56d456c58 Mon Sep 17 00:00:00 2001 From: NatLee Date: Tue, 7 Jan 2025 05:26:07 +0800 Subject: [PATCH 25/69] [fix] model input size -> (n, w, h, c) --- deepface/models/demography/Emotion.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/deepface/models/demography/Emotion.py b/deepface/models/demography/Emotion.py index caf862b..66d09cc 100644 --- a/deepface/models/demography/Emotion.py +++ b/deepface/models/demography/Emotion.py @@ -73,7 +73,10 @@ class EmotionClient(Demography): imgs = self._preprocess_batch_or_single_input(img) # Preprocess each image and add channel dimension for grayscale images - processed_imgs = np.expand_dims(np.array([self._preprocess_image(img) for img in imgs]), axis=0) + processed_imgs = np.expand_dims(np.array([self._preprocess_image(img) for img in imgs]), axis=-1) + + # Reshape input for model (expected shape=(n, 48, 48, 1)), where n is the batch size + processed_imgs = processed_imgs.reshape(processed_imgs.shape[0], 48, 48, 1) # Prediction # Emotion model input shape is (48, 48, 1, n), where n is the batch size From c44af00269678748b4b68656454a817d39cc0d4f Mon Sep 17 00:00:00 2001 From: NatLee Date: Tue, 7 Jan 2025 05:26:27 +0800 Subject: [PATCH 26/69] [fix] check for input number of faces --- deepface/modules/demography.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/deepface/modules/demography.py b/deepface/modules/demography.py index 36b8305..447d146 100644 --- a/deepface/modules/demography.py +++ b/deepface/modules/demography.py @@ -192,7 +192,7 @@ def analyze( for idx, predictions in enumerate(emotion_predictions): sum_of_predictions = predictions.sum() resp_objects[idx]["emotion"] = {} - + # Calculate emotion probabilities and store in response for i, emotion_label in enumerate(Emotion.labels): emotion_probability = 100 * predictions[i] / sum_of_predictions @@ -205,14 +205,16 @@ def analyze( # Build the age model model = modeling.build_model(task="facial_attribute", model_name="Age") age_predictions = model.predict(faces_array) - - # Handle single vs multiple age predictions - if len(age_predictions.shape) == 1: - # Single face case - reshape predictions to 2D array for consistent handling - age_predictions = age_predictions.reshape(1, -1) - for idx, age in enumerate(age_predictions): - resp_objects[idx]["age"] = np.argmax(age) + # Handle single vs multiple age predictions + if faces_array.shape[0] == 1: + # Single face case - reshape predictions to 2D array for consistent handling + resp_objects[idx]["age"] = int(np.argmax(age_predictions)) + else: + # Multiple face case - iterate over each prediction + for idx, age in enumerate(age_predictions): + resp_objects[idx]["age"] = int(age) + elif action == "gender": # Build the gender model From ad577b42063b236268d9e5f8670e8117342b8a72 Mon Sep 17 00:00:00 2001 From: NatLee Date: Tue, 7 Jan 2025 05:33:47 +0800 Subject: [PATCH 27/69] [update] refactor response object creation in analyze function --- deepface/modules/demography.py | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/deepface/modules/demography.py b/deepface/modules/demography.py index 447d146..0c52eb0 100644 --- a/deepface/modules/demography.py +++ b/deepface/modules/demography.py @@ -162,10 +162,8 @@ def analyze( # Convert the list of valid faces to a numpy array faces_array = np.array(valid_faces) - # Create placeholder response objects for each face - for _ in range(len(valid_faces)): - resp_objects.append({}) - + # Preprocess the result to a list of dictionaries + resp_objects = [] # For each action, predict the corresponding attribute pbar = tqdm( @@ -177,6 +175,7 @@ def analyze( for index in pbar: action = actions[index] pbar.set_description(f"Action: {action}") + resp_object = {} if action == "emotion": # Build the emotion model @@ -191,15 +190,15 @@ def analyze( # Process predictions for each face for idx, predictions in enumerate(emotion_predictions): sum_of_predictions = predictions.sum() - resp_objects[idx]["emotion"] = {} + resp_object["emotion"] = {} # Calculate emotion probabilities and store in response for i, emotion_label in enumerate(Emotion.labels): emotion_probability = 100 * predictions[i] / sum_of_predictions - resp_objects[idx]["emotion"][emotion_label] = emotion_probability + resp_object["emotion"][emotion_label] = emotion_probability # Store dominant emotion - resp_objects[idx]["dominant_emotion"] = Emotion.labels[np.argmax(predictions)] + resp_object["dominant_emotion"] = Emotion.labels[np.argmax(predictions)] elif action == "age": # Build the age model @@ -209,11 +208,11 @@ def analyze( # Handle single vs multiple age predictions if faces_array.shape[0] == 1: # Single face case - reshape predictions to 2D array for consistent handling - resp_objects[idx]["age"] = int(np.argmax(age_predictions)) + resp_object["age"] = int(np.argmax(age_predictions)) else: # Multiple face case - iterate over each prediction for idx, age in enumerate(age_predictions): - resp_objects[idx]["age"] = int(age) + resp_object["age"] = int(age) elif action == "gender": @@ -228,13 +227,13 @@ def analyze( # Process predictions for each face for idx, predictions in enumerate(gender_predictions): - resp_objects[idx]["gender"] = {} + resp_object["gender"] = {} for i, gender_label in enumerate(Gender.labels): gender_prediction = 100 * predictions[i] - resp_objects[idx]["gender"][gender_label] = gender_prediction + resp_object["gender"][gender_label] = gender_prediction - resp_objects[idx]["dominant_gender"] = Gender.labels[np.argmax(predictions)] + resp_object["dominant_gender"] = Gender.labels[np.argmax(predictions)] elif action == "race": # Build the race model @@ -248,13 +247,16 @@ def analyze( for idx, predictions in enumerate(race_predictions): sum_of_predictions = predictions.sum() - resp_objects[idx]["race"] = {} + resp_object["race"] = {} for i, race_label in enumerate(Race.labels): race_prediction = 100 * predictions[i] / sum_of_predictions - resp_objects[idx]["race"][race_label] = race_prediction + resp_object["race"][race_label] = race_prediction - resp_objects[idx]["dominant_race"] = Race.labels[np.argmax(predictions)] + resp_object["dominant_race"] = Race.labels[np.argmax(predictions)] + + # Add the response object to the list of response objects + resp_objects.append(resp_object) # Add the face region and confidence to the response objects for idx, resp_obj in enumerate(resp_objects): From 52a38ba21a9b7edb331e2a6f25e68115ca1a663c Mon Sep 17 00:00:00 2001 From: NatLee Date: Tue, 7 Jan 2025 05:54:44 +0800 Subject: [PATCH 28/69] [fix] use prediction shape to avoid confuse situation of predictions --- deepface/modules/demography.py | 196 ++++++++++++++------------------- 1 file changed, 81 insertions(+), 115 deletions(-) diff --git a/deepface/modules/demography.py b/deepface/modules/demography.py index 0c52eb0..9789007 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, Optional # 3rd party dependencies import numpy as np @@ -117,8 +117,6 @@ def analyze( f"Invalid action passed ({repr(action)})). " "Valid actions are `emotion`, `age`, `gender`, `race`." ) - # --------------------------------- - resp_objects = [] img_objs = detection.extract_faces( img_path=img_path, @@ -130,137 +128,105 @@ def analyze( anti_spoofing=anti_spoofing, ) - # Anti-spoofing check if anti_spoofing and any(img_obj.get("is_real", True) is False for img_obj in img_objs): raise ValueError("Spoof detected in the given image.") - # Prepare the input for the model - valid_faces = [] - face_regions = [] - face_confidences = [] - - for img_obj in img_objs: - # Extract the face content + def preprocess_face(img_obj: Dict[str, Any]) -> Optional[np.ndarray]: + """ + Preprocess the face image for analysis. + """ img_content = img_obj["face"] - # Check if the face content is empty if img_content.shape[0] == 0 or img_content.shape[1] == 0: - continue + return None + img_content = img_content[:, :, ::-1] # BGR to RGB + return preprocessing.resize_image(img=img_content, target_size=(224, 224)) - # Convert the image to RGB format from BGR - img_content = img_content[:, :, ::-1] - # Resize the image to the target size for the model - img_content = preprocessing.resize_image(img=img_content, target_size=(224, 224)) - - valid_faces.append(img_content) - face_regions.append(img_obj["facial_area"]) - face_confidences.append(img_obj["confidence"]) - - # If no valid faces are found, return an empty list - if not valid_faces: + # Filter out empty faces + face_data = [(preprocess_face(img_obj), img_obj["facial_area"], img_obj["confidence"]) + for img_obj in img_objs if img_obj["face"].size > 0] + + if not face_data: return [] - # Convert the list of valid faces to a numpy array + # Unpack the face data + valid_faces, face_regions, face_confidences = zip(*face_data) faces_array = np.array(valid_faces) - # Preprocess the result to a list of dictionaries - resp_objects = [] + # Initialize the results list with face regions and confidence scores + results = [{"region": region, "face_confidence": conf} + for region, conf in zip(face_regions, face_confidences)] - # For each action, predict the corresponding attribute + # Iterate over the actions and perform analysis pbar = tqdm( - range(0, len(actions)), + actions, desc="Finding actions", disable=silent if len(actions) > 1 else True, ) - - for index in pbar: - action = actions[index] + + for action in pbar: pbar.set_description(f"Action: {action}") - resp_object = {} + model = modeling.build_model(task="facial_attribute", model_name=action.capitalize()) + predictions = model.predict(faces_array) + # If the model returns a single prediction, reshape it to match the number of faces + # Use number of faces and number of predictions shape to determine the correct shape of predictions + # For example, if there are 1 face to predict with Emotion model, reshape predictions to (1, 7) + if faces_array.shape[0] == 1 and len(predictions.shape) == 1: + # For models like `Emotion`, which return a single prediction for a single face + predictions = predictions.reshape(1, -1) + + # Update the results with the predictions + # ---------------------------------------- + # For emotion, calculate the percentage of each emotion and find the dominant emotion if action == "emotion": - # Build the emotion model - model = modeling.build_model(task="facial_attribute", model_name="Emotion") - emotion_predictions = model.predict(faces_array) - - # Handle single vs multiple emotion predictions - if len(emotion_predictions.shape) == 1: - # Single face case - reshape predictions to 2D array for consistent handling - emotion_predictions = emotion_predictions.reshape(1, -1) - - # Process predictions for each face - for idx, predictions in enumerate(emotion_predictions): - sum_of_predictions = predictions.sum() - resp_object["emotion"] = {} - - # Calculate emotion probabilities and store in response - for i, emotion_label in enumerate(Emotion.labels): - emotion_probability = 100 * predictions[i] / sum_of_predictions - resp_object["emotion"][emotion_label] = emotion_probability - - # Store dominant emotion - resp_object["dominant_emotion"] = Emotion.labels[np.argmax(predictions)] - + emotion_results = [ + { + "emotion": { + label: 100 * pred[i] / pred.sum() + for i, label in enumerate(Emotion.labels) + }, + "dominant_emotion": Emotion.labels[np.argmax(pred)] + } + for pred in predictions + ] + for result, emotion_result in zip(results, emotion_results): + result.update(emotion_result) + # ---------------------------------------- + # For age, find the dominant age category (0-100) elif action == "age": - # Build the age model - model = modeling.build_model(task="facial_attribute", model_name="Age") - age_predictions = model.predict(faces_array) - - # Handle single vs multiple age predictions - if faces_array.shape[0] == 1: - # Single face case - reshape predictions to 2D array for consistent handling - resp_object["age"] = int(np.argmax(age_predictions)) - else: - # Multiple face case - iterate over each prediction - for idx, age in enumerate(age_predictions): - resp_object["age"] = int(age) - - + age_results = [{"age": int(np.argmax(pred) if len(pred.shape) > 0 else pred)} + for pred in predictions] + for result, age_result in zip(results, age_results): + result.update(age_result) + # ---------------------------------------- + # For gender, calculate the percentage of each gender and find the dominant gender elif action == "gender": - # Build the gender model - model = modeling.build_model(task="facial_attribute", model_name="Gender") - gender_predictions = model.predict(faces_array) - - # Handle single vs multiple gender predictions - if len(gender_predictions.shape) == 1: - # Single face case - reshape predictions to 2D array for consistent handling - gender_predictions = gender_predictions.reshape(1, -1) - - # Process predictions for each face - for idx, predictions in enumerate(gender_predictions): - resp_object["gender"] = {} - - for i, gender_label in enumerate(Gender.labels): - gender_prediction = 100 * predictions[i] - resp_object["gender"][gender_label] = gender_prediction - - resp_object["dominant_gender"] = Gender.labels[np.argmax(predictions)] - + gender_results = [ + { + "gender": { + label: 100 * pred[i] + for i, label in enumerate(Gender.labels) + }, + "dominant_gender": Gender.labels[np.argmax(pred)] + } + for pred in predictions + ] + for result, gender_result in zip(results, gender_results): + result.update(gender_result) + # ---------------------------------------- + # For race, calculate the percentage of each race and find the dominant race elif action == "race": - # Build the race model - model = modeling.build_model(task="facial_attribute", model_name="Race") - race_predictions = model.predict(faces_array) + race_results = [ + { + "race": { + label: 100 * pred[i] / pred.sum() + for i, label in enumerate(Race.labels) + }, + "dominant_race": Race.labels[np.argmax(pred)] + } + for pred in predictions + ] + for result, race_result in zip(results, race_results): + result.update(race_result) - # Handle single vs multiple race predictions - if len(race_predictions.shape) == 1: - # Single face case - reshape predictions to 2D array for consistent handling - race_predictions = race_predictions.reshape(1, -1) - - for idx, predictions in enumerate(race_predictions): - sum_of_predictions = predictions.sum() - resp_object["race"] = {} - - for i, race_label in enumerate(Race.labels): - race_prediction = 100 * predictions[i] / sum_of_predictions - resp_object["race"][race_label] = race_prediction - - resp_object["dominant_race"] = Race.labels[np.argmax(predictions)] - - # Add the response object to the list of response objects - resp_objects.append(resp_object) - - # Add the face region and confidence to the response objects - for idx, resp_obj in enumerate(resp_objects): - resp_obj["region"] = face_regions[idx] - resp_obj["face_confidence"] = face_confidences[idx] - - return resp_objects + return results \ No newline at end of file From ba8c651c7a16f98db983d84e10959811b17e29bf Mon Sep 17 00:00:00 2001 From: NatLee Date: Tue, 7 Jan 2025 05:56:09 +0800 Subject: [PATCH 29/69] [fix] 1 img input for the `Emotion` model --- deepface/models/demography/Emotion.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/deepface/models/demography/Emotion.py b/deepface/models/demography/Emotion.py index 66d09cc..ac2af29 100644 --- a/deepface/models/demography/Emotion.py +++ b/deepface/models/demography/Emotion.py @@ -72,14 +72,16 @@ class EmotionClient(Demography): # Preprocessing input image or image list. imgs = self._preprocess_batch_or_single_input(img) - # Preprocess each image and add channel dimension for grayscale images - processed_imgs = np.expand_dims(np.array([self._preprocess_image(img) for img in imgs]), axis=-1) - - # Reshape input for model (expected shape=(n, 48, 48, 1)), where n is the batch size - processed_imgs = processed_imgs.reshape(processed_imgs.shape[0], 48, 48, 1) + if imgs.shape[0] == 1: + # Preprocess single image and add channel dimension for grayscale images + processed_imgs = np.expand_dims(np.array([self._preprocess_image(img) for img in imgs]), axis=0) + else: + # Preprocess batch of images and add channel dimension for grayscale images + processed_imgs = np.expand_dims(np.array([self._preprocess_image(img) for img in imgs]), axis=-1) + # Reshape input for model (expected shape=(n, 48, 48, 1)), where n is the batch size + processed_imgs = processed_imgs.reshape(processed_imgs.shape[0], 48, 48, 1) # Prediction - # Emotion model input shape is (48, 48, 1, n), where n is the batch size predictions = self._predict_internal(processed_imgs) return predictions From 4284252a265823f05ab0ae5dfcf84379d838497a Mon Sep 17 00:00:00 2001 From: h-alice Date: Tue, 7 Jan 2025 11:19:35 +0800 Subject: [PATCH 30/69] Remove obsolete comment. --- deepface/models/Demography.py | 1 - 1 file changed, 1 deletion(-) diff --git a/deepface/models/Demography.py b/deepface/models/Demography.py index 7bba87f..911032d 100644 --- a/deepface/models/Demography.py +++ b/deepface/models/Demography.py @@ -51,7 +51,6 @@ class Demography(ABC): img: Single image as np.ndarray (224, 224, 3) or List of images as List[np.ndarray] or Batch of images as np.ndarray (n, 224, 224, 3) - NOTE: If the imput is grayscale, then there's no channel dimension. Returns: Four-dimensional numpy array (n, 224, 224, 3) """ From eb7b8411e88e275f6b625ac3f1405f46840fd514 Mon Sep 17 00:00:00 2001 From: h-alice Date: Mon, 13 Jan 2025 17:23:44 +0800 Subject: [PATCH 31/69] Documentation --- deepface/models/Demography.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/deepface/models/Demography.py b/deepface/models/Demography.py index 911032d..fb9d106 100644 --- a/deepface/models/Demography.py +++ b/deepface/models/Demography.py @@ -29,19 +29,21 @@ class Demography(ABC): Args: img_batch: Batch of images as np.ndarray (n, x, y, c), with n >= 1, x = image width, y = image height, c = channel + Or Single image as np.ndarray (1, x, y, c), with x = image width, y = image height and c = channel + The channel dimension may be omitted if the image is grayscale. (For emotion model) """ if not self.model_name: # Check if called from derived class - raise NotImplementedError("virtual method must not be called directly") + raise NotImplementedError("no model selected") assert img_batch.ndim == 4, "expected 4-dimensional tensor input" if img_batch.shape[0] == 1: # Single image - if img_batch.shape[-1] != 3: # Check if grayscale + if img_batch.shape[-1] != 3: # Check if grayscale by checking last dimension, if not 3, it is grayscale. img_batch = img_batch.squeeze(0) # Remove batch dimension predict_result = self.model(img_batch, training=False).numpy()[0, :] # Predict with legacy method. return predict_result else: # Batch of images - return self.model.predict_on_batch(img_batch) + return self.model.predict_on_batch(img_batch) # Predict with batch prediction def _preprocess_batch_or_single_input(self, img: Union[np.ndarray, List[np.ndarray]]) -> np.ndarray: @@ -54,10 +56,8 @@ class Demography(ABC): Returns: Four-dimensional numpy array (n, 224, 224, 3) """ - if isinstance(img, list): # Convert from list to image batch. - image_batch = np.array(img) - else: - image_batch = img + + image_batch = np.array(img) # Remove batch dimension in advance if exists image_batch = image_batch.squeeze() From 688fbe6b902f5a368142284fad03cc6054bb0d9b Mon Sep 17 00:00:00 2001 From: NatLee Date: Mon, 13 Jan 2025 22:27:11 +0800 Subject: [PATCH 32/69] [fix] lint --- deepface/models/Demography.py | 38 +++++++++++++++++++--------------- deepface/modules/demography.py | 8 +++---- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/deepface/models/Demography.py b/deepface/models/Demography.py index fb9d106..329a156 100644 --- a/deepface/models/Demography.py +++ b/deepface/models/Demography.py @@ -28,24 +28,32 @@ class Demography(ABC): And switch to batch prediction if receives batched images. Args: - img_batch: Batch of images as np.ndarray (n, x, y, c), with n >= 1, x = image width, y = image height, c = channel - Or Single image as np.ndarray (1, x, y, c), with x = image width, y = image height and c = channel - The channel dimension may be omitted if the image is grayscale. (For emotion model) + img_batch: + Batch of images as np.ndarray (n, x, y, c) + with n >= 1, x = image width, y = image height, c = channel + Or Single image as np.ndarray (1, x, y, c) + with x = image width, y = image height and c = channel + The channel dimension may be omitted if the image is grayscale. (For emotion model) """ if not self.model_name: # Check if called from derived class raise NotImplementedError("no model selected") - assert img_batch.ndim == 4, "expected 4-dimensional tensor input" + # Single image + if img_batch.shape[0] == 1: + # Check if grayscale by checking last dimension, if not 3, it is grayscale. + if img_batch.shape[-1] != 3: + # Remove batch dimension + img_batch = img_batch.squeeze(0) + # Predict with legacy method. + return self.model(img_batch, training=False).numpy()[0, :] + # Batch of images + # Predict with batch prediction + return self.model.predict_on_batch(img_batch) - if img_batch.shape[0] == 1: # Single image - if img_batch.shape[-1] != 3: # Check if grayscale by checking last dimension, if not 3, it is grayscale. - img_batch = img_batch.squeeze(0) # Remove batch dimension - predict_result = self.model(img_batch, training=False).numpy()[0, :] # Predict with legacy method. - return predict_result - else: # Batch of images - return self.model.predict_on_batch(img_batch) # Predict with batch prediction - - def _preprocess_batch_or_single_input(self, img: Union[np.ndarray, List[np.ndarray]]) -> np.ndarray: + def _preprocess_batch_or_single_input( + self, + img: Union[np.ndarray, List[np.ndarray]] + ) -> np.ndarray: """ Preprocess single or batch of images, return as 4-D numpy array. @@ -56,15 +64,11 @@ class Demography(ABC): Returns: Four-dimensional numpy array (n, 224, 224, 3) """ - image_batch = np.array(img) - # Remove batch dimension in advance if exists image_batch = image_batch.squeeze() - # Check input dimension if len(image_batch.shape) == 3: # Single image - add batch dimension image_batch = np.expand_dims(image_batch, axis=0) - return image_batch diff --git a/deepface/modules/demography.py b/deepface/modules/demography.py index 9789007..c78f73b 100644 --- a/deepface/modules/demography.py +++ b/deepface/modules/demography.py @@ -168,9 +168,9 @@ def analyze( model = modeling.build_model(task="facial_attribute", model_name=action.capitalize()) predictions = model.predict(faces_array) - # If the model returns a single prediction, reshape it to match the number of faces - # Use number of faces and number of predictions shape to determine the correct shape of predictions - # For example, if there are 1 face to predict with Emotion model, reshape predictions to (1, 7) + # If the model returns a single prediction, reshape it to match the number of faces. + # Determine the correct shape of predictions by using number of faces and predictions shape. + # Example: For 1 face with Emotion model, predictions will be reshaped to (1, 7). if faces_array.shape[0] == 1 and len(predictions.shape) == 1: # For models like `Emotion`, which return a single prediction for a single face predictions = predictions.reshape(1, -1) @@ -229,4 +229,4 @@ def analyze( for result, race_result in zip(results, race_results): result.update(race_result) - return results \ No newline at end of file + return results From fa4044adae2ba84cf4b3d07916416717361a7a61 Mon Sep 17 00:00:00 2001 From: h-alice Date: Mon, 13 Jan 2025 23:14:40 +0800 Subject: [PATCH 33/69] patch: Greyscale image prediction condition. --- deepface/models/Demography.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/deepface/models/Demography.py b/deepface/models/Demography.py index 329a156..5fcc431 100644 --- a/deepface/models/Demography.py +++ b/deepface/models/Demography.py @@ -38,14 +38,15 @@ class Demography(ABC): if not self.model_name: # Check if called from derived class raise NotImplementedError("no model selected") assert img_batch.ndim == 4, "expected 4-dimensional tensor input" - # Single image - if img_batch.shape[0] == 1: + + if img_batch.shape[-1] != 3: # Handle grayscale image, check last dimension. # Check if grayscale by checking last dimension, if not 3, it is grayscale. - if img_batch.shape[-1] != 3: - # Remove batch dimension - img_batch = img_batch.squeeze(0) + img_batch = img_batch.squeeze(0) # Remove batch dimension + + if img_batch.shape[0] == 1: # Single image # Predict with legacy method. return self.model(img_batch, training=False).numpy()[0, :] + # Batch of images # Predict with batch prediction return self.model.predict_on_batch(img_batch) From 910d6e1d80938dcca85f5c61d4d74d904703ff0b Mon Sep 17 00:00:00 2001 From: h-alice Date: Mon, 13 Jan 2025 23:31:22 +0800 Subject: [PATCH 34/69] patch: fix dimension. --- deepface/models/Demography.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/deepface/models/Demography.py b/deepface/models/Demography.py index 5fcc431..2a78afd 100644 --- a/deepface/models/Demography.py +++ b/deepface/models/Demography.py @@ -33,23 +33,20 @@ class Demography(ABC): with n >= 1, x = image width, y = image height, c = channel Or Single image as np.ndarray (1, x, y, c) with x = image width, y = image height and c = channel - The channel dimension may be omitted if the image is grayscale. (For emotion model) + The channel dimension will be 1 if input is grayscale. (For emotion model) """ if not self.model_name: # Check if called from derived class raise NotImplementedError("no model selected") assert img_batch.ndim == 4, "expected 4-dimensional tensor input" - - if img_batch.shape[-1] != 3: # Handle grayscale image, check last dimension. - # Check if grayscale by checking last dimension, if not 3, it is grayscale. - img_batch = img_batch.squeeze(0) # Remove batch dimension - + if img_batch.shape[0] == 1: # Single image + img_batch = img_batch.squeeze(0) # Remove batch dimension # Predict with legacy method. return self.model(img_batch, training=False).numpy()[0, :] - - # Batch of images - # Predict with batch prediction - return self.model.predict_on_batch(img_batch) + else: + # Batch of images + # Predict with batch prediction + return self.model.predict_on_batch(img_batch) def _preprocess_batch_or_single_input( self, From 72b6db19d695b6272b2655c39843e7d61d565384 Mon Sep 17 00:00:00 2001 From: h-alice Date: Mon, 13 Jan 2025 23:35:48 +0800 Subject: [PATCH 35/69] patch: fix dimension --- deepface/models/Demography.py | 1 - 1 file changed, 1 deletion(-) diff --git a/deepface/models/Demography.py b/deepface/models/Demography.py index 2a78afd..c9cfe30 100644 --- a/deepface/models/Demography.py +++ b/deepface/models/Demography.py @@ -40,7 +40,6 @@ class Demography(ABC): assert img_batch.ndim == 4, "expected 4-dimensional tensor input" if img_batch.shape[0] == 1: # Single image - img_batch = img_batch.squeeze(0) # Remove batch dimension # Predict with legacy method. return self.model(img_batch, training=False).numpy()[0, :] else: From a23893a5fa410e121a9caa4c6b8f79d62f0ce4a4 Mon Sep 17 00:00:00 2001 From: h-alice Date: Mon, 13 Jan 2025 23:44:28 +0800 Subject: [PATCH 36/69] patch: emotion dimension. --- deepface/models/demography/Emotion.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/deepface/models/demography/Emotion.py b/deepface/models/demography/Emotion.py index ac2af29..499c246 100644 --- a/deepface/models/demography/Emotion.py +++ b/deepface/models/demography/Emotion.py @@ -72,14 +72,7 @@ class EmotionClient(Demography): # Preprocessing input image or image list. imgs = self._preprocess_batch_or_single_input(img) - if imgs.shape[0] == 1: - # Preprocess single image and add channel dimension for grayscale images - processed_imgs = np.expand_dims(np.array([self._preprocess_image(img) for img in imgs]), axis=0) - else: - # Preprocess batch of images and add channel dimension for grayscale images - processed_imgs = np.expand_dims(np.array([self._preprocess_image(img) for img in imgs]), axis=-1) - # Reshape input for model (expected shape=(n, 48, 48, 1)), where n is the batch size - processed_imgs = processed_imgs.reshape(processed_imgs.shape[0], 48, 48, 1) + processed_imgs = np.expand_dims(np.array([self._preprocess_image(img) for img in imgs]), axis=-1) # Prediction predictions = self._predict_internal(processed_imgs) From da4a0c5452994575ca8ea7bfb9a6ae7c84782e00 Mon Sep 17 00:00:00 2001 From: h-alice Date: Tue, 14 Jan 2025 09:12:35 +0800 Subject: [PATCH 37/69] patch: Lint --- deepface/models/Demography.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/deepface/models/Demography.py b/deepface/models/Demography.py index c9cfe30..87869b9 100644 --- a/deepface/models/Demography.py +++ b/deepface/models/Demography.py @@ -38,14 +38,14 @@ class Demography(ABC): if not self.model_name: # Check if called from derived class raise NotImplementedError("no model selected") assert img_batch.ndim == 4, "expected 4-dimensional tensor input" - + if img_batch.shape[0] == 1: # Single image # Predict with legacy method. return self.model(img_batch, training=False).numpy()[0, :] - else: - # Batch of images - # Predict with batch prediction - return self.model.predict_on_batch(img_batch) + + # Batch of images + # Predict with batch prediction + return self.model.predict_on_batch(img_batch) def _preprocess_batch_or_single_input( self, From c72b47484de784f3b0a51722390caa8f61b25ed8 Mon Sep 17 00:00:00 2001 From: Nat Lee Date: Tue, 14 Jan 2025 18:12:32 +0800 Subject: [PATCH 38/69] [update] lint --- deepface/modules/demography.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/deepface/modules/demography.py b/deepface/modules/demography.py index c78f73b..528224d 100644 --- a/deepface/modules/demography.py +++ b/deepface/modules/demography.py @@ -9,7 +9,6 @@ from tqdm import tqdm from deepface.modules import modeling, detection, preprocessing from deepface.models.demography import Gender, Race, Emotion -# pylint: disable=trailing-whitespace def analyze( img_path: Union[str, np.ndarray], actions: Union[tuple, list] = ("emotion", "age", "gender", "race"), @@ -142,39 +141,40 @@ def analyze( return preprocessing.resize_image(img=img_content, target_size=(224, 224)) # Filter out empty faces - face_data = [(preprocess_face(img_obj), img_obj["facial_area"], img_obj["confidence"]) - for img_obj in img_objs if img_obj["face"].size > 0] - + face_data = [ + ( + preprocess_face(img_obj), + img_obj["facial_area"], + img_obj["confidence"] + ) + for img_obj in img_objs if img_obj["face"].size > 0 + ] + if not face_data: return [] # Unpack the face data valid_faces, face_regions, face_confidences = zip(*face_data) faces_array = np.array(valid_faces) - # Initialize the results list with face regions and confidence scores - results = [{"region": region, "face_confidence": conf} + results = [{"region": region, "face_confidence": conf} for region, conf in zip(face_regions, face_confidences)] - # Iterate over the actions and perform analysis pbar = tqdm( actions, desc="Finding actions", disable=silent if len(actions) > 1 else True, ) - for action in pbar: pbar.set_description(f"Action: {action}") model = modeling.build_model(task="facial_attribute", model_name=action.capitalize()) predictions = model.predict(faces_array) - # If the model returns a single prediction, reshape it to match the number of faces. # Determine the correct shape of predictions by using number of faces and predictions shape. # Example: For 1 face with Emotion model, predictions will be reshaped to (1, 7). if faces_array.shape[0] == 1 and len(predictions.shape) == 1: # For models like `Emotion`, which return a single prediction for a single face predictions = predictions.reshape(1, -1) - # Update the results with the predictions # ---------------------------------------- # For emotion, calculate the percentage of each emotion and find the dominant emotion @@ -194,7 +194,7 @@ def analyze( # ---------------------------------------- # For age, find the dominant age category (0-100) elif action == "age": - age_results = [{"age": int(np.argmax(pred) if len(pred.shape) > 0 else pred)} + age_results = [{"age": int(np.argmax(pred) if len(pred.shape) > 0 else pred)} for pred in predictions] for result, age_result in zip(results, age_results): result.update(age_result) @@ -228,5 +228,4 @@ def analyze( ] for result, race_result in zip(results, race_results): result.update(race_result) - return results From 7e719dfdebee52b6bb40c5fb0c3c230b2613557c Mon Sep 17 00:00:00 2001 From: h-alice Date: Thu, 16 Jan 2025 17:09:45 +0800 Subject: [PATCH 39/69] Patch: Make Age model capable to handle single or batched input. --- deepface/models/demography/Age.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/deepface/models/demography/Age.py b/deepface/models/demography/Age.py index ae53487..e449aca 100644 --- a/deepface/models/demography/Age.py +++ b/deepface/models/demography/Age.py @@ -41,7 +41,7 @@ class ApparentAgeClient(Demography): self.model = load_model() self.model_name = "Age" - def predict(self, img: Union[np.ndarray, List[np.ndarray]]) -> np.ndarray: + def predict(self, img: Union[np.ndarray, List[np.ndarray]]) -> Union[np.float64, np.ndarray]: """ Predict apparent age(s) for single or multiple faces Args: @@ -49,7 +49,7 @@ class ApparentAgeClient(Demography): List of images as List[np.ndarray] or Batch of images as np.ndarray (n, 224, 224, 3) Returns: - np.ndarray (n,) + np.ndarray (age_classes,) if single image, np.ndarray (n, age_classes) if batched images. """ # Preprocessing input image or image list. imgs = self._preprocess_batch_or_single_input(img) @@ -58,11 +58,11 @@ class ApparentAgeClient(Demography): age_predictions = self._predict_internal(imgs) # Calculate apparent ages - apparent_ages = np.array( - [find_apparent_age(age_prediction) for age_prediction in age_predictions] - ) + if len(age_predictions.shape) == 1: # Single prediction list + return find_apparent_age(age_predictions) + else: # Batched predictions + return np.array([find_apparent_age(age_prediction) for age_prediction in age_predictions]) - return apparent_ages def load_model( url=WEIGHTS_URL, @@ -98,15 +98,16 @@ def load_model( return age_model - def find_apparent_age(age_predictions: np.ndarray) -> np.float64: """ Find apparent age prediction from a given probas of ages Args: - age_predictions (?) + age_predictions (age_classes,) Returns: apparent_age (float) """ + assert len(age_predictions.shape) == 1, "Input should be a list of age predictions, \ + not batched. Got shape: {}".format(age_predictions.shape) output_indexes = np.arange(0, 101) apparent_age = np.sum(age_predictions * output_indexes) return apparent_age From 6a7bbdb92676c76c4aa045424e16f921c7bd3ded Mon Sep 17 00:00:00 2001 From: h-alice Date: Thu, 16 Jan 2025 17:17:25 +0800 Subject: [PATCH 40/69] REVERT demography.py --- deepface/modules/demography.py | 174 +++++++++++++++------------------ 1 file changed, 78 insertions(+), 96 deletions(-) diff --git a/deepface/modules/demography.py b/deepface/modules/demography.py index 528224d..b9991d9 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, Optional +from typing import Any, Dict, List, Union # 3rd party dependencies import numpy as np @@ -9,6 +9,7 @@ from tqdm import tqdm from deepface.modules import modeling, detection, preprocessing from deepface.models.demography import Gender, Race, Emotion + def analyze( img_path: Union[str, np.ndarray], actions: Union[tuple, list] = ("emotion", "age", "gender", "race"), @@ -116,6 +117,8 @@ def analyze( f"Invalid action passed ({repr(action)})). " "Valid actions are `emotion`, `age`, `gender`, `race`." ) + # --------------------------------- + resp_objects = [] img_objs = detection.extract_faces( img_path=img_path, @@ -127,105 +130,84 @@ def analyze( anti_spoofing=anti_spoofing, ) - if anti_spoofing and any(img_obj.get("is_real", True) is False for img_obj in img_objs): - raise ValueError("Spoof detected in the given image.") + for img_obj in img_objs: + if anti_spoofing is True and img_obj.get("is_real", True) is False: + raise ValueError("Spoof detected in the given image.") - def preprocess_face(img_obj: Dict[str, Any]) -> Optional[np.ndarray]: - """ - Preprocess the face image for analysis. - """ img_content = img_obj["face"] + img_region = img_obj["facial_area"] + img_confidence = img_obj["confidence"] if img_content.shape[0] == 0 or img_content.shape[1] == 0: - return None - img_content = img_content[:, :, ::-1] # BGR to RGB - return preprocessing.resize_image(img=img_content, target_size=(224, 224)) + continue - # Filter out empty faces - face_data = [ - ( - preprocess_face(img_obj), - img_obj["facial_area"], - img_obj["confidence"] + # rgb to bgr + img_content = img_content[:, :, ::-1] + + # resize input image + img_content = preprocessing.resize_image(img=img_content, target_size=(224, 224)) + + obj = {} + # facial attribute analysis + pbar = tqdm( + range(0, len(actions)), + desc="Finding actions", + disable=silent if len(actions) > 1 else True, ) - for img_obj in img_objs if img_obj["face"].size > 0 - ] + for index in pbar: + action = actions[index] + pbar.set_description(f"Action: {action}") - if not face_data: - return [] + if action == "emotion": + emotion_predictions = modeling.build_model( + task="facial_attribute", model_name="Emotion" + ).predict(img_content) + sum_of_predictions = emotion_predictions.sum() - # Unpack the face data - valid_faces, face_regions, face_confidences = zip(*face_data) - faces_array = np.array(valid_faces) - # Initialize the results list with face regions and confidence scores - results = [{"region": region, "face_confidence": conf} - for region, conf in zip(face_regions, face_confidences)] - # Iterate over the actions and perform analysis - pbar = tqdm( - actions, - desc="Finding actions", - disable=silent if len(actions) > 1 else True, - ) - for action in pbar: - pbar.set_description(f"Action: {action}") - model = modeling.build_model(task="facial_attribute", model_name=action.capitalize()) - predictions = model.predict(faces_array) - # If the model returns a single prediction, reshape it to match the number of faces. - # Determine the correct shape of predictions by using number of faces and predictions shape. - # Example: For 1 face with Emotion model, predictions will be reshaped to (1, 7). - if faces_array.shape[0] == 1 and len(predictions.shape) == 1: - # For models like `Emotion`, which return a single prediction for a single face - predictions = predictions.reshape(1, -1) - # Update the results with the predictions - # ---------------------------------------- - # For emotion, calculate the percentage of each emotion and find the dominant emotion - if action == "emotion": - emotion_results = [ - { - "emotion": { - label: 100 * pred[i] / pred.sum() - for i, label in enumerate(Emotion.labels) - }, - "dominant_emotion": Emotion.labels[np.argmax(pred)] - } - for pred in predictions - ] - for result, emotion_result in zip(results, emotion_results): - result.update(emotion_result) - # ---------------------------------------- - # For age, find the dominant age category (0-100) - elif action == "age": - age_results = [{"age": int(np.argmax(pred) if len(pred.shape) > 0 else pred)} - for pred in predictions] - for result, age_result in zip(results, age_results): - result.update(age_result) - # ---------------------------------------- - # For gender, calculate the percentage of each gender and find the dominant gender - elif action == "gender": - gender_results = [ - { - "gender": { - label: 100 * pred[i] - for i, label in enumerate(Gender.labels) - }, - "dominant_gender": Gender.labels[np.argmax(pred)] - } - for pred in predictions - ] - for result, gender_result in zip(results, gender_results): - result.update(gender_result) - # ---------------------------------------- - # For race, calculate the percentage of each race and find the dominant race - elif action == "race": - race_results = [ - { - "race": { - label: 100 * pred[i] / pred.sum() - for i, label in enumerate(Race.labels) - }, - "dominant_race": Race.labels[np.argmax(pred)] - } - for pred in predictions - ] - for result, race_result in zip(results, race_results): - result.update(race_result) - return results + obj["emotion"] = {} + for i, emotion_label in enumerate(Emotion.labels): + emotion_prediction = 100 * emotion_predictions[i] / sum_of_predictions + obj["emotion"][emotion_label] = emotion_prediction + + obj["dominant_emotion"] = Emotion.labels[np.argmax(emotion_predictions)] + + elif action == "age": + apparent_age = modeling.build_model( + task="facial_attribute", model_name="Age" + ).predict(img_content) + # int cast is for exception - object of type 'float32' is not JSON serializable + print(apparent_age.shape) + obj["age"] = int(apparent_age) + + elif action == "gender": + gender_predictions = modeling.build_model( + task="facial_attribute", model_name="Gender" + ).predict(img_content) + obj["gender"] = {} + for i, gender_label in enumerate(Gender.labels): + gender_prediction = 100 * gender_predictions[i] + obj["gender"][gender_label] = gender_prediction + + obj["dominant_gender"] = Gender.labels[np.argmax(gender_predictions)] + + elif action == "race": + race_predictions = modeling.build_model( + task="facial_attribute", model_name="Race" + ).predict(img_content) + sum_of_predictions = race_predictions.sum() + + obj["race"] = {} + for i, race_label in enumerate(Race.labels): + race_prediction = 100 * race_predictions[i] / sum_of_predictions + obj["race"][race_label] = race_prediction + + obj["dominant_race"] = Race.labels[np.argmax(race_predictions)] + + # ----------------------------- + # mention facial areas + obj["region"] = img_region + # include image confidence + obj["face_confidence"] = img_confidence + + resp_objects.append(obj) + + return resp_objects From 6a8d1d95d301ca9978a450ed3209b93d52cd6b26 Mon Sep 17 00:00:00 2001 From: h-alice Date: Thu, 16 Jan 2025 17:32:45 +0800 Subject: [PATCH 41/69] patch: Lint --- deepface/models/demography/Age.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/deepface/models/demography/Age.py b/deepface/models/demography/Age.py index e449aca..c960159 100644 --- a/deepface/models/demography/Age.py +++ b/deepface/models/demography/Age.py @@ -49,7 +49,8 @@ class ApparentAgeClient(Demography): List of images as List[np.ndarray] or Batch of images as np.ndarray (n, 224, 224, 3) Returns: - np.ndarray (age_classes,) if single image, np.ndarray (n, age_classes) if batched images. + np.ndarray (age_classes,) if single image, + np.ndarray (n, age_classes) if batched images. """ # Preprocessing input image or image list. imgs = self._preprocess_batch_or_single_input(img) @@ -60,8 +61,9 @@ class ApparentAgeClient(Demography): # Calculate apparent ages if len(age_predictions.shape) == 1: # Single prediction list return find_apparent_age(age_predictions) - else: # Batched predictions - return np.array([find_apparent_age(age_prediction) for age_prediction in age_predictions]) + + return np.array([ + find_apparent_age(age_prediction) for age_prediction in age_predictions]) def load_model( @@ -106,8 +108,8 @@ def find_apparent_age(age_predictions: np.ndarray) -> np.float64: Returns: apparent_age (float) """ - assert len(age_predictions.shape) == 1, "Input should be a list of age predictions, \ - not batched. Got shape: {}".format(age_predictions.shape) + assert len(age_predictions.shape) == 1, f"Input should be a list of predictions, \ + not batched. Got shape: {age_predictions.shape}" output_indexes = np.arange(0, 101) apparent_age = np.sum(age_predictions * output_indexes) return apparent_age From 0d7e15147f527edc0ef09dadbd73f38d87972d1f Mon Sep 17 00:00:00 2001 From: NatLee Date: Thu, 16 Jan 2025 20:48:59 +0800 Subject: [PATCH 42/69] [update] rm `print` --- deepface/modules/demography.py | 1 - 1 file changed, 1 deletion(-) diff --git a/deepface/modules/demography.py b/deepface/modules/demography.py index b9991d9..2258c1e 100644 --- a/deepface/modules/demography.py +++ b/deepface/modules/demography.py @@ -175,7 +175,6 @@ def analyze( task="facial_attribute", model_name="Age" ).predict(img_content) # int cast is for exception - object of type 'float32' is not JSON serializable - print(apparent_age.shape) obj["age"] = int(apparent_age) elif action == "gender": From db4b749c986eb622559c968fe85cd099accd46a0 Mon Sep 17 00:00:00 2001 From: Nat Lee Date: Mon, 20 Jan 2025 18:14:48 +0800 Subject: [PATCH 43/69] [update] add emotions batch test --- tests/test_analyze.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/test_analyze.py b/tests/test_analyze.py index 27805f5..5949497 100644 --- a/tests/test_analyze.py +++ b/tests/test_analyze.py @@ -1,5 +1,6 @@ # 3rd party dependencies import cv2 +import numpy as np # project dependencies from deepface import DeepFace @@ -136,7 +137,7 @@ def test_analyze_for_different_detectors(): else: assert result["gender"]["Man"] < result["gender"]["Woman"] -def test_analyze_for_multiple_faces(): +def test_analyze_for_multiple_faces_in_one_image(): img = "dataset/img4.jpg" # Copy and combine the same image to create multiple faces img = cv2.imread(img) @@ -147,4 +148,13 @@ def test_analyze_for_multiple_faces(): logger.debug(demography) assert demography["age"] > 20 and demography["age"] < 40 assert demography["dominant_gender"] == "Woman" - logger.info("✅ test analyze for multiple faces done") + logger.info("✅ test analyze for multiple faces in one image done") + +def test_batch_detect_emotion_for_multiple_faces(): + img = "dataset/img4.jpg" + img = cv2.imread(img) + imgs = [img, img] + results = DeepFace.demography.Emotion.EmotionClient().predict(imgs) + # Check two faces emotions are the same + assert np.array_equal(results[0], results[1]) + logger.info("✅ test batch detect emotion for multiple faces done") From 95bb92c933dcc2b1c591a6b4de7c992eabb9661a Mon Sep 17 00:00:00 2001 From: h-alice Date: Tue, 21 Jan 2025 11:25:33 +0800 Subject: [PATCH 44/69] Remove redundant squeeze. --- deepface/models/Demography.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/deepface/models/Demography.py b/deepface/models/Demography.py index 87869b9..1493059 100644 --- a/deepface/models/Demography.py +++ b/deepface/models/Demography.py @@ -62,8 +62,7 @@ class Demography(ABC): Four-dimensional numpy array (n, 224, 224, 3) """ image_batch = np.array(img) - # Remove batch dimension in advance if exists - image_batch = image_batch.squeeze() + # Check input dimension if len(image_batch.shape) == 3: # Single image - add batch dimension From 61b6931ea3eef95939cc690943d1e8337bda31bb Mon Sep 17 00:00:00 2001 From: Nat Lee Date: Tue, 21 Jan 2025 11:58:41 +0800 Subject: [PATCH 45/69] [update] modify test of `emotion` and add client of `age`, `gender` and `race` tests --- tests/test_analyze.py | 44 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/tests/test_analyze.py b/tests/test_analyze.py index 5949497..63b2686 100644 --- a/tests/test_analyze.py +++ b/tests/test_analyze.py @@ -4,6 +4,7 @@ import numpy as np # project dependencies from deepface import DeepFace +from deepface.models.demography import Age, Emotion, Gender, Race from deepface.commons.logger import Logger logger = Logger() @@ -150,11 +151,46 @@ def test_analyze_for_multiple_faces_in_one_image(): assert demography["dominant_gender"] == "Woman" logger.info("✅ test analyze for multiple faces in one image done") -def test_batch_detect_emotion_for_multiple_faces(): - img = "dataset/img4.jpg" - img = cv2.imread(img) +def test_batch_detect_age_for_multiple_faces(): + # Load test image and resize to model input size + img = cv2.resize(cv2.imread("dataset/img1.jpg"), (224, 224)) imgs = [img, img] - results = DeepFace.demography.Emotion.EmotionClient().predict(imgs) + results = Age.ApparentAgeClient().predict(imgs) + # Check there are two ages detected + assert len(results) == 2 + # Check two faces ages are the same + assert np.array_equal(results[0], results[1]) + logger.info("✅ test batch detect age for multiple faces done") + +def test_batch_detect_emotion_for_multiple_faces(): + # Load test image and resize to model input size + img = cv2.resize(cv2.imread("dataset/img1.jpg"), (224, 224)) + imgs = [img, img] + results = Emotion.EmotionClient().predict(imgs) + # Check there are two emotions detected + assert len(results) == 2 # Check two faces emotions are the same assert np.array_equal(results[0], results[1]) logger.info("✅ test batch detect emotion for multiple faces done") + +def test_batch_detect_gender_for_multiple_faces(): + # Load test image and resize to model input size + img = cv2.resize(cv2.imread("dataset/img1.jpg"), (224, 224)) + imgs = [img, img] + results = Gender.GenderClient().predict(imgs) + # Check there are two genders detected + assert len(results) == 2 + # Check two genders are the same + assert np.array_equal(results[0], results[1]) + logger.info("✅ test batch detect gender for multiple faces done") + +def test_batch_detect_race_for_multiple_faces(): + # Load test image and resize to model input size + img = cv2.resize(cv2.imread("dataset/img1.jpg"), (224, 224)) + imgs = [img, img] + results = Race.RaceClient().predict(imgs) + # Check there are two races detected + assert len(results) == 2 + # Check two races are the same + assert np.array_equal(results[0], results[1]) + logger.info("✅ test batch detect race for multiple faces done") \ No newline at end of file From 6df7b7d8e97470d8a59ec25a7c7e3dbe55955c6d Mon Sep 17 00:00:00 2001 From: h-alice Date: Wed, 22 Jan 2025 16:54:51 +0800 Subject: [PATCH 46/69] Add support for batched input. --- deepface/DeepFace.py | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/deepface/DeepFace.py b/deepface/DeepFace.py index 3abe6db..5ae05aa 100644 --- a/deepface/DeepFace.py +++ b/deepface/DeepFace.py @@ -174,7 +174,7 @@ 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: @@ -206,7 +206,10 @@ def analyze( anti_spoofing (boolean): Flag to enable anti spoofing (default is False). Returns: - results (List[Dict[str, Any]]): A list of dictionaries, where each dictionary represents + (List[List[Dict[str, Any]]]): A list of analysis results if received batched image, + explained below. + + (List[Dict[str, Any]]): A list of dictionaries, where each dictionary represents the analysis results for a detected face. Each dictionary in the list contains the following keys: @@ -253,6 +256,29 @@ def analyze( - 'middle eastern': Confidence score for Middle Eastern ethnicity. - '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: + resp_obj = demography.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 + return demography.analyze( img_path=img_path, actions=actions, From b584d29ce3b0218dcc6683449c2cbe22bea20f64 Mon Sep 17 00:00:00 2001 From: h-alice Date: Wed, 22 Jan 2025 16:55:09 +0800 Subject: [PATCH 47/69] Refine some tests. --- tests/test_analyze.py | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/tests/test_analyze.py b/tests/test_analyze.py index 5949497..8afdbcd 100644 --- a/tests/test_analyze.py +++ b/tests/test_analyze.py @@ -17,6 +17,7 @@ def test_standard_analyze(): demography_objs = DeepFace.analyze(img, silent=True) for demography in demography_objs: 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") @@ -30,6 +31,7 @@ def test_analyze_with_all_actions_as_tuple(): for demography in demography_objs: logger.debug(f"Demography: {demography}") + assert type(demography) == dict age = demography["age"] gender = demography["dominant_gender"] race = demography["dominant_race"] @@ -54,6 +56,7 @@ def test_analyze_with_all_actions_as_list(): for demography in demography_objs: logger.debug(f"Demography: {demography}") + assert type(demography) == dict age = demography["age"] gender = demography["dominant_gender"] race = demography["dominant_race"] @@ -75,6 +78,7 @@ def test_analyze_for_some_actions(): demography_objs = DeepFace.analyze(img, ["age", "gender"], silent=True) for demography in demography_objs: + assert type(demography) == dict age = demography["age"] gender = demography["dominant_gender"] @@ -96,6 +100,7 @@ def test_analyze_for_preloaded_image(): resp_objs = DeepFace.analyze(img, silent=True) for resp_obj in resp_objs: 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" @@ -132,23 +137,31 @@ 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_multiple_faces_in_one_image(): +def test_analyze_for_batched_image(): img = "dataset/img4.jpg" # Copy and combine the same image to create multiple faces img = cv2.imread(img) - img = cv2.hconcat([img, img]) - demography_objs = DeepFace.analyze(img, silent=True) - assert len(demography_objs) == 2 - for demography in demography_objs: - logger.debug(demography) - assert demography["age"] > 20 and demography["age"] < 40 - assert demography["dominant_gender"] == "Woman" - logger.info("✅ test analyze for multiple faces in one image done") + img = np.stack([img, img]) + assert len(img.shape) == 4 # Check dimension. + assert img.shape[0] == 2 # Check batch size. + + demography_batch = DeepFace.analyze(img, silent=True) + # 2 image in batch, so 2 demography objects. + assert len(demography_batch) == 2 + + for demography_objs in demography_batch: + assert len(demography_objs) == 1 # 1 face in each image + for demography in demography_objs: # Iterate over faces + assert type(demography) == dict # Check type + assert demography["age"] > 20 and demography["age"] < 40 + assert demography["dominant_gender"] == "Woman" + logger.info("✅ test analyze for multiple faces done") def test_batch_detect_emotion_for_multiple_faces(): img = "dataset/img4.jpg" From 0ab3ac2d51341cdcf4c43c7d5053280618293704 Mon Sep 17 00:00:00 2001 From: NatLee Date: Sat, 25 Jan 2025 17:42:41 +0800 Subject: [PATCH 48/69] [fix] avoid problem of precision in float --- tests/test_analyze.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_analyze.py b/tests/test_analyze.py index 46d4d16..a36acc5 100644 --- a/tests/test_analyze.py +++ b/tests/test_analyze.py @@ -171,8 +171,9 @@ def test_batch_detect_age_for_multiple_faces(): results = Age.ApparentAgeClient().predict(imgs) # Check there are two ages detected assert len(results) == 2 - # Check two faces ages are the same - assert np.array_equal(results[0], results[1]) + # Check two faces ages are the same in integer format(e.g. 23.6 -> 23) + # Must use int() to compare because of max float precision issue in different platforms + assert np.array_equal(int(results[0]), int(results[1])) logger.info("✅ test batch detect age for multiple faces done") def test_batch_detect_emotion_for_multiple_faces(): From 5bc8dc1a9f5e65d2a82ea886e468691d8fd2cf92 Mon Sep 17 00:00:00 2001 From: Sefik Ilkin Serengil Date: Tue, 4 Feb 2025 09:03:40 +0000 Subject: [PATCH 49/69] Update README.md remove product hunt and hackernews badges --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 830c71f..4e3e5cd 100644 --- a/README.md +++ b/README.md @@ -409,19 +409,19 @@ If you do like this work, then you can support it financially on [Patreon](https + Featured on Hacker News - DeepFace - A Lightweight Deep Face Recognition Library for Python | Product Hunt +--> ## Citation From 80d083a3e4e4cc353a82600b2a7f5cb59a0c4a4d Mon Sep 17 00:00:00 2001 From: Sefik Ilkin Serengil Date: Tue, 4 Feb 2025 14:36:23 +0000 Subject: [PATCH 50/69] Add files via upload --- icon/github_sponsor_button.png | Bin 0 -> 8072 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 icon/github_sponsor_button.png diff --git a/icon/github_sponsor_button.png b/icon/github_sponsor_button.png new file mode 100644 index 0000000000000000000000000000000000000000..e011d894279ae88ebd2c0de64613ac9dbf543f7d GIT binary patch literal 8072 zcmY+JcQ{;4^!SM&B1oe5M1mk%5Sa4QtTK4OGzu)KgJikBA+&lN4dCtt4xij~iPmF<{I@Mj4yF^4pRGJzphD1cfzBl)3 zcgSwaStBUu=0facsIE*@J;JtqQ@QP+q^m?kR12oW+mPPWDcm*Qdl3;aaQ`dBnubr0 zh=@4ZHC2?}`djVil7FDrDY4hipeJE&CPWzxY;{6 zNZK%xeylZpo(`o7NxZ^ipT@?4%M|r;3wwLLzHEuqF6aIMLaHz}jfi*VMy5=5@bt7o zufN40QPEUx?b|S?7kqFw`M~%ld|etfXH3zTdWyX%Y=p)pp{i;AreS9j&ern<_tU2n z1wHCpsH%lozc!D+z3AC`;dILA#KclO_9VvS5vAS8QxMdVoh@k*$bH+y+@=t-lUMLs??*Y2_by zD5AaL#`4_p#15oNEQZ?exhqYkflhO#E{%1cDm%Z*s9m@^>I4$+ z&C-tM0>alA`?-V{x#oxdqzz*(b7KzaYw_s5%IjLiA5VC zmg>va)`dFdHVLYok3(2hVr&~TAeEa;vcg|xq9V_m|Mhf}%xhp*qNPmG{&7#GiHU6^ zVnVC3$;?c1%jr?j-oz$^u%3_j*Ru#_JxC-AU}nUsO9{eWIZYXZ2z*>?Cj8W7H*y2$ zoZ!7TF+jOWm@7B_Pus+SuIgLjQn&Zj|7X1{4Ui#}e+6xvHgm3OWFFz~z6jjgo-x(< zw{wh;4%7WUpm0WcaH3^Epj~Ctyvu(=?cow%TNcz~(^$vye>x^XnvnEF%a#Dl>8Fxk z|2q;AV7N7S$+i|>Ki3!}NyRLaW3@e#A2Qc!Ty=Gsl$xqSK|z7n)X{nCOt3oQthZ{r z=6%qvVK-c8F*g3L>KfyaeoDxua}39xVtf`-xtSN#_IY^b$tA{ z;jmjHV!Hs$A+H4`bfz@9t>~df^2==dVhzW+R4YEltkW^mr`cZH51UWaK(Bbj-{i>! zI1a3<(me=24<^^+)Uj?!(m|osL8&9(C0Om0r6TbIj6&|gjUx{qC}knq`bkQlY)g>y zxCZaJ7X{pk)z;csFCOczo-(Vt%^g!N9B&>ZCkE%b?`{244`5_)<4m#)%z8|Bx{YY# zL&G9cf31a?s?)uZW3;O=OZHq0ecg3hmj0iN<;0mnc` zsYS*wNW{Y7WE*26aGc8#2Ol3j-_eD4;>^BqD^kEXO4)5#Bz$a?O!#SMS?)9`^$lp>m*aZb);^h5&55>y$+v{Z3IDJ zLQE0+#Y_?H9jnK#6X^Fs!g1;gHK+9YKuv^+dGK!>jfc-Hs;_gZhS2pxk3C1)%f$N? z&qS~fN3qL7lQi~>>z%i_F~p4Y^KWcv_7-k8(r6~8m%uEhSUPBYNS_b<(@@r z!)yKO&tt4@9j_H-b7$rdan#50T|XFv@UGhOcKm3$nN56c5vjY^zE6((4yvh-Z=Z%u zZsIAWLoNuTAIsV}fgIEI4jf3Ep&0@7{u$T0d^Ng-2_=~GaY}8s`W|*8gSKmfg-wF6 z4vzSAvj(7U9Ms~U`P)!cTB!>MS)T$Da!@X}Cr-0TO5*OXLUA85%Z{+*$tR7HDR6iQd2OPY&?;BQ0|rc>Z%O`>D>(n zh`HmEfwe%eq0}y=`lzRr#28U=$%8QqI(hsaLd*TabJy>Ug<0pk8&MQ-pxUG&KI|?% zf3H*0b1tsWT50^Mc3K&wSW1TSRF&VJn}s_FhzFBXv&&@~W`9AQoQA!y_lrvFLF=v* zVXof6=?X{H5IEfIeLDMQFkM1mb12Jb6VQV#u|sYZsUw|E(ghoUXDr`Bsro2$b8>W~ z`!;?54rP)2Rc8+&1ZR}Plw=+6IxcPb9*c(GWk~htZlbBT zgO7rYPlk`_8`fY{4T`>s+Vlo>%}6bVg*JkyRjZ zz|%}95&o@5UCLv*y$(&&5bQMPe_GHJRbhUfS1BPMTTVfNOLQHS!-i|Ve&wKQlnZ0nM6^$LprjJD9+ z986m?@&|Dlk(&3`&$ z@DT{0Zzv$azQfnf=p`uQW*I#uY;kf?$P@;4<~^avKtBI}@B zh40cve1%lIAwL)|JBN59H0Rn53cDl748L(j-l({T)qvSyyrz#tTo+K0R^FrX^iwRt zij|fEEW_8J&!mMm5xA*cY+mXUb?Qd7&sL6Ampji?r(R37w2O+K4nnS`SO#X`>HgJ) zhPU)uLJ8r8Hp!?Mim!`D!5=UwG1v}j7OAj2*9ASPk&gBe-TG49*~dlWS^}@5sLny7 zek+!V%Rzq0hT+@GCjqVo;_ja=963VQHC>a4WrE@ugS$tN-K}R|yfSjTKHOuK`Ld&M z`Y1|~4tidTnQ#L50HmS7G6f{#fXc%)H zL%J>&Yu-Ft#pmr$y_X7I(PWILpk2AMHAT#3NwfBJEZe8vcV8y(r|!bd@z6Er1Hwp~ zL2V+;kjSd$l>%z^e5#$P?)FjCWRY$*I%hZ4ly^`v7grJnxYD@#s+23!6*BEMJ1aKH zb-!hPYi>6(OpkpC=l!w+J1whKl5n;@*gY)94x*M}BEfZN^+ZwmD5UY@B##JBm$P4_ z@jxY2`Gn1zU5a>yIlM|k?ddEMEt_0h(Lx<3w3qG7cA-A(_+ zjRzbmPc#uWQR0RaAg$+c9O|^V1rt8$9-!frj_qOj1U-!Fz#3HnjT=o~c&umhKRqcb zbHn5qxJ_}wx~VW;TVrcGOn>m5$j9#+fsJ=HPu+`hn)meF{z5g%ZsLFpt3XvJP2chC zYA?do=_-j2K2i1N%h>9r2u3cO3%x>yU6IfaW|R%4@q5ftRi|(G{|*iI-=2_~Zks|7 zbs2n^tMlWdmkT{u{JmX3z8gJC($2|Lh}L)W{C3ceq`uC)On}tL9@2TNM?7ZNT=g`Z z=lp#yM<13w-VGO{-~cDyo(4eqm%V7{1r1gDG~7w$d>8%oXhA^*C6pT^Mp@VgzerimlnN(1JyiEn%+}RczY; zEEFwpdj)f~8?Jo`F4ZqWBdy9*DN!d>wvw!p|GXanm4i6~iI_^e?2Z7#-_Z{LT+}YU zatwJG;IaNlkqz<2p@W6{%5|J?M!VZr*;1V04@Y!S zgw%de=#brZ3EUlyX}Ew|Uq~lcL06sh3Am$)5-^)|p`L^0Kr(kOdbME#WoO{>u>TU@ zeQDHtKKr&RC7uC>jHocWK(d;h2s?anJmPf0G12HtxN=ohi1rMjDi_O-<86gYuG{ZD zBrMjCj=j@{7W*xSq%a6w$l0^*d)(P)(;&8Mfj_<(Y>ax^ z@`iQiCJ?n;S$Wvns~_rdR;C%QOBHHW^WtY$=X2k_Z$>B8B{#X`{as^W2Mgaz?1~EcnC42O7q9*(yB?FzJrkNe)_xZsIoHIqt>v_y?;Szs z(&(%g1FXp`pJ=HU-YJaF&R7@uD1t`CUVa##RFQK{U7vVI5*5W7-ZBn3K`Lc+28*Qh zkYmhYZOuhW0K)?1fNIKhkLSau;d?nZM^GiRxFgZZGty{vCct0Uu~{y?j37?Q^`3bQX&sRXu!m3;&NI>c;Z=>=It*b`}7kD>F{pj)Q#5!qNBAqfQonR~?R z=jjvVPPf)l9q6*t_dlnuA3`kS<{Mp*SW9u>Y?zsNXnfCiHDuk6Fe%EMnznS$P? zJxt|$p9v5NdlB;R_o;T1RS%eo)RbAMNy)`@Iy__S;h z`Sc**Pv)Cdve#rh?yg&;Z81xX5asLDiK$}KT3i07-9r=!d2mTaFG_w|5v%CS^kCh^Tm`dWWzC!{` zL*f+u`@pYJ+1YNL?<1dPS|&4`R&ouKZhGsybe-3sWDqJQrR?PmvuyXTMc%)TX#mbm zAWqK4E1ir+aj7FI1`+IS*IMi~qj2Y%9$lvgaCKlPe+kLu8GD@rilJ~-w+U! zv1~V;)fw_d%W5`Q_G&UUV8%Xx<@5u<*7lZbjS7Xds!@a$>$(Sh7zdaPK*-!7U&JcC z8L#)M52|U;Dnl-9z%xdL71IMN=c(4$qDAX_sQUj|@jnp^Jl=l!wn9q+c(pMpeuC7` zAydkLsv}+-Sx7wkixG=)Nqugd+(RU9A)Xq(V{KuPn~9w)yqRTQkBVgOh*G~9(mmq< zpLQ~;*Y*|?&#I7KYUFuKbf1FE82R>G92=0LlVxciwv7ZvOMY466_< zx6>J{WtF0z2n}@H`HlF77t#kUk$EUeW+7?G;#d z-Yv~_eUlmSjO6mGUL-yt@%p~u5sZbbFMUYMT2V|>?o%AcdE}Kc#1CgnPHxTgkM*3f z9xeJ?E_$UbAl&`$){ZxseTaH?`I(fcdE=*UeYy3o<~?7jZczh1 zlp`Y^CG&R>6fvRsij(>+dJ@oo%P_JcA8xx=GiHN~xg^EcMl?Y;$6 z^p+tZ?AM}UF_M$oacaA2J~X7O{_A;hSsdr@fiu;rjn0UVadQH$r~vq|q;~#~6Xwj# zEGdX_m!_L-KlwOM28%E+$%VV8G`3??>a_#ERfeCC=(yKOdo+%!%!J@v70v=U4<1M! zn|RRYW#mmss2$Me_RIDJzSO}@9m0h(j$Vu7BC?|&5>C0bQ&*$d7e83l_v+|30HT5x zXBi1%qnSsKql`bcOUtdXLeA3NMh2)(KO`^`t~8mMIhmO?X{LVX273Kdd3nD(e9fnUMM@}sft*1E2!c@d#4`-@4o#+vELG^`JZe~u?9AWM=fX5=5L=dy)&~;S-qsqTt)K| zE?z(HKzn#Bf1T!Z!wv7)Jm-9|3}OsM+v0*Gc4>k09($d0MaP%a@(H!RQh(v30qDWy z)1Nf=tUAA7|KcL%gu5qTaaI*U{p4MM5t-$W$8=a~7QX<0piiABjP-DK8XZU$Z;x^C z!tbOcC235S>b}#u74;L@h`7DLN!vk|urO#0>2;6@J<=O2pS&Hvsn3tu{aaI|!FF{vN2-F&C) zAjF@-NVKTPO9sso-i!iED)1TYl+L3GqrKV{KI1$Fx5F&yP{PUA_9-z#U1Gy|X+7fT6Mm|fkqL((b*lH=a%ptk(TV$ zxwQI~yL4|`+vuLP+)PD1>bTB+m<1&9AayD+ia5u*`9MBr6kga|T^1cqIlzbyY{7l( z6YsjT1m_gxjr#7WkkfE{Ot{K~x)-3>SAL}Jemj}k$=JDW5nnbxDAmqzwIzgX9FnYB z5o5-w^7!?>O*OeCYX!#F`vo4nFL^rYF!ZB(De&m<=rn5*ghzwA8@9#D(M7sBI1&(H zbMSWvtOB9$*pB)$yf^3Nh|)Vdpcm`VWEnjF;E3#%bgm?1Fw3G z@(M^E5&XkmWiW;XoG6r$b5v^E9ag_eV3k}uFKgKztCd$(Rg*dUjqYcyu!A$Ol`+5Y zW?Hf=;JVo!b2SV{6J}g%Vm5smDu!yDytOS={6iZIaB^7 zu58xnc;j~>zezi60E}x?9VV2)|i*?c0`H_&k5b zgh1ym&l}XfrJyP*QK~qm(iWpO*{mRp*?ygNOOAzlbqEUv zGQ~5CeRuWgUl=~#)cwx!H9{=lCi2U3aQ++G1W*rLC0^0~C4OKf)1`UUy0uTYR*;jb zvG4CckS+Q3e3Ysw0UCcYED4uCd&SOk$D!w)Q0y(zyAu~^fIRZ3t5UHmp=+d2J=CeV zNl_u8BKt`neWrA8WNYSG_vyrp8o2c6XiA1H!&h$7aOKln-FQ6nWQ9@$4rsaaqtgv~ z!q69e2-n^@>#iqe>ZDLHOlF(7Gm^F+I`EJio2$(?UNPy_9vY5LkMg1t77}wA@E@j)!%k)(57{4f0MlqA*rlp zo$n2;g-3()OuGGDSnRur@7Vh2Lk(J?orjJ=e_xws?j9s&6j#=(s~+p_e0vfVl=qxk zWkx7>8ws{I$Xd{n#V&0I?CgO?JN>5^^-p{crxQZKuAocLZ|5^q>z@(U&L=$nDh!1E z9uMR<0vo>CpR-*160_gyw#$cNrhc@$0q4w{Vdzu@)&JmMIhror~9;)H2C7G6y z=PMcaefoM_uqT@-36?By&A2|=8co!w*ORUhxdd$W)H+Dk`{+Kqly{OAoq!s4?5H$J zL689j@0`7o3w}wHSEvW*H27tHln8n(kS+{iyEMb@yln@w3x-`I%b5rAGWk~wGa(7H zd^5Os+wAD&_J!I}#zc8RY~{aD$mXp0wv_Lok3Jb0^_5oXYqRGJ@D)-pAZjLq(D%;{ zq*~Wd`v$<6m{&;@eZH2*h~OD16A_2EVZSP3A$JEw$!T z65@yYxkMj?zF*nIF#kVNRyz;3E~2HSol{d&tFwcJSXMh;1jj0}2)9E&QU91T6Rbi^ z{9myzG41h>AIbGXTdar_3Bo;%`6LJitrR^Suo#EUfdGv5L60c*9y*AyvMBGi%7&LQx@CWQ~x9 q-Qjbj$4=KIpxN{}gSJ}K7NI=n5l$T;K6pdVi8NL9RH~IfeEuKRPqmBy literal 0 HcmV?d00001 From 76af76f3851c98e72839c3f6cfaad7fa8d8bff26 Mon Sep 17 00:00:00 2001 From: Sefik Ilkin Serengil Date: Tue, 4 Feb 2025 14:37:50 +0000 Subject: [PATCH 51/69] Update README.md github sponsor button --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 4e3e5cd..e810a6d 100644 --- a/README.md +++ b/README.md @@ -405,6 +405,10 @@ If you do like this work, then you can support it financially on [Patreon](https + + + + From 0652aba338a9e26d8f3bf882bfa61ecdc7da8293 Mon Sep 17 00:00:00 2001 From: Sefik Ilkin Serengil Date: Tue, 4 Feb 2025 14:38:18 +0000 Subject: [PATCH 52/69] Update README.md github sponsor button size --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e810a6d..4fac7f1 100644 --- a/README.md +++ b/README.md @@ -406,7 +406,7 @@ If you do like this work, then you can support it financially on [Patreon](https - + From 5f297575e3b2980b346098c90c044f7b8abc747c Mon Sep 17 00:00:00 2001 From: Sefik Ilkin Serengil Date: Tue, 4 Feb 2025 14:38:33 +0000 Subject: [PATCH 53/69] Update README.md github sponsor button size --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4fac7f1..5e824b0 100644 --- a/README.md +++ b/README.md @@ -406,7 +406,7 @@ If you do like this work, then you can support it financially on [Patreon](https - + From 72e82f06054cc870e9c69f3880a83487d9932e24 Mon Sep 17 00:00:00 2001 From: Sefik Ilkin Serengil Date: Tue, 4 Feb 2025 14:38:53 +0000 Subject: [PATCH 54/69] Update README.md github sponsor button size --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5e824b0..59da5ee 100644 --- a/README.md +++ b/README.md @@ -406,7 +406,7 @@ If you do like this work, then you can support it financially on [Patreon](https - + From 0ef420bc10c8a863501f23235d1d4c7fa35ef1ba Mon Sep 17 00:00:00 2001 From: galthran-wq Date: Tue, 11 Feb 2025 13:01:29 +0000 Subject: [PATCH 55/69] batched inputs in representation --- deepface/models/FacialRecognition.py | 10 +- deepface/modules/representation.py | 138 ++++++++++++++++----------- 2 files changed, 88 insertions(+), 60 deletions(-) diff --git a/deepface/models/FacialRecognition.py b/deepface/models/FacialRecognition.py index a6ee7b5..ae9958e 100644 --- a/deepface/models/FacialRecognition.py +++ b/deepface/models/FacialRecognition.py @@ -18,7 +18,7 @@ class FacialRecognition(ABC): input_shape: Tuple[int, int] output_shape: int - def forward(self, img: np.ndarray) -> List[float]: + def forward(self, img: np.ndarray) -> Union[List[float], List[List[float]]]: if not isinstance(self.model, Model): raise ValueError( "You must overwrite forward method if it is not a keras model," @@ -26,4 +26,10 @@ class FacialRecognition(ABC): ) # model.predict causes memory issue when it is called in a for loop # embedding = model.predict(img, verbose=0)[0].tolist() - return self.model(img, training=False).numpy()[0].tolist() + if img.shape == 4 and img.shape[0] == 1: + img = img[0] + embeddings = self.model(img, training=False).numpy() + if embeddings.shape[0] == 1: + return embeddings[0].tolist() + else: + return embeddings.tolist() diff --git a/deepface/modules/representation.py b/deepface/modules/representation.py index d880645..f0fb1db 100644 --- a/deepface/modules/representation.py +++ b/deepface/modules/representation.py @@ -11,7 +11,7 @@ from deepface.models.FacialRecognition import FacialRecognition def represent( - img_path: Union[str, np.ndarray], + img_path: Union[str, np.ndarray, List[Union[str, np.ndarray]]], model_name: str = "VGG-Face", enforce_detection: bool = True, detector_backend: str = "opencv", @@ -25,9 +25,9 @@ def represent( Represent facial images as multi-dimensional vector embeddings. 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, or list): The exact path to the image, a numpy array in BGR format, + a base64 encoded image, or a list of these. If the source image contains multiple faces, + the result will include information for each detected face. model_name (str): Model for face recognition. Options: VGG-Face, Facenet, Facenet512, OpenFace, DeepFace, DeepID, Dlib, ArcFace, SFace and GhostFaceNet @@ -70,70 +70,92 @@ def represent( task="facial_recognition", model_name=model_name ) - # --------------------------------- - # we have run pre-process in verification. so, this can be skipped if it is coming from verify. - target_size = model.input_shape - if detector_backend != "skip": - 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, - anti_spoofing=anti_spoofing, - max_faces=max_faces, - ) - else: # skip - # Try load. If load error, will raise exception internal - img, _ = image_utils.load_image(img_path) + # Handle list of image paths or 4D numpy array + if isinstance(img_path, list): + images = img_path + elif isinstance(img_path, np.ndarray) and img_path.ndim == 4: + images = [img_path[i] for i in range(img_path.shape[0])] + else: + images = [img_path] - if len(img.shape) != 3: - raise ValueError(f"Input img must be 3 dimensional but it is {img.shape}") + batch_images = [] + batch_regions = [] + batch_confidences = [] - # make dummy region and confidence to keep compatibility with `extract_faces` - img_objs = [ - { - "face": img, - "facial_area": {"x": 0, "y": 0, "w": img.shape[0], "h": img.shape[1]}, - "confidence": 0, - } - ] - # --------------------------------- + for single_img_path in images: + # --------------------------------- + # we have run pre-process in verification. so, this can be skipped if it is coming from verify. + target_size = model.input_shape + if detector_backend != "skip": + img_objs = detection.extract_faces( + img_path=single_img_path, + detector_backend=detector_backend, + grayscale=False, + enforce_detection=enforce_detection, + align=align, + expand_percentage=expand_percentage, + anti_spoofing=anti_spoofing, + max_faces=max_faces, + ) + else: # skip + # Try load. If load error, will raise exception internal + img, _ = image_utils.load_image(single_img_path) - if max_faces is not None and max_faces < len(img_objs): - # sort as largest facial areas come first - img_objs = sorted( - img_objs, - key=lambda img_obj: img_obj["facial_area"]["w"] * img_obj["facial_area"]["h"], - reverse=True, - ) - # discard rest of the items - img_objs = img_objs[0:max_faces] + if len(img.shape) != 3: + raise ValueError(f"Input img must be 3 dimensional but it is {img.shape}") - for img_obj in img_objs: - if anti_spoofing is True and img_obj.get("is_real", True) is False: - raise ValueError("Spoof detected in the given image.") - img = img_obj["face"] + # make dummy region and confidence to keep compatibility with `extract_faces` + img_objs = [ + { + "face": img, + "facial_area": {"x": 0, "y": 0, "w": img.shape[0], "h": img.shape[1]}, + "confidence": 0, + } + ] + # --------------------------------- - # bgr to rgb - img = img[:, :, ::-1] + if max_faces is not None and max_faces < len(img_objs): + # sort as largest facial areas come first + img_objs = sorted( + img_objs, + key=lambda img_obj: img_obj["facial_area"]["w"] * img_obj["facial_area"]["h"], + reverse=True, + ) + # discard rest of the items + img_objs = img_objs[0:max_faces] - region = img_obj["facial_area"] - confidence = img_obj["confidence"] + for img_obj in img_objs: + if anti_spoofing is True and img_obj.get("is_real", True) is False: + raise ValueError("Spoof detected in the given image.") + img = img_obj["face"] - # resize to expected shape of ml model - img = preprocessing.resize_image( - img=img, - # thanks to DeepId (!) - target_size=(target_size[1], target_size[0]), - ) + # bgr to rgb + img = img[:, :, ::-1] - # custom normalization - img = preprocessing.normalize_input(img=img, normalization=normalization) + region = img_obj["facial_area"] + confidence = img_obj["confidence"] - embedding = model.forward(img) + # resize to expected shape of ml model + img = preprocessing.resize_image( + img=img, + # thanks to DeepId (!) + target_size=(target_size[1], target_size[0]), + ) + # custom normalization + img = preprocessing.normalize_input(img=img, normalization=normalization) + + batch_images.append(img) + batch_regions.append(region) + batch_confidences.append(confidence) + + # Convert list of images to a numpy array for batch processing + batch_images = np.concat(batch_images) + + # Forward pass through the model for the entire batch + embeddings = model.forward(batch_images) + + for embedding, region, confidence in zip(embeddings, batch_regions, batch_confidences): resp_objs.append( { "embedding": embedding, From 72919d95f416e2e501166a8194542bd764ca5111 Mon Sep 17 00:00:00 2001 From: galthran-wq Date: Tue, 11 Feb 2025 13:27:13 +0000 Subject: [PATCH 56/69] typo --- deepface/modules/representation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deepface/modules/representation.py b/deepface/modules/representation.py index f0fb1db..a017114 100644 --- a/deepface/modules/representation.py +++ b/deepface/modules/representation.py @@ -150,7 +150,7 @@ def represent( batch_confidences.append(confidence) # Convert list of images to a numpy array for batch processing - batch_images = np.concat(batch_images) + batch_images = np.concatenate(batch_images, axis=0) # Forward pass through the model for the entire batch embeddings = model.forward(batch_images) From d7a985bf128db24761477894b0bedb95190c1f87 Mon Sep 17 00:00:00 2001 From: galthran-wq Date: Tue, 11 Feb 2025 13:34:50 +0000 Subject: [PATCH 57/69] update DeepFace represent method --- deepface/DeepFace.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/deepface/DeepFace.py b/deepface/DeepFace.py index 3abe6db..51cc8e2 100644 --- a/deepface/DeepFace.py +++ b/deepface/DeepFace.py @@ -373,7 +373,7 @@ def find( def represent( - img_path: Union[str, np.ndarray, IO[bytes]], + img_path: Union[str, np.ndarray, IO[bytes], List[Union[str, np.ndarray]]], model_name: str = "VGG-Face", enforce_detection: bool = True, detector_backend: str = "opencv", @@ -387,10 +387,12 @@ def represent( Represent facial images as multi-dimensional vector embeddings. 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], or List[Union[str, np.ndarray]]): 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. + the result will include information for each detected face. If a list is provided, + each element should be a string or numpy array representing an image, and the function + will process images in batch. model_name (str): Model for face recognition. Options: VGG-Face, Facenet, Facenet512, OpenFace, DeepFace, DeepID, Dlib, ArcFace, SFace and GhostFaceNet From bb134b25d2110886667ac550009168b75c6dad04 Mon Sep 17 00:00:00 2001 From: galthran-wq Date: Tue, 11 Feb 2025 13:56:09 +0000 Subject: [PATCH 58/69] compatibility --- deepface/modules/representation.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/deepface/modules/representation.py b/deepface/modules/representation.py index a017114..6bdbf1b 100644 --- a/deepface/modules/representation.py +++ b/deepface/modules/representation.py @@ -154,6 +154,8 @@ def represent( # Forward pass through the model for the entire batch embeddings = model.forward(batch_images) + if len(batch_images) == 1: + embeddings = [embeddings] for embedding, region, confidence in zip(embeddings, batch_regions, batch_confidences): resp_objs.append( From c60152e9a55fb137b313def615ef95d6d828acf6 Mon Sep 17 00:00:00 2001 From: galthran-wq Date: Tue, 11 Feb 2025 16:58:44 +0000 Subject: [PATCH 59/69] batched represent --- tests/test_represent.py | 47 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/tests/test_represent.py b/tests/test_represent.py index b33def7..f09834e 100644 --- a/tests/test_represent.py +++ b/tests/test_represent.py @@ -2,6 +2,7 @@ import io import cv2 import pytest +import numpy as np # project dependencies from deepface import DeepFace @@ -81,3 +82,49 @@ def test_max_faces(): max_faces = 1 results = DeepFace.represent(img_path="dataset/couple.jpg", max_faces=max_faces) assert len(results) == max_faces + + +def test_batched_represent(): + img_paths = [ + "dataset/img1.jpg", + "dataset/img2.jpg", + "dataset/img3.jpg", + "dataset/img4.jpg", + "dataset/img5.jpg", + ] + + def _test_for_model(model_name: str): + embedding_objs = DeepFace.represent(img_path=img_paths, model_name=model_name) + assert len(embedding_objs) == len(img_paths) + if model_name == "VGG-Face": + for embedding_obj in embedding_objs: + embedding = embedding_obj["embedding"] + logger.debug(f"Function returned {len(embedding)} dimensional vector") + assert len(embedding) == 4096 + embedding_objs_one_by_one = [ + embedding_obj + for img_path in img_paths + for embedding_obj in DeepFace.represent(img_path=img_path, model_name=model_name) + ] + for embedding_obj_one_by_one, embedding_obj in zip(embedding_objs_one_by_one, embedding_objs): + assert np.allclose( + embedding_obj_one_by_one["embedding"], + embedding_obj["embedding"], + rtol=1e-2, + atol=1e-2 + ) + + for model_name in [ + "VGG-Face", + "Facenet", + "Facenet512", + "OpenFace", + # "DeepFace", + "DeepID", + # "Dlib", + "ArcFace", + "SFace", + "GhostFaceNet" + ]: + _test_for_model(model_name) + logger.info("✅ test batch represent function done") From 8fb70eb43fd4c0002718f661dca8234a18253632 Mon Sep 17 00:00:00 2001 From: galthran-wq Date: Tue, 11 Feb 2025 17:00:59 +0000 Subject: [PATCH 60/69] VGGFace batched inference --- deepface/models/facial_recognition/VGGFace.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/deepface/models/facial_recognition/VGGFace.py b/deepface/models/facial_recognition/VGGFace.py index bfcbcad..a3ea2e3 100644 --- a/deepface/models/facial_recognition/VGGFace.py +++ b/deepface/models/facial_recognition/VGGFace.py @@ -57,8 +57,7 @@ class VggFaceClient(FacialRecognition): def forward(self, img: np.ndarray) -> List[float]: """ Generates embeddings using the VGG-Face model. - This method incorporates an additional normalization layer, - necessitating the override of the forward method. + This method incorporates an additional normalization layer. Args: img (np.ndarray): pre-loaded image in BGR @@ -70,8 +69,14 @@ 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 = self.model(img, training=False).numpy()[0].tolist() - embedding = verification.l2_normalize(embedding) + embedding = super().forward(img) + if ( + isinstance(embedding, list) and + isinstance(embedding[0], list) + ): + embedding = verification.l2_normalize(embedding, axis=1) + else: + embedding = verification.l2_normalize(embedding) return embedding.tolist() From 035d3c8ba8bd17d31b76e07d8cb020238cf334b8 Mon Sep 17 00:00:00 2001 From: galthran-wq Date: Tue, 11 Feb 2025 17:01:24 +0000 Subject: [PATCH 61/69] SFace pseudo-batched inference --- deepface/models/facial_recognition/SFace.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/deepface/models/facial_recognition/SFace.py b/deepface/models/facial_recognition/SFace.py index f6a01ca..53dfb86 100644 --- a/deepface/models/facial_recognition/SFace.py +++ b/deepface/models/facial_recognition/SFace.py @@ -1,5 +1,5 @@ # built-in dependencies -from typing import Any, List +from typing import Any, List, Union # 3rd party dependencies import numpy as np @@ -27,7 +27,7 @@ class SFaceClient(FacialRecognition): self.input_shape = (112, 112) self.output_shape = 128 - def forward(self, img: np.ndarray) -> List[float]: + def forward(self, img: np.ndarray) -> Union[List[float], List[List[float]]]: """ Find embeddings with SFace model This model necessitates the override of the forward method @@ -37,14 +37,18 @@ class SFaceClient(FacialRecognition): Returns embeddings (list): multi-dimensional vector """ - # return self.model.predict(img)[0].tolist() + input_blob = (img * 255).astype(np.uint8) - # revert the image to original format and preprocess using the model - input_blob = (img[0] * 255).astype(np.uint8) + embeddings = [] + for i in range(input_blob.shape[0]): + embedding = self.model.model.feature(input_blob[i]) + embeddings.append(embedding) + embeddings = np.concatenate(embeddings, axis=0) - embeddings = self.model.model.feature(input_blob) - - return embeddings[0].tolist() + if embeddings.shape[0] == 1: + return embeddings[0].tolist() + else: + return embeddings.tolist() def load_model( From 3a9385fad8cd3027889ac41463339ad64cdaaf85 Mon Sep 17 00:00:00 2001 From: galthran-wq Date: Tue, 11 Feb 2025 17:03:00 +0000 Subject: [PATCH 62/69] List->Sequence typing --- deepface/DeepFace.py | 8 ++++---- deepface/modules/representation.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/deepface/DeepFace.py b/deepface/DeepFace.py index 51cc8e2..9f3ff1e 100644 --- a/deepface/DeepFace.py +++ b/deepface/DeepFace.py @@ -2,7 +2,7 @@ import os import warnings import logging -from typing import Any, Dict, IO, List, Union, Optional +from typing import Any, Dict, IO, List, Union, Optional, Sequence # this has to be set before importing tensorflow os.environ["TF_USE_LEGACY_KERAS"] = "1" @@ -373,7 +373,7 @@ def find( def represent( - img_path: Union[str, np.ndarray, IO[bytes], List[Union[str, np.ndarray]]], + img_path: Union[str, np.ndarray, IO[bytes], Sequence[Union[str, np.ndarray, IO[bytes]]]], model_name: str = "VGG-Face", enforce_detection: bool = True, detector_backend: str = "opencv", @@ -387,10 +387,10 @@ def represent( Represent facial images as multi-dimensional vector embeddings. Args: - img_path (str, np.ndarray, IO[bytes], or List[Union[str, np.ndarray]]): The exact path to the image, a numpy array + img_path (str, np.ndarray, IO[bytes], or Sequence[Union[str, np.ndarray, IO[bytes]]]): 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. If a list is provided, + the result will include information for each detected face. If a sequence is provided, each element should be a string or numpy array representing an image, and the function will process images in batch. diff --git a/deepface/modules/representation.py b/deepface/modules/representation.py index 6bdbf1b..d36f5bc 100644 --- a/deepface/modules/representation.py +++ b/deepface/modules/representation.py @@ -1,5 +1,5 @@ # built-in dependencies -from typing import Any, Dict, List, Union, Optional +from typing import Any, Dict, List, Union, Optional, Sequence, IO # 3rd party dependencies import numpy as np @@ -11,7 +11,7 @@ from deepface.models.FacialRecognition import FacialRecognition def represent( - img_path: Union[str, np.ndarray, List[Union[str, np.ndarray]]], + img_path: Union[str, IO[bytes], np.ndarray, Sequence[Union[str, np.ndarray, IO[bytes]]]], model_name: str = "VGG-Face", enforce_detection: bool = True, detector_backend: str = "opencv", @@ -25,8 +25,8 @@ def represent( Represent facial images as multi-dimensional vector embeddings. Args: - img_path (str, np.ndarray, or list): The exact path to the image, a numpy array in BGR format, - a base64 encoded image, or a list of these. If the source image contains multiple faces, + img_path (str, np.ndarray, or Sequence[Union[str, np.ndarray]]): The exact path to the image, a numpy array in BGR format, + a base64 encoded image, or a sequence of these. If the source image contains multiple faces, the result will include information for each detected face. model_name (str): Model for face recognition. Options: VGG-Face, Facenet, Facenet512, From a4a579e5eb0913029a9debae3a480aac01012b98 Mon Sep 17 00:00:00 2001 From: galthran-wq Date: Tue, 11 Feb 2025 17:21:01 +0000 Subject: [PATCH 63/69] dlib pseudo-batched forward --- deepface/models/facial_recognition/Dlib.py | 42 ++++++++++++---------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/deepface/models/facial_recognition/Dlib.py b/deepface/models/facial_recognition/Dlib.py index 7b29dec..0d58bb8 100644 --- a/deepface/models/facial_recognition/Dlib.py +++ b/deepface/models/facial_recognition/Dlib.py @@ -1,5 +1,5 @@ # built-in dependencies -from typing import List +from typing import List, Union # 3rd party dependencies import numpy as np @@ -26,35 +26,39 @@ class DlibClient(FacialRecognition): self.input_shape = (150, 150) self.output_shape = 128 - def forward(self, img: np.ndarray) -> List[float]: + def forward(self, img: np.ndarray) -> Union[List[float], List[List[float]]]: """ Find embeddings with Dlib model. This model necessitates the override of the forward method because it is not a keras model. Args: - img (np.ndarray): pre-loaded image in BGR + img (np.ndarray): pre-loaded image(s) in BGR Returns - embeddings (list): multi-dimensional vector + embeddings (list of lists or list of floats): multi-dimensional vectors """ - # return self.model.predict(img)[0].tolist() + # Handle single image case + if len(img.shape) == 3: + img = np.expand_dims(img, axis=0) - # extract_faces returns 4 dimensional images - if len(img.shape) == 4: - img = img[0] + embeddings = [] + for single_img in img: + # bgr to rgb + single_img = single_img[:, :, ::-1] # bgr to rgb - # bgr to rgb - img = img[:, :, ::-1] # bgr to rgb + # img is in scale of [0, 1] but expected [0, 255] + if single_img.max() <= 1: + single_img = single_img * 255 - # img is in scale of [0, 1] but expected [0, 255] - if img.max() <= 1: - img = img * 255 + single_img = single_img.astype(np.uint8) - img = img.astype(np.uint8) - - img_representation = self.model.model.compute_face_descriptor(img) - img_representation = np.array(img_representation) - img_representation = np.expand_dims(img_representation, axis=0) - return img_representation[0].tolist() + img_representation = self.model.model.compute_face_descriptor(single_img) + img_representation = np.array(img_representation) + embeddings.append(img_representation.tolist()) + + if len(embeddings) == 1: + return embeddings[0] + else: + return embeddings class DlibResNet: From 8becc975123a63c7398be0353ab2d31353447e9f Mon Sep 17 00:00:00 2001 From: galthran-wq Date: Tue, 11 Feb 2025 17:25:32 +0000 Subject: [PATCH 64/69] dlib true-batched forward --- deepface/models/facial_recognition/Dlib.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/deepface/models/facial_recognition/Dlib.py b/deepface/models/facial_recognition/Dlib.py index 0d58bb8..cb95f08 100644 --- a/deepface/models/facial_recognition/Dlib.py +++ b/deepface/models/facial_recognition/Dlib.py @@ -40,21 +40,17 @@ class DlibClient(FacialRecognition): if len(img.shape) == 3: img = np.expand_dims(img, axis=0) - embeddings = [] - for single_img in img: - # bgr to rgb - single_img = single_img[:, :, ::-1] # bgr to rgb + # bgr to rgb + img = img[:, :, :, ::-1] # bgr to rgb - # img is in scale of [0, 1] but expected [0, 255] - if single_img.max() <= 1: - single_img = single_img * 255 + # img is in scale of [0, 1] but expected [0, 255] + if img.max() <= 1: + img = img * 255 - single_img = single_img.astype(np.uint8) + img = img.astype(np.uint8) - img_representation = self.model.model.compute_face_descriptor(single_img) - img_representation = np.array(img_representation) - embeddings.append(img_representation.tolist()) - + embeddings = self.model.model.compute_face_descriptor(img) + embeddings = [np.array(embedding).tolist() for embedding in embeddings] if len(embeddings) == 1: return embeddings[0] else: From 9e12c92d8a00623019d8587d236acdfa2f7527d2 Mon Sep 17 00:00:00 2001 From: galthran-wq Date: Tue, 11 Feb 2025 17:35:53 +0000 Subject: [PATCH 65/69] refactor test --- tests/test_represent.py | 71 +++++++++++++++++++++-------------------- 1 file changed, 36 insertions(+), 35 deletions(-) diff --git a/tests/test_represent.py b/tests/test_represent.py index f09834e..3ac65fa 100644 --- a/tests/test_represent.py +++ b/tests/test_represent.py @@ -3,6 +3,7 @@ import io import cv2 import pytest import numpy as np +import pytest # project dependencies from deepface import DeepFace @@ -84,7 +85,19 @@ def test_max_faces(): assert len(results) == max_faces -def test_batched_represent(): +@pytest.mark.parametrize("model_name", [ + "VGG-Face", + "Facenet", + "Facenet512", + "OpenFace", + "DeepFace", + "DeepID", + "Dlib", + "ArcFace", + "SFace", + "GhostFaceNet" +]) +def test_batched_represent(model_name): img_paths = [ "dataset/img1.jpg", "dataset/img2.jpg", @@ -93,38 +106,26 @@ def test_batched_represent(): "dataset/img5.jpg", ] - def _test_for_model(model_name: str): - embedding_objs = DeepFace.represent(img_path=img_paths, model_name=model_name) - assert len(embedding_objs) == len(img_paths) - if model_name == "VGG-Face": - for embedding_obj in embedding_objs: - embedding = embedding_obj["embedding"] - logger.debug(f"Function returned {len(embedding)} dimensional vector") - assert len(embedding) == 4096 - embedding_objs_one_by_one = [ - embedding_obj - for img_path in img_paths - for embedding_obj in DeepFace.represent(img_path=img_path, model_name=model_name) - ] - for embedding_obj_one_by_one, embedding_obj in zip(embedding_objs_one_by_one, embedding_objs): - assert np.allclose( - embedding_obj_one_by_one["embedding"], - embedding_obj["embedding"], - rtol=1e-2, - atol=1e-2 - ) + embedding_objs = DeepFace.represent(img_path=img_paths, model_name=model_name) + assert len(embedding_objs) == len(img_paths), f"Expected {len(img_paths)} embeddings, got {len(embedding_objs)}" - for model_name in [ - "VGG-Face", - "Facenet", - "Facenet512", - "OpenFace", - # "DeepFace", - "DeepID", - # "Dlib", - "ArcFace", - "SFace", - "GhostFaceNet" - ]: - _test_for_model(model_name) - logger.info("✅ test batch represent function done") + if model_name == "VGG-Face": + for embedding_obj in embedding_objs: + embedding = embedding_obj["embedding"] + logger.debug(f"Function returned {len(embedding)} dimensional vector") + assert len(embedding) == 4096, f"Expected embedding of length 4096, got {len(embedding)}" + + embedding_objs_one_by_one = [ + embedding_obj + for img_path in img_paths + for embedding_obj in DeepFace.represent(img_path=img_path, model_name=model_name) + ] + for embedding_obj_one_by_one, embedding_obj in zip(embedding_objs_one_by_one, embedding_objs): + assert np.allclose( + embedding_obj_one_by_one["embedding"], + embedding_obj["embedding"], + rtol=1e-2, + atol=1e-2 + ), "Embeddings do not match within tolerance" + + logger.info(f"✅ test batch represent function for model {model_name} done") From da03b479d89d2f650b20234c884f7b3da69b76cb Mon Sep 17 00:00:00 2001 From: galthran-wq Date: Tue, 11 Feb 2025 17:52:48 +0000 Subject: [PATCH 66/69] remove unnecessary models from the test --- tests/test_represent.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tests/test_represent.py b/tests/test_represent.py index 3ac65fa..bc83a4e 100644 --- a/tests/test_represent.py +++ b/tests/test_represent.py @@ -88,14 +88,7 @@ def test_max_faces(): @pytest.mark.parametrize("model_name", [ "VGG-Face", "Facenet", - "Facenet512", - "OpenFace", - "DeepFace", - "DeepID", - "Dlib", - "ArcFace", "SFace", - "GhostFaceNet" ]) def test_batched_represent(model_name): img_paths = [ From f1734b23675f397fa7e89334cbaeb8353ba24b4b Mon Sep 17 00:00:00 2001 From: galthran-wq Date: Tue, 11 Feb 2025 20:05:23 +0000 Subject: [PATCH 67/69] linting --- deepface/DeepFace.py | 3 ++- deepface/models/FacialRecognition.py | 3 +-- deepface/models/facial_recognition/Dlib.py | 3 +-- deepface/models/facial_recognition/SFace.py | 3 +-- deepface/models/facial_recognition/VGGFace.py | 2 +- deepface/modules/representation.py | 9 ++++++--- 6 files changed, 12 insertions(+), 11 deletions(-) diff --git a/deepface/DeepFace.py b/deepface/DeepFace.py index 9f3ff1e..02dcf3f 100644 --- a/deepface/DeepFace.py +++ b/deepface/DeepFace.py @@ -387,7 +387,8 @@ def represent( Represent facial images as multi-dimensional vector embeddings. Args: - img_path (str, np.ndarray, IO[bytes], or Sequence[Union[str, np.ndarray, IO[bytes]]]): The exact path to the image, a numpy array + img_path (str, np.ndarray, IO[bytes], or Sequence[Union[str, np.ndarray, IO[bytes]]]): + 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. If a sequence is provided, diff --git a/deepface/models/FacialRecognition.py b/deepface/models/FacialRecognition.py index ae9958e..410a033 100644 --- a/deepface/models/FacialRecognition.py +++ b/deepface/models/FacialRecognition.py @@ -31,5 +31,4 @@ class FacialRecognition(ABC): embeddings = self.model(img, training=False).numpy() if embeddings.shape[0] == 1: return embeddings[0].tolist() - else: - return embeddings.tolist() + return embeddings.tolist() diff --git a/deepface/models/facial_recognition/Dlib.py b/deepface/models/facial_recognition/Dlib.py index cb95f08..a2e5ca6 100644 --- a/deepface/models/facial_recognition/Dlib.py +++ b/deepface/models/facial_recognition/Dlib.py @@ -53,8 +53,7 @@ class DlibClient(FacialRecognition): embeddings = [np.array(embedding).tolist() for embedding in embeddings] if len(embeddings) == 1: return embeddings[0] - else: - return embeddings + return embeddings class DlibResNet: diff --git a/deepface/models/facial_recognition/SFace.py b/deepface/models/facial_recognition/SFace.py index 53dfb86..eeebbe3 100644 --- a/deepface/models/facial_recognition/SFace.py +++ b/deepface/models/facial_recognition/SFace.py @@ -47,8 +47,7 @@ class SFaceClient(FacialRecognition): if embeddings.shape[0] == 1: return embeddings[0].tolist() - else: - return embeddings.tolist() + return embeddings.tolist() def load_model( diff --git a/deepface/models/facial_recognition/VGGFace.py b/deepface/models/facial_recognition/VGGFace.py index a3ea2e3..bffd0d6 100644 --- a/deepface/models/facial_recognition/VGGFace.py +++ b/deepface/models/facial_recognition/VGGFace.py @@ -71,7 +71,7 @@ class VggFaceClient(FacialRecognition): # instead we are now calculating it with traditional way not with keras backend embedding = super().forward(img) if ( - isinstance(embedding, list) and + isinstance(embedding, list) and isinstance(embedding[0], list) ): embedding = verification.l2_normalize(embedding, axis=1) diff --git a/deepface/modules/representation.py b/deepface/modules/representation.py index d36f5bc..56eaef2 100644 --- a/deepface/modules/representation.py +++ b/deepface/modules/representation.py @@ -25,8 +25,10 @@ def represent( Represent facial images as multi-dimensional vector embeddings. Args: - img_path (str, np.ndarray, or Sequence[Union[str, np.ndarray]]): The exact path to the image, a numpy array in BGR format, - a base64 encoded image, or a sequence of these. If the source image contains multiple faces, + img_path (str, np.ndarray, or Sequence[Union[str, np.ndarray]]): + The exact path to the image, a numpy array in BGR format, + a base64 encoded image, or a sequence of these. + If the source image contains multiple faces, the result will include information for each detected face. model_name (str): Model for face recognition. Options: VGG-Face, Facenet, Facenet512, @@ -84,7 +86,8 @@ def represent( for single_img_path in images: # --------------------------------- - # we have run pre-process in verification. so, this can be skipped if it is coming from verify. + # we have run pre-process in verification. + # so, this can be skipped if it is coming from verify. target_size = model.input_shape if detector_backend != "skip": img_objs = detection.extract_faces( From 01f872d9e9b2052dbd787a6a2c19b052ba6aba22 Mon Sep 17 00:00:00 2001 From: Nat Lee Date: Fri, 14 Feb 2025 15:52:48 +0800 Subject: [PATCH 68/69] [update] mv control logic to `demography` --- deepface/DeepFace.py | 23 ----------------------- deepface/modules/demography.py | 24 ++++++++++++++++++++++++ 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/deepface/DeepFace.py b/deepface/DeepFace.py index 5ae05aa..2a6c94c 100644 --- a/deepface/DeepFace.py +++ b/deepface/DeepFace.py @@ -256,29 +256,6 @@ def analyze( - 'middle eastern': Confidence score for Middle Eastern ethnicity. - '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: - resp_obj = demography.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 - return demography.analyze( img_path=img_path, actions=actions, diff --git a/deepface/modules/demography.py b/deepface/modules/demography.py index 2258c1e..85cbe81 100644 --- a/deepface/modules/demography.py +++ b/deepface/modules/demography.py @@ -100,6 +100,30 @@ 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, + ) + + # 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): actions = (actions,) From 3037e4e10a2079ab75ad0e18f9578e3e9d33fa44 Mon Sep 17 00:00:00 2001 From: Sefik Ilkin Serengil Date: Tue, 18 Feb 2025 19:55:12 +0000 Subject: [PATCH 69/69] post batch changes --- deepface/DeepFace.py | 13 +-- deepface/models/Demography.py | 10 +- deepface/models/demography/Age.py | 16 ++-- deepface/modules/demography.py | 1 - deepface/modules/representation.py | 45 ++++----- tests/test_analyze.py | 42 +++++--- tests/test_represent.py | 148 +++++++++++++++++++++++------ 7 files changed, 191 insertions(+), 84 deletions(-) diff --git a/deepface/DeepFace.py b/deepface/DeepFace.py index ae0fb0b..30d8910 100644 --- a/deepface/DeepFace.py +++ b/deepface/DeepFace.py @@ -206,9 +206,9 @@ def analyze( anti_spoofing (boolean): Flag to enable anti spoofing (default is False). Returns: - (List[List[Dict[str, Any]]]): A list of analysis results if received batched image, + (List[List[Dict[str, Any]]]): A list of analysis results if received batched image, explained below. - + (List[Dict[str, Any]]): A list of dictionaries, where each dictionary represents the analysis results for a detected face. Each dictionary in the list contains the following keys: @@ -385,12 +385,12 @@ def represent( normalization: str = "base", anti_spoofing: bool = False, max_faces: Optional[int] = None, -) -> List[Dict[str, Any]]: +) -> Union[List[Dict[str, Any]], List[List[Dict[str, Any]]]]: """ Represent facial images as multi-dimensional vector embeddings. Args: - img_path (str, np.ndarray, IO[bytes], or Sequence[Union[str, np.ndarray, IO[bytes]]]): + img_path (str, np.ndarray, IO[bytes], or Sequence[Union[str, np.ndarray, IO[bytes]]]): 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, @@ -423,8 +423,9 @@ def represent( max_faces (int): Set a limit on the number of faces to be processed (default is None). Returns: - results (List[Dict[str, Any]]): A list of dictionaries, each containing the - following fields: + results (List[Dict[str, Any]] or List[Dict[str, Any]]): A list of dictionaries. + Result type becomes List of List of Dict if batch input passed. + Each containing the following fields: - embedding (List[float]): Multidimensional vector representing facial features. The number of dimensions varies based on the reference model diff --git a/deepface/models/Demography.py b/deepface/models/Demography.py index 1493059..0d8a2de 100644 --- a/deepface/models/Demography.py +++ b/deepface/models/Demography.py @@ -24,7 +24,7 @@ class Demography(ABC): def _predict_internal(self, img_batch: np.ndarray) -> np.ndarray: """ Predict for single image or batched images. - This method uses legacy method while receiving single image as input. + This method uses legacy method while receiving single image as input. And switch to batch prediction if receives batched images. Args: @@ -35,11 +35,11 @@ class Demography(ABC): with x = image width, y = image height and c = channel The channel dimension will be 1 if input is grayscale. (For emotion model) """ - if not self.model_name: # Check if called from derived class + if not self.model_name: # Check if called from derived class raise NotImplementedError("no model selected") assert img_batch.ndim == 4, "expected 4-dimensional tensor input" - if img_batch.shape[0] == 1: # Single image + if img_batch.shape[0] == 1: # Single image # Predict with legacy method. return self.model(img_batch, training=False).numpy()[0, :] @@ -48,10 +48,8 @@ class Demography(ABC): return self.model.predict_on_batch(img_batch) def _preprocess_batch_or_single_input( - self, - img: Union[np.ndarray, List[np.ndarray]] + self, img: Union[np.ndarray, List[np.ndarray]] ) -> np.ndarray: - """ Preprocess single or batch of images, return as 4-D numpy array. Args: diff --git a/deepface/models/demography/Age.py b/deepface/models/demography/Age.py index c960159..f5a56c6 100644 --- a/deepface/models/demography/Age.py +++ b/deepface/models/demography/Age.py @@ -13,7 +13,6 @@ from deepface.commons.logger import Logger logger = Logger() -# ---------------------------------------- # dependency configurations tf_version = package_utils.get_tf_major_version() @@ -25,12 +24,11 @@ else: from tensorflow.keras.models import Model, Sequential from tensorflow.keras.layers import Convolution2D, Flatten, Activation -# ---------------------------------------- - WEIGHTS_URL = ( "https://github.com/serengil/deepface_models/releases/download/v1.0/age_model_weights.h5" ) + # pylint: disable=too-few-public-methods class ApparentAgeClient(Demography): """ @@ -49,7 +47,7 @@ class ApparentAgeClient(Demography): List of images as List[np.ndarray] or Batch of images as np.ndarray (n, 224, 224, 3) Returns: - np.ndarray (age_classes,) if single image, + np.ndarray (age_classes,) if single image, np.ndarray (n, age_classes) if batched images. """ # Preprocessing input image or image list. @@ -59,11 +57,10 @@ class ApparentAgeClient(Demography): age_predictions = self._predict_internal(imgs) # Calculate apparent ages - if len(age_predictions.shape) == 1: # Single prediction list + if len(age_predictions.shape) == 1: # Single prediction list return find_apparent_age(age_predictions) - return np.array([ - find_apparent_age(age_prediction) for age_prediction in age_predictions]) + return np.array([find_apparent_age(age_prediction) for age_prediction in age_predictions]) def load_model( @@ -100,6 +97,7 @@ def load_model( return age_model + def find_apparent_age(age_predictions: np.ndarray) -> np.float64: """ Find apparent age prediction from a given probas of ages @@ -108,7 +106,9 @@ def find_apparent_age(age_predictions: np.ndarray) -> np.float64: Returns: apparent_age (float) """ - assert len(age_predictions.shape) == 1, f"Input should be a list of predictions, \ + assert ( + len(age_predictions.shape) == 1 + ), f"Input should be a list of predictions, \ not batched. Got shape: {age_predictions.shape}" output_indexes = np.arange(0, 101) apparent_age = np.sum(age_predictions * output_indexes) diff --git a/deepface/modules/demography.py b/deepface/modules/demography.py index 85cbe81..d3ce8e6 100644 --- a/deepface/modules/demography.py +++ b/deepface/modules/demography.py @@ -123,7 +123,6 @@ def analyze( 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): actions = (actions,) diff --git a/deepface/modules/representation.py b/deepface/modules/representation.py index 56eaef2..dc894df 100644 --- a/deepface/modules/representation.py +++ b/deepface/modules/representation.py @@ -20,12 +20,12 @@ def represent( normalization: str = "base", anti_spoofing: bool = False, max_faces: Optional[int] = None, -) -> List[Dict[str, Any]]: +) -> Union[List[Dict[str, Any]], List[List[Dict[str, Any]]]]: """ Represent facial images as multi-dimensional vector embeddings. Args: - img_path (str, np.ndarray, or Sequence[Union[str, np.ndarray]]): + img_path (str, np.ndarray, or Sequence[Union[str, np.ndarray]]): The exact path to the image, a numpy array in BGR format, a base64 encoded image, or a sequence of these. If the source image contains multiple faces, @@ -53,8 +53,9 @@ def represent( max_faces (int): Set a limit on the number of faces to be processed (default is None). Returns: - results (List[Dict[str, Any]]): A list of dictionaries, each containing the - following fields: + results (List[Dict[str, Any]] or List[Dict[str, Any]]): A list of dictionaries. + Result type becomes List of List of Dict if batch input passed. + Each containing the following fields: - embedding (List[float]): Multidimensional vector representing facial features. The number of dimensions varies based on the reference model @@ -80,14 +81,10 @@ def represent( else: images = [img_path] - batch_images = [] - batch_regions = [] - batch_confidences = [] + batch_images, batch_regions, batch_confidences, batch_indexes = [], [], [], [] - for single_img_path in images: - # --------------------------------- - # we have run pre-process in verification. - # so, this can be skipped if it is coming from verify. + for idx, single_img_path in enumerate(images): + # we have run pre-process in verification. so, skip if it is coming from verify. target_size = model.input_shape if detector_backend != "skip": img_objs = detection.extract_faces( @@ -130,6 +127,7 @@ def represent( for img_obj in img_objs: if anti_spoofing is True and img_obj.get("is_real", True) is False: raise ValueError("Spoof detected in the given image.") + img = img_obj["face"] # bgr to rgb @@ -151,22 +149,25 @@ def represent( batch_images.append(img) batch_regions.append(region) batch_confidences.append(confidence) + batch_indexes.append(idx) # Convert list of images to a numpy array for batch processing batch_images = np.concatenate(batch_images, axis=0) # Forward pass through the model for the entire batch embeddings = model.forward(batch_images) - if len(batch_images) == 1: - embeddings = [embeddings] - for embedding, region, confidence in zip(embeddings, batch_regions, batch_confidences): - resp_objs.append( - { - "embedding": embedding, - "facial_area": region, - "face_confidence": confidence, - } - ) + 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) - return resp_objs + return resp_objs[0] if len(images) == 1 else resp_objs diff --git a/tests/test_analyze.py b/tests/test_analyze.py index a36acc5..6f8c996 100644 --- a/tests/test_analyze.py +++ b/tests/test_analyze.py @@ -144,26 +144,39 @@ def test_analyze_for_different_detectors(): else: assert result["gender"]["Man"] < result["gender"]["Woman"] -def test_analyze_for_batched_image(): - img = "dataset/img4.jpg" + +def test_analyze_for_numpy_batched_image(): + img1_path = "dataset/img4.jpg" + img2_path = "dataset/couple.jpg" + # Copy and combine the same image to create multiple faces - img = cv2.imread(img) - img = np.stack([img, img]) - assert len(img.shape) == 4 # Check dimension. - assert img.shape[0] == 2 # Check batch size. + img1 = cv2.imread(img1_path) + img2 = cv2.imread(img2_path) + + expected_num_faces = [1, 2] + + img1 = cv2.resize(img1, (500, 500)) + img2 = cv2.resize(img2, (500, 500)) + + img = np.stack([img1, img2]) + assert len(img.shape) == 4 # Check dimension. + assert img.shape[0] == 2 # Check batch size. demography_batch = DeepFace.analyze(img, silent=True) # 2 image in batch, so 2 demography objects. - assert len(demography_batch) == 2 + assert len(demography_batch) == 2 - for demography_objs in demography_batch: - assert len(demography_objs) == 1 # 1 face in each image - for demography in demography_objs: # Iterate over faces - assert type(demography) == dict # Check type + for i, demography_objs in enumerate(demography_batch): + + assert len(demography_objs) == expected_num_faces[i] + for demography in demography_objs: # Iterate over faces + assert isinstance(demography, dict) # Check type assert demography["age"] > 20 and demography["age"] < 40 - assert demography["dominant_gender"] == "Woman" + assert demography["dominant_gender"] in ["Woman", "Man"] + logger.info("✅ test analyze for multiple faces done") + def test_batch_detect_age_for_multiple_faces(): # Load test image and resize to model input size img = cv2.resize(cv2.imread("dataset/img1.jpg"), (224, 224)) @@ -176,6 +189,7 @@ def test_batch_detect_age_for_multiple_faces(): assert np.array_equal(int(results[0]), int(results[1])) logger.info("✅ test batch detect age for multiple faces done") + def test_batch_detect_emotion_for_multiple_faces(): # Load test image and resize to model input size img = cv2.resize(cv2.imread("dataset/img1.jpg"), (224, 224)) @@ -187,6 +201,7 @@ def test_batch_detect_emotion_for_multiple_faces(): assert np.array_equal(results[0], results[1]) logger.info("✅ test batch detect emotion for multiple faces done") + def test_batch_detect_gender_for_multiple_faces(): # Load test image and resize to model input size img = cv2.resize(cv2.imread("dataset/img1.jpg"), (224, 224)) @@ -198,6 +213,7 @@ def test_batch_detect_gender_for_multiple_faces(): assert np.array_equal(results[0], results[1]) logger.info("✅ test batch detect gender for multiple faces done") + def test_batch_detect_race_for_multiple_faces(): # Load test image and resize to model input size img = cv2.resize(cv2.imread("dataset/img1.jpg"), (224, 224)) @@ -207,4 +223,4 @@ def test_batch_detect_race_for_multiple_faces(): assert len(results) == 2 # Check two races are the same assert np.array_equal(results[0], results[1]) - logger.info("✅ test batch detect race for multiple faces done") \ No newline at end of file + logger.info("✅ test batch detect race for multiple faces done") diff --git a/tests/test_represent.py b/tests/test_represent.py index bc83a4e..4e22a03 100644 --- a/tests/test_represent.py +++ b/tests/test_represent.py @@ -15,7 +15,12 @@ logger = Logger() def test_standard_represent(): img_path = "dataset/img1.jpg" embedding_objs = DeepFace.represent(img_path) + # type should be list of dict + assert isinstance(embedding_objs, list) + for embedding_obj in embedding_objs: + assert isinstance(embedding_obj, dict) + embedding = embedding_obj["embedding"] logger.debug(f"Function returned {len(embedding)} dimensional vector") assert len(embedding) == 4096 @@ -25,18 +30,18 @@ def test_standard_represent(): def test_standard_represent_with_io_object(): img_path = "dataset/img1.jpg" default_embedding_objs = DeepFace.represent(img_path) - io_embedding_objs = DeepFace.represent(open(img_path, 'rb')) + io_embedding_objs = DeepFace.represent(open(img_path, "rb")) assert default_embedding_objs == io_embedding_objs # Confirm non-seekable io objects are handled properly - io_obj = io.BytesIO(open(img_path, 'rb').read()) + io_obj = io.BytesIO(open(img_path, "rb").read()) io_obj.seek = None no_seek_io_embedding_objs = DeepFace.represent(io_obj) assert default_embedding_objs == no_seek_io_embedding_objs # Confirm non-image io objects raise exceptions - with pytest.raises(ValueError, match='Failed to decode image'): - DeepFace.represent(io.BytesIO(open(r'../requirements.txt', 'rb').read())) + with pytest.raises(ValueError, match="Failed to decode image"): + DeepFace.represent(io.BytesIO(open(r"../requirements.txt", "rb").read())) logger.info("✅ test standard represent with io object function done") @@ -57,6 +62,27 @@ def test_represent_for_skipped_detector_backend_with_image_path(): logger.info("✅ test represent function for skipped detector and image path input backend done") +def test_represent_for_preloaded_image(): + face_img = "dataset/img5.jpg" + img = cv2.imread(face_img) + img_objs = DeepFace.represent(img_path=img) + # type should be list of dict + assert isinstance(img_objs, list) + assert len(img_objs) >= 1 + + for img_obj in img_objs: + assert isinstance(img_obj, dict) + assert "embedding" in img_obj.keys() + assert "facial_area" in img_obj.keys() + assert isinstance(img_obj["facial_area"], dict) + assert "x" in img_obj["facial_area"].keys() + assert "y" in img_obj["facial_area"].keys() + assert "w" in img_obj["facial_area"].keys() + assert "h" in img_obj["facial_area"].keys() + assert "face_confidence" in img_obj.keys() + logger.info("✅ test represent function for skipped detector and preloaded image done") + + def test_represent_for_skipped_detector_backend_with_preloaded_image(): face_img = "dataset/img5.jpg" img = cv2.imread(face_img) @@ -85,40 +111,106 @@ def test_max_faces(): assert len(results) == max_faces -@pytest.mark.parametrize("model_name", [ - "VGG-Face", - "Facenet", - "SFace", -]) -def test_batched_represent(model_name): +@pytest.mark.parametrize( + "model_name", + [ + "VGG-Face", + "Facenet", + "SFace", + ], +) +def test_batched_represent_for_list_input(model_name): img_paths = [ "dataset/img1.jpg", "dataset/img2.jpg", "dataset/img3.jpg", "dataset/img4.jpg", "dataset/img5.jpg", + "dataset/couple.jpg", ] - embedding_objs = DeepFace.represent(img_path=img_paths, model_name=model_name) - assert len(embedding_objs) == len(img_paths), f"Expected {len(img_paths)} embeddings, got {len(embedding_objs)}" + expected_faces = [1, 1, 1, 1, 1, 2] - if model_name == "VGG-Face": + batched_embedding_objs = DeepFace.represent(img_path=img_paths, model_name=model_name) + + # type should be list of list of dict for batch input + assert isinstance(batched_embedding_objs, list) + + assert len(batched_embedding_objs) == len( + img_paths + ), f"Expected {len(img_paths)} embeddings, got {len(batched_embedding_objs)}" + + # the last one has two faces + for idx, embedding_objs in enumerate(batched_embedding_objs): + # type should be list of list of dict for batch input + # batched_embedding_objs was list already, embedding_objs should be list of dict + assert isinstance(embedding_objs, list) for embedding_obj in embedding_objs: - embedding = embedding_obj["embedding"] - logger.debug(f"Function returned {len(embedding)} dimensional vector") - assert len(embedding) == 4096, f"Expected embedding of length 4096, got {len(embedding)}" + assert isinstance(embedding_obj, dict) - embedding_objs_one_by_one = [ - embedding_obj - for img_path in img_paths - for embedding_obj in DeepFace.represent(img_path=img_path, model_name=model_name) + assert expected_faces[idx] == len( + embedding_objs + ), f"{img_paths[idx]} has {expected_faces[idx]} faces, but got {len(embedding_objs)} embeddings!" + + for idx, img_path in enumerate(img_paths): + single_embedding_objs = DeepFace.represent(img_path=img_path, model_name=model_name) + # type should be list of dict for single input + assert isinstance(single_embedding_objs, list) + for embedding_obj in single_embedding_objs: + assert isinstance(embedding_obj, dict) + + assert len(single_embedding_objs) == len(batched_embedding_objs[idx]) + + for alpha, beta in zip(single_embedding_objs, batched_embedding_objs[idx]): + assert np.allclose( + alpha["embedding"], beta["embedding"], rtol=1e-2, atol=1e-2 + ), "Embeddings do not match within tolerance" + + logger.info(f"✅ test batch represent function with string input for model {model_name} done") + + +@pytest.mark.parametrize( + "model_name", + [ + "VGG-Face", + "Facenet", + "SFace", + ], +) +def test_batched_represent_for_numpy_input(model_name): + img_paths = [ + "dataset/img1.jpg", + "dataset/img2.jpg", + "dataset/img3.jpg", + "dataset/img4.jpg", + "dataset/img5.jpg", + "dataset/couple.jpg", ] - for embedding_obj_one_by_one, embedding_obj in zip(embedding_objs_one_by_one, embedding_objs): - assert np.allclose( - embedding_obj_one_by_one["embedding"], - embedding_obj["embedding"], - rtol=1e-2, - atol=1e-2 - ), "Embeddings do not match within tolerance" + expected_faces = [1, 1, 1, 1, 1, 2] - logger.info(f"✅ test batch represent function for model {model_name} done") + imgs = [] + for img_path in img_paths: + img = cv2.imread(img_path) + img = cv2.resize(img, (1000, 1000)) + img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + # print(img.shape) + imgs.append(img) + + imgs = np.array(imgs) + assert imgs.ndim == 4 and imgs.shape[0] == len(img_paths) + + batched_embedding_objs = DeepFace.represent(img_path=imgs, model_name=model_name) + + # type should be list of list of dict for batch input + assert isinstance(batched_embedding_objs, list) + for idx, batched_embedding_obj in enumerate(batched_embedding_objs): + assert isinstance(batched_embedding_obj, list) + # it also has to have the expected number of faces + assert len(batched_embedding_obj) == expected_faces[idx] + for embedding_obj in batched_embedding_obj: + assert isinstance(embedding_obj, dict) + + # we should have the same number of embeddings as the number of images + assert len(batched_embedding_objs) == len(img_paths) + + logger.info(f"✅ test batch represent function with numpy input for model {model_name} done")