From cb0285709efec10d063fa0ed27aef9a4471f77da Mon Sep 17 00:00:00 2001 From: Sefik Ilkin Serengil Date: Sat, 5 Oct 2024 21:19:39 +0100 Subject: [PATCH 01/11] unused sequantial removed --- deepface/models/facial_recognition/VGGFace.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/deepface/models/facial_recognition/VGGFace.py b/deepface/models/facial_recognition/VGGFace.py index 56c8a54..fa86e28 100644 --- a/deepface/models/facial_recognition/VGGFace.py +++ b/deepface/models/facial_recognition/VGGFace.py @@ -140,9 +140,7 @@ def load_model( file_name="vgg_face_weights.h5", source_url=url ) - model = weight_utils.load_model_weights( - model=model, weight_file=weight_file - ) + model = weight_utils.load_model_weights(model=model, weight_file=weight_file) # 2622d dimensional model # vgg_face_descriptor = Model(inputs=model.layers[0].input, outputs=model.layers[-2].output) @@ -151,7 +149,6 @@ def load_model( # - softmax causes underfitting # - added normalization layer to avoid underfitting with euclidean # as described here: https://github.com/serengil/deepface/issues/944 - base_model_output = Sequential() base_model_output = Flatten()(model.layers[-5].output) # keras backend's l2 normalization layer troubles some gpu users (e.g. issue 957, 966) # base_model_output = Lambda(lambda x: K.l2_normalize(x, axis=1), name="norm_layer")( From 6d1d6d32b3bc412db8f4a692b867cd41087f2157 Mon Sep 17 00:00:00 2001 From: Sefik Ilkin Serengil Date: Sat, 5 Oct 2024 21:54:15 +0100 Subject: [PATCH 02/11] indexes for source urls of weights added --- deepface/commons/weight_utils.py | 51 ++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/deepface/commons/weight_utils.py b/deepface/commons/weight_utils.py index d1aecf9..067b8f9 100644 --- a/deepface/commons/weight_utils.py +++ b/deepface/commons/weight_utils.py @@ -19,6 +19,40 @@ else: logger = Logger() +# pylint: disable=line-too-long +WEIGHTS = { + "facial_recognition": { + "VGG-Face": "https://github.com/serengil/deepface_models/releases/download/v1.0/vgg_face_weights.h5", + "Facenet": "https://github.com/serengil/deepface_models/releases/download/v1.0/facenet_weights.h5", + "Facenet512": "https://github.com/serengil/deepface_models/releases/download/v1.0/facenet512_weights.h5", + "OpenFace": "https://github.com/serengil/deepface_models/releases/download/v1.0/openface_weights.h5", + "FbDeepFace": "https://github.com/swghosh/DeepFace/releases/download/weights-vggface2-2d-aligned/VGGFace2_DeepFace_weights_val-0.9034.h5.zip", + "ArcFace": "https://github.com/serengil/deepface_models/releases/download/v1.0/arcface_weights.h5", + "DeepID": "https://github.com/serengil/deepface_models/releases/download/v1.0/deepid_keras_weights.h5", + "SFace": "https://github.com/opencv/opencv_zoo/raw/main/models/face_recognition_sface/face_recognition_sface_2021dec.onnx", + "GhostFaceNet": "https://github.com/HamadYA/GhostFaceNets/releases/download/v1.2/GhostFaceNet_W1.3_S1_ArcFace.h5", + "Dlib": "http://dlib.net/files/dlib_face_recognition_resnet_model_v1.dat.bz2", + }, + "demography": { + "Age": "https://github.com/serengil/deepface_models/releases/download/v1.0/age_model_weights.h5", + "Gender": "https://github.com/serengil/deepface_models/releases/download/v1.0/gender_model_weights.h5", + "Emotion": "https://github.com/serengil/deepface_models/releases/download/v1.0/facial_expression_model_weights.h5", + "Race": "https://github.com/serengil/deepface_models/releases/download/v1.0/race_model_single_batch.h5", + }, + "detection": { + "ssd_model": "https://github.com/opencv/opencv/raw/3.4.0/samples/dnn/face_detector/deploy.prototxt", + "ssd_weights": "https://github.com/opencv/opencv_3rdparty/raw/dnn_samples_face_detector_20170830/res10_300x300_ssd_iter_140000.caffemodel", + "yolo": "https://drive.google.com/uc?id=1qcr9DbgsX3ryrz2uU8w4Xm3cOrRywXqb", + "yunet": "https://github.com/opencv/opencv_zoo/raw/main/models/face_detection_yunet/face_detection_yunet_2023mar.onnx", + "dlib": "http://dlib.net/files/shape_predictor_5_face_landmarks.dat.bz2", + "centerface": "https://github.com/Star-Clouds/CenterFace/raw/master/models/onnx/centerface.onnx", + }, + "spoofing": { + "MiniFASNetV2": "https://github.com/minivision-ai/Silent-Face-Anti-Spoofing/raw/master/resources/anti_spoof_models/2.7_80x80_MiniFASNetV2.pth", + "MiniFASNetV1SE": "https://github.com/minivision-ai/Silent-Face-Anti-Spoofing/raw/master/resources/anti_spoof_models/4_0_0_80x80_MiniFASNetV1SE.pth", + }, +} + ALLOWED_COMPRESS_TYPES = ["zip", "bz2"] @@ -95,3 +129,20 @@ def load_model_weights(model: Sequential, weight_file: str) -> Sequential: "and copying it to the target folder." ) from err return model + + +def retrieve_model_source(model_name: str, task: str) -> str: + """ + Find the source url of a given model name + Args: + model_name (str): given model name + Returns: + weight_url (str): source url of the given model + """ + if task not in ["facial_recognition", "detection", "demography", "spoofing"]: + raise ValueError(f"unimplemented task - {task}") + + source_url = WEIGHTS.get(task, {}).get(model_name) + if source_url is None: + raise ValueError(f"Source url cannot be found for given model {task}-{model_name}") + return source_url From 6b115ebac2e9aa28a4dee7945edc75947c9a7603 Mon Sep 17 00:00:00 2001 From: Sefik Ilkin Serengil Date: Sat, 5 Oct 2024 22:09:37 +0100 Subject: [PATCH 03/11] retinaface's nose and mouth field are returned in extract faces --- deepface/models/Detector.py | 7 ++- deepface/models/face_detection/RetinaFace.py | 9 ++++ deepface/modules/detection.py | 46 ++++++++++++++++---- 3 files changed, 52 insertions(+), 10 deletions(-) diff --git a/deepface/models/Detector.py b/deepface/models/Detector.py index be1130f..48f235c 100644 --- a/deepface/models/Detector.py +++ b/deepface/models/Detector.py @@ -45,6 +45,7 @@ class FacialAreaRegion: confidence (float, optional): Confidence score associated with the face detection. Default is None. """ + x: int y: int w: int @@ -52,6 +53,9 @@ class FacialAreaRegion: left_eye: Optional[Tuple[int, int]] = None right_eye: Optional[Tuple[int, int]] = None confidence: Optional[float] = None + nose: Optional[Tuple[int, int]] = None + mouth_right: Optional[Tuple[int, int]] = None + mouth_left: Optional[Tuple[int, int]] = None @dataclass @@ -63,7 +67,8 @@ class DetectedFace: img (np.ndarray): detected face image as numpy array facial_area (FacialAreaRegion): detected face's metadata (e.g. bounding box) confidence (float): confidence score for face detection - """ + """ + img: np.ndarray facial_area: FacialAreaRegion confidence: float diff --git a/deepface/models/face_detection/RetinaFace.py b/deepface/models/face_detection/RetinaFace.py index a3b1468..d3b81e7 100644 --- a/deepface/models/face_detection/RetinaFace.py +++ b/deepface/models/face_detection/RetinaFace.py @@ -42,10 +42,16 @@ class RetinaFaceClient(Detector): # retinaface sets left and right eyes with respect to the person left_eye = identity["landmarks"]["left_eye"] right_eye = identity["landmarks"]["right_eye"] + nose = identity["landmarks"]["nose"] + mouth_right = identity["landmarks"]["mouth_right"] + mouth_left = identity["landmarks"]["mouth_left"] # eyes are list of float, need to cast them tuple of int left_eye = tuple(int(i) for i in left_eye) right_eye = tuple(int(i) for i in right_eye) + nose = tuple(int(i) for i in nose) + mouth_right = tuple(int(i) for i in mouth_right) + mouth_left = tuple(int(i) for i in mouth_left) confidence = identity["score"] @@ -57,6 +63,9 @@ class RetinaFaceClient(Detector): left_eye=left_eye, right_eye=right_eye, confidence=confidence, + nose=nose, + mouth_left=mouth_left, + mouth_right=mouth_right, ) resp.append(facial_area) diff --git a/deepface/modules/detection.py b/deepface/modules/detection.py index 46165a9..6ab3a87 100644 --- a/deepface/modules/detection.py +++ b/deepface/modules/detection.py @@ -148,16 +148,26 @@ def extract_faces( w = min(width - x - 1, int(current_region.w)) h = min(height - y - 1, int(current_region.h)) + facial_area = { + "x": x, + "y": y, + "w": w, + "h": h, + "left_eye": current_region.left_eye, + "right_eye": current_region.right_eye, + } + + # optional nose, mouth_left and mouth_right fields are coming just for retinaface + if current_region.nose: + facial_area["nose"] = current_region.nose + if current_region.mouth_left: + facial_area["mouth_left"] = current_region.mouth_left + if current_region.mouth_right: + facial_area["mouth_right"] = current_region.mouth_right + resp_obj = { "face": current_img, - "facial_area": { - "x": x, - "y": y, - "w": w, - "h": h, - "left_eye": current_region.left_eye, - "right_eye": current_region.right_eye, - }, + "facial_area": facial_area, "confidence": round(float(current_region.confidence or 0), 2), } @@ -272,6 +282,9 @@ def expand_and_align_face( left_eye = facial_area.left_eye right_eye = facial_area.right_eye confidence = facial_area.confidence + nose = facial_area.nose + mouth_left = facial_area.mouth_left + mouth_right = facial_area.mouth_right if expand_percentage > 0: # Expand the facial region height and width by the provided percentage @@ -305,11 +318,26 @@ def expand_and_align_face( left_eye = (left_eye[0] - width_border, left_eye[1] - height_border) if right_eye is not None: right_eye = (right_eye[0] - width_border, right_eye[1] - height_border) + if nose is not None: + nose = (nose[0] - width_border, nose[1] - height_border) + if mouth_left is not None: + mouth_left = (mouth_left[0] - width_border, mouth_left[1] - height_border) + if mouth_right is not None: + mouth_right = (mouth_right[0] - width_border, mouth_right[1] - height_border) return DetectedFace( img=detected_face, facial_area=FacialAreaRegion( - x=x, y=y, h=h, w=w, confidence=confidence, left_eye=left_eye, right_eye=right_eye + x=x, + y=y, + h=h, + w=w, + confidence=confidence, + left_eye=left_eye, + right_eye=right_eye, + nose=nose, + mouth_left=mouth_left, + mouth_right=mouth_right, ), confidence=confidence, ) From f7eb2d7873a72609e4949d158f1953077fa01012 Mon Sep 17 00:00:00 2001 From: Sefik Ilkin Serengil Date: Sat, 5 Oct 2024 22:29:26 +0100 Subject: [PATCH 04/11] too many args warning discarded --- deepface/models/Detector.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deepface/models/Detector.py b/deepface/models/Detector.py index 48f235c..004f0d3 100644 --- a/deepface/models/Detector.py +++ b/deepface/models/Detector.py @@ -6,7 +6,7 @@ import numpy as np # Notice that all facial detector models must be inherited from this class -# pylint: disable=unnecessary-pass, too-few-public-methods +# pylint: disable=unnecessary-pass, too-few-public-methods, too-many-instance-attributes class Detector(ABC): @abstractmethod def detect_faces(self, img: np.ndarray) -> List["FacialAreaRegion"]: From 0ec185721e23243fca52266537736e440897617a Mon Sep 17 00:00:00 2001 From: Sefik Ilkin Serengil Date: Sat, 5 Oct 2024 22:30:20 +0100 Subject: [PATCH 05/11] nose and mouth unavailable case covered --- deepface/models/face_detection/RetinaFace.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/deepface/models/face_detection/RetinaFace.py b/deepface/models/face_detection/RetinaFace.py index d3b81e7..c687322 100644 --- a/deepface/models/face_detection/RetinaFace.py +++ b/deepface/models/face_detection/RetinaFace.py @@ -42,16 +42,19 @@ class RetinaFaceClient(Detector): # retinaface sets left and right eyes with respect to the person left_eye = identity["landmarks"]["left_eye"] right_eye = identity["landmarks"]["right_eye"] - nose = identity["landmarks"]["nose"] - mouth_right = identity["landmarks"]["mouth_right"] - mouth_left = identity["landmarks"]["mouth_left"] + nose = identity["landmarks"].get("nose") + mouth_right = identity["landmarks"].get("mouth_right") + mouth_left = identity["landmarks"].get("mouth_left") # eyes are list of float, need to cast them tuple of int left_eye = tuple(int(i) for i in left_eye) right_eye = tuple(int(i) for i in right_eye) - nose = tuple(int(i) for i in nose) - mouth_right = tuple(int(i) for i in mouth_right) - mouth_left = tuple(int(i) for i in mouth_left) + if nose is not None: + nose = tuple(int(i) for i in nose) + if mouth_right is not None: + mouth_right = tuple(int(i) for i in mouth_right) + if mouth_left is not None: + mouth_left = tuple(int(i) for i in mouth_left) confidence = identity["score"] From 532004e7eeb4400badee220e489bc28535ca44f7 Mon Sep 17 00:00:00 2001 From: Sefik Ilkin Serengil Date: Sat, 5 Oct 2024 22:30:49 +0100 Subject: [PATCH 06/11] avalability of nose and mouth check --- deepface/modules/detection.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/deepface/modules/detection.py b/deepface/modules/detection.py index 6ab3a87..17bd5d9 100644 --- a/deepface/modules/detection.py +++ b/deepface/modules/detection.py @@ -158,11 +158,11 @@ def extract_faces( } # optional nose, mouth_left and mouth_right fields are coming just for retinaface - if current_region.nose: + if current_region.nose is not None: facial_area["nose"] = current_region.nose - if current_region.mouth_left: + if current_region.mouth_left is not None: facial_area["mouth_left"] = current_region.mouth_left - if current_region.mouth_right: + if current_region.mouth_right is not None: facial_area["mouth_right"] = current_region.mouth_right resp_obj = { From 234d0db6c59d0a01103c115af1ad873458da68dc Mon Sep 17 00:00:00 2001 From: Sefik Ilkin Serengil Date: Sun, 6 Oct 2024 09:58:09 +0100 Subject: [PATCH 07/11] all models can be downloaded with one shot --- deepface/commons/weight_utils.py | 127 ++++++++++++------ deepface/models/demography/Age.py | 11 +- deepface/models/demography/Emotion.py | 20 ++- deepface/models/demography/Gender.py | 5 +- deepface/models/demography/Race.py | 18 +-- deepface/models/face_detection/Dlib.py | 3 +- deepface/models/face_detection/Ssd.py | 7 +- deepface/models/face_detection/Yolo.py | 4 +- deepface/models/face_detection/YuNet.py | 4 +- deepface/models/facial_recognition/ArcFace.py | 4 +- deepface/models/facial_recognition/DeepID.py | 5 +- deepface/models/facial_recognition/Dlib.py | 3 +- deepface/models/facial_recognition/Facenet.py | 7 +- .../models/facial_recognition/FbDeepFace.py | 3 +- .../models/facial_recognition/GhostFaceNet.py | 8 +- .../models/facial_recognition/OpenFace.py | 4 +- deepface/models/facial_recognition/SFace.py | 3 +- deepface/models/facial_recognition/VGGFace.py | 6 +- deepface/models/spoofing/FasNet.py | 7 +- 19 files changed, 156 insertions(+), 93 deletions(-) diff --git a/deepface/commons/weight_utils.py b/deepface/commons/weight_utils.py index 067b8f9..3b8a85c 100644 --- a/deepface/commons/weight_utils.py +++ b/deepface/commons/weight_utils.py @@ -11,6 +11,33 @@ import gdown from deepface.commons import folder_utils, package_utils from deepface.commons.logger import Logger +# weight urls as variables +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 +from deepface.models.facial_recognition.FbDeepFace import WEIGHTS_URL as FBDEEPFACE_WEIGHTS +from deepface.models.facial_recognition.ArcFace import WEIGHTS_URL as ARCFACE_WEIGHTS +from deepface.models.facial_recognition.DeepID import WEIGHTS_URL as DEEPID_WEIGHTS +from deepface.models.facial_recognition.SFace import WEIGHTS_URL as SFACE_WEIGHTS +from deepface.models.facial_recognition.GhostFaceNet import WEIGHTS_URL as GHOSTFACENET_WEIGHTS +from deepface.models.facial_recognition.Dlib import WEIGHT_URL as DLIB_FR_WEIGHTS +from deepface.models.demography.Age import WEIGHTS_URL as AGE_WEIGHTS +from deepface.models.demography.Gender import WEIGHTS_URL as GENDER_WEIGHTS +from deepface.models.demography.Race import WEIGHTS_URL as RACE_WEIGHTS +from deepface.models.demography.Emotion import WEIGHTS_URL as EMOTION_WEIGHTS +from deepface.models.spoofing.FasNet import ( + FIRST_WEIGHTS_URL as FASNET_1ST_WEIGHTS, + SECOND_WEIGHTS_URL as FASNET_2ND_WEIGHTS, +) +from deepface.models.face_detection.Ssd import MODEL_URL as SSD_MODEL, WEIGHTS_URL as SSD_WEIGHTS +from deepface.models.face_detection.Yolo import ( + WEIGHT_URL as YOLOV8_WEIGHTS, + WEIGHT_NAME as YOLOV8_WEIGHT_NAME, +) +from deepface.models.face_detection.YuNet import WEIGHTS_URL as YUNET_WEIGHTS +from deepface.models.face_detection.Dlib import WEIGHTS_URL as DLIB_FD_WEIGHTS +from deepface.models.face_detection.CenterFace import WEIGHTS_URL as CENTERFACE_WEIGHTS + tf_version = package_utils.get_tf_major_version() if tf_version == 1: from keras.models import Sequential @@ -20,38 +47,40 @@ else: logger = Logger() # pylint: disable=line-too-long -WEIGHTS = { - "facial_recognition": { - "VGG-Face": "https://github.com/serengil/deepface_models/releases/download/v1.0/vgg_face_weights.h5", - "Facenet": "https://github.com/serengil/deepface_models/releases/download/v1.0/facenet_weights.h5", - "Facenet512": "https://github.com/serengil/deepface_models/releases/download/v1.0/facenet512_weights.h5", - "OpenFace": "https://github.com/serengil/deepface_models/releases/download/v1.0/openface_weights.h5", - "FbDeepFace": "https://github.com/swghosh/DeepFace/releases/download/weights-vggface2-2d-aligned/VGGFace2_DeepFace_weights_val-0.9034.h5.zip", - "ArcFace": "https://github.com/serengil/deepface_models/releases/download/v1.0/arcface_weights.h5", - "DeepID": "https://github.com/serengil/deepface_models/releases/download/v1.0/deepid_keras_weights.h5", - "SFace": "https://github.com/opencv/opencv_zoo/raw/main/models/face_recognition_sface/face_recognition_sface_2021dec.onnx", - "GhostFaceNet": "https://github.com/HamadYA/GhostFaceNets/releases/download/v1.2/GhostFaceNet_W1.3_S1_ArcFace.h5", - "Dlib": "http://dlib.net/files/dlib_face_recognition_resnet_model_v1.dat.bz2", +WEIGHTS = [ + # facial recognition + VGGFACE_WEIGHTS, + FACENET128_WEIGHTS, + FACENET512_WEIGHTS, + OPENFACE_WEIGHTS, + FBDEEPFACE_WEIGHTS, + ARCFACE_WEIGHTS, + DEEPID_WEIGHTS, + SFACE_WEIGHTS, + { + "filename": "ghostfacenet_v1.h5", + "url": GHOSTFACENET_WEIGHTS, }, - "demography": { - "Age": "https://github.com/serengil/deepface_models/releases/download/v1.0/age_model_weights.h5", - "Gender": "https://github.com/serengil/deepface_models/releases/download/v1.0/gender_model_weights.h5", - "Emotion": "https://github.com/serengil/deepface_models/releases/download/v1.0/facial_expression_model_weights.h5", - "Race": "https://github.com/serengil/deepface_models/releases/download/v1.0/race_model_single_batch.h5", + DLIB_FR_WEIGHTS, + # demography + AGE_WEIGHTS, + GENDER_WEIGHTS, + RACE_WEIGHTS, + EMOTION_WEIGHTS, + # spoofing + FASNET_1ST_WEIGHTS, + FASNET_2ND_WEIGHTS, + # face detection + SSD_MODEL, + SSD_WEIGHTS, + { + "filename": YOLOV8_WEIGHT_NAME, + "url": YOLOV8_WEIGHTS, }, - "detection": { - "ssd_model": "https://github.com/opencv/opencv/raw/3.4.0/samples/dnn/face_detector/deploy.prototxt", - "ssd_weights": "https://github.com/opencv/opencv_3rdparty/raw/dnn_samples_face_detector_20170830/res10_300x300_ssd_iter_140000.caffemodel", - "yolo": "https://drive.google.com/uc?id=1qcr9DbgsX3ryrz2uU8w4Xm3cOrRywXqb", - "yunet": "https://github.com/opencv/opencv_zoo/raw/main/models/face_detection_yunet/face_detection_yunet_2023mar.onnx", - "dlib": "http://dlib.net/files/shape_predictor_5_face_landmarks.dat.bz2", - "centerface": "https://github.com/Star-Clouds/CenterFace/raw/master/models/onnx/centerface.onnx", - }, - "spoofing": { - "MiniFASNetV2": "https://github.com/minivision-ai/Silent-Face-Anti-Spoofing/raw/master/resources/anti_spoof_models/2.7_80x80_MiniFASNetV2.pth", - "MiniFASNetV1SE": "https://github.com/minivision-ai/Silent-Face-Anti-Spoofing/raw/master/resources/anti_spoof_models/4_0_0_80x80_MiniFASNetV1SE.pth", - }, -} + YUNET_WEIGHTS, + DLIB_FD_WEIGHTS, + CENTERFACE_WEIGHTS, +] ALLOWED_COMPRESS_TYPES = ["zip", "bz2"] @@ -131,18 +160,30 @@ def load_model_weights(model: Sequential, weight_file: str) -> Sequential: return model -def retrieve_model_source(model_name: str, task: str) -> str: +def download_all_models_in_one_shot() -> None: """ - Find the source url of a given model name - Args: - model_name (str): given model name - Returns: - weight_url (str): source url of the given model + Download all model weights in one shot """ - if task not in ["facial_recognition", "detection", "demography", "spoofing"]: - raise ValueError(f"unimplemented task - {task}") - - source_url = WEIGHTS.get(task, {}).get(model_name) - if source_url is None: - raise ValueError(f"Source url cannot be found for given model {task}-{model_name}") - return source_url + for i in WEIGHTS: + if isinstance(i, str): + url = i + filename = i.split("/")[-1] + compress_type = None + # if compressed file will be downloaded, get rid of its extension + if filename.endswith(tuple(ALLOWED_COMPRESS_TYPES)): + for ext in ALLOWED_COMPRESS_TYPES: + compress_type = ext + if filename.endswith(f".{ext}"): + filename = filename[: -(len(ext) + 1)] + break + elif isinstance(i, dict): + filename = i["filename"] + url = i["url"] + else: + raise ValueError("unimplemented scenario") + logger.info( + f"Downloading {url} to ~/.deepface/weights/{filename} with {compress_type} compression" + ) + download_weights_if_necessary( + file_name=filename, source_url=url, compress_type=compress_type + ) diff --git a/deepface/models/demography/Age.py b/deepface/models/demography/Age.py index 29efdf5..67ab3ae 100644 --- a/deepface/models/demography/Age.py +++ b/deepface/models/demography/Age.py @@ -23,6 +23,10 @@ else: # ---------------------------------------- +WEIGHTS_URL = ( + "https://github.com/serengil/deepface_models/releases/download/v1.0/age_model_weights.h5" +) + # pylint: disable=too-few-public-methods class ApparentAgeClient(Demography): """ @@ -41,7 +45,7 @@ class ApparentAgeClient(Demography): def load_model( - url="https://github.com/serengil/deepface_models/releases/download/v1.0/age_model_weights.h5", + url=WEIGHTS_URL, ) -> Model: """ Construct age model, download its weights and load @@ -70,12 +74,11 @@ def load_model( file_name="age_model_weights.h5", source_url=url ) - age_model = weight_utils.load_model_weights( - model=age_model, weight_file=weight_file - ) + age_model = weight_utils.load_model_weights(model=age_model, weight_file=weight_file) return age_model + def find_apparent_age(age_predictions: np.ndarray) -> np.float64: """ Find apparent age prediction from a given probas of ages diff --git a/deepface/models/demography/Emotion.py b/deepface/models/demography/Emotion.py index 3d1d88f..d2633b5 100644 --- a/deepface/models/demography/Emotion.py +++ b/deepface/models/demography/Emotion.py @@ -7,11 +7,6 @@ from deepface.commons import package_utils, weight_utils from deepface.models.Demography import Demography from deepface.commons.logger import Logger -logger = Logger() - -# ------------------------------------------- -# pylint: disable=line-too-long -# ------------------------------------------- # dependency configuration tf_version = package_utils.get_tf_major_version() @@ -28,12 +23,17 @@ else: Dense, Dropout, ) -# ------------------------------------------- # Labels for the emotions that can be detected by the model. labels = ["angry", "disgust", "fear", "happy", "sad", "surprise", "neutral"] -# pylint: disable=too-few-public-methods +logger = Logger() + +# pylint: disable=line-too-long, disable=too-few-public-methods + +WEIGHTS_URL = "https://github.com/serengil/deepface_models/releases/download/v1.0/facial_expression_model_weights.h5" + + class EmotionClient(Demography): """ Emotion model class @@ -56,7 +56,7 @@ class EmotionClient(Demography): def load_model( - url="https://github.com/serengil/deepface_models/releases/download/v1.0/facial_expression_model_weights.h5", + url=WEIGHTS_URL, ) -> Sequential: """ Consruct emotion model, download and load weights @@ -96,8 +96,6 @@ def load_model( file_name="facial_expression_model_weights.h5", source_url=url ) - model = weight_utils.load_model_weights( - model=model, weight_file=weight_file - ) + model = weight_utils.load_model_weights(model=model, weight_file=weight_file) return model diff --git a/deepface/models/demography/Gender.py b/deepface/models/demography/Gender.py index 2f3a142..ad1c15e 100644 --- a/deepface/models/demography/Gender.py +++ b/deepface/models/demography/Gender.py @@ -21,7 +21,8 @@ if tf_version == 1: else: from tensorflow.keras.models import Model, Sequential from tensorflow.keras.layers import Convolution2D, Flatten, Activation -# ------------------------------------- + +WEIGHTS_URL="https://github.com/serengil/deepface_models/releases/download/v1.0/gender_model_weights.h5" # Labels for the genders that can be detected by the model. labels = ["Woman", "Man"] @@ -43,7 +44,7 @@ class GenderClient(Demography): def load_model( - url="https://github.com/serengil/deepface_models/releases/download/v1.0/gender_model_weights.h5", + url=WEIGHTS_URL, ) -> Model: """ Construct gender model, download its weights and load diff --git a/deepface/models/demography/Race.py b/deepface/models/demography/Race.py index a393667..2334c8b 100644 --- a/deepface/models/demography/Race.py +++ b/deepface/models/demography/Race.py @@ -7,11 +7,8 @@ from deepface.commons import package_utils, weight_utils from deepface.models.Demography import Demography from deepface.commons.logger import Logger -logger = Logger() - -# -------------------------- # pylint: disable=line-too-long -# -------------------------- + # dependency configurations tf_version = package_utils.get_tf_major_version() @@ -21,10 +18,15 @@ if tf_version == 1: else: from tensorflow.keras.models import Model, Sequential from tensorflow.keras.layers import Convolution2D, Flatten, Activation -# -------------------------- + +WEIGHTS_URL = ( + "https://github.com/serengil/deepface_models/releases/download/v1.0/race_model_single_batch.h5" +) # Labels for the ethnic phenotypes that can be detected by the model. labels = ["asian", "indian", "black", "white", "middle eastern", "latino hispanic"] +logger = Logger() + # pylint: disable=too-few-public-methods class RaceClient(Demography): """ @@ -42,7 +44,7 @@ class RaceClient(Demography): def load_model( - url="https://github.com/serengil/deepface_models/releases/download/v1.0/race_model_single_batch.h5", + url=WEIGHTS_URL, ) -> Model: """ Construct race model, download its weights and load @@ -69,8 +71,6 @@ def load_model( file_name="race_model_single_batch.h5", source_url=url ) - race_model = weight_utils.load_model_weights( - model=race_model, weight_file=weight_file - ) + race_model = weight_utils.load_model_weights(model=race_model, weight_file=weight_file) return race_model diff --git a/deepface/models/face_detection/Dlib.py b/deepface/models/face_detection/Dlib.py index c96f1a3..26bce84 100644 --- a/deepface/models/face_detection/Dlib.py +++ b/deepface/models/face_detection/Dlib.py @@ -11,6 +11,7 @@ from deepface.commons.logger import Logger logger = Logger() +WEIGHTS_URL="http://dlib.net/files/shape_predictor_5_face_landmarks.dat.bz2" class DlibClient(Detector): def __init__(self): @@ -34,7 +35,7 @@ class DlibClient(Detector): # check required file exists in the home/.deepface/weights folder weight_file = weight_utils.download_weights_if_necessary( file_name="shape_predictor_5_face_landmarks.dat", - source_url="http://dlib.net/files/shape_predictor_5_face_landmarks.dat.bz2", + source_url=WEIGHTS_URL, compress_type="bz2", ) diff --git a/deepface/models/face_detection/Ssd.py b/deepface/models/face_detection/Ssd.py index 4250888..449144f 100644 --- a/deepface/models/face_detection/Ssd.py +++ b/deepface/models/face_detection/Ssd.py @@ -16,6 +16,9 @@ logger = Logger() # pylint: disable=line-too-long, c-extension-no-member +MODEL_URL = "https://github.com/opencv/opencv/raw/3.4.0/samples/dnn/face_detector/deploy.prototxt" +WEIGHTS_URL = "https://github.com/opencv/opencv_3rdparty/raw/dnn_samples_face_detector_20170830/res10_300x300_ssd_iter_140000.caffemodel" + class SsdClient(Detector): def __init__(self): @@ -31,13 +34,13 @@ class SsdClient(Detector): # model structure output_model = weight_utils.download_weights_if_necessary( file_name="deploy.prototxt", - source_url="https://github.com/opencv/opencv/raw/3.4.0/samples/dnn/face_detector/deploy.prototxt", + source_url=MODEL_URL, ) # pre-trained weights output_weights = weight_utils.download_weights_if_necessary( file_name="res10_300x300_ssd_iter_140000.caffemodel", - source_url="https://github.com/opencv/opencv_3rdparty/raw/dnn_samples_face_detector_20170830/res10_300x300_ssd_iter_140000.caffemodel", + source_url=WEIGHTS_URL, ) try: diff --git a/deepface/models/face_detection/Yolo.py b/deepface/models/face_detection/Yolo.py index a4f5a46..a0211ed 100644 --- a/deepface/models/face_detection/Yolo.py +++ b/deepface/models/face_detection/Yolo.py @@ -12,7 +12,7 @@ from deepface.commons.logger import Logger logger = Logger() # Model's weights paths -PATH = ".deepface/weights/yolov8n-face.pt" +WEIGHT_NAME = "yolov8n-face.pt" # Google Drive URL from repo (https://github.com/derronqi/yolov8-face) ~6MB WEIGHT_URL = "https://drive.google.com/uc?id=1qcr9DbgsX3ryrz2uU8w4Xm3cOrRywXqb" @@ -39,7 +39,7 @@ class YoloClient(Detector): ) from e weight_file = weight_utils.download_weights_if_necessary( - file_name="yolov8n-face.pt", source_url=WEIGHT_URL + file_name=WEIGHT_NAME, source_url=WEIGHT_URL ) # Return face_detector diff --git a/deepface/models/face_detection/YuNet.py b/deepface/models/face_detection/YuNet.py index 398aed2..cd4d155 100644 --- a/deepface/models/face_detection/YuNet.py +++ b/deepface/models/face_detection/YuNet.py @@ -13,6 +13,8 @@ from deepface.commons.logger import Logger logger = Logger() +WEIGHTS_URL = "https://github.com/opencv/opencv_zoo/raw/main/models/face_detection_yunet/face_detection_yunet_2023mar.onnx" + class YuNetClient(Detector): def __init__(self): @@ -41,7 +43,7 @@ class YuNetClient(Detector): # pylint: disable=C0301 weight_file = weight_utils.download_weights_if_necessary( file_name="face_detection_yunet_2023mar.onnx", - source_url="https://github.com/opencv/opencv_zoo/raw/main/models/face_detection_yunet/face_detection_yunet_2023mar.onnx", + source_url=WEIGHTS_URL, ) try: diff --git a/deepface/models/facial_recognition/ArcFace.py b/deepface/models/facial_recognition/ArcFace.py index 596192f..39e8315 100644 --- a/deepface/models/facial_recognition/ArcFace.py +++ b/deepface/models/facial_recognition/ArcFace.py @@ -42,6 +42,8 @@ else: Dense, ) +WEIGHTS_URL="https://github.com/serengil/deepface_models/releases/download/v1.0/arcface_weights.h5" + # pylint: disable=too-few-public-methods class ArcFaceClient(FacialRecognition): """ @@ -56,7 +58,7 @@ class ArcFaceClient(FacialRecognition): def load_model( - url="https://github.com/serengil/deepface_models/releases/download/v1.0/arcface_weights.h5", + url=WEIGHTS_URL, ) -> Model: """ Construct ArcFace model, download its weights and load diff --git a/deepface/models/facial_recognition/DeepID.py b/deepface/models/facial_recognition/DeepID.py index ea03b4e..d449afa 100644 --- a/deepface/models/facial_recognition/DeepID.py +++ b/deepface/models/facial_recognition/DeepID.py @@ -34,8 +34,7 @@ else: # pylint: disable=line-too-long - -# ------------------------------------- +WEIGHTS_URL="https://github.com/serengil/deepface_models/releases/download/v1.0/deepid_keras_weights.h5" # pylint: disable=too-few-public-methods class DeepIdClient(FacialRecognition): @@ -51,7 +50,7 @@ class DeepIdClient(FacialRecognition): def load_model( - url="https://github.com/serengil/deepface_models/releases/download/v1.0/deepid_keras_weights.h5", + url=WEIGHTS_URL, ) -> Model: """ Construct DeepId model, download its weights and load diff --git a/deepface/models/facial_recognition/Dlib.py b/deepface/models/facial_recognition/Dlib.py index 3d50521..7b29dec 100644 --- a/deepface/models/facial_recognition/Dlib.py +++ b/deepface/models/facial_recognition/Dlib.py @@ -12,6 +12,7 @@ from deepface.commons.logger import Logger logger = Logger() # pylint: disable=too-few-public-methods +WEIGHT_URL = "http://dlib.net/files/dlib_face_recognition_resnet_model_v1.dat.bz2" class DlibClient(FacialRecognition): @@ -70,7 +71,7 @@ class DlibResNet: weight_file = weight_utils.download_weights_if_necessary( file_name="dlib_face_recognition_resnet_model_v1.dat", - source_url="http://dlib.net/files/dlib_face_recognition_resnet_model_v1.dat.bz2", + source_url=WEIGHT_URL, compress_type="bz2", ) diff --git a/deepface/models/facial_recognition/Facenet.py b/deepface/models/facial_recognition/Facenet.py index b1ad37c..e296861 100644 --- a/deepface/models/facial_recognition/Facenet.py +++ b/deepface/models/facial_recognition/Facenet.py @@ -39,6 +39,9 @@ else: from tensorflow.keras.layers import add from tensorflow.keras import backend as K +FACENET128_WEIGHTS="https://github.com/serengil/deepface_models/releases/download/v1.0/facenet_weights.h5" +FACENET512_WEIGHTS="https://github.com/serengil/deepface_models/releases/download/v1.0/facenet512_weights.h5" + # -------------------------------- # pylint: disable=too-few-public-methods @@ -1654,7 +1657,7 @@ def InceptionResNetV1(dimension: int = 128) -> Model: def load_facenet128d_model( - url="https://github.com/serengil/deepface_models/releases/download/v1.0/facenet_weights.h5", + url=FACENET128_WEIGHTS, ) -> Model: """ Construct FaceNet-128d model, download weights and then load weights @@ -1676,7 +1679,7 @@ def load_facenet128d_model( def load_facenet512d_model( - url="https://github.com/serengil/deepface_models/releases/download/v1.0/facenet512_weights.h5", + url=FACENET512_WEIGHTS, ) -> Model: """ Construct FaceNet-512d model, download its weights and load diff --git a/deepface/models/facial_recognition/FbDeepFace.py b/deepface/models/facial_recognition/FbDeepFace.py index fb41d62..42b6019 100644 --- a/deepface/models/facial_recognition/FbDeepFace.py +++ b/deepface/models/facial_recognition/FbDeepFace.py @@ -30,6 +30,7 @@ else: Dropout, ) +WEIGHTS_URL="https://github.com/swghosh/DeepFace/releases/download/weights-vggface2-2d-aligned/VGGFace2_DeepFace_weights_val-0.9034.h5.zip" # ------------------------------------- # pylint: disable=line-too-long, too-few-public-methods @@ -54,7 +55,7 @@ class DeepFaceClient(FacialRecognition): def load_model( - url="https://github.com/swghosh/DeepFace/releases/download/weights-vggface2-2d-aligned/VGGFace2_DeepFace_weights_val-0.9034.h5.zip", + url=WEIGHTS_URL, ) -> Model: """ Construct DeepFace model, download its weights and load diff --git a/deepface/models/facial_recognition/GhostFaceNet.py b/deepface/models/facial_recognition/GhostFaceNet.py index 37bd728..bf13fbc 100644 --- a/deepface/models/facial_recognition/GhostFaceNet.py +++ b/deepface/models/facial_recognition/GhostFaceNet.py @@ -48,7 +48,7 @@ else: # pylint: disable=line-too-long, too-few-public-methods, no-else-return, unsubscriptable-object, comparison-with-callable -PRETRAINED_WEIGHTS = "https://github.com/HamadYA/GhostFaceNets/releases/download/v1.2/GhostFaceNet_W1.3_S1_ArcFace.h5" +WEIGHTS_URL = "https://github.com/HamadYA/GhostFaceNets/releases/download/v1.2/GhostFaceNet_W1.3_S1_ArcFace.h5" class GhostFaceNetClient(FacialRecognition): @@ -71,12 +71,10 @@ def load_model(): model = GhostFaceNetV1() weight_file = weight_utils.download_weights_if_necessary( - file_name="ghostfacenet_v1.h5", source_url=PRETRAINED_WEIGHTS + file_name="ghostfacenet_v1.h5", source_url=WEIGHTS_URL ) - model = weight_utils.load_model_weights( - model=model, weight_file=weight_file - ) + model = weight_utils.load_model_weights(model=model, weight_file=weight_file) return model diff --git a/deepface/models/facial_recognition/OpenFace.py b/deepface/models/facial_recognition/OpenFace.py index c9c1b7a..48b4169 100644 --- a/deepface/models/facial_recognition/OpenFace.py +++ b/deepface/models/facial_recognition/OpenFace.py @@ -24,6 +24,8 @@ else: # pylint: disable=unnecessary-lambda +WEIGHTS_URL="https://github.com/serengil/deepface_models/releases/download/v1.0/openface_weights.h5" + # --------------------------------------- # pylint: disable=too-few-public-methods @@ -40,7 +42,7 @@ class OpenFaceClient(FacialRecognition): def load_model( - url="https://github.com/serengil/deepface_models/releases/download/v1.0/openface_weights.h5", + url=WEIGHTS_URL, ) -> Model: """ Consturct OpenFace model, download its weights and load diff --git a/deepface/models/facial_recognition/SFace.py b/deepface/models/facial_recognition/SFace.py index 0f1d421..f6a01ca 100644 --- a/deepface/models/facial_recognition/SFace.py +++ b/deepface/models/facial_recognition/SFace.py @@ -13,6 +13,7 @@ from deepface.commons.logger import Logger logger = Logger() # pylint: disable=line-too-long, too-few-public-methods +WEIGHTS_URL = "https://github.com/opencv/opencv_zoo/raw/main/models/face_recognition_sface/face_recognition_sface_2021dec.onnx" class SFaceClient(FacialRecognition): @@ -47,7 +48,7 @@ class SFaceClient(FacialRecognition): def load_model( - url="https://github.com/opencv/opencv_zoo/raw/main/models/face_recognition_sface/face_recognition_sface_2021dec.onnx", + url=WEIGHTS_URL, ) -> Any: """ Construct SFace model, download its weights and load diff --git a/deepface/models/facial_recognition/VGGFace.py b/deepface/models/facial_recognition/VGGFace.py index fa86e28..bfcbcad 100644 --- a/deepface/models/facial_recognition/VGGFace.py +++ b/deepface/models/facial_recognition/VGGFace.py @@ -38,6 +38,10 @@ else: # --------------------------------------- +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): """ @@ -126,7 +130,7 @@ def base_model() -> Sequential: def load_model( - url="https://github.com/serengil/deepface_models/releases/download/v1.0/vgg_face_weights.h5", + url=WEIGHTS_URL, ) -> Model: """ Final VGG-Face model being used for finding embeddings diff --git a/deepface/models/spoofing/FasNet.py b/deepface/models/spoofing/FasNet.py index 5eb6f92..9b6457d 100644 --- a/deepface/models/spoofing/FasNet.py +++ b/deepface/models/spoofing/FasNet.py @@ -12,6 +12,9 @@ from deepface.commons.logger import Logger logger = Logger() # pylint: disable=line-too-long, too-few-public-methods, nested-min-max +FIRST_WEIGHTS_URL="https://github.com/minivision-ai/Silent-Face-Anti-Spoofing/raw/master/resources/anti_spoof_models/2.7_80x80_MiniFASNetV2.pth" +SECOND_WEIGHTS_URL="https://github.com/minivision-ai/Silent-Face-Anti-Spoofing/raw/master/resources/anti_spoof_models/4_0_0_80x80_MiniFASNetV1SE.pth" + class Fasnet: """ Mini Face Anti Spoofing Net Library from repo: github.com/minivision-ai/Silent-Face-Anti-Spoofing @@ -35,12 +38,12 @@ class Fasnet: # download pre-trained models if not installed yet first_model_weight_file = weight_utils.download_weights_if_necessary( file_name="2.7_80x80_MiniFASNetV2.pth", - source_url="https://github.com/minivision-ai/Silent-Face-Anti-Spoofing/raw/master/resources/anti_spoof_models/2.7_80x80_MiniFASNetV2.pth", + source_url=FIRST_WEIGHTS_URL, ) second_model_weight_file = weight_utils.download_weights_if_necessary( file_name="4_0_0_80x80_MiniFASNetV1SE.pth", - source_url="https://github.com/minivision-ai/Silent-Face-Anti-Spoofing/raw/master/resources/anti_spoof_models/4_0_0_80x80_MiniFASNetV1SE.pth", + source_url=SECOND_WEIGHTS_URL, ) # guarantees Fasnet imported and torch installed From ee4f8e6f024211fcc8c1be76ec5cf4935e0546b4 Mon Sep 17 00:00:00 2001 From: Sefik Ilkin Serengil Date: Sun, 6 Oct 2024 10:03:28 +0100 Subject: [PATCH 08/11] circular import problem --- deepface/commons/weight_utils.py | 128 ++++++++++++++++--------------- 1 file changed, 67 insertions(+), 61 deletions(-) diff --git a/deepface/commons/weight_utils.py b/deepface/commons/weight_utils.py index 3b8a85c..e794405 100644 --- a/deepface/commons/weight_utils.py +++ b/deepface/commons/weight_utils.py @@ -11,32 +11,6 @@ import gdown from deepface.commons import folder_utils, package_utils from deepface.commons.logger import Logger -# weight urls as variables -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 -from deepface.models.facial_recognition.FbDeepFace import WEIGHTS_URL as FBDEEPFACE_WEIGHTS -from deepface.models.facial_recognition.ArcFace import WEIGHTS_URL as ARCFACE_WEIGHTS -from deepface.models.facial_recognition.DeepID import WEIGHTS_URL as DEEPID_WEIGHTS -from deepface.models.facial_recognition.SFace import WEIGHTS_URL as SFACE_WEIGHTS -from deepface.models.facial_recognition.GhostFaceNet import WEIGHTS_URL as GHOSTFACENET_WEIGHTS -from deepface.models.facial_recognition.Dlib import WEIGHT_URL as DLIB_FR_WEIGHTS -from deepface.models.demography.Age import WEIGHTS_URL as AGE_WEIGHTS -from deepface.models.demography.Gender import WEIGHTS_URL as GENDER_WEIGHTS -from deepface.models.demography.Race import WEIGHTS_URL as RACE_WEIGHTS -from deepface.models.demography.Emotion import WEIGHTS_URL as EMOTION_WEIGHTS -from deepface.models.spoofing.FasNet import ( - FIRST_WEIGHTS_URL as FASNET_1ST_WEIGHTS, - SECOND_WEIGHTS_URL as FASNET_2ND_WEIGHTS, -) -from deepface.models.face_detection.Ssd import MODEL_URL as SSD_MODEL, WEIGHTS_URL as SSD_WEIGHTS -from deepface.models.face_detection.Yolo import ( - WEIGHT_URL as YOLOV8_WEIGHTS, - WEIGHT_NAME as YOLOV8_WEIGHT_NAME, -) -from deepface.models.face_detection.YuNet import WEIGHTS_URL as YUNET_WEIGHTS -from deepface.models.face_detection.Dlib import WEIGHTS_URL as DLIB_FD_WEIGHTS -from deepface.models.face_detection.CenterFace import WEIGHTS_URL as CENTERFACE_WEIGHTS tf_version = package_utils.get_tf_major_version() if tf_version == 1: @@ -46,41 +20,7 @@ else: logger = Logger() -# pylint: disable=line-too-long -WEIGHTS = [ - # facial recognition - VGGFACE_WEIGHTS, - FACENET128_WEIGHTS, - FACENET512_WEIGHTS, - OPENFACE_WEIGHTS, - FBDEEPFACE_WEIGHTS, - ARCFACE_WEIGHTS, - DEEPID_WEIGHTS, - SFACE_WEIGHTS, - { - "filename": "ghostfacenet_v1.h5", - "url": GHOSTFACENET_WEIGHTS, - }, - DLIB_FR_WEIGHTS, - # demography - AGE_WEIGHTS, - GENDER_WEIGHTS, - RACE_WEIGHTS, - EMOTION_WEIGHTS, - # spoofing - FASNET_1ST_WEIGHTS, - FASNET_2ND_WEIGHTS, - # face detection - SSD_MODEL, - SSD_WEIGHTS, - { - "filename": YOLOV8_WEIGHT_NAME, - "url": YOLOV8_WEIGHTS, - }, - YUNET_WEIGHTS, - DLIB_FD_WEIGHTS, - CENTERFACE_WEIGHTS, -] +# pylint: disable=line-too-long, use-maxsplit-arg ALLOWED_COMPRESS_TYPES = ["zip", "bz2"] @@ -164,6 +104,72 @@ def download_all_models_in_one_shot() -> None: """ Download all model weights in one shot """ + + # weight urls as variables + 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 + from deepface.models.facial_recognition.FbDeepFace import WEIGHTS_URL as FBDEEPFACE_WEIGHTS + from deepface.models.facial_recognition.ArcFace import WEIGHTS_URL as ARCFACE_WEIGHTS + from deepface.models.facial_recognition.DeepID import WEIGHTS_URL as DEEPID_WEIGHTS + from deepface.models.facial_recognition.SFace import WEIGHTS_URL as SFACE_WEIGHTS + from deepface.models.facial_recognition.GhostFaceNet import WEIGHTS_URL as GHOSTFACENET_WEIGHTS + from deepface.models.facial_recognition.Dlib import WEIGHT_URL as DLIB_FR_WEIGHTS + from deepface.models.demography.Age import WEIGHTS_URL as AGE_WEIGHTS + from deepface.models.demography.Gender import WEIGHTS_URL as GENDER_WEIGHTS + from deepface.models.demography.Race import WEIGHTS_URL as RACE_WEIGHTS + from deepface.models.demography.Emotion import WEIGHTS_URL as EMOTION_WEIGHTS + from deepface.models.spoofing.FasNet import ( + FIRST_WEIGHTS_URL as FASNET_1ST_WEIGHTS, + SECOND_WEIGHTS_URL as FASNET_2ND_WEIGHTS, + ) + from deepface.models.face_detection.Ssd import ( + MODEL_URL as SSD_MODEL, + WEIGHTS_URL as SSD_WEIGHTS, + ) + from deepface.models.face_detection.Yolo import ( + WEIGHT_URL as YOLOV8_WEIGHTS, + WEIGHT_NAME as YOLOV8_WEIGHT_NAME, + ) + from deepface.models.face_detection.YuNet import WEIGHTS_URL as YUNET_WEIGHTS + from deepface.models.face_detection.Dlib import WEIGHTS_URL as DLIB_FD_WEIGHTS + from deepface.models.face_detection.CenterFace import WEIGHTS_URL as CENTERFACE_WEIGHTS + + WEIGHTS = [ + # facial recognition + VGGFACE_WEIGHTS, + FACENET128_WEIGHTS, + FACENET512_WEIGHTS, + OPENFACE_WEIGHTS, + FBDEEPFACE_WEIGHTS, + ARCFACE_WEIGHTS, + DEEPID_WEIGHTS, + SFACE_WEIGHTS, + { + "filename": "ghostfacenet_v1.h5", + "url": GHOSTFACENET_WEIGHTS, + }, + DLIB_FR_WEIGHTS, + # demography + AGE_WEIGHTS, + GENDER_WEIGHTS, + RACE_WEIGHTS, + EMOTION_WEIGHTS, + # spoofing + FASNET_1ST_WEIGHTS, + FASNET_2ND_WEIGHTS, + # face detection + SSD_MODEL, + SSD_WEIGHTS, + { + "filename": YOLOV8_WEIGHT_NAME, + "url": YOLOV8_WEIGHTS, + }, + YUNET_WEIGHTS, + DLIB_FD_WEIGHTS, + CENTERFACE_WEIGHTS, + ] + for i in WEIGHTS: if isinstance(i, str): url = i From 6d71177ff2c69a24a55e4c027cb22a568c1f3748 Mon Sep 17 00:00:00 2001 From: Sefik Ilkin Serengil Date: Sun, 6 Oct 2024 10:18:58 +0100 Subject: [PATCH 09/11] linting updates --- deepface/models/face_detection/YuNet.py | 1 + deepface/models/facial_recognition/Facenet.py | 17 +++++++++-------- .../models/facial_recognition/FbDeepFace.py | 3 +-- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/deepface/models/face_detection/YuNet.py b/deepface/models/face_detection/YuNet.py index cd4d155..9075927 100644 --- a/deepface/models/face_detection/YuNet.py +++ b/deepface/models/face_detection/YuNet.py @@ -13,6 +13,7 @@ from deepface.commons.logger import Logger logger = Logger() +# pylint:disable=line-too-long WEIGHTS_URL = "https://github.com/opencv/opencv_zoo/raw/main/models/face_detection_yunet/face_detection_yunet_2023mar.onnx" diff --git a/deepface/models/facial_recognition/Facenet.py b/deepface/models/facial_recognition/Facenet.py index e296861..b75e620 100644 --- a/deepface/models/facial_recognition/Facenet.py +++ b/deepface/models/facial_recognition/Facenet.py @@ -39,8 +39,13 @@ else: from tensorflow.keras.layers import add from tensorflow.keras import backend as K -FACENET128_WEIGHTS="https://github.com/serengil/deepface_models/releases/download/v1.0/facenet_weights.h5" -FACENET512_WEIGHTS="https://github.com/serengil/deepface_models/releases/download/v1.0/facenet512_weights.h5" +# pylint:disable=line-too-long +FACENET128_WEIGHTS = ( + "https://github.com/serengil/deepface_models/releases/download/v1.0/facenet_weights.h5" +) +FACENET512_WEIGHTS = ( + "https://github.com/serengil/deepface_models/releases/download/v1.0/facenet512_weights.h5" +) # -------------------------------- @@ -1671,9 +1676,7 @@ def load_facenet128d_model( weight_file = weight_utils.download_weights_if_necessary( file_name="facenet_weights.h5", source_url=url ) - model = weight_utils.load_model_weights( - model=model, weight_file=weight_file - ) + model = weight_utils.load_model_weights(model=model, weight_file=weight_file) return model @@ -1692,8 +1695,6 @@ def load_facenet512d_model( weight_file = weight_utils.download_weights_if_necessary( file_name="facenet512_weights.h5", source_url=url ) - model = weight_utils.load_model_weights( - model=model, weight_file=weight_file - ) + model = weight_utils.load_model_weights(model=model, weight_file=weight_file) return model diff --git a/deepface/models/facial_recognition/FbDeepFace.py b/deepface/models/facial_recognition/FbDeepFace.py index 42b6019..7bab660 100644 --- a/deepface/models/facial_recognition/FbDeepFace.py +++ b/deepface/models/facial_recognition/FbDeepFace.py @@ -30,10 +30,9 @@ else: Dropout, ) +# pylint: disable=line-too-long, too-few-public-methods WEIGHTS_URL="https://github.com/swghosh/DeepFace/releases/download/weights-vggface2-2d-aligned/VGGFace2_DeepFace_weights_val-0.9034.h5.zip" -# ------------------------------------- -# pylint: disable=line-too-long, too-few-public-methods class DeepFaceClient(FacialRecognition): """ Fb's DeepFace model class From a93fb63c9729f7645fe76d1908d98ff1f0caa8ac Mon Sep 17 00:00:00 2001 From: Sefik Ilkin Serengil Date: Sun, 6 Oct 2024 20:40:33 +0100 Subject: [PATCH 10/11] single and batch distance functions are stored in verify module --- deepface/DeepFace.py | 10 +-- deepface/commons/weight_utils.py | 2 +- deepface/modules/recognition.py | 135 +++++++++---------------------- deepface/modules/verification.py | 80 +++++++++++++++--- 4 files changed, 113 insertions(+), 114 deletions(-) diff --git a/deepface/DeepFace.py b/deepface/DeepFace.py index 7d7e81f..af5245f 100644 --- a/deepface/DeepFace.py +++ b/deepface/DeepFace.py @@ -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. @@ -364,7 +364,7 @@ def find( silent=silent, refresh_database=refresh_database, anti_spoofing=anti_spoofing, - batched=batched + batched=batched, ) diff --git a/deepface/commons/weight_utils.py b/deepface/commons/weight_utils.py index e794405..d6770c0 100644 --- a/deepface/commons/weight_utils.py +++ b/deepface/commons/weight_utils.py @@ -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 diff --git a/deepface/modules/recognition.py b/deepface/modules/recognition.py index a4848eb..df7068d 100644 --- a/deepface/modules/recognition.py +++ b/deepface/modules/recognition.py @@ -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. @@ -266,7 +266,7 @@ def find( align, threshold, normalization, - anti_spoofing + anti_spoofing, ) df = pd.DataFrame(representations) @@ -441,6 +441,7 @@ def __find_bulk_embeddings( return representations + def find_batched( representations: List[Dict[str, Any]], source_objs: List[Dict[str, Any]], @@ -459,11 +460,11 @@ 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. @@ -471,7 +472,7 @@ def find_batched( 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). @@ -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. """ @@ -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 = [] @@ -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 diff --git a/deepface/modules/verification.py b/deepface/modules/verification.py index 3756db7..83fe409 100644 --- a/deepface/modules/verification.py +++ b/deepface/modules/verification.py @@ -263,14 +263,16 @@ def __extract_faces_and_embeddings( def find_cosine_distance( source_representation: Union[np.ndarray, list], test_representation: Union[np.ndarray, list] -) -> np.float64: +) -> Union[np.float64, np.ndarray]: """ Find cosine distance between two given vectors Args: source_representation (np.ndarray or list): 1st vector test_representation (np.ndarray or list): 2nd vector Returns - distance (np.float64): calculated cosine distance + distance (np.float64 or np.ndarray): calculated cosine distance(s). + it is type of np.float64 for given single embeddings + or type of np.ndarray for given batch embeddings """ if isinstance(source_representation, list): source_representation = np.array(source_representation) @@ -278,22 +280,41 @@ def find_cosine_distance( if isinstance(test_representation, list): test_representation = np.array(test_representation) - a = np.dot(source_representation, test_representation) - b = np.linalg.norm(source_representation) - c = np.linalg.norm(test_representation) - return 1 - a / (b * c) + if len(source_representation.shape) == 1 and len(test_representation.shape) == 1: + # single embedding + a = np.dot(source_representation, test_representation) + b = np.linalg.norm(source_representation) + c = np.linalg.norm(test_representation) + distances = 1 - a / (b * c) + elif len(source_representation.shape) == 2 and len(test_representation.shape) == 2: + # list of embeddings (batch) + # source_representation's shape is (N, D) + # test_representation's shape is (M, D) + # distances' shape is (M, N) + source_embeddings_norm = l2_normalize(source_representation, axis=1) + test_embeddings_norm = l2_normalize(test_representation, axis=1) + cosine_similarities = np.dot(test_embeddings_norm, source_embeddings_norm.T) + distances = 1 - cosine_similarities + else: + raise ValueError( + "embeddings can either be 1 or 2 dimensional " + f"but it is {len(source_representation.shape)} & {len(test_representation.shape)}" + ) + return distances def find_euclidean_distance( source_representation: Union[np.ndarray, list], test_representation: Union[np.ndarray, list] -) -> np.float64: +) -> Union[np.float64, np.ndarray]: """ Find euclidean distance between two given vectors Args: source_representation (np.ndarray or list): 1st vector test_representation (np.ndarray or list): 2nd vector Returns - distance (np.float64): calculated euclidean distance + distance (np.float64 or np.ndarray): calculated euclidean distance(s). + it is type of np.float64 for given single embeddings + or type of np.ndarray for given batch embeddings """ if isinstance(source_representation, list): source_representation = np.array(source_representation) @@ -301,7 +322,23 @@ def find_euclidean_distance( if isinstance(test_representation, list): test_representation = np.array(test_representation) - return np.linalg.norm(source_representation - test_representation) + if len(source_representation.shape) == 1 and len(test_representation.shape) == 1: + # single embedding + diff = source_representation - test_representation + distances = np.linalg.norm(diff) + elif len(source_representation.shape) == 2 and len(test_representation.shape) == 2: + # list of embeddings (batch) + # source_representation's shape is (N, D) + # test_representation's shape is (M, D) + # distances' shape is (M, N) + diff = source_representation[None, :, :] - test_representation[:, None, :] # (M, N, D) + distances = np.linalg.norm(diff, axis=2) # (M, N) + else: + raise ValueError( + "embeddings can either be 1 or 2 dimensional " + f"but it is {len(source_representation.shape)} & {len(test_representation.shape)}" + ) + return distances def l2_normalize( @@ -325,22 +362,41 @@ def find_distance( alpha_embedding: Union[np.ndarray, list], beta_embedding: Union[np.ndarray, list], distance_metric: str, -) -> np.float64: +) -> Union[np.float64, np.ndarray]: """ Wrapper to find distance between vectors according to the given distance metric Args: source_representation (np.ndarray or list): 1st vector test_representation (np.ndarray or list): 2nd vector Returns - distance (np.float64): calculated cosine distance + distance (np.float64 or np.ndarray): calculated cosine distance(s). + it is type of np.float64 for given single embeddings + or type of np.ndarray for given batch embeddings """ + if isinstance(alpha_embedding, list): + alpha_embedding = np.array(alpha_embedding) + + if isinstance(beta_embedding, list): + beta_embedding = np.array(beta_embedding) + if distance_metric == "cosine": distance = find_cosine_distance(alpha_embedding, beta_embedding) elif distance_metric == "euclidean": distance = find_euclidean_distance(alpha_embedding, beta_embedding) elif distance_metric == "euclidean_l2": + if len(alpha_embedding.shape) == 1 and len(beta_embedding.shape) == 1: + # single embedding + axis = None + elif len(alpha_embedding.shape) == 2 and len(beta_embedding.shape) == 2: + # list of embeddings (batch) + axis = 1 + else: + raise ValueError( + "embeddings can either be 1 or 2 dimensional " + f"but it is {len(alpha_embedding.shape)} & {len(beta_embedding.shape)}" + ) distance = find_euclidean_distance( - l2_normalize(alpha_embedding), l2_normalize(beta_embedding) + l2_normalize(alpha_embedding, axis=axis), l2_normalize(beta_embedding, axis=axis) ) else: raise ValueError("Invalid distance_metric passed - ", distance_metric) From 53a96f635ab7261ca4b0b791a6afc07fd5484e31 Mon Sep 17 00:00:00 2001 From: Sefik Ilkin Serengil Date: Sun, 6 Oct 2024 21:00:07 +0100 Subject: [PATCH 11/11] refactoring distance functions --- deepface/modules/verification.py | 144 ++++++++++++++----------------- 1 file changed, 66 insertions(+), 78 deletions(-) diff --git a/deepface/modules/verification.py b/deepface/modules/verification.py index 83fe409..540b63b 100644 --- a/deepface/modules/verification.py +++ b/deepface/modules/verification.py @@ -265,40 +265,34 @@ def find_cosine_distance( source_representation: Union[np.ndarray, list], test_representation: Union[np.ndarray, list] ) -> Union[np.float64, np.ndarray]: """ - Find cosine distance between two given vectors + Find cosine distance between two given vectors or batches of vectors. Args: - source_representation (np.ndarray or list): 1st vector - test_representation (np.ndarray or list): 2nd vector + source_representation (np.ndarray or list): 1st vector or batch of vectors. + test_representation (np.ndarray or list): 2nd vector or batch of vectors. Returns - distance (np.float64 or np.ndarray): calculated cosine distance(s). - it is type of np.float64 for given single embeddings - or type of np.ndarray for given batch embeddings + np.float64 or np.ndarray: Calculated cosine distance(s). + It returns a np.float64 for single embeddings and np.ndarray for batch embeddings. """ - if isinstance(source_representation, list): - source_representation = np.array(source_representation) + # Convert inputs to numpy arrays if necessary + source_representation = np.asarray(source_representation) + test_representation = np.asarray(test_representation) - if isinstance(test_representation, list): - test_representation = np.array(test_representation) - - if len(source_representation.shape) == 1 and len(test_representation.shape) == 1: + if source_representation.ndim == 1 and test_representation.ndim == 1: # single embedding - a = np.dot(source_representation, test_representation) - b = np.linalg.norm(source_representation) - c = np.linalg.norm(test_representation) - distances = 1 - a / (b * c) - elif len(source_representation.shape) == 2 and len(test_representation.shape) == 2: + dot_product = np.dot(source_representation, test_representation) + source_norm = np.linalg.norm(source_representation) + test_norm = np.linalg.norm(test_representation) + distances = 1 - dot_product / (source_norm * test_norm) + elif source_representation.ndim == 2 and test_representation.ndim == 2: # list of embeddings (batch) - # source_representation's shape is (N, D) - # test_representation's shape is (M, D) - # distances' shape is (M, N) - source_embeddings_norm = l2_normalize(source_representation, axis=1) - test_embeddings_norm = l2_normalize(test_representation, axis=1) - cosine_similarities = np.dot(test_embeddings_norm, source_embeddings_norm.T) + source_normed = l2_normalize(source_representation, axis=1) # (N, D) + test_normed = l2_normalize(test_representation, axis=1) # (M, D) + cosine_similarities = np.dot(test_normed, source_normed.T) # (M, N) distances = 1 - cosine_similarities else: raise ValueError( - "embeddings can either be 1 or 2 dimensional " - f"but it is {len(source_representation.shape)} & {len(test_representation.shape)}" + f"Embeddings must be 1D or 2D, but received " + f"source shape: {source_representation.shape}, test shape: {test_representation.shape}" ) return distances @@ -307,36 +301,33 @@ def find_euclidean_distance( source_representation: Union[np.ndarray, list], test_representation: Union[np.ndarray, list] ) -> Union[np.float64, np.ndarray]: """ - Find euclidean distance between two given vectors + Find Euclidean distance between two vectors or batches of vectors. + Args: - source_representation (np.ndarray or list): 1st vector - test_representation (np.ndarray or list): 2nd vector - Returns - distance (np.float64 or np.ndarray): calculated euclidean distance(s). - it is type of np.float64 for given single embeddings - or type of np.ndarray for given batch embeddings + source_representation (np.ndarray or list): 1st vector or batch of vectors. + test_representation (np.ndarray or list): 2nd vector or batch of vectors. + + Returns: + np.float64 or np.ndarray: Euclidean distance(s). + Returns a np.float64 for single embeddings and np.ndarray for batch embeddings. """ - if isinstance(source_representation, list): - source_representation = np.array(source_representation) + # Convert inputs to numpy arrays if necessary + source_representation = np.asarray(source_representation) + test_representation = np.asarray(test_representation) - if isinstance(test_representation, list): - test_representation = np.array(test_representation) - - if len(source_representation.shape) == 1 and len(test_representation.shape) == 1: - # single embedding - diff = source_representation - test_representation - distances = np.linalg.norm(diff) - elif len(source_representation.shape) == 2 and len(test_representation.shape) == 2: - # list of embeddings (batch) - # source_representation's shape is (N, D) - # test_representation's shape is (M, D) - # distances' shape is (M, N) - diff = source_representation[None, :, :] - test_representation[:, None, :] # (M, N, D) + # Single embedding case (1D arrays) + if source_representation.ndim == 1 and test_representation.ndim == 1: + distances = np.linalg.norm(source_representation - test_representation) + # Batch embeddings case (2D arrays) + elif source_representation.ndim == 2 and test_representation.ndim == 2: + diff = ( + source_representation[None, :, :] - test_representation[:, None, :] + ) # (N, D) - (M, D) = (M, N, D) distances = np.linalg.norm(diff, axis=2) # (M, N) else: raise ValueError( - "embeddings can either be 1 or 2 dimensional " - f"but it is {len(source_representation.shape)} & {len(test_representation.shape)}" + f"Embeddings must be 1D or 2D, but received " + f"source shape: {source_representation.shape}, test shape: {test_representation.shape}" ) return distances @@ -352,8 +343,8 @@ def l2_normalize( Returns: np.ndarray: l2 normalized vector """ - if isinstance(x, list): - x = np.array(x) + # Convert inputs to numpy arrays if necessary + x = np.asarray(x) norm = np.linalg.norm(x, axis=axis, keepdims=True) return x / (norm + epsilon) @@ -364,40 +355,37 @@ def find_distance( distance_metric: str, ) -> Union[np.float64, np.ndarray]: """ - Wrapper to find distance between vectors according to the given distance metric - Args: - source_representation (np.ndarray or list): 1st vector - test_representation (np.ndarray or list): 2nd vector - Returns - distance (np.float64 or np.ndarray): calculated cosine distance(s). - it is type of np.float64 for given single embeddings - or type of np.ndarray for given batch embeddings - """ - if isinstance(alpha_embedding, list): - alpha_embedding = np.array(alpha_embedding) + Wrapper to find the distance between vectors based on the specified distance metric. - if isinstance(beta_embedding, list): - beta_embedding = np.array(beta_embedding) + Args: + alpha_embedding (np.ndarray or list): 1st vector or batch of vectors. + beta_embedding (np.ndarray or list): 2nd vector or batch of vectors. + distance_metric (str): The type of distance to compute + ('cosine', 'euclidean', or 'euclidean_l2'). + + Returns: + np.float64 or np.ndarray: The calculated distance(s). + """ + # Convert inputs to numpy arrays if necessary + alpha_embedding = np.asarray(alpha_embedding) + beta_embedding = np.asarray(beta_embedding) + + # Ensure that both embeddings are either 1D or 2D + if alpha_embedding.ndim != beta_embedding.ndim or alpha_embedding.ndim not in (1, 2): + raise ValueError( + f"Both embeddings must be either 1D or 2D, but received " + f"alpha shape: {alpha_embedding.shape}, beta shape: {beta_embedding.shape}" + ) if distance_metric == "cosine": distance = find_cosine_distance(alpha_embedding, beta_embedding) elif distance_metric == "euclidean": distance = find_euclidean_distance(alpha_embedding, beta_embedding) elif distance_metric == "euclidean_l2": - if len(alpha_embedding.shape) == 1 and len(beta_embedding.shape) == 1: - # single embedding - axis = None - elif len(alpha_embedding.shape) == 2 and len(beta_embedding.shape) == 2: - # list of embeddings (batch) - axis = 1 - else: - raise ValueError( - "embeddings can either be 1 or 2 dimensional " - f"but it is {len(alpha_embedding.shape)} & {len(beta_embedding.shape)}" - ) - distance = find_euclidean_distance( - l2_normalize(alpha_embedding, axis=axis), l2_normalize(beta_embedding, axis=axis) - ) + axis = None if alpha_embedding.ndim == 1 else 1 + normalized_alpha = l2_normalize(alpha_embedding, axis=axis) + normalized_beta = l2_normalize(beta_embedding, axis=axis) + distance = find_euclidean_distance(normalized_alpha, normalized_beta) else: raise ValueError("Invalid distance_metric passed - ", distance_metric) return np.round(distance, 6)