diff --git a/DeepFace.py b/DeepFace.py new file mode 100644 index 0000000000000000000000000000000000000000..cb29852c35f70cd6c2f909ec2972c41a29dd1d5b --- /dev/null +++ b/DeepFace.py @@ -0,0 +1,585 @@ +# common dependencies +import os +import warnings +import logging +from typing import Any, Dict, List, Union, Optional +from deepface.commons.os_path import os_path + +# this has to be set before importing tensorflow +os.environ["TF_USE_LEGACY_KERAS"] = "1" + +# pylint: disable=wrong-import-position + +# 3rd party dependencies +import numpy as np +import pandas as pd +import tensorflow as tf + +# package dependencies +from deepface.commons import package_utils, folder_utils +from deepface.commons import logger as log +from deepface.modules import ( + modeling, + representation, + verification, + recognition, + demography, + detection, + streaming, + preprocessing, + cloudservice, +) +from deepface import __version__ + +logger = log.get_singletonish_logger() + +# ----------------------------------- +# configurations for dependencies + +# users should install tf_keras package if they are using tf 2.16 or later versions +package_utils.validate_for_keras3() + +warnings.filterwarnings("ignore") +os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3" +tf_version = package_utils.get_tf_major_version() +if tf_version == 2: + tf.get_logger().setLevel(logging.ERROR) +# ----------------------------------- + +# create required folders if necessary to store model weights +folder_utils.initialize_folder() + + +def build_model(model_name: str) -> Any: + """ + This function builds a deepface model + Args: + model_name (string): face recognition or facial attribute model + VGG-Face, Facenet, OpenFace, DeepFace, DeepID for face recognition + Age, Gender, Emotion, Race for facial attributes + Returns: + built_model + """ + return modeling.build_model(model_name=model_name) + + +def verify( + 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", + enforce_detection: bool = True, + 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. + Args: + img1_path (str or np.ndarray or List[float]): Path to the first image. + Accepts exact image path as a string, numpy array (BGR), base64 encoded images + or pre-calculated embeddings. + + img2_path (str or np.ndarray or List[float]): Path to the second image. + Accepts exact image path 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, SFace and GhostFaceNet (default is VGG-Face). + + detector_backend (string): face detector backend. Options: 'opencv', 'retinaface', + 'mtcnn', 'ssd', 'dlib', 'mediapipe', 'yolov8', 'centerface' or 'skip' + (default is opencv). + + distance_metric (string): Metric for measuring similarity. Options: 'cosine', + 'euclidean', 'euclidean_l2' (default is cosine). + + enforce_detection (boolean): If no face is detected in an image, raise an exception. + Set to False to avoid the exception for low-resolution images (default is True). + + align (bool): Flag to enable face alignment (default is True). + + expand_percentage (int): expand detected facial area with a percentage (default is 0). + + 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. + + - 'verified' (bool): Indicates whether the images represent the same person (True) + or different persons (False). + + - 'distance' (float): The distance measure between the face vectors. + A lower distance indicates higher similarity. + + - 'max_threshold_to_verify' (float): The maximum threshold used for verification. + If the distance is below this threshold, the images are considered a match. + + - 'model' (str): The chosen face recognition model. + + - 'distance_metric' (str): The chosen similarity metric for measuring distances. + + - 'facial_areas' (dict): Rectangular regions of interest for faces in both images. + - 'img1': {'x': int, 'y': int, 'w': int, 'h': int} + Region of interest for the first image. + - 'img2': {'x': int, 'y': int, 'w': int, 'h': int} + Region of interest for the second image. + + - 'time' (float): Time taken for the verification process in seconds. + """ + + return verification.verify( + img1_path=img1_path, + img2_path=img2_path, + model_name=model_name, + detector_backend=detector_backend, + distance_metric=distance_metric, + enforce_detection=enforce_detection, + align=align, + expand_percentage=expand_percentage, + normalization=normalization, + silent=silent, + ) + + +def analyze( + img_path: Union[str, np.ndarray], + actions: Union[tuple, list] = ("emotion", "age", "gender", "race"), + enforce_detection: bool = True, + detector_backend: str = "opencv", + align: bool = True, + expand_percentage: int = 0, + silent: bool = False, +) -> List[Dict[str, Any]]: + """ + Analyze facial attributes such as age, gender, emotion, and race in the provided image. + Args: + img_path (str or np.ndarray): The exact path to the image, a numpy array in BGR format, + or a base64 encoded image. If the source image contains multiple faces, the result will + include information for each detected face. + + actions (tuple): Attributes to analyze. The default is ('age', 'gender', 'emotion', 'race'). + You can exclude some of these attributes from the analysis if needed. + + enforce_detection (boolean): If no face is detected in an image, raise an exception. + Set to False to avoid the exception for low-resolution images (default is True). + + detector_backend (string): face detector backend. Options: 'opencv', 'retinaface', + 'mtcnn', 'ssd', 'dlib', 'mediapipe', 'yolov8', 'centerface' or 'skip' + (default is opencv). + + distance_metric (string): Metric for measuring similarity. Options: 'cosine', + 'euclidean', 'euclidean_l2' (default is cosine). + + align (boolean): Perform alignment based on the eye positions (default is True). + + expand_percentage (int): expand detected facial area with a percentage (default is 0). + + silent (boolean): Suppress or allow some log messages for a quieter analysis process + (default is False). + + Returns: + results (List[Dict[str, Any]]): A list of dictionaries, where each dictionary represents + the analysis results for a detected face. Each dictionary in the list contains the + following keys: + + - 'region' (dict): Represents the rectangular region of the detected face in the image. + - 'x': x-coordinate of the top-left corner of the face. + - 'y': y-coordinate of the top-left corner of the face. + - 'w': Width of the detected face region. + - 'h': Height of the detected face region. + + - 'age' (float): Estimated age of the detected face. + + - 'face_confidence' (float): Confidence score for the detected face. + Indicates the reliability of the face detection. + + - 'dominant_gender' (str): The dominant gender in the detected face. + Either "Man" or "Woman". + + - 'gender' (dict): Confidence scores for each gender category. + - 'Man': Confidence score for the male gender. + - 'Woman': Confidence score for the female gender. + + - 'dominant_emotion' (str): The dominant emotion in the detected face. + Possible values include "sad," "angry," "surprise," "fear," "happy," + "disgust," and "neutral" + + - 'emotion' (dict): Confidence scores for each emotion category. + - 'sad': Confidence score for sadness. + - 'angry': Confidence score for anger. + - 'surprise': Confidence score for surprise. + - 'fear': Confidence score for fear. + - 'happy': Confidence score for happiness. + - 'disgust': Confidence score for disgust. + - 'neutral': Confidence score for neutrality. + + - 'dominant_race' (str): The dominant race in the detected face. + Possible values include "indian," "asian," "latino hispanic," + "black," "middle eastern," and "white." + + - 'race' (dict): Confidence scores for each race category. + - 'indian': Confidence score for Indian ethnicity. + - 'asian': Confidence score for Asian ethnicity. + - 'latino hispanic': Confidence score for Latino/Hispanic ethnicity. + - 'black': Confidence score for Black ethnicity. + - 'middle eastern': Confidence score for Middle Eastern ethnicity. + - 'white': Confidence score for White ethnicity. + """ + return demography.analyze( + img_path=img_path, + actions=actions, + enforce_detection=enforce_detection, + detector_backend=detector_backend, + align=align, + expand_percentage=expand_percentage, + silent=silent, + ) + + +def find( + img_path: Union[str, np.ndarray], + db_path: str, + model_name: str = "VGG-Face", + distance_metric: str = "cosine", + enforce_detection: bool = True, + detector_backend: str = "opencv", + align: bool = True, + expand_percentage: int = 0, + threshold: Optional[float] = None, + normalization: str = "base", + silent: bool = False, +) -> List[pd.DataFrame]: + """ + Identify individuals in a database + Args: + img_path (str or np.ndarray): The exact path to the image, a numpy array in BGR format, + or a base64 encoded image. If the source image contains multiple faces, the result will + include information for each detected face. + + db_path (string): Path to the folder containing image files. All detected faces + in the database will be considered in the decision-making process. + + model_name (str): Model for face recognition. Options: VGG-Face, Facenet, Facenet512, + OpenFace, DeepFace, DeepID, Dlib, ArcFace, SFace and GhostFaceNet (default is VGG-Face). + + distance_metric (string): Metric for measuring similarity. Options: 'cosine', + 'euclidean', 'euclidean_l2' (default is cosine). + + enforce_detection (boolean): If no face is detected in an image, raise an exception. + Set to False to avoid the exception for low-resolution images (default is True). + + detector_backend (string): face detector backend. Options: 'opencv', 'retinaface', + 'mtcnn', 'ssd', 'dlib', 'mediapipe', 'yolov8', 'centerface' or 'skip' + (default is opencv). + + align (boolean): Perform alignment based on the eye positions (default is True). + + expand_percentage (int): expand detected facial area with a percentage (default is 0). + + threshold (float): Specify a threshold to determine whether a pair represents the same + person or different individuals. This threshold is used for comparing distances. + If left unset, default pre-tuned threshold values will be applied based on the specified + model name and distance metric (default is None). + + 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: + results (List[pd.DataFrame]): A list of pandas dataframes. Each dataframe corresponds + to the identity information for an individual detected in the source image. + The DataFrame columns include: + + - 'identity': Identity label of the detected individual. + + - 'target_x', 'target_y', 'target_w', 'target_h': Bounding box coordinates of the + target face in the database. + + - 'source_x', 'source_y', 'source_w', 'source_h': Bounding box coordinates of the + detected face in the source image. + + - 'threshold': threshold to determine a pair whether same person or different persons + + - 'distance': Similarity score between the faces based on the + specified model and distance metric + """ + return recognition.find( + img_path=img_path, + db_path=db_path, + model_name=model_name, + distance_metric=distance_metric, + enforce_detection=enforce_detection, + detector_backend=detector_backend, + align=align, + expand_percentage=expand_percentage, + threshold=threshold, + normalization=normalization, + silent=silent, + ) + + +def represent( + img_path: Union[str, np.ndarray], + model_name: str = "VGG-Face", + enforce_detection: bool = True, + detector_backend: str = "opencv", + align: bool = True, + expand_percentage: int = 0, + normalization: str = "base", +) -> List[Dict[str, Any]]: + """ + Represent facial images as multi-dimensional vector embeddings. + + Args: + img_path (str or np.ndarray): The exact path to the image, a numpy array in BGR format, + or a base64 encoded image. If the source image contains multiple faces, the result will + include information for each detected face. + + model_name (str): Model for face recognition. Options: VGG-Face, Facenet, Facenet512, + OpenFace, DeepFace, DeepID, Dlib, ArcFace, SFace and GhostFaceNet + (default is VGG-Face.). + + enforce_detection (boolean): If no face is detected in an image, raise an exception. + Default is True. Set to False to avoid the exception for low-resolution images + (default is True). + + detector_backend (string): face detector backend. Options: 'opencv', 'retinaface', + 'mtcnn', 'ssd', 'dlib', 'mediapipe', 'yolov8', 'centerface' or 'skip' + (default is opencv). + + align (boolean): Perform alignment based on the eye positions (default is True). + + expand_percentage (int): expand detected facial area with a percentage (default is 0). + + normalization (string): Normalize the input image before feeding it to the model. + Default is base. Options: base, raw, Facenet, Facenet2018, VGGFace, VGGFace2, ArcFace + (default is base). + + Returns: + results (List[Dict[str, Any]]): A list of dictionaries, each containing the + following fields: + + - embedding (List[float]): Multidimensional vector representing facial features. + The number of dimensions varies based on the reference model + (e.g., FaceNet returns 128 dimensions, VGG-Face returns 4096 dimensions). + + - facial_area (dict): Detected facial area by face detection in dictionary format. + Contains 'x' and 'y' as the left-corner point, and 'w' and 'h' + as the width and height. If `detector_backend` is set to 'skip', it represents + the full image area and is nonsensical. + + - face_confidence (float): Confidence score of face detection. If `detector_backend` is set + to 'skip', the confidence will be 0 and is nonsensical. + """ + return representation.represent( + img_path=img_path, + model_name=model_name, + enforce_detection=enforce_detection, + detector_backend=detector_backend, + align=align, + expand_percentage=expand_percentage, + normalization=normalization, + ) + + +def stream( + db_path: str = "", + model_name: str = "VGG-Face", + detector_backend: str = "opencv", + distance_metric: str = "cosine", + enable_face_analysis: bool = True, + source: Any = 0, + time_threshold: int = 5, + frame_threshold: int = 5, +) -> None: + """ + Run real time face recognition and facial attribute analysis + + Args: + db_path (string): Path to the folder containing image files. All detected faces + in the database will be considered in the decision-making process. + + model_name (str): Model for face recognition. Options: VGG-Face, Facenet, Facenet512, + OpenFace, DeepFace, DeepID, Dlib, ArcFace, SFace and GhostFaceNet (default is VGG-Face). + + detector_backend (string): face detector backend. Options: 'opencv', 'retinaface', + 'mtcnn', 'ssd', 'dlib', 'mediapipe', 'yolov8', 'centerface' or 'skip' + (default is opencv). + + distance_metric (string): Metric for measuring similarity. Options: 'cosine', + 'euclidean', 'euclidean_l2' (default is cosine). + + enable_face_analysis (bool): Flag to enable face analysis (default is True). + + source (Any): The source for the video stream (default is 0, which represents the + default camera). + + time_threshold (int): The time threshold (in seconds) for face recognition (default is 5). + + frame_threshold (int): The frame threshold for face recognition (default is 5). + Returns: + None + """ + + time_threshold = max(time_threshold, 1) + frame_threshold = max(frame_threshold, 1) + + streaming.analysis( + db_path=db_path, + model_name=model_name, + detector_backend=detector_backend, + distance_metric=distance_metric, + enable_face_analysis=enable_face_analysis, + source=source, + time_threshold=time_threshold, + frame_threshold=frame_threshold, + ) + + +def extract_faces( + img_path: Union[str, np.ndarray], + detector_backend: str = "opencv", + enforce_detection: bool = True, + align: bool = True, + expand_percentage: int = 0, + grayscale: bool = False, +) -> List[Dict[str, Any]]: + """ + Extract faces from a given image + + Args: + img_path (str or np.ndarray): Path to the first image. Accepts exact image path + as a string, numpy array (BGR), or base64 encoded images. + + detector_backend (string): face detector backend. Options: 'opencv', 'retinaface', + 'mtcnn', 'ssd', 'dlib', 'mediapipe', 'yolov8', 'centerface' or 'skip' + (default is opencv). + + enforce_detection (boolean): If no face is detected in an image, raise an exception. + Set to False to avoid the exception for low-resolution images (default is True). + + align (bool): Flag to enable face alignment (default is True). + + expand_percentage (int): expand detected facial area with a percentage (default is 0). + + grayscale (boolean): Flag to convert the image to grayscale before + processing (default is False). + + Returns: + results (List[Dict[str, Any]]): A list of dictionaries, where each dictionary contains: + + - "face" (np.ndarray): The detected face as a NumPy array. + + - "facial_area" (Dict[str, Any]): The detected face's regions as a dictionary containing: + - keys 'x', 'y', 'w', 'h' with int values + - keys 'left_eye', 'right_eye' with a tuple of 2 ints as values. left and right eyes + are eyes on the left and right respectively with respect to the person itself + instead of observer. + + - "confidence" (float): The confidence score associated with the detected face. + """ + + return detection.extract_faces( + img_path=img_path, + detector_backend=detector_backend, + enforce_detection=enforce_detection, + align=align, + expand_percentage=expand_percentage, + grayscale=grayscale, + ) + + +def cli() -> None: + """ + command line interface function will be offered in this block + """ + import fire + + fire.Fire() + + +# deprecated function(s) + + +def detectFace( + img_path: Union[str, np.ndarray], + target_size: tuple = (224, 224), + detector_backend: str = "opencv", + enforce_detection: bool = True, + align: bool = True, +) -> Union[np.ndarray, None]: + """ + Deprecated face detection function. Use extract_faces for same functionality. + + Args: + img_path (str or np.ndarray): Path to the first image. Accepts exact image path + as a string, numpy array (BGR), or base64 encoded images. + + target_size (tuple): final shape of facial image. black pixels will be + added to resize the image (default is (224, 224)). + + detector_backend (string): face detector backend. Options: 'opencv', 'retinaface', + 'mtcnn', 'ssd', 'dlib', 'mediapipe', 'yolov8', 'centerface' or 'skip' + (default is opencv). + + enforce_detection (boolean): If no face is detected in an image, raise an exception. + Set to False to avoid the exception for low-resolution images (default is True). + + align (bool): Flag to enable face alignment (default is True). + + Returns: + img (np.ndarray): detected (and aligned) facial area image as numpy array + """ + logger.warn("Function detectFace is deprecated. Use extract_faces instead.") + face_objs = extract_faces( + img_path=img_path, + detector_backend=detector_backend, + enforce_detection=enforce_detection, + align=align, + grayscale=False, + ) + extracted_face = None + if len(face_objs) > 0: + extracted_face = face_objs[0]["face"] + extracted_face = preprocessing.resize_image(img=extracted_face, target_size=target_size) + return extracted_face + + +def sync_datasets(): + # Set the local directories + base_dir = os_path.get_main_directory() + + missing_dir = os.path.join(base_dir, 'mafqoud', 'images', 'missing_people') + founded_dir = os.path.join(base_dir, 'mafqoud', 'images', 'founded_people') + + # Ensure the directories exist + os.makedirs(missing_dir, exist_ok=True) + os.makedirs(founded_dir, exist_ok=True) + + missing_people = cloudservice.sync_folder('missing_people', missing_dir) + + founded_people = cloudservice.sync_folder('founded_people', founded_dir) + +def delete_pkls(): + # Set the local directories + base_dir = os_path.get_main_directory() + + missing_dir = os.path.join(base_dir, 'mafqoud', 'images', 'missing_people') + founded_dir = os.path.join(base_dir, 'mafqoud', 'images', 'founded_people') + + # Ensure the directories exist + os.makedirs(missing_dir, exist_ok=True) + os.makedirs(founded_dir, exist_ok=True) + + cloudservice.delete_pkl_files(missing_dir) + cloudservice.delete_pkl_files(founded_dir) + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..2b0f9fbb7d024c081d12ea0418b02a0969c5d35a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Sefik Ilkin Serengil + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..cb8e9aeb40d45d5c58a014b95cc19eb7237a7908 --- /dev/null +++ b/Makefile @@ -0,0 +1,8 @@ +test: + cd tests && python -m pytest . -s --disable-warnings + +lint: + python -m pylint deepface/ --fail-under=10 + +coverage: + pip install pytest-cov && cd tests && python -m pytest --cov=deepface \ No newline at end of file diff --git a/Train.py b/Train.py new file mode 100644 index 0000000000000000000000000000000000000000..4ad7bc8847148bf242801204469afbf50647c446 --- /dev/null +++ b/Train.py @@ -0,0 +1,55 @@ + +# from deepface import DeepFace +# import os +# models = [ +# "VGG-Face", +# "Facenet", +# "Facenet512", +# "OpenFace", +# "DeepFace", +# "DeepID", +# "ArcFace", +# "Dlib", +# "SFace", +# ] + +# metrics = ["cosine", "euclidean", "euclidean_l2"] + +# backends = [ +# 'opencv', +# 'ssd', +# 'dlib', +# 'mtcnn', +# 'retinaface', +# 'mediapipe', +# 'yolov8', +# 'yunet', +# 'fastmtcnn', +# ] + +# # df = DeepFace.find(img_path='F:/projects/python/mafqoud/dataset/missing_people/m0.jpg' +# # , db_path='F:/projects/python/mafqoud/dataset/founded_people' +# # , enforce_detection = True +# # , model_name = models[2] +# # , distance_metric = metrics[2] +# # , detector_backend = backends[3]) + +# DeepFace.stream(db_path = "F:/deepface") + +# base_dir = os.path.abspath(os.path.dirname(__file__)) +# # base_dir = "f:\\" +# founded_dir = os.path.join(base_dir, 'mafqoud', 'images', 'founded_people') +# def get_main_directory(): +# path = os.path.abspath(__file__) +# drive, _ = os.path.splitdrive(path) +# if not drive.endswith(os.path.sep): +# drive += os.path.sep +# return drive + +# base_dir = get_main_directory() +# missing_dir = os.path.join(base_dir, 'mafqoud', 'images', 'missing_people') +# print(missing_dir) + +# print(base_dir) +# print(missing_dir) +# print(founded_dir) \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..69d27b416452135a4da763cc8c98a8872648ee34 --- /dev/null +++ b/__init__.py @@ -0,0 +1 @@ +__version__ = "0.0.90" diff --git a/__pycache__/DeepFace.cpython-312.pyc b/__pycache__/DeepFace.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..07660320af17f02337ef35b18d4b21dfb4fd26d2 Binary files /dev/null and b/__pycache__/DeepFace.cpython-312.pyc differ diff --git a/__pycache__/__init__.cpython-312.pyc b/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e596fa2bbf8d79722878d5466394b7a5b54c72c1 Binary files /dev/null and b/__pycache__/__init__.cpython-312.pyc differ diff --git a/api/__init__.py b/api/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/api/__pycache__/__init__.cpython-312.pyc b/api/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f73d5c0638c5bc62fa9cba5fba5e9688e58c6086 Binary files /dev/null and b/api/__pycache__/__init__.cpython-312.pyc differ diff --git a/api/postman/deepface-api.postman_collection.json b/api/postman/deepface-api.postman_collection.json new file mode 100644 index 0000000000000000000000000000000000000000..0cbb0a3886499a37bc54b4a61b780eccbdd22f96 --- /dev/null +++ b/api/postman/deepface-api.postman_collection.json @@ -0,0 +1,102 @@ +{ + "info": { + "_postman_id": "4c0b144e-4294-4bdd-8072-bcb326b1fed2", + "name": "deepface-api", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "Represent", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"model_name\": \"Facenet\",\n \"img\": \"/Users/sefik/Desktop/deepface/tests/dataset/img1.jpg\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://127.0.0.1:5000/represent", + "protocol": "http", + "host": [ + "127", + "0", + "0", + "1" + ], + "port": "5000", + "path": [ + "represent" + ] + } + }, + "response": [] + }, + { + "name": "Face verification", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": " {\n \t\"img1_path\": \"/Users/sefik/Desktop/deepface/tests/dataset/img1.jpg\",\n \"img2_path\": \"/Users/sefik/Desktop/deepface/tests/dataset/img2.jpg\",\n \"model_name\": \"Facenet\",\n \"detector_backend\": \"mtcnn\",\n \"distance_metric\": \"euclidean\"\n }", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://127.0.0.1:5000/verify", + "protocol": "http", + "host": [ + "127", + "0", + "0", + "1" + ], + "port": "5000", + "path": [ + "verify" + ] + } + }, + "response": [] + }, + { + "name": "Face analysis", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"img_path\": \"/Users/sefik/Desktop/deepface/tests/dataset/couple.jpg\",\n \"actions\": [\"age\", \"gender\", \"emotion\", \"race\"]\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://127.0.0.1:5000/analyze", + "protocol": "http", + "host": [ + "127", + "0", + "0", + "1" + ], + "port": "5000", + "path": [ + "analyze" + ] + } + }, + "response": [] + } + ] +} \ No newline at end of file diff --git a/api/src/__init__.py b/api/src/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/api/src/__pycache__/__init__.cpython-312.pyc b/api/src/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..76f1d160b308ecc7c721b448b7950fe5f747d6b2 Binary files /dev/null and b/api/src/__pycache__/__init__.cpython-312.pyc differ diff --git a/api/src/__pycache__/app.cpython-312.pyc b/api/src/__pycache__/app.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2009180827db802f40567e1dcaced5caf6aa8a6f Binary files /dev/null and b/api/src/__pycache__/app.cpython-312.pyc differ diff --git a/api/src/api.py b/api/src/api.py new file mode 100644 index 0000000000000000000000000000000000000000..70ccb512210c5b9882c84c073b6359674591d08a --- /dev/null +++ b/api/src/api.py @@ -0,0 +1,10 @@ +import argparse +import app +import os + +if __name__ == "__main__": + deepface_app = app.create_app() + parser = argparse.ArgumentParser() + parser.add_argument("-p", "--port", type=int, default=int(os.getenv('DEFAULT_PORT')), help="Port of serving api") + args = parser.parse_args() + deepface_app.run(host="0.0.0.0", port=args.port) diff --git a/api/src/app.py b/api/src/app.py new file mode 100644 index 0000000000000000000000000000000000000000..69ec52f5cea3175717e4d68924e52d34d23a1d68 --- /dev/null +++ b/api/src/app.py @@ -0,0 +1,11 @@ +# 3rd parth dependencies +from flask import Flask +from deepface.api.src.modules.core.routes import blueprint + + +def create_app(): + app = Flask(__name__) + app.register_blueprint(blueprint) + print(app.url_map) + return app + diff --git a/api/src/modules/__init__.py b/api/src/modules/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/api/src/modules/__pycache__/__init__.cpython-312.pyc b/api/src/modules/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1509f1b4935cda2f331772616f8e03fd776397a9 Binary files /dev/null and b/api/src/modules/__pycache__/__init__.cpython-312.pyc differ diff --git a/api/src/modules/core/__init__.py b/api/src/modules/core/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/api/src/modules/core/__pycache__/__init__.cpython-312.pyc b/api/src/modules/core/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..caa62da742fbd294b70178009710595bbcfdcab6 Binary files /dev/null and b/api/src/modules/core/__pycache__/__init__.cpython-312.pyc differ diff --git a/api/src/modules/core/__pycache__/routes.cpython-312.pyc b/api/src/modules/core/__pycache__/routes.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0edb67d059d24106d0b25c1aa459cdede9017dd6 Binary files /dev/null and b/api/src/modules/core/__pycache__/routes.cpython-312.pyc differ diff --git a/api/src/modules/core/__pycache__/service.cpython-312.pyc b/api/src/modules/core/__pycache__/service.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f9879c164a8bb201e3806b035acf05ce69fa4c92 Binary files /dev/null and b/api/src/modules/core/__pycache__/service.cpython-312.pyc differ diff --git a/api/src/modules/core/routes.py b/api/src/modules/core/routes.py new file mode 100644 index 0000000000000000000000000000000000000000..eb5d942d7e72321dc9f8f0dde055ef8d318fc4fb --- /dev/null +++ b/api/src/modules/core/routes.py @@ -0,0 +1,207 @@ +from flask import Blueprint, request , jsonify +from deepface.api.src.modules.core import service +from deepface.commons.logger import Logger +from deepface.commons.os_path import os_path +import json +import os + +logger = Logger(module="api/src/routes.py") + +blueprint = Blueprint("routes", __name__) + + +@blueprint.route("/") +def home(): + return "

Welcome to DeepFace API!

