post batch changes

This commit is contained in:
Sefik Ilkin Serengil 2025-02-20 17:34:51 +00:00
parent 6c714a8e4a
commit f758dcc133
7 changed files with 135 additions and 54 deletions

View File

@ -166,7 +166,7 @@ def verify(
def analyze( def analyze(
img_path: Union[str, np.ndarray, IO[bytes]], img_path: Union[str, np.ndarray, IO[bytes], List[str], List[np.ndarray], List[IO[bytes]]],
actions: Union[tuple, list] = ("emotion", "age", "gender", "race"), actions: Union[tuple, list] = ("emotion", "age", "gender", "race"),
enforce_detection: bool = True, enforce_detection: bool = True,
detector_backend: str = "opencv", detector_backend: str = "opencv",
@ -178,7 +178,7 @@ def analyze(
""" """
Analyze facial attributes such as age, gender, emotion, and race in the provided image. Analyze facial attributes such as age, gender, emotion, and race in the provided image.
Args: Args:
img_path (str or np.ndarray or IO[bytes]): The exact path to the image, a numpy array img_path (str, np.ndarray, IO[bytes], list): The exact path to the image, a numpy array
in BGR format, a file object that supports at least `.read` and is opened in binary in BGR format, a file object that supports at least `.read` and is opened in binary
mode, or a base64 encoded image. If the source image contains multiple faces, mode, or a base64 encoded image. If the source image contains multiple faces,
the result will include information for each detected face. the result will include information for each detected face.

View File

@ -11,6 +11,7 @@ else:
# Notice that all facial recognition models must be inherited from this class # Notice that all facial recognition models must be inherited from this class
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
class FacialRecognition(ABC): class FacialRecognition(ABC):
model: Union[Model, Any] model: Union[Model, Any]
@ -24,11 +25,24 @@ class FacialRecognition(ABC):
"You must overwrite forward method if it is not a keras model," "You must overwrite forward method if it is not a keras model,"
f"but {self.model_name} not overwritten!" f"but {self.model_name} not overwritten!"
) )
# model.predict causes memory issue when it is called in a for loop
# embedding = model.predict(img, verbose=0)[0].tolist() # predict expexts e.g. (1, 224, 224, 3) shaped inputs
if img.shape == 4 and img.shape[0] == 1: if img.ndim == 3:
img = img[0] img = np.expand_dims(img, axis=0)
embeddings = self.model(img, training=False).numpy()
if img.ndim == 4 and img.shape[0] == 1:
# model.predict causes memory issue when it is called in a for loop
# embedding = model.predict(img, verbose=0)[0].tolist()
embeddings = self.model(img, training=False).numpy()
elif img.ndim == 4 and img.shape[0] > 1:
embeddings = self.model.predict_on_batch(img)
else:
raise ValueError(f"Input image must be (1, X, X, 3) shaped but it is {img.shape}")
assert isinstance(
embeddings, np.ndarray
), f"Embeddings must be numpy array but it is {type(embeddings)}"
if embeddings.shape[0] == 1: if embeddings.shape[0] == 1:
return embeddings[0].tolist() return embeddings[0].tolist()
return embeddings.tolist() return embeddings.tolist()

View File

@ -42,6 +42,7 @@ WEIGHTS_URL = (
"https://github.com/serengil/deepface_models/releases/download/v1.0/vgg_face_weights.h5" "https://github.com/serengil/deepface_models/releases/download/v1.0/vgg_face_weights.h5"
) )
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
class VggFaceClient(FacialRecognition): class VggFaceClient(FacialRecognition):
""" """
@ -70,10 +71,7 @@ class VggFaceClient(FacialRecognition):
# having normalization layer in descriptor troubles for some gpu users (e.g. issue 957, 966) # having normalization layer in descriptor troubles for some gpu users (e.g. issue 957, 966)
# instead we are now calculating it with traditional way not with keras backend # instead we are now calculating it with traditional way not with keras backend
embedding = super().forward(img) embedding = super().forward(img)
if ( if isinstance(embedding, list) and len(embedding) > 0 and isinstance(embedding[0], list):
isinstance(embedding, list) and
isinstance(embedding[0], list)
):
embedding = verification.l2_normalize(embedding, axis=1) embedding = verification.l2_normalize(embedding, axis=1)
else: else:
embedding = verification.l2_normalize(embedding) embedding = verification.l2_normalize(embedding)

View File

@ -1,5 +1,5 @@
# built-in dependencies # built-in dependencies
from typing import Any, Dict, List, Union from typing import Any, Dict, List, Union, IO
# 3rd party dependencies # 3rd party dependencies
import numpy as np import numpy as np
@ -11,7 +11,7 @@ from deepface.models.demography import Gender, Race, Emotion
def analyze( def analyze(
img_path: Union[str, np.ndarray], img_path: Union[str, np.ndarray, IO[bytes], List[str], List[np.ndarray], List[IO[bytes]]],
actions: Union[tuple, list] = ("emotion", "age", "gender", "race"), actions: Union[tuple, list] = ("emotion", "age", "gender", "race"),
enforce_detection: bool = True, enforce_detection: bool = True,
detector_backend: str = "opencv", detector_backend: str = "opencv",
@ -19,14 +19,14 @@ def analyze(
expand_percentage: int = 0, expand_percentage: int = 0,
silent: bool = False, silent: bool = False,
anti_spoofing: 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. Analyze facial attributes such as age, gender, emotion, and race in the provided image.
Args: Args:
img_path (str or np.ndarray): The exact path to the image, a numpy array in BGR format, img_path (str, np.ndarray, IO[bytes], list): The exact path to the image,
or a base64 encoded image. If the source image contains multiple faces, the result will a numpy array in BGR format, or a base64 encoded image. If the source image
include information for each detected face. contains multiple faces, the result will include information for each detected face.
actions (tuple): Attributes to analyze. The default is ('age', 'gender', 'emotion', 'race'). actions (tuple): Attributes to analyze. The default is ('age', 'gender', 'emotion', 'race').
You can exclude some of these attributes from the analysis if needed. You can exclude some of these attributes from the analysis if needed.
@ -100,28 +100,28 @@ def analyze(
- 'white': Confidence score for White ethnicity. - 'white': Confidence score for White ethnicity.
""" """
if isinstance(img_path, np.ndarray) and len(img_path.shape) == 4: # batch input
# Received 4-D array, which means image batch. if (isinstance(img_path, np.ndarray) and img_path.ndim == 4 and img_path.shape[0] > 1) or (
# Check batch dimension and process each image separately. isinstance(img_path, list)
if img_path.shape[0] > 1: ):
batch_resp_obj = [] batch_resp_obj = []
# Execute analysis for each image in the batch. # Execute analysis for each image in the batch.
for single_img in img_path: for single_img in img_path:
# Call the analyze function for each image in the batch. # Call the analyze function for each image in the batch.
resp_obj = analyze( resp_obj = analyze(
img_path=single_img, img_path=single_img,
actions=actions, actions=actions,
enforce_detection=enforce_detection, enforce_detection=enforce_detection,
detector_backend=detector_backend, detector_backend=detector_backend,
align=align, align=align,
expand_percentage=expand_percentage, expand_percentage=expand_percentage,
silent=silent, silent=silent,
anti_spoofing=anti_spoofing, anti_spoofing=anti_spoofing,
) )
# Append the response object to the batch response list. # Append the response object to the batch response list.
batch_resp_obj.append(resp_obj) batch_resp_obj.append(resp_obj)
return batch_resp_obj return batch_resp_obj
# if actions is passed as tuple with single item, interestingly it becomes str here # if actions is passed as tuple with single item, interestingly it becomes str here
if isinstance(actions, str): if isinstance(actions, str):

View File

@ -1,5 +1,6 @@
# built-in dependencies # built-in dependencies
from typing import Any, Dict, List, Union, Optional, Sequence, IO from typing import Any, Dict, List, Union, Optional, Sequence, IO
from collections import defaultdict
# 3rd party dependencies # 3rd party dependencies
import numpy as np import numpy as np
@ -157,17 +158,16 @@ def represent(
# Forward pass through the model for the entire batch # Forward pass through the model for the entire batch
embeddings = model.forward(batch_images) embeddings = model.forward(batch_images)
for idx in range(0, len(images)): resp_objs_dict = defaultdict(list)
resp_obj = [] for idy, batch_index in enumerate(batch_indexes):
for idy, batch_index in enumerate(batch_indexes): resp_objs_dict[batch_index].append(
if idx == batch_index: {
resp_obj.append( "embedding": embeddings if len(batch_images) == 1 else embeddings[idy],
{ "facial_area": batch_regions[idy],
"embedding": embeddings if len(batch_images) == 1 else embeddings[idy], "face_confidence": batch_confidences[idy],
"facial_area": batch_regions[idy], }
"face_confidence": batch_confidences[idy], )
}
) resp_objs = [resp_objs_dict[idx] for idx in range(len(images))]
resp_objs.append(resp_obj)
return resp_objs[0] if len(images) == 1 else resp_objs return resp_objs[0] if len(images) == 1 else resp_objs

View File

@ -16,9 +16,13 @@ detectors = ["opencv", "mtcnn"]
def test_standard_analyze(): def test_standard_analyze():
img = "dataset/img4.jpg" img = "dataset/img4.jpg"
demography_objs = DeepFace.analyze(img, silent=True) demography_objs = DeepFace.analyze(img, silent=True)
# return type should be list of dict for non batch input
assert isinstance(demography_objs, list)
for demography in demography_objs: for demography in demography_objs:
assert isinstance(demography, dict)
logger.debug(demography) logger.debug(demography)
assert type(demography) == dict
assert demography["age"] > 20 and demography["age"] < 40 assert demography["age"] > 20 and demography["age"] < 40
assert demography["dominant_gender"] == "Woman" assert demography["dominant_gender"] == "Woman"
logger.info("✅ test standard analyze done") logger.info("✅ test standard analyze done")
@ -99,9 +103,13 @@ def test_analyze_for_some_actions():
def test_analyze_for_preloaded_image(): def test_analyze_for_preloaded_image():
img = cv2.imread("dataset/img1.jpg") img = cv2.imread("dataset/img1.jpg")
resp_objs = DeepFace.analyze(img, silent=True) resp_objs = DeepFace.analyze(img, silent=True)
# return type should be list of dict for non batch input
assert isinstance(resp_objs, list)
for resp_obj in resp_objs: for resp_obj in resp_objs:
assert isinstance(resp_obj, dict)
logger.debug(resp_obj) logger.debug(resp_obj)
assert type(resp_obj) == dict
assert resp_obj["age"] > 20 and resp_obj["age"] < 40 assert resp_obj["age"] > 20 and resp_obj["age"] < 40
assert resp_obj["dominant_gender"] == "Woman" assert resp_obj["dominant_gender"] == "Woman"
@ -127,7 +135,10 @@ def test_analyze_for_different_detectors():
results = DeepFace.analyze( results = DeepFace.analyze(
img_path, actions=("gender",), detector_backend=detector, enforce_detection=False img_path, actions=("gender",), detector_backend=detector, enforce_detection=False
) )
# return type should be list of dict for non batch input
assert isinstance(results, list)
for result in results: for result in results:
assert isinstance(result, dict)
logger.debug(result) logger.debug(result)
# validate keys # validate keys
@ -138,13 +149,63 @@ def test_analyze_for_different_detectors():
] ]
# validate probabilities # validate probabilities
assert type(result) == dict
if result["dominant_gender"] == "Man": if result["dominant_gender"] == "Man":
assert result["gender"]["Man"] > result["gender"]["Woman"] assert result["gender"]["Man"] > result["gender"]["Woman"]
else: else:
assert result["gender"]["Man"] < result["gender"]["Woman"] assert result["gender"]["Man"] < result["gender"]["Woman"]
def test_analyze_for_batched_image_as_list_of_string():
img_paths = ["dataset/img1.jpg", "dataset/img2.jpg", "dataset/couple.jpg"]
expected_faces = [1, 1, 2]
demography_batch = DeepFace.analyze(img_path=img_paths, silent=True)
# return type should be list of list of dict for batch input
assert isinstance(demography_batch, list)
# 3 image in batch, so 3 demography objects
assert len(demography_batch) == len(img_paths)
for idx, demography_objs in enumerate(demography_batch):
assert isinstance(demography_objs, list)
assert len(demography_objs) == expected_faces[idx]
for demography_obj in demography_objs:
assert isinstance(demography_obj, dict)
assert demography_obj["age"] > 20 and demography_obj["age"] < 40
assert demography_obj["dominant_gender"] in ["Woman", "Man"]
logger.info("✅ test analyze for batched image as list of string done")
def test_analyze_for_batched_image_as_list_of_numpy():
img_paths = ["dataset/img1.jpg", "dataset/img2.jpg", "dataset/couple.jpg"]
expected_faces = [1, 1, 2]
imgs = []
for img_path in img_paths:
img = cv2.imread(img_path)
imgs.append(img)
demography_batch = DeepFace.analyze(img_path=imgs, silent=True)
# return type should be list of list of dict for batch input
assert isinstance(demography_batch, list)
# 3 image in batch, so 3 demography objects
assert len(demography_batch) == len(img_paths)
for idx, demography_objs in enumerate(demography_batch):
assert isinstance(demography_objs, list)
assert len(demography_objs) == expected_faces[idx]
for demography_obj in demography_objs:
assert isinstance(demography_obj, dict)
assert demography_obj["age"] > 20 and demography_obj["age"] < 40
assert demography_obj["dominant_gender"] in ["Woman", "Man"]
logger.info("✅ test analyze for batched image as list of numpy done")
def test_analyze_for_numpy_batched_image(): def test_analyze_for_numpy_batched_image():
img1_path = "dataset/img4.jpg" img1_path = "dataset/img4.jpg"
img2_path = "dataset/couple.jpg" img2_path = "dataset/couple.jpg"
@ -163,14 +224,20 @@ def test_analyze_for_numpy_batched_image():
assert img.shape[0] == 2 # Check batch size. assert img.shape[0] == 2 # Check batch size.
demography_batch = DeepFace.analyze(img, silent=True) demography_batch = DeepFace.analyze(img, silent=True)
# return type should be list of list of dict for batch input
assert isinstance(demography_batch, list)
# 2 image in batch, so 2 demography objects. # 2 image in batch, so 2 demography objects.
assert len(demography_batch) == 2 assert len(demography_batch) == 2
for i, demography_objs in enumerate(demography_batch): for i, demography_objs in enumerate(demography_batch):
assert isinstance(demography_objs, list)
assert len(demography_objs) == expected_num_faces[i] assert len(demography_objs) == expected_num_faces[i]
for demography in demography_objs: # Iterate over faces for demography in demography_objs: # Iterate over faces
assert isinstance(demography, dict) # Check type assert isinstance(demography, dict)
assert demography["age"] > 20 and demography["age"] < 40 assert demography["age"] > 20 and demography["age"] < 40
assert demography["dominant_gender"] in ["Woman", "Man"] assert demography["dominant_gender"] in ["Woman", "Man"]

View File

@ -162,6 +162,8 @@ def test_batched_represent_for_list_input(model_name):
assert len(single_embedding_objs) == len(batched_embedding_objs[idx]) assert len(single_embedding_objs) == len(batched_embedding_objs[idx])
for alpha, beta in zip(single_embedding_objs, batched_embedding_objs[idx]): for alpha, beta in zip(single_embedding_objs, batched_embedding_objs[idx]):
assert isinstance(alpha, dict)
assert isinstance(beta, dict)
assert np.allclose( assert np.allclose(
alpha["embedding"], beta["embedding"], rtol=1e-2, atol=1e-2 alpha["embedding"], beta["embedding"], rtol=1e-2, atol=1e-2
), "Embeddings do not match within tolerance" ), "Embeddings do not match within tolerance"