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

single and batch distance functions are stored in verify module #1359

Merged
merged 2 commits into from
Oct 6, 2024
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
10 changes: 5 additions & 5 deletions deepface/DeepFace.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,18 +323,18 @@ def find(
anti_spoofing (boolean): Flag to enable anti spoofing (default is False).

Returns:
results (List[pd.DataFrame] or List[List[Dict[str, Any]]]):
results (List[pd.DataFrame] or List[List[Dict[str, Any]]]):
A list of pandas dataframes (if `batched=False`) or
a list of dicts (if `batched=True`).
Each dataframe or dict corresponds to the identity information for
an individual detected in the source image.

Note: If you have a large database and/or a source photo with many faces,
use `batched=True`, as it is optimized for large batch processing.
Please pay attention that when using `batched=True`, the function returns
use `batched=True`, as it is optimized for large batch processing.
Please pay attention that when using `batched=True`, the function returns
a list of dicts (not a list of DataFrames),
but with the same keys as the columns in the DataFrame.

The DataFrame columns or dict keys include:

- 'identity': Identity label of the detected individual.
Expand Down Expand Up @@ -364,7 +364,7 @@ def find(
silent=silent,
refresh_database=refresh_database,
anti_spoofing=anti_spoofing,
batched=batched
batched=batched,
)


Expand Down
2 changes: 1 addition & 1 deletion deepface/commons/weight_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ def download_all_models_in_one_shot() -> None:
Download all model weights in one shot
"""

# weight urls as variables
# import model weights from module here to avoid circular import issue
from deepface.models.facial_recognition.VGGFace import WEIGHTS_URL as VGGFACE_WEIGHTS
from deepface.models.facial_recognition.Facenet import FACENET128_WEIGHTS, FACENET512_WEIGHTS
from deepface.models.facial_recognition.OpenFace import WEIGHTS_URL as OPENFACE_WEIGHTS
Expand Down
135 changes: 39 additions & 96 deletions deepface/modules/recognition.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,18 +78,18 @@ def find(


Returns:
results (List[pd.DataFrame] or List[List[Dict[str, Any]]]):
results (List[pd.DataFrame] or List[List[Dict[str, Any]]]):
A list of pandas dataframes (if `batched=False`) or
a list of dicts (if `batched=True`).
Each dataframe or dict corresponds to the identity information for
an individual detected in the source image.

Note: If you have a large database and/or a source photo with many faces,
use `batched=True`, as it is optimized for large batch processing.
Please pay attention that when using `batched=True`, the function returns
use `batched=True`, as it is optimized for large batch processing.
Please pay attention that when using `batched=True`, the function returns
a list of dicts (not a list of DataFrames),
but with the same keys as the columns in the DataFrame.

The DataFrame columns or dict keys include:

- 'identity': Identity label of the detected individual.
Expand Down Expand Up @@ -266,7 +266,7 @@ def find(
align,
threshold,
normalization,
anti_spoofing
anti_spoofing,
)

df = pd.DataFrame(representations)
Expand Down Expand Up @@ -441,6 +441,7 @@ def __find_bulk_embeddings(

return representations


def find_batched(
representations: List[Dict[str, Any]],
source_objs: List[Dict[str, Any]],
Expand All @@ -459,19 +460,19 @@ def find_batched(
The function uses batch processing for efficient computation of distances.

Args:
representations (List[Dict[str, Any]]):
A list of dictionaries containing precomputed target embeddings and associated metadata.
representations (List[Dict[str, Any]]):
A list of dictionaries containing precomputed target embeddings and associated metadata.
Each dictionary should have at least the key `embedding`.
source_objs (List[Dict[str, Any]]):

source_objs (List[Dict[str, Any]]):
A list of dictionaries representing the source images to compare against
the target embeddings. Each dictionary should contain:
- `face`: The image data or path to the source face image.
- `facial_area`: A dictionary with keys `x`, `y`, `w`, `h`
indicating the facial region.
- Optionally, `is_real`: A boolean indicating if the face is real
(used for anti-spoofing).

model_name (str): Model for face recognition. Options: VGG-Face, Facenet, Facenet512,
OpenFace, DeepFace, DeepID, Dlib, ArcFace, SFace and GhostFaceNet (default is VGG-Face).

Expand Down Expand Up @@ -499,7 +500,7 @@ def find_batched(
anti_spoofing (boolean): Flag to enable anti spoofing (default is False).

Returns:
List[List[Dict[str, Any]]]:
List[List[Dict[str, Any]]]:
A list where each element corresponds to a source face and
contains a list of dictionaries with matching faces.
"""
Expand All @@ -508,27 +509,24 @@ def find_batched(
metadata = set()

for item in representations:
emb = item.get('embedding')
emb = item.get("embedding")
if emb is not None:
embeddings_list.append(emb)
valid_mask.append(True)
else:
embeddings_list.append(np.zeros_like(representations[0]['embedding']))
embeddings_list.append(np.zeros_like(representations[0]["embedding"]))
valid_mask.append(False)

metadata.update(item.keys())

# remove embedding key from other keys
metadata.discard('embedding')
metadata.discard("embedding")
metadata = list(metadata)

embeddings = np.array(embeddings_list) # (N, D)
valid_mask = np.array(valid_mask) # (N,)
embeddings = np.array(embeddings_list) # (N, D)
valid_mask = np.array(valid_mask) # (N,)

data = {
key: np.array([item.get(key, None) for item in representations])
for key in metadata
}
data = {key: np.array([item.get(key, None) for item in representations]) for key in metadata}

target_embeddings = []
source_regions = []
Expand Down Expand Up @@ -558,101 +556,46 @@ def find_batched(
target_threshold = threshold or verification.find_threshold(model_name, distance_metric)
target_thresholds.append(target_threshold)

target_embeddings = np.array(target_embeddings) # (M, D)
target_thresholds = np.array(target_thresholds) # (M,)
target_embeddings = np.array(target_embeddings) # (M, D)
target_thresholds = np.array(target_thresholds) # (M,)
source_regions_arr = {
'source_x': np.array([region['x'] for region in source_regions]),
'source_y': np.array([region['y'] for region in source_regions]),
'source_w': np.array([region['w'] for region in source_regions]),
'source_h': np.array([region['h'] for region in source_regions]),
"source_x": np.array([region["x"] for region in source_regions]),
"source_y": np.array([region["y"] for region in source_regions]),
"source_w": np.array([region["w"] for region in source_regions]),
"source_h": np.array([region["h"] for region in source_regions]),
}

def find_cosine_distance_batch(
embeddings: np.ndarray, target_embeddings: np.ndarray
) -> np.ndarray:
"""
Find the cosine distances between batches of embeddings
Args:
embeddings (np.ndarray): array of shape (N, D)
target_embeddings (np.ndarray): array of shape (M, D)
Returns:
np.ndarray: distance matrix of shape (M, N)
"""
embeddings_norm = verification.l2_normalize(embeddings, axis=1)
target_embeddings_norm = verification.l2_normalize(target_embeddings, axis=1)
cosine_similarities = np.dot(target_embeddings_norm, embeddings_norm.T)
cosine_distances = 1 - cosine_similarities
return cosine_distances

def find_euclidean_distance_batch(
embeddings: np.ndarray, target_embeddings: np.ndarray
) -> np.ndarray:
"""
Find the Euclidean distances between batches of embeddings
Args:
embeddings (np.ndarray): array of shape (N, D)
target_embeddings (np.ndarray): array of shape (M, D)
Returns:
np.ndarray: distance matrix of shape (M, N)
"""
diff = embeddings[None, :, :] - target_embeddings[:, None, :] # (M, N, D)
distances = np.linalg.norm(diff, axis=2) # (M, N)
return distances

def find_distance_batch(
embeddings: np.ndarray, target_embeddings: np.ndarray, distance_metric: str,
) -> np.ndarray:
"""
Find pairwise distances between batches of embeddings using the specified distance metric
Args:
embeddings (np.ndarray): array of shape (N, D)
target_embeddings (np.ndarray): array of shape (M, D)
distance_metric (str): distance metric ('cosine', 'euclidean', 'euclidean_l2')
Returns:
np.ndarray: distance matrix of shape (M, N)
"""
if distance_metric == "cosine":
distances = find_cosine_distance_batch(embeddings, target_embeddings)
elif distance_metric == "euclidean":
distances = find_euclidean_distance_batch(embeddings, target_embeddings)
elif distance_metric == "euclidean_l2":
embeddings_norm = verification.l2_normalize(embeddings, axis=1)
target_embeddings_norm = verification.l2_normalize(target_embeddings, axis=1)
distances = find_euclidean_distance_batch(embeddings_norm, target_embeddings_norm)
else:
raise ValueError("Invalid distance_metric passed - ", distance_metric)
return np.round(distances, 6)

distances = find_distance_batch(embeddings, target_embeddings, distance_metric) # (M, N)
distances = verification.find_distance(embeddings, target_embeddings, distance_metric) # (M, N)
distances[:, ~valid_mask] = np.inf

resp_obj = []

for i in range(len(target_embeddings)):
target_distances = distances[i] # (N,)
target_distances = distances[i] # (N,)
target_threshold = target_thresholds[i]

N = embeddings.shape[0]
result_data = dict(data)
result_data.update({
'source_x': np.full(N, source_regions_arr['source_x'][i]),
'source_y': np.full(N, source_regions_arr['source_y'][i]),
'source_w': np.full(N, source_regions_arr['source_w'][i]),
'source_h': np.full(N, source_regions_arr['source_h'][i]),
'threshold': np.full(N, target_threshold),
'distance': target_distances,
})
result_data.update(
{
"source_x": np.full(N, source_regions_arr["source_x"][i]),
"source_y": np.full(N, source_regions_arr["source_y"][i]),
"source_w": np.full(N, source_regions_arr["source_w"][i]),
"source_h": np.full(N, source_regions_arr["source_h"][i]),
"threshold": np.full(N, target_threshold),
"distance": target_distances,
}
)

mask = target_distances <= target_threshold
filtered_data = {key: value[mask] for key, value in result_data.items()}

sorted_indices = np.argsort(filtered_data['distance'])
sorted_indices = np.argsort(filtered_data["distance"])
sorted_data = {key: value[sorted_indices] for key, value in filtered_data.items()}

num_results = len(sorted_data['distance'])
num_results = len(sorted_data["distance"])
result_dicts = [
{key: sorted_data[key][i] for key in sorted_data}
for i in range(num_results)
{key: sorted_data[key][i] for key in sorted_data} for i in range(num_results)
]
resp_obj.append(result_dicts)
return resp_obj
Loading