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

View File

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

View File

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

View File

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

View File

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

View File

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

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])
for alpha, beta in zip(single_embedding_objs, batched_embedding_objs[idx]):
assert isinstance(alpha, dict)
assert isinstance(beta, dict)
assert np.allclose(
alpha["embedding"], beta["embedding"], rtol=1e-2, atol=1e-2
), "Embeddings do not match within tolerance"