Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

post batch changes #1440

Merged
merged 1 commit into from
Feb 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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