" + + +@blueprint.route("/represent", methods=["POST"]) +def represent(): + input_args = request.get_json() + + if input_args is None: + return {"message": "empty input set passed"} + + img_path = input_args.get("img") or input_args.get("img_path") + if img_path is None: + return {"message": "you must pass img_path input"} + + model_name = input_args.get("model_name", "VGG-Face") + detector_backend = input_args.get("detector_backend", "opencv") + enforce_detection = input_args.get("enforce_detection", True) + align = input_args.get("align", True) + + obj = service.represent( + img_path=img_path, + model_name=model_name, + detector_backend=detector_backend, + enforce_detection=enforce_detection, + align=align, + ) + + logger.debug(obj) + + return obj + + +@blueprint.route("/verify", methods=["POST"]) +def verify(): + input_args = request.get_json() + + if input_args is None: + return {"message": "empty input set passed"} + + img1_path = input_args.get("img1") or input_args.get("img1_path") + img2_path = input_args.get("img2") or input_args.get("img2_path") + + if img1_path is None: + return {"message": "you must pass img1_path input"} + + if img2_path is None: + return {"message": "you must pass img2_path input"} + + model_name = input_args.get("model_name", "VGG-Face") + detector_backend = input_args.get("detector_backend", "opencv") + enforce_detection = input_args.get("enforce_detection", True) + distance_metric = input_args.get("distance_metric", "cosine") + align = input_args.get("align", True) + + verification = service.verify( + img1_path=img1_path, + img2_path=img2_path, + model_name=model_name, + detector_backend=detector_backend, + distance_metric=distance_metric, + align=align, + enforce_detection=enforce_detection, + ) + + logger.debug(verification) + + return verification + + +@blueprint.route("/analyze", methods=["POST"]) +def analyze(): + input_args = request.get_json() + + if input_args is None: + return {"message": "empty input set passed"} + + img_path = input_args.get("img") or input_args.get("img_path") + if img_path is None: + return {"message": "you must pass img_path input"} + + detector_backend = input_args.get("detector_backend", "opencv") + enforce_detection = input_args.get("enforce_detection", True) + align = input_args.get("align", True) + actions = input_args.get("actions", ["age", "gender", "emotion", "race"]) + + demographies = service.analyze( + img_path=img_path, + actions=actions, + detector_backend=detector_backend, + enforce_detection=enforce_detection, + align=align, + ) + + logger.debug(demographies) + + return demographies + +@blueprint.route("/find", methods=["POST"]) +def find(): + input_args = request.get_json() + + if input_args is None: + response = jsonify({'error': 'empty input set passed'}) + response.status_code = 500 + return response + + img_name = input_args.get("img") or input_args.get("img_name") + img_type = input_args.get("img_type") + + if img_name is None: + response = jsonify({'error': 'you must pass img_name input'}) + response.status_code = 404 + return response + + if img_type == "missing" or img_type == "missing_person" or img_type == "missing_people" or img_type == "missing person" or img_type == "missing people" : + + img_path = os.path.join( os_path.get_main_directory() , 'mafqoud' , 'images' , "missing_people" , img_name) + db_path = os.path.join( os_path.get_main_directory() , 'mafqoud' , 'images' , "founded_people") + + elif img_type == "founded" or img_type == "founded_person" or img_type == "founded_people" or img_type == "founded person" or img_type == "founded people" : + + img_path = os.path.join( os_path.get_main_directory() , 'mafqoud' , 'images' , "founded_people" , img_name) + db_path = os.path.join( os_path.get_main_directory() , 'mafqoud' , 'images' , "missing_people") + + else : + + response = jsonify({'error': 'the type of the image is not correct and it should be one of those : ( missing , missing_people , missing_people , missing person , missing people ) or ( founded , founded_people , founded_people , founded person , founded people )'}) + response.status_code = 400 + return response + + print(img_path) + if not os.path.exists(img_path) or not os.path.isfile(img_path): + # If the image does not exist, return a JSON response with status code 404 + response = jsonify({'error': 'Image not found'}) + response.status_code = 404 + return response + + + model_name = input_args.get("model_name", "Facenet512") + detector_backend = input_args.get("detector_backend", "mtcnn") + enforce_detection = input_args.get("enforce_detection", True) + distance_metric = input_args.get("distance_metric", "euclidean_l2") + align = input_args.get("align", True) + + if img_name is None: + return {"message": "you must pass img1_path input"} + + if db_path is None: + dataset_path = os.path.join(path.get_parent_path(), 'dataset') + if img_type == "missing_person": + img_path = os.path.join(dataset_path, 'missing_people', img_name) + db_path = os.path.join(dataset_path, 'founded_people') + elif img_type == "founded_people": + img_path = os.path.join(dataset_path, 'founded_people', img_name) + db_path = os.path.join(dataset_path, 'missing_people') + + results = service.find( + img_path=img_path, + db_path=db_path, + model_name=model_name, + detector_backend=detector_backend, + distance_metric=distance_metric, + align=align, + enforce_detection=enforce_detection, + ) + + # Calculate similarity_percentage for each row + results[0]['similarity_percentage'] =100 - ((results[0]['distance'] / results[0]['threshold']) * 100) + + data = [] + for _, row in results[0].iterrows(): + data.append({ + "identity": row['identity'], + "similarity_percentage": row['similarity_percentage'] + }) + + json_data = json.dumps(data, indent=4) + + + logger.debug(json_data) + return json_data + + +@blueprint.route("/dataset/sync", methods=["GET"]) +def sync_datasets(): + result = service.sync_datasets() + return jsonify(result) + + +@blueprint.route("/delete/pkls", methods=["GET"]) +def delete_pkls(): + result = service.delete_pkls() + return jsonify(result) \ No newline at end of file diff --git a/api/src/modules/core/service.py b/api/src/modules/core/service.py new file mode 100644 index 0000000000000000000000000000000000000000..121a8b842292a305332094b3e333cf21a5ddc94e --- /dev/null +++ b/api/src/modules/core/service.py @@ -0,0 +1,84 @@ +from deepface import DeepFace + +# pylint: disable=broad-except + + +def represent(img_path, model_name, detector_backend, enforce_detection, align): + try: + result = {} + embedding_objs = DeepFace.represent( + img_path=img_path, + model_name=model_name, + detector_backend=detector_backend, + enforce_detection=enforce_detection, + align=align, + ) + result["results"] = embedding_objs + return result + except Exception as err: + return {"error": f"Exception while representing: {str(err)}"}, 400 + + +def verify( + img1_path, img2_path, model_name, detector_backend, distance_metric, enforce_detection, align +): + try: + obj = DeepFace.verify( + img1_path=img1_path, + img2_path=img2_path, + model_name=model_name, + detector_backend=detector_backend, + distance_metric=distance_metric, + align=align, + enforce_detection=enforce_detection, + ) + return obj + except Exception as err: + return {"error": f"Exception while verifying: {str(err)}"}, 400 + + +def analyze(img_path, actions, detector_backend, enforce_detection, align): + try: + result = {} + demographies = DeepFace.analyze( + img_path=img_path, + actions=actions, + detector_backend=detector_backend, + enforce_detection=enforce_detection, + align=align, + silent=True, + ) + result["results"] = demographies + return result + except Exception as err: + return {"error": f"Exception while analyzing: {str(err)}"}, 400 + +def find(img_path, db_path, model_name, detector_backend, distance_metric, enforce_detection, align): + try: + obj = DeepFace.find( + img_path=img_path, + db_path=db_path, + model_name=model_name, + detector_backend=detector_backend, + distance_metric=distance_metric, + align=align, + enforce_detection=enforce_detection, + ) + return obj + except Exception as err: + return {"error": f"Exception while Findind: {str(err)}"}, 400 + + +def sync_datasets(): + try: + DeepFace.sync_datasets() + return {'data': 'synced successfully'}, 200 + except Exception as e: + return {'error': str(e)}, 400 + +def delete_pkls(): + try: + DeepFace.delete_pkls() + return {'data': 'pkl files deleted successfully'}, 200 + except Exception as e: + return {'error': str(e)}, 400 \ No newline at end of file diff --git a/basemodels/ArcFace.py b/basemodels/ArcFace.py new file mode 100644 index 0000000000000000000000000000000000000000..43dd4247ccdbc8f52b4e9803425f16a700120ba4 --- /dev/null +++ b/basemodels/ArcFace.py @@ -0,0 +1,179 @@ +import os +import gdown +from deepface.commons import package_utils, folder_utils +from deepface.models.FacialRecognition import FacialRecognition + +from deepface.commons import logger as log + +logger = log.get_singletonish_logger() + +# pylint: disable=unsubscriptable-object + +# -------------------------------- +# dependency configuration + +tf_version = package_utils.get_tf_major_version() + +if tf_version == 1: + from keras.models import Model + from keras.engine import training + from keras.layers import ( + ZeroPadding2D, + Input, + Conv2D, + BatchNormalization, + PReLU, + Add, + Dropout, + Flatten, + Dense, + ) +else: + from tensorflow.keras.models import Model + from tensorflow.python.keras.engine import training + from tensorflow.keras.layers import ( + ZeroPadding2D, + Input, + Conv2D, + BatchNormalization, + PReLU, + Add, + Dropout, + Flatten, + Dense, + ) + +# pylint: disable=too-few-public-methods +class ArcFaceClient(FacialRecognition): + """ + ArcFace model class + """ + + def __init__(self): + self.model = load_model() + self.model_name = "ArcFace" + self.input_shape = (112, 112) + self.output_shape = 512 + + +def load_model( + url="https://github.com/serengil/deepface_models/releases/download/v1.0/arcface_weights.h5", +) -> Model: + """ + Construct ArcFace model, download its weights and load + Returns: + model (Model) + """ + base_model = ResNet34() + inputs = base_model.inputs[0] + arcface_model = base_model.outputs[0] + arcface_model = BatchNormalization(momentum=0.9, epsilon=2e-5)(arcface_model) + arcface_model = Dropout(0.4)(arcface_model) + arcface_model = Flatten()(arcface_model) + arcface_model = Dense(512, activation=None, use_bias=True, kernel_initializer="glorot_normal")( + arcface_model + ) + embedding = BatchNormalization(momentum=0.9, epsilon=2e-5, name="embedding", scale=True)( + arcface_model + ) + model = Model(inputs, embedding, name=base_model.name) + + # --------------------------------------- + # check the availability of pre-trained weights + + home = folder_utils.get_deepface_home() + + file_name = "arcface_weights.h5" + output = home + "/.deepface/weights/" + file_name + + if os.path.isfile(output) != True: + + logger.info(f"{file_name} will be downloaded to {output}") + gdown.download(url, output, quiet=False) + + # --------------------------------------- + + model.load_weights(output) + + return model + + +def ResNet34() -> Model: + """ + ResNet34 model + Returns: + model (Model) + """ + img_input = Input(shape=(112, 112, 3)) + + x = ZeroPadding2D(padding=1, name="conv1_pad")(img_input) + x = Conv2D( + 64, 3, strides=1, use_bias=False, kernel_initializer="glorot_normal", name="conv1_conv" + )(x) + x = BatchNormalization(axis=3, epsilon=2e-5, momentum=0.9, name="conv1_bn")(x) + x = PReLU(shared_axes=[1, 2], name="conv1_prelu")(x) + x = stack_fn(x) + + model = training.Model(img_input, x, name="ResNet34") + + return model + + +def block1(x, filters, kernel_size=3, stride=1, conv_shortcut=True, name=None): + bn_axis = 3 + + if conv_shortcut: + shortcut = Conv2D( + filters, + 1, + strides=stride, + use_bias=False, + kernel_initializer="glorot_normal", + name=name + "_0_conv", + )(x) + shortcut = BatchNormalization( + axis=bn_axis, epsilon=2e-5, momentum=0.9, name=name + "_0_bn" + )(shortcut) + else: + shortcut = x + + x = BatchNormalization(axis=bn_axis, epsilon=2e-5, momentum=0.9, name=name + "_1_bn")(x) + x = ZeroPadding2D(padding=1, name=name + "_1_pad")(x) + x = Conv2D( + filters, + 3, + strides=1, + kernel_initializer="glorot_normal", + use_bias=False, + name=name + "_1_conv", + )(x) + x = BatchNormalization(axis=bn_axis, epsilon=2e-5, momentum=0.9, name=name + "_2_bn")(x) + x = PReLU(shared_axes=[1, 2], name=name + "_1_prelu")(x) + + x = ZeroPadding2D(padding=1, name=name + "_2_pad")(x) + x = Conv2D( + filters, + kernel_size, + strides=stride, + kernel_initializer="glorot_normal", + use_bias=False, + name=name + "_2_conv", + )(x) + x = BatchNormalization(axis=bn_axis, epsilon=2e-5, momentum=0.9, name=name + "_3_bn")(x) + + x = Add(name=name + "_add")([shortcut, x]) + return x + + +def stack1(x, filters, blocks, stride1=2, name=None): + x = block1(x, filters, stride=stride1, name=name + "_block1") + for i in range(2, blocks + 1): + x = block1(x, filters, conv_shortcut=False, name=name + "_block" + str(i)) + return x + + +def stack_fn(x): + x = stack1(x, 64, 3, name="conv2") + x = stack1(x, 128, 4, name="conv3") + x = stack1(x, 256, 6, name="conv4") + return stack1(x, 512, 3, name="conv5") diff --git a/basemodels/DeepID.py b/basemodels/DeepID.py new file mode 100644 index 0000000000000000000000000000000000000000..b68b37915e7d34ac4d18e7e833e68e886d38b932 --- /dev/null +++ b/basemodels/DeepID.py @@ -0,0 +1,99 @@ +import os +import gdown +from deepface.commons import package_utils, folder_utils +from deepface.models.FacialRecognition import FacialRecognition +from deepface.commons import logger as log + +logger = log.get_singletonish_logger() + +tf_version = package_utils.get_tf_major_version() + +if tf_version == 1: + from keras.models import Model + from keras.layers import ( + Conv2D, + Activation, + Input, + Add, + MaxPooling2D, + Flatten, + Dense, + Dropout, + ) +else: + from tensorflow.keras.models import Model + from tensorflow.keras.layers import ( + Conv2D, + Activation, + Input, + Add, + MaxPooling2D, + Flatten, + Dense, + Dropout, + ) + +# pylint: disable=line-too-long + + +# ------------------------------------- + +# pylint: disable=too-few-public-methods +class DeepIdClient(FacialRecognition): + """ + DeepId model class + """ + + def __init__(self): + self.model = load_model() + self.model_name = "DeepId" + self.input_shape = (47, 55) + self.output_shape = 160 + + +def load_model( + url="https://github.com/serengil/deepface_models/releases/download/v1.0/deepid_keras_weights.h5", +) -> Model: + """ + Construct DeepId model, download its weights and load + """ + + myInput = Input(shape=(55, 47, 3)) + + x = Conv2D(20, (4, 4), name="Conv1", activation="relu", input_shape=(55, 47, 3))(myInput) + x = MaxPooling2D(pool_size=2, strides=2, name="Pool1")(x) + x = Dropout(rate=0.99, name="D1")(x) + + x = Conv2D(40, (3, 3), name="Conv2", activation="relu")(x) + x = MaxPooling2D(pool_size=2, strides=2, name="Pool2")(x) + x = Dropout(rate=0.99, name="D2")(x) + + x = Conv2D(60, (3, 3), name="Conv3", activation="relu")(x) + x = MaxPooling2D(pool_size=2, strides=2, name="Pool3")(x) + x = Dropout(rate=0.99, name="D3")(x) + + x1 = Flatten()(x) + fc11 = Dense(160, name="fc11")(x1) + + x2 = Conv2D(80, (2, 2), name="Conv4", activation="relu")(x) + x2 = Flatten()(x2) + fc12 = Dense(160, name="fc12")(x2) + + y = Add()([fc11, fc12]) + y = Activation("relu", name="deepid")(y) + + model = Model(inputs=[myInput], outputs=y) + + # --------------------------------- + + home = folder_utils.get_deepface_home() + + if os.path.isfile(home + "/.deepface/weights/deepid_keras_weights.h5") != True: + logger.info("deepid_keras_weights.h5 will be downloaded...") + + output = home + "/.deepface/weights/deepid_keras_weights.h5" + gdown.download(url, output, quiet=False) + + model.load_weights(home + "/.deepface/weights/deepid_keras_weights.h5") + + return model diff --git a/basemodels/Dlib.py b/basemodels/Dlib.py new file mode 100644 index 0000000000000000000000000000000000000000..f13b7cad1f4c79c5f805b0da4d5fc3a03ee82f7f --- /dev/null +++ b/basemodels/Dlib.py @@ -0,0 +1,89 @@ +from typing import List +import os +import bz2 +import gdown +import numpy as np +from deepface.commons import folder_utils +from deepface.models.FacialRecognition import FacialRecognition +from deepface.commons import logger as log + +logger = log.get_singletonish_logger() + +# pylint: disable=too-few-public-methods + + +class DlibClient(FacialRecognition): + """ + Dlib model class + """ + + def __init__(self): + self.model = DlibResNet() + self.model_name = "Dlib" + self.input_shape = (150, 150) + self.output_shape = 128 + + def forward(self, img: np.ndarray) -> List[float]: + """ + Find embeddings with Dlib model. + This model necessitates the override of the forward method + because it is not a keras model. + Args: + img (np.ndarray): pre-loaded image in BGR + Returns + embeddings (list): multi-dimensional vector + """ + # return self.model.predict(img)[0].tolist() + + # extract_faces returns 4 dimensional images + if len(img.shape) == 4: + img = img[0] + + # bgr to rgb + img = img[:, :, ::-1] # bgr to rgb + + # img is in scale of [0, 1] but expected [0, 255] + if img.max() <= 1: + img = img * 255 + + img = img.astype(np.uint8) + + img_representation = self.model.model.compute_face_descriptor(img) + img_representation = np.array(img_representation) + img_representation = np.expand_dims(img_representation, axis=0) + return img_representation[0].tolist() + + +class DlibResNet: + def __init__(self): + + ## this is not a must dependency. do not import it in the global level. + try: + import dlib + except ModuleNotFoundError as e: + raise ImportError( + "Dlib is an optional dependency, ensure the library is installed." + "Please install using 'pip install dlib' " + ) from e + + home = folder_utils.get_deepface_home() + weight_file = home + "/.deepface/weights/dlib_face_recognition_resnet_model_v1.dat" + + # download pre-trained model if it does not exist + if os.path.isfile(weight_file) != True: + logger.info("dlib_face_recognition_resnet_model_v1.dat is going to be downloaded") + + file_name = "dlib_face_recognition_resnet_model_v1.dat.bz2" + url = f"http://dlib.net/files/{file_name}" + output = f"{home}/.deepface/weights/{file_name}" + gdown.download(url, output, quiet=False) + + zipfile = bz2.BZ2File(output) + data = zipfile.read() + newfilepath = output[:-4] # discard .bz2 extension + with open(newfilepath, "wb") as f: + f.write(data) + + self.model = dlib.face_recognition_model_v1(weight_file) + + # return None # classes must return None diff --git a/basemodels/Facenet.py b/basemodels/Facenet.py new file mode 100644 index 0000000000000000000000000000000000000000..bcbb7b718fcb995663876ad7c22c614e4f77e5c8 --- /dev/null +++ b/basemodels/Facenet.py @@ -0,0 +1,1715 @@ +import os +import gdown +from deepface.commons import package_utils, folder_utils +from deepface.models.FacialRecognition import FacialRecognition +from deepface.commons import logger as log + +logger = log.get_singletonish_logger() + +# -------------------------------- +# dependency configuration + +tf_version = package_utils.get_tf_major_version() + +if tf_version == 1: + from keras.models import Model + from keras.layers import Activation + from keras.layers import BatchNormalization + from keras.layers import Concatenate + from keras.layers import Conv2D + from keras.layers import Dense + from keras.layers import Dropout + from keras.layers import GlobalAveragePooling2D + from keras.layers import Input + from keras.layers import Lambda + from keras.layers import MaxPooling2D + from keras.layers import add + from keras import backend as K +else: + from tensorflow.keras.models import Model + from tensorflow.keras.layers import Activation + from tensorflow.keras.layers import BatchNormalization + from tensorflow.keras.layers import Concatenate + from tensorflow.keras.layers import Conv2D + from tensorflow.keras.layers import Dense + from tensorflow.keras.layers import Dropout + from tensorflow.keras.layers import GlobalAveragePooling2D + from tensorflow.keras.layers import Input + from tensorflow.keras.layers import Lambda + from tensorflow.keras.layers import MaxPooling2D + from tensorflow.keras.layers import add + from tensorflow.keras import backend as K + +# -------------------------------- + +# pylint: disable=too-few-public-methods +class FaceNet128dClient(FacialRecognition): + """ + FaceNet-128d model class + """ + + def __init__(self): + self.model = load_facenet128d_model() + self.model_name = "FaceNet-128d" + self.input_shape = (160, 160) + self.output_shape = 128 + + +class FaceNet512dClient(FacialRecognition): + """ + FaceNet-1512d model class + """ + + def __init__(self): + self.model = load_facenet512d_model() + self.model_name = "FaceNet-512d" + self.input_shape = (160, 160) + self.output_shape = 512 + + +def scaling(x, scale): + return x * scale + + +def InceptionResNetV1(dimension: int = 128) -> Model: + """ + InceptionResNetV1 model heavily inspired from + github.com/davidsandberg/facenet/blob/master/src/models/inception_resnet_v1.py + As mentioned in Sandberg's repo's readme, pre-trained models are using Inception ResNet v1 + Besides training process is documented at + sefiks.com/2018/09/03/face-recognition-with-facenet-in-keras/ + + Args: + dimension (int): number of dimensions in the embedding layer + Returns: + model (Model) + """ + + inputs = Input(shape=(160, 160, 3)) + x = Conv2D(32, 3, strides=2, padding="valid", use_bias=False, name="Conv2d_1a_3x3")(inputs) + x = BatchNormalization( + axis=3, momentum=0.995, epsilon=0.001, scale=False, name="Conv2d_1a_3x3_BatchNorm" + )(x) + x = Activation("relu", name="Conv2d_1a_3x3_Activation")(x) + x = Conv2D(32, 3, strides=1, padding="valid", use_bias=False, name="Conv2d_2a_3x3")(x) + x = BatchNormalization( + axis=3, momentum=0.995, epsilon=0.001, scale=False, name="Conv2d_2a_3x3_BatchNorm" + )(x) + x = Activation("relu", name="Conv2d_2a_3x3_Activation")(x) + x = Conv2D(64, 3, strides=1, padding="same", use_bias=False, name="Conv2d_2b_3x3")(x) + x = BatchNormalization( + axis=3, momentum=0.995, epsilon=0.001, scale=False, name="Conv2d_2b_3x3_BatchNorm" + )(x) + x = Activation("relu", name="Conv2d_2b_3x3_Activation")(x) + x = MaxPooling2D(3, strides=2, name="MaxPool_3a_3x3")(x) + x = Conv2D(80, 1, strides=1, padding="valid", use_bias=False, name="Conv2d_3b_1x1")(x) + x = BatchNormalization( + axis=3, momentum=0.995, epsilon=0.001, scale=False, name="Conv2d_3b_1x1_BatchNorm" + )(x) + x = Activation("relu", name="Conv2d_3b_1x1_Activation")(x) + x = Conv2D(192, 3, strides=1, padding="valid", use_bias=False, name="Conv2d_4a_3x3")(x) + x = BatchNormalization( + axis=3, momentum=0.995, epsilon=0.001, scale=False, name="Conv2d_4a_3x3_BatchNorm" + )(x) + x = Activation("relu", name="Conv2d_4a_3x3_Activation")(x) + x = Conv2D(256, 3, strides=2, padding="valid", use_bias=False, name="Conv2d_4b_3x3")(x) + x = BatchNormalization( + axis=3, momentum=0.995, epsilon=0.001, scale=False, name="Conv2d_4b_3x3_BatchNorm" + )(x) + x = Activation("relu", name="Conv2d_4b_3x3_Activation")(x) + + # 5x Block35 (Inception-ResNet-A block): + branch_0 = Conv2D( + 32, 1, strides=1, padding="same", use_bias=False, name="Block35_1_Branch_0_Conv2d_1x1" + )(x) + branch_0 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block35_1_Branch_0_Conv2d_1x1_BatchNorm", + )(branch_0) + branch_0 = Activation("relu", name="Block35_1_Branch_0_Conv2d_1x1_Activation")(branch_0) + branch_1 = Conv2D( + 32, 1, strides=1, padding="same", use_bias=False, name="Block35_1_Branch_1_Conv2d_0a_1x1" + )(x) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block35_1_Branch_1_Conv2d_0a_1x1_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block35_1_Branch_1_Conv2d_0a_1x1_Activation")(branch_1) + branch_1 = Conv2D( + 32, 3, strides=1, padding="same", use_bias=False, name="Block35_1_Branch_1_Conv2d_0b_3x3" + )(branch_1) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block35_1_Branch_1_Conv2d_0b_3x3_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block35_1_Branch_1_Conv2d_0b_3x3_Activation")(branch_1) + branch_2 = Conv2D( + 32, 1, strides=1, padding="same", use_bias=False, name="Block35_1_Branch_2_Conv2d_0a_1x1" + )(x) + branch_2 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block35_1_Branch_2_Conv2d_0a_1x1_BatchNorm", + )(branch_2) + branch_2 = Activation("relu", name="Block35_1_Branch_2_Conv2d_0a_1x1_Activation")(branch_2) + branch_2 = Conv2D( + 32, 3, strides=1, padding="same", use_bias=False, name="Block35_1_Branch_2_Conv2d_0b_3x3" + )(branch_2) + branch_2 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block35_1_Branch_2_Conv2d_0b_3x3_BatchNorm", + )(branch_2) + branch_2 = Activation("relu", name="Block35_1_Branch_2_Conv2d_0b_3x3_Activation")(branch_2) + branch_2 = Conv2D( + 32, 3, strides=1, padding="same", use_bias=False, name="Block35_1_Branch_2_Conv2d_0c_3x3" + )(branch_2) + branch_2 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block35_1_Branch_2_Conv2d_0c_3x3_BatchNorm", + )(branch_2) + branch_2 = Activation("relu", name="Block35_1_Branch_2_Conv2d_0c_3x3_Activation")(branch_2) + branches = [branch_0, branch_1, branch_2] + mixed = Concatenate(axis=3, name="Block35_1_Concatenate")(branches) + up = Conv2D(256, 1, strides=1, padding="same", use_bias=True, name="Block35_1_Conv2d_1x1")( + mixed + ) + up = Lambda(scaling, output_shape=K.int_shape(up)[1:], arguments={"scale": 0.17})(up) + x = add([x, up]) + x = Activation("relu", name="Block35_1_Activation")(x) + + branch_0 = Conv2D( + 32, 1, strides=1, padding="same", use_bias=False, name="Block35_2_Branch_0_Conv2d_1x1" + )(x) + branch_0 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block35_2_Branch_0_Conv2d_1x1_BatchNorm", + )(branch_0) + branch_0 = Activation("relu", name="Block35_2_Branch_0_Conv2d_1x1_Activation")(branch_0) + branch_1 = Conv2D( + 32, 1, strides=1, padding="same", use_bias=False, name="Block35_2_Branch_1_Conv2d_0a_1x1" + )(x) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block35_2_Branch_1_Conv2d_0a_1x1_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block35_2_Branch_1_Conv2d_0a_1x1_Activation")(branch_1) + branch_1 = Conv2D( + 32, 3, strides=1, padding="same", use_bias=False, name="Block35_2_Branch_1_Conv2d_0b_3x3" + )(branch_1) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block35_2_Branch_1_Conv2d_0b_3x3_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block35_2_Branch_1_Conv2d_0b_3x3_Activation")(branch_1) + branch_2 = Conv2D( + 32, 1, strides=1, padding="same", use_bias=False, name="Block35_2_Branch_2_Conv2d_0a_1x1" + )(x) + branch_2 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block35_2_Branch_2_Conv2d_0a_1x1_BatchNorm", + )(branch_2) + branch_2 = Activation("relu", name="Block35_2_Branch_2_Conv2d_0a_1x1_Activation")(branch_2) + branch_2 = Conv2D( + 32, 3, strides=1, padding="same", use_bias=False, name="Block35_2_Branch_2_Conv2d_0b_3x3" + )(branch_2) + branch_2 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block35_2_Branch_2_Conv2d_0b_3x3_BatchNorm", + )(branch_2) + branch_2 = Activation("relu", name="Block35_2_Branch_2_Conv2d_0b_3x3_Activation")(branch_2) + branch_2 = Conv2D( + 32, 3, strides=1, padding="same", use_bias=False, name="Block35_2_Branch_2_Conv2d_0c_3x3" + )(branch_2) + branch_2 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block35_2_Branch_2_Conv2d_0c_3x3_BatchNorm", + )(branch_2) + branch_2 = Activation("relu", name="Block35_2_Branch_2_Conv2d_0c_3x3_Activation")(branch_2) + branches = [branch_0, branch_1, branch_2] + mixed = Concatenate(axis=3, name="Block35_2_Concatenate")(branches) + up = Conv2D(256, 1, strides=1, padding="same", use_bias=True, name="Block35_2_Conv2d_1x1")( + mixed + ) + up = Lambda(scaling, output_shape=K.int_shape(up)[1:], arguments={"scale": 0.17})(up) + x = add([x, up]) + x = Activation("relu", name="Block35_2_Activation")(x) + + branch_0 = Conv2D( + 32, 1, strides=1, padding="same", use_bias=False, name="Block35_3_Branch_0_Conv2d_1x1" + )(x) + branch_0 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block35_3_Branch_0_Conv2d_1x1_BatchNorm", + )(branch_0) + branch_0 = Activation("relu", name="Block35_3_Branch_0_Conv2d_1x1_Activation")(branch_0) + branch_1 = Conv2D( + 32, 1, strides=1, padding="same", use_bias=False, name="Block35_3_Branch_1_Conv2d_0a_1x1" + )(x) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block35_3_Branch_1_Conv2d_0a_1x1_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block35_3_Branch_1_Conv2d_0a_1x1_Activation")(branch_1) + branch_1 = Conv2D( + 32, 3, strides=1, padding="same", use_bias=False, name="Block35_3_Branch_1_Conv2d_0b_3x3" + )(branch_1) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block35_3_Branch_1_Conv2d_0b_3x3_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block35_3_Branch_1_Conv2d_0b_3x3_Activation")(branch_1) + branch_2 = Conv2D( + 32, 1, strides=1, padding="same", use_bias=False, name="Block35_3_Branch_2_Conv2d_0a_1x1" + )(x) + branch_2 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block35_3_Branch_2_Conv2d_0a_1x1_BatchNorm", + )(branch_2) + branch_2 = Activation("relu", name="Block35_3_Branch_2_Conv2d_0a_1x1_Activation")(branch_2) + branch_2 = Conv2D( + 32, 3, strides=1, padding="same", use_bias=False, name="Block35_3_Branch_2_Conv2d_0b_3x3" + )(branch_2) + branch_2 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block35_3_Branch_2_Conv2d_0b_3x3_BatchNorm", + )(branch_2) + branch_2 = Activation("relu", name="Block35_3_Branch_2_Conv2d_0b_3x3_Activation")(branch_2) + branch_2 = Conv2D( + 32, 3, strides=1, padding="same", use_bias=False, name="Block35_3_Branch_2_Conv2d_0c_3x3" + )(branch_2) + branch_2 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block35_3_Branch_2_Conv2d_0c_3x3_BatchNorm", + )(branch_2) + branch_2 = Activation("relu", name="Block35_3_Branch_2_Conv2d_0c_3x3_Activation")(branch_2) + branches = [branch_0, branch_1, branch_2] + mixed = Concatenate(axis=3, name="Block35_3_Concatenate")(branches) + up = Conv2D(256, 1, strides=1, padding="same", use_bias=True, name="Block35_3_Conv2d_1x1")( + mixed + ) + up = Lambda(scaling, output_shape=K.int_shape(up)[1:], arguments={"scale": 0.17})(up) + x = add([x, up]) + x = Activation("relu", name="Block35_3_Activation")(x) + + branch_0 = Conv2D( + 32, 1, strides=1, padding="same", use_bias=False, name="Block35_4_Branch_0_Conv2d_1x1" + )(x) + branch_0 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block35_4_Branch_0_Conv2d_1x1_BatchNorm", + )(branch_0) + branch_0 = Activation("relu", name="Block35_4_Branch_0_Conv2d_1x1_Activation")(branch_0) + branch_1 = Conv2D( + 32, 1, strides=1, padding="same", use_bias=False, name="Block35_4_Branch_1_Conv2d_0a_1x1" + )(x) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block35_4_Branch_1_Conv2d_0a_1x1_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block35_4_Branch_1_Conv2d_0a_1x1_Activation")(branch_1) + branch_1 = Conv2D( + 32, 3, strides=1, padding="same", use_bias=False, name="Block35_4_Branch_1_Conv2d_0b_3x3" + )(branch_1) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block35_4_Branch_1_Conv2d_0b_3x3_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block35_4_Branch_1_Conv2d_0b_3x3_Activation")(branch_1) + branch_2 = Conv2D( + 32, 1, strides=1, padding="same", use_bias=False, name="Block35_4_Branch_2_Conv2d_0a_1x1" + )(x) + branch_2 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block35_4_Branch_2_Conv2d_0a_1x1_BatchNorm", + )(branch_2) + branch_2 = Activation("relu", name="Block35_4_Branch_2_Conv2d_0a_1x1_Activation")(branch_2) + branch_2 = Conv2D( + 32, 3, strides=1, padding="same", use_bias=False, name="Block35_4_Branch_2_Conv2d_0b_3x3" + )(branch_2) + branch_2 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block35_4_Branch_2_Conv2d_0b_3x3_BatchNorm", + )(branch_2) + branch_2 = Activation("relu", name="Block35_4_Branch_2_Conv2d_0b_3x3_Activation")(branch_2) + branch_2 = Conv2D( + 32, 3, strides=1, padding="same", use_bias=False, name="Block35_4_Branch_2_Conv2d_0c_3x3" + )(branch_2) + branch_2 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block35_4_Branch_2_Conv2d_0c_3x3_BatchNorm", + )(branch_2) + branch_2 = Activation("relu", name="Block35_4_Branch_2_Conv2d_0c_3x3_Activation")(branch_2) + branches = [branch_0, branch_1, branch_2] + mixed = Concatenate(axis=3, name="Block35_4_Concatenate")(branches) + up = Conv2D(256, 1, strides=1, padding="same", use_bias=True, name="Block35_4_Conv2d_1x1")( + mixed + ) + up = Lambda(scaling, output_shape=K.int_shape(up)[1:], arguments={"scale": 0.17})(up) + x = add([x, up]) + x = Activation("relu", name="Block35_4_Activation")(x) + + branch_0 = Conv2D( + 32, 1, strides=1, padding="same", use_bias=False, name="Block35_5_Branch_0_Conv2d_1x1" + )(x) + branch_0 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block35_5_Branch_0_Conv2d_1x1_BatchNorm", + )(branch_0) + branch_0 = Activation("relu", name="Block35_5_Branch_0_Conv2d_1x1_Activation")(branch_0) + branch_1 = Conv2D( + 32, 1, strides=1, padding="same", use_bias=False, name="Block35_5_Branch_1_Conv2d_0a_1x1" + )(x) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block35_5_Branch_1_Conv2d_0a_1x1_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block35_5_Branch_1_Conv2d_0a_1x1_Activation")(branch_1) + branch_1 = Conv2D( + 32, 3, strides=1, padding="same", use_bias=False, name="Block35_5_Branch_1_Conv2d_0b_3x3" + )(branch_1) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block35_5_Branch_1_Conv2d_0b_3x3_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block35_5_Branch_1_Conv2d_0b_3x3_Activation")(branch_1) + branch_2 = Conv2D( + 32, 1, strides=1, padding="same", use_bias=False, name="Block35_5_Branch_2_Conv2d_0a_1x1" + )(x) + branch_2 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block35_5_Branch_2_Conv2d_0a_1x1_BatchNorm", + )(branch_2) + branch_2 = Activation("relu", name="Block35_5_Branch_2_Conv2d_0a_1x1_Activation")(branch_2) + branch_2 = Conv2D( + 32, 3, strides=1, padding="same", use_bias=False, name="Block35_5_Branch_2_Conv2d_0b_3x3" + )(branch_2) + branch_2 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block35_5_Branch_2_Conv2d_0b_3x3_BatchNorm", + )(branch_2) + branch_2 = Activation("relu", name="Block35_5_Branch_2_Conv2d_0b_3x3_Activation")(branch_2) + branch_2 = Conv2D( + 32, 3, strides=1, padding="same", use_bias=False, name="Block35_5_Branch_2_Conv2d_0c_3x3" + )(branch_2) + branch_2 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block35_5_Branch_2_Conv2d_0c_3x3_BatchNorm", + )(branch_2) + branch_2 = Activation("relu", name="Block35_5_Branch_2_Conv2d_0c_3x3_Activation")(branch_2) + branches = [branch_0, branch_1, branch_2] + mixed = Concatenate(axis=3, name="Block35_5_Concatenate")(branches) + up = Conv2D(256, 1, strides=1, padding="same", use_bias=True, name="Block35_5_Conv2d_1x1")( + mixed + ) + up = Lambda(scaling, output_shape=K.int_shape(up)[1:], arguments={"scale": 0.17})(up) + x = add([x, up]) + x = Activation("relu", name="Block35_5_Activation")(x) + + # Mixed 6a (Reduction-A block): + branch_0 = Conv2D( + 384, 3, strides=2, padding="valid", use_bias=False, name="Mixed_6a_Branch_0_Conv2d_1a_3x3" + )(x) + branch_0 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Mixed_6a_Branch_0_Conv2d_1a_3x3_BatchNorm", + )(branch_0) + branch_0 = Activation("relu", name="Mixed_6a_Branch_0_Conv2d_1a_3x3_Activation")(branch_0) + branch_1 = Conv2D( + 192, 1, strides=1, padding="same", use_bias=False, name="Mixed_6a_Branch_1_Conv2d_0a_1x1" + )(x) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Mixed_6a_Branch_1_Conv2d_0a_1x1_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Mixed_6a_Branch_1_Conv2d_0a_1x1_Activation")(branch_1) + branch_1 = Conv2D( + 192, 3, strides=1, padding="same", use_bias=False, name="Mixed_6a_Branch_1_Conv2d_0b_3x3" + )(branch_1) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Mixed_6a_Branch_1_Conv2d_0b_3x3_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Mixed_6a_Branch_1_Conv2d_0b_3x3_Activation")(branch_1) + branch_1 = Conv2D( + 256, 3, strides=2, padding="valid", use_bias=False, name="Mixed_6a_Branch_1_Conv2d_1a_3x3" + )(branch_1) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Mixed_6a_Branch_1_Conv2d_1a_3x3_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Mixed_6a_Branch_1_Conv2d_1a_3x3_Activation")(branch_1) + branch_pool = MaxPooling2D( + 3, strides=2, padding="valid", name="Mixed_6a_Branch_2_MaxPool_1a_3x3" + )(x) + branches = [branch_0, branch_1, branch_pool] + x = Concatenate(axis=3, name="Mixed_6a")(branches) + + # 10x Block17 (Inception-ResNet-B block): + branch_0 = Conv2D( + 128, 1, strides=1, padding="same", use_bias=False, name="Block17_1_Branch_0_Conv2d_1x1" + )(x) + branch_0 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block17_1_Branch_0_Conv2d_1x1_BatchNorm", + )(branch_0) + branch_0 = Activation("relu", name="Block17_1_Branch_0_Conv2d_1x1_Activation")(branch_0) + branch_1 = Conv2D( + 128, 1, strides=1, padding="same", use_bias=False, name="Block17_1_Branch_1_Conv2d_0a_1x1" + )(x) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block17_1_Branch_1_Conv2d_0a_1x1_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block17_1_Branch_1_Conv2d_0a_1x1_Activation")(branch_1) + branch_1 = Conv2D( + 128, + [1, 7], + strides=1, + padding="same", + use_bias=False, + name="Block17_1_Branch_1_Conv2d_0b_1x7", + )(branch_1) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block17_1_Branch_1_Conv2d_0b_1x7_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block17_1_Branch_1_Conv2d_0b_1x7_Activation")(branch_1) + branch_1 = Conv2D( + 128, + [7, 1], + strides=1, + padding="same", + use_bias=False, + name="Block17_1_Branch_1_Conv2d_0c_7x1", + )(branch_1) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block17_1_Branch_1_Conv2d_0c_7x1_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block17_1_Branch_1_Conv2d_0c_7x1_Activation")(branch_1) + branches = [branch_0, branch_1] + mixed = Concatenate(axis=3, name="Block17_1_Concatenate")(branches) + up = Conv2D(896, 1, strides=1, padding="same", use_bias=True, name="Block17_1_Conv2d_1x1")( + mixed + ) + up = Lambda(scaling, output_shape=K.int_shape(up)[1:], arguments={"scale": 0.1})(up) + x = add([x, up]) + x = Activation("relu", name="Block17_1_Activation")(x) + + branch_0 = Conv2D( + 128, 1, strides=1, padding="same", use_bias=False, name="Block17_2_Branch_0_Conv2d_1x1" + )(x) + branch_0 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block17_2_Branch_0_Conv2d_1x1_BatchNorm", + )(branch_0) + branch_0 = Activation("relu", name="Block17_2_Branch_0_Conv2d_1x1_Activation")(branch_0) + branch_1 = Conv2D( + 128, 1, strides=1, padding="same", use_bias=False, name="Block17_2_Branch_2_Conv2d_0a_1x1" + )(x) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block17_2_Branch_2_Conv2d_0a_1x1_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block17_2_Branch_2_Conv2d_0a_1x1_Activation")(branch_1) + branch_1 = Conv2D( + 128, + [1, 7], + strides=1, + padding="same", + use_bias=False, + name="Block17_2_Branch_2_Conv2d_0b_1x7", + )(branch_1) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block17_2_Branch_2_Conv2d_0b_1x7_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block17_2_Branch_2_Conv2d_0b_1x7_Activation")(branch_1) + branch_1 = Conv2D( + 128, + [7, 1], + strides=1, + padding="same", + use_bias=False, + name="Block17_2_Branch_2_Conv2d_0c_7x1", + )(branch_1) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block17_2_Branch_2_Conv2d_0c_7x1_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block17_2_Branch_2_Conv2d_0c_7x1_Activation")(branch_1) + branches = [branch_0, branch_1] + mixed = Concatenate(axis=3, name="Block17_2_Concatenate")(branches) + up = Conv2D(896, 1, strides=1, padding="same", use_bias=True, name="Block17_2_Conv2d_1x1")( + mixed + ) + up = Lambda(scaling, output_shape=K.int_shape(up)[1:], arguments={"scale": 0.1})(up) + x = add([x, up]) + x = Activation("relu", name="Block17_2_Activation")(x) + + branch_0 = Conv2D( + 128, 1, strides=1, padding="same", use_bias=False, name="Block17_3_Branch_0_Conv2d_1x1" + )(x) + branch_0 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block17_3_Branch_0_Conv2d_1x1_BatchNorm", + )(branch_0) + branch_0 = Activation("relu", name="Block17_3_Branch_0_Conv2d_1x1_Activation")(branch_0) + branch_1 = Conv2D( + 128, 1, strides=1, padding="same", use_bias=False, name="Block17_3_Branch_3_Conv2d_0a_1x1" + )(x) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block17_3_Branch_3_Conv2d_0a_1x1_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block17_3_Branch_3_Conv2d_0a_1x1_Activation")(branch_1) + branch_1 = Conv2D( + 128, + [1, 7], + strides=1, + padding="same", + use_bias=False, + name="Block17_3_Branch_3_Conv2d_0b_1x7", + )(branch_1) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block17_3_Branch_3_Conv2d_0b_1x7_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block17_3_Branch_3_Conv2d_0b_1x7_Activation")(branch_1) + branch_1 = Conv2D( + 128, + [7, 1], + strides=1, + padding="same", + use_bias=False, + name="Block17_3_Branch_3_Conv2d_0c_7x1", + )(branch_1) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block17_3_Branch_3_Conv2d_0c_7x1_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block17_3_Branch_3_Conv2d_0c_7x1_Activation")(branch_1) + branches = [branch_0, branch_1] + mixed = Concatenate(axis=3, name="Block17_3_Concatenate")(branches) + up = Conv2D(896, 1, strides=1, padding="same", use_bias=True, name="Block17_3_Conv2d_1x1")( + mixed + ) + up = Lambda(scaling, output_shape=K.int_shape(up)[1:], arguments={"scale": 0.1})(up) + x = add([x, up]) + x = Activation("relu", name="Block17_3_Activation")(x) + + branch_0 = Conv2D( + 128, 1, strides=1, padding="same", use_bias=False, name="Block17_4_Branch_0_Conv2d_1x1" + )(x) + branch_0 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block17_4_Branch_0_Conv2d_1x1_BatchNorm", + )(branch_0) + branch_0 = Activation("relu", name="Block17_4_Branch_0_Conv2d_1x1_Activation")(branch_0) + branch_1 = Conv2D( + 128, 1, strides=1, padding="same", use_bias=False, name="Block17_4_Branch_4_Conv2d_0a_1x1" + )(x) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block17_4_Branch_4_Conv2d_0a_1x1_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block17_4_Branch_4_Conv2d_0a_1x1_Activation")(branch_1) + branch_1 = Conv2D( + 128, + [1, 7], + strides=1, + padding="same", + use_bias=False, + name="Block17_4_Branch_4_Conv2d_0b_1x7", + )(branch_1) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block17_4_Branch_4_Conv2d_0b_1x7_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block17_4_Branch_4_Conv2d_0b_1x7_Activation")(branch_1) + branch_1 = Conv2D( + 128, + [7, 1], + strides=1, + padding="same", + use_bias=False, + name="Block17_4_Branch_4_Conv2d_0c_7x1", + )(branch_1) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block17_4_Branch_4_Conv2d_0c_7x1_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block17_4_Branch_4_Conv2d_0c_7x1_Activation")(branch_1) + branches = [branch_0, branch_1] + mixed = Concatenate(axis=3, name="Block17_4_Concatenate")(branches) + up = Conv2D(896, 1, strides=1, padding="same", use_bias=True, name="Block17_4_Conv2d_1x1")( + mixed + ) + up = Lambda(scaling, output_shape=K.int_shape(up)[1:], arguments={"scale": 0.1})(up) + x = add([x, up]) + x = Activation("relu", name="Block17_4_Activation")(x) + + branch_0 = Conv2D( + 128, 1, strides=1, padding="same", use_bias=False, name="Block17_5_Branch_0_Conv2d_1x1" + )(x) + branch_0 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block17_5_Branch_0_Conv2d_1x1_BatchNorm", + )(branch_0) + branch_0 = Activation("relu", name="Block17_5_Branch_0_Conv2d_1x1_Activation")(branch_0) + branch_1 = Conv2D( + 128, 1, strides=1, padding="same", use_bias=False, name="Block17_5_Branch_5_Conv2d_0a_1x1" + )(x) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block17_5_Branch_5_Conv2d_0a_1x1_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block17_5_Branch_5_Conv2d_0a_1x1_Activation")(branch_1) + branch_1 = Conv2D( + 128, + [1, 7], + strides=1, + padding="same", + use_bias=False, + name="Block17_5_Branch_5_Conv2d_0b_1x7", + )(branch_1) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block17_5_Branch_5_Conv2d_0b_1x7_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block17_5_Branch_5_Conv2d_0b_1x7_Activation")(branch_1) + branch_1 = Conv2D( + 128, + [7, 1], + strides=1, + padding="same", + use_bias=False, + name="Block17_5_Branch_5_Conv2d_0c_7x1", + )(branch_1) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block17_5_Branch_5_Conv2d_0c_7x1_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block17_5_Branch_5_Conv2d_0c_7x1_Activation")(branch_1) + branches = [branch_0, branch_1] + mixed = Concatenate(axis=3, name="Block17_5_Concatenate")(branches) + up = Conv2D(896, 1, strides=1, padding="same", use_bias=True, name="Block17_5_Conv2d_1x1")( + mixed + ) + up = Lambda(scaling, output_shape=K.int_shape(up)[1:], arguments={"scale": 0.1})(up) + x = add([x, up]) + x = Activation("relu", name="Block17_5_Activation")(x) + + branch_0 = Conv2D( + 128, 1, strides=1, padding="same", use_bias=False, name="Block17_6_Branch_0_Conv2d_1x1" + )(x) + branch_0 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block17_6_Branch_0_Conv2d_1x1_BatchNorm", + )(branch_0) + branch_0 = Activation("relu", name="Block17_6_Branch_0_Conv2d_1x1_Activation")(branch_0) + branch_1 = Conv2D( + 128, 1, strides=1, padding="same", use_bias=False, name="Block17_6_Branch_6_Conv2d_0a_1x1" + )(x) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block17_6_Branch_6_Conv2d_0a_1x1_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block17_6_Branch_6_Conv2d_0a_1x1_Activation")(branch_1) + branch_1 = Conv2D( + 128, + [1, 7], + strides=1, + padding="same", + use_bias=False, + name="Block17_6_Branch_6_Conv2d_0b_1x7", + )(branch_1) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block17_6_Branch_6_Conv2d_0b_1x7_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block17_6_Branch_6_Conv2d_0b_1x7_Activation")(branch_1) + branch_1 = Conv2D( + 128, + [7, 1], + strides=1, + padding="same", + use_bias=False, + name="Block17_6_Branch_6_Conv2d_0c_7x1", + )(branch_1) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block17_6_Branch_6_Conv2d_0c_7x1_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block17_6_Branch_6_Conv2d_0c_7x1_Activation")(branch_1) + branches = [branch_0, branch_1] + mixed = Concatenate(axis=3, name="Block17_6_Concatenate")(branches) + up = Conv2D(896, 1, strides=1, padding="same", use_bias=True, name="Block17_6_Conv2d_1x1")( + mixed + ) + up = Lambda(scaling, output_shape=K.int_shape(up)[1:], arguments={"scale": 0.1})(up) + x = add([x, up]) + x = Activation("relu", name="Block17_6_Activation")(x) + + branch_0 = Conv2D( + 128, 1, strides=1, padding="same", use_bias=False, name="Block17_7_Branch_0_Conv2d_1x1" + )(x) + branch_0 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block17_7_Branch_0_Conv2d_1x1_BatchNorm", + )(branch_0) + branch_0 = Activation("relu", name="Block17_7_Branch_0_Conv2d_1x1_Activation")(branch_0) + branch_1 = Conv2D( + 128, 1, strides=1, padding="same", use_bias=False, name="Block17_7_Branch_7_Conv2d_0a_1x1" + )(x) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block17_7_Branch_7_Conv2d_0a_1x1_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block17_7_Branch_7_Conv2d_0a_1x1_Activation")(branch_1) + branch_1 = Conv2D( + 128, + [1, 7], + strides=1, + padding="same", + use_bias=False, + name="Block17_7_Branch_7_Conv2d_0b_1x7", + )(branch_1) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block17_7_Branch_7_Conv2d_0b_1x7_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block17_7_Branch_7_Conv2d_0b_1x7_Activation")(branch_1) + branch_1 = Conv2D( + 128, + [7, 1], + strides=1, + padding="same", + use_bias=False, + name="Block17_7_Branch_7_Conv2d_0c_7x1", + )(branch_1) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block17_7_Branch_7_Conv2d_0c_7x1_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block17_7_Branch_7_Conv2d_0c_7x1_Activation")(branch_1) + branches = [branch_0, branch_1] + mixed = Concatenate(axis=3, name="Block17_7_Concatenate")(branches) + up = Conv2D(896, 1, strides=1, padding="same", use_bias=True, name="Block17_7_Conv2d_1x1")( + mixed + ) + up = Lambda(scaling, output_shape=K.int_shape(up)[1:], arguments={"scale": 0.1})(up) + x = add([x, up]) + x = Activation("relu", name="Block17_7_Activation")(x) + + branch_0 = Conv2D( + 128, 1, strides=1, padding="same", use_bias=False, name="Block17_8_Branch_0_Conv2d_1x1" + )(x) + branch_0 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block17_8_Branch_0_Conv2d_1x1_BatchNorm", + )(branch_0) + branch_0 = Activation("relu", name="Block17_8_Branch_0_Conv2d_1x1_Activation")(branch_0) + branch_1 = Conv2D( + 128, 1, strides=1, padding="same", use_bias=False, name="Block17_8_Branch_8_Conv2d_0a_1x1" + )(x) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block17_8_Branch_8_Conv2d_0a_1x1_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block17_8_Branch_8_Conv2d_0a_1x1_Activation")(branch_1) + branch_1 = Conv2D( + 128, + [1, 7], + strides=1, + padding="same", + use_bias=False, + name="Block17_8_Branch_8_Conv2d_0b_1x7", + )(branch_1) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block17_8_Branch_8_Conv2d_0b_1x7_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block17_8_Branch_8_Conv2d_0b_1x7_Activation")(branch_1) + branch_1 = Conv2D( + 128, + [7, 1], + strides=1, + padding="same", + use_bias=False, + name="Block17_8_Branch_8_Conv2d_0c_7x1", + )(branch_1) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block17_8_Branch_8_Conv2d_0c_7x1_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block17_8_Branch_8_Conv2d_0c_7x1_Activation")(branch_1) + branches = [branch_0, branch_1] + mixed = Concatenate(axis=3, name="Block17_8_Concatenate")(branches) + up = Conv2D(896, 1, strides=1, padding="same", use_bias=True, name="Block17_8_Conv2d_1x1")( + mixed + ) + up = Lambda(scaling, output_shape=K.int_shape(up)[1:], arguments={"scale": 0.1})(up) + x = add([x, up]) + x = Activation("relu", name="Block17_8_Activation")(x) + + branch_0 = Conv2D( + 128, 1, strides=1, padding="same", use_bias=False, name="Block17_9_Branch_0_Conv2d_1x1" + )(x) + branch_0 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block17_9_Branch_0_Conv2d_1x1_BatchNorm", + )(branch_0) + branch_0 = Activation("relu", name="Block17_9_Branch_0_Conv2d_1x1_Activation")(branch_0) + branch_1 = Conv2D( + 128, 1, strides=1, padding="same", use_bias=False, name="Block17_9_Branch_9_Conv2d_0a_1x1" + )(x) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block17_9_Branch_9_Conv2d_0a_1x1_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block17_9_Branch_9_Conv2d_0a_1x1_Activation")(branch_1) + branch_1 = Conv2D( + 128, + [1, 7], + strides=1, + padding="same", + use_bias=False, + name="Block17_9_Branch_9_Conv2d_0b_1x7", + )(branch_1) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block17_9_Branch_9_Conv2d_0b_1x7_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block17_9_Branch_9_Conv2d_0b_1x7_Activation")(branch_1) + branch_1 = Conv2D( + 128, + [7, 1], + strides=1, + padding="same", + use_bias=False, + name="Block17_9_Branch_9_Conv2d_0c_7x1", + )(branch_1) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block17_9_Branch_9_Conv2d_0c_7x1_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block17_9_Branch_9_Conv2d_0c_7x1_Activation")(branch_1) + branches = [branch_0, branch_1] + mixed = Concatenate(axis=3, name="Block17_9_Concatenate")(branches) + up = Conv2D(896, 1, strides=1, padding="same", use_bias=True, name="Block17_9_Conv2d_1x1")( + mixed + ) + up = Lambda(scaling, output_shape=K.int_shape(up)[1:], arguments={"scale": 0.1})(up) + x = add([x, up]) + x = Activation("relu", name="Block17_9_Activation")(x) + + branch_0 = Conv2D( + 128, 1, strides=1, padding="same", use_bias=False, name="Block17_10_Branch_0_Conv2d_1x1" + )(x) + branch_0 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block17_10_Branch_0_Conv2d_1x1_BatchNorm", + )(branch_0) + branch_0 = Activation("relu", name="Block17_10_Branch_0_Conv2d_1x1_Activation")(branch_0) + branch_1 = Conv2D( + 128, 1, strides=1, padding="same", use_bias=False, name="Block17_10_Branch_10_Conv2d_0a_1x1" + )(x) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block17_10_Branch_10_Conv2d_0a_1x1_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block17_10_Branch_10_Conv2d_0a_1x1_Activation")(branch_1) + branch_1 = Conv2D( + 128, + [1, 7], + strides=1, + padding="same", + use_bias=False, + name="Block17_10_Branch_10_Conv2d_0b_1x7", + )(branch_1) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block17_10_Branch_10_Conv2d_0b_1x7_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block17_10_Branch_10_Conv2d_0b_1x7_Activation")(branch_1) + branch_1 = Conv2D( + 128, + [7, 1], + strides=1, + padding="same", + use_bias=False, + name="Block17_10_Branch_10_Conv2d_0c_7x1", + )(branch_1) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block17_10_Branch_10_Conv2d_0c_7x1_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block17_10_Branch_10_Conv2d_0c_7x1_Activation")(branch_1) + branches = [branch_0, branch_1] + mixed = Concatenate(axis=3, name="Block17_10_Concatenate")(branches) + up = Conv2D(896, 1, strides=1, padding="same", use_bias=True, name="Block17_10_Conv2d_1x1")( + mixed + ) + up = Lambda(scaling, output_shape=K.int_shape(up)[1:], arguments={"scale": 0.1})(up) + x = add([x, up]) + x = Activation("relu", name="Block17_10_Activation")(x) + + # Mixed 7a (Reduction-B block): 8 x 8 x 2080 + branch_0 = Conv2D( + 256, 1, strides=1, padding="same", use_bias=False, name="Mixed_7a_Branch_0_Conv2d_0a_1x1" + )(x) + branch_0 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Mixed_7a_Branch_0_Conv2d_0a_1x1_BatchNorm", + )(branch_0) + branch_0 = Activation("relu", name="Mixed_7a_Branch_0_Conv2d_0a_1x1_Activation")(branch_0) + branch_0 = Conv2D( + 384, 3, strides=2, padding="valid", use_bias=False, name="Mixed_7a_Branch_0_Conv2d_1a_3x3" + )(branch_0) + branch_0 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Mixed_7a_Branch_0_Conv2d_1a_3x3_BatchNorm", + )(branch_0) + branch_0 = Activation("relu", name="Mixed_7a_Branch_0_Conv2d_1a_3x3_Activation")(branch_0) + branch_1 = Conv2D( + 256, 1, strides=1, padding="same", use_bias=False, name="Mixed_7a_Branch_1_Conv2d_0a_1x1" + )(x) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Mixed_7a_Branch_1_Conv2d_0a_1x1_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Mixed_7a_Branch_1_Conv2d_0a_1x1_Activation")(branch_1) + branch_1 = Conv2D( + 256, 3, strides=2, padding="valid", use_bias=False, name="Mixed_7a_Branch_1_Conv2d_1a_3x3" + )(branch_1) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Mixed_7a_Branch_1_Conv2d_1a_3x3_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Mixed_7a_Branch_1_Conv2d_1a_3x3_Activation")(branch_1) + branch_2 = Conv2D( + 256, 1, strides=1, padding="same", use_bias=False, name="Mixed_7a_Branch_2_Conv2d_0a_1x1" + )(x) + branch_2 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Mixed_7a_Branch_2_Conv2d_0a_1x1_BatchNorm", + )(branch_2) + branch_2 = Activation("relu", name="Mixed_7a_Branch_2_Conv2d_0a_1x1_Activation")(branch_2) + branch_2 = Conv2D( + 256, 3, strides=1, padding="same", use_bias=False, name="Mixed_7a_Branch_2_Conv2d_0b_3x3" + )(branch_2) + branch_2 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Mixed_7a_Branch_2_Conv2d_0b_3x3_BatchNorm", + )(branch_2) + branch_2 = Activation("relu", name="Mixed_7a_Branch_2_Conv2d_0b_3x3_Activation")(branch_2) + branch_2 = Conv2D( + 256, 3, strides=2, padding="valid", use_bias=False, name="Mixed_7a_Branch_2_Conv2d_1a_3x3" + )(branch_2) + branch_2 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Mixed_7a_Branch_2_Conv2d_1a_3x3_BatchNorm", + )(branch_2) + branch_2 = Activation("relu", name="Mixed_7a_Branch_2_Conv2d_1a_3x3_Activation")(branch_2) + branch_pool = MaxPooling2D( + 3, strides=2, padding="valid", name="Mixed_7a_Branch_3_MaxPool_1a_3x3" + )(x) + branches = [branch_0, branch_1, branch_2, branch_pool] + x = Concatenate(axis=3, name="Mixed_7a")(branches) + + # 5x Block8 (Inception-ResNet-C block): + + branch_0 = Conv2D( + 192, 1, strides=1, padding="same", use_bias=False, name="Block8_1_Branch_0_Conv2d_1x1" + )(x) + branch_0 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block8_1_Branch_0_Conv2d_1x1_BatchNorm", + )(branch_0) + branch_0 = Activation("relu", name="Block8_1_Branch_0_Conv2d_1x1_Activation")(branch_0) + branch_1 = Conv2D( + 192, 1, strides=1, padding="same", use_bias=False, name="Block8_1_Branch_1_Conv2d_0a_1x1" + )(x) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block8_1_Branch_1_Conv2d_0a_1x1_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block8_1_Branch_1_Conv2d_0a_1x1_Activation")(branch_1) + branch_1 = Conv2D( + 192, + [1, 3], + strides=1, + padding="same", + use_bias=False, + name="Block8_1_Branch_1_Conv2d_0b_1x3", + )(branch_1) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block8_1_Branch_1_Conv2d_0b_1x3_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block8_1_Branch_1_Conv2d_0b_1x3_Activation")(branch_1) + branch_1 = Conv2D( + 192, + [3, 1], + strides=1, + padding="same", + use_bias=False, + name="Block8_1_Branch_1_Conv2d_0c_3x1", + )(branch_1) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block8_1_Branch_1_Conv2d_0c_3x1_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block8_1_Branch_1_Conv2d_0c_3x1_Activation")(branch_1) + branches = [branch_0, branch_1] + mixed = Concatenate(axis=3, name="Block8_1_Concatenate")(branches) + up = Conv2D(1792, 1, strides=1, padding="same", use_bias=True, name="Block8_1_Conv2d_1x1")( + mixed + ) + up = Lambda(scaling, output_shape=K.int_shape(up)[1:], arguments={"scale": 0.2})(up) + x = add([x, up]) + x = Activation("relu", name="Block8_1_Activation")(x) + + branch_0 = Conv2D( + 192, 1, strides=1, padding="same", use_bias=False, name="Block8_2_Branch_0_Conv2d_1x1" + )(x) + branch_0 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block8_2_Branch_0_Conv2d_1x1_BatchNorm", + )(branch_0) + branch_0 = Activation("relu", name="Block8_2_Branch_0_Conv2d_1x1_Activation")(branch_0) + branch_1 = Conv2D( + 192, 1, strides=1, padding="same", use_bias=False, name="Block8_2_Branch_2_Conv2d_0a_1x1" + )(x) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block8_2_Branch_2_Conv2d_0a_1x1_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block8_2_Branch_2_Conv2d_0a_1x1_Activation")(branch_1) + branch_1 = Conv2D( + 192, + [1, 3], + strides=1, + padding="same", + use_bias=False, + name="Block8_2_Branch_2_Conv2d_0b_1x3", + )(branch_1) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block8_2_Branch_2_Conv2d_0b_1x3_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block8_2_Branch_2_Conv2d_0b_1x3_Activation")(branch_1) + branch_1 = Conv2D( + 192, + [3, 1], + strides=1, + padding="same", + use_bias=False, + name="Block8_2_Branch_2_Conv2d_0c_3x1", + )(branch_1) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block8_2_Branch_2_Conv2d_0c_3x1_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block8_2_Branch_2_Conv2d_0c_3x1_Activation")(branch_1) + branches = [branch_0, branch_1] + mixed = Concatenate(axis=3, name="Block8_2_Concatenate")(branches) + up = Conv2D(1792, 1, strides=1, padding="same", use_bias=True, name="Block8_2_Conv2d_1x1")( + mixed + ) + up = Lambda(scaling, output_shape=K.int_shape(up)[1:], arguments={"scale": 0.2})(up) + x = add([x, up]) + x = Activation("relu", name="Block8_2_Activation")(x) + + branch_0 = Conv2D( + 192, 1, strides=1, padding="same", use_bias=False, name="Block8_3_Branch_0_Conv2d_1x1" + )(x) + branch_0 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block8_3_Branch_0_Conv2d_1x1_BatchNorm", + )(branch_0) + branch_0 = Activation("relu", name="Block8_3_Branch_0_Conv2d_1x1_Activation")(branch_0) + branch_1 = Conv2D( + 192, 1, strides=1, padding="same", use_bias=False, name="Block8_3_Branch_3_Conv2d_0a_1x1" + )(x) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block8_3_Branch_3_Conv2d_0a_1x1_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block8_3_Branch_3_Conv2d_0a_1x1_Activation")(branch_1) + branch_1 = Conv2D( + 192, + [1, 3], + strides=1, + padding="same", + use_bias=False, + name="Block8_3_Branch_3_Conv2d_0b_1x3", + )(branch_1) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block8_3_Branch_3_Conv2d_0b_1x3_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block8_3_Branch_3_Conv2d_0b_1x3_Activation")(branch_1) + branch_1 = Conv2D( + 192, + [3, 1], + strides=1, + padding="same", + use_bias=False, + name="Block8_3_Branch_3_Conv2d_0c_3x1", + )(branch_1) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block8_3_Branch_3_Conv2d_0c_3x1_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block8_3_Branch_3_Conv2d_0c_3x1_Activation")(branch_1) + branches = [branch_0, branch_1] + mixed = Concatenate(axis=3, name="Block8_3_Concatenate")(branches) + up = Conv2D(1792, 1, strides=1, padding="same", use_bias=True, name="Block8_3_Conv2d_1x1")( + mixed + ) + up = Lambda(scaling, output_shape=K.int_shape(up)[1:], arguments={"scale": 0.2})(up) + x = add([x, up]) + x = Activation("relu", name="Block8_3_Activation")(x) + + branch_0 = Conv2D( + 192, 1, strides=1, padding="same", use_bias=False, name="Block8_4_Branch_0_Conv2d_1x1" + )(x) + branch_0 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block8_4_Branch_0_Conv2d_1x1_BatchNorm", + )(branch_0) + branch_0 = Activation("relu", name="Block8_4_Branch_0_Conv2d_1x1_Activation")(branch_0) + branch_1 = Conv2D( + 192, 1, strides=1, padding="same", use_bias=False, name="Block8_4_Branch_4_Conv2d_0a_1x1" + )(x) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block8_4_Branch_4_Conv2d_0a_1x1_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block8_4_Branch_4_Conv2d_0a_1x1_Activation")(branch_1) + branch_1 = Conv2D( + 192, + [1, 3], + strides=1, + padding="same", + use_bias=False, + name="Block8_4_Branch_4_Conv2d_0b_1x3", + )(branch_1) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block8_4_Branch_4_Conv2d_0b_1x3_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block8_4_Branch_4_Conv2d_0b_1x3_Activation")(branch_1) + branch_1 = Conv2D( + 192, + [3, 1], + strides=1, + padding="same", + use_bias=False, + name="Block8_4_Branch_4_Conv2d_0c_3x1", + )(branch_1) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block8_4_Branch_4_Conv2d_0c_3x1_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block8_4_Branch_4_Conv2d_0c_3x1_Activation")(branch_1) + branches = [branch_0, branch_1] + mixed = Concatenate(axis=3, name="Block8_4_Concatenate")(branches) + up = Conv2D(1792, 1, strides=1, padding="same", use_bias=True, name="Block8_4_Conv2d_1x1")( + mixed + ) + up = Lambda(scaling, output_shape=K.int_shape(up)[1:], arguments={"scale": 0.2})(up) + x = add([x, up]) + x = Activation("relu", name="Block8_4_Activation")(x) + + branch_0 = Conv2D( + 192, 1, strides=1, padding="same", use_bias=False, name="Block8_5_Branch_0_Conv2d_1x1" + )(x) + branch_0 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block8_5_Branch_0_Conv2d_1x1_BatchNorm", + )(branch_0) + branch_0 = Activation("relu", name="Block8_5_Branch_0_Conv2d_1x1_Activation")(branch_0) + branch_1 = Conv2D( + 192, 1, strides=1, padding="same", use_bias=False, name="Block8_5_Branch_5_Conv2d_0a_1x1" + )(x) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block8_5_Branch_5_Conv2d_0a_1x1_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block8_5_Branch_5_Conv2d_0a_1x1_Activation")(branch_1) + branch_1 = Conv2D( + 192, + [1, 3], + strides=1, + padding="same", + use_bias=False, + name="Block8_5_Branch_5_Conv2d_0b_1x3", + )(branch_1) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block8_5_Branch_5_Conv2d_0b_1x3_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block8_5_Branch_5_Conv2d_0b_1x3_Activation")(branch_1) + branch_1 = Conv2D( + 192, + [3, 1], + strides=1, + padding="same", + use_bias=False, + name="Block8_5_Branch_5_Conv2d_0c_3x1", + )(branch_1) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block8_5_Branch_5_Conv2d_0c_3x1_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block8_5_Branch_5_Conv2d_0c_3x1_Activation")(branch_1) + branches = [branch_0, branch_1] + mixed = Concatenate(axis=3, name="Block8_5_Concatenate")(branches) + up = Conv2D(1792, 1, strides=1, padding="same", use_bias=True, name="Block8_5_Conv2d_1x1")( + mixed + ) + up = Lambda(scaling, output_shape=K.int_shape(up)[1:], arguments={"scale": 0.2})(up) + x = add([x, up]) + x = Activation("relu", name="Block8_5_Activation")(x) + + branch_0 = Conv2D( + 192, 1, strides=1, padding="same", use_bias=False, name="Block8_6_Branch_0_Conv2d_1x1" + )(x) + branch_0 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block8_6_Branch_0_Conv2d_1x1_BatchNorm", + )(branch_0) + branch_0 = Activation("relu", name="Block8_6_Branch_0_Conv2d_1x1_Activation")(branch_0) + branch_1 = Conv2D( + 192, 1, strides=1, padding="same", use_bias=False, name="Block8_6_Branch_1_Conv2d_0a_1x1" + )(x) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block8_6_Branch_1_Conv2d_0a_1x1_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block8_6_Branch_1_Conv2d_0a_1x1_Activation")(branch_1) + branch_1 = Conv2D( + 192, + [1, 3], + strides=1, + padding="same", + use_bias=False, + name="Block8_6_Branch_1_Conv2d_0b_1x3", + )(branch_1) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block8_6_Branch_1_Conv2d_0b_1x3_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block8_6_Branch_1_Conv2d_0b_1x3_Activation")(branch_1) + branch_1 = Conv2D( + 192, + [3, 1], + strides=1, + padding="same", + use_bias=False, + name="Block8_6_Branch_1_Conv2d_0c_3x1", + )(branch_1) + branch_1 = BatchNormalization( + axis=3, + momentum=0.995, + epsilon=0.001, + scale=False, + name="Block8_6_Branch_1_Conv2d_0c_3x1_BatchNorm", + )(branch_1) + branch_1 = Activation("relu", name="Block8_6_Branch_1_Conv2d_0c_3x1_Activation")(branch_1) + branches = [branch_0, branch_1] + mixed = Concatenate(axis=3, name="Block8_6_Concatenate")(branches) + up = Conv2D(1792, 1, strides=1, padding="same", use_bias=True, name="Block8_6_Conv2d_1x1")( + mixed + ) + up = Lambda(scaling, output_shape=K.int_shape(up)[1:], arguments={"scale": 1})(up) + x = add([x, up]) + + # Classification block + x = GlobalAveragePooling2D(name="AvgPool")(x) + x = Dropout(1.0 - 0.8, name="Dropout")(x) + # Bottleneck + x = Dense(dimension, use_bias=False, name="Bottleneck")(x) + x = BatchNormalization(momentum=0.995, epsilon=0.001, scale=False, name="Bottleneck_BatchNorm")( + x + ) + + # Create model + model = Model(inputs, x, name="inception_resnet_v1") + + return model + + +def load_facenet128d_model( + url="https://github.com/serengil/deepface_models/releases/download/v1.0/facenet_weights.h5", +) -> Model: + """ + Construct FaceNet-128d model, download weights and then load weights + Args: + dimension (int): construct FaceNet-128d or FaceNet-512d models + Returns: + model (Model) + """ + model = InceptionResNetV1() + + # ----------------------------------- + + home = folder_utils.get_deepface_home() + + if os.path.isfile(home + "/.deepface/weights/facenet_weights.h5") != True: + logger.info("facenet_weights.h5 will be downloaded...") + + output = home + "/.deepface/weights/facenet_weights.h5" + gdown.download(url, output, quiet=False) + + # ----------------------------------- + + model.load_weights(home + "/.deepface/weights/facenet_weights.h5") + + # ----------------------------------- + + return model + + +def load_facenet512d_model( + url="https://github.com/serengil/deepface_models/releases/download/v1.0/facenet512_weights.h5", +) -> Model: + """ + Construct FaceNet-512d model, download its weights and load + Returns: + model (Model) + """ + + model = InceptionResNetV1(dimension=512) + + # ------------------------- + + home = folder_utils.get_deepface_home() + + if os.path.isfile(home + "/.deepface/weights/facenet512_weights.h5") != True: + logger.info("facenet512_weights.h5 will be downloaded...") + + output = home + "/.deepface/weights/facenet512_weights.h5" + gdown.download(url, output, quiet=False) + + # ------------------------- + + model.load_weights(home + "/.deepface/weights/facenet512_weights.h5") + + # ------------------------- + + return model diff --git a/basemodels/FbDeepFace.py b/basemodels/FbDeepFace.py new file mode 100644 index 0000000000000000000000000000000000000000..f51a5112809314a9166fee965c1d07703db5bfb7 --- /dev/null +++ b/basemodels/FbDeepFace.py @@ -0,0 +1,105 @@ +import os +import zipfile +import gdown +from deepface.commons import package_utils, folder_utils +from deepface.models.FacialRecognition import FacialRecognition +from deepface.commons import logger as log + +logger = log.get_singletonish_logger() + +# -------------------------------- +# dependency configuration + +tf_major = package_utils.get_tf_major_version() +tf_minor = package_utils.get_tf_minor_version() + +if tf_major == 1: + from keras.models import Model, Sequential + from keras.layers import ( + Convolution2D, + MaxPooling2D, + Flatten, + Dense, + Dropout, + ) +else: + from tensorflow.keras.models import Model, Sequential + from tensorflow.keras.layers import ( + Convolution2D, + MaxPooling2D, + Flatten, + Dense, + Dropout, + ) + + +# ------------------------------------- +# pylint: disable=line-too-long, too-few-public-methods +class DeepFaceClient(FacialRecognition): + """ + Fb's DeepFace model class + """ + + def __init__(self): + # DeepFace requires tf 2.12 or less + if tf_major == 2 and tf_minor > 12: + # Ref: https://github.com/serengil/deepface/pull/1079 + raise ValueError( + "DeepFace model requires LocallyConnected2D but it is no longer supported" + f" after tf 2.12 but you have {tf_major}.{tf_minor}. You need to downgrade your tf." + ) + + self.model = load_model() + self.model_name = "DeepFace" + self.input_shape = (152, 152) + self.output_shape = 4096 + + +def load_model( + url="https://github.com/swghosh/DeepFace/releases/download/weights-vggface2-2d-aligned/VGGFace2_DeepFace_weights_val-0.9034.h5.zip", +) -> Model: + """ + Construct DeepFace model, download its weights and load + """ + # we have some checks for this dependency in the init of client + # putting this in global causes library initialization + if tf_major == 1: + from keras.layers import LocallyConnected2D + else: + from tensorflow.keras.layers import LocallyConnected2D + + base_model = Sequential() + base_model.add( + Convolution2D(32, (11, 11), activation="relu", name="C1", input_shape=(152, 152, 3)) + ) + base_model.add(MaxPooling2D(pool_size=3, strides=2, padding="same", name="M2")) + base_model.add(Convolution2D(16, (9, 9), activation="relu", name="C3")) + base_model.add(LocallyConnected2D(16, (9, 9), activation="relu", name="L4")) + base_model.add(LocallyConnected2D(16, (7, 7), strides=2, activation="relu", name="L5")) + base_model.add(LocallyConnected2D(16, (5, 5), activation="relu", name="L6")) + base_model.add(Flatten(name="F0")) + base_model.add(Dense(4096, activation="relu", name="F7")) + base_model.add(Dropout(rate=0.5, name="D0")) + base_model.add(Dense(8631, activation="softmax", name="F8")) + + # --------------------------------- + + home = folder_utils.get_deepface_home() + + if os.path.isfile(home + "/.deepface/weights/VGGFace2_DeepFace_weights_val-0.9034.h5") != True: + logger.info("VGGFace2_DeepFace_weights_val-0.9034.h5 will be downloaded...") + + output = home + "/.deepface/weights/VGGFace2_DeepFace_weights_val-0.9034.h5.zip" + + gdown.download(url, output, quiet=False) + + # unzip VGGFace2_DeepFace_weights_val-0.9034.h5.zip + with zipfile.ZipFile(output, "r") as zip_ref: + zip_ref.extractall(home + "/.deepface/weights/") + + base_model.load_weights(home + "/.deepface/weights/VGGFace2_DeepFace_weights_val-0.9034.h5") + + # drop F8 and D0. F7 is the representation layer. + deepface_model = Model(inputs=base_model.layers[0].input, outputs=base_model.layers[-3].output) + + return deepface_model diff --git a/basemodels/GhostFaceNet.py b/basemodels/GhostFaceNet.py new file mode 100644 index 0000000000000000000000000000000000000000..1917833845d03a4a106129e537891a2bce46b4e2 --- /dev/null +++ b/basemodels/GhostFaceNet.py @@ -0,0 +1,312 @@ +# built-in dependencies +import os + +# 3rd party dependencies +import gdown +import tensorflow as tf + +# project dependencies +from deepface.commons import package_utils, folder_utils +from deepface.models.FacialRecognition import FacialRecognition +from deepface.commons import logger as log + +logger = log.get_singletonish_logger() + +tf_major = package_utils.get_tf_major_version() +if tf_major == 1: + import keras + from keras import backend as K + from keras.models import Model + from keras.layers import ( + Activation, + Add, + BatchNormalization, + Concatenate, + Conv2D, + DepthwiseConv2D, + GlobalAveragePooling2D, + Input, + Reshape, + Multiply, + ReLU, + PReLU, + ) +else: + from tensorflow import keras + from tensorflow.keras import backend as K + from tensorflow.keras.models import Model + from tensorflow.keras.layers import ( + Activation, + Add, + BatchNormalization, + Concatenate, + Conv2D, + DepthwiseConv2D, + GlobalAveragePooling2D, + Input, + Reshape, + Multiply, + ReLU, + PReLU, + ) + + +# 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" + + +class GhostFaceNetClient(FacialRecognition): + """ + GhostFaceNet model (GhostFaceNetV1 backbone) + Repo: https://github.com/HamadYA/GhostFaceNets + Pre-trained weights: https://github.com/HamadYA/GhostFaceNets/releases/tag/v1.2 + GhostFaceNet_W1.3_S1_ArcFace.h5 ~ 16.5MB + Author declared that this backbone and pre-trained weights got 99.7667% accuracy on LFW + """ + + def __init__(self): + self.model_name = "GhostFaceNet" + self.input_shape = (112, 112) + self.output_shape = 512 + self.model = load_model() + + +def load_model(): + model = GhostFaceNetV1() + + home = folder_utils.get_deepface_home() + output = home + "/.deepface/weights/ghostfacenet_v1.h5" + + if os.path.isfile(output) is not True: + logger.info(f"Pre-trained weights is downloaded from {PRETRAINED_WEIGHTS} to {output}") + gdown.download(PRETRAINED_WEIGHTS, output, quiet=False) + logger.info(f"Pre-trained weights is just downloaded to {output}") + + model.load_weights(output) + + return model + + +def GhostFaceNetV1() -> Model: + """ + Build GhostFaceNetV1 model. Refactored from + github.com/HamadYA/GhostFaceNets/blob/main/backbones/ghost_model.py + Returns: + model (Model) + """ + inputs = Input(shape=(112, 112, 3)) + + out_channel = 20 + + nn = Conv2D( + out_channel, + (3, 3), + strides=1, + padding="same", + use_bias=False, + kernel_initializer=keras.initializers.VarianceScaling( + scale=2.0, mode="fan_out", distribution="truncated_normal" + ), + )(inputs) + + nn = BatchNormalization(axis=-1)(nn) + nn = Activation("relu")(nn) + + dwkernels = [3, 3, 3, 5, 5, 3, 3, 3, 3, 3, 3, 5, 5, 5, 5, 5] + exps = [20, 64, 92, 92, 156, 312, 260, 240, 240, 624, 872, 872, 1248, 1248, 1248, 664] + outs = [20, 32, 32, 52, 52, 104, 104, 104, 104, 144, 144, 208, 208, 208, 208, 208] + strides_set = [1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1] + reductions = [0, 0, 0, 24, 40, 0, 0, 0, 0, 156, 220, 220, 0, 312, 0, 168] + + pre_out = out_channel + for dwk, stride, exp, out, reduction in zip(dwkernels, strides_set, exps, outs, reductions): + shortcut = not (out == pre_out and stride == 1) + nn = ghost_bottleneck(nn, dwk, stride, exp, out, reduction, shortcut) + pre_out = out + + nn = Conv2D( + 664, + (1, 1), + strides=(1, 1), + padding="valid", + use_bias=False, + kernel_initializer=keras.initializers.VarianceScaling( + scale=2.0, mode="fan_out", distribution="truncated_normal" + ), + )(nn) + nn = BatchNormalization(axis=-1)(nn) + nn = Activation("relu")(nn) + + xx = Model(inputs=inputs, outputs=nn, name="GhostFaceNetV1") + + # post modelling + inputs = xx.inputs[0] + nn = xx.outputs[0] + + nn = keras.layers.DepthwiseConv2D(nn.shape[1], use_bias=False, name="GDC_dw")(nn) + nn = keras.layers.BatchNormalization(momentum=0.99, epsilon=0.001, name="GDC_batchnorm")(nn) + nn = keras.layers.Conv2D( + 512, 1, use_bias=True, kernel_initializer="glorot_normal", name="GDC_conv" + )(nn) + nn = keras.layers.Flatten(name="GDC_flatten")(nn) + + embedding = keras.layers.BatchNormalization( + momentum=0.99, epsilon=0.001, scale=True, name="pre_embedding" + )(nn) + embedding_fp32 = keras.layers.Activation("linear", dtype="float32", name="embedding")(embedding) + + model = keras.models.Model(inputs, embedding_fp32, name=xx.name) + model = replace_relu_with_prelu(model=model) + return model + + +def se_module(inputs, reduction): + """ + Refactored from github.com/HamadYA/GhostFaceNets/blob/main/backbones/ghost_model.py + """ + # get the channel axis + channel_axis = 1 if K.image_data_format() == "channels_first" else -1 + # filters = channel axis shape + filters = inputs.shape[channel_axis] + + # from None x H x W x C to None x C + se = GlobalAveragePooling2D()(inputs) + + # Reshape None x C to None 1 x 1 x C + se = Reshape((1, 1, filters))(se) + + # Squeeze by using C*se_ratio. The size will be 1 x 1 x C*se_ratio + se = Conv2D( + reduction, + kernel_size=1, + use_bias=True, + kernel_initializer=keras.initializers.VarianceScaling( + scale=2.0, mode="fan_out", distribution="truncated_normal" + ), + )(se) + se = Activation("relu")(se) + + # Excitation using C filters. The size will be 1 x 1 x C + se = Conv2D( + filters, + kernel_size=1, + use_bias=True, + kernel_initializer=keras.initializers.VarianceScaling( + scale=2.0, mode="fan_out", distribution="truncated_normal" + ), + )(se) + se = Activation("hard_sigmoid")(se) + + return Multiply()([inputs, se]) + + +def ghost_module(inputs, out, convkernel=1, dwkernel=3, add_activation=True): + """ + Refactored from github.com/HamadYA/GhostFaceNets/blob/main/backbones/ghost_model.py + """ + conv_out_channel = out // 2 + cc = Conv2D( + conv_out_channel, + convkernel, + use_bias=False, + strides=(1, 1), + padding="same", + kernel_initializer=keras.initializers.VarianceScaling( + scale=2.0, mode="fan_out", distribution="truncated_normal" + ), + )(inputs) + cc = BatchNormalization(axis=-1)(cc) + if add_activation: + cc = Activation("relu")(cc) + + nn = DepthwiseConv2D( + dwkernel, + 1, + padding="same", + use_bias=False, + depthwise_initializer=keras.initializers.VarianceScaling( + scale=2.0, mode="fan_out", distribution="truncated_normal" + ), + )(cc) + nn = BatchNormalization(axis=-1)(nn) + if add_activation: + nn = Activation("relu")(nn) + return Concatenate()([cc, nn]) + + +def ghost_bottleneck(inputs, dwkernel, strides, exp, out, reduction, shortcut=True): + """ + Refactored from github.com/HamadYA/GhostFaceNets/blob/main/backbones/ghost_model.py + """ + nn = ghost_module(inputs, exp, add_activation=True) + if strides > 1: + # Extra depth conv if strides higher than 1 + nn = DepthwiseConv2D( + dwkernel, + strides, + padding="same", + use_bias=False, + depthwise_initializer=keras.initializers.VarianceScaling( + scale=2.0, mode="fan_out", distribution="truncated_normal" + ), + )(nn) + nn = BatchNormalization(axis=-1)(nn) + + if reduction > 0: + # Squeeze and excite + nn = se_module(nn, reduction) + + # Point-wise linear projection + nn = ghost_module(nn, out, add_activation=False) # ghost2 = GhostModule(exp, out, relu=False) + + if shortcut: + xx = DepthwiseConv2D( + dwkernel, + strides, + padding="same", + use_bias=False, + depthwise_initializer=keras.initializers.VarianceScaling( + scale=2.0, mode="fan_out", distribution="truncated_normal" + ), + )(inputs) + xx = BatchNormalization(axis=-1)(xx) + xx = Conv2D( + out, + (1, 1), + strides=(1, 1), + padding="valid", + use_bias=False, + kernel_initializer=keras.initializers.VarianceScaling( + scale=2.0, mode="fan_out", distribution="truncated_normal" + ), + )(xx) + xx = BatchNormalization(axis=-1)(xx) + else: + xx = inputs + return Add()([xx, nn]) + + +def replace_relu_with_prelu(model) -> Model: + """ + Replaces relu activation function in the built model with prelu. + Refactored from github.com/HamadYA/GhostFaceNets/blob/main/backbones/ghost_model.py + Args: + model (Model): built model with relu activation functions + Returns + model (Model): built model with prelu activation functions + """ + + def convert_relu(layer): + if isinstance(layer, ReLU) or ( + isinstance(layer, Activation) and layer.activation == keras.activations.relu + ): + layer_name = layer.name.replace("_relu", "_prelu") + return PReLU( + shared_axes=[1, 2], + alpha_initializer=tf.initializers.Constant(0.25), + name=layer_name, + ) + return layer + + input_tensors = keras.layers.Input(model.input_shape[1:]) + return keras.models.clone_model(model, input_tensors=input_tensors, clone_function=convert_relu) diff --git a/basemodels/OpenFace.py b/basemodels/OpenFace.py new file mode 100644 index 0000000000000000000000000000000000000000..cc335b6cda30ef5965a0f57058715ddc7840099c --- /dev/null +++ b/basemodels/OpenFace.py @@ -0,0 +1,397 @@ +import os +import gdown +import tensorflow as tf +from deepface.commons import package_utils, folder_utils +from deepface.models.FacialRecognition import FacialRecognition +from deepface.commons import logger as log + +logger = log.get_singletonish_logger() + +tf_version = package_utils.get_tf_major_version() +if tf_version == 1: + from keras.models import Model + from keras.layers import Conv2D, ZeroPadding2D, Input, concatenate + from keras.layers import Dense, Activation, Lambda, Flatten, BatchNormalization + from keras.layers import MaxPooling2D, AveragePooling2D + from keras import backend as K +else: + from tensorflow.keras.models import Model + from tensorflow.keras.layers import Conv2D, ZeroPadding2D, Input, concatenate + from tensorflow.keras.layers import Dense, Activation, Lambda, Flatten, BatchNormalization + from tensorflow.keras.layers import MaxPooling2D, AveragePooling2D + from tensorflow.keras import backend as K + +# pylint: disable=unnecessary-lambda + +# --------------------------------------- + +# pylint: disable=too-few-public-methods +class OpenFaceClient(FacialRecognition): + """ + OpenFace model class + """ + + def __init__(self): + self.model = load_model() + self.model_name = "OpenFace" + self.input_shape = (96, 96) + self.output_shape = 128 + + +def load_model( + url="https://github.com/serengil/deepface_models/releases/download/v1.0/openface_weights.h5", +) -> Model: + """ + Consturct OpenFace model, download its weights and load + Returns: + model (Model) + """ + myInput = Input(shape=(96, 96, 3)) + + x = ZeroPadding2D(padding=(3, 3), input_shape=(96, 96, 3))(myInput) + x = Conv2D(64, (7, 7), strides=(2, 2), name="conv1")(x) + x = BatchNormalization(axis=3, epsilon=0.00001, name="bn1")(x) + x = Activation("relu")(x) + x = ZeroPadding2D(padding=(1, 1))(x) + x = MaxPooling2D(pool_size=3, strides=2)(x) + x = Lambda(lambda x: tf.nn.lrn(x, alpha=1e-4, beta=0.75), name="lrn_1")(x) + x = Conv2D(64, (1, 1), name="conv2")(x) + x = BatchNormalization(axis=3, epsilon=0.00001, name="bn2")(x) + x = Activation("relu")(x) + x = ZeroPadding2D(padding=(1, 1))(x) + x = Conv2D(192, (3, 3), name="conv3")(x) + x = BatchNormalization(axis=3, epsilon=0.00001, name="bn3")(x) + x = Activation("relu")(x) + x = Lambda(lambda x: tf.nn.lrn(x, alpha=1e-4, beta=0.75), name="lrn_2")(x) # x is equal added + x = ZeroPadding2D(padding=(1, 1))(x) + x = MaxPooling2D(pool_size=3, strides=2)(x) + + # Inception3a + inception_3a_3x3 = Conv2D(96, (1, 1), name="inception_3a_3x3_conv1")(x) + inception_3a_3x3 = BatchNormalization(axis=3, epsilon=0.00001, name="inception_3a_3x3_bn1")( + inception_3a_3x3 + ) + inception_3a_3x3 = Activation("relu")(inception_3a_3x3) + inception_3a_3x3 = ZeroPadding2D(padding=(1, 1))(inception_3a_3x3) + inception_3a_3x3 = Conv2D(128, (3, 3), name="inception_3a_3x3_conv2")(inception_3a_3x3) + inception_3a_3x3 = BatchNormalization(axis=3, epsilon=0.00001, name="inception_3a_3x3_bn2")( + inception_3a_3x3 + ) + inception_3a_3x3 = Activation("relu")(inception_3a_3x3) + + inception_3a_5x5 = Conv2D(16, (1, 1), name="inception_3a_5x5_conv1")(x) + inception_3a_5x5 = BatchNormalization(axis=3, epsilon=0.00001, name="inception_3a_5x5_bn1")( + inception_3a_5x5 + ) + inception_3a_5x5 = Activation("relu")(inception_3a_5x5) + inception_3a_5x5 = ZeroPadding2D(padding=(2, 2))(inception_3a_5x5) + inception_3a_5x5 = Conv2D(32, (5, 5), name="inception_3a_5x5_conv2")(inception_3a_5x5) + inception_3a_5x5 = BatchNormalization(axis=3, epsilon=0.00001, name="inception_3a_5x5_bn2")( + inception_3a_5x5 + ) + inception_3a_5x5 = Activation("relu")(inception_3a_5x5) + + inception_3a_pool = MaxPooling2D(pool_size=3, strides=2)(x) + inception_3a_pool = Conv2D(32, (1, 1), name="inception_3a_pool_conv")(inception_3a_pool) + inception_3a_pool = BatchNormalization(axis=3, epsilon=0.00001, name="inception_3a_pool_bn")( + inception_3a_pool + ) + inception_3a_pool = Activation("relu")(inception_3a_pool) + inception_3a_pool = ZeroPadding2D(padding=((3, 4), (3, 4)))(inception_3a_pool) + + inception_3a_1x1 = Conv2D(64, (1, 1), name="inception_3a_1x1_conv")(x) + inception_3a_1x1 = BatchNormalization(axis=3, epsilon=0.00001, name="inception_3a_1x1_bn")( + inception_3a_1x1 + ) + inception_3a_1x1 = Activation("relu")(inception_3a_1x1) + + inception_3a = concatenate( + [inception_3a_3x3, inception_3a_5x5, inception_3a_pool, inception_3a_1x1], axis=3 + ) + + # Inception3b + inception_3b_3x3 = Conv2D(96, (1, 1), name="inception_3b_3x3_conv1")(inception_3a) + inception_3b_3x3 = BatchNormalization(axis=3, epsilon=0.00001, name="inception_3b_3x3_bn1")( + inception_3b_3x3 + ) + inception_3b_3x3 = Activation("relu")(inception_3b_3x3) + inception_3b_3x3 = ZeroPadding2D(padding=(1, 1))(inception_3b_3x3) + inception_3b_3x3 = Conv2D(128, (3, 3), name="inception_3b_3x3_conv2")(inception_3b_3x3) + inception_3b_3x3 = BatchNormalization(axis=3, epsilon=0.00001, name="inception_3b_3x3_bn2")( + inception_3b_3x3 + ) + inception_3b_3x3 = Activation("relu")(inception_3b_3x3) + + inception_3b_5x5 = Conv2D(32, (1, 1), name="inception_3b_5x5_conv1")(inception_3a) + inception_3b_5x5 = BatchNormalization(axis=3, epsilon=0.00001, name="inception_3b_5x5_bn1")( + inception_3b_5x5 + ) + inception_3b_5x5 = Activation("relu")(inception_3b_5x5) + inception_3b_5x5 = ZeroPadding2D(padding=(2, 2))(inception_3b_5x5) + inception_3b_5x5 = Conv2D(64, (5, 5), name="inception_3b_5x5_conv2")(inception_3b_5x5) + inception_3b_5x5 = BatchNormalization(axis=3, epsilon=0.00001, name="inception_3b_5x5_bn2")( + inception_3b_5x5 + ) + inception_3b_5x5 = Activation("relu")(inception_3b_5x5) + + inception_3b_pool = Lambda(lambda x: x**2, name="power2_3b")(inception_3a) + inception_3b_pool = AveragePooling2D(pool_size=(3, 3), strides=(3, 3))(inception_3b_pool) + inception_3b_pool = Lambda(lambda x: x * 9, name="mult9_3b")(inception_3b_pool) + inception_3b_pool = Lambda(lambda x: K.sqrt(x), name="sqrt_3b")(inception_3b_pool) + inception_3b_pool = Conv2D(64, (1, 1), name="inception_3b_pool_conv")(inception_3b_pool) + inception_3b_pool = BatchNormalization(axis=3, epsilon=0.00001, name="inception_3b_pool_bn")( + inception_3b_pool + ) + inception_3b_pool = Activation("relu")(inception_3b_pool) + inception_3b_pool = ZeroPadding2D(padding=(4, 4))(inception_3b_pool) + + inception_3b_1x1 = Conv2D(64, (1, 1), name="inception_3b_1x1_conv")(inception_3a) + inception_3b_1x1 = BatchNormalization(axis=3, epsilon=0.00001, name="inception_3b_1x1_bn")( + inception_3b_1x1 + ) + inception_3b_1x1 = Activation("relu")(inception_3b_1x1) + + inception_3b = concatenate( + [inception_3b_3x3, inception_3b_5x5, inception_3b_pool, inception_3b_1x1], axis=3 + ) + + # Inception3c + inception_3c_3x3 = Conv2D(128, (1, 1), strides=(1, 1), name="inception_3c_3x3_conv1")( + inception_3b + ) + inception_3c_3x3 = BatchNormalization(axis=3, epsilon=0.00001, name="inception_3c_3x3_bn1")( + inception_3c_3x3 + ) + inception_3c_3x3 = Activation("relu")(inception_3c_3x3) + inception_3c_3x3 = ZeroPadding2D(padding=(1, 1))(inception_3c_3x3) + inception_3c_3x3 = Conv2D(256, (3, 3), strides=(2, 2), name="inception_3c_3x3_conv" + "2")( + inception_3c_3x3 + ) + inception_3c_3x3 = BatchNormalization( + axis=3, epsilon=0.00001, name="inception_3c_3x3_bn" + "2" + )(inception_3c_3x3) + inception_3c_3x3 = Activation("relu")(inception_3c_3x3) + + inception_3c_5x5 = Conv2D(32, (1, 1), strides=(1, 1), name="inception_3c_5x5_conv1")( + inception_3b + ) + inception_3c_5x5 = BatchNormalization(axis=3, epsilon=0.00001, name="inception_3c_5x5_bn1")( + inception_3c_5x5 + ) + inception_3c_5x5 = Activation("relu")(inception_3c_5x5) + inception_3c_5x5 = ZeroPadding2D(padding=(2, 2))(inception_3c_5x5) + inception_3c_5x5 = Conv2D(64, (5, 5), strides=(2, 2), name="inception_3c_5x5_conv" + "2")( + inception_3c_5x5 + ) + inception_3c_5x5 = BatchNormalization( + axis=3, epsilon=0.00001, name="inception_3c_5x5_bn" + "2" + )(inception_3c_5x5) + inception_3c_5x5 = Activation("relu")(inception_3c_5x5) + + inception_3c_pool = MaxPooling2D(pool_size=3, strides=2)(inception_3b) + inception_3c_pool = ZeroPadding2D(padding=((0, 1), (0, 1)))(inception_3c_pool) + + inception_3c = concatenate([inception_3c_3x3, inception_3c_5x5, inception_3c_pool], axis=3) + + # inception 4a + inception_4a_3x3 = Conv2D(96, (1, 1), strides=(1, 1), name="inception_4a_3x3_conv" + "1")( + inception_3c + ) + inception_4a_3x3 = BatchNormalization( + axis=3, epsilon=0.00001, name="inception_4a_3x3_bn" + "1" + )(inception_4a_3x3) + inception_4a_3x3 = Activation("relu")(inception_4a_3x3) + inception_4a_3x3 = ZeroPadding2D(padding=(1, 1))(inception_4a_3x3) + inception_4a_3x3 = Conv2D(192, (3, 3), strides=(1, 1), name="inception_4a_3x3_conv" + "2")( + inception_4a_3x3 + ) + inception_4a_3x3 = BatchNormalization( + axis=3, epsilon=0.00001, name="inception_4a_3x3_bn" + "2" + )(inception_4a_3x3) + inception_4a_3x3 = Activation("relu")(inception_4a_3x3) + + inception_4a_5x5 = Conv2D(32, (1, 1), strides=(1, 1), name="inception_4a_5x5_conv1")( + inception_3c + ) + inception_4a_5x5 = BatchNormalization(axis=3, epsilon=0.00001, name="inception_4a_5x5_bn1")( + inception_4a_5x5 + ) + inception_4a_5x5 = Activation("relu")(inception_4a_5x5) + inception_4a_5x5 = ZeroPadding2D(padding=(2, 2))(inception_4a_5x5) + inception_4a_5x5 = Conv2D(64, (5, 5), strides=(1, 1), name="inception_4a_5x5_conv" + "2")( + inception_4a_5x5 + ) + inception_4a_5x5 = BatchNormalization( + axis=3, epsilon=0.00001, name="inception_4a_5x5_bn" + "2" + )(inception_4a_5x5) + inception_4a_5x5 = Activation("relu")(inception_4a_5x5) + + inception_4a_pool = Lambda(lambda x: x**2, name="power2_4a")(inception_3c) + inception_4a_pool = AveragePooling2D(pool_size=(3, 3), strides=(3, 3))(inception_4a_pool) + inception_4a_pool = Lambda(lambda x: x * 9, name="mult9_4a")(inception_4a_pool) + inception_4a_pool = Lambda(lambda x: K.sqrt(x), name="sqrt_4a")(inception_4a_pool) + + inception_4a_pool = Conv2D(128, (1, 1), strides=(1, 1), name="inception_4a_pool_conv" + "")( + inception_4a_pool + ) + inception_4a_pool = BatchNormalization( + axis=3, epsilon=0.00001, name="inception_4a_pool_bn" + "" + )(inception_4a_pool) + inception_4a_pool = Activation("relu")(inception_4a_pool) + inception_4a_pool = ZeroPadding2D(padding=(2, 2))(inception_4a_pool) + + inception_4a_1x1 = Conv2D(256, (1, 1), strides=(1, 1), name="inception_4a_1x1_conv" + "")( + inception_3c + ) + inception_4a_1x1 = BatchNormalization(axis=3, epsilon=0.00001, name="inception_4a_1x1_bn" + "")( + inception_4a_1x1 + ) + inception_4a_1x1 = Activation("relu")(inception_4a_1x1) + + inception_4a = concatenate( + [inception_4a_3x3, inception_4a_5x5, inception_4a_pool, inception_4a_1x1], axis=3 + ) + + # inception4e + inception_4e_3x3 = Conv2D(160, (1, 1), strides=(1, 1), name="inception_4e_3x3_conv" + "1")( + inception_4a + ) + inception_4e_3x3 = BatchNormalization( + axis=3, epsilon=0.00001, name="inception_4e_3x3_bn" + "1" + )(inception_4e_3x3) + inception_4e_3x3 = Activation("relu")(inception_4e_3x3) + inception_4e_3x3 = ZeroPadding2D(padding=(1, 1))(inception_4e_3x3) + inception_4e_3x3 = Conv2D(256, (3, 3), strides=(2, 2), name="inception_4e_3x3_conv" + "2")( + inception_4e_3x3 + ) + inception_4e_3x3 = BatchNormalization( + axis=3, epsilon=0.00001, name="inception_4e_3x3_bn" + "2" + )(inception_4e_3x3) + inception_4e_3x3 = Activation("relu")(inception_4e_3x3) + + inception_4e_5x5 = Conv2D(64, (1, 1), strides=(1, 1), name="inception_4e_5x5_conv" + "1")( + inception_4a + ) + inception_4e_5x5 = BatchNormalization( + axis=3, epsilon=0.00001, name="inception_4e_5x5_bn" + "1" + )(inception_4e_5x5) + inception_4e_5x5 = Activation("relu")(inception_4e_5x5) + inception_4e_5x5 = ZeroPadding2D(padding=(2, 2))(inception_4e_5x5) + inception_4e_5x5 = Conv2D(128, (5, 5), strides=(2, 2), name="inception_4e_5x5_conv" + "2")( + inception_4e_5x5 + ) + inception_4e_5x5 = BatchNormalization( + axis=3, epsilon=0.00001, name="inception_4e_5x5_bn" + "2" + )(inception_4e_5x5) + inception_4e_5x5 = Activation("relu")(inception_4e_5x5) + + inception_4e_pool = MaxPooling2D(pool_size=3, strides=2)(inception_4a) + inception_4e_pool = ZeroPadding2D(padding=((0, 1), (0, 1)))(inception_4e_pool) + + inception_4e = concatenate([inception_4e_3x3, inception_4e_5x5, inception_4e_pool], axis=3) + + # inception5a + inception_5a_3x3 = Conv2D(96, (1, 1), strides=(1, 1), name="inception_5a_3x3_conv" + "1")( + inception_4e + ) + inception_5a_3x3 = BatchNormalization( + axis=3, epsilon=0.00001, name="inception_5a_3x3_bn" + "1" + )(inception_5a_3x3) + inception_5a_3x3 = Activation("relu")(inception_5a_3x3) + inception_5a_3x3 = ZeroPadding2D(padding=(1, 1))(inception_5a_3x3) + inception_5a_3x3 = Conv2D(384, (3, 3), strides=(1, 1), name="inception_5a_3x3_conv" + "2")( + inception_5a_3x3 + ) + inception_5a_3x3 = BatchNormalization( + axis=3, epsilon=0.00001, name="inception_5a_3x3_bn" + "2" + )(inception_5a_3x3) + inception_5a_3x3 = Activation("relu")(inception_5a_3x3) + + inception_5a_pool = Lambda(lambda x: x**2, name="power2_5a")(inception_4e) + inception_5a_pool = AveragePooling2D(pool_size=(3, 3), strides=(3, 3))(inception_5a_pool) + inception_5a_pool = Lambda(lambda x: x * 9, name="mult9_5a")(inception_5a_pool) + inception_5a_pool = Lambda(lambda x: K.sqrt(x), name="sqrt_5a")(inception_5a_pool) + + inception_5a_pool = Conv2D(96, (1, 1), strides=(1, 1), name="inception_5a_pool_conv" + "")( + inception_5a_pool + ) + inception_5a_pool = BatchNormalization( + axis=3, epsilon=0.00001, name="inception_5a_pool_bn" + "" + )(inception_5a_pool) + inception_5a_pool = Activation("relu")(inception_5a_pool) + inception_5a_pool = ZeroPadding2D(padding=(1, 1))(inception_5a_pool) + + inception_5a_1x1 = Conv2D(256, (1, 1), strides=(1, 1), name="inception_5a_1x1_conv" + "")( + inception_4e + ) + inception_5a_1x1 = BatchNormalization(axis=3, epsilon=0.00001, name="inception_5a_1x1_bn" + "")( + inception_5a_1x1 + ) + inception_5a_1x1 = Activation("relu")(inception_5a_1x1) + + inception_5a = concatenate([inception_5a_3x3, inception_5a_pool, inception_5a_1x1], axis=3) + + # inception_5b + inception_5b_3x3 = Conv2D(96, (1, 1), strides=(1, 1), name="inception_5b_3x3_conv" + "1")( + inception_5a + ) + inception_5b_3x3 = BatchNormalization( + axis=3, epsilon=0.00001, name="inception_5b_3x3_bn" + "1" + )(inception_5b_3x3) + inception_5b_3x3 = Activation("relu")(inception_5b_3x3) + inception_5b_3x3 = ZeroPadding2D(padding=(1, 1))(inception_5b_3x3) + inception_5b_3x3 = Conv2D(384, (3, 3), strides=(1, 1), name="inception_5b_3x3_conv" + "2")( + inception_5b_3x3 + ) + inception_5b_3x3 = BatchNormalization( + axis=3, epsilon=0.00001, name="inception_5b_3x3_bn" + "2" + )(inception_5b_3x3) + inception_5b_3x3 = Activation("relu")(inception_5b_3x3) + + inception_5b_pool = MaxPooling2D(pool_size=3, strides=2)(inception_5a) + + inception_5b_pool = Conv2D(96, (1, 1), strides=(1, 1), name="inception_5b_pool_conv" + "")( + inception_5b_pool + ) + inception_5b_pool = BatchNormalization( + axis=3, epsilon=0.00001, name="inception_5b_pool_bn" + "" + )(inception_5b_pool) + inception_5b_pool = Activation("relu")(inception_5b_pool) + + inception_5b_pool = ZeroPadding2D(padding=(1, 1))(inception_5b_pool) + + inception_5b_1x1 = Conv2D(256, (1, 1), strides=(1, 1), name="inception_5b_1x1_conv" + "")( + inception_5a + ) + inception_5b_1x1 = BatchNormalization(axis=3, epsilon=0.00001, name="inception_5b_1x1_bn" + "")( + inception_5b_1x1 + ) + inception_5b_1x1 = Activation("relu")(inception_5b_1x1) + + inception_5b = concatenate([inception_5b_3x3, inception_5b_pool, inception_5b_1x1], axis=3) + + av_pool = AveragePooling2D(pool_size=(3, 3), strides=(1, 1))(inception_5b) + reshape_layer = Flatten()(av_pool) + dense_layer = Dense(128, name="dense_layer")(reshape_layer) + norm_layer = Lambda(lambda x: K.l2_normalize(x, axis=1), name="norm_layer")(dense_layer) + + # Final Model + model = Model(inputs=[myInput], outputs=norm_layer) + + # ----------------------------------- + + home = folder_utils.get_deepface_home() + + if os.path.isfile(home + "/.deepface/weights/openface_weights.h5") != True: + logger.info("openface_weights.h5 will be downloaded...") + + output = home + "/.deepface/weights/openface_weights.h5" + gdown.download(url, output, quiet=False) + + # ----------------------------------- + + model.load_weights(home + "/.deepface/weights/openface_weights.h5") + + # ----------------------------------- + + return model diff --git a/basemodels/SFace.py b/basemodels/SFace.py new file mode 100644 index 0000000000000000000000000000000000000000..ddd9360f17f3822c4c0d41d7bcc2ddce56b23e0b --- /dev/null +++ b/basemodels/SFace.py @@ -0,0 +1,87 @@ +# built-in dependencies +import os +from typing import Any, List + +# 3rd party dependencies +import numpy as np +import cv2 as cv +import gdown + +# project dependencies +from deepface.commons import folder_utils +from deepface.models.FacialRecognition import FacialRecognition +from deepface.commons import logger as log + +logger = log.get_singletonish_logger() + +# pylint: disable=line-too-long, too-few-public-methods + + +class SFaceClient(FacialRecognition): + """ + SFace model class + """ + + def __init__(self): + self.model = load_model() + self.model_name = "SFace" + self.input_shape = (112, 112) + self.output_shape = 128 + + def forward(self, img: np.ndarray) -> List[float]: + """ + Find embeddings with SFace model + This model necessitates the override of the forward method + because it is not a keras model. + Args: + img (np.ndarray): pre-loaded image in BGR + Returns + embeddings (list): multi-dimensional vector + """ + # return self.model.predict(img)[0].tolist() + + # revert the image to original format and preprocess using the model + input_blob = (img[0] * 255).astype(np.uint8) + + embeddings = self.model.model.feature(input_blob) + + return embeddings[0].tolist() + + +def load_model( + url="https://github.com/opencv/opencv_zoo/raw/main/models/face_recognition_sface/face_recognition_sface_2021dec.onnx", +) -> Any: + """ + Construct SFace model, download its weights and load + """ + + home = folder_utils.get_deepface_home() + + file_name = home + "/.deepface/weights/face_recognition_sface_2021dec.onnx" + + if not os.path.isfile(file_name): + + logger.info("sface weights will be downloaded...") + + gdown.download(url, file_name, quiet=False) + + model = SFaceWrapper(model_path=file_name) + + return model + + +class SFaceWrapper: + def __init__(self, model_path): + """ + SFace wrapper covering model construction, layer infos and predict + """ + try: + self.model = cv.FaceRecognizerSF.create( + model=model_path, config="", backend_id=0, target_id=0 + ) + except Exception as err: + raise ValueError( + "Exception while calling opencv.FaceRecognizerSF module." + + "This is an optional dependency." + + "You can install it as pip install opencv-contrib-python." + ) from err diff --git a/basemodels/VGGFace.py b/basemodels/VGGFace.py new file mode 100644 index 0000000000000000000000000000000000000000..819150e33bb3ac28c03c87e2dd615aa85534715d --- /dev/null +++ b/basemodels/VGGFace.py @@ -0,0 +1,160 @@ +from typing import List +import os +import gdown +import numpy as np +from deepface.commons import package_utils, folder_utils +from deepface.modules import verification +from deepface.models.FacialRecognition import FacialRecognition +from deepface.commons import logger as log + +logger = log.get_singletonish_logger() + +# --------------------------------------- + +tf_version = package_utils.get_tf_major_version() +if tf_version == 1: + from keras.models import Model, Sequential + from keras.layers import ( + Convolution2D, + ZeroPadding2D, + MaxPooling2D, + Flatten, + Dropout, + Activation, + ) +else: + from tensorflow.keras.models import Model, Sequential + from tensorflow.keras.layers import ( + Convolution2D, + ZeroPadding2D, + MaxPooling2D, + Flatten, + Dropout, + Activation, + ) + +# --------------------------------------- + +# pylint: disable=too-few-public-methods +class VggFaceClient(FacialRecognition): + """ + VGG-Face model class + """ + + def __init__(self): + self.model = load_model() + self.model_name = "VGG-Face" + self.input_shape = (224, 224) + self.output_shape = 4096 + + def forward(self, img: np.ndarray) -> List[float]: + """ + Generates embeddings using the VGG-Face model. + This method incorporates an additional normalization layer, + necessitating the override of the forward method. + + Args: + img (np.ndarray): pre-loaded image in BGR + Returns + embeddings (list): multi-dimensional vector + """ + # model.predict causes memory issue when it is called in a for loop + # embedding = model.predict(img, verbose=0)[0].tolist() + + # having normalization layer in descriptor troubles for some gpu users (e.g. issue 957, 966) + # instead we are now calculating it with traditional way not with keras backend + embedding = self.model(img, training=False).numpy()[0].tolist() + embedding = verification.l2_normalize(embedding) + return embedding.tolist() + + +def base_model() -> Sequential: + """ + Base model of VGG-Face being used for classification - not to find embeddings + Returns: + model (Sequential): model was trained to classify 2622 identities + """ + model = Sequential() + model.add(ZeroPadding2D((1, 1), input_shape=(224, 224, 3))) + model.add(Convolution2D(64, (3, 3), activation="relu")) + model.add(ZeroPadding2D((1, 1))) + model.add(Convolution2D(64, (3, 3), activation="relu")) + model.add(MaxPooling2D((2, 2), strides=(2, 2))) + + model.add(ZeroPadding2D((1, 1))) + model.add(Convolution2D(128, (3, 3), activation="relu")) + model.add(ZeroPadding2D((1, 1))) + model.add(Convolution2D(128, (3, 3), activation="relu")) + model.add(MaxPooling2D((2, 2), strides=(2, 2))) + + model.add(ZeroPadding2D((1, 1))) + model.add(Convolution2D(256, (3, 3), activation="relu")) + model.add(ZeroPadding2D((1, 1))) + model.add(Convolution2D(256, (3, 3), activation="relu")) + model.add(ZeroPadding2D((1, 1))) + model.add(Convolution2D(256, (3, 3), activation="relu")) + model.add(MaxPooling2D((2, 2), strides=(2, 2))) + + model.add(ZeroPadding2D((1, 1))) + model.add(Convolution2D(512, (3, 3), activation="relu")) + model.add(ZeroPadding2D((1, 1))) + model.add(Convolution2D(512, (3, 3), activation="relu")) + model.add(ZeroPadding2D((1, 1))) + model.add(Convolution2D(512, (3, 3), activation="relu")) + model.add(MaxPooling2D((2, 2), strides=(2, 2))) + + model.add(ZeroPadding2D((1, 1))) + model.add(Convolution2D(512, (3, 3), activation="relu")) + model.add(ZeroPadding2D((1, 1))) + model.add(Convolution2D(512, (3, 3), activation="relu")) + model.add(ZeroPadding2D((1, 1))) + model.add(Convolution2D(512, (3, 3), activation="relu")) + model.add(MaxPooling2D((2, 2), strides=(2, 2))) + + model.add(Convolution2D(4096, (7, 7), activation="relu")) + model.add(Dropout(0.5)) + model.add(Convolution2D(4096, (1, 1), activation="relu")) + model.add(Dropout(0.5)) + model.add(Convolution2D(2622, (1, 1))) + model.add(Flatten()) + model.add(Activation("softmax")) + + return model + + +def load_model( + url="https://github.com/serengil/deepface_models/releases/download/v1.0/vgg_face_weights.h5", +) -> Model: + """ + Final VGG-Face model being used for finding embeddings + Returns: + model (Model): returning 4096 dimensional vectors + """ + + model = base_model() + + home = folder_utils.get_deepface_home() + output = home + "/.deepface/weights/vgg_face_weights.h5" + + if os.path.isfile(output) != True: + logger.info("vgg_face_weights.h5 will be downloaded...") + gdown.download(url, output, quiet=False) + + model.load_weights(output) + + # 2622d dimensional model + # vgg_face_descriptor = Model(inputs=model.layers[0].input, outputs=model.layers[-2].output) + + # 4096 dimensional model offers 6% to 14% increasement on accuracy! + # - 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")( + # base_model_output + # ) + vgg_face_descriptor = Model(inputs=model.input, outputs=base_model_output) + + return vgg_face_descriptor diff --git a/basemodels/__init__.py b/basemodels/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/basemodels/__pycache__/ArcFace.cpython-312.pyc b/basemodels/__pycache__/ArcFace.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..be1310431e4c589ca14dd236bd7414b1592ffaa4 Binary files /dev/null and b/basemodels/__pycache__/ArcFace.cpython-312.pyc differ diff --git a/basemodels/__pycache__/DeepID.cpython-312.pyc b/basemodels/__pycache__/DeepID.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6c9465926ee1752c559db897c1a36662eb171e6b Binary files /dev/null and b/basemodels/__pycache__/DeepID.cpython-312.pyc differ diff --git a/basemodels/__pycache__/Dlib.cpython-312.pyc b/basemodels/__pycache__/Dlib.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..88a8f9993a9b8e9e94a40e97070a660969112c7f Binary files /dev/null and b/basemodels/__pycache__/Dlib.cpython-312.pyc differ diff --git a/basemodels/__pycache__/Facenet.cpython-312.pyc b/basemodels/__pycache__/Facenet.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9b98a1816d53adff3f919ee2664e5ddff6096792 Binary files /dev/null and b/basemodels/__pycache__/Facenet.cpython-312.pyc differ diff --git a/basemodels/__pycache__/FbDeepFace.cpython-312.pyc b/basemodels/__pycache__/FbDeepFace.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..123a7ab941667ba718db0f002f6a7be35fecb981 Binary files /dev/null and b/basemodels/__pycache__/FbDeepFace.cpython-312.pyc differ diff --git a/basemodels/__pycache__/GhostFaceNet.cpython-312.pyc b/basemodels/__pycache__/GhostFaceNet.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1b8261aca70ed9203b8d2435c8e09f26894ca14f Binary files /dev/null and b/basemodels/__pycache__/GhostFaceNet.cpython-312.pyc differ diff --git a/basemodels/__pycache__/OpenFace.cpython-312.pyc b/basemodels/__pycache__/OpenFace.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..af1be7b2e85a63476ee9f3150d1fadd2273109f7 Binary files /dev/null and b/basemodels/__pycache__/OpenFace.cpython-312.pyc differ diff --git a/basemodels/__pycache__/SFace.cpython-312.pyc b/basemodels/__pycache__/SFace.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cbf759566c5cd94004455fa31f79b9adf5b197a2 Binary files /dev/null and b/basemodels/__pycache__/SFace.cpython-312.pyc differ diff --git a/basemodels/__pycache__/VGGFace.cpython-312.pyc b/basemodels/__pycache__/VGGFace.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6d691944b09c1109cdb173f3ba5d303951695d8f Binary files /dev/null and b/basemodels/__pycache__/VGGFace.cpython-312.pyc differ diff --git a/basemodels/__pycache__/__init__.cpython-312.pyc b/basemodels/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2745ee3693ceef9f4c7d564f3011ce627cc9771d Binary files /dev/null and b/basemodels/__pycache__/__init__.cpython-312.pyc differ diff --git a/commons/__init__.py b/commons/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/commons/__pycache__/__init__.cpython-312.pyc b/commons/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6822571cd6b16a6ae15e675f7389374e20305391 Binary files /dev/null and b/commons/__pycache__/__init__.cpython-312.pyc differ diff --git a/commons/__pycache__/file_utils.cpython-312.pyc b/commons/__pycache__/file_utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9ba8762a0835203b5db6c951af93a41cea0b0b5d Binary files /dev/null and b/commons/__pycache__/file_utils.cpython-312.pyc differ diff --git a/commons/__pycache__/folder_utils.cpython-312.pyc b/commons/__pycache__/folder_utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8fd4bc8a74f147a16a247c8e2e47c60c0e80a678 Binary files /dev/null and b/commons/__pycache__/folder_utils.cpython-312.pyc differ diff --git a/commons/__pycache__/image_utils.cpython-312.pyc b/commons/__pycache__/image_utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cfc8eea499516194034fb16b9a5e8c979a981901 Binary files /dev/null and b/commons/__pycache__/image_utils.cpython-312.pyc differ diff --git a/commons/__pycache__/logger.cpython-312.pyc b/commons/__pycache__/logger.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1ecd6dc6b06bbdc71b230fffe7df0c47ddc16bab Binary files /dev/null and b/commons/__pycache__/logger.cpython-312.pyc differ diff --git a/commons/__pycache__/os_path.cpython-312.pyc b/commons/__pycache__/os_path.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c1e954046f9f8a0862cc5715cff2c35c84a977f8 Binary files /dev/null and b/commons/__pycache__/os_path.cpython-312.pyc differ diff --git a/commons/__pycache__/package_utils.cpython-312.pyc b/commons/__pycache__/package_utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..90619a9d518348a51be4092aa3ab056049f4beb5 Binary files /dev/null and b/commons/__pycache__/package_utils.cpython-312.pyc differ diff --git a/commons/constant.py b/commons/constant.py new file mode 100644 index 0000000000000000000000000000000000000000..22f63499f051c89d0bae3e7325aae8b46c7b678a --- /dev/null +++ b/commons/constant.py @@ -0,0 +1,4 @@ +import os + +SRC_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +ROOT_DIR = os.path.dirname(SRC_DIR) diff --git a/commons/folder_utils.py b/commons/folder_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..ef5fbc71bb11415199813e2f51c5dbaf6ad07640 --- /dev/null +++ b/commons/folder_utils.py @@ -0,0 +1,35 @@ +import os +from pathlib import Path +from deepface.commons import logger as log + +logger = log.get_singletonish_logger() + + +def initialize_folder() -> None: + """ + Initialize the folder for storing model weights. + + Raises: + OSError: if the folder cannot be created. + """ + home = get_deepface_home() + deepface_home_path = home + "/.deepface" + weights_path = deepface_home_path + "/weights" + + if not os.path.exists(deepface_home_path): + os.makedirs(deepface_home_path, exist_ok=True) + logger.info(f"Directory {home}/.deepface created") + + if not os.path.exists(weights_path): + os.makedirs(weights_path, exist_ok=True) + logger.info(f"Directory {home}/.deepface/weights created") + + +def get_deepface_home() -> str: + """ + Get the home directory for storing model weights + + Returns: + str: the home directory. + """ + return str(os.getenv("DEEPFACE_HOME", default=str(Path.home()))) diff --git a/commons/image_utils.py b/commons/image_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..c25e41167fcc70604ae64bce47a3d86531dd846f --- /dev/null +++ b/commons/image_utils.py @@ -0,0 +1,149 @@ +# built-in dependencies +import os +import io +from typing import List, Union, Tuple +import hashlib +import base64 +from pathlib import Path + +# 3rd party dependencies +import requests +import numpy as np +import cv2 +from PIL import Image + + +def list_images(path: str) -> List[str]: + """ + List images in a given path + Args: + path (str): path's location + Returns: + images (list): list of exact image paths + """ + images = [] + for r, _, f in os.walk(path): + for file in f: + exact_path = os.path.join(r, file) + + _, ext = os.path.splitext(exact_path) + ext_lower = ext.lower() + + if ext_lower not in {".jpg", ".jpeg", ".png"}: + continue + + with Image.open(exact_path) as img: # lazy + if img.format.lower() in ["jpeg", "png"]: + images.append(exact_path) + return images + + +def find_image_hash(file_path: str) -> str: + """ + Find the hash of given image file with its properties + finding the hash of image content is costly operation + Args: + file_path (str): exact image path + Returns: + hash (str): digest with sha1 algorithm + """ + file_stats = os.stat(file_path) + + # some properties + file_size = file_stats.st_size + creation_time = file_stats.st_ctime + modification_time = file_stats.st_mtime + + properties = f"{file_size}-{creation_time}-{modification_time}" + + hasher = hashlib.sha1() + hasher.update(properties.encode("utf-8")) + return hasher.hexdigest() + + +def load_image(img: Union[str, np.ndarray]) -> Tuple[np.ndarray, str]: + """ + Load image from path, url, base64 or numpy array. + Args: + img: a path, url, base64 or numpy array. + Returns: + image (numpy array): the loaded image in BGR format + image name (str): image name itself + """ + + # The image is already a numpy array + if isinstance(img, np.ndarray): + return img, "numpy array" + + if isinstance(img, Path): + img = str(img) + + if not isinstance(img, str): + raise ValueError(f"img must be numpy array or str but it is {type(img)}") + + # The image is a base64 string + if img.startswith("data:image/"): + return load_image_from_base64(img), "base64 encoded string" + + # The image is a url + if img.lower().startswith("http://") or img.lower().startswith("https://"): + return load_image_from_web(url=img), img + + # The image is a path + if os.path.isfile(img) is not True: + raise ValueError(f"Confirm that {img} exists") + + # image must be a file on the system then + + # image name must have english characters + if img.isascii() is False: + raise ValueError(f"Input image must not have non-english characters - {img}") + + img_obj_bgr = cv2.imread(img) + # img_obj_rgb = cv2.cvtColor(img_obj_bgr, cv2.COLOR_BGR2RGB) + return img_obj_bgr, img + + +def load_image_from_base64(uri: str) -> np.ndarray: + """ + Load image from base64 string. + Args: + uri: a base64 string. + Returns: + numpy array: the loaded image. + """ + + encoded_data_parts = uri.split(",") + + if len(encoded_data_parts) < 2: + raise ValueError("format error in base64 encoded string") + + encoded_data = encoded_data_parts[1] + decoded_bytes = base64.b64decode(encoded_data) + + # similar to find functionality, we are just considering these extensions + # content type is safer option than file extension + with Image.open(io.BytesIO(decoded_bytes)) as img: + file_type = img.format.lower() + if file_type not in ["jpeg", "png"]: + raise ValueError(f"input image can be jpg or png, but it is {file_type}") + + nparr = np.fromstring(decoded_bytes, np.uint8) + img_bgr = cv2.imdecode(nparr, cv2.IMREAD_COLOR) + # img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) + return img_bgr + + +def load_image_from_web(url: str) -> np.ndarray: + """ + Loading an image from web + Args: + url: link for the image + Returns: + img (np.ndarray): equivalent to pre-loaded image from opencv (BGR format) + """ + response = requests.get(url, stream=True, timeout=60) + response.raise_for_status() + image_array = np.asarray(bytearray(response.raw.read()), dtype=np.uint8) + img = cv2.imdecode(image_array, cv2.IMREAD_COLOR) + return img diff --git a/commons/logger.py b/commons/logger.py new file mode 100644 index 0000000000000000000000000000000000000000..45339532d1c1392bfc3a14ff01ca7ec5dbbf8385 --- /dev/null +++ b/commons/logger.py @@ -0,0 +1,54 @@ +import os +import logging +from datetime import datetime + +# pylint: disable=broad-except +class Logger: + def __init__(self, module=None): + self.module = module + log_level = os.environ.get("DEEPFACE_LOG_LEVEL", str(logging.INFO)) + try: + self.log_level = int(log_level) + except Exception as err: + self.dump_log( + f"Exception while parsing $DEEPFACE_LOG_LEVEL." + f"Expected int but it is {log_level} ({str(err)})." + "Setting app log level to info." + ) + self.log_level = logging.INFO + + def info(self, message): + if self.log_level <= logging.INFO: + self.dump_log(f"{message}") + + def debug(self, message): + if self.log_level <= logging.DEBUG: + self.dump_log(f"🕷️ {message}") + + def warn(self, message): + if self.log_level <= logging.WARNING: + self.dump_log(f"⚠️ {message}") + + def error(self, message): + if self.log_level <= logging.ERROR: + self.dump_log(f"🔴 {message}") + + def critical(self, message): + if self.log_level <= logging.CRITICAL: + self.dump_log(f"💥 {message}") + + def dump_log(self, message): + print(f"{str(datetime.now())[2:-7]} - {message}") + + +def get_singletonish_logger(): + # singleton design pattern + global model_obj + + if not "model_obj" in globals(): + model_obj = {} + + if "logger" not in model_obj.keys(): + model_obj["logger"] = Logger(module="Singleton") + + return model_obj["logger"] diff --git a/commons/os_path.py b/commons/os_path.py new file mode 100644 index 0000000000000000000000000000000000000000..954bcf81d42fa5d3590048991909d7a6cd1fc53b --- /dev/null +++ b/commons/os_path.py @@ -0,0 +1,10 @@ +import os + +class os_path : + + def get_main_directory(): + path = os.path.abspath(__file__) + drive, _ = os.path.splitdrive(path) + if not drive.endswith(os.path.sep): + drive += os.path.sep + return drive \ No newline at end of file diff --git a/commons/package_utils.py b/commons/package_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..c35db8bc55114b58068d4eb3705c58e6b17030f3 --- /dev/null +++ b/commons/package_utils.py @@ -0,0 +1,46 @@ +# 3rd party dependencies +import tensorflow as tf + +# package dependencies +from deepface.commons import logger as log + +logger = log.get_singletonish_logger() + + +def get_tf_major_version() -> int: + """ + Find tensorflow's major version + Returns + major_version (int) + """ + return int(tf.__version__.split(".", maxsplit=1)[0]) + + +def get_tf_minor_version() -> int: + """ + Find tensorflow's minor version + Returns + minor_version (int) + """ + return int(tf.__version__.split(".", maxsplit=-1)[1]) + + +def validate_for_keras3(): + tf_major = get_tf_major_version() + tf_minor = get_tf_minor_version() + + # tf_keras is a must dependency after tf 2.16 + if tf_major == 1 or (tf_major == 2 and tf_minor < 16): + return + + try: + import tf_keras + + logger.debug(f"tf_keras is already available - {tf_keras.__version__}") + except ImportError as err: + # you may consider to install that package here + raise ValueError( + f"You have tensorflow {tf.__version__} and this requires " + "tf-keras package. Please run `pip install tf-keras` " + "or downgrade your tensorflow." + ) from err diff --git a/commons/path.py b/commons/path.py new file mode 100644 index 0000000000000000000000000000000000000000..30abbb198e9f3dbb0266485663cd4bb0a12686f2 --- /dev/null +++ b/commons/path.py @@ -0,0 +1,9 @@ +import os + +class path : + +def get_parent_path(path,levels=1): + for _ in range(levels): + path = os.path.dirname(path) + return path + diff --git a/detectors/CenterFace.py b/detectors/CenterFace.py new file mode 100644 index 0000000000000000000000000000000000000000..84db3345c977841d9b2ca69aa60cdd0f2a6d7219 --- /dev/null +++ b/detectors/CenterFace.py @@ -0,0 +1,217 @@ +# built-in dependencies +import os +from typing import List + +# 3rd party dependencies +import numpy as np +import cv2 +import gdown + +# project dependencies +from deepface.commons import folder_utils +from deepface.models.Detector import Detector, FacialAreaRegion +from deepface.commons import logger as log + +logger = log.get_singletonish_logger() + +# pylint: disable=c-extension-no-member + +WEIGHTS_URL = "https://github.com/Star-Clouds/CenterFace/raw/master/models/onnx/centerface.onnx" + + +class CenterFaceClient(Detector): + def __init__(self): + # BUG: model must be flushed for each call + # self.model = self.build_model() + pass + + def build_model(self): + """ + Download pre-trained weights of CenterFace model if necessary and load built model + """ + weights_path = f"{folder_utils.get_deepface_home()}/.deepface/weights/centerface.onnx" + if not os.path.isfile(weights_path): + logger.info(f"Downloading CenterFace weights from {WEIGHTS_URL} to {weights_path}...") + try: + gdown.download(WEIGHTS_URL, weights_path, quiet=False) + except Exception as err: + raise ValueError( + f"Exception while downloading CenterFace weights from {WEIGHTS_URL}." + f"You may consider to download it to {weights_path} manually." + ) from err + logger.info(f"CenterFace model is just downloaded to {os.path.basename(weights_path)}") + + return CenterFace(weight_path=weights_path) + + def detect_faces(self, img: np.ndarray) -> List["FacialAreaRegion"]: + """ + Detect and align face with CenterFace + + Args: + img (np.ndarray): pre-loaded image as numpy array + + Returns: + results (List[FacialAreaRegion]): A list of FacialAreaRegion objects + """ + resp = [] + + threshold = float(os.getenv("CENTERFACE_THRESHOLD", "0.80")) + + # BUG: model causes problematic results from 2nd call if it is not flushed + # detections, landmarks = self.model.forward( + # img, img.shape[0], img.shape[1], threshold=threshold + # ) + detections, landmarks = self.build_model().forward( + img, img.shape[0], img.shape[1], threshold=threshold + ) + + for i, detection in enumerate(detections): + boxes, confidence = detection[:4], detection[4] + + x = boxes[0] + y = boxes[1] + w = boxes[2] - x + h = boxes[3] - y + + landmark = landmarks[i] + + right_eye = (int(landmark[0]), int(landmark[1])) + left_eye = (int(landmark[2]), int(landmark[3])) + # nose = (int(landmark[4]), int(landmark [5])) + # mouth_right = (int(landmark[6]), int(landmark [7])) + # mouth_left = (int(landmark[8]), int(landmark [9])) + + facial_area = FacialAreaRegion( + x=int(x), + y=int(y), + w=int(w), + h=int(h), + left_eye=left_eye, + right_eye=right_eye, + confidence=min(max(0, float(confidence)), 1.0), + ) + resp.append(facial_area) + + return resp + + +class CenterFace: + """ + This class is heavily inspired from + github.com/Star-Clouds/CenterFace/blob/master/prj-python/centerface.py + """ + + def __init__(self, weight_path: str): + self.net = cv2.dnn.readNetFromONNX(weight_path) + self.img_h_new, self.img_w_new, self.scale_h, self.scale_w = 0, 0, 0, 0 + + def forward(self, img, height, width, threshold=0.5): + self.img_h_new, self.img_w_new, self.scale_h, self.scale_w = self.transform(height, width) + return self.inference_opencv(img, threshold) + + def inference_opencv(self, img, threshold): + blob = cv2.dnn.blobFromImage( + img, + scalefactor=1.0, + size=(self.img_w_new, self.img_h_new), + mean=(0, 0, 0), + swapRB=True, + crop=False, + ) + self.net.setInput(blob) + heatmap, scale, offset, lms = self.net.forward(["537", "538", "539", "540"]) + return self.postprocess(heatmap, lms, offset, scale, threshold) + + def transform(self, h, w): + img_h_new, img_w_new = int(np.ceil(h / 32) * 32), int(np.ceil(w / 32) * 32) + scale_h, scale_w = img_h_new / h, img_w_new / w + return img_h_new, img_w_new, scale_h, scale_w + + def postprocess(self, heatmap, lms, offset, scale, threshold): + dets, lms = self.decode( + heatmap, scale, offset, lms, (self.img_h_new, self.img_w_new), threshold=threshold + ) + if len(dets) > 0: + dets[:, 0:4:2], dets[:, 1:4:2] = ( + dets[:, 0:4:2] / self.scale_w, + dets[:, 1:4:2] / self.scale_h, + ) + lms[:, 0:10:2], lms[:, 1:10:2] = ( + lms[:, 0:10:2] / self.scale_w, + lms[:, 1:10:2] / self.scale_h, + ) + else: + dets = np.empty(shape=[0, 5], dtype=np.float32) + lms = np.empty(shape=[0, 10], dtype=np.float32) + return dets, lms + + def decode(self, heatmap, scale, offset, landmark, size, threshold=0.1): + heatmap = np.squeeze(heatmap) + scale0, scale1 = scale[0, 0, :, :], scale[0, 1, :, :] + offset0, offset1 = offset[0, 0, :, :], offset[0, 1, :, :] + c0, c1 = np.where(heatmap > threshold) + boxes, lms = [], [] + if len(c0) > 0: + # pylint:disable=consider-using-enumerate + for i in range(len(c0)): + s0, s1 = np.exp(scale0[c0[i], c1[i]]) * 4, np.exp(scale1[c0[i], c1[i]]) * 4 + o0, o1 = offset0[c0[i], c1[i]], offset1[c0[i], c1[i]] + s = heatmap[c0[i], c1[i]] + x1, y1 = max(0, (c1[i] + o1 + 0.5) * 4 - s1 / 2), max( + 0, (c0[i] + o0 + 0.5) * 4 - s0 / 2 + ) + x1, y1 = min(x1, size[1]), min(y1, size[0]) + boxes.append([x1, y1, min(x1 + s1, size[1]), min(y1 + s0, size[0]), s]) + lm = [] + for j in range(5): + lm.append(landmark[0, j * 2 + 1, c0[i], c1[i]] * s1 + x1) + lm.append(landmark[0, j * 2, c0[i], c1[i]] * s0 + y1) + lms.append(lm) + boxes = np.asarray(boxes, dtype=np.float32) + keep = self.nms(boxes[:, :4], boxes[:, 4], 0.3) + boxes = boxes[keep, :] + lms = np.asarray(lms, dtype=np.float32) + lms = lms[keep, :] + return boxes, lms + + def nms(self, boxes, scores, nms_thresh): + x1 = boxes[:, 0] + y1 = boxes[:, 1] + x2 = boxes[:, 2] + y2 = boxes[:, 3] + areas = (x2 - x1 + 1) * (y2 - y1 + 1) + order = np.argsort(scores)[::-1] + num_detections = boxes.shape[0] + suppressed = np.zeros((num_detections,), dtype=bool) + + keep = [] + for _i in range(num_detections): + i = order[_i] + if suppressed[i]: + continue + keep.append(i) + + ix1 = x1[i] + iy1 = y1[i] + ix2 = x2[i] + iy2 = y2[i] + iarea = areas[i] + + for _j in range(_i + 1, num_detections): + j = order[_j] + if suppressed[j]: + continue + + xx1 = max(ix1, x1[j]) + yy1 = max(iy1, y1[j]) + xx2 = min(ix2, x2[j]) + yy2 = min(iy2, y2[j]) + w = max(0, xx2 - xx1 + 1) + h = max(0, yy2 - yy1 + 1) + + inter = w * h + ovr = inter / (iarea + areas[j] - inter) + if ovr >= nms_thresh: + suppressed[j] = True + + return keep diff --git a/detectors/DetectorWrapper.py b/detectors/DetectorWrapper.py new file mode 100644 index 0000000000000000000000000000000000000000..9a5c11b21a7537f4ee734def884f975fddde7920 --- /dev/null +++ b/detectors/DetectorWrapper.py @@ -0,0 +1,204 @@ +from typing import Any, List, Tuple +import numpy as np +from deepface.modules import detection +from deepface.models.Detector import Detector, DetectedFace, FacialAreaRegion +from deepface.detectors import ( + FastMtCnn, + MediaPipe, + MtCnn, + OpenCv, + Dlib, + RetinaFace, + Ssd, + Yolo, + YuNet, + CenterFace, +) +from deepface.commons import logger as log + +logger = log.get_singletonish_logger() + + +def build_model(detector_backend: str) -> Any: + """ + Build a face detector model + Args: + detector_backend (str): backend detector name + Returns: + built detector (Any) + """ + global face_detector_obj # singleton design pattern + + backends = { + "opencv": OpenCv.OpenCvClient, + "mtcnn": MtCnn.MtCnnClient, + "ssd": Ssd.SsdClient, + "dlib": Dlib.DlibClient, + "retinaface": RetinaFace.RetinaFaceClient, + "mediapipe": MediaPipe.MediaPipeClient, + "yolov8": Yolo.YoloClient, + "yunet": YuNet.YuNetClient, + "fastmtcnn": FastMtCnn.FastMtCnnClient, + "centerface": CenterFace.CenterFaceClient, + } + + if not "face_detector_obj" in globals(): + face_detector_obj = {} + + built_models = list(face_detector_obj.keys()) + if detector_backend not in built_models: + face_detector = backends.get(detector_backend) + + if face_detector: + face_detector = face_detector() + face_detector_obj[detector_backend] = face_detector + else: + raise ValueError("invalid detector_backend passed - " + detector_backend) + + return face_detector_obj[detector_backend] + + +def detect_faces( + detector_backend: str, img: np.ndarray, align: bool = True, expand_percentage: int = 0 +) -> List[DetectedFace]: + """ + Detect face(s) from a given image + Args: + detector_backend (str): detector name + + img (np.ndarray): pre-loaded image + + align (bool): enable or disable alignment after detection + + expand_percentage (int): expand detected facial area with a percentage (default is 0). + + Returns: + results (List[DetectedFace]): A list of DetectedFace objects + where each object contains: + + - img (np.ndarray): The detected face as a NumPy array. + + - facial_area (FacialAreaRegion): The facial area region represented as x, y, w, h, + left_eye and right eye. left eye and right eye are eyes on the left and right + with respect to the person instead of observer. + + - confidence (float): The confidence score associated with the detected face. + """ + face_detector: Detector = build_model(detector_backend) + + # validate expand percentage score + if expand_percentage < 0: + logger.warn( + f"Expand percentage cannot be negative but you set it to {expand_percentage}." + "Overwritten it to 0." + ) + expand_percentage = 0 + + # find facial areas of given image + facial_areas = face_detector.detect_faces(img) + + results = [] + for facial_area in facial_areas: + x = facial_area.x + y = facial_area.y + w = facial_area.w + h = facial_area.h + left_eye = facial_area.left_eye + right_eye = facial_area.right_eye + confidence = facial_area.confidence + + if expand_percentage > 0: + # Expand the facial region height and width by the provided percentage + # ensuring that the expanded region stays within img.shape limits + expanded_w = w + int(w * expand_percentage / 100) + expanded_h = h + int(h * expand_percentage / 100) + + x = max(0, x - int((expanded_w - w) / 2)) + y = max(0, y - int((expanded_h - h) / 2)) + w = min(img.shape[1] - x, expanded_w) + h = min(img.shape[0] - y, expanded_h) + + # extract detected face unaligned + detected_face = img[int(y) : int(y + h), int(x) : int(x + w)] + + # align original image, then find projection of detected face area after alignment + if align is True: # and left_eye is not None and right_eye is not None: + aligned_img, angle = detection.align_face( + img=img, left_eye=left_eye, right_eye=right_eye + ) + rotated_x1, rotated_y1, rotated_x2, rotated_y2 = rotate_facial_area( + facial_area=(x, y, x + w, y + h), angle=angle, size=(img.shape[0], img.shape[1]) + ) + detected_face = aligned_img[ + int(rotated_y1) : int(rotated_y2), int(rotated_x1) : int(rotated_x2) + ] + + result = 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 + ), + confidence=confidence, + ) + results.append(result) + return results + + +def rotate_facial_area( + facial_area: Tuple[int, int, int, int], angle: float, size: Tuple[int, int] +) -> Tuple[int, int, int, int]: + """ + Rotate the facial area around its center. + Inspried from the work of @UmutDeniz26 - github.com/serengil/retinaface/pull/80 + + Args: + facial_area (tuple of int): Representing the (x1, y1, x2, y2) of the facial area. + x2 is equal to x1 + w1, and y2 is equal to y1 + h1 + angle (float): Angle of rotation in degrees. Its sign determines the direction of rotation. + Note that angles > 360 degrees are normalized to the range [0, 360). + size (tuple of int): Tuple representing the size of the image (width, height). + + Returns: + rotated_coordinates (tuple of int): Representing the new coordinates + (x1, y1, x2, y2) or (x1, y1, x1+w1, y1+h1) of the rotated facial area. + """ + + # Normalize the witdh of the angle so we don't have to + # worry about rotations greater than 360 degrees. + # We workaround the quirky behavior of the modulo operator + # for negative angle values. + direction = 1 if angle >= 0 else -1 + angle = abs(angle) % 360 + if angle == 0: + return facial_area + + # Angle in radians + angle = angle * np.pi / 180 + + height, weight = size + + # Translate the facial area to the center of the image + x = (facial_area[0] + facial_area[2]) / 2 - weight / 2 + y = (facial_area[1] + facial_area[3]) / 2 - height / 2 + + # Rotate the facial area + x_new = x * np.cos(angle) + y * direction * np.sin(angle) + y_new = -x * direction * np.sin(angle) + y * np.cos(angle) + + # Translate the facial area back to the original position + x_new = x_new + weight / 2 + y_new = y_new + height / 2 + + # Calculate projected coordinates after alignment + x1 = x_new - (facial_area[2] - facial_area[0]) / 2 + y1 = y_new - (facial_area[3] - facial_area[1]) / 2 + x2 = x_new + (facial_area[2] - facial_area[0]) / 2 + y2 = y_new + (facial_area[3] - facial_area[1]) / 2 + + # validate projected coordinates are in image's boundaries + x1 = max(int(x1), 0) + y1 = max(int(y1), 0) + x2 = min(int(x2), weight) + y2 = min(int(y2), height) + + return (x1, y1, x2, y2) diff --git a/detectors/Dlib.py b/detectors/Dlib.py new file mode 100644 index 0000000000000000000000000000000000000000..0699efbb3f4e5ab7c6d86fb3c59dc2b567f5246f --- /dev/null +++ b/detectors/Dlib.py @@ -0,0 +1,114 @@ +from typing import List +import os +import bz2 +import gdown +import numpy as np +from deepface.commons import folder_utils +from deepface.models.Detector import Detector, FacialAreaRegion +from deepface.commons import logger as log + +logger = log.get_singletonish_logger() + + +class DlibClient(Detector): + def __init__(self): + self.model = self.build_model() + + def build_model(self) -> dict: + """ + Build a dlib hog face detector model + Returns: + model (Any) + """ + home = folder_utils.get_deepface_home() + + # this is not a must dependency. do not import it in the global level. + try: + import dlib + except ModuleNotFoundError as e: + raise ImportError( + "Dlib is an optional detector, ensure the library is installed." + "Please install using 'pip install dlib' " + ) from e + + # check required file exists in the home/.deepface/weights folder + if os.path.isfile(home + "/.deepface/weights/shape_predictor_5_face_landmarks.dat") != True: + + file_name = "shape_predictor_5_face_landmarks.dat.bz2" + logger.info(f"{file_name} is going to be downloaded") + + url = f"http://dlib.net/files/{file_name}" + output = f"{home}/.deepface/weights/{file_name}" + + gdown.download(url, output, quiet=False) + + zipfile = bz2.BZ2File(output) + data = zipfile.read() + newfilepath = output[:-4] # discard .bz2 extension + with open(newfilepath, "wb") as f: + f.write(data) + + face_detector = dlib.get_frontal_face_detector() + sp = dlib.shape_predictor(home + "/.deepface/weights/shape_predictor_5_face_landmarks.dat") + + detector = {} + detector["face_detector"] = face_detector + detector["sp"] = sp + return detector + + def detect_faces(self, img: np.ndarray) -> List[FacialAreaRegion]: + """ + Detect and align face with dlib + + Args: + img (np.ndarray): pre-loaded image as numpy array + + Returns: + results (List[FacialAreaRegion]): A list of FacialAreaRegion objects + """ + resp = [] + + face_detector = self.model["face_detector"] + + # note that, by design, dlib's fhog face detector scores are >0 but not capped at 1 + detections, scores, _ = face_detector.run(img, 1) + + if len(detections) > 0: + + for idx, detection in enumerate(detections): + left = detection.left() + right = detection.right() + top = detection.top() + bottom = detection.bottom() + + y = int(max(0, top)) + h = int(min(bottom, img.shape[0]) - y) + x = int(max(0, left)) + w = int(min(right, img.shape[1]) - x) + + shape = self.model["sp"](img, detection) + + right_eye = ( + int((shape.part(2).x + shape.part(3).x) // 2), + int((shape.part(2).y + shape.part(3).y) // 2), + ) + left_eye = ( + int((shape.part(0).x + shape.part(1).x) // 2), + int((shape.part(0).y + shape.part(1).y) // 2), + ) + + # never saw confidence higher than +3.5 github.com/davisking/dlib/issues/761 + confidence = scores[idx] + + facial_area = FacialAreaRegion( + x=x, + y=y, + w=w, + h=h, + left_eye=left_eye, + right_eye=right_eye, + confidence=min(max(0, confidence), 1.0), + ) + resp.append(facial_area) + + return resp diff --git a/detectors/FastMtCnn.py b/detectors/FastMtCnn.py new file mode 100644 index 0000000000000000000000000000000000000000..ee8e69c124f87b2f801c9ec0c89d334a1edfd8f5 --- /dev/null +++ b/detectors/FastMtCnn.py @@ -0,0 +1,89 @@ +from typing import Any, Union, List +import cv2 +import numpy as np +from deepface.models.Detector import Detector, FacialAreaRegion + +# Link -> https://github.com/timesler/facenet-pytorch +# Examples https://www.kaggle.com/timesler/guide-to-mtcnn-in-facenet-pytorch + + +class FastMtCnnClient(Detector): + def __init__(self): + self.model = self.build_model() + + def detect_faces(self, img: np.ndarray) -> List[FacialAreaRegion]: + """ + Detect and align face with mtcnn + + Args: + img (np.ndarray): pre-loaded image as numpy array + + Returns: + results (List[FacialAreaRegion]): A list of FacialAreaRegion objects + """ + resp = [] + + img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # mtcnn expects RGB but OpenCV read BGR + detections = self.model.detect( + img_rgb, landmarks=True + ) # returns boundingbox, prob, landmark + if ( + detections is not None + and len(detections) > 0 + and not any(detection is None for detection in detections) # issue 1043 + ): + for regions, confidence, eyes in zip(*detections): + x, y, w, h = xyxy_to_xywh(regions) + right_eye = eyes[0] + left_eye = eyes[1] + + left_eye = tuple(int(i) for i in left_eye) + right_eye = tuple(int(i) for i in right_eye) + + facial_area = FacialAreaRegion( + x=x, + y=y, + w=w, + h=h, + left_eye=left_eye, + right_eye=right_eye, + confidence=confidence, + ) + resp.append(facial_area) + + return resp + + def build_model(self) -> Any: + """ + Build a fast mtcnn face detector model + Returns: + model (Any) + """ + # this is not a must dependency. do not import it in the global level. + try: + from facenet_pytorch import MTCNN as fast_mtcnn + import torch + except ModuleNotFoundError as e: + raise ImportError( + "FastMtcnn is an optional detector, ensure the library is installed." + "Please install using 'pip install facenet-pytorch' " + ) from e + + device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu') + face_detector = fast_mtcnn(device=device) + + return face_detector + + +def xyxy_to_xywh(regions: Union[list, tuple]) -> tuple: + """ + Convert (x1, y1, x2, y2) format to (x, y, w, h) format. + Args: + regions (list or tuple): facial area coordinates as x, y, x+w, y+h + Returns: + regions (tuple): facial area coordinates as x, y, w, h + """ + x, y, x_plus_w, y_plus_h = regions[0], regions[1], regions[2], regions[3] + w = x_plus_w - x + h = y_plus_h - y + return (x, y, w, h) diff --git a/detectors/MediaPipe.py b/detectors/MediaPipe.py new file mode 100644 index 0000000000000000000000000000000000000000..099b0d40b158a28d5f0091136e1959d54ff18470 --- /dev/null +++ b/detectors/MediaPipe.py @@ -0,0 +1,76 @@ +from typing import Any, List +import numpy as np +from deepface.models.Detector import Detector, FacialAreaRegion + +# Link - https://google.github.io/mediapipe/solutions/face_detection + + +class MediaPipeClient(Detector): + def __init__(self): + self.model = self.build_model() + + def build_model(self) -> Any: + """ + Build a mediapipe face detector model + Returns: + model (Any) + """ + # this is not a must dependency. do not import it in the global level. + try: + import mediapipe as mp + except ModuleNotFoundError as e: + raise ImportError( + "MediaPipe is an optional detector, ensure the library is installed." + "Please install using 'pip install mediapipe' " + ) from e + + mp_face_detection = mp.solutions.face_detection + face_detection = mp_face_detection.FaceDetection(min_detection_confidence=0.7) + return face_detection + + def detect_faces(self, img: np.ndarray) -> List[FacialAreaRegion]: + """ + Detect and align face with mediapipe + + Args: + img (np.ndarray): pre-loaded image as numpy array + + Returns: + results (List[FacialAreaRegion]): A list of FacialAreaRegion objects + """ + resp = [] + + img_width = img.shape[1] + img_height = img.shape[0] + + results = self.model.process(img) + + # If no face has been detected, return an empty list + if results.detections is None: + return resp + + # Extract the bounding box, the landmarks and the confidence score + for current_detection in results.detections: + (confidence,) = current_detection.score + + bounding_box = current_detection.location_data.relative_bounding_box + landmarks = current_detection.location_data.relative_keypoints + + x = int(bounding_box.xmin * img_width) + w = int(bounding_box.width * img_width) + y = int(bounding_box.ymin * img_height) + h = int(bounding_box.height * img_height) + + right_eye = (int(landmarks[0].x * img_width), int(landmarks[0].y * img_height)) + left_eye = (int(landmarks[1].x * img_width), int(landmarks[1].y * img_height)) + # nose = (int(landmarks[2].x * img_width), int(landmarks[2].y * img_height)) + # mouth = (int(landmarks[3].x * img_width), int(landmarks[3].y * img_height)) + # right_ear = (int(landmarks[4].x * img_width), int(landmarks[4].y * img_height)) + # left_ear = (int(landmarks[5].x * img_width), int(landmarks[5].y * img_height)) + + facial_area = FacialAreaRegion( + x=x, y=y, w=w, h=h, left_eye=left_eye, right_eye=right_eye, confidence=confidence + ) + resp.append(facial_area) + + return resp diff --git a/detectors/MtCnn.py b/detectors/MtCnn.py new file mode 100644 index 0000000000000000000000000000000000000000..527c9e5bf17078603b1d68051f217f461c364b5b --- /dev/null +++ b/detectors/MtCnn.py @@ -0,0 +1,55 @@ +from typing import List +import numpy as np +from mtcnn import MTCNN +from deepface.models.Detector import Detector, FacialAreaRegion + +# pylint: disable=too-few-public-methods +class MtCnnClient(Detector): + """ + Class to cover common face detection functionalitiy for MtCnn backend + """ + + def __init__(self): + self.model = MTCNN() + + def detect_faces(self, img: np.ndarray) -> List[FacialAreaRegion]: + """ + Detect and align face with mtcnn + + Args: + img (np.ndarray): pre-loaded image as numpy array + + Returns: + results (List[FacialAreaRegion]): A list of FacialAreaRegion objects + """ + + resp = [] + + # mtcnn expects RGB but OpenCV read BGR + # img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + img_rgb = img[:, :, ::-1] + detections = self.model.detect_faces(img_rgb) + + if detections is not None and len(detections) > 0: + + for current_detection in detections: + x, y, w, h = current_detection["box"] + confidence = current_detection["confidence"] + # mtcnn detector assigns left eye with respect to the observer + # but we are setting it with respect to the person itself + left_eye = current_detection["keypoints"]["right_eye"] + right_eye = current_detection["keypoints"]["left_eye"] + + facial_area = FacialAreaRegion( + x=x, + y=y, + w=w, + h=h, + left_eye=left_eye, + right_eye=right_eye, + confidence=confidence, + ) + + resp.append(facial_area) + + return resp diff --git a/detectors/OpenCv.py b/detectors/OpenCv.py new file mode 100644 index 0000000000000000000000000000000000000000..9b59d7da206f270a5e93428ba423845198c26e49 --- /dev/null +++ b/detectors/OpenCv.py @@ -0,0 +1,178 @@ +import os +from typing import Any, List +import cv2 +import numpy as np +from deepface.models.Detector import Detector, FacialAreaRegion + + +class OpenCvClient(Detector): + """ + Class to cover common face detection functionalitiy for OpenCv backend + """ + + def __init__(self): + self.model = self.build_model() + + def build_model(self): + """ + Build opencv's face and eye detector models + Returns: + model (dict): including face_detector and eye_detector keys + """ + detector = {} + detector["face_detector"] = self.__build_cascade("haarcascade") + detector["eye_detector"] = self.__build_cascade("haarcascade_eye") + return detector + + def detect_faces(self, img: np.ndarray) -> List[FacialAreaRegion]: + """ + Detect and align face with opencv + + Args: + img (np.ndarray): pre-loaded image as numpy array + + Returns: + results (List[FacialAreaRegion]): A list of FacialAreaRegion objects + """ + resp = [] + + detected_face = None + + faces = [] + try: + # faces = detector["face_detector"].detectMultiScale(img, 1.3, 5) + + # note that, by design, opencv's haarcascade scores are >0 but not capped at 1 + faces, _, scores = self.model["face_detector"].detectMultiScale3( + img, 1.1, 10, outputRejectLevels=True + ) + except: + pass + + if len(faces) > 0: + for (x, y, w, h), confidence in zip(faces, scores): + detected_face = img[int(y) : int(y + h), int(x) : int(x + w)] + left_eye, right_eye = self.find_eyes(img=detected_face) + + # eyes found in the detected face instead image itself + # detected face's coordinates should be added + if left_eye is not None: + left_eye = (int(x + left_eye[0]), int(y + left_eye[1])) + if right_eye is not None: + right_eye = (int(x + right_eye[0]), int(y + right_eye[1])) + + facial_area = FacialAreaRegion( + x=x, + y=y, + w=w, + h=h, + left_eye=left_eye, + right_eye=right_eye, + confidence=(100 - confidence) / 100, + ) + resp.append(facial_area) + + return resp + + def find_eyes(self, img: np.ndarray) -> tuple: + """ + Find the left and right eye coordinates of given image + Args: + img (np.ndarray): given image + Returns: + left and right eye (tuple) + """ + left_eye = None + right_eye = None + + # if image has unexpectedly 0 dimension then skip alignment + if img.shape[0] == 0 or img.shape[1] == 0: + return left_eye, right_eye + + detected_face_gray = cv2.cvtColor( + img, cv2.COLOR_BGR2GRAY + ) # eye detector expects gray scale image + + eyes = self.model["eye_detector"].detectMultiScale(detected_face_gray, 1.1, 10) + + # ---------------------------------------------------------------- + + # opencv eye detection module is not strong. it might find more than 2 eyes! + # besides, it returns eyes with different order in each call (issue 435) + # this is an important issue because opencv is the default detector and ssd also uses this + # find the largest 2 eye. Thanks to @thelostpeace + + eyes = sorted(eyes, key=lambda v: abs(v[2] * v[3]), reverse=True) + + # ---------------------------------------------------------------- + if len(eyes) >= 2: + # decide left and right eye + + eye_1 = eyes[0] + eye_2 = eyes[1] + + if eye_1[0] < eye_2[0]: + right_eye = eye_1 + left_eye = eye_2 + else: + right_eye = eye_2 + left_eye = eye_1 + + # ----------------------- + # find center of eyes + left_eye = ( + int(left_eye[0] + (left_eye[2] / 2)), + int(left_eye[1] + (left_eye[3] / 2)), + ) + right_eye = ( + int(right_eye[0] + (right_eye[2] / 2)), + int(right_eye[1] + (right_eye[3] / 2)), + ) + return left_eye, right_eye + + def __build_cascade(self, model_name="haarcascade") -> Any: + """ + Build a opencv face&eye detector models + Returns: + model (Any) + """ + opencv_path = self.__get_opencv_path() + if model_name == "haarcascade": + face_detector_path = opencv_path + "haarcascade_frontalface_default.xml" + if os.path.isfile(face_detector_path) != True: + raise ValueError( + "Confirm that opencv is installed on your environment! Expected path ", + face_detector_path, + " violated.", + ) + detector = cv2.CascadeClassifier(face_detector_path) + + elif model_name == "haarcascade_eye": + eye_detector_path = opencv_path + "haarcascade_eye.xml" + if os.path.isfile(eye_detector_path) != True: + raise ValueError( + "Confirm that opencv is installed on your environment! Expected path ", + eye_detector_path, + " violated.", + ) + detector = cv2.CascadeClassifier(eye_detector_path) + + else: + raise ValueError(f"unimplemented model_name for build_cascade - {model_name}") + + return detector + + def __get_opencv_path(self) -> str: + """ + Returns where opencv installed + Returns: + installation_path (str) + """ + opencv_home = cv2.__file__ + folders = opencv_home.split(os.path.sep)[0:-1] + + path = folders[0] + for folder in folders[1:]: + path = path + "/" + folder + + return path + "/data/" diff --git a/detectors/RetinaFace.py b/detectors/RetinaFace.py new file mode 100644 index 0000000000000000000000000000000000000000..a21a7931837f1099650bb440c7db1d82dea6629b --- /dev/null +++ b/detectors/RetinaFace.py @@ -0,0 +1,59 @@ +from typing import List +import numpy as np +from retinaface import RetinaFace as rf +from deepface.models.Detector import Detector, FacialAreaRegion + +# pylint: disable=too-few-public-methods +class RetinaFaceClient(Detector): + def __init__(self): + self.model = rf.build_model() + + def detect_faces(self, img: np.ndarray) -> List[FacialAreaRegion]: + """ + Detect and align face with retinaface + + Args: + img (np.ndarray): pre-loaded image as numpy array + + Returns: + results (List[FacialAreaRegion]): A list of FacialAreaRegion objects + """ + resp = [] + + obj = rf.detect_faces(img, model=self.model, threshold=0.9) + + if not isinstance(obj, dict): + return resp + + for face_idx in obj.keys(): + identity = obj[face_idx] + detection = identity["facial_area"] + + y = detection[1] + h = detection[3] - y + x = detection[0] + w = detection[2] - x + + # retinaface sets left and right eyes with respect to the person + left_eye = identity["landmarks"]["left_eye"] + right_eye = identity["landmarks"]["right_eye"] + + # 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) + + confidence = identity["score"] + + facial_area = FacialAreaRegion( + x=x, + y=y, + w=w, + h=h, + left_eye=left_eye, + right_eye=right_eye, + confidence=confidence, + ) + + resp.append(facial_area) + + return resp diff --git a/detectors/Ssd.py b/detectors/Ssd.py new file mode 100644 index 0000000000000000000000000000000000000000..a8d68eb0e0c57dab8b370e5b9bbbb409de256262 --- /dev/null +++ b/detectors/Ssd.py @@ -0,0 +1,153 @@ +from typing import List +import os +import gdown +import cv2 +import pandas as pd +import numpy as np +from deepface.detectors import OpenCv +from deepface.commons import folder_utils +from deepface.models.Detector import Detector, FacialAreaRegion +from deepface.commons import logger as log + +logger = log.get_singletonish_logger() + +# pylint: disable=line-too-long, c-extension-no-member + + +class SsdClient(Detector): + def __init__(self): + self.model = self.build_model() + + def build_model(self) -> dict: + """ + Build a ssd detector model + Returns: + model (dict) + """ + + home = folder_utils.get_deepface_home() + + # model structure + if os.path.isfile(home + "/.deepface/weights/deploy.prototxt") != True: + + logger.info("deploy.prototxt will be downloaded...") + + url = "https://github.com/opencv/opencv/raw/3.4.0/samples/dnn/face_detector/deploy.prototxt" + + output = home + "/.deepface/weights/deploy.prototxt" + + gdown.download(url, output, quiet=False) + + # pre-trained weights + if ( + os.path.isfile(home + "/.deepface/weights/res10_300x300_ssd_iter_140000.caffemodel") + != True + ): + + logger.info("res10_300x300_ssd_iter_140000.caffemodel will be downloaded...") + + url = "https://github.com/opencv/opencv_3rdparty/raw/dnn_samples_face_detector_20170830/res10_300x300_ssd_iter_140000.caffemodel" + + output = home + "/.deepface/weights/res10_300x300_ssd_iter_140000.caffemodel" + + gdown.download(url, output, quiet=False) + + try: + face_detector = cv2.dnn.readNetFromCaffe( + home + "/.deepface/weights/deploy.prototxt", + home + "/.deepface/weights/res10_300x300_ssd_iter_140000.caffemodel", + ) + except Exception as err: + raise ValueError( + "Exception while calling opencv.dnn module." + + "This is an optional dependency." + + "You can install it as pip install opencv-contrib-python." + ) from err + + detector = {} + detector["face_detector"] = face_detector + detector["opencv_module"] = OpenCv.OpenCvClient() + + return detector + + def detect_faces(self, img: np.ndarray) -> List[FacialAreaRegion]: + """ + Detect and align face with ssd + + Args: + img (np.ndarray): pre-loaded image as numpy array + + Returns: + results (List[FacialAreaRegion]): A list of FacialAreaRegion objects + """ + opencv_module: OpenCv.OpenCvClient = self.model["opencv_module"] + + resp = [] + + detected_face = None + + ssd_labels = ["img_id", "is_face", "confidence", "left", "top", "right", "bottom"] + + target_size = (300, 300) + + original_size = img.shape + + current_img = cv2.resize(img, target_size) + + aspect_ratio_x = original_size[1] / target_size[1] + aspect_ratio_y = original_size[0] / target_size[0] + + imageBlob = cv2.dnn.blobFromImage(image=current_img) + + face_detector = self.model["face_detector"] + face_detector.setInput(imageBlob) + detections = face_detector.forward() + + detections_df = pd.DataFrame(detections[0][0], columns=ssd_labels) + + detections_df = detections_df[detections_df["is_face"] == 1] # 0: background, 1: face + detections_df = detections_df[detections_df["confidence"] >= 0.90] + + detections_df["left"] = (detections_df["left"] * 300).astype(int) + detections_df["bottom"] = (detections_df["bottom"] * 300).astype(int) + detections_df["right"] = (detections_df["right"] * 300).astype(int) + detections_df["top"] = (detections_df["top"] * 300).astype(int) + + if detections_df.shape[0] > 0: + + for _, instance in detections_df.iterrows(): + + left = instance["left"] + right = instance["right"] + bottom = instance["bottom"] + top = instance["top"] + confidence = instance["confidence"] + + x = int(left * aspect_ratio_x) + y = int(top * aspect_ratio_y) + w = int(right * aspect_ratio_x) - int(left * aspect_ratio_x) + h = int(bottom * aspect_ratio_y) - int(top * aspect_ratio_y) + + detected_face = img[int(y) : int(y + h), int(x) : int(x + w)] + + left_eye, right_eye = opencv_module.find_eyes(detected_face) + + # eyes found in the detected face instead image itself + # detected face's coordinates should be added + if left_eye is not None: + left_eye = (int(x + left_eye[0]), int(y + left_eye[1])) + if right_eye is not None: + right_eye = (int(x + right_eye[0]), int(y + right_eye[1])) + + facial_area = FacialAreaRegion( + x=x, + y=y, + w=w, + h=h, + left_eye=left_eye, + right_eye=right_eye, + confidence=confidence, + ) + resp.append(facial_area) + + return resp diff --git a/detectors/Yolo.py b/detectors/Yolo.py new file mode 100644 index 0000000000000000000000000000000000000000..5ff1c8079b7b1453573564889c74bec5131f7110 --- /dev/null +++ b/detectors/Yolo.py @@ -0,0 +1,101 @@ +import os +from typing import Any, List +import numpy as np +import gdown +from deepface.models.Detector import Detector, FacialAreaRegion +from deepface.commons import folder_utils +from deepface.commons import logger as log + +logger = log.get_singletonish_logger() + +# Model's weights paths +PATH = "/.deepface/weights/yolov8n-face.pt" + +# Google Drive URL from repo (https://github.com/derronqi/yolov8-face) ~6MB +WEIGHT_URL = "https://drive.google.com/uc?id=1qcr9DbgsX3ryrz2uU8w4Xm3cOrRywXqb" + + +class YoloClient(Detector): + def __init__(self): + self.model = self.build_model() + + def build_model(self) -> Any: + """ + Build a yolo detector model + Returns: + model (Any) + """ + + # Import the Ultralytics YOLO model + try: + from ultralytics import YOLO + except ModuleNotFoundError as e: + raise ImportError( + "Yolo is an optional detector, ensure the library is installed. \ + Please install using 'pip install ultralytics' " + ) from e + + weight_path = f"{folder_utils.get_deepface_home()}{PATH}" + + # Download the model's weights if they don't exist + if not os.path.isfile(weight_path): + logger.info(f"Downloading Yolo weights from {WEIGHT_URL} to {weight_path}...") + try: + gdown.download(WEIGHT_URL, weight_path, quiet=False) + except Exception as err: + raise ValueError( + f"Exception while downloading Yolo weights from {WEIGHT_URL}." + f"You may consider to download it to {weight_path} manually." + ) from err + logger.info(f"Yolo model is just downloaded to {os.path.basename(weight_path)}") + + # Return face_detector + return YOLO(weight_path) + + def detect_faces(self, img: np.ndarray) -> List[FacialAreaRegion]: + """ + Detect and align face with yolo + + Args: + img (np.ndarray): pre-loaded image as numpy array + + Returns: + results (List[FacialAreaRegion]): A list of FacialAreaRegion objects + """ + resp = [] + + # Detect faces + results = self.model.predict(img, verbose=False, show=False, conf=0.25)[0] + + # For each face, extract the bounding box, the landmarks and confidence + for result in results: + + if result.boxes is None or result.keypoints is None: + continue + + # Extract the bounding box and the confidence + x, y, w, h = result.boxes.xywh.tolist()[0] + confidence = result.boxes.conf.tolist()[0] + + # right_eye_conf = result.keypoints.conf[0][0] + # left_eye_conf = result.keypoints.conf[0][1] + right_eye = result.keypoints.xy[0][0].tolist() + left_eye = result.keypoints.xy[0][1].tolist() + + # 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) + + x, y, w, h = int(x - w / 2), int(y - h / 2), int(w), int(h) + facial_area = FacialAreaRegion( + x=x, + y=y, + w=w, + h=h, + left_eye=left_eye, + right_eye=right_eye, + confidence=confidence, + ) + resp.append(facial_area) + + return resp diff --git a/detectors/YuNet.py b/detectors/YuNet.py new file mode 100644 index 0000000000000000000000000000000000000000..c43b80f026641a7d0a94b8fc129ea2310604c4c0 --- /dev/null +++ b/detectors/YuNet.py @@ -0,0 +1,133 @@ +# built-in dependencies +import os +from typing import Any, List + +# 3rd party dependencies +import cv2 +import numpy as np +import gdown + +# project dependencies +from deepface.commons import folder_utils +from deepface.models.Detector import Detector, FacialAreaRegion +from deepface.commons import logger as log + +logger = log.get_singletonish_logger() + + +class YuNetClient(Detector): + def __init__(self): + self.model = self.build_model() + + def build_model(self) -> Any: + """ + Build a yunet detector model + Returns: + model (Any) + """ + + opencv_version = cv2.__version__.split(".") + if not len(opencv_version) >= 2: + raise ValueError( + f"OpenCv's version must have major and minor values but it is {opencv_version}" + ) + + opencv_version_major = int(opencv_version[0]) + opencv_version_minor = int(opencv_version[1]) + + if opencv_version_major < 4 or (opencv_version_major == 4 and opencv_version_minor < 8): + # min requirement: https://github.com/opencv/opencv_zoo/issues/172 + raise ValueError(f"YuNet requires opencv-python >= 4.8 but you have {cv2.__version__}") + + # pylint: disable=C0301 + url = "https://github.com/opencv/opencv_zoo/raw/main/models/face_detection_yunet/face_detection_yunet_2023mar.onnx" + file_name = "face_detection_yunet_2023mar.onnx" + home = folder_utils.get_deepface_home() + if os.path.isfile(home + f"/.deepface/weights/{file_name}") is False: + logger.info(f"{file_name} will be downloaded...") + output = home + f"/.deepface/weights/{file_name}" + gdown.download(url, output, quiet=False) + + try: + face_detector = cv2.FaceDetectorYN_create( + home + f"/.deepface/weights/{file_name}", "", (0, 0) + ) + except Exception as err: + raise ValueError( + "Exception while calling opencv.FaceDetectorYN_create module." + + "This is an optional dependency." + + "You can install it as pip install opencv-contrib-python." + ) from err + return face_detector + + def detect_faces(self, img: np.ndarray) -> List[FacialAreaRegion]: + """ + Detect and align face with yunet + + Args: + img (np.ndarray): pre-loaded image as numpy array + + Returns: + results (List[FacialAreaRegion]): A list of FacialAreaRegion objects + """ + # FaceDetector.detect_faces does not support score_threshold parameter. + # We can set it via environment variable. + score_threshold = float(os.environ.get("yunet_score_threshold", "0.9")) + resp = [] + faces = [] + height, width = img.shape[0], img.shape[1] + # resize image if it is too large (Yunet fails to detect faces on large input sometimes) + # I picked 640 as a threshold because it is the default value of max_size in Yunet. + resized = False + r = 1 # resize factor + if height > 640 or width > 640: + r = 640.0 / max(height, width) + img = cv2.resize(img, (int(width * r), int(height * r))) + height, width = img.shape[0], img.shape[1] + resized = True + self.model.setInputSize((width, height)) + self.model.setScoreThreshold(score_threshold) + _, faces = self.model.detect(img) + if faces is None: + return resp + for face in faces: + # pylint: disable=W0105 + """ + The detection output faces is a two-dimension array of type CV_32F, + whose rows are the detected face instances, columns are the location + of a face and 5 facial landmarks. + The format of each row is as follows: + x1, y1, w, h, x_re, y_re, x_le, y_le, x_nt, y_nt, + x_rcm, y_rcm, x_lcm, y_lcm, + where x1, y1, w, h are the top-left coordinates, width and height of + the face bounding box, + {x, y}_{re, le, nt, rcm, lcm} stands for the coordinates of right eye, + left eye, nose tip, the right corner and left corner of the mouth respectively. + """ + (x, y, w, h, x_le, y_le, x_re, y_re) = list(map(int, face[:8])) + + # YuNet returns negative coordinates if it thinks part of the detected face + # is outside the frame. + x = max(x, 0) + y = max(y, 0) + if resized: + x, y, w, h = int(x / r), int(y / r), int(w / r), int(h / r) + x_re, y_re, x_le, y_le = ( + int(x_re / r), + int(y_re / r), + int(x_le / r), + int(y_le / r), + ) + confidence = float(face[-1]) + + facial_area = FacialAreaRegion( + x=x, + y=y, + w=w, + h=h, + confidence=confidence, + left_eye=(x_re, y_re), + right_eye=(x_le, y_le), + ) + resp.append(facial_area) + return resp diff --git a/detectors/__init__.py b/detectors/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/detectors/__pycache__/CenterFace.cpython-312.pyc b/detectors/__pycache__/CenterFace.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a322ed2382287f6ce47a4b99e7533d23f0861f71 Binary files /dev/null and b/detectors/__pycache__/CenterFace.cpython-312.pyc differ diff --git a/detectors/__pycache__/DetectorWrapper.cpython-312.pyc b/detectors/__pycache__/DetectorWrapper.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7aa184cda0659d788087d358696e7184c3b4e0a7 Binary files /dev/null and b/detectors/__pycache__/DetectorWrapper.cpython-312.pyc differ diff --git a/detectors/__pycache__/Dlib.cpython-312.pyc b/detectors/__pycache__/Dlib.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2db16a62096906c5bb697bdc3dc265305fc21256 Binary files /dev/null and b/detectors/__pycache__/Dlib.cpython-312.pyc differ diff --git a/detectors/__pycache__/FastMtCnn.cpython-312.pyc b/detectors/__pycache__/FastMtCnn.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..03f0d44c45868123b766c4cf6de25fb533477576 Binary files /dev/null and b/detectors/__pycache__/FastMtCnn.cpython-312.pyc differ diff --git a/detectors/__pycache__/MediaPipe.cpython-312.pyc b/detectors/__pycache__/MediaPipe.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2fb29f3184ed8749528f7b4c91ce60c6df078d92 Binary files /dev/null and b/detectors/__pycache__/MediaPipe.cpython-312.pyc differ diff --git a/detectors/__pycache__/MtCnn.cpython-312.pyc b/detectors/__pycache__/MtCnn.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d9ffb41c88d4b0f06f9710f134b5acdcdda79c81 Binary files /dev/null and b/detectors/__pycache__/MtCnn.cpython-312.pyc differ diff --git a/detectors/__pycache__/OpenCv.cpython-312.pyc b/detectors/__pycache__/OpenCv.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ecc629d7792e6d68a1f00c24ceca592fcce2f67b Binary files /dev/null and b/detectors/__pycache__/OpenCv.cpython-312.pyc differ diff --git a/detectors/__pycache__/RetinaFace.cpython-312.pyc b/detectors/__pycache__/RetinaFace.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..559badd3057cce3302d51f609ed7269f3cba9577 Binary files /dev/null and b/detectors/__pycache__/RetinaFace.cpython-312.pyc differ diff --git a/detectors/__pycache__/Ssd.cpython-312.pyc b/detectors/__pycache__/Ssd.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fac51ca6e4eadc6b7e0e611ca3f36e8f0d295b29 Binary files /dev/null and b/detectors/__pycache__/Ssd.cpython-312.pyc differ diff --git a/detectors/__pycache__/Yolo.cpython-312.pyc b/detectors/__pycache__/Yolo.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a49b6a7eaf056026a988cb620f046dbfafb9b98a Binary files /dev/null and b/detectors/__pycache__/Yolo.cpython-312.pyc differ diff --git a/detectors/__pycache__/YuNet.cpython-312.pyc b/detectors/__pycache__/YuNet.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a5d600ea1ad900e461ba391d0acf756386d929f9 Binary files /dev/null and b/detectors/__pycache__/YuNet.cpython-312.pyc differ diff --git a/detectors/__pycache__/__init__.cpython-312.pyc b/detectors/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..28af10659b18bb1dfe8bb76501142836e7923abd Binary files /dev/null and b/detectors/__pycache__/__init__.cpython-312.pyc differ diff --git a/extendedmodels/Age.py b/extendedmodels/Age.py new file mode 100644 index 0000000000000000000000000000000000000000..2e99995ae93c7c5215d7a7038f91782d4ca022c1 --- /dev/null +++ b/extendedmodels/Age.py @@ -0,0 +1,92 @@ +import os +import gdown +import numpy as np +from deepface.basemodels import VGGFace +from deepface.commons import package_utils, folder_utils +from deepface.models.Demography import Demography +from deepface.commons import logger as log + +logger = log.get_singletonish_logger() + +# ---------------------------------------- +# dependency configurations + +tf_version = package_utils.get_tf_major_version() + +if tf_version == 1: + from keras.models import Model, Sequential + from keras.layers import Convolution2D, Flatten, Activation +else: + from tensorflow.keras.models import Model, Sequential + from tensorflow.keras.layers import Convolution2D, Flatten, Activation + +# ---------------------------------------- + +# pylint: disable=too-few-public-methods +class ApparentAgeClient(Demography): + """ + Age model class + """ + + def __init__(self): + self.model = load_model() + self.model_name = "Age" + + def predict(self, img: np.ndarray) -> np.float64: + age_predictions = self.model.predict(img, verbose=0)[0, :] + return find_apparent_age(age_predictions) + + +def load_model( + url="https://github.com/serengil/deepface_models/releases/download/v1.0/age_model_weights.h5", +) -> Model: + """ + Construct age model, download its weights and load + Returns: + model (Model) + """ + + model = VGGFace.base_model() + + # -------------------------- + + classes = 101 + base_model_output = Sequential() + base_model_output = Convolution2D(classes, (1, 1), name="predictions")(model.layers[-4].output) + base_model_output = Flatten()(base_model_output) + base_model_output = Activation("softmax")(base_model_output) + + # -------------------------- + + age_model = Model(inputs=model.input, outputs=base_model_output) + + # -------------------------- + + # load weights + + home = folder_utils.get_deepface_home() + + if os.path.isfile(home + "/.deepface/weights/age_model_weights.h5") != True: + logger.info("age_model_weights.h5 will be downloaded...") + + output = home + "/.deepface/weights/age_model_weights.h5" + gdown.download(url, output, quiet=False) + + age_model.load_weights(home + "/.deepface/weights/age_model_weights.h5") + + return age_model + + # -------------------------- + + +def find_apparent_age(age_predictions: np.ndarray) -> np.float64: + """ + Find apparent age prediction from a given probas of ages + Args: + age_predictions (?) + Returns: + apparent_age (float) + """ + output_indexes = np.array(list(range(0, 101))) + apparent_age = np.sum(age_predictions * output_indexes) + return apparent_age diff --git a/extendedmodels/Emotion.py b/extendedmodels/Emotion.py new file mode 100644 index 0000000000000000000000000000000000000000..e0b93bff2324bc2adda67ba854c89aa974e1d416 --- /dev/null +++ b/extendedmodels/Emotion.py @@ -0,0 +1,106 @@ +# built-in dependencies +import os + +# 3rd party dependencies +import gdown +import numpy as np +import cv2 + +# project dependencies +from deepface.commons import package_utils, folder_utils +from deepface.models.Demography import Demography +from deepface.commons import logger as log + +logger = log.get_singletonish_logger() + +# ------------------------------------------- +# pylint: disable=line-too-long +# ------------------------------------------- +# dependency configuration +tf_version = package_utils.get_tf_major_version() + +if tf_version == 1: + from keras.models import Sequential + from keras.layers import Conv2D, MaxPooling2D, AveragePooling2D, Flatten, Dense, Dropout +else: + from tensorflow.keras.models import Sequential + from tensorflow.keras.layers import ( + Conv2D, + MaxPooling2D, + AveragePooling2D, + Flatten, + 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 +class EmotionClient(Demography): + """ + Emotion model class + """ + + def __init__(self): + self.model = load_model() + self.model_name = "Emotion" + + def predict(self, img: np.ndarray) -> np.ndarray: + img_gray = cv2.cvtColor(img[0], cv2.COLOR_BGR2GRAY) + img_gray = cv2.resize(img_gray, (48, 48)) + img_gray = np.expand_dims(img_gray, axis=0) + + emotion_predictions = self.model.predict(img_gray, verbose=0)[0, :] + return emotion_predictions + + +def load_model( + url="https://github.com/serengil/deepface_models/releases/download/v1.0/facial_expression_model_weights.h5", +) -> Sequential: + """ + Consruct emotion model, download and load weights + """ + + num_classes = 7 + + model = Sequential() + + # 1st convolution layer + model.add(Conv2D(64, (5, 5), activation="relu", input_shape=(48, 48, 1))) + model.add(MaxPooling2D(pool_size=(5, 5), strides=(2, 2))) + + # 2nd convolution layer + model.add(Conv2D(64, (3, 3), activation="relu")) + model.add(Conv2D(64, (3, 3), activation="relu")) + model.add(AveragePooling2D(pool_size=(3, 3), strides=(2, 2))) + + # 3rd convolution layer + model.add(Conv2D(128, (3, 3), activation="relu")) + model.add(Conv2D(128, (3, 3), activation="relu")) + model.add(AveragePooling2D(pool_size=(3, 3), strides=(2, 2))) + + model.add(Flatten()) + + # fully connected neural networks + model.add(Dense(1024, activation="relu")) + model.add(Dropout(0.2)) + model.add(Dense(1024, activation="relu")) + model.add(Dropout(0.2)) + + model.add(Dense(num_classes, activation="softmax")) + + # ---------------------------- + + home = folder_utils.get_deepface_home() + + if os.path.isfile(home + "/.deepface/weights/facial_expression_model_weights.h5") != True: + logger.info("facial_expression_model_weights.h5 will be downloaded...") + + output = home + "/.deepface/weights/facial_expression_model_weights.h5" + gdown.download(url, output, quiet=False) + + model.load_weights(home + "/.deepface/weights/facial_expression_model_weights.h5") + + return model diff --git a/extendedmodels/Gender.py b/extendedmodels/Gender.py new file mode 100644 index 0000000000000000000000000000000000000000..84cb4465eacadcebb562629c6a94b7244ef42596 --- /dev/null +++ b/extendedmodels/Gender.py @@ -0,0 +1,84 @@ +# built-in dependencies +import os + +# 3rd party dependencies +import gdown +import numpy as np + +# project dependencies +from deepface.basemodels import VGGFace +from deepface.commons import package_utils, folder_utils +from deepface.models.Demography import Demography +from deepface.commons import logger as log + +logger = log.get_singletonish_logger() + +# ------------------------------------- +# pylint: disable=line-too-long +# ------------------------------------- +# dependency configurations + +tf_version = package_utils.get_tf_major_version() +if tf_version == 1: + from keras.models import Model, Sequential + from keras.layers import Convolution2D, Flatten, Activation +else: + from tensorflow.keras.models import Model, Sequential + from tensorflow.keras.layers import Convolution2D, Flatten, Activation +# ------------------------------------- + +# Labels for the genders that can be detected by the model. +labels = ["Woman", "Man"] + +# pylint: disable=too-few-public-methods +class GenderClient(Demography): + """ + Gender model class + """ + + def __init__(self): + self.model = load_model() + self.model_name = "Gender" + + def predict(self, img: np.ndarray) -> np.ndarray: + return self.model.predict(img, verbose=0)[0, :] + + +def load_model( + url="https://github.com/serengil/deepface_models/releases/download/v1.0/gender_model_weights.h5", +) -> Model: + """ + Construct gender model, download its weights and load + Returns: + model (Model) + """ + + model = VGGFace.base_model() + + # -------------------------- + + classes = 2 + base_model_output = Sequential() + base_model_output = Convolution2D(classes, (1, 1), name="predictions")(model.layers[-4].output) + base_model_output = Flatten()(base_model_output) + base_model_output = Activation("softmax")(base_model_output) + + # -------------------------- + + gender_model = Model(inputs=model.input, outputs=base_model_output) + + # -------------------------- + + # load weights + + home = folder_utils.get_deepface_home() + + if os.path.isfile(home + "/.deepface/weights/gender_model_weights.h5") != True: + logger.info("gender_model_weights.h5 will be downloaded...") + + output = home + "/.deepface/weights/gender_model_weights.h5" + gdown.download(url, output, quiet=False) + + gender_model.load_weights(home + "/.deepface/weights/gender_model_weights.h5") + + return gender_model diff --git a/extendedmodels/Race.py b/extendedmodels/Race.py new file mode 100644 index 0000000000000000000000000000000000000000..1943dea9ebb4fae37fc86de4a541917c0b6131be --- /dev/null +++ b/extendedmodels/Race.py @@ -0,0 +1,81 @@ +# built-in dependencies +import os + +# 3rd party dependencies +import gdown +import numpy as np + +# project dependencies +from deepface.basemodels import VGGFace +from deepface.commons import package_utils, folder_utils +from deepface.models.Demography import Demography +from deepface.commons import logger as log + +logger = log.get_singletonish_logger() + +# -------------------------- +# pylint: disable=line-too-long +# -------------------------- +# dependency configurations +tf_version = package_utils.get_tf_major_version() + +if tf_version == 1: + from keras.models import Model, Sequential + from keras.layers import Convolution2D, Flatten, Activation +else: + from tensorflow.keras.models import Model, Sequential + from tensorflow.keras.layers import Convolution2D, Flatten, Activation +# -------------------------- +# Labels for the ethnic phenotypes that can be detected by the model. +labels = ["asian", "indian", "black", "white", "middle eastern", "latino hispanic"] + +# pylint: disable=too-few-public-methods +class RaceClient(Demography): + """ + Race model class + """ + + def __init__(self): + self.model = load_model() + self.model_name = "Race" + + def predict(self, img: np.ndarray) -> np.ndarray: + return self.model.predict(img, verbose=0)[0, :] + + +def load_model( + url="https://github.com/serengil/deepface_models/releases/download/v1.0/race_model_single_batch.h5", +) -> Model: + """ + Construct race model, download its weights and load + """ + + model = VGGFace.base_model() + + # -------------------------- + + classes = 6 + base_model_output = Sequential() + base_model_output = Convolution2D(classes, (1, 1), name="predictions")(model.layers[-4].output) + base_model_output = Flatten()(base_model_output) + base_model_output = Activation("softmax")(base_model_output) + + # -------------------------- + + race_model = Model(inputs=model.input, outputs=base_model_output) + + # -------------------------- + + # load weights + + home = folder_utils.get_deepface_home() + + if os.path.isfile(home + "/.deepface/weights/race_model_single_batch.h5") != True: + logger.info("race_model_single_batch.h5 will be downloaded...") + + output = home + "/.deepface/weights/race_model_single_batch.h5" + gdown.download(url, output, quiet=False) + + race_model.load_weights(home + "/.deepface/weights/race_model_single_batch.h5") + + return race_model diff --git a/extendedmodels/__init__.py b/extendedmodels/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/extendedmodels/__pycache__/Age.cpython-312.pyc b/extendedmodels/__pycache__/Age.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..55c63f3ed82d7fa82d4384f8f79f8b1c0c7db4a4 Binary files /dev/null and b/extendedmodels/__pycache__/Age.cpython-312.pyc differ diff --git a/extendedmodels/__pycache__/Emotion.cpython-312.pyc b/extendedmodels/__pycache__/Emotion.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..12baf9ab35c2ccc7b18e18f3c09d9732900a648b Binary files /dev/null and b/extendedmodels/__pycache__/Emotion.cpython-312.pyc differ diff --git a/extendedmodels/__pycache__/Gender.cpython-312.pyc b/extendedmodels/__pycache__/Gender.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7c767f14c6d1c05c5360a809c0a107287a7532f7 Binary files /dev/null and b/extendedmodels/__pycache__/Gender.cpython-312.pyc differ diff --git a/extendedmodels/__pycache__/Race.cpython-312.pyc b/extendedmodels/__pycache__/Race.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bebe66b8c639df068160ecd103dbdfda79bdff23 Binary files /dev/null and b/extendedmodels/__pycache__/Race.cpython-312.pyc differ diff --git a/extendedmodels/__pycache__/__init__.cpython-312.pyc b/extendedmodels/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8338741c319b0bbce3b28144c3079f1ba99c935f Binary files /dev/null and b/extendedmodels/__pycache__/__init__.cpython-312.pyc differ diff --git a/models/Demography.py b/models/Demography.py new file mode 100644 index 0000000000000000000000000000000000000000..ad9392029307f7539095e2d0372df1cec81ec0c0 --- /dev/null +++ b/models/Demography.py @@ -0,0 +1,22 @@ +from typing import Union +from abc import ABC, abstractmethod +import numpy as np +from deepface.commons import package_utils + +tf_version = package_utils.get_tf_major_version() +if tf_version == 1: + from keras.models import Model +else: + from tensorflow.keras.models import Model + +# Notice that all facial attribute analysis models must be inherited from this class + + +# pylint: disable=too-few-public-methods +class Demography(ABC): + model: Model + model_name: str + + @abstractmethod + def predict(self, img: np.ndarray) -> Union[np.ndarray, np.float64]: + pass diff --git a/models/Detector.py b/models/Detector.py new file mode 100644 index 0000000000000000000000000000000000000000..0a1e48d29b791cf5f0159f451a74a85bdc603432 --- /dev/null +++ b/models/Detector.py @@ -0,0 +1,89 @@ +from typing import List, Tuple, Optional +from abc import ABC, abstractmethod +import numpy as np + +# Notice that all facial detector models must be inherited from this class + + +# pylint: disable=unnecessary-pass, too-few-public-methods +class Detector(ABC): + @abstractmethod + def detect_faces(self, img: np.ndarray) -> List["FacialAreaRegion"]: + """ + Interface for detect and align face + + Args: + img (np.ndarray): pre-loaded image as numpy array + + Returns: + results (List[FacialAreaRegion]): A list of FacialAreaRegion objects + where each object contains: + + - facial_area (FacialAreaRegion): The facial area region represented + as x, y, w, h, left_eye and right_eye. left eye and right eye are + eyes on the left and right respectively with respect to the person + instead of observer. + """ + pass + + +class FacialAreaRegion: + x: int + y: int + w: int + h: int + left_eye: Tuple[int, int] + right_eye: Tuple[int, int] + confidence: float + + def __init__( + self, + x: int, + y: int, + w: int, + h: int, + left_eye: Optional[Tuple[int, int]] = None, + right_eye: Optional[Tuple[int, int]] = None, + confidence: Optional[float] = None, + ): + """ + Initialize a Face object. + + Args: + x (int): The x-coordinate of the top-left corner of the bounding box. + y (int): The y-coordinate of the top-left corner of the bounding box. + w (int): The width of the bounding box. + h (int): The height of the bounding box. + left_eye (tuple): The coordinates (x, y) of the left eye with respect to + the person instead of observer. Default is None. + right_eye (tuple): The coordinates (x, y) of the right eye with respect to + the person instead of observer. Default is None. + confidence (float, optional): Confidence score associated with the face detection. + Default is None. + """ + self.x = x + self.y = y + self.w = w + self.h = h + self.left_eye = left_eye + self.right_eye = right_eye + self.confidence = confidence + + +class DetectedFace: + img: np.ndarray + facial_area: FacialAreaRegion + confidence: float + + def __init__(self, img: np.ndarray, facial_area: FacialAreaRegion, confidence: float): + """ + Initialize detected face object. + + Args: + 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 + """ + self.img = img + self.facial_area = facial_area + self.confidence = confidence diff --git a/models/FacialRecognition.py b/models/FacialRecognition.py new file mode 100644 index 0000000000000000000000000000000000000000..a6ee7b59d3693cb63775d180a3a0089ca7b3ed53 --- /dev/null +++ b/models/FacialRecognition.py @@ -0,0 +1,29 @@ +from abc import ABC +from typing import Any, Union, List, Tuple +import numpy as np +from deepface.commons import package_utils + +tf_version = package_utils.get_tf_major_version() +if tf_version == 2: + from tensorflow.keras.models import Model +else: + from keras.models import Model + +# Notice that all facial recognition models must be inherited from this class + +# pylint: disable=too-few-public-methods +class FacialRecognition(ABC): + model: Union[Model, Any] + model_name: str + input_shape: Tuple[int, int] + output_shape: int + + def forward(self, img: np.ndarray) -> List[float]: + if not isinstance(self.model, Model): + raise ValueError( + "You must overwrite forward method if it is not a keras model," + f"but {self.model_name} not overwritten!" + ) + # model.predict causes memory issue when it is called in a for loop + # embedding = model.predict(img, verbose=0)[0].tolist() + return self.model(img, training=False).numpy()[0].tolist() diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/models/__pycache__/Demography.cpython-312.pyc b/models/__pycache__/Demography.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..af3b626079083735a56cbd3a6ca72d912264ac74 Binary files /dev/null and b/models/__pycache__/Demography.cpython-312.pyc differ diff --git a/models/__pycache__/Detector.cpython-312.pyc b/models/__pycache__/Detector.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ef7ce08a87a7ce15e64fe83fad9d67fe47818591 Binary files /dev/null and b/models/__pycache__/Detector.cpython-312.pyc differ diff --git a/models/__pycache__/FacialRecognition.cpython-312.pyc b/models/__pycache__/FacialRecognition.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8bc189c2232027e7ff27272299483eaee29c3f70 Binary files /dev/null and b/models/__pycache__/FacialRecognition.cpython-312.pyc differ diff --git a/models/__pycache__/__init__.cpython-312.pyc b/models/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9e4ccf3993d51eb67e8ce722408aaddc827c0453 Binary files /dev/null and b/models/__pycache__/__init__.cpython-312.pyc differ diff --git a/modules/__init__.py b/modules/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/modules/__pycache__/__init__.cpython-312.pyc b/modules/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..19226cb8175cb8050a724a3241862dca418cb84a Binary files /dev/null and b/modules/__pycache__/__init__.cpython-312.pyc differ diff --git a/modules/__pycache__/cloudservice.cpython-312.pyc b/modules/__pycache__/cloudservice.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..89872657e942464a835b0e4bd32ce403209385c9 Binary files /dev/null and b/modules/__pycache__/cloudservice.cpython-312.pyc differ diff --git a/modules/__pycache__/demography.cpython-312.pyc b/modules/__pycache__/demography.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..eb30ea97362f9a5f6597c6d6928f8dd49ab527ea Binary files /dev/null and b/modules/__pycache__/demography.cpython-312.pyc differ diff --git a/modules/__pycache__/detection.cpython-312.pyc b/modules/__pycache__/detection.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6c0cacc02a9129f4945ad2e97098c539ad7a57fd Binary files /dev/null and b/modules/__pycache__/detection.cpython-312.pyc differ diff --git a/modules/__pycache__/modeling.cpython-312.pyc b/modules/__pycache__/modeling.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9104ed4d1b5c28ff59ef1401caf3edcb12bdccf9 Binary files /dev/null and b/modules/__pycache__/modeling.cpython-312.pyc differ diff --git a/modules/__pycache__/preprocessing.cpython-312.pyc b/modules/__pycache__/preprocessing.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..13c64b0141c9fc2e7a2302c29f1c56fd7588fe40 Binary files /dev/null and b/modules/__pycache__/preprocessing.cpython-312.pyc differ diff --git a/modules/__pycache__/recognition.cpython-312.pyc b/modules/__pycache__/recognition.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9c35bdce084985aea650eeb04bfd4426aab1d074 Binary files /dev/null and b/modules/__pycache__/recognition.cpython-312.pyc differ diff --git a/modules/__pycache__/representation.cpython-312.pyc b/modules/__pycache__/representation.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0c74fb06a148093a808c1e53a2a4f53266176f1e Binary files /dev/null and b/modules/__pycache__/representation.cpython-312.pyc differ diff --git a/modules/__pycache__/streaming.cpython-312.pyc b/modules/__pycache__/streaming.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..182782529ece35d5aeeea77533116570e56420d6 Binary files /dev/null and b/modules/__pycache__/streaming.cpython-312.pyc differ diff --git a/modules/__pycache__/verification.cpython-312.pyc b/modules/__pycache__/verification.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..16696d20450beb6f24c0c4503f631103aabe91b5 Binary files /dev/null and b/modules/__pycache__/verification.cpython-312.pyc differ diff --git a/modules/cloudservice.py b/modules/cloudservice.py new file mode 100644 index 0000000000000000000000000000000000000000..7a56708491b226a42e026c701c499607e225ba17 --- /dev/null +++ b/modules/cloudservice.py @@ -0,0 +1,154 @@ +# from flask import Flask, jsonify +# import cloudinary +# import cloudinary.api +# import cloudinary.uploader +# import os +# import glob +# import requests +# from dotenv import load_dotenv +# from deepface.commons.logger import Logger + +# logger = Logger(module="modules/cloudservice.py") + +# load_dotenv() + +# # Configure Cloudinary +# cloudinary.config( +# cloud_name=os.getenv('CLOUDINARY_CLOUD_NAME'), +# api_key=os.getenv('CLOUDINARY_API_KEY'), +# api_secret=os.getenv('CLOUDINARY_API_SECRET') +# ) + + +# def fetch_cloudinary_images(folder_name): + +# resources = [] + +# res = cloudinary.api.resources(type='upload', resource_type='image', prefix=f'mafqoud/images/{folder_name}') +# resources.extend(res.get('resources', [])) +# return resources + +# def download_image(url, local_path): +# response = requests.get(url, stream=True) +# if response.status_code == 200: +# with open(local_path, 'wb') as file: +# for chunk in response: +# file.write(chunk) + +# def sync_folder(folder_name, local_dir): +# cloudinary_images = fetch_cloudinary_images(folder_name) +# cloudinary_urls = {img['secure_url']: img['public_id'] for img in cloudinary_images} + +# # Download new images and track downloaded image paths +# downloaded_paths = [] +# for img in cloudinary_images: +# url = img['secure_url'] +# public_id = img['public_id'] +# file_name = url.split('/')[-1] # Get the actual file name +# local_path = os.path.join(local_dir, file_name) +# print(local_path) + +# if not os.path.exists(local_path): +# download_image(url, local_path) +# downloaded_paths.append(local_path) + +# # Remove old images +# local_images = [os.path.join(local_dir, f) for f in os.listdir(local_dir) if os.path.isfile(os.path.join(local_dir, f))] +# for local_path in local_images: +# if local_path not in downloaded_paths and (local_path.endswith('.jpg') or local_path.endswith('.jpeg') or local_path.endswith('.png')): +# os.remove(local_path) + +# def delete_pkl_files(directory): +# """Delete all .pkl files in the specified directory.""" +# pkl_files = glob.glob(os.path.join(directory, '*.pkl')) +# for pkl_file in pkl_files: +# os.remove(pkl_file) + + +from flask import Flask, jsonify +import cloudinary +import cloudinary.api +import cloudinary.uploader +import os +import glob +import requests +from dotenv import load_dotenv +from deepface.commons.logger import Logger +from deepface import DeepFace # Assuming this is the correct import for training + +logger = Logger(module="modules/cloudservice.py") + +load_dotenv() + +# Configure Cloudinary +cloudinary.config( + cloud_name=os.getenv('CLOUDINARY_CLOUD_NAME'), + api_key=os.getenv('CLOUDINARY_API_KEY'), + api_secret=os.getenv('CLOUDINARY_API_SECRET') +) + + +def fetch_cloudinary_images(folder_name): + resources = [] + next_cursor = None + + while True: + if next_cursor: + res = cloudinary.api.resources( + type='upload', + resource_type='image', + prefix=f'mafqoud/images/{folder_name}', + max_results=500, + next_cursor=next_cursor + ) + else: + res = cloudinary.api.resources( + type='upload', + resource_type='image', + prefix=f'mafqoud/images/{folder_name}', + max_results=500 + ) + + resources.extend(res.get('resources', [])) + next_cursor = res.get('next_cursor') + + if not next_cursor: + break + + return resources + +def download_image(url, local_path): + response = requests.get(url, stream=True) + if response.status_code == 200: + with open(local_path, 'wb') as file: + for chunk in response: + file.write(chunk) + +def sync_folder(folder_name, local_dir): + cloudinary_images = fetch_cloudinary_images(folder_name) + cloudinary_urls = {img['secure_url']: img['public_id'] for img in cloudinary_images} + + # Download new images and track downloaded image paths + downloaded_paths = [] + for img in cloudinary_images: + url = img['secure_url'] + public_id = img['public_id'] + file_name = url.split('/')[-1] # Get the actual file name + local_path = os.path.join(local_dir, file_name) + print(local_path) + + if not os.path.exists(local_path): + download_image(url, local_path) + downloaded_paths.append(local_path) + + # Remove old images + local_images = [os.path.join(local_dir, f) for f in os.listdir(local_dir) if os.path.isfile(os.path.join(local_dir, f))] + for local_path in local_images: + if local_path not in downloaded_paths and (local_path.endswith('.jpg') or local_path.endswith('.jpeg') or local_path.endswith('.png')): + os.remove(local_path) + +def delete_pkl_files(directory): + """Delete all .pkl files in the specified directory.""" + pkl_files = glob.glob(os.path.join(directory, '*.pkl')) + for pkl_file in pkl_files: + os.remove(pkl_file) diff --git a/modules/demography.py b/modules/demography.py new file mode 100644 index 0000000000000000000000000000000000000000..f11f71d1ee63e4df954d85b55ee0278b03d76da0 --- /dev/null +++ b/modules/demography.py @@ -0,0 +1,197 @@ +# built-in dependencies +from typing import Any, Dict, List, Union + +# 3rd party dependencies +import numpy as np +from tqdm import tqdm + +# project dependencies +from deepface.modules import modeling, detection, preprocessing +from deepface.extendedmodels import Gender, Race, Emotion + + +def analyze( + img_path: Union[str, np.ndarray], + actions: Union[tuple, list] = ("emotion", "age", "gender", "race"), + enforce_detection: bool = True, + detector_backend: str = "opencv", + align: bool = True, + expand_percentage: int = 0, + silent: bool = False, +) -> List[Dict[str, Any]]: + """ + Analyze facial attributes such as age, gender, emotion, and race in the provided image. + + Args: + img_path (str or np.ndarray): The exact path to the image, a numpy array in BGR format, + or a base64 encoded image. If the source image contains multiple faces, the result will + include information for each detected face. + + actions (tuple): Attributes to analyze. The default is ('age', 'gender', 'emotion', 'race'). + You can exclude some of these attributes from the analysis if needed. + + enforce_detection (boolean): If no face is detected in an image, raise an exception. + Set to False to avoid the exception for low-resolution images (default is True). + + detector_backend (string): face detector backend. Options: 'opencv', 'retinaface', + 'mtcnn', 'ssd', 'dlib', 'mediapipe', 'yolov8', 'centerface' or 'skip' + (default is opencv). + + distance_metric (string): Metric for measuring similarity. Options: 'cosine', + 'euclidean', 'euclidean_l2' (default is cosine). + + align (boolean): Perform alignment based on the eye positions (default is True). + + expand_percentage (int): expand detected facial area with a percentage (default is 0). + + silent (boolean): Suppress or allow some log messages for a quieter analysis process + (default is False). + + Returns: + results (List[Dict[str, Any]]): A list of dictionaries, where each dictionary represents + the analysis results for a detected face. + + Each dictionary in the list contains the following keys: + + - 'region' (dict): Represents the rectangular region of the detected face in the image. + - 'x': x-coordinate of the top-left corner of the face. + - 'y': y-coordinate of the top-left corner of the face. + - 'w': Width of the detected face region. + - 'h': Height of the detected face region. + + - 'age' (float): Estimated age of the detected face. + + - 'face_confidence' (float): Confidence score for the detected face. + Indicates the reliability of the face detection. + + - 'dominant_gender' (str): The dominant gender in the detected face. + Either "Man" or "Woman." + + - 'gender' (dict): Confidence scores for each gender category. + - 'Man': Confidence score for the male gender. + - 'Woman': Confidence score for the female gender. + + - 'dominant_emotion' (str): The dominant emotion in the detected face. + Possible values include "sad," "angry," "surprise," "fear," "happy," + "disgust," and "neutral." + + - 'emotion' (dict): Confidence scores for each emotion category. + - 'sad': Confidence score for sadness. + - 'angry': Confidence score for anger. + - 'surprise': Confidence score for surprise. + - 'fear': Confidence score for fear. + - 'happy': Confidence score for happiness. + - 'disgust': Confidence score for disgust. + - 'neutral': Confidence score for neutrality. + + - 'dominant_race' (str): The dominant race in the detected face. + Possible values include "indian," "asian," "latino hispanic," + "black," "middle eastern," and "white." + + - 'race' (dict): Confidence scores for each race category. + - 'indian': Confidence score for Indian ethnicity. + - 'asian': Confidence score for Asian ethnicity. + - 'latino hispanic': Confidence score for Latino/Hispanic ethnicity. + - 'black': Confidence score for Black ethnicity. + - 'middle eastern': Confidence score for Middle Eastern ethnicity. + - 'white': Confidence score for White ethnicity. + """ + + # if actions is passed as tuple with single item, interestingly it becomes str here + if isinstance(actions, str): + actions = (actions,) + + # check if actions is not an iterable or empty. + if not hasattr(actions, "__getitem__") or not actions: + raise ValueError("`actions` must be a list of strings.") + + actions = list(actions) + + # For each action, check if it is valid + for action in actions: + if action not in ("emotion", "age", "gender", "race"): + raise ValueError( + f"Invalid action passed ({repr(action)})). " + "Valid actions are `emotion`, `age`, `gender`, `race`." + ) + # --------------------------------- + resp_objects = [] + + img_objs = detection.extract_faces( + img_path=img_path, + detector_backend=detector_backend, + grayscale=False, + enforce_detection=enforce_detection, + align=align, + expand_percentage=expand_percentage, + ) + + for img_obj in img_objs: + img_content = img_obj["face"] + img_region = img_obj["facial_area"] + img_confidence = img_obj["confidence"] + if img_content.shape[0] == 0 or img_content.shape[1] == 0: + continue + + # rgb to bgr + img_content = img_content[:, :, ::-1] + + # resize input image + img_content = preprocessing.resize_image(img=img_content, target_size=(224, 224)) + + obj = {} + # facial attribute analysis + pbar = tqdm( + range(0, len(actions)), + desc="Finding actions", + disable=silent if len(actions) > 1 else True, + ) + for index in pbar: + action = actions[index] + pbar.set_description(f"Action: {action}") + + if action == "emotion": + emotion_predictions = modeling.build_model("Emotion").predict(img_content) + sum_of_predictions = emotion_predictions.sum() + + obj["emotion"] = {} + for i, emotion_label in enumerate(Emotion.labels): + emotion_prediction = 100 * emotion_predictions[i] / sum_of_predictions + obj["emotion"][emotion_label] = emotion_prediction + + obj["dominant_emotion"] = Emotion.labels[np.argmax(emotion_predictions)] + + elif action == "age": + apparent_age = modeling.build_model("Age").predict(img_content) + # int cast is for exception - object of type 'float32' is not JSON serializable + obj["age"] = int(apparent_age) + + elif action == "gender": + gender_predictions = modeling.build_model("Gender").predict(img_content) + obj["gender"] = {} + for i, gender_label in enumerate(Gender.labels): + gender_prediction = 100 * gender_predictions[i] + obj["gender"][gender_label] = gender_prediction + + obj["dominant_gender"] = Gender.labels[np.argmax(gender_predictions)] + + elif action == "race": + race_predictions = modeling.build_model("Race").predict(img_content) + sum_of_predictions = race_predictions.sum() + + obj["race"] = {} + for i, race_label in enumerate(Race.labels): + race_prediction = 100 * race_predictions[i] / sum_of_predictions + obj["race"][race_label] = race_prediction + + obj["dominant_race"] = Race.labels[np.argmax(race_predictions)] + + # ----------------------------- + # mention facial areas + obj["region"] = img_region + # include image confidence + obj["face_confidence"] = img_confidence + + resp_objects.append(obj) + + return resp_objects diff --git a/modules/detection.py b/modules/detection.py new file mode 100644 index 0000000000000000000000000000000000000000..ad4b28874796bd0015088abdf271ab33b4b1b404 --- /dev/null +++ b/modules/detection.py @@ -0,0 +1,160 @@ +# built-in dependencies +from typing import Any, Dict, List, Tuple, Union + +# 3rd part dependencies +import numpy as np +import cv2 +from PIL import Image + +# project dependencies +from deepface.models.Detector import DetectedFace, FacialAreaRegion +from deepface.detectors import DetectorWrapper +from deepface.commons import image_utils +from deepface.commons import logger as log + +logger = log.get_singletonish_logger() + +# pylint: disable=no-else-raise + + +def extract_faces( + img_path: Union[str, np.ndarray], + detector_backend: str = "opencv", + enforce_detection: bool = True, + align: bool = True, + expand_percentage: int = 0, + grayscale: bool = False, +) -> List[Dict[str, Any]]: + """ + Extract faces from a given image + + Args: + img_path (str or np.ndarray): Path to the first image. Accepts exact image path + as a string, numpy array (BGR), or base64 encoded images. + + detector_backend (string): face detector backend. Options: 'opencv', 'retinaface', + 'mtcnn', 'ssd', 'dlib', 'mediapipe', 'yolov8', 'centerface' or 'skip' + (default is opencv) + + enforce_detection (boolean): If no face is detected in an image, raise an exception. + Default is True. Set to False to avoid the exception for low-resolution images. + + align (bool): Flag to enable face alignment (default is True). + + expand_percentage (int): expand detected facial area with a percentage + + grayscale (boolean): Flag to convert the image to grayscale before + processing (default is False). + + Returns: + results (List[Dict[str, Any]]): A list of dictionaries, where each dictionary contains: + + - "face" (np.ndarray): The detected face as a NumPy array in RGB format. + + - "facial_area" (Dict[str, Any]): The detected face's regions as a dictionary containing: + - keys 'x', 'y', 'w', 'h' with int values + - keys 'left_eye', 'right_eye' with a tuple of 2 ints as values. + left eye and right eye are eyes on the left and right respectively with respect + to the person itself instead of observer. + + - "confidence" (float): The confidence score associated with the detected face. + """ + + resp_objs = [] + + # img might be path, base64 or numpy array. Convert it to numpy whatever it is. + img, img_name = image_utils.load_image(img_path) + + if img is None: + raise ValueError(f"Exception while loading {img_name}") + + base_region = FacialAreaRegion(x=0, y=0, w=img.shape[1], h=img.shape[0], confidence=0) + + if detector_backend == "skip": + face_objs = [DetectedFace(img=img, facial_area=base_region, confidence=0)] + else: + face_objs = DetectorWrapper.detect_faces( + detector_backend=detector_backend, + img=img, + align=align, + expand_percentage=expand_percentage, + ) + + # in case of no face found + if len(face_objs) == 0 and enforce_detection is True: + if img_name is not None: + raise ValueError( + f"Face could not be detected in {img_name}." + "Please confirm that the picture is a face photo " + "or consider to set enforce_detection param to False." + ) + else: + raise ValueError( + "Face could not be detected. Please confirm that the picture is a face photo " + "or consider to set enforce_detection param to False." + ) + + if len(face_objs) == 0 and enforce_detection is False: + face_objs = [DetectedFace(img=img, facial_area=base_region, confidence=0)] + + for face_obj in face_objs: + current_img = face_obj.img + current_region = face_obj.facial_area + + if current_img.shape[0] == 0 or current_img.shape[1] == 0: + continue + + if grayscale is True: + current_img = cv2.cvtColor(current_img, cv2.COLOR_BGR2GRAY) + + current_img = current_img / 255 # normalize input in [0, 1] + + resp_objs.append( + { + "face": current_img[:, :, ::-1], + "facial_area": { + "x": int(current_region.x), + "y": int(current_region.y), + "w": int(current_region.w), + "h": int(current_region.h), + "left_eye": current_region.left_eye, + "right_eye": current_region.right_eye, + }, + "confidence": round(current_region.confidence, 2), + } + ) + + if len(resp_objs) == 0 and enforce_detection == True: + raise ValueError( + f"Exception while extracting faces from {img_name}." + "Consider to set enforce_detection arg to False." + ) + + return resp_objs + + +def align_face( + img: np.ndarray, + left_eye: Union[list, tuple], + right_eye: Union[list, tuple], +) -> Tuple[np.ndarray, float]: + """ + Align a given image horizantally with respect to their left and right eye locations + Args: + img (np.ndarray): pre-loaded image with detected face + left_eye (list or tuple): coordinates of left eye with respect to the person itself + right_eye(list or tuple): coordinates of right eye with respect to the person itself + Returns: + img (np.ndarray): aligned facial image + """ + # if eye could not be detected for the given image, return image itself + if left_eye is None or right_eye is None: + return img, 0 + + # sometimes unexpectedly detected images come with nil dimensions + if img.shape[0] == 0 or img.shape[1] == 0: + return img, 0 + + angle = float(np.degrees(np.arctan2(left_eye[1] - right_eye[1], left_eye[0] - right_eye[0]))) + img = np.array(Image.fromarray(img).rotate(angle)) + return img, angle diff --git a/modules/modeling.py b/modules/modeling.py new file mode 100644 index 0000000000000000000000000000000000000000..b40dcb5c249f1c1c15526fc98b1c75861fe3d347 --- /dev/null +++ b/modules/modeling.py @@ -0,0 +1,61 @@ +# built-in dependencies +from typing import Any + +# project dependencies +from deepface.basemodels import ( + VGGFace, + OpenFace, + FbDeepFace, + DeepID, + ArcFace, + SFace, + Dlib, + Facenet, + GhostFaceNet +) +from deepface.extendedmodels import Age, Gender, Race, Emotion + + +def build_model(model_name: str) -> Any: + """ + This function builds a deepface model + Parameters: + model_name (string): face recognition or facial attribute model + VGG-Face, Facenet, OpenFace, DeepFace, DeepID for face recognition + Age, Gender, Emotion, Race for facial attributes + + Returns: + built model class + """ + + # singleton design pattern + global model_obj + + models = { + "VGG-Face": VGGFace.VggFaceClient, + "OpenFace": OpenFace.OpenFaceClient, + "Facenet": Facenet.FaceNet128dClient, + "Facenet512": Facenet.FaceNet512dClient, + "DeepFace": FbDeepFace.DeepFaceClient, + "DeepID": DeepID.DeepIdClient, + "Dlib": Dlib.DlibClient, + "ArcFace": ArcFace.ArcFaceClient, + "SFace": SFace.SFaceClient, + "GhostFaceNet": GhostFaceNet.GhostFaceNetClient, + "Emotion": Emotion.EmotionClient, + "Age": Age.ApparentAgeClient, + "Gender": Gender.GenderClient, + "Race": Race.RaceClient, + } + + if not "model_obj" in globals(): + model_obj = {} + + if not model_name in model_obj.keys(): + model = models.get(model_name) + if model: + model_obj[model_name] = model() + else: + raise ValueError(f"Invalid model_name passed - {model_name}") + + return model_obj[model_name] diff --git a/modules/preprocessing.py b/modules/preprocessing.py new file mode 100644 index 0000000000000000000000000000000000000000..459adbab3e0dff3459e4ae21f413ece226bbf3b8 --- /dev/null +++ b/modules/preprocessing.py @@ -0,0 +1,121 @@ +# built-in dependencies +from typing import Tuple + +# 3rd party +import numpy as np +import cv2 + +# project dependencies +from deepface.commons import package_utils + + +tf_major_version = package_utils.get_tf_major_version() +if tf_major_version == 1: + from keras.preprocessing import image +elif tf_major_version == 2: + from tensorflow.keras.preprocessing import image + + +def normalize_input(img: np.ndarray, normalization: str = "base") -> np.ndarray: + """Normalize input image. + + Args: + img (numpy array): the input image. + normalization (str, optional): the normalization technique. Defaults to "base", + for no normalization. + + Returns: + numpy array: the normalized image. + """ + + # issue 131 declares that some normalization techniques improves the accuracy + + if normalization == "base": + return img + + # @trevorgribble and @davedgd contributed this feature + # restore input in scale of [0, 255] because it was normalized in scale of + # [0, 1] in preprocess_face + img *= 255 + + if normalization == "raw": + pass # return just restored pixels + + elif normalization == "Facenet": + mean, std = img.mean(), img.std() + img = (img - mean) / std + + elif normalization == "Facenet2018": + # simply / 127.5 - 1 (similar to facenet 2018 model preprocessing step as @iamrishab posted) + img /= 127.5 + img -= 1 + + elif normalization == "VGGFace": + # mean subtraction based on VGGFace1 training data + img[..., 0] -= 93.5940 + img[..., 1] -= 104.7624 + img[..., 2] -= 129.1863 + + elif normalization == "VGGFace2": + # mean subtraction based on VGGFace2 training data + img[..., 0] -= 91.4953 + img[..., 1] -= 103.8827 + img[..., 2] -= 131.0912 + + elif normalization == "ArcFace": + # Reference study: The faces are cropped and resized to 112×112, + # and each pixel (ranged between [0, 255]) in RGB images is normalised + # by subtracting 127.5 then divided by 128. + img -= 127.5 + img /= 128 + else: + raise ValueError(f"unimplemented normalization type - {normalization}") + + return img + + +def resize_image(img: np.ndarray, target_size: Tuple[int, int]) -> np.ndarray: + """ + Resize an image to expected size of a ml model with adding black pixels. + Args: + img (np.ndarray): pre-loaded image as numpy array + target_size (tuple): input shape of ml model + Returns: + img (np.ndarray): resized input image + """ + factor_0 = target_size[0] / img.shape[0] + factor_1 = target_size[1] / img.shape[1] + factor = min(factor_0, factor_1) + + dsize = ( + int(img.shape[1] * factor), + int(img.shape[0] * factor), + ) + img = cv2.resize(img, dsize) + + diff_0 = target_size[0] - img.shape[0] + diff_1 = target_size[1] - img.shape[1] + + # Put the base image in the middle of the padded image + img = np.pad( + img, + ( + (diff_0 // 2, diff_0 - diff_0 // 2), + (diff_1 // 2, diff_1 - diff_1 // 2), + (0, 0), + ), + "constant", + ) + + # double check: if target image is not still the same size with target. + if img.shape[0:2] != target_size: + img = cv2.resize(img, target_size) + + # make it 4-dimensional how ML models expect + img = image.img_to_array(img) + img = np.expand_dims(img, axis=0) + + if img.max() > 1: + img = (img.astype(np.float32) / 255.0).astype(np.float32) + + return img diff --git a/modules/recognition.py b/modules/recognition.py new file mode 100644 index 0000000000000000000000000000000000000000..011863445b882455e04da9d25d9a93bd7b60bb8a --- /dev/null +++ b/modules/recognition.py @@ -0,0 +1,391 @@ +# built-in dependencies +import os +import pickle +from typing import List, Union, Optional, Dict, Any +import time + +# 3rd party dependencies +import numpy as np +import pandas as pd +from tqdm import tqdm + +# project dependencies +from deepface.commons import image_utils +from deepface.modules import representation, detection, verification +from deepface.commons import logger as log + +logger = log.get_singletonish_logger() + + +def find( + img_path: Union[str, np.ndarray], + db_path: str, + model_name: str = "VGG-Face", + distance_metric: str = "cosine", + enforce_detection: bool = True, + detector_backend: str = "opencv", + align: bool = True, + expand_percentage: int = 0, + threshold: Optional[float] = None, + normalization: str = "base", + silent: bool = False, +) -> List[pd.DataFrame]: + """ + Identify individuals in a database + + Args: + img_path (str or np.ndarray): The exact path to the image, a numpy array in BGR format, + or a base64 encoded image. If the source image contains multiple faces, the result will + include information for each detected face. + + db_path (string): Path to the folder containing image files. All detected faces + in the database will be considered in the decision-making process. + + model_name (str): Model for face recognition. Options: VGG-Face, Facenet, Facenet512, + OpenFace, DeepFace, DeepID, Dlib, ArcFace, SFace and GhostFaceNet (default is VGG-Face). + + distance_metric (string): Metric for measuring similarity. Options: 'cosine', + 'euclidean', 'euclidean_l2'. + + enforce_detection (boolean): If no face is detected in an image, raise an exception. + Default is True. Set to False to avoid the exception for low-resolution images. + + detector_backend (string): face detector backend. Options: 'opencv', 'retinaface', + 'mtcnn', 'ssd', 'dlib', 'mediapipe', 'yolov8', 'centerface' or 'skip'. + + align (boolean): Perform alignment based on the eye positions. + + expand_percentage (int): expand detected facial area with a percentage (default is 0). + + threshold (float): Specify a threshold to determine whether a pair represents the same + person or different individuals. This threshold is used for comparing distances. + If left unset, default pre-tuned threshold values will be applied based on the specified + model name and distance metric (default is None). + + normalization (string): Normalize the input image before feeding it to the model. + Default is base. Options: base, raw, Facenet, Facenet2018, VGGFace, VGGFace2, ArcFace + + silent (boolean): Suppress or allow some log messages for a quieter analysis process. + + Returns: + results (List[pd.DataFrame]): A list of pandas dataframes. Each dataframe corresponds + to the identity information for an individual detected in the source image. + The DataFrame columns include: + + - 'identity': Identity label of the detected individual. + + - 'target_x', 'target_y', 'target_w', 'target_h': Bounding box coordinates of the + target face in the database. + + - 'source_x', 'source_y', 'source_w', 'source_h': Bounding box coordinates of the + detected face in the source image. + + - 'threshold': threshold to determine a pair whether same person or different persons + + - 'distance': Similarity score between the faces based on the + specified model and distance metric + """ + + tic = time.time() + + if os.path.isdir(db_path) is not True: + raise ValueError("Passed db_path does not exist!") + + file_parts = [ + "ds", + "model", + model_name, + "detector", + detector_backend, + "aligned" if align else "unaligned", + "normalization", + normalization, + "expand", + str(expand_percentage), + ] + + file_name = "_".join(file_parts) + ".pkl" + file_name = file_name.replace("-", "").lower() + + datastore_path = os.path.join(db_path, file_name) + representations = [] + + # required columns for representations + df_cols = [ + "identity", + "hash", + "embedding", + "target_x", + "target_y", + "target_w", + "target_h", + ] + + # Ensure the proper pickle file exists + if not os.path.exists(datastore_path): + with open(datastore_path, "wb") as f: + pickle.dump([], f) + + # Load the representations from the pickle file + with open(datastore_path, "rb") as f: + representations = pickle.load(f) + + # check each item of representations list has required keys + for i, current_representation in enumerate(representations): + missing_keys = list(set(df_cols) - set(current_representation.keys())) + if len(missing_keys) > 0: + raise ValueError( + f"{i}-th item does not have some required keys - {missing_keys}." + f"Consider to delete {datastore_path}" + ) + + # embedded images + pickled_images = [representation["identity"] for representation in representations] + + # Get the list of images on storage + storage_images = image_utils.list_images(path=db_path) + + if len(storage_images) == 0: + raise ValueError(f"No item found in {db_path}") + + # Enforce data consistency amongst on disk images and pickle file + must_save_pickle = False + new_images = list(set(storage_images) - set(pickled_images)) # images added to storage + old_images = list(set(pickled_images) - set(storage_images)) # images removed from storage + + # detect replaced images + replaced_images = [] + for current_representation in representations: + identity = current_representation["identity"] + if identity in old_images: + continue + alpha_hash = current_representation["hash"] + beta_hash = image_utils.find_image_hash(identity) + if alpha_hash != beta_hash: + logger.debug(f"Even though {identity} represented before, it's replaced later.") + replaced_images.append(identity) + + if not silent and (len(new_images) > 0 or len(old_images) > 0 or len(replaced_images) > 0): + logger.info( + f"Found {len(new_images)} newly added image(s)" + f", {len(old_images)} removed image(s)" + f", {len(replaced_images)} replaced image(s)." + ) + + # append replaced images into both old and new images. these will be dropped and re-added. + new_images = new_images + replaced_images + old_images = old_images + replaced_images + + # remove old images first + if len(old_images) > 0: + representations = [rep for rep in representations if rep["identity"] not in old_images] + must_save_pickle = True + + # find representations for new images + if len(new_images) > 0: + representations += __find_bulk_embeddings( + employees=new_images, + model_name=model_name, + detector_backend=detector_backend, + enforce_detection=enforce_detection, + align=align, + expand_percentage=expand_percentage, + normalization=normalization, + silent=silent, + ) # add new images + must_save_pickle = True + + if must_save_pickle: + with open(datastore_path, "wb") as f: + pickle.dump(representations, f) + if not silent: + logger.info(f"There are now {len(representations)} representations in {file_name}") + + # Should we have no representations bailout + if len(representations) == 0: + if not silent: + toc = time.time() + logger.info(f"find function duration {toc - tic} seconds") + return [] + + # ---------------------------- + # now, we got representations for facial database + df = pd.DataFrame(representations) + + if silent is False: + logger.info(f"Searching {img_path} in {df.shape[0]} length datastore") + + # img path might have more than once face + source_objs = detection.extract_faces( + img_path=img_path, + detector_backend=detector_backend, + grayscale=False, + enforce_detection=enforce_detection, + align=align, + expand_percentage=expand_percentage, + ) + + resp_obj = [] + + for source_obj in source_objs: + source_img = source_obj["face"] + source_region = source_obj["facial_area"] + target_embedding_obj = representation.represent( + img_path=source_img, + model_name=model_name, + enforce_detection=enforce_detection, + detector_backend="skip", + align=align, + normalization=normalization, + ) + + target_representation = target_embedding_obj[0]["embedding"] + + result_df = df.copy() # df will be filtered in each img + result_df["source_x"] = source_region["x"] + result_df["source_y"] = source_region["y"] + result_df["source_w"] = source_region["w"] + result_df["source_h"] = source_region["h"] + + distances = [] + for _, instance in df.iterrows(): + source_representation = instance["embedding"] + if source_representation is None: + distances.append(float("inf")) # no representation for this image + continue + + target_dims = len(list(target_representation)) + source_dims = len(list(source_representation)) + if target_dims != source_dims: + raise ValueError( + "Source and target embeddings must have same dimensions but " + + f"{target_dims}:{source_dims}. Model structure may change" + + " after pickle created. Delete the {file_name} and re-run." + ) + + distance = verification.find_distance( + source_representation, target_representation, distance_metric + ) + + distances.append(distance) + + # --------------------------- + target_threshold = threshold or verification.find_threshold(model_name, distance_metric) + + result_df["threshold"] = target_threshold + result_df["distance"] = distances + + result_df = result_df.drop(columns=["embedding"]) + # pylint: disable=unsubscriptable-object + result_df = result_df[result_df["distance"] <= target_threshold] + result_df = result_df.sort_values(by=["distance"], ascending=True).reset_index(drop=True) + + resp_obj.append(result_df) + + # ----------------------------------- + + if not silent: + toc = time.time() + logger.info(f"find function duration {toc - tic} seconds") + + return resp_obj + + +def __find_bulk_embeddings( + employees: List[str], + model_name: str = "VGG-Face", + detector_backend: str = "opencv", + enforce_detection: bool = True, + align: bool = True, + expand_percentage: int = 0, + normalization: str = "base", + silent: bool = False, +) -> List[Dict["str", Any]]: + """ + Find embeddings of a list of images + + Args: + employees (list): list of exact image paths + + model_name (str): Model for face recognition. Options: VGG-Face, Facenet, Facenet512, + OpenFace, DeepFace, DeepID, Dlib, ArcFace, SFace and GhostFaceNet (default is VGG-Face). + + detector_backend (str): face detector model name + + enforce_detection (bool): set this to False if you + want to proceed when you cannot detect any face + + align (bool): enable or disable alignment of image + before feeding to facial recognition model + + expand_percentage (int): expand detected facial area with a + percentage (default is 0). + + normalization (bool): normalization technique + + silent (bool): enable or disable informative logging + Returns: + representations (list): pivot list of dict with + image name, hash, embedding and detected face area's coordinates + """ + representations = [] + for employee in tqdm( + employees, + desc="Finding representations", + disable=silent, + ): + file_hash = image_utils.find_image_hash(employee) + + try: + img_objs = detection.extract_faces( + img_path=employee, + detector_backend=detector_backend, + grayscale=False, + enforce_detection=enforce_detection, + align=align, + expand_percentage=expand_percentage, + ) + + except ValueError as err: + logger.error(f"Exception while extracting faces from {employee}: {str(err)}") + img_objs = [] + + if len(img_objs) == 0: + representations.append( + { + "identity": employee, + "hash": file_hash, + "embedding": None, + "target_x": 0, + "target_y": 0, + "target_w": 0, + "target_h": 0, + } + ) + else: + for img_obj in img_objs: + img_content = img_obj["face"] + img_region = img_obj["facial_area"] + embedding_obj = representation.represent( + img_path=img_content, + model_name=model_name, + enforce_detection=enforce_detection, + detector_backend="skip", + align=align, + normalization=normalization, + ) + + img_representation = embedding_obj[0]["embedding"] + representations.append( + { + "identity": employee, + "hash": file_hash, + "embedding": img_representation, + "target_x": img_region["x"], + "target_y": img_region["y"], + "target_w": img_region["w"], + "target_h": img_region["h"], + } + ) + + return representations diff --git a/modules/representation.py b/modules/representation.py new file mode 100644 index 0000000000000000000000000000000000000000..9e8a1a6974f31962f3fc68a6f5261822132c09a6 --- /dev/null +++ b/modules/representation.py @@ -0,0 +1,120 @@ +# built-in dependencies +from typing import Any, Dict, List, Union + +# 3rd party dependencies +import numpy as np + +# project dependencies +from deepface.commons import image_utils +from deepface.modules import modeling, detection, preprocessing +from deepface.models.FacialRecognition import FacialRecognition + + +def represent( + img_path: Union[str, np.ndarray], + model_name: str = "VGG-Face", + enforce_detection: bool = True, + detector_backend: str = "opencv", + align: bool = True, + expand_percentage: int = 0, + normalization: str = "base", +) -> List[Dict[str, Any]]: + """ + Represent facial images as multi-dimensional vector embeddings. + + Args: + img_path (str or np.ndarray): The exact path to the image, a numpy array in BGR format, + or a base64 encoded image. If the source image contains multiple faces, the result will + include information for each detected face. + + model_name (str): Model for face recognition. Options: VGG-Face, Facenet, Facenet512, + OpenFace, DeepFace, DeepID, Dlib, ArcFace, SFace and GhostFaceNet + + enforce_detection (boolean): If no face is detected in an image, raise an exception. + Default is True. Set to False to avoid the exception for low-resolution images. + + detector_backend (string): face detector backend. Options: 'opencv', 'retinaface', + 'mtcnn', 'ssd', 'dlib', 'mediapipe', 'yolov8', 'centerface' or 'skip'. + + align (boolean): Perform alignment based on the eye positions. + + expand_percentage (int): expand detected facial area with a percentage (default is 0). + + normalization (string): Normalize the input image before feeding it to the model. + Default is base. Options: base, raw, Facenet, Facenet2018, VGGFace, VGGFace2, ArcFace + + Returns: + results (List[Dict[str, Any]]): A list of dictionaries, each containing the + following fields: + + - embedding (List[float]): Multidimensional vector representing facial features. + The number of dimensions varies based on the reference model + (e.g., FaceNet returns 128 dimensions, VGG-Face returns 4096 dimensions). + - facial_area (dict): Detected facial area by face detection in dictionary format. + Contains 'x' and 'y' as the left-corner point, and 'w' and 'h' + as the width and height. If `detector_backend` is set to 'skip', it represents + the full image area and is nonsensical. + - face_confidence (float): Confidence score of face detection. If `detector_backend` is set + to 'skip', the confidence will be 0 and is nonsensical. + """ + resp_objs = [] + + model: FacialRecognition = modeling.build_model(model_name) + + # --------------------------------- + # we have run pre-process in verification. so, this can be skipped if it is coming from verify. + target_size = model.input_shape + if detector_backend != "skip": + img_objs = detection.extract_faces( + img_path=img_path, + detector_backend=detector_backend, + grayscale=False, + enforce_detection=enforce_detection, + align=align, + expand_percentage=expand_percentage, + ) + else: # skip + # Try load. If load error, will raise exception internal + img, _ = image_utils.load_image(img_path) + + if len(img.shape) != 3: + raise ValueError(f"Input img must be 3 dimensional but it is {img.shape}") + + # make dummy region and confidence to keep compatibility with `extract_faces` + img_objs = [ + { + "face": img, + "facial_area": {"x": 0, "y": 0, "w": img.shape[1], "h": img.shape[2]}, + "confidence": 0, + } + ] + # --------------------------------- + + for img_obj in img_objs: + img = img_obj["face"] + + # rgb to bgr + img = img[:, :, ::-1] + + region = img_obj["facial_area"] + confidence = img_obj["confidence"] + + # resize to expected shape of ml model + img = preprocessing.resize_image( + img=img, + # thanks to DeepId (!) + target_size=(target_size[1], target_size[0]), + ) + + # custom normalization + img = preprocessing.normalize_input(img=img, normalization=normalization) + + embedding = model.forward(img) + + resp_obj = {} + resp_obj["embedding"] = embedding + resp_obj["facial_area"] = region + resp_obj["face_confidence"] = confidence + resp_objs.append(resp_obj) + + return resp_objs diff --git a/modules/streaming.py b/modules/streaming.py new file mode 100644 index 0000000000000000000000000000000000000000..95d05d91c68a64a4c9fd2adc47484f5da0faf130 --- /dev/null +++ b/modules/streaming.py @@ -0,0 +1,980 @@ +# built-in dependencies +import os +import time +from typing import List, Tuple, Optional + +# 3rd party dependencies +import numpy as np +import pandas as pd +import cv2 + +# project dependencies +from deepface import DeepFace +from deepface.commons import logger as log + +logger = log.get_singletonish_logger() + +# dependency configuration +os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2" + + +IDENTIFIED_IMG_SIZE = 112 +TEXT_COLOR = (255, 255, 255) + + +def analysis( + db_path: str, + model_name="VGG-Face", + detector_backend="opencv", + distance_metric="cosine", + enable_face_analysis=True, + source=0, + time_threshold=5, + frame_threshold=5, +): + """ + Run real time face recognition and facial attribute analysis + + Args: + db_path (string): Path to the folder containing image files. All detected faces + in the database will be considered in the decision-making process. + + model_name (str): Model for face recognition. Options: VGG-Face, Facenet, Facenet512, + OpenFace, DeepFace, DeepID, Dlib, ArcFace, SFace and GhostFaceNet (default is VGG-Face). + + detector_backend (string): face detector backend. Options: 'opencv', 'retinaface', + 'mtcnn', 'ssd', 'dlib', 'mediapipe', 'yolov8', 'centerface' or 'skip' + (default is opencv). + + distance_metric (string): Metric for measuring similarity. Options: 'cosine', + 'euclidean', 'euclidean_l2' (default is cosine). + + enable_face_analysis (bool): Flag to enable face analysis (default is True). + + source (Any): The source for the video stream (default is 0, which represents the + default camera). + + time_threshold (int): The time threshold (in seconds) for face recognition (default is 5). + + frame_threshold (int): The frame threshold for face recognition (default is 5). + Returns: + None + """ + # initialize models + build_demography_models(enable_face_analysis=enable_face_analysis) + build_facial_recognition_model(model_name=model_name) + # call a dummy find function for db_path once to create embeddings before starting webcam + _ = search_identity( + detected_face=np.zeros([224, 224, 3]), + db_path=db_path, + detector_backend=detector_backend, + distance_metric=distance_metric, + model_name=model_name, + ) + + freezed_img = None + freeze = False + num_frames_with_faces = 0 + tic = time.time() + + cap = cv2.VideoCapture(source) # webcam + while True: + has_frame, img = cap.read() + if not has_frame: + break + + # we are adding some figures into img such as identified facial image, age, gender + # that is why, we need raw image itself to make analysis + raw_img = img.copy() + + faces_coordinates = [] + if freeze is False: + faces_coordinates = grab_facial_areas(img=img, detector_backend=detector_backend) + + # we will pass img to analyze modules (identity, demography) and add some illustrations + # that is why, we will not be able to extract detected face from img clearly + detected_faces = extract_facial_areas(img=img, faces_coordinates=faces_coordinates) + + img = highlight_facial_areas(img=img, faces_coordinates=faces_coordinates) + img = countdown_to_freeze( + img=img, + faces_coordinates=faces_coordinates, + frame_threshold=frame_threshold, + num_frames_with_faces=num_frames_with_faces, + ) + + num_frames_with_faces = num_frames_with_faces + 1 if len(faces_coordinates) else 0 + + freeze = num_frames_with_faces > 0 and num_frames_with_faces % frame_threshold == 0 + if freeze: + # add analyze results into img - derive from raw_img + img = highlight_facial_areas(img=raw_img, faces_coordinates=faces_coordinates) + + # age, gender and emotion analysis + img = perform_demography_analysis( + enable_face_analysis=enable_face_analysis, + img=raw_img, + faces_coordinates=faces_coordinates, + detected_faces=detected_faces, + ) + # facial recogntion analysis + img = perform_facial_recognition( + img=img, + faces_coordinates=faces_coordinates, + detected_faces=detected_faces, + db_path=db_path, + detector_backend=detector_backend, + distance_metric=distance_metric, + model_name=model_name, + ) + + # freeze the img after analysis + freezed_img = img.copy() + + # start counter for freezing + tic = time.time() + logger.info("freezed") + + elif freeze is True and time.time() - tic > time_threshold: + freeze = False + freezed_img = None + # reset counter for freezing + tic = time.time() + logger.info("freeze released") + + freezed_img = countdown_to_release(img=freezed_img, tic=tic, time_threshold=time_threshold) + + cv2.imshow("img", img if freezed_img is None else freezed_img) + + if cv2.waitKey(1) & 0xFF == ord("q"): # press q to quit + break + + # kill open cv things + cap.release() + cv2.destroyAllWindows() + + +def build_facial_recognition_model(model_name: str) -> None: + """ + Build facial recognition model + Args: + model_name (str): Model for face recognition. Options: VGG-Face, Facenet, Facenet512, + OpenFace, DeepFace, DeepID, Dlib, ArcFace, SFace and GhostFaceNet (default is VGG-Face). + Returns + input_shape (tuple): input shape of given facial recognitio n model. + """ + _ = DeepFace.build_model(model_name=model_name) + logger.info(f"{model_name} is built") + + +def search_identity( + detected_face: np.ndarray, + db_path: str, + model_name: str, + detector_backend: str, + distance_metric: str, +) -> Tuple[Optional[str], Optional[np.ndarray]]: + """ + Search an identity in facial database. + Args: + detected_face (np.ndarray): extracted individual facial image + db_path (string): Path to the folder containing image files. All detected faces + in the database will be considered in the decision-making process. + model_name (str): Model for face recognition. Options: VGG-Face, Facenet, Facenet512, + OpenFace, DeepFace, DeepID, Dlib, ArcFace, SFace and GhostFaceNet (default is VGG-Face). + detector_backend (string): face detector backend. Options: 'opencv', 'retinaface', + 'mtcnn', 'ssd', 'dlib', 'mediapipe', 'yolov8', 'centerface' or 'skip' + (default is opencv). + distance_metric (string): Metric for measuring similarity. Options: 'cosine', + 'euclidean', 'euclidean_l2' (default is cosine). + Returns: + result (tuple): result consisting of following objects + identified image path (str) + identified image itself (np.ndarray) + """ + target_path = None + try: + dfs = DeepFace.find( + img_path=detected_face, + db_path=db_path, + model_name=model_name, + detector_backend=detector_backend, + distance_metric=distance_metric, + enforce_detection=False, + silent=True, + ) + except ValueError as err: + if f"No item found in {db_path}" in str(err): + logger.warn( + f"No item is found in {db_path}." + "So, no facial recognition analysis will be performed." + ) + dfs = [] + else: + raise err + if len(dfs) == 0: + # you may consider to return unknown person's image here + return None, None + + # detected face is coming from parent, safe to access 1st index + df = dfs[0] + + if df.shape[0] == 0: + return None, None + + candidate = df.iloc[0] + target_path = candidate["identity"] + logger.info(f"Hello, {target_path}") + + # load found identity image - extracted if possible + target_objs = DeepFace.extract_faces( + img_path=target_path, + detector_backend=detector_backend, + enforce_detection=False, + align=True, + ) + + # extract facial area of the identified image if and only if it has one face + # otherwise, show image as is + if len(target_objs) == 1: + # extract 1st item directly + target_obj = target_objs[0] + target_img = target_obj["face"] + target_img = cv2.resize(target_img, (IDENTIFIED_IMG_SIZE, IDENTIFIED_IMG_SIZE)) + target_img *= 255 + target_img = target_img[:, :, ::-1] + else: + target_img = cv2.imread(target_path) + + return target_path.split("/")[-1], target_img + + +def build_demography_models(enable_face_analysis: bool) -> None: + """ + Build demography analysis models + Args: + enable_face_analysis (bool): Flag to enable face analysis (default is True). + Returns: + None + """ + if enable_face_analysis is False: + return + DeepFace.build_model(model_name="Age") + logger.info("Age model is just built") + DeepFace.build_model(model_name="Gender") + logger.info("Gender model is just built") + DeepFace.build_model(model_name="Emotion") + logger.info("Emotion model is just built") + + +def highlight_facial_areas( + img: np.ndarray, faces_coordinates: List[Tuple[int, int, int, int]] +) -> np.ndarray: + """ + Highlight detected faces with rectangles in the given image + Args: + img (np.ndarray): image itself + faces_coordinates (list): list of face coordinates as tuple with x, y, w and h + Returns: + img (np.ndarray): image with highlighted facial areas + """ + for x, y, w, h in faces_coordinates: + # highlight facial area with rectangle + cv2.rectangle(img, (x, y), (x + w, y + h), (67, 67, 67), 1) + return img + + +def countdown_to_freeze( + img: np.ndarray, + faces_coordinates: List[Tuple[int, int, int, int]], + frame_threshold: int, + num_frames_with_faces: int, +) -> np.ndarray: + """ + Highlight time to freeze in the image's facial areas + Args: + img (np.ndarray): image itself + faces_coordinates (list): list of face coordinates as tuple with x, y, w and h + frame_threshold (int): how many sequantial frames required with face(s) to freeze + num_frames_with_faces (int): how many sequantial frames do we have with face(s) + Returns: + img (np.ndarray): image with counter values + """ + for x, y, w, h in faces_coordinates: + cv2.putText( + img, + str(frame_threshold - (num_frames_with_faces % frame_threshold)), + (int(x + w / 4), int(y + h / 1.5)), + cv2.FONT_HERSHEY_SIMPLEX, + 4, + (255, 255, 255), + 2, + ) + return img + + +def countdown_to_release( + img: Optional[np.ndarray], tic: float, time_threshold: int +) -> Optional[np.ndarray]: + """ + Highlight time to release the freezing in the image top left area + Args: + img (np.ndarray): image itself + tic (float): time specifying when freezing started + time_threshold (int): freeze time threshold + Returns: + img (np.ndarray): image with time to release the freezing + """ + # do not take any action if it is not frozen yet + if img is None: + return img + toc = time.time() + time_left = int(time_threshold - (toc - tic) + 1) + cv2.rectangle(img, (10, 10), (90, 50), (67, 67, 67), -10) + cv2.putText( + img, + str(time_left), + (40, 40), + cv2.FONT_HERSHEY_SIMPLEX, + 1, + (255, 255, 255), + 1, + ) + return img + + +def grab_facial_areas( + img: np.ndarray, detector_backend: str, threshold: int = 130 +) -> List[Tuple[int, int, int, int]]: + """ + Find facial area coordinates in the given image + Args: + img (np.ndarray): image itself + detector_backend (string): face detector backend. Options: 'opencv', 'retinaface', + 'mtcnn', 'ssd', 'dlib', 'mediapipe', 'yolov8', 'centerface' or 'skip' + (default is opencv). + threshold (int): threshold for facial area, discard smaller ones + Returns + result (list): list of tuple with x, y, w and h coordinates + """ + try: + face_objs = DeepFace.extract_faces( + img_path=img, + detector_backend=detector_backend, + # you may consider to extract with larger expanding value + expand_percentage=0, + ) + faces = [ + ( + face_obj["facial_area"]["x"], + face_obj["facial_area"]["y"], + face_obj["facial_area"]["w"], + face_obj["facial_area"]["h"], + ) + for face_obj in face_objs + if face_obj["facial_area"]["w"] > threshold + ] + return faces + except: # to avoid exception if no face detected + return [] + + +def extract_facial_areas( + img: np.ndarray, faces_coordinates: List[Tuple[int, int, int, int]] +) -> List[np.ndarray]: + """ + Extract facial areas as numpy array from given image + Args: + img (np.ndarray): image itself + faces_coordinates (list): list of facial area coordinates as tuple with + x, y, w and h values + Returns: + detected_faces (list): list of detected facial area images + """ + detected_faces = [] + for x, y, w, h in faces_coordinates: + detected_face = img[int(y) : int(y + h), int(x) : int(x + w)] + detected_faces.append(detected_face) + return detected_faces + + +def perform_facial_recognition( + img: np.ndarray, + detected_faces: List[np.ndarray], + faces_coordinates: List[Tuple[int, int, int, int]], + db_path: str, + detector_backend: str, + distance_metric: str, + model_name: str, +) -> np.ndarray: + """ + Perform facial recognition + Args: + img (np.ndarray): image itself + detected_faces (list): list of extracted detected face images as numpy + faces_coordinates (list): list of facial area coordinates as tuple with + x, y, w and h values + db_path (string): Path to the folder containing image files. All detected faces + in the database will be considered in the decision-making process. + detector_backend (string): face detector backend. Options: 'opencv', 'retinaface', + 'mtcnn', 'ssd', 'dlib', 'mediapipe', 'yolov8', 'centerface' or 'skip' + (default is opencv). + distance_metric (string): Metric for measuring similarity. Options: 'cosine', + 'euclidean', 'euclidean_l2' (default is cosine). + model_name (str): Model for face recognition. Options: VGG-Face, Facenet, Facenet512, + OpenFace, DeepFace, DeepID, Dlib, ArcFace, SFace and GhostFaceNet (default is VGG-Face). + Returns: + img (np.ndarray): image with identified face informations + """ + for idx, (x, y, w, h) in enumerate(faces_coordinates): + detected_face = detected_faces[idx] + target_label, target_img = search_identity( + detected_face=detected_face, + db_path=db_path, + detector_backend=detector_backend, + distance_metric=distance_metric, + model_name=model_name, + ) + if target_label is None: + continue + + img = overlay_identified_face( + img=img, + target_img=target_img, + label=target_label, + x=x, + y=y, + w=w, + h=h, + ) + + return img + + +def perform_demography_analysis( + enable_face_analysis: bool, + img: np.ndarray, + faces_coordinates: List[Tuple[int, int, int, int]], + detected_faces: List[np.ndarray], +) -> np.ndarray: + """ + Perform demography analysis on given image + Args: + enable_face_analysis (bool): Flag to enable face analysis. + img (np.ndarray): image itself + faces_coordinates (list): list of face coordinates as tuple with + x, y, w and h values + detected_faces (list): list of extracted detected face images as numpy + Returns: + img (np.ndarray): image with analyzed demography information + """ + if enable_face_analysis is False: + return img + for idx, (x, y, w, h) in enumerate(faces_coordinates): + detected_face = detected_faces[idx] + demographies = DeepFace.analyze( + img_path=detected_face, + actions=("age", "gender", "emotion"), + detector_backend="skip", + enforce_detection=False, + silent=True, + ) + + if len(demographies) == 0: + continue + + # safe to access 1st index because detector backend is skip + demography = demographies[0] + + img = overlay_emotion(img=img, emotion_probas=demography["emotion"], x=x, y=y, w=w, h=h) + img = overlay_age_gender( + img=img, + apparent_age=demography["age"], + gender=demography["dominant_gender"][0:1], # M or W + x=x, + y=y, + w=w, + h=h, + ) + return img + + +def overlay_identified_face( + img: np.ndarray, + target_img: np.ndarray, + label: str, + x: int, + y: int, + w: int, + h: int, +) -> np.ndarray: + """ + Overlay the identified face onto image itself + Args: + img (np.ndarray): image itself + target_img (np.ndarray): identified face's image + label (str): name of the identified face + x (int): x coordinate of the face on the given image + y (int): y coordinate of the face on the given image + w (int): w coordinate of the face on the given image + h (int): h coordinate of the face on the given image + Returns: + img (np.ndarray): image with overlayed identity + """ + try: + if y - IDENTIFIED_IMG_SIZE > 0 and x + w + IDENTIFIED_IMG_SIZE < img.shape[1]: + # top right + img[ + y - IDENTIFIED_IMG_SIZE : y, + x + w : x + w + IDENTIFIED_IMG_SIZE, + ] = target_img + + overlay = img.copy() + opacity = 0.4 + cv2.rectangle( + img, + (x + w, y), + (x + w + IDENTIFIED_IMG_SIZE, y + 20), + (46, 200, 255), + cv2.FILLED, + ) + cv2.addWeighted( + overlay, + opacity, + img, + 1 - opacity, + 0, + img, + ) + + cv2.putText( + img, + label, + (x + w, y + 10), + cv2.FONT_HERSHEY_SIMPLEX, + 0.5, + TEXT_COLOR, + 1, + ) + + # connect face and text + cv2.line( + img, + (x + int(w / 2), y), + (x + 3 * int(w / 4), y - int(IDENTIFIED_IMG_SIZE / 2)), + (67, 67, 67), + 1, + ) + cv2.line( + img, + (x + 3 * int(w / 4), y - int(IDENTIFIED_IMG_SIZE / 2)), + (x + w, y - int(IDENTIFIED_IMG_SIZE / 2)), + (67, 67, 67), + 1, + ) + + elif y + h + IDENTIFIED_IMG_SIZE < img.shape[0] and x - IDENTIFIED_IMG_SIZE > 0: + # bottom left + img[ + y + h : y + h + IDENTIFIED_IMG_SIZE, + x - IDENTIFIED_IMG_SIZE : x, + ] = target_img + + overlay = img.copy() + opacity = 0.4 + cv2.rectangle( + img, + (x - IDENTIFIED_IMG_SIZE, y + h - 20), + (x, y + h), + (46, 200, 255), + cv2.FILLED, + ) + cv2.addWeighted( + overlay, + opacity, + img, + 1 - opacity, + 0, + img, + ) + + cv2.putText( + img, + label, + (x - IDENTIFIED_IMG_SIZE, y + h - 10), + cv2.FONT_HERSHEY_SIMPLEX, + 0.5, + TEXT_COLOR, + 1, + ) + + # connect face and text + cv2.line( + img, + (x + int(w / 2), y + h), + ( + x + int(w / 2) - int(w / 4), + y + h + int(IDENTIFIED_IMG_SIZE / 2), + ), + (67, 67, 67), + 1, + ) + cv2.line( + img, + ( + x + int(w / 2) - int(w / 4), + y + h + int(IDENTIFIED_IMG_SIZE / 2), + ), + (x, y + h + int(IDENTIFIED_IMG_SIZE / 2)), + (67, 67, 67), + 1, + ) + + elif y - IDENTIFIED_IMG_SIZE > 0 and x - IDENTIFIED_IMG_SIZE > 0: + # top left + img[y - IDENTIFIED_IMG_SIZE : y, x - IDENTIFIED_IMG_SIZE : x] = target_img + + overlay = img.copy() + opacity = 0.4 + cv2.rectangle( + img, + (x - IDENTIFIED_IMG_SIZE, y), + (x, y + 20), + (46, 200, 255), + cv2.FILLED, + ) + cv2.addWeighted( + overlay, + opacity, + img, + 1 - opacity, + 0, + img, + ) + + cv2.putText( + img, + label, + (x - IDENTIFIED_IMG_SIZE, y + 10), + cv2.FONT_HERSHEY_SIMPLEX, + 0.5, + TEXT_COLOR, + 1, + ) + + # connect face and text + cv2.line( + img, + (x + int(w / 2), y), + ( + x + int(w / 2) - int(w / 4), + y - int(IDENTIFIED_IMG_SIZE / 2), + ), + (67, 67, 67), + 1, + ) + cv2.line( + img, + ( + x + int(w / 2) - int(w / 4), + y - int(IDENTIFIED_IMG_SIZE / 2), + ), + (x, y - int(IDENTIFIED_IMG_SIZE / 2)), + (67, 67, 67), + 1, + ) + + elif ( + x + w + IDENTIFIED_IMG_SIZE < img.shape[1] + and y + h + IDENTIFIED_IMG_SIZE < img.shape[0] + ): + # bottom righ + img[ + y + h : y + h + IDENTIFIED_IMG_SIZE, + x + w : x + w + IDENTIFIED_IMG_SIZE, + ] = target_img + + overlay = img.copy() + opacity = 0.4 + cv2.rectangle( + img, + (x + w, y + h - 20), + (x + w + IDENTIFIED_IMG_SIZE, y + h), + (46, 200, 255), + cv2.FILLED, + ) + cv2.addWeighted( + overlay, + opacity, + img, + 1 - opacity, + 0, + img, + ) + + cv2.putText( + img, + label, + (x + w, y + h - 10), + cv2.FONT_HERSHEY_SIMPLEX, + 0.5, + TEXT_COLOR, + 1, + ) + + # connect face and text + cv2.line( + img, + (x + int(w / 2), y + h), + ( + x + int(w / 2) + int(w / 4), + y + h + int(IDENTIFIED_IMG_SIZE / 2), + ), + (67, 67, 67), + 1, + ) + cv2.line( + img, + ( + x + int(w / 2) + int(w / 4), + y + h + int(IDENTIFIED_IMG_SIZE / 2), + ), + (x + w, y + h + int(IDENTIFIED_IMG_SIZE / 2)), + (67, 67, 67), + 1, + ) + else: + logger.info("cannot put facial recognition info on the image") + except Exception as err: # pylint: disable=broad-except + logger.error(str(err)) + return img + + +def overlay_emotion( + img: np.ndarray, emotion_probas: dict, x: int, y: int, w: int, h: int +) -> np.ndarray: + """ + Overlay the analyzed emotion of face onto image itself + Args: + img (np.ndarray): image itself + emotion_probas (dict): probability of different emotionas dictionary + x (int): x coordinate of the face on the given image + y (int): y coordinate of the face on the given image + w (int): w coordinate of the face on the given image + h (int): h coordinate of the face on the given image + Returns: + img (np.ndarray): image with overlay emotion analsis results + """ + emotion_df = pd.DataFrame(emotion_probas.items(), columns=["emotion", "score"]) + emotion_df = emotion_df.sort_values(by=["score"], ascending=False).reset_index(drop=True) + + # background of mood box + + # transparency + overlay = img.copy() + opacity = 0.4 + + # put gray background to the right of the detected image + if x + w + IDENTIFIED_IMG_SIZE < img.shape[1]: + cv2.rectangle( + img, + (x + w, y), + (x + w + IDENTIFIED_IMG_SIZE, y + h), + (64, 64, 64), + cv2.FILLED, + ) + cv2.addWeighted(overlay, opacity, img, 1 - opacity, 0, img) + + # put gray background to the left of the detected image + elif x - IDENTIFIED_IMG_SIZE > 0: + cv2.rectangle( + img, + (x - IDENTIFIED_IMG_SIZE, y), + (x, y + h), + (64, 64, 64), + cv2.FILLED, + ) + cv2.addWeighted(overlay, opacity, img, 1 - opacity, 0, img) + + for index, instance in emotion_df.iterrows(): + current_emotion = instance["emotion"] + emotion_label = f"{current_emotion} " + emotion_score = instance["score"] / 100 + + filled_bar_x = 35 # this is the size if an emotion is 100% + bar_x = int(filled_bar_x * emotion_score) + + if x + w + IDENTIFIED_IMG_SIZE < img.shape[1]: + + text_location_y = y + 20 + (index + 1) * 20 + text_location_x = x + w + + if text_location_y < y + h: + cv2.putText( + img, + emotion_label, + (text_location_x, text_location_y), + cv2.FONT_HERSHEY_SIMPLEX, + 0.5, + (255, 255, 255), + 1, + ) + + cv2.rectangle( + img, + (x + w + 70, y + 13 + (index + 1) * 20), + ( + x + w + 70 + bar_x, + y + 13 + (index + 1) * 20 + 5, + ), + (255, 255, 255), + cv2.FILLED, + ) + + elif x - IDENTIFIED_IMG_SIZE > 0: + + text_location_y = y + 20 + (index + 1) * 20 + text_location_x = x - IDENTIFIED_IMG_SIZE + + if text_location_y <= y + h: + cv2.putText( + img, + emotion_label, + (text_location_x, text_location_y), + cv2.FONT_HERSHEY_SIMPLEX, + 0.5, + (255, 255, 255), + 1, + ) + + cv2.rectangle( + img, + ( + x - IDENTIFIED_IMG_SIZE + 70, + y + 13 + (index + 1) * 20, + ), + ( + x - IDENTIFIED_IMG_SIZE + 70 + bar_x, + y + 13 + (index + 1) * 20 + 5, + ), + (255, 255, 255), + cv2.FILLED, + ) + + return img + + +def overlay_age_gender( + img: np.ndarray, apparent_age: float, gender: str, x: int, y: int, w: int, h: int +) -> np.ndarray: + """ + Overlay the analyzed age and gender of face onto image itself + Args: + img (np.ndarray): image itself + apparent_age (float): analyzed apparent age + gender (str): analyzed gender + x (int): x coordinate of the face on the given image + y (int): y coordinate of the face on the given image + w (int): w coordinate of the face on the given image + h (int): h coordinate of the face on the given image + Returns: + img (np.ndarray): image with overlay age and gender analsis results + """ + logger.debug(f"{apparent_age} years old {gender}") + analysis_report = f"{int(apparent_age)} {gender}" + + info_box_color = (46, 200, 255) + + # show its age and gender on the top of the image + if y - IDENTIFIED_IMG_SIZE + int(IDENTIFIED_IMG_SIZE / 5) > 0: + + triangle_coordinates = np.array( + [ + (x + int(w / 2), y), + ( + x + int(w / 2) - int(w / 10), + y - int(IDENTIFIED_IMG_SIZE / 3), + ), + ( + x + int(w / 2) + int(w / 10), + y - int(IDENTIFIED_IMG_SIZE / 3), + ), + ] + ) + + cv2.drawContours( + img, + [triangle_coordinates], + 0, + info_box_color, + -1, + ) + + cv2.rectangle( + img, + ( + x + int(w / 5), + y - IDENTIFIED_IMG_SIZE + int(IDENTIFIED_IMG_SIZE / 5), + ), + (x + w - int(w / 5), y - int(IDENTIFIED_IMG_SIZE / 3)), + info_box_color, + cv2.FILLED, + ) + + cv2.putText( + img, + analysis_report, + (x + int(w / 3.5), y - int(IDENTIFIED_IMG_SIZE / 2.1)), + cv2.FONT_HERSHEY_SIMPLEX, + 1, + (0, 111, 255), + 2, + ) + + # show its age and gender on the top of the image + elif y + h + IDENTIFIED_IMG_SIZE - int(IDENTIFIED_IMG_SIZE / 5) < img.shape[0]: + + triangle_coordinates = np.array( + [ + (x + int(w / 2), y + h), + ( + x + int(w / 2) - int(w / 10), + y + h + int(IDENTIFIED_IMG_SIZE / 3), + ), + ( + x + int(w / 2) + int(w / 10), + y + h + int(IDENTIFIED_IMG_SIZE / 3), + ), + ] + ) + + cv2.drawContours( + img, + [triangle_coordinates], + 0, + info_box_color, + -1, + ) + + cv2.rectangle( + img, + (x + int(w / 5), y + h + int(IDENTIFIED_IMG_SIZE / 3)), + ( + x + w - int(w / 5), + y + h + IDENTIFIED_IMG_SIZE - int(IDENTIFIED_IMG_SIZE / 5), + ), + info_box_color, + cv2.FILLED, + ) + + cv2.putText( + img, + analysis_report, + (x + int(w / 3.5), y + h + int(IDENTIFIED_IMG_SIZE / 1.5)), + cv2.FONT_HERSHEY_SIMPLEX, + 1, + (0, 111, 255), + 2, + ) + + return img diff --git a/modules/verification.py b/modules/verification.py new file mode 100644 index 0000000000000000000000000000000000000000..6bb5248f4fa525cd2e3761af1fd92c737542aa6d --- /dev/null +++ b/modules/verification.py @@ -0,0 +1,374 @@ +# built-in dependencies +import time +from typing import Any, Dict, Union, List, Tuple + +# 3rd party dependencies +import numpy as np + +# project dependencies +from deepface.modules import representation, detection, modeling +from deepface.models.FacialRecognition import FacialRecognition +from deepface.commons import logger as log + +logger = log.get_singletonish_logger() + + +def verify( + 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", + enforce_detection: bool = True, + 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. + + The verification function converts facial images to vectors and calculates the similarity + between those vectors. Vectors of images of the same person should exhibit higher similarity + (or lower distance) than vectors of images of different persons. + + Args: + img1_path (str or np.ndarray or List[float]): Path to the first image. + Accepts exact image path as a string, numpy array (BGR), base64 encoded images + or pre-calculated embeddings. + + img2_path (str or np.ndarray or or List[float]): Path to the second image. + Accepts exact image path 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, SFace and GhostFaceNet (default is VGG-Face). + + detector_backend (string): face detector backend. Options: 'opencv', 'retinaface', + 'mtcnn', 'ssd', 'dlib', 'mediapipe', 'yolov8', 'centerface' or 'skip' + (default is opencv) + + distance_metric (string): Metric for measuring similarity. Options: 'cosine', + 'euclidean', 'euclidean_l2' (default is cosine). + + enforce_detection (boolean): If no face is detected in an image, raise an exception. + Set to False to avoid the exception for low-resolution images (default is True). + + align (bool): Flag to enable face alignment (default is True). + + expand_percentage (int): expand detected facial area with a percentage (default is 0). + + 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. + + - 'verified' (bool): Indicates whether the images represent the same person (True) + or different persons (False). + + - 'distance' (float): The distance measure between the face vectors. + A lower distance indicates higher similarity. + + - 'max_threshold_to_verify' (float): The maximum threshold used for verification. + If the distance is below this threshold, the images are considered a match. + + - 'model' (str): The chosen face recognition model. + + - 'similarity_metric' (str): The chosen similarity metric for measuring distances. + + - 'facial_areas' (dict): Rectangular regions of interest for faces in both images. + - 'img1': {'x': int, 'y': int, 'w': int, 'h': int} + Region of interest for the first image. + - 'img2': {'x': int, 'y': int, 'w': int, 'h': int} + Region of interest for the second image. + + - 'time' (float): Time taken for the verification process in seconds. + """ + + tic = time.time() + + model: FacialRecognition = modeling.build_model(model_name) + dims = model.output_shape + + # extract faces from img1 + 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: + try: + img1_embeddings, img1_facial_areas = __extract_faces_and_embeddings( + img_path=img1_path, + model_name=model_name, + detector_backend=detector_backend, + 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 + + # extract faces from img2 + 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: + try: + img2_embeddings, img2_facial_areas = __extract_faces_and_embeddings( + img_path=img2_path, + model_name=model_name, + detector_backend=detector_backend, + enforce_detection=enforce_detection, + align=align, + expand_percentage=expand_percentage, + normalization=normalization, + ) + except ValueError as err: + raise ValueError("Exception while processing img2_path") from err + + no_facial_area = { + "x": None, + "y": None, + "w": None, + "h": None, + "left_eye": None, + "right_eye": None, + } + + distances = [] + 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) + 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 = facial_areas[np.argmin(distances)] + + toc = time.time() + + resp_obj = { + "verified": distance <= threshold, + "distance": distance, + "threshold": threshold, + "model": model_name, + "detector_backend": detector_backend, + "similarity_metric": distance_metric, + "facial_areas": {"img1": facial_areas[0], "img2": facial_areas[1]}, + "time": round(toc - tic, 2), + } + + 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 = [] + + img_objs = detection.extract_faces( + img_path=img_path, + detector_backend=detector_backend, + grayscale=False, + enforce_detection=enforce_detection, + align=align, + expand_percentage=expand_percentage, + ) + + # 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: + """ + 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 + """ + if isinstance(source_representation, list): + source_representation = np.array(source_representation) + + if isinstance(test_representation, list): + test_representation = np.array(test_representation) + + a = np.matmul(np.transpose(source_representation), test_representation) + b = np.sum(np.multiply(source_representation, source_representation)) + c = np.sum(np.multiply(test_representation, test_representation)) + return 1 - (a / (np.sqrt(b) * np.sqrt(c))) + + +def find_euclidean_distance( + source_representation: Union[np.ndarray, list], test_representation: Union[np.ndarray, list] +) -> np.float64: + """ + 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 + """ + if isinstance(source_representation, list): + source_representation = np.array(source_representation) + + if isinstance(test_representation, list): + test_representation = np.array(test_representation) + + euclidean_distance = source_representation - test_representation + euclidean_distance = np.sum(np.multiply(euclidean_distance, euclidean_distance)) + euclidean_distance = np.sqrt(euclidean_distance) + return euclidean_distance + + +def l2_normalize(x: Union[np.ndarray, list]) -> np.ndarray: + """ + Normalize input vector with l2 + Args: + x (np.ndarray or list): given vector + Returns: + y (np.ndarray): l2 normalized vector + """ + if isinstance(x, list): + x = np.array(x) + return x / np.sqrt(np.sum(np.multiply(x, x))) + + +def find_distance( + alpha_embedding: Union[np.ndarray, list], + beta_embedding: Union[np.ndarray, list], + distance_metric: str, +) -> np.float64: + """ + 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 + """ + 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": + distance = find_euclidean_distance( + l2_normalize(alpha_embedding), l2_normalize(beta_embedding) + ) + else: + raise ValueError("Invalid distance_metric passed - ", distance_metric) + return distance + + +def find_threshold(model_name: str, distance_metric: str) -> float: + """ + Retrieve pre-tuned threshold values for a model and distance metric pair + Args: + model_name (str): Model for face recognition. Options: VGG-Face, Facenet, Facenet512, + OpenFace, DeepFace, DeepID, Dlib, ArcFace, SFace and GhostFaceNet (default is VGG-Face). + distance_metric (str): distance metric name. Options are cosine, euclidean + and euclidean_l2. + Returns: + threshold (float): threshold value for that model name and distance metric + pair. Distances less than this threshold will be classified same person. + """ + + base_threshold = {"cosine": 0.40, "euclidean": 0.55, "euclidean_l2": 0.75} + + thresholds = { + # "VGG-Face": {"cosine": 0.40, "euclidean": 0.60, "euclidean_l2": 0.86}, # 2622d + "VGG-Face": { + "cosine": 0.68, + "euclidean": 1.17, + "euclidean_l2": 1.17, + }, # 4096d - tuned with LFW + "Facenet": {"cosine": 0.40, "euclidean": 10, "euclidean_l2": 0.80}, + "Facenet512": {"cosine": 0.30, "euclidean": 23.56, "euclidean_l2": 1.04}, + "ArcFace": {"cosine": 0.68, "euclidean": 4.15, "euclidean_l2": 1.13}, + "Dlib": {"cosine": 0.07, "euclidean": 0.6, "euclidean_l2": 0.4}, + "SFace": {"cosine": 0.593, "euclidean": 10.734, "euclidean_l2": 1.055}, + "OpenFace": {"cosine": 0.10, "euclidean": 0.55, "euclidean_l2": 0.55}, + "DeepFace": {"cosine": 0.23, "euclidean": 64, "euclidean_l2": 0.64}, + "DeepID": {"cosine": 0.015, "euclidean": 45, "euclidean_l2": 0.17}, + "GhostFaceNet": {"cosine": 0.65, "euclidean": 35.71, "euclidean_l2": 1.10}, + } + + threshold = thresholds.get(model_name, base_threshold).get(distance_metric, 0.4) + + return threshold diff --git a/package_info.json b/package_info.json new file mode 100644 index 0000000000000000000000000000000000000000..36c186393773faad2a29563b5a2868ae268f9c96 --- /dev/null +++ b/package_info.json @@ -0,0 +1,3 @@ +{ + "version": "0.0.90" +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..0d1fefc5fdafa1ef09b193b827fd21de96ab9723 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,16 @@ +requests>=2.27.1 +numpy>=1.14.0 +pandas>=0.23.4 +gdown>=3.10.1 +tqdm>=4.30.0 +Pillow>=5.2.0 +opencv-python>=4.5.5.64 +tensorflow>=1.9.0 +keras>=2.2.0 +Flask>=1.1.2 +mtcnn>=0.1.0 +retina-face>=0.0.1 +fire>=0.4.0 +gunicorn>=20.1.0 +cloudinary>=1.40.0 +python-dotenv>=1.0.1 diff --git a/requirements_additional.txt b/requirements_additional.txt new file mode 100644 index 0000000000000000000000000000000000000000..0344661d6387c28115da5949aa75bf4dd31d5c1a --- /dev/null +++ b/requirements_additional.txt @@ -0,0 +1,5 @@ +opencv-contrib-python>=4.3.0.36 +mediapipe>=0.8.7.3 +dlib>=19.20.0 +ultralytics>=8.0.122 +facenet-pytorch>=2.5.3 \ No newline at end of file diff --git a/requirements_local b/requirements_local new file mode 100644 index 0000000000000000000000000000000000000000..e869c3f4053c7f1fb74814e0783e9057f37b9fc4 --- /dev/null +++ b/requirements_local @@ -0,0 +1,6 @@ +numpy==1.22.3 +pandas==2.0.3 +Pillow==9.0.0 +opencv-python==4.9.0.80 +tensorflow==2.9.0 +keras==2.9.0 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000000000000000000000000000000000000..6424cecf31ddf8110307952e38e0da9ff2c5c68a --- /dev/null +++ b/setup.py @@ -0,0 +1,37 @@ +import json +import setuptools + +with open("README.md", "r", encoding="utf-8") as fh: + long_description = fh.read() + +with open("requirements.txt", "r", encoding="utf-8") as f: + requirements = f.read().split("\n") + +with open("package_info.json", "r", encoding="utf-8") as f: + package_info = json.load(f) + +setuptools.setup( + name="deepface", + version=package_info["version"], + author="Sefik Ilkin Serengil", + author_email="serengil@gmail.com", + description=( + "A Lightweight Face Recognition and Facial Attribute Analysis Framework" + " (Age, Gender, Emotion, Race) for Python" + ), + data_files=[("", ["README.md", "requirements.txt", "package_info.json"])], + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/serengil/deepface", + packages=setuptools.find_packages(), + classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + ], + entry_points={ + "console_scripts": ["deepface = deepface.DeepFace:cli"], + }, + python_requires=">=3.7", + install_requires=requirements, +) diff --git a/spoofmodels/__pycache__/FasNet.cpython-312.pyc b/spoofmodels/__pycache__/FasNet.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..644c8129a5e2160ea8fb954a2c412b8dcdffa9c4 Binary files /dev/null and b/spoofmodels/__pycache__/FasNet.cpython-312.pyc differ diff --git a/spoofmodels/__pycache__/__init__.cpython-312.pyc b/spoofmodels/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..627b50816d636ae70da4eb2844948c8f3ca3644c Binary files /dev/null and b/spoofmodels/__pycache__/__init__.cpython-312.pyc differ