support embedding input for verify

This commit is contained in:
Sefik Ilkin Serengil 2024-03-08 15:22:58 +00:00
parent d7c2998e1b
commit 6eced68e69
3 changed files with 183 additions and 46 deletions

View File

@ -62,6 +62,7 @@ def verify(
align: bool = True,
expand_percentage: int = 0,
normalization: str = "base",
silent: bool = False,
) -> Dict[str, Any]:
"""
Verify if an image pair represents the same person or different persons.
@ -91,6 +92,9 @@ def verify(
normalization (string): Normalize the input image before feeding it to the model.
Options: base, raw, Facenet, Facenet2018, VGGFace, VGGFace2, ArcFace (default is base)
silent (boolean): Suppress or allow some log messages for a quieter analysis process
(default is False).
Returns:
result (dict): A dictionary containing verification results with following keys.
@ -126,6 +130,7 @@ def verify(
align=align,
expand_percentage=expand_percentage,
normalization=normalization,
silent=silent,
)

View File

@ -1,6 +1,6 @@
# built-in dependencies
import time
from typing import Any, Dict, Union
from typing import Any, Dict, Union, List, Tuple
# 3rd party dependencies
import numpy as np
@ -8,11 +8,14 @@ import numpy as np
# project dependencies
from deepface.modules import representation, detection, modeling
from deepface.models.FacialRecognition import FacialRecognition
from deepface.commons.logger import Logger
logger = Logger(module="deepface/modules/verification.py")
def verify(
img1_path: Union[str, np.ndarray],
img2_path: Union[str, np.ndarray],
img1_path: Union[str, np.ndarray, List[float]],
img2_path: Union[str, np.ndarray, List[float]],
model_name: str = "VGG-Face",
detector_backend: str = "opencv",
distance_metric: str = "cosine",
@ -20,6 +23,7 @@ def verify(
align: bool = True,
expand_percentage: int = 0,
normalization: str = "base",
silent: bool = False,
) -> Dict[str, Any]:
"""
Verify if an image pair represents the same person or different persons.
@ -30,10 +34,10 @@ def verify(
Args:
img1_path (str or np.ndarray): Path to the first image. Accepts exact image path
as a string, numpy array (BGR), or base64 encoded images.
as a string, numpy array (BGR), base64 encoded images or pre-calculated embeddings.
img2_path (str or np.ndarray): Path to the second image. Accepts exact image path
as a string, numpy array (BGR), or base64 encoded images.
as a string, numpy array (BGR), base64 encoded images or pre-calculated embeddings.
model_name (str): Model for face recognition. Options: VGG-Face, Facenet, Facenet512,
OpenFace, DeepFace, DeepID, Dlib, ArcFace and SFace (default is VGG-Face).
@ -54,6 +58,9 @@ def verify(
normalization (string): Normalize the input image before feeding it to the model.
Options: base, raw, Facenet, Facenet2018, VGGFace, VGGFace2, ArcFace (default is base)
silent (boolean): Suppress or allow some log messages for a quieter analysis process
(default is False).
Returns:
result (dict): A dictionary containing verification results.
@ -81,74 +88,96 @@ def verify(
tic = time.time()
# --------------------------------
model: FacialRecognition = modeling.build_model(model_name)
target_size = model.input_shape
dims = model.output_shape
try:
img1_objs = detection.extract_faces(
if isinstance(img1_path, list):
# given image is already pre-calculated embedding
if not all(isinstance(dim, float) for dim in img1_path):
raise ValueError(
"When passing img1_path as a list, ensure that all its items are of type float."
)
if silent is False:
logger.warn(
"You passed 1st image as pre-calculated embeddings."
f"Please ensure that embeddings have been calculated for the {model_name} model."
)
if len(img1_path) != dims:
raise ValueError(
f"embeddings of {model_name} should have {dims} dimensions,"
f" but it has {len(img1_path)} dimensions input"
)
img1_embeddings = [img1_path]
img1_facial_areas = [None]
else:
img1_embeddings, img1_facial_areas = __extract_faces_and_embeddings(
img_path=img1_path,
target_size=target_size,
model_name=model_name,
detector_backend=detector_backend,
grayscale=False,
enforce_detection=enforce_detection,
align=align,
expand_percentage=expand_percentage,
normalization=normalization,
)
except ValueError as err:
raise ValueError("Exception while processing img1_path") from err
try:
img2_objs = detection.extract_faces(
if isinstance(img2_path, list):
# given image is already pre-calculated embedding
if not all(isinstance(dim, float) for dim in img2_path):
raise ValueError(
"When passing img2_path as a list, ensure that all its items are of type float."
)
if silent is False:
logger.warn(
"You passed 2nd image as pre-calculated embeddings."
f"Please ensure that embeddings have been calculated for the {model_name} model."
)
if len(img2_path) != dims:
raise ValueError(
f"embeddings of {model_name} should have {dims} dimensions,"
f" but it has {len(img2_path)} dimensions input"
)
img2_embeddings = [img2_path]
img2_facial_areas = [None]
else:
img2_embeddings, img2_facial_areas = __extract_faces_and_embeddings(
img_path=img2_path,
target_size=target_size,
model_name=model_name,
detector_backend=detector_backend,
grayscale=False,
enforce_detection=enforce_detection,
align=align,
expand_percentage=expand_percentage,
)
except ValueError as err:
raise ValueError("Exception while processing img2_path") from err
img1_embeddings = []
for img1_obj in img1_objs:
img1_embedding_obj = representation.represent(
img_path=img1_obj["face"],
model_name=model_name,
enforce_detection=enforce_detection,
detector_backend="skip",
align=align,
normalization=normalization,
)
img1_embedding = img1_embedding_obj[0]["embedding"]
img1_embeddings.append(img1_embedding)
img2_embeddings = []
for img2_obj in img2_objs:
img2_embedding_obj = representation.represent(
img_path=img2_obj["face"],
model_name=model_name,
enforce_detection=enforce_detection,
detector_backend="skip",
align=align,
normalization=normalization,
)
img2_embedding = img2_embedding_obj[0]["embedding"]
img2_embeddings.append(img2_embedding)
no_facial_area = {
"x": None,
"y": None,
"w": None,
"h": None,
"left_eye": None,
"right_eye": None,
}
distances = []
regions = []
facial_areas = []
for idx, img1_embedding in enumerate(img1_embeddings):
for idy, img2_embedding in enumerate(img2_embeddings):
distance = find_distance(img1_embedding, img2_embedding, distance_metric)
distances.append(distance)
regions.append((img1_objs[idx]["facial_area"], img2_objs[idy]["facial_area"]))
facial_areas.append(
(img1_facial_areas[idx] or no_facial_area, img2_facial_areas[idy] or no_facial_area)
)
# find the face pair with minimum distance
threshold = find_threshold(model_name, distance_metric)
distance = float(min(distances)) # best distance
facial_areas = regions[np.argmin(distances)]
facial_areas = facial_areas[np.argmin(distances)]
toc = time.time()
@ -166,6 +195,58 @@ def verify(
return resp_obj
def __extract_faces_and_embeddings(
img_path: Union[str, np.ndarray],
model_name: str = "VGG-Face",
detector_backend: str = "opencv",
enforce_detection: bool = True,
align: bool = True,
expand_percentage: int = 0,
normalization: str = "base",
) -> Tuple[List[List[float]], List[dict]]:
"""
Extract facial areas and find corresponding embeddings for given image
Returns:
embeddings (List[float])
facial areas (List[dict])
"""
embeddings = []
facial_areas = []
model: FacialRecognition = modeling.build_model(model_name)
target_size = model.input_shape
try:
img_objs = detection.extract_faces(
img_path=img_path,
target_size=target_size,
detector_backend=detector_backend,
grayscale=False,
enforce_detection=enforce_detection,
align=align,
expand_percentage=expand_percentage,
)
except ValueError as err:
raise ValueError("Exception while processing img1_path") from err
# find embeddings for each face
for img_obj in img_objs:
img_embedding_obj = representation.represent(
img_path=img_obj["face"],
model_name=model_name,
enforce_detection=enforce_detection,
detector_backend="skip",
align=align,
normalization=normalization,
)
# already extracted face given, safe to access its 1st item
img_embedding = img_embedding_obj[0]["embedding"]
embeddings.append(img_embedding)
facial_areas.append(img_obj["facial_area"])
return embeddings, facial_areas
def find_cosine_distance(
source_representation: Union[np.ndarray, list], test_representation: Union[np.ndarray, list]
) -> np.float64:

View File

@ -1,3 +1,4 @@
import pytest
import cv2
from deepface import DeepFace
from deepface.commons.logger import Logger
@ -100,3 +101,53 @@ def test_verify_for_preloaded_image():
res = DeepFace.verify(img1, img2)
assert res["verified"] is True
logger.info("✅ test verify for pre-loaded image done")
def test_verify_for_precalculated_embeddings():
model_name = "Facenet"
img1_path = "dataset/img1.jpg"
img2_path = "dataset/img2.jpg"
img1_embedding = DeepFace.represent(img_path=img1_path, model_name=model_name)[0]["embedding"]
img2_embedding = DeepFace.represent(img_path=img2_path, model_name=model_name)[0]["embedding"]
result = DeepFace.verify(
img1_path=img1_embedding, img2_path=img2_embedding, model_name=model_name, silent=True
)
assert result["verified"] is True
assert result["distance"] < result["threshold"]
assert result["model"] == model_name
logger.info("✅ test verify for pre-calculated embeddings done")
def test_verify_with_precalculated_embeddings_for_incorrect_model():
# generate embeddings with VGG (default)
img1_path = "dataset/img1.jpg"
img2_path = "dataset/img2.jpg"
img1_embedding = DeepFace.represent(img_path=img1_path)[0]["embedding"]
img2_embedding = DeepFace.represent(img_path=img2_path)[0]["embedding"]
with pytest.raises(
ValueError,
match="embeddings of Facenet should have 128 dimensions, but it has 4096 dimensions input",
):
_ = DeepFace.verify(
img1_path=img1_embedding, img2_path=img2_embedding, model_name="Facenet", silent=True
)
logger.info("✅ test verify with pre-calculated embeddings for incorrect model done")
def test_verify_for_broken_embeddings():
img1_embeddings = ["a", "b", "c"]
img2_embeddings = [1, 2, 3]
with pytest.raises(
ValueError,
match="When passing img1_path as a list, ensure that all its items are of type float.",
):
_ = DeepFace.verify(img1_path=img1_embeddings, img2_path=img2_embeddings)
logger.info("✅ test verify for broken embeddings content is done")