From c42df046f14ea69c689764c129503ec7cf72262f Mon Sep 17 00:00:00 2001 From: Nat Lee Date: Thu, 5 Dec 2024 17:55:10 +0800 Subject: [PATCH 01/48] [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 67ab3ae65..f3a21a14d 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 @@ def predict(self, img: np.ndarray) -> np.float64: 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/48] [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 ad1c15e3c..77f0ec1d1 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 @@ def predict(self, img: np.ndarray) -> np.ndarray: # 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/48] [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 f3a21a14d..d9d08feaf 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 77f0ec1d1..f55c5719c 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/48] [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 d9d08feaf..9f2052e3f 100644 --- a/deepface/models/demography/Age.py +++ b/deepface/models/demography/Age.py @@ -64,7 +64,9 @@ def predicts(self, imgs: List[np.ndarray]) -> np.ndarray: 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/48] [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 9f2052e3f..9c7ef3c3d 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 @@ def __init__(self): 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 f55c5719c..ac8716af2 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 @@ def __init__(self): 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/48] [update] enhance predict methods in Emotion and Race models to support single and batch inputs --- deepface/models/demography/Emotion.py | 64 +++++++++++++++++++++++---- deepface/models/demography/Race.py | 43 ++++++++++++++++-- 2 files changed, 95 insertions(+), 12 deletions(-) diff --git a/deepface/models/demography/Emotion.py b/deepface/models/demography/Emotion.py index d2633b519..e6cb3d94d 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 @@ def __init__(self): 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) - - # 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, :] + return img_gray + + 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 2334c8b46..4537bed0e 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 @@ def __init__(self): 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/48] [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 4537bed0e..cec6aaad2 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/48] [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 | 158 +++++++++++++++----------- 5 files changed, 99 insertions(+), 104 deletions(-) diff --git a/deepface/models/demography/Age.py b/deepface/models/demography/Age.py index 9c7ef3c3d..d470cff6f 100644 --- a/deepface/models/demography/Age.py +++ b/deepface/models/demography/Age.py @@ -40,7 +40,7 @@ def __init__(self): 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 @@ def predict(self, img: Union[np.ndarray, List[np.ndarray]]) -> Union[np.float64, 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 @@ def predict(self, img: Union[np.ndarray, List[np.ndarray]]) -> Union[np.float64, 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 @@ def predict(self, img: Union[np.ndarray, List[np.ndarray]]) -> Union[np.float64, [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 e6cb3d94d..065795e37 100644 --- a/deepface/models/demography/Emotion.py +++ b/deepface/models/demography/Emotion.py @@ -58,7 +58,7 @@ def _preprocess_image(self, img: np.ndarray) -> np.ndarray: 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 @@ def predict(self, img: Union[np.ndarray, List[np.ndarray]]) -> Union[np.ndarray, 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 @@ def predict(self, img: Union[np.ndarray, List[np.ndarray]]) -> Union[np.ndarray, 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 @@ def predict(self, img: Union[np.ndarray, List[np.ndarray]]) -> Union[np.ndarray, # 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 ac8716af2..23fd69b2c 100644 --- a/deepface/models/demography/Gender.py +++ b/deepface/models/demography/Gender.py @@ -40,7 +40,7 @@ def __init__(self): 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 @@ def predict(self, img: Union[np.ndarray, List[np.ndarray]]) -> Union[np.ndarray, 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 @@ def predict(self, img: Union[np.ndarray, List[np.ndarray]]) -> Union[np.ndarray, 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 cec6aaad2..dc4a7889a 100644 --- a/deepface/models/demography/Race.py +++ b/deepface/models/demography/Race.py @@ -40,7 +40,7 @@ def __init__(self): 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 @@ def predict(self, img: Union[np.ndarray, List[np.ndarray]]) -> Union[np.ndarray, 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 @@ def predict(self, img: Union[np.ndarray, List[np.ndarray]]) -> Union[np.ndarray, 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 b68314b9c..4c58314c2 100644 --- a/deepface/modules/demography.py +++ b/deepface/modules/demography.py @@ -9,7 +9,7 @@ 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, ) + # 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: - if anti_spoofing is True and img_obj.get("is_real", True) is False: - raise ValueError("Spoof detected in the given image.") - + # 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}") - - if action == "emotion": - emotion_predictions = modeling.build_model( - task="facial_attribute", model_name="Emotion" - ).predict(img_content) - sum_of_predictions = emotion_predictions.sum() - - 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 - 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 + valid_faces.append(img_content) + face_regions.append(img_obj["facial_area"]) + face_confidences.append(img_obj["confidence"]) - obj["dominant_gender"] = Gender.labels[np.argmax(gender_predictions)] + # If no valid faces are found, return an empty list + if not valid_faces: + return [] - elif action == "race": - race_predictions = modeling.build_model( - task="facial_attribute", model_name="Race" - ).predict(img_content) - sum_of_predictions = race_predictions.sum() + # Convert the list of valid faces to a numpy array + faces_array = np.array(valid_faces) + resp_objects = [{} for _ in range(len(valid_faces))] - obj["race"] = {} + # 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 * predictions[i] / sum_of_predictions + resp_objects[idx]["emotion"][emotion_label] = emotion_prediction + + 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) + + for idx, age in enumerate(age_predictions): + resp_objects[idx]["age"] = int(age) + + 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 * predictions[i] + resp_objects[idx]["gender"][gender_label] = gender_prediction + + resp_objects[idx]["dominant_gender"] = Gender.labels[np.argmax(predictions)] + + 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 - - 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) + 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)] + + # 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/48] [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 bad44260e..976952b64 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/48] 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 ad9392029..1dcef4171 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/48] [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 236bbdf09..57ffbcf4f 100644 --- a/deepface/models/demography/Age.py +++ b/deepface/models/demography/Age.py @@ -75,32 +75,6 @@ def predict(self, img: Union[np.ndarray, List[np.ndarray]]) -> np.ndarray: 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 2ef4cc2ef..1c06a7630 100644 --- a/deepface/models/demography/Gender.py +++ b/deepface/models/demography/Gender.py @@ -70,27 +70,6 @@ def predict(self, img: Union[np.ndarray, List[np.ndarray]]) -> np.ndarray: 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/48] 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 c199cd5fe..b10a33f25 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/48] 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 1dcef4171..e73fe65df 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 57ffbcf4f..5bc409f7a 100644 --- a/deepface/models/demography/Age.py +++ b/deepface/models/demography/Age.py @@ -51,19 +51,8 @@ def predict(self, img: Union[np.ndarray, List[np.ndarray]]) -> np.ndarray: 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 065795e37..10e51155d 100644 --- a/deepface/models/demography/Emotion.py +++ b/deepface/models/demography/Emotion.py @@ -69,19 +69,8 @@ def predict(self, img: Union[np.ndarray, List[np.ndarray]]) -> np.ndarray: 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 1c06a7630..f7e705ebc 100644 --- a/deepface/models/demography/Gender.py +++ b/deepface/models/demography/Gender.py @@ -51,19 +51,8 @@ def predict(self, img: Union[np.ndarray, List[np.ndarray]]) -> np.ndarray: 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 dc4a7889a..0c6a2f0de 100644 --- a/deepface/models/demography/Race.py +++ b/deepface/models/demography/Race.py @@ -51,19 +51,8 @@ def predict(self, img: Union[np.ndarray, List[np.ndarray]]) -> np.ndarray: 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/48] 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 976952b64..27805f509 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/48] [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 b10a33f25..6b6382f43 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/48] 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 e73fe65df..d240f1ef7 100644 --- a/deepface/models/Demography.py +++ b/deepface/models/Demography.py @@ -43,6 +43,6 @@ def _preprocess_batch_or_single_input(self, img: Union[np.ndarray, List[np.ndarr # 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/48] 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 d240f1ef7..bf4ea6c76 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/48] 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 bf4ea6c76..64e56e262 100644 --- a/deepface/models/Demography.py +++ b/deepface/models/Demography.py @@ -37,8 +37,7 @@ def _predict_internal(self, img_batch: np.ndarray) -> np.ndarray: 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/48] 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 5bc409f7a..7284aad2b 100644 --- a/deepface/models/demography/Age.py +++ b/deepface/models/demography/Age.py @@ -54,8 +54,8 @@ def predict(self, img: Union[np.ndarray, List[np.ndarray]]) -> np.ndarray: # 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 10e51155d..e6ebf5f41 100644 --- a/deepface/models/demography/Emotion.py +++ b/deepface/models/demography/Emotion.py @@ -78,8 +78,8 @@ def predict(self, img: Union[np.ndarray, List[np.ndarray]]) -> np.ndarray: # 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 f7e705ebc..b6a3ef1c3 100644 --- a/deepface/models/demography/Gender.py +++ b/deepface/models/demography/Gender.py @@ -54,8 +54,8 @@ def predict(self, img: Union[np.ndarray, List[np.ndarray]]) -> np.ndarray: # 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 0c6a2f0de..eae5154cc 100644 --- a/deepface/models/demography/Race.py +++ b/deepface/models/demography/Race.py @@ -54,8 +54,8 @@ def predict(self, img: Union[np.ndarray, List[np.ndarray]]) -> np.ndarray: # 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/48] [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 64e56e262..d0a00f18a 100644 --- a/deepface/models/Demography.py +++ b/deepface/models/Demography.py @@ -28,7 +28,7 @@ def _predict_internal(self, img_batch: np.ndarray) -> np.ndarray: 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/48] [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 e6ebf5f41..caf862b5a 100644 --- a/deepface/models/demography/Emotion.py +++ b/deepface/models/demography/Emotion.py @@ -72,13 +72,11 @@ def predict(self, img: Union[np.ndarray, List[np.ndarray]]) -> np.ndarray: # 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/48] [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 7284aad2b..ae5348766 100644 --- a/deepface/models/demography/Age.py +++ b/deepface/models/demography/Age.py @@ -54,9 +54,9 @@ def predict(self, img: Union[np.ndarray, List[np.ndarray]]) -> np.ndarray: # 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/48] [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 d0a00f18a..7bba87f5e 100644 --- a/deepface/models/Demography.py +++ b/deepface/models/Demography.py @@ -36,7 +36,8 @@ def _predict_internal(self, img_batch: np.ndarray) -> np.ndarray: 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/48] [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 6b6382f43..36b8305f5 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/48] [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 caf862b5a..66d09ccb2 100644 --- a/deepface/models/demography/Emotion.py +++ b/deepface/models/demography/Emotion.py @@ -73,7 +73,10 @@ def predict(self, img: Union[np.ndarray, List[np.ndarray]]) -> np.ndarray: 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/48] [fix] check for input number of faces --- deepface/modules/demography.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/deepface/modules/demography.py b/deepface/modules/demography.py index 36b8305f5..447d14666 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: + if faces_array.shape[0] == 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) + 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/48] [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 447d14666..0c52eb041 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/48] [fix] use prediction shape to avoid confuse situation of predictions --- deepface/modules/demography.py | 200 ++++++++++++++------------------- 1 file changed, 83 insertions(+), 117 deletions(-) diff --git a/deepface/modules/demography.py b/deepface/modules/demography.py index 0c52eb041..978900746 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 - - # 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"]) + return None + img_content = img_content[:, :, ::-1] # BGR to RGB + return preprocessing.resize_image(img=img_content, target_size=(224, 224)) - # 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] - pbar.set_description(f"Action: {action}") - resp_object = {} + 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 + # 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) - - # 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 + 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 \ 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/48] [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 66d09ccb2..ac2af29e3 100644 --- a/deepface/models/demography/Emotion.py +++ b/deepface/models/demography/Emotion.py @@ -72,14 +72,16 @@ def predict(self, img: Union[np.ndarray, List[np.ndarray]]) -> np.ndarray: # 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/48] 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 7bba87f5e..911032d58 100644 --- a/deepface/models/Demography.py +++ b/deepface/models/Demography.py @@ -51,7 +51,6 @@ def _preprocess_batch_or_single_input(self, img: Union[np.ndarray, List[np.ndarr 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/48] 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 911032d58..fb9d106b7 100644 --- a/deepface/models/Demography.py +++ b/deepface/models/Demography.py @@ -29,19 +29,21 @@ def _predict_internal(self, img_batch: np.ndarray) -> np.ndarray: 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 @@ def _preprocess_batch_or_single_input(self, img: Union[np.ndarray, List[np.ndarr 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/48] [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 fb9d106b7..329a156d3 100644 --- a/deepface/models/Demography.py +++ b/deepface/models/Demography.py @@ -28,24 +28,32 @@ def _predict_internal(self, img_batch: np.ndarray) -> np.ndarray: 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 @@ def _preprocess_batch_or_single_input(self, img: Union[np.ndarray, List[np.ndarr 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 978900746..c78f73bd6 100644 --- a/deepface/modules/demography.py +++ b/deepface/modules/demography.py @@ -168,9 +168,9 @@ def preprocess_face(img_obj: Dict[str, Any]) -> Optional[np.ndarray]: 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 preprocess_face(img_obj: Dict[str, Any]) -> Optional[np.ndarray]: 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/48] 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 329a156d3..5fcc43138 100644 --- a/deepface/models/Demography.py +++ b/deepface/models/Demography.py @@ -38,14 +38,15 @@ def _predict_internal(self, img_batch: np.ndarray) -> np.ndarray: 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/48] 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 5fcc43138..2a78afd8c 100644 --- a/deepface/models/Demography.py +++ b/deepface/models/Demography.py @@ -33,23 +33,20 @@ def _predict_internal(self, img_batch: np.ndarray) -> np.ndarray: 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/48] 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 2a78afd8c..c9cfe304f 100644 --- a/deepface/models/Demography.py +++ b/deepface/models/Demography.py @@ -40,7 +40,6 @@ def _predict_internal(self, img_batch: np.ndarray) -> np.ndarray: 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/48] 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 ac2af29e3..499c246cf 100644 --- a/deepface/models/demography/Emotion.py +++ b/deepface/models/demography/Emotion.py @@ -72,14 +72,7 @@ def predict(self, img: Union[np.ndarray, List[np.ndarray]]) -> np.ndarray: # 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/48] 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 c9cfe304f..87869b9a9 100644 --- a/deepface/models/Demography.py +++ b/deepface/models/Demography.py @@ -38,14 +38,14 @@ def _predict_internal(self, img_batch: np.ndarray) -> np.ndarray: 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/48] [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 c78f73bd6..528224db6 100644 --- a/deepface/modules/demography.py +++ b/deepface/modules/demography.py @@ -9,7 +9,6 @@ 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 preprocess_face(img_obj: Dict[str, Any]) -> Optional[np.ndarray]: 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 preprocess_face(img_obj: Dict[str, Any]) -> Optional[np.ndarray]: # ---------------------------------------- # 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 preprocess_face(img_obj: Dict[str, Any]) -> Optional[np.ndarray]: ] 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/48] 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 ae5348766..e449acaba 100644 --- a/deepface/models/demography/Age.py +++ b/deepface/models/demography/Age.py @@ -41,7 +41,7 @@ def __init__(self): 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 @@ def predict(self, img: Union[np.ndarray, List[np.ndarray]]) -> np.ndarray: 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 @@ def predict(self, img: Union[np.ndarray, List[np.ndarray]]) -> np.ndarray: 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/48] REVERT demography.py --- deepface/modules/demography.py | 180 +++++++++++++++------------------ 1 file changed, 81 insertions(+), 99 deletions(-) diff --git a/deepface/modules/demography.py b/deepface/modules/demography.py index 528224db6..b9991d955 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 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)) - - # Filter out empty faces - face_data = [ - ( - preprocess_face(img_obj), - img_obj["facial_area"], - img_obj["confidence"] + continue + + # 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 - ] - - 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} - 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 + for index in pbar: + action = actions[index] + pbar.set_description(f"Action: {action}") + + if action == "emotion": + emotion_predictions = modeling.build_model( + task="facial_attribute", model_name="Emotion" + ).predict(img_content) + sum_of_predictions = emotion_predictions.sum() + + 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/48] 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 e449acaba..c96015919 100644 --- a/deepface/models/demography/Age.py +++ b/deepface/models/demography/Age.py @@ -49,7 +49,8 @@ def predict(self, img: Union[np.ndarray, List[np.ndarray]]) -> Union[np.float64, 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 @@ def predict(self, img: Union[np.ndarray, List[np.ndarray]]) -> Union[np.float64, # 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/48] [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 b9991d955..2258c1efe 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/48] [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 27805f509..5949497c5 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/48] 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 87869b9a9..1493059b3 100644 --- a/deepface/models/Demography.py +++ b/deepface/models/Demography.py @@ -62,8 +62,7 @@ def _preprocess_batch_or_single_input( 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/48] [update] modify test of `emotion` and add client of `age`, `gender` and `race` tests --- tests/test_analyze.py | 42 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/tests/test_analyze.py b/tests/test_analyze.py index 5949497c5..63b2686be 100644 --- a/tests/test_analyze.py +++ b/tests/test_analyze.py @@ -4,6 +4,7 @@ # 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_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 = 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(): - img = "dataset/img4.jpg" - img = cv2.imread(img) + # 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 = 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/48] 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 3abe6db97..5ae05aadd 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/48] 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 5949497c5..8afdbcda2 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/48] [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 46d4d16fe..a36acc5d1 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():