Skip to content

Commit

Permalink
post batch changes
Browse files Browse the repository at this point in the history
  • Loading branch information
serengil committed Feb 20, 2025
1 parent 6c714a8 commit f758dcc
Show file tree
Hide file tree
Showing 7 changed files with 136 additions and 55 deletions.
4 changes: 2 additions & 2 deletions deepface/DeepFace.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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.
Expand Down
24 changes: 19 additions & 5 deletions deepface/models/FacialRecognition.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

# 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]
Expand All @@ -24,11 +25,24 @@ def forward(self, img: np.ndarray) -> Union[List[float], List[List[float]]]:
"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()
6 changes: 2 additions & 4 deletions deepface/models/facial_recognition/VGGFace.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"https://github.com/serengil/deepface_models/releases/download/v1.0/vgg_face_weights.h5"
)


# pylint: disable=too-few-public-methods
class VggFaceClient(FacialRecognition):
"""
Expand Down Expand Up @@ -70,10 +71,7 @@ def forward(self, img: np.ndarray) -> List[float]:
# 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)
Expand Down
56 changes: 28 additions & 28 deletions deepface/modules/demography.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -11,22 +11,22 @@


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",
align: bool = True,
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.
Expand Down Expand Up @@ -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,
)

# Append the response object to the batch response list.
batch_resp_obj.append(resp_obj)
return batch_resp_obj
# 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

# if actions is passed as tuple with single item, interestingly it becomes str here
if isinstance(actions, str):
Expand Down
24 changes: 12 additions & 12 deletions deepface/modules/representation.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
75 changes: 71 additions & 4 deletions tests/test_analyze.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,13 @@
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")
Expand Down Expand Up @@ -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"

Expand All @@ -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
Expand All @@ -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"
Expand All @@ -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"]

Expand Down
2 changes: 2 additions & 0 deletions tests/test_represent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit f758dcc

Please sign in to comment.