diff --git a/CITATION.md b/CITATION.md new file mode 100644 index 0000000..910ed29 --- /dev/null +++ b/CITATION.md @@ -0,0 +1,71 @@ +## Cite DeepFace Papers + +Please cite deepface in your publications if it helps your research. Here are its BibTex entries: + +### Facial Recognition + +If you use deepface in your research for facial recogntion purposes, please cite these publications: + +```BibTeX +@article{serengil2024lightface, + title = {A Benchmark of Facial Recognition Pipelines and Co-Usability Performances of Modules}, + author = {Serengil, Sefik Ilkin and Ozpinar, Alper}, + journal = {Bilisim Teknolojileri Dergisi}, + volume = {17}, + number = {2}, + pages = {95-107}, + year = {2024}, + doi = {10.17671/gazibtd.1399077}, + url = {https://dergipark.org.tr/en/pub/gazibtd/issue/84331/1399077}, + publisher = {Gazi University} +} +``` + +```BibTeX +@inproceedings{serengil2020lightface, + title = {LightFace: A Hybrid Deep Face Recognition Framework}, + author = {Serengil, Sefik Ilkin and Ozpinar, Alper}, + booktitle = {2020 Innovations in Intelligent Systems and Applications Conference (ASYU)}, + pages = {23-27}, + year = {2020}, + doi = {10.1109/ASYU50717.2020.9259802}, + url = {https://ieeexplore.ieee.org/document/9259802}, + organization = {IEEE} +} +``` + +### Facial Attribute Analysis + +If you use deepface in your research for facial attribute analysis purposes such as age, gender, emotion or ethnicity prediction, please cite the this publication. + +```BibTeX +@inproceedings{serengil2021lightface, + title = {HyperExtended LightFace: A Facial Attribute Analysis Framework}, + author = {Serengil, Sefik Ilkin and Ozpinar, Alper}, + booktitle = {2021 International Conference on Engineering and Emerging Technologies (ICEET)}, + pages = {1-4}, + year = {2021}, + doi = {10.1109/ICEET53442.2021.9659697}, + url = {https://ieeexplore.ieee.org/document/9659697/}, + organization = {IEEE} +} +``` + +### Additional Papers + +We have additionally released these papers within the DeepFace project for a multitude of purposes. + +```BibTeX +@misc{serengil2023db, + title = {An evaluation of sql and nosql databases for facial recognition pipelines}, + author = {Serengil, Sefik Ilkin and Ozpinar, Alper}, + year = {2023}, + archivePrefix = {Cambridge Open Engage}, + doi = {10.33774/coe-2023-18rcn}, + url = {https://www.cambridge.org/engage/coe/article-details/63f3e5541d2d184063d4f569} +} +``` + +### Repositories + +Also, if you use deepface in your GitHub projects, please add `deepface` in the `requirements.txt`. Thereafter, your project will be listed in its [dependency graph](https://github.com/serengil/deepface/network/dependents). \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d85d5bc --- /dev/null +++ b/Dockerfile @@ -0,0 +1,58 @@ +# base image +FROM python:3.8.12 +LABEL org.opencontainers.image.source https://github.com/serengil/deepface + +# ----------------------------------- +# create required folder +RUN mkdir /app +RUN mkdir /app/deepface + +# ----------------------------------- +# switch to application directory +WORKDIR /app + +# ----------------------------------- +# update image os +RUN apt-get update +RUN apt-get install ffmpeg libsm6 libxext6 -y + +# ----------------------------------- +# Copy required files from repo into image +COPY ./deepface /app/deepface +# even though we will use local requirements, this one is required to perform install deepface from source code +COPY ./requirements.txt /app/requirements.txt +COPY ./requirements_local /app/requirements_local.txt +COPY ./package_info.json /app/ +COPY ./setup.py /app/ +COPY ./README.md /app/ + +# ----------------------------------- +# if you plan to use a GPU, you should install the 'tensorflow-gpu' package +# RUN pip install --trusted-host pypi.org --trusted-host pypi.python.org --trusted-host=files.pythonhosted.org tensorflow-gpu + +# if you plan to use face anti-spoofing, then activate this line +# RUN pip install --trusted-host pypi.org --trusted-host pypi.python.org --trusted-host=files.pythonhosted.org torch==2.1.2 +# ----------------------------------- +# install deepface from pypi release (might be out-of-date) +# RUN pip install --trusted-host pypi.org --trusted-host pypi.python.org --trusted-host=files.pythonhosted.org deepface +# ----------------------------------- +# install dependencies - deepface with these dependency versions is working +RUN pip install --trusted-host pypi.org --trusted-host pypi.python.org --trusted-host=files.pythonhosted.org -r /app/requirements_local.txt +# install deepface from source code (always up-to-date) +RUN pip install --trusted-host pypi.org --trusted-host pypi.python.org --trusted-host=files.pythonhosted.org -e . + +# ----------------------------------- +# some packages are optional in deepface. activate if your task depends on one. +# RUN pip install --trusted-host pypi.org --trusted-host pypi.python.org --trusted-host=files.pythonhosted.org cmake==3.24.1.1 +# RUN pip install --trusted-host pypi.org --trusted-host pypi.python.org --trusted-host=files.pythonhosted.org dlib==19.20.0 +# RUN pip install --trusted-host pypi.org --trusted-host pypi.python.org --trusted-host=files.pythonhosted.org lightgbm==2.3.1 + +# ----------------------------------- +# environment variables +ENV PYTHONUNBUFFERED=1 + +# ----------------------------------- +# run the app (re-configure port if necessary) +WORKDIR /app/deepface/api/src +EXPOSE 5000 +CMD ["gunicorn", "--workers=1", "--timeout=3600", "--bind=0.0.0.0:5000", "app:create_app()"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2b0f9fb --- /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 0000000..cb8e9ae --- /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/README.md b/README.md deleted file mode 100644 index b37be1b..0000000 --- a/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# faceta-algo-server - -faceta算法模型库 \ No newline at end of file diff --git a/benchmarks/Evaluate-Results.ipynb b/benchmarks/Evaluate-Results.ipynb new file mode 100644 index 0000000..e2a7172 --- /dev/null +++ b/benchmarks/Evaluate-Results.ipynb @@ -0,0 +1,1844 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "59b076ef", + "metadata": {}, + "source": [ + "# Evaluate DeepFace's Results In The Big Picture" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "79200f8c", + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "from IPython.display import display, HTML\n", + "from sklearn import metrics\n", + "import matplotlib.pyplot as plt" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "bbc11592", + "metadata": {}, + "outputs": [], + "source": [ + "alignment = [False, True]\n", + "models = [\"Facenet512\", \"Facenet\", \"VGG-Face\", \"ArcFace\", \"Dlib\", \"GhostFaceNet\", \"SFace\", \"OpenFace\", \"DeepFace\", \"DeepID\"]\n", + "detectors = [\"retinaface\", \"mtcnn\", \"fastmtcnn\", \"dlib\", \"yolov8\", \"yunet\", \"centerface\", \"mediapipe\", \"ssd\", \"opencv\", \"skip\"]\n", + "distance_metrics = [\"euclidean\", \"euclidean_l2\", \"cosine\"]" + ] + }, + { + "cell_type": "markdown", + "id": "e0dabf1b", + "metadata": {}, + "source": [ + "# Main results" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "03b09fa3", + "metadata": { + "scrolled": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "euclidean for alignment False\n" + ] + }, + { + "data": { + "text/html": [ + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
Facenet512FacenetVGG-FaceArcFaceDlibGhostFaceNetSFaceOpenFaceDeepFaceDeepID
detector
retinaface96.192.895.784.188.383.278.670.867.464.3
mtcnn95.992.595.581.889.383.276.370.965.963.2
fastmtcnn96.393.096.082.290.082.776.871.266.564.3
dlib96.089.094.182.696.365.673.175.961.861.9
yolov894.890.895.283.288.477.671.668.968.266.3
yunet97.996.596.384.191.482.778.271.765.565.2
centerface97.495.495.883.290.382.076.569.965.762.9
mediapipe94.987.193.171.191.961.973.277.661.762.4
ssd97.294.996.783.988.684.982.069.966.764.0
opencv94.190.295.889.891.291.086.971.168.461.1
skip92.064.190.656.669.075.181.457.460.860.7
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "euclidean_l2 for alignment False\n" + ] + }, + { + "data": { + "text/html": [ + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
Facenet512FacenetVGG-FaceArcFaceDlibGhostFaceNetSFaceOpenFaceDeepFaceDeepID
detector
retinaface98.095.995.795.788.489.590.670.867.764.6
mtcnn97.896.295.595.989.288.091.170.967.064.0
fastmtcnn97.796.696.095.989.687.889.771.267.864.2
dlib96.589.994.193.895.663.075.075.962.661.8
yolov897.795.895.295.088.188.789.868.968.965.3
yunet98.396.896.396.191.788.090.571.767.663.2
centerface97.496.395.895.890.286.889.369.968.463.1
mediapipe96.390.093.189.391.865.674.677.664.961.6
ssd97.997.096.796.689.491.593.069.968.764.9
opencv96.292.995.893.291.593.391.771.168.361.6
skip91.467.690.657.269.378.483.457.462.661.6
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "cosine for alignment False\n" + ] + }, + { + "data": { + "text/html": [ + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
Facenet512FacenetVGG-FaceArcFaceDlibGhostFaceNetSFaceOpenFaceDeepFaceDeepID
detector
retinaface98.095.995.795.788.489.590.670.867.763.7
mtcnn97.896.295.595.989.288.091.170.967.064.0
fastmtcnn97.796.696.095.989.687.889.771.267.862.7
dlib96.589.994.193.895.663.075.075.962.661.7
yolov897.795.895.295.088.188.789.868.968.965.3
yunet98.396.896.396.191.788.090.571.767.663.2
centerface97.496.395.895.890.286.889.369.968.462.6
mediapipe96.390.093.189.391.864.874.677.664.961.6
ssd97.997.096.796.689.491.593.069.968.763.8
opencv96.292.995.893.291.593.391.771.168.161.1
skip91.467.690.654.869.378.483.457.462.661.1
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "euclidean for alignment True\n" + ] + }, + { + "data": { + "text/html": [ + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
Facenet512FacenetVGG-FaceArcFaceDlibGhostFaceNetSFaceOpenFaceDeepFaceDeepID
detector
retinaface95.993.595.885.288.985.980.269.467.065.6
mtcnn95.293.895.983.789.483.077.470.266.563.3
fastmtcnn96.093.495.883.591.182.877.769.466.764.0
dlib96.090.894.588.696.865.766.375.863.460.4
yolov894.491.995.084.189.277.673.468.769.066.5
yunet97.396.196.084.992.284.079.470.965.865.2
centerface97.695.895.783.690.482.877.468.965.562.8
mediapipe95.188.692.973.293.163.272.578.761.862.2
ssd88.985.687.075.883.179.176.966.863.462.5
opencv88.284.287.373.084.483.881.166.465.559.6
skip92.064.190.656.669.075.181.457.460.860.7
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "euclidean_l2 for alignment True\n" + ] + }, + { + "data": { + "text/html": [ + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
Facenet512FacenetVGG-FaceArcFaceDlibGhostFaceNetSFaceOpenFaceDeepFaceDeepID
detector
retinaface98.496.495.896.689.190.592.469.467.764.4
mtcnn97.696.895.996.090.089.890.570.266.464.0
fastmtcnn98.197.295.896.491.089.590.069.467.464.1
dlib97.092.694.595.196.463.369.875.866.559.5
yolov897.395.795.095.588.888.991.968.767.566.0
yunet97.997.496.096.791.689.191.070.966.563.6
centerface97.796.895.796.590.987.589.368.967.864.0
mediapipe96.190.692.990.392.664.475.478.764.763.0
ssd88.787.587.086.283.382.284.666.864.162.6
opencv87.684.887.384.684.085.083.666.463.860.9
skip91.467.690.657.269.378.483.457.462.661.6
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "cosine for alignment True\n" + ] + }, + { + "data": { + "text/html": [ + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
Facenet512FacenetVGG-FaceArcFaceDlibGhostFaceNetSFaceOpenFaceDeepFaceDeepID
detector
retinaface98.496.495.896.689.190.592.469.467.764.4
mtcnn97.696.895.996.090.089.890.570.266.363.0
fastmtcnn98.197.295.896.491.089.590.069.467.463.6
dlib97.092.694.595.196.463.369.875.866.558.7
yolov897.395.795.095.588.888.991.968.767.565.9
yunet97.997.496.096.791.689.191.070.966.563.5
centerface97.796.895.796.590.987.589.368.967.863.6
mediapipe96.190.692.990.392.664.375.478.764.863.0
ssd88.787.587.086.283.382.284.566.863.862.6
opencv87.684.987.284.684.085.083.666.263.760.1
skip91.467.690.654.869.378.483.457.462.661.1
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "for align in alignment:\n", + " for metric in distance_metrics:\n", + " df = pd.read_csv(f\"results/pivot_{metric}_with_alignment_{align}.csv\")\n", + " df = df.rename(columns = {'Unnamed: 0': 'detector'})\n", + " df = df.set_index('detector')\n", + "\n", + " print(f\"{metric} for alignment {align}\")\n", + " display(HTML(df.to_html()))\n", + " display(HTML(\"
\"))" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "aef6dc64", + "metadata": {}, + "outputs": [], + "source": [ + "def create_github_table():\n", + " for metric in distance_metrics:\n", + " for align in [True, False]:\n", + " df = pd.read_csv(f\"results/pivot_{metric}_with_alignment_{align}.csv\")\n", + " df = df.rename(columns = {'Unnamed: 0': 'detector'})\n", + " df = df.set_index('detector')\n", + " \n", + " print(f\"Performance Matrix for {metric} while alignment is {align} \\n\")\n", + " header = \"| | \"\n", + " for col_name in df.columns.tolist():\n", + " header += f\"{col_name} |\"\n", + " print(header)\n", + " # -------------------------------\n", + " seperator = \"| --- | \"\n", + " for col_name in df.columns.tolist():\n", + " seperator += \" --- |\"\n", + " print(seperator)\n", + " # -------------------------------\n", + " for index, instance in df.iterrows():\n", + " line = f\"| {instance.name} |\"\n", + " for i in instance.values:\n", + " if i < 97.5:\n", + " line += f\"{i} |\"\n", + " else:\n", + " line += f\"**{i}** |\"\n", + " print(line)\n", + " \n", + " print(\"\\n---------------------------\")" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "5004caaa", + "metadata": {}, + "outputs": [], + "source": [ + "# create_github_table()" + ] + }, + { + "cell_type": "markdown", + "id": "965c655f", + "metadata": {}, + "source": [ + "# Alignment impact" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "6ce20a58", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
max_alignment_impact
ArcFace6.0
DeepFace3.9
GhostFaceNet2.7
Facenet2.7
SFace2.1
Dlib1.4
DeepID1.4
OpenFace1.1
Facenet5120.5
VGG-Face0.4
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "align_df = None\n", + "\n", + "for distance_metric in distance_metrics:\n", + " df1 = (\n", + " pd.read_csv(f\"results/pivot_{distance_metric}_with_alignment_True.csv\")\n", + " .rename(columns = {'Unnamed: 0': 'detector'})\n", + " .set_index('detector')\n", + " )\n", + " df2 = (\n", + " pd.read_csv(f\"results/pivot_{distance_metric}_with_alignment_False.csv\")\n", + " .rename(columns = {'Unnamed: 0': 'detector'})\n", + " .set_index('detector')\n", + " )\n", + " df1 = df1[df1.index != \"skip\"]\n", + " df2 = df2[df2.index != \"skip\"]\n", + " pivot_df = df1.subtract(df2)\n", + " \n", + " pivot_df = pivot_df.max()\n", + " pivot_df = pd.DataFrame(pivot_df, columns=[f'alignment_impact_of_{distance_metric}'])\n", + " # display(HTML(pivot_df.to_html()))\n", + "\n", + " if align_df is None:\n", + " align_df = pivot_df.copy()\n", + " else:\n", + " align_df = align_df.merge(pivot_df, left_index=True, right_index=True)\n", + "\n", + "# display(HTML(align_df.to_html()))\n", + "align_df = pd.DataFrame(align_df.max(axis=1), columns = [\"max_alignment_impact\"])\n", + "align_df = align_df.sort_values(by=[\"max_alignment_impact\"], ascending=False)\n", + "display(HTML(align_df.to_html()))" + ] + }, + { + "cell_type": "markdown", + "id": "f66e349f", + "metadata": {}, + "source": [ + "## Detection impact" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "34eca61b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
max_detection_impact
ArcFace41.8
Facenet32.4
Dlib27.3
OpenFace20.2
GhostFaceNet15.9
SFace9.6
DeepFace7.6
Facenet5126.9
VGG-Face6.1
DeepID5.6
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "detect_df = None\n", + "for distance_metric in distance_metrics:\n", + " tmp_df = (\n", + " pd.read_csv(f\"results/pivot_{distance_metric}_with_alignment_False.csv\")\n", + " .rename(columns = {'Unnamed: 0': 'detector'})\n", + " .set_index('detector')\n", + " )\n", + " ref_df = tmp_df[tmp_df.index == \"skip\"]\n", + " \n", + " j = []\n", + " for i in range(0, len(detectors) - 1):\n", + " j.append(ref_df)\n", + " minus_df = pd.concat(j)\n", + " \n", + " tmp_df = tmp_df[tmp_df.index != \"skip\"]\n", + " minus_df.index = tmp_df.index\n", + " \n", + " # print(\"performance with no detection\")\n", + " # display(HTML(ref_df.to_html()))\n", + " \n", + " # print(\"pivot\")\n", + " tmp_df = tmp_df.subtract(minus_df)\n", + " # display(HTML(tmp_df.to_html()))\n", + " \n", + " # print(\"avg of detector impact for models\")\n", + " # avg_df = pd.DataFrame(tmp_df.mean()).T\n", + " avg_df = pd.DataFrame(tmp_df.max(), columns=[f\"detection_impact_of_{distance_metric}\"])\n", + " # display(HTML(avg_df.to_html()))\n", + "\n", + " if detect_df is None:\n", + " detect_df = avg_df.copy()\n", + " else:\n", + " detect_df = detect_df.merge(avg_df, left_index=True, right_index=True)\n", + "\n", + "# display(HTML(detect_df.to_html()))\n", + "detect_df = pd.DataFrame(detect_df.max(axis=1), columns = [\"max_detection_impact\"])\n", + "detect_df = detect_df.sort_values(by=[\"max_detection_impact\"], ascending=False)\n", + "display(HTML(detect_df.to_html()))\n" + ] + }, + { + "cell_type": "markdown", + "id": "1bdf64a3", + "metadata": {}, + "source": [ + "# facial recognition model's best scores" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "0cb1f232", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
best_accuracy_score
Facenet51298.4
Human-beings97.5
Facenet97.4
Dlib96.8
VGG-Face96.7
ArcFace96.7
GhostFaceNet93.3
SFace93.0
OpenFace78.7
DeepFace69.0
DeepID66.5
\n", + "
" + ], + "text/plain": [ + " best_accuracy_score\n", + "Facenet512 98.4\n", + "Human-beings 97.5\n", + "Facenet 97.4\n", + "Dlib 96.8\n", + "VGG-Face 96.7\n", + "ArcFace 96.7\n", + "GhostFaceNet 93.3\n", + "SFace 93.0\n", + "OpenFace 78.7\n", + "DeepFace 69.0\n", + "DeepID 66.5" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df = pd.DataFrame()\n", + "for align in alignment:\n", + " for distance_metric in distance_metrics:\n", + " tmp_df = (\n", + " pd.read_csv(f\"results/pivot_{distance_metric}_with_alignment_{align}.csv\")\n", + " .rename(columns = {'Unnamed: 0': 'detector'})\n", + " .set_index('detector')\n", + " )\n", + " df = pd.concat([df, tmp_df])\n", + "\n", + "pivot_df = pd.DataFrame(df.max(), columns = [\"best_accuracy_score\"])\n", + "\n", + "# add human comparison\n", + "pivot_df.loc[\"Human-beings\"] = 97.5\n", + "\n", + "pivot_df = pivot_df.sort_values(by = [\"best_accuracy_score\"], ascending = False)\n", + "pivot_df" + ] + }, + { + "cell_type": "markdown", + "id": "b81ebe92", + "metadata": {}, + "source": [ + "# ROC Curves" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "bcb4db0a", + "metadata": {}, + "outputs": [], + "source": [ + "def plot_roc(model_name, detector_backend, distance_metric, align):\n", + " alignment_text = \"aligned\" if align == True else \"unaligned\"\n", + "\n", + " df = pd.read_csv(f\"outputs/test/{model_name}_{detector_backend}_{distance_metric}_{alignment_text}.csv\")\n", + " \n", + " #normalize\n", + " df[\"distances_normalized\"] = df[\"distances\"] / df[\"distances\"].max()\n", + " df[\"actuals_normalized\"] = 0\n", + " idx = df[df[\"actuals\"] == False].index\n", + " df.loc[idx, \"actuals_normalized\"] = 1\n", + " \n", + " y_actual = df[\"actuals_normalized\"].values.tolist()\n", + " y_pred_proba = df[\"distances_normalized\"].values.tolist()\n", + " \n", + " fpr, tpr, _ = metrics.roc_curve(y_actual, y_pred_proba)\n", + " auc = metrics.roc_auc_score(y_actual, y_pred_proba)\n", + " auc = round(auc, 4)\n", + "\n", + " # best accuracy score\n", + " result_path = f\"results/pivot_{distance_metric}_with_alignment_{align}.csv\"\n", + " result_df = pd.read_csv(result_path)\n", + " acc = result_df[result_df[\"Unnamed: 0\"] == detector_backend][model_name].values[0]\n", + "\n", + " label = f\"{model_name}_{detector_backend}_{distance_metric}_{alignment_text} (acc: {acc}, auc: {auc})\"\n", + "\n", + " return acc, auc, fpr, tpr, label" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "84b3d5b5", + "metadata": { + "scrolled": false + }, + "outputs": [ + { + "data": { + "image/png": "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gELKNK4hm3EMmTCuIX9sB76ffPJJHsuAEwZMGSuPP3UbvMfiPxKA5JqamuT3+1OuW5bFzw8AAADABmOMEon2lOvxeNjFagD7sh3XwDiG4pXVSAcUN4/fJ2+awBcAUvH7/WkDXwAAAACZGWO0YOEE25uyAU7JdSM1aXP3rt2wl3EMxY3AFyXJGKOOSCTleiyyycVqAAAAAACQEol222FvXd1oeb3BPFeEUpBzWGukExct19K21J3l2co0roFxDMWNwBclxxijWVMv08oPlhW6FAAAAABAGck0jiGTrcc1HHLwvLQbsnm9QQIzZD1GwQ1j6mrU16ri+7OEEfiiKKXr4I1FNtkOewcOG66qQMDJ0gAAAAAAJShTmLt5HMOpamt715Hr+XyhtIEvSp/bYxQyGVEb1NMjh0o55rR075Y+Al8UnWw6eC+85xFZgeqU61WBAD+kAAAAAKDCuT1bl3ENxa8UxyhkQlCLLXoU+H744YeaOXOmPvzwQ91xxx2qr6/Xn/70J+28887aa6+9nK6xohljZGKpfwCZaNzFatzREYnYCnsHDhuuYO86fpgBAAAAANLKZrZube1wjR41K6fnmoxryB8numrzEdbmijEKcFLWge/LL7+sY445RgcddJBeeeUVXX/99aqvr9dbb72l+++/X0888UQ+6qxIxhitnfG2oitaC11KwaTr4KV7FwAAAACQLWbrlq5inHfLGAUUo6wD3yuuuELXXXedJk2apF69enXe/vWvf1133nmno8VVOhNL2A57/YN7y2P1vO2/WFmBalnVqUc2AAAAAACQDWbrlq5wwrl5t5IzYS1BLYpR1oHvkiVL9Oijj3a7vb6+XuvWrXOkKHQ3YMpYefy+lOseix8wAAAAAACgMBwZtZBBOP7l+XOddysR1qJ8ZR34brfddlq1apV22WWXLrcvWrRIO+20k2OFoSuP3ydvmsAXAAAAAACgEAoxaiHk86rGR04CJJN14Hvqqafq8ssv1+OPPy6Px6NEIqHXX39dTU1NOvvss/NRIwCgB4wxisViKdej0aiL1QAAAAAoRk505objzo5ayGRMXY1C3vIbawk4JevA94YbbtBFF12khoYGxeNxDR8+XPF4XKeffrqmTJmSjxoBAFkyxqi5uVktLS2FLgUAAADIO2OMEon2lOvxeNjFakpHPjpznRi1kAmjGID0sg58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f2233XaqqanRfvvtp4cffrjLNXjcAKRXsYGviRklovFubyYaL3RpQN4dffTRWrVqVZe3XXbZxdFrZOre3baDd3httUb0CiZ9Gxqyv/nh+++/3+Xjqq+v71zbuHGj9t13X911111J7xsOh7Vw4UJdddVVWrhwoZ588km9//77OuGEE3r8ediywV9P1NXVOd7l0dbWpn/9618aP368Bg4cqF69ejl6/mITDAa7fA8Uox122KGkvg4///nP1djYKK+3eB5CrFy5UuPGjdPQoUM1b948Pf/883rnnXf0ne98p/OYtrY2HX300dp55501b948vfbaa+rVq5fGjx9v6//p9OnTS/4PhB9//LGOO+44HXHEEVq8eLF++MMf6txzz9ULL7yQ8j5/+tOfdMYZZ+iCCy7Q0qVL9ctf/lK333677rzzzs5j7r77bk2ePFlXX3213nnnHV1zzTW66KKL9Ic//KHzmCeffLLLz+alS5fK5/Pp5JNP7jzmmGOO0YYNG0r6yXMhffTRRxo5cqT+/Oc/64YbbtCiRYs0d+5cXXbZZXr22Wf10ksvFbpESdLMmTO7fC8888wzrl27urpa999/v/7xj3+4dk04o7a2Vn369Cl0GXlXCo9btvbEE0+od+/eOuiggwpdShfnnnuuXnzxRT388MNasmSJjjrqKI0bN06fffZZ5zFf+cpXdOedd2rJkiV67bXXNGTIEB111FFau3ZtyvMOGjRIN910kxYsWKA333xTX//613XiiSfqnXfecePDclQ8Htdxxx2naDSqv/3tb3rwwQf1wAMPaOrUqSnvY+fxliTddtttuvLKK3XFFVfonXfe0UsvvaTx48d3rjc1NXV7Ljp8+PAujwl22GEHXXnllZo7d67efvttNTY2qrGxsctjFh43ABmYCrN+/Xojybz7wz+ZlstfSfsWj3QkPUe0vd3cOuE4c+uE40y0vd3ljyB7kUjETJs2zUybNs1EIpFCl+OIRCJhOjo2pnyLRNaal2bval6avavp6Njo+PXb29vNu+++a9pL4Ou/rYkTJ5oTTzwx6drPfvYzM2LECBMKhcygQYPMhRdeaDZs2NDlmNdee80cdthhJhgMmu22284cddRR5vPPPzfGGBOPx80NN9xghgwZYqqrq81XRowwtzz4iFm8fqNZvH6juffZPxlJ5ldPP2uG7zfSVAeDZp8xY83zC98yiUSi8xpPPfWUGTlypAkEAmaXXXYxV199tYnFYsYYYwYPHmwkdb4NHjzYGGPMX//6VyPJ/Oc//7H1eZBkfv/732c87o033jCSzIoVKzIe+/HHHxtJZtasWebQQw81gUDAzJw50xhjzL333mv22GMPEwgEzLBhw8xdd93VpZat3w477DBjTPev1WGHHWa+//3vm0svvdRsv/32pl+/fmbatGldakj3NdzyOdr67a9//atZt26dOfXUU83AgQNNMBg0I0aMMI8++miX88bjcXPzzTeb3Xbbzfj9ftPQ0GCuu+66zvVPP/3UnHzyyaaurs5sv/325oQTTjAff/xxxs/ZFvfff78ZPny48fv9pn///uaiiy7qXFuxYoU54YQTTE1NjenVq5c5+eSTzerVqzvXFy9ebA4//HBTW1trevXqZUaNGmXmz59vjDFm5syZpq6urvPYadOmmX333dc89NBDZvDgwaZ3797mlFNOMa2trV0+1q2/j/fZZx/z+OOP2/o4Ojo6zHe/+90v/w985Stm+vTpXY5J9nW95JJLOt9fuXKlOfbYY011dbUZMmSI+fWvf20GDx5sbr/99s5jJJl7773XnHTSSSYYDJqhQ4eap59+ust1lixZYo4++mhTU1Nj6uvrzZlnnmnWrl3bud7W1mbOOussU1NTY/r3729uvfXWbrVs61//+pfxeDxm6dKlXW534mdHuu+vTH71q1+Z+vp6E4/HO297++23jSTzj3/8wxhjzPz5840k8+mnn6Y8JpVFixaZnXbayaxatcr2z46tLV++3Jxwwgmmvr7e1NTUmP3339+8+OKLXY5Jdt66urrOnyHGGNPS0mJOPfVUs/3225tQKGRGjx5t/v73v9uu47LLLjN77bVXl9tOOeUUM378+JT3Oe2008y3v/3tLrf9/Oc/N4MGDer8uX3AAQeYpqamLsdMmjTJHHTQQSnPe/vtt5tevXqZtra2Lrc3NjaaM88809bHg67Gjx9vBg0a1O1zusXWv2ft/AyZM2eO+epXv9r5c/nyyy/v/F1sjDGPP/64GTFihKmurjY77LCDOfLII01bW5uZNm1a0t81W66b7P+PG7+Htvz8/8Y3vmFOPvnkztsXLVpkJHU5Nt3Pz4kTJ3b7+Oz8vsv0+TzssMPMRRddZC666CLTu3dv06dPHzNlypQuX7dNmzaZH//4x2bgwIEmFAqZMWPGdH5ujfnyd97zzz9v9thjD1NTU2PGjx9vVq5c2aWWVL9zTzvtNDNhwoQux0ajUdOnTx/z4IMPZvwYM32N3n77bXPEEUd0fs+cd955XX5X/PWvfzVf/epXTSgUMnV1debAAw80n3zyiTHmy6/fFlt+l95yyy2mf//+ZocddjDf+973TDQatf35Smfb6xmz+efWlseddmt46KGHzOjRo01tba3p16+fOe2008yaNWu6fMxbP37d9nGLMcZce+21ZscddzS1tbXmnHPOMZdffnlePhczZ840DQ0NJhgMmpNOOsnceuut3WrZ1nHHHdft5/8bb7xhxo0bZ/r06WN69+5tDj30ULNgwYIux/znP/8x559/vqmvrzeBQMDstdde5g9/+EPnerrHDJmEw2Hj8/nMs88+2+X2UaNGmSuvvDLl/bbkBC+99JKt62yx/fbbm/vuu8/28XYeKyZ7PHbiiSeaiRMndr6/adMmc9lll5lBgwYZv99vdtttt6zqeO6554zX6+3ymPruu+82vXv3TpkZ2Hm89fnnn5tgMJjV53Hx4sVGknnllVfSHjdy5EgzZcqULre5/bihlHMAFI8tP2/Wr1+f1+sUT3tOkfEP7i2PxaenGBljtGDhBM15ee+Ub6++NrYgdYWjHQV5Mw5tbub1evXzn/9c77zzjh588EH95S9/0WWXXda5vnjxYh155JEaPny45s6dq9dee03HH3+84vHNnfE33nijHnroIc2YMUNvL12qM753sa48/xytWzBPI3oFtUsoIEm6/4Zr9Yvbb9Mb8+erd8Cvn1x8YWfn3Kuvvqqzzz5bl1xyid5991396le/0gMPPKDrr79ekjpfjrWlQ2jbl2ftt99+GjBggL7xjW/o9ddfz/lzsn79enk8nqw6ba+44gpdcsklWrZsmcaPH69f//rXmjp1qq6//notW7ZMN9xwg6666io9+OCDkqQ33nhDkvTSSy9p1apVevLJJ1Oe+8EHH1RNTY3mzZunn/70p/rJT36iF198sXM93dfwwAMP1Pvvvy9J+t3vfqdVq1bpwAMP1KZNmzR69Gj98Y9/1NKlS3X++efrrLPO6qxLkiZPnqybbrpJV111ld599109+uij6tevn6TNXczjx49Xr1699Oqrr+r1119XbW2tjj76aEWj0Yyfr7vvvlsXXXSRzj//fC1ZskTPPPOMhg4dKklKJBI68cQT9fnnn+vll1/Wiy++qI8++kinnHJK5/3POOMMDRo0SPPnz9eCBQt0xRVXyLKslNf78MMP9dRTT+nZZ5/Vs88+q5dfflk33XRT5/rW38fvvPOOfvSjH+nMM8/Uyy+/nPFjSSQSGjRokB5//HG9++67mjp1qv7v//5Pjz32WMb7brFljMicOXP0u9/9Tvfcc4/+9a9/dTvummuu0YQJE/T222/r2GOP1RlnnKHPP/9ckvTFF1/o61//ukaOHKk333xTzz//vNasWaMJEyZ03v/SSy/Vyy+/rKefflp//vOfNWfOHC1cmH6zy9dee02hUEh77rlnl9tz/dmR7vtL2vzy6W27R7YWiUTk9/u7dB0Hg8HOmiVp2LBh6tOnj+6//35Fo1G1t7fr/vvv15577qkhQ4akPHc4HNbpp5+uu+66S/3790/7+Umlra1Nxx57rGbPnq1Fixbp6KOP1vHHH69PP/00q3Mcdthh+uyzz/TMM8/orbfe0mWXXdb5MsYto2HmzJmT8hxz587VuHHjutw2fvx4zZ07N+V9IpGIqquru9wWDAb1z3/+UytWrEh7zBtvvJGye/r+++/Xqaee2uXlm5I0ZswYvfrqqynrKQRjjGKbNhXkze7v93//+9/685//rIsuuqjb53SLbTvU0/0M+eyzz3Tsscfqq1/9qt566y3dfffduv/++3XddddJklatWqXTTjtN3/3ud7Vs2TLNmTNH3/zmN2WMUVNTkyZMmNDl1UQHHnhg2vrd/D1000036Xe/+53efPPNpLVk+vl5xx136IADDtB5553X+fE1NDSk/fgyfT63ePDBB1VVVaU33nhDd9xxh2677Tbdd999nesXX3yx5s6dq1mzZuntt9/WySefrKOPPrpLx3I4HNatt96qhx9+WK+88oo+/fRTNTU1da6n+517xhln6A9/+EOXl2e/8MILCofD+n//7/+l/Ril9F+jjRs3avz48dp+++01f/58Pf7443rppZd08cUXS5I6Ojp00kkn6bDDDtPbb7+tuXPn6vzzz0/7yoq//vWv+vDDD/XXv/61sztx6zE1dj5fucpUQywW07XXXqu33npLTz31lD755JO0v8+29etf/1rXX3+9br75Zi1YsEA777yz7r777qzryPS5mDdvns455xxdfPHFWrx4sY444ohu35/JvPbaa9p///273LZhwwZNnDhRr732mv7+979r991317HHHqsNGzZI2vxY6ZhjjtHrr7+uRx55RO+++65uuukm+Xw+SZkfMzzwwANpvy86OjoUj8eT/l7a8phgW9FoVPfcc4/q6uq07777Zvy4pc0dsrNmzdLGjRt1wAEH2LqP5MxjRWnz48Xf/OY3+vnPf65ly5bpV7/6lWprazvXhwwZoquvvjrl/efOnau99967y+Ot8ePHq7W1NWXHsp3HWy+++KISiYQ+++wz7bnnnho0aJAmTJiglpaWlLXcd999+spXvqJDDjkk6boxRrNnz9b777+vQw89tMtaMT5uAIpGXuPkIrQlSX//jQUmHulI+bb1X9O3RYdvYXV0bOzs3s30Nv/Nk9N+LXsq2V/2NkZiZvDlzxbkbWMklqbariZOnGh8Pp+pqanpfNu2e2uLxx9/3PTp06fz/dNOOy1lx9amTZtMKBQyf/vb34wxxnQkEmbx+o3m/5010Zx62mnGmC+7GLb+i+8f//hHI6nzc3nkkUeaG264ocu5H374YTNgwIDO95WkQ+i9994zM2bMMG+++aZ5/fXXTWNjo6mqqurWUZDuHNtqb283o0aNMqeffnra47bY0uG77V/pd9ttt26dStdee6054IADutxv0aJFXY5J1gl68MEHdznmq1/9qrn88stT1rTt1/A///lPl26rVI477jjz4x//2BhjTGtrqwkEAubee+9NeuzDDz9shg0b1uX/WiQSMcFg0Lzwwgtpr2OMMQMHDkzZcfHnP//Z+Hy+Ll2Z77zzjpFk3njjDWOMMb169TIPPPBA0vsn6/ANhUJdOnovvfRSM3bsWGNM9+/jLc455xxz2n+/j7N10UUXmW9961ud76fr8F22bJmR1NmhbIwx//jHP4ykbh2+W3c4tLW1GUnmT3/6kzFm8/fXUUcd1aWOlpaWzb//3n/fbNiwwfj9fvPYY491rv/73/82wWAwbYfv7bffbnbdddeMH3M2PzsyfX8ZY8xZZ51lrrjiipTrS5cuNVVVVeanP/2piUQi5vPPPzff+ta3jKQuP0+WLFlidtttN+P1eo3X6zXDhg3r7B5L5fzzzzfnnHNO5/t2fnbYsddee5lf/OIXac+7dYfvr371K9OrVy/z73//O+n5/vnPf5phw4aZefPmpbzm7rvv3u3n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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# to show all models in same graph\n", + "plt.figure(figsize=(17, 8))\n", + "\n", + "for model_name in models:\n", + " # to show graphs model by model\n", + " # plt.figure(figsize=(17, 8))\n", + " accs = []\n", + " aucs = []\n", + " fprs = []\n", + " tprs = []\n", + " labels = []\n", + " for distance_metric in distance_metrics:\n", + " # for detector_backend in robust_face_detectors:\n", + " for detector_backend in detectors:\n", + " for align in alignment:\n", + " if detector_backend == \"skip\" and align is True:\n", + " continue\n", + " acc, auc, fpr, tpr, label = plot_roc(model_name, detector_backend, distance_metric, align)\n", + " accs.append(acc)\n", + " aucs.append(auc)\n", + " fprs.append(fpr)\n", + " tprs.append(tpr)\n", + " labels.append(label)\n", + " # ---------------------------------\n", + " #sort by auc\n", + " df = pd.DataFrame({\"acc\": accs, \"auc\": aucs, \"fpr\": fprs, \"tpr\": tprs, \"label\": labels})\n", + " # df = df.sort_values(by = [\"auc\"], ascending = False).reset_index()\n", + " df = df.sort_values(by = [\"acc\"], ascending = False).reset_index()\n", + " \n", + " for index, instance in df.iterrows():\n", + " fpr = instance[\"fpr\"]\n", + " tpr = instance[\"tpr\"]\n", + " auc = instance[\"auc\"]\n", + " acc = instance[\"acc\"]\n", + " label = instance[\"label\"]\n", + " \n", + " plt.plot(fpr, tpr, label=label)\n", + " plt.ylabel(\"True Positive Rate\")\n", + " plt.xlabel(\"False Positive Rate\")\n", + " plt.legend(loc=\"lower center\", ncol=2)\n", + " # normally this should be [0, 1] but that scale makes graphs not legible\n", + " # plt.xlim([0, 1])\n", + " plt.xlim([0, 0.3])\n", + "\n", + " # to show the best auc value\n", + " break\n", + " \n", + " # to show graphs model by model\n", + " # plt.show()\n", + " # print(\"----------------\")\n", + "\n", + "# to show all models in same graph\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "661c5236", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.16" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/benchmarks/Perform-Experiments.ipynb b/benchmarks/Perform-Experiments.ipynb new file mode 100644 index 0000000..977e690 --- /dev/null +++ b/benchmarks/Perform-Experiments.ipynb @@ -0,0 +1,352 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "8133a99d", + "metadata": {}, + "source": [ + "# Perform Experiments with DeepFace on LFW dataset" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "5aab0cbe", + "metadata": {}, + "outputs": [], + "source": [ + "# built-in dependencies\n", + "import os\n", + "\n", + "# 3rd party dependencies\n", + "import numpy as np\n", + "import pandas as pd\n", + "from tqdm import tqdm\n", + "import matplotlib.pyplot as plt\n", + "from sklearn.metrics import accuracy_score\n", + "from sklearn.datasets import fetch_lfw_pairs\n", + "from deepface import DeepFace" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "64c9ed9a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "This experiment is done with pip package of deepface with 0.0.90 version\n" + ] + } + ], + "source": [ + "print(f\"This experiment is done with pip package of deepface with {DeepFace.__version__} version\")" + ] + }, + { + "cell_type": "markdown", + "id": "feaec973", + "metadata": {}, + "source": [ + "### Configuration Sets" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "453104b4", + "metadata": {}, + "outputs": [], + "source": [ + "# all configuration alternatives for 4 dimensions of arguments\n", + "alignment = [True, False]\n", + "models = [\"Facenet512\", \"Facenet\", \"VGG-Face\", \"ArcFace\", \"Dlib\", \"GhostFaceNet\", \"SFace\", \"OpenFace\", \"DeepFace\", \"DeepID\"]\n", + "detectors = [\"retinaface\", \"mtcnn\", \"fastmtcnn\", \"dlib\", \"yolov8\", \"yunet\", \"centerface\", \"mediapipe\", \"ssd\", \"opencv\", \"skip\"]\n", + "metrics = [\"euclidean\", \"euclidean_l2\", \"cosine\"]\n", + "expand_percentage = 0" + ] + }, + { + "cell_type": "markdown", + "id": "c9aeb57a", + "metadata": {}, + "source": [ + "### Create Required Folders if necessary" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "671d8a00", + "metadata": {}, + "outputs": [], + "source": [ + "target_paths = [\"lfwe\", \"dataset\", \"outputs\", \"outputs/test\", \"results\"]\n", + "for target_path in target_paths:\n", + " if not os.path.exists(target_path):\n", + " os.mkdir(target_path)\n", + " print(f\"{target_path} is just created\")" + ] + }, + { + "cell_type": "markdown", + "id": "fc31f03a", + "metadata": {}, + "source": [ + "### Load LFW Dataset" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "721a7d70", + "metadata": {}, + "outputs": [], + "source": [ + "pairs_touch = \"outputs/test_lfwe.txt\"\n", + "instances = 1000 #pairs.shape[0]" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "010184d8", + "metadata": {}, + "outputs": [], + "source": [ + "target_path = \"dataset/test_lfw.npy\"\n", + "labels_path = \"dataset/test_labels.npy\"\n", + "\n", + "if os.path.exists(target_path) != True:\n", + " fetch_lfw_pairs = fetch_lfw_pairs(subset = 'test', color = True\n", + " , resize = 2\n", + " , funneled = False\n", + " , slice_=None\n", + " )\n", + " pairs = fetch_lfw_pairs.pairs\n", + " labels = fetch_lfw_pairs.target\n", + " target_names = fetch_lfw_pairs.target_names\n", + " np.save(target_path, pairs)\n", + " np.save(labels_path, labels)\n", + "else:\n", + " if not os.path.exists(pairs_touch):\n", + " # loading pairs takes some time. but if we extract these pairs as image, no need to load it anymore\n", + " pairs = np.load(target_path)\n", + " labels = np.load(labels_path) " + ] + }, + { + "cell_type": "markdown", + "id": "005f582e", + "metadata": {}, + "source": [ + "### Save LFW image pairs into file system" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "5bc23313", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 1000/1000 [00:00<00:00, 190546.25it/s]\n" + ] + } + ], + "source": [ + "for i in tqdm(range(0, instances)):\n", + " img1_target = f\"lfwe/test/{i}_1.jpg\"\n", + " img2_target = f\"lfwe/test/{i}_2.jpg\"\n", + " \n", + " if not os.path.exists(img1_target):\n", + " img1 = pairs[i][0]\n", + " # plt.imsave(img1_target, img1/255) #works for my mac\n", + " plt.imsave(img1_target, img1) #works for my debian\n", + " \n", + " if not os.path.exists(img2_target):\n", + " img2 = pairs[i][1]\n", + " # plt.imsave(img2_target, img2/255) #works for my mac\n", + " plt.imsave(img2_target, img2) #works for my debian\n", + " \n", + "if not os.path.exists(pairs_touch):\n", + " open(pairs_touch,'a').close()" + ] + }, + { + "cell_type": "markdown", + "id": "6f8fa8fa", + "metadata": {}, + "source": [ + "### Perform Experiments\n", + "\n", + "This block will save the experiments results in outputs folder" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "e7fba936", + "metadata": {}, + "outputs": [], + "source": [ + "for model_name in models:\n", + " for detector_backend in detectors:\n", + " for distance_metric in metrics:\n", + " for align in alignment:\n", + " \n", + " if detector_backend == \"skip\" and align is True:\n", + " # Alignment is not possible for a skipped detector configuration\n", + " continue\n", + " \n", + " alignment_text = \"aligned\" if align is True else \"unaligned\"\n", + " task = f\"{model_name}_{detector_backend}_{distance_metric}_{alignment_text}\"\n", + " output_file = f\"outputs/test/{task}.csv\"\n", + " if os.path.exists(output_file):\n", + " #print(f\"{output_file} is available already\")\n", + " continue\n", + " \n", + " distances = []\n", + " for i in tqdm(range(0, instances), desc = task):\n", + " img1_target = f\"lfwe/test/{i}_1.jpg\"\n", + " img2_target = f\"lfwe/test/{i}_2.jpg\"\n", + " result = DeepFace.verify(\n", + " img1_path=img1_target,\n", + " img2_path=img2_target,\n", + " model_name=model_name,\n", + " detector_backend=detector_backend,\n", + " distance_metric=distance_metric,\n", + " align=align,\n", + " enforce_detection=False,\n", + " expand_percentage=expand_percentage,\n", + " )\n", + " distance = result[\"distance\"]\n", + " distances.append(distance)\n", + " # -----------------------------------\n", + " df = pd.DataFrame(list(labels), columns = [\"actuals\"])\n", + " df[\"distances\"] = distances\n", + " df.to_csv(output_file, index=False)" + ] + }, + { + "cell_type": "markdown", + "id": "a0b8dafa", + "metadata": {}, + "source": [ + "### Calculate Results\n", + "\n", + "Experiments were responsible for calculating distances. We will calculate the best accuracy scores in this block." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "67376e76", + "metadata": {}, + "outputs": [], + "source": [ + "data = [[0 for _ in range(len(models))] for _ in range(len(detectors))]\n", + "base_df = pd.DataFrame(data, columns=models, index=detectors)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "f2cc536b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "results/pivot_euclidean_with_alignment_True.csv saved\n", + "results/pivot_euclidean_l2_with_alignment_True.csv saved\n", + "results/pivot_cosine_with_alignment_True.csv saved\n", + "results/pivot_euclidean_with_alignment_False.csv saved\n", + "results/pivot_euclidean_l2_with_alignment_False.csv saved\n", + "results/pivot_cosine_with_alignment_False.csv saved\n" + ] + } + ], + "source": [ + "for is_aligned in alignment:\n", + " for distance_metric in metrics:\n", + "\n", + " current_df = base_df.copy()\n", + " \n", + " target_file = f\"results/pivot_{distance_metric}_with_alignment_{is_aligned}.csv\"\n", + " if os.path.exists(target_file):\n", + " continue\n", + " \n", + " for model_name in models:\n", + " for detector_backend in detectors:\n", + "\n", + " align = \"aligned\" if is_aligned is True else \"unaligned\"\n", + "\n", + " if detector_backend == \"skip\" and is_aligned is True:\n", + " # Alignment is not possible for a skipped detector configuration\n", + " align = \"unaligned\"\n", + "\n", + " source_file = f\"outputs/test/{model_name}_{detector_backend}_{distance_metric}_{align}.csv\"\n", + " df = pd.read_csv(source_file)\n", + " \n", + " positive_mean = df[(df[\"actuals\"] == True) | (df[\"actuals\"] == 1)][\"distances\"].mean()\n", + " negative_mean = df[(df[\"actuals\"] == False) | (df[\"actuals\"] == 0)][\"distances\"].mean()\n", + "\n", + " distances = sorted(df[\"distances\"].values.tolist())\n", + "\n", + " items = []\n", + " for i, distance in enumerate(distances):\n", + " if distance >= positive_mean and distance <= negative_mean:\n", + " sandbox_df = df.copy()\n", + " sandbox_df[\"predictions\"] = False\n", + " idx = sandbox_df[sandbox_df[\"distances\"] < distance].index\n", + " sandbox_df.loc[idx, \"predictions\"] = True\n", + "\n", + " actuals = sandbox_df.actuals.values.tolist()\n", + " predictions = sandbox_df.predictions.values.tolist()\n", + " accuracy = 100*accuracy_score(actuals, predictions)\n", + " items.append((distance, accuracy))\n", + "\n", + " pivot_df = pd.DataFrame(items, columns = [\"distance\", \"accuracy\"])\n", + " pivot_df = pivot_df.sort_values(by = [\"accuracy\"], ascending = False)\n", + " threshold = pivot_df.iloc[0][\"distance\"]\n", + " # print(f\"threshold for {model_name}/{detector_backend} is {threshold}\")\n", + " accuracy = pivot_df.iloc[0][\"accuracy\"]\n", + "\n", + " # print(source_file, round(accuracy, 1))\n", + " current_df.at[detector_backend, model_name] = round(accuracy, 1)\n", + " \n", + " current_df.to_csv(target_file)\n", + " print(f\"{target_file} saved\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.16" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/benchmarks/README.md b/benchmarks/README.md new file mode 100644 index 0000000..1370c8e --- /dev/null +++ b/benchmarks/README.md @@ -0,0 +1,134 @@ +# Benchmarks + +[`🎥 Video Tutorial`](https://youtu.be/eKOZawGR3y0) + +DeepFace offers various configurations that significantly impact accuracy, including the facial recognition model, face detector model, distance metric, and alignment mode. Our experiments conducted on the [LFW dataset](https://sefiks.com/2020/08/27/labeled-faces-in-the-wild-for-face-recognition/) using different combinations of these configurations yield the following results. + +You can reproduce the results by executing the `Perform-Experiments.ipynb` and `Evaluate-Results.ipynb` notebooks, respectively. + +## ROC Curves + +ROC curves provide a valuable means of evaluating the performance of different models on a broader scale. The following illusration shows ROC curves for different facial recognition models alongside their optimal configurations yielding the highest accuracy scores. + +

+ +In summary, FaceNet-512d surpasses human-level accuracy, while FaceNet-128d reaches it, with Dlib, VGG-Face, and ArcFace closely trailing but slightly below, and GhostFaceNet and SFace making notable contributions despite not leading, while OpenFace, DeepFace, and DeepId exhibit lower performance. + +## Accuracy Scores + +Please note that humans achieve a 97.5% accuracy score on the same dataset. Configurations that outperform this benchmark are highlighted in bold. + +## Performance Matrix for euclidean while alignment is True + +| | Facenet512 |Facenet |VGG-Face |ArcFace |Dlib |GhostFaceNet |SFace |OpenFace |DeepFace |DeepID | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| retinaface |95.9 |93.5 |95.8 |85.2 |88.9 |85.9 |80.2 |69.4 |67.0 |65.6 | +| mtcnn |95.2 |93.8 |95.9 |83.7 |89.4 |83.0 |77.4 |70.2 |66.5 |63.3 | +| fastmtcnn |96.0 |93.4 |95.8 |83.5 |91.1 |82.8 |77.7 |69.4 |66.7 |64.0 | +| dlib |96.0 |90.8 |94.5 |88.6 |96.8 |65.7 |66.3 |75.8 |63.4 |60.4 | +| yolov8 |94.4 |91.9 |95.0 |84.1 |89.2 |77.6 |73.4 |68.7 |69.0 |66.5 | +| yunet |97.3 |96.1 |96.0 |84.9 |92.2 |84.0 |79.4 |70.9 |65.8 |65.2 | +| centerface |**97.6** |95.8 |95.7 |83.6 |90.4 |82.8 |77.4 |68.9 |65.5 |62.8 | +| mediapipe |95.1 |88.6 |92.9 |73.2 |93.1 |63.2 |72.5 |78.7 |61.8 |62.2 | +| ssd |88.9 |85.6 |87.0 |75.8 |83.1 |79.1 |76.9 |66.8 |63.4 |62.5 | +| opencv |88.2 |84.2 |87.3 |73.0 |84.4 |83.8 |81.1 |66.4 |65.5 |59.6 | +| skip |92.0 |64.1 |90.6 |56.6 |69.0 |75.1 |81.4 |57.4 |60.8 |60.7 | + +## Performance Matrix for euclidean while alignment is False + +| | Facenet512 |Facenet |VGG-Face |ArcFace |Dlib |GhostFaceNet |SFace |OpenFace |DeepFace |DeepID | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| retinaface |96.1 |92.8 |95.7 |84.1 |88.3 |83.2 |78.6 |70.8 |67.4 |64.3 | +| mtcnn |95.9 |92.5 |95.5 |81.8 |89.3 |83.2 |76.3 |70.9 |65.9 |63.2 | +| fastmtcnn |96.3 |93.0 |96.0 |82.2 |90.0 |82.7 |76.8 |71.2 |66.5 |64.3 | +| dlib |96.0 |89.0 |94.1 |82.6 |96.3 |65.6 |73.1 |75.9 |61.8 |61.9 | +| yolov8 |94.8 |90.8 |95.2 |83.2 |88.4 |77.6 |71.6 |68.9 |68.2 |66.3 | +| yunet |**97.9** |96.5 |96.3 |84.1 |91.4 |82.7 |78.2 |71.7 |65.5 |65.2 | +| centerface |97.4 |95.4 |95.8 |83.2 |90.3 |82.0 |76.5 |69.9 |65.7 |62.9 | +| mediapipe |94.9 |87.1 |93.1 |71.1 |91.9 |61.9 |73.2 |77.6 |61.7 |62.4 | +| ssd |97.2 |94.9 |96.7 |83.9 |88.6 |84.9 |82.0 |69.9 |66.7 |64.0 | +| opencv |94.1 |90.2 |95.8 |89.8 |91.2 |91.0 |86.9 |71.1 |68.4 |61.1 | +| skip |92.0 |64.1 |90.6 |56.6 |69.0 |75.1 |81.4 |57.4 |60.8 |60.7 | + +## Performance Matrix for euclidean_l2 while alignment is True + +| | Facenet512 |Facenet |VGG-Face |ArcFace |Dlib |GhostFaceNet |SFace |OpenFace |DeepFace |DeepID | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| retinaface |**98.4** |96.4 |95.8 |96.6 |89.1 |90.5 |92.4 |69.4 |67.7 |64.4 | +| mtcnn |**97.6** |96.8 |95.9 |96.0 |90.0 |89.8 |90.5 |70.2 |66.4 |64.0 | +| fastmtcnn |**98.1** |97.2 |95.8 |96.4 |91.0 |89.5 |90.0 |69.4 |67.4 |64.1 | +| dlib |97.0 |92.6 |94.5 |95.1 |96.4 |63.3 |69.8 |75.8 |66.5 |59.5 | +| yolov8 |97.3 |95.7 |95.0 |95.5 |88.8 |88.9 |91.9 |68.7 |67.5 |66.0 | +| yunet |**97.9** |97.4 |96.0 |96.7 |91.6 |89.1 |91.0 |70.9 |66.5 |63.6 | +| centerface |**97.7** |96.8 |95.7 |96.5 |90.9 |87.5 |89.3 |68.9 |67.8 |64.0 | +| mediapipe |96.1 |90.6 |92.9 |90.3 |92.6 |64.4 |75.4 |78.7 |64.7 |63.0 | +| ssd |88.7 |87.5 |87.0 |86.2 |83.3 |82.2 |84.6 |66.8 |64.1 |62.6 | +| opencv |87.6 |84.8 |87.3 |84.6 |84.0 |85.0 |83.6 |66.4 |63.8 |60.9 | +| skip |91.4 |67.6 |90.6 |57.2 |69.3 |78.4 |83.4 |57.4 |62.6 |61.6 | + +## Performance Matrix for euclidean_l2 while alignment is False + +| | Facenet512 |Facenet |VGG-Face |ArcFace |Dlib |GhostFaceNet |SFace |OpenFace |DeepFace |DeepID | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| retinaface |**98.0** |95.9 |95.7 |95.7 |88.4 |89.5 |90.6 |70.8 |67.7 |64.6 | +| mtcnn |**97.8** |96.2 |95.5 |95.9 |89.2 |88.0 |91.1 |70.9 |67.0 |64.0 | +| fastmtcnn |**97.7** |96.6 |96.0 |95.9 |89.6 |87.8 |89.7 |71.2 |67.8 |64.2 | +| dlib |96.5 |89.9 |94.1 |93.8 |95.6 |63.0 |75.0 |75.9 |62.6 |61.8 | +| yolov8 |**97.7** |95.8 |95.2 |95.0 |88.1 |88.7 |89.8 |68.9 |68.9 |65.3 | +| yunet |**98.3** |96.8 |96.3 |96.1 |91.7 |88.0 |90.5 |71.7 |67.6 |63.2 | +| centerface |97.4 |96.3 |95.8 |95.8 |90.2 |86.8 |89.3 |69.9 |68.4 |63.1 | +| mediapipe |96.3 |90.0 |93.1 |89.3 |91.8 |65.6 |74.6 |77.6 |64.9 |61.6 | +| ssd |**97.9** |97.0 |96.7 |96.6 |89.4 |91.5 |93.0 |69.9 |68.7 |64.9 | +| opencv |96.2 |92.9 |95.8 |93.2 |91.5 |93.3 |91.7 |71.1 |68.3 |61.6 | +| skip |91.4 |67.6 |90.6 |57.2 |69.3 |78.4 |83.4 |57.4 |62.6 |61.6 | + +## Performance Matrix for cosine while alignment is True + +| | Facenet512 |Facenet |VGG-Face |ArcFace |Dlib |GhostFaceNet |SFace |OpenFace |DeepFace |DeepID | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| retinaface |**98.4** |96.4 |95.8 |96.6 |89.1 |90.5 |92.4 |69.4 |67.7 |64.4 | +| mtcnn |**97.6** |96.8 |95.9 |96.0 |90.0 |89.8 |90.5 |70.2 |66.3 |63.0 | +| fastmtcnn |**98.1** |97.2 |95.8 |96.4 |91.0 |89.5 |90.0 |69.4 |67.4 |63.6 | +| dlib |97.0 |92.6 |94.5 |95.1 |96.4 |63.3 |69.8 |75.8 |66.5 |58.7 | +| yolov8 |97.3 |95.7 |95.0 |95.5 |88.8 |88.9 |91.9 |68.7 |67.5 |65.9 | +| yunet |**97.9** |97.4 |96.0 |96.7 |91.6 |89.1 |91.0 |70.9 |66.5 |63.5 | +| centerface |**97.7** |96.8 |95.7 |96.5 |90.9 |87.5 |89.3 |68.9 |67.8 |63.6 | +| mediapipe |96.1 |90.6 |92.9 |90.3 |92.6 |64.3 |75.4 |78.7 |64.8 |63.0 | +| ssd |88.7 |87.5 |87.0 |86.2 |83.3 |82.2 |84.5 |66.8 |63.8 |62.6 | +| opencv |87.6 |84.9 |87.2 |84.6 |84.0 |85.0 |83.6 |66.2 |63.7 |60.1 | +| skip |91.4 |67.6 |90.6 |54.8 |69.3 |78.4 |83.4 |57.4 |62.6 |61.1 | + +## Performance Matrix for cosine while alignment is False + +| | Facenet512 |Facenet |VGG-Face |ArcFace |Dlib |GhostFaceNet |SFace |OpenFace |DeepFace |DeepID | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| retinaface |**98.0** |95.9 |95.7 |95.7 |88.4 |89.5 |90.6 |70.8 |67.7 |63.7 | +| mtcnn |**97.8** |96.2 |95.5 |95.9 |89.2 |88.0 |91.1 |70.9 |67.0 |64.0 | +| fastmtcnn |**97.7** |96.6 |96.0 |95.9 |89.6 |87.8 |89.7 |71.2 |67.8 |62.7 | +| dlib |96.5 |89.9 |94.1 |93.8 |95.6 |63.0 |75.0 |75.9 |62.6 |61.7 | +| yolov8 |**97.7** |95.8 |95.2 |95.0 |88.1 |88.7 |89.8 |68.9 |68.9 |65.3 | +| yunet |**98.3** |96.8 |96.3 |96.1 |91.7 |88.0 |90.5 |71.7 |67.6 |63.2 | +| centerface |97.4 |96.3 |95.8 |95.8 |90.2 |86.8 |89.3 |69.9 |68.4 |62.6 | +| mediapipe |96.3 |90.0 |93.1 |89.3 |91.8 |64.8 |74.6 |77.6 |64.9 |61.6 | +| ssd |**97.9** |97.0 |96.7 |96.6 |89.4 |91.5 |93.0 |69.9 |68.7 |63.8 | +| opencv |96.2 |92.9 |95.8 |93.2 |91.5 |93.3 |91.7 |71.1 |68.1 |61.1 | +| skip |91.4 |67.6 |90.6 |54.8 |69.3 |78.4 |83.4 |57.4 |62.6 |61.1 | + +# Citation + +Please cite deepface in your publications if it helps your research - see [`CITATIONS`](https://github.com/serengil/deepface/blob/master/CITATION.md) for more details. Here is its BibTex entry: + +```BibTeX +@article{serengil2024lightface, + title = {A Benchmark of Facial Recognition Pipelines and Co-Usability Performances of Modules}, + author = {Serengil, Sefik Ilkin and Ozpinar, Alper}, + journal = {Bilisim Teknolojileri Dergisi}, + volume = {17}, + number = {2}, + pages = {95-107}, + year = {2024}, + doi = {10.17671/gazibtd.1399077}, + url = {https://dergipark.org.tr/en/pub/gazibtd/issue/84331/1399077}, + publisher = {Gazi University} +} +``` diff --git a/deepface/.DS_Store b/deepface/.DS_Store new file mode 100644 index 0000000..532d227 Binary files /dev/null and b/deepface/.DS_Store differ diff --git a/deepface/DeepFace.py b/deepface/DeepFace.py new file mode 100644 index 0000000..5848d7b --- /dev/null +++ b/deepface/DeepFace.py @@ -0,0 +1,615 @@ +# common dependencies +import os +import warnings +import logging +from typing import Any, Dict, List, Union, Optional + +# 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.logger import Logger +from deepface.modules import ( + modeling, + representation, + verification, + recognition, + demography, + detection, + streaming, + preprocessing, +) +from deepface import __version__ + +logger = 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, task: str = "facial_recognition") -> Any: + """ + This function builds a pre-trained model + Args: + model_name (str): model identifier + - VGG-Face, Facenet, Facenet512, OpenFace, DeepFace, DeepID, Dlib, + ArcFace, SFace, GhostFaceNet for face recognition + - Age, Gender, Emotion, Race for facial attributes + - opencv, mtcnn, ssd, dlib, retinaface, mediapipe, yolov8, yunet, + fastmtcnn or centerface for face detectors + - Fasnet for spoofing + task (str): facial_recognition, facial_attribute, face_detector, spoofing + default is facial_recognition + Returns: + built_model + """ + return modeling.build_model(task=task, 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, + threshold: Optional[float] = None, + anti_spoofing: 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). + + 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). + + anti_spoofing (boolean): Flag to enable anti spoofing (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. + + - 'threshold' (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, + threshold=threshold, + anti_spoofing=anti_spoofing, + ) + + +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, + anti_spoofing: 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). + + anti_spoofing (boolean): Flag to enable anti spoofing (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, + anti_spoofing=anti_spoofing, + ) + + +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, + refresh_database: bool = True, + anti_spoofing: 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). + + refresh_database (boolean): Synchronizes the images representation (pkl) file with the + directory/db files, if set to false, it will ignore any file changes inside the db_path + (default is True). + + anti_spoofing (boolean): Flag to enable anti spoofing (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, + refresh_database=refresh_database, + anti_spoofing=anti_spoofing, + ) + + +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", + anti_spoofing: bool = False, + max_faces: Optional[int] = None, +) -> 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). + + anti_spoofing (boolean): Flag to enable anti spoofing (default is False). + + max_faces (int): Set a limit on the number of faces to be processed (default is None). + + 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, + anti_spoofing=anti_spoofing, + max_faces=max_faces, + ) + + +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, + anti_spoofing: bool = False, +) -> 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). + + anti_spoofing (boolean): Flag to enable anti spoofing (default is False). + 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, + anti_spoofing=anti_spoofing, + ) + + +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, + color_face: str = "rgb", + normalize_face: bool = True, + anti_spoofing: 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): (Deprecated) Flag to convert the output face image to grayscale + (default is False). + + color_face (string): Color to return face image output. Options: 'rgb', 'bgr' or 'gray' + (default is 'rgb'). + + normalize_face (boolean): Flag to enable normalization (divide by 255) of the output + face image output face image normalization (default is True). + + anti_spoofing (boolean): Flag to enable anti spoofing (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. + + - "is_real" (boolean): antispoofing analyze result. this key is just available in the + result only if anti_spoofing is set to True in input arguments. + + - "antispoof_score" (float): score of antispoofing analyze result. this key is + just available in the result only if anti_spoofing is set to True in input arguments. + """ + + return detection.extract_faces( + img_path=img_path, + detector_backend=detector_backend, + enforce_detection=enforce_detection, + align=align, + expand_percentage=expand_percentage, + grayscale=grayscale, + color_face=color_face, + normalize_face=normalize_face, + anti_spoofing=anti_spoofing, + ) + + +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, + grayscale=False, + enforce_detection=enforce_detection, + align=align, + ) + 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 diff --git a/deepface/__init__.py b/deepface/__init__.py new file mode 100644 index 0000000..cffae17 --- /dev/null +++ b/deepface/__init__.py @@ -0,0 +1 @@ +__version__ = "0.0.94" diff --git a/deepface/api/.DS_Store b/deepface/api/.DS_Store new file mode 100644 index 0000000..eaff783 Binary files /dev/null and b/deepface/api/.DS_Store differ diff --git a/deepface/api/__init__.py b/deepface/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/deepface/api/postman/deepface-api.postman_collection.json b/deepface/api/postman/deepface-api.postman_collection.json new file mode 100644 index 0000000..d36f8ca --- /dev/null +++ b/deepface/api/postman/deepface-api.postman_collection.json @@ -0,0 +1,133 @@ +{ + "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": [] + }, + { + "name": "Face extractor", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"img_path\": \"/Users/sefik/Desktop/deepface/tests/dataset/couple.jpg\",\n \n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://127.0.0.1:5005/extract_faces", + "protocol": "http", + "host": [ + "127", + "0", + "0", + "1" + ], + "port": "5005", + "path": [ + "extract_faces" + ] + } + }, + "response": [] + } + ] +} \ No newline at end of file diff --git a/deepface/api/src/__init__.py b/deepface/api/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/deepface/api/src/__pycache__/api.cpython-39.pyc b/deepface/api/src/__pycache__/api.cpython-39.pyc new file mode 100644 index 0000000..ad4e60e Binary files /dev/null and b/deepface/api/src/__pycache__/api.cpython-39.pyc differ diff --git a/deepface/api/src/__pycache__/app.cpython-39.pyc b/deepface/api/src/__pycache__/app.cpython-39.pyc new file mode 100644 index 0000000..b27876b Binary files /dev/null and b/deepface/api/src/__pycache__/app.cpython-39.pyc differ diff --git a/deepface/api/src/api.py b/deepface/api/src/api.py new file mode 100644 index 0000000..8573e32 --- /dev/null +++ b/deepface/api/src/api.py @@ -0,0 +1,10 @@ +import argparse +import app + +if __name__ == "__main__": + deepface_app = app.create_app() + parser = argparse.ArgumentParser() + parser.add_argument("-p", "--port", type=int, + default=5000, help="Port of serving api") + args = parser.parse_args() + deepface_app.run(host="0.0.0.0", port=args.port) diff --git a/deepface/api/src/app.py b/deepface/api/src/app.py new file mode 100644 index 0000000..d11f576 --- /dev/null +++ b/deepface/api/src/app.py @@ -0,0 +1,18 @@ +# 3rd parth dependencies +from flask import Flask +from flask_cors import CORS + +# project dependencies +from deepface import DeepFace +from deepface.commons.logger import Logger +from deepface.api.src.modules.core.routes import blueprint + +logger = Logger() + + +def create_app(): + app = Flask(__name__) + CORS(app) + app.register_blueprint(blueprint) + logger.info(f"Welcome to DeepFace API v{DeepFace.__version__}!") + return app diff --git a/deepface/api/src/modules/__init__.py b/deepface/api/src/modules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/deepface/api/src/modules/core/__init__.py b/deepface/api/src/modules/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/deepface/api/src/modules/core/routes.py b/deepface/api/src/modules/core/routes.py new file mode 100644 index 0000000..5f2f5ff --- /dev/null +++ b/deepface/api/src/modules/core/routes.py @@ -0,0 +1,122 @@ +from flask import Blueprint, request +from deepface import DeepFace +from deepface.api.src.modules.core import service +from deepface.commons.logger import Logger + +logger = Logger() + +blueprint = Blueprint("routes", __name__) + + +@blueprint.route("/") +def home(): + return f"

Welcome to DeepFace API v{DeepFace.__version__}!

" + + +@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,hhhhh"} + + obj = service.represent( + img_path=img_path, + 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), + anti_spoofing=input_args.get("anti_spoofing", False), + max_faces=input_args.get("max_faces"), + ) + + 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"} + + verification = service.verify( + img1_path=img1_path, + img2_path=img2_path, + model_name=input_args.get("model_name", "VGG-Face"), + detector_backend=input_args.get("detector_backend", "opencv"), + distance_metric=input_args.get("distance_metric", "cosine"), + align=input_args.get("align", True), + enforce_detection=input_args.get("enforce_detection", True), + anti_spoofing=input_args.get("anti_spoofing", False), + ) + + 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"} + + demographies = service.analyze( + img_path=img_path, + actions=input_args.get( + "actions", ["age", "gender", "emotion", "race"]), + detector_backend=input_args.get("detector_backend", "opencv"), + enforce_detection=input_args.get("enforce_detection", True), + align=input_args.get("align", True), + anti_spoofing=input_args.get("anti_spoofing", False), + ) + + logger.debug(demographies) + + return demographies + + +@blueprint.route("/extract", methods=["POST"]) +def extract(): + 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"} + print('represent:', img_path) + + demographies = service.extract( + img_path=img_path, + detector_backend=input_args.get("detector_backend", "yolov8"), + enforce_detection=input_args.get("enforce_detection", False), + align=input_args.get("align", True), + anti_spoofing=input_args.get("anti_spoofing", False), + ) + + logger.debug(demographies) + + return demographies diff --git a/deepface/api/src/modules/core/service.py b/deepface/api/src/modules/core/service.py new file mode 100644 index 0000000..9e087e9 --- /dev/null +++ b/deepface/api/src/modules/core/service.py @@ -0,0 +1,114 @@ +# built-in dependencies +import traceback +from typing import Optional + +# project dependencies +from deepface import DeepFace + +# pylint: disable=broad-except + + +def represent( + img_path: str, + model_name: str, + detector_backend: str, + enforce_detection: bool, + align: bool, + anti_spoofing: bool, + max_faces: Optional[int] = None, +): + try: + result = {} + embedding_objs = DeepFace.represent( + img_path=img_path, + model_name=model_name, + detector_backend=detector_backend, + enforce_detection=enforce_detection, + align=align, + anti_spoofing=anti_spoofing, + max_faces=max_faces, + ) + result["results"] = embedding_objs + return result + except Exception as err: + tb_str = traceback.format_exc() + return {"error": f"Exception while representing: {str(err)} - {tb_str}"}, 400 + + +def verify( + img1_path: str, + img2_path: str, + model_name: str, + detector_backend: str, + distance_metric: str, + enforce_detection: bool, + align: bool, + anti_spoofing: bool, +): + 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, + anti_spoofing=anti_spoofing, + ) + return obj + except Exception as err: + tb_str = traceback.format_exc() + return {"error": f"Exception while verifying: {str(err)} - {tb_str}"}, 400 + + +def analyze( + img_path: str, + actions: list, + detector_backend: str, + enforce_detection: bool, + align: bool, + anti_spoofing: bool, +): + try: + result = {} + demographies = DeepFace.analyze( + img_path=img_path, + actions=actions, + detector_backend=detector_backend, + enforce_detection=enforce_detection, + align=align, + silent=True, + anti_spoofing=anti_spoofing, + ) + result["results"] = demographies + return result + except Exception as err: + tb_str = traceback.format_exc() + return {"error": f"Exception while analyzing: {str(err)} - {tb_str}"}, 400 + + +def extract( + img_path: str, + detector_backend: str, + enforce_detection: bool, + align: bool, + anti_spoofing: bool, +): + try: + result = {} + demographies = DeepFace.extract_faces( + img_path=img_path, + detector_backend=detector_backend, + enforce_detection=enforce_detection, + align=align, + expand_percentage=0, + grayscale=False, + normalize_face=True, + anti_spoofing=anti_spoofing + ) + result["results"] = demographies + return result + except Exception as err: + tb_str = traceback.format_exc() + return {"error": f"Exception while detecting: {str(err)} - {tb_str}"}, 400 diff --git a/deepface/commons/__init__.py b/deepface/commons/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/deepface/commons/constant.py b/deepface/commons/constant.py new file mode 100644 index 0000000..22f6349 --- /dev/null +++ b/deepface/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/deepface/commons/folder_utils.py b/deepface/commons/folder_utils.py new file mode 100644 index 0000000..416eaba --- /dev/null +++ b/deepface/commons/folder_utils.py @@ -0,0 +1,34 @@ +import os +from deepface.commons.logger import Logger + +logger = 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 = os.path.join(home, ".deepface") + weights_path = os.path.join(deepface_home_path, "weights") + + if not os.path.exists(deepface_home_path): + os.makedirs(deepface_home_path, exist_ok=True) + logger.info(f"Directory {deepface_home_path} has been created") + + if not os.path.exists(weights_path): + os.makedirs(weights_path, exist_ok=True) + logger.info(f"Directory {weights_path} has been 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=os.path.expanduser("~"))) diff --git a/deepface/commons/image_utils.py b/deepface/commons/image_utils.py new file mode 100644 index 0000000..8d9e058 --- /dev/null +++ b/deepface/commons/image_utils.py @@ -0,0 +1,148 @@ +# 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_lower = os.path.splitext(exact_path)[-1].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://", "https://")): + return load_image_from_web(url=img), img + + # The image is a path + if not os.path.isfile(img): + raise ValueError(f"Confirm that {img} exists") + + # image must be a file on the system then + + # image name must have english characters + if not img.isascii(): + 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/deepface/commons/logger.py b/deepface/commons/logger.py new file mode 100644 index 0000000..f494eb8 --- /dev/null +++ b/deepface/commons/logger.py @@ -0,0 +1,57 @@ +import os +import logging +from datetime import datetime + +# pylint: disable=broad-except +class Logger: + """ + A Logger class for logging messages with a specific log level. + + The class follows the singleton design pattern, ensuring that only one + instance of the Logger is created. The parameters of the first instance + are preserved across all instances. + """ + + __instance = None + + def __new__(cls): + if cls.__instance is None: + cls.__instance = super(Logger, cls).__new__(cls) + return cls.__instance + + def __init__(self): + if not hasattr(self, "_singleton_initialized"): + self._singleton_initialized = True # to prevent multiple initializations + 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}") diff --git a/deepface/commons/package_utils.py b/deepface/commons/package_utils.py new file mode 100644 index 0000000..8513f67 --- /dev/null +++ b/deepface/commons/package_utils.py @@ -0,0 +1,65 @@ +# built-in dependencies +import hashlib + +# 3rd party dependencies +import tensorflow as tf + +# package dependencies +from deepface.commons.logger import Logger + +logger = 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 + + +def find_file_hash(file_path: str, hash_algorithm: str = "sha256") -> str: + """ + Find the hash of a given file with its content + Args: + file_path (str): exact path of a given file + hash_algorithm (str): hash algorithm + Returns: + hash (str) + """ + hash_func = hashlib.new(hash_algorithm) + with open(file_path, "rb") as f: + while chunk := f.read(8192): + hash_func.update(chunk) + return hash_func.hexdigest() diff --git a/deepface/commons/weight_utils.py b/deepface/commons/weight_utils.py new file mode 100644 index 0000000..f1ccf2c --- /dev/null +++ b/deepface/commons/weight_utils.py @@ -0,0 +1,97 @@ +# built-in dependencies +import os +from typing import Optional +import zipfile +import bz2 + +# 3rd party dependencies +import gdown + +# project dependencies +from deepface.commons import folder_utils, package_utils +from deepface.commons.logger import Logger + +tf_version = package_utils.get_tf_major_version() +if tf_version == 1: + from keras.models import Sequential +else: + from tensorflow.keras.models import Sequential + +logger = Logger() + +ALLOWED_COMPRESS_TYPES = ["zip", "bz2"] + + +def download_weights_if_necessary( + file_name: str, source_url: str, compress_type: Optional[str] = None +) -> str: + """ + Download the weights of a pre-trained model from external source if not downloaded yet. + Args: + file_name (str): target file name with extension + source_url (url): source url to be downloaded + compress_type (optional str): compress type e.g. zip or bz2 + Returns + target_file (str): exact path for the target file + """ + home = folder_utils.get_deepface_home() + + target_file = os.path.join(home, ".deepface/weights", file_name) + + if os.path.isfile(target_file): + logger.debug(f"{file_name} is already available at {target_file}") + return target_file + + if compress_type is not None and compress_type not in ALLOWED_COMPRESS_TYPES: + raise ValueError(f"unimplemented compress type - {compress_type}") + + try: + logger.info(f"🔗 {file_name} will be downloaded from {source_url} to {target_file}...") + + if compress_type is None: + gdown.download(source_url, target_file, quiet=False) + elif compress_type is not None and compress_type in ALLOWED_COMPRESS_TYPES: + gdown.download(source_url, f"{target_file}.{compress_type}", quiet=False) + + except Exception as err: + raise ValueError( + f"⛓️‍💥 An exception occurred while downloading {file_name} from {source_url}. " + f"Consider downloading it manually to {target_file}." + ) from err + + # uncompress downloaded file + if compress_type == "zip": + with zipfile.ZipFile(f"{target_file}.zip", "r") as zip_ref: + zip_ref.extractall(os.path.join(home, ".deepface/weights")) + logger.info(f"{target_file}.zip unzipped") + elif compress_type == "bz2": + bz2file = bz2.BZ2File(f"{target_file}.bz2") + data = bz2file.read() + with open(target_file, "wb") as f: + f.write(data) + logger.info(f"{target_file}.bz2 unzipped") + + return target_file + + +def load_model_weights(model: Sequential, weight_file: str) -> Sequential: + """ + Load pre-trained weights for a given model + Args: + model (keras.models.Sequential): pre-built model + weight_file (str): exact path of pre-trained weights + Returns: + model (keras.models.Sequential): pre-built model with + updated weights + """ + try: + model.load_weights(weight_file) + except Exception as err: + raise ValueError( + f"An exception occurred while loading the pre-trained weights from {weight_file}." + "This might have happened due to an interruption during the download." + "You may want to delete it and allow DeepFace to download it again during the next run." + "If the issue persists, consider downloading the file directly from the source " + "and copying it to the target folder." + ) from err + return model diff --git a/deepface/models/Demography.py b/deepface/models/Demography.py new file mode 100644 index 0000000..ad93920 --- /dev/null +++ b/deepface/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/deepface/models/Detector.py b/deepface/models/Detector.py new file mode 100644 index 0000000..be1130f --- /dev/null +++ b/deepface/models/Detector.py @@ -0,0 +1,69 @@ +from typing import List, Tuple, Optional +from abc import ABC, abstractmethod +from dataclasses import dataclass +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 + + +@dataclass +class FacialAreaRegion: + """ + 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. + """ + 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 + + +@dataclass +class DetectedFace: + """ + 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 + """ + img: np.ndarray + facial_area: FacialAreaRegion + confidence: float diff --git a/deepface/models/FacialRecognition.py b/deepface/models/FacialRecognition.py new file mode 100644 index 0000000..a6ee7b5 --- /dev/null +++ b/deepface/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/deepface/models/__init__.py b/deepface/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/deepface/models/demography/Age.py b/deepface/models/demography/Age.py new file mode 100644 index 0000000..29efdf5 --- /dev/null +++ b/deepface/models/demography/Age.py @@ -0,0 +1,89 @@ +# 3rd party dependencies +import numpy as np + +# project dependencies +from deepface.models.facial_recognition import VGGFace +from deepface.commons import package_utils, weight_utils +from deepface.models.Demography import Demography +from deepface.commons.logger import Logger + +logger = 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: + # model.predict causes memory issue when it is called in a for loop + # age_predictions = self.model.predict(img, verbose=0)[0, :] + age_predictions = self.model(img, training=False).numpy()[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 + weight_file = weight_utils.download_weights_if_necessary( + file_name="age_model_weights.h5", source_url=url + ) + + age_model = weight_utils.load_model_weights( + model=age_model, weight_file=weight_file + ) + + return age_model + +def find_apparent_age(age_predictions: np.ndarray) -> np.float64: + """ + Find apparent age prediction from a given probas of ages + Args: + age_predictions (?) + Returns: + apparent_age (float) + """ + output_indexes = np.arange(0, 101) + apparent_age = np.sum(age_predictions * output_indexes) + return apparent_age diff --git a/deepface/models/demography/Emotion.py b/deepface/models/demography/Emotion.py new file mode 100644 index 0000000..3d1d88f --- /dev/null +++ b/deepface/models/demography/Emotion.py @@ -0,0 +1,103 @@ +# 3rd party dependencies +import numpy as np +import cv2 + +# project dependencies +from deepface.commons import package_utils, weight_utils +from deepface.models.Demography import Demography +from deepface.commons.logger import Logger + +logger = Logger() + +# ------------------------------------------- +# pylint: disable=line-too-long +# ------------------------------------------- +# dependency configuration +tf_version = package_utils.get_tf_major_version() + +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) + + # model.predict causes memory issue when it is called in a for loop + # emotion_predictions = self.model.predict(img_gray, verbose=0)[0, :] + emotion_predictions = self.model(img_gray, training=False).numpy()[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")) + + # ---------------------------- + + weight_file = weight_utils.download_weights_if_necessary( + file_name="facial_expression_model_weights.h5", source_url=url + ) + + model = weight_utils.load_model_weights( + model=model, weight_file=weight_file + ) + + return model diff --git a/deepface/models/demography/Gender.py b/deepface/models/demography/Gender.py new file mode 100644 index 0000000..2f3a142 --- /dev/null +++ b/deepface/models/demography/Gender.py @@ -0,0 +1,79 @@ +# 3rd party dependencies +import numpy as np + +# project dependencies +from deepface.models.facial_recognition import VGGFace +from deepface.commons import package_utils, weight_utils +from deepface.models.Demography import Demography +from deepface.commons.logger import Logger + +logger = Logger() + +# ------------------------------------- +# pylint: disable=line-too-long +# ------------------------------------- +# dependency configurations + +tf_version = package_utils.get_tf_major_version() +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: + # model.predict causes memory issue when it is called in a for loop + # return self.model.predict(img, verbose=0)[0, :] + return self.model(img, training=False).numpy()[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 + weight_file = weight_utils.download_weights_if_necessary( + file_name="gender_model_weights.h5", source_url=url + ) + + gender_model = weight_utils.load_model_weights( + model=gender_model, weight_file=weight_file + ) + + return gender_model diff --git a/deepface/models/demography/Race.py b/deepface/models/demography/Race.py new file mode 100644 index 0000000..a393667 --- /dev/null +++ b/deepface/models/demography/Race.py @@ -0,0 +1,76 @@ +# 3rd party dependencies +import numpy as np + +# project dependencies +from deepface.models.facial_recognition import VGGFace +from deepface.commons import package_utils, weight_utils +from deepface.models.Demography import Demography +from deepface.commons.logger import Logger + +logger = Logger() + +# -------------------------- +# pylint: disable=line-too-long +# -------------------------- +# dependency configurations +tf_version = package_utils.get_tf_major_version() + +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: + # model.predict causes memory issue when it is called in a for loop + # return self.model.predict(img, verbose=0)[0, :] + return self.model(img, training=False).numpy()[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 + weight_file = weight_utils.download_weights_if_necessary( + file_name="race_model_single_batch.h5", source_url=url + ) + + race_model = weight_utils.load_model_weights( + model=race_model, weight_file=weight_file + ) + + return race_model diff --git a/deepface/models/demography/__init__.py b/deepface/models/demography/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/deepface/models/face_detection/CenterFace.py b/deepface/models/face_detection/CenterFace.py new file mode 100644 index 0000000..d8e08bd --- /dev/null +++ b/deepface/models/face_detection/CenterFace.py @@ -0,0 +1,208 @@ +# built-in dependencies +import os +from typing import List + +# 3rd party dependencies +import numpy as np +import cv2 + +# project dependencies +from deepface.commons import weight_utils +from deepface.models.Detector import Detector, FacialAreaRegion +from deepface.commons.logger import Logger + +logger = 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 = weight_utils.download_weights_if_necessary( + file_name="centerface.onnx", source_url=WEIGHTS_URL + ) + + 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/deepface/models/face_detection/Dlib.py b/deepface/models/face_detection/Dlib.py new file mode 100644 index 0000000..c96f1a3 --- /dev/null +++ b/deepface/models/face_detection/Dlib.py @@ -0,0 +1,104 @@ +# built-in dependencies +from typing import List + +# 3rd party dependencies +import numpy as np + +# project dependencies +from deepface.commons import weight_utils +from deepface.models.Detector import Detector, FacialAreaRegion +from deepface.commons.logger import Logger + +logger = 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) + """ + # 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 + weight_file = weight_utils.download_weights_if_necessary( + file_name="shape_predictor_5_face_landmarks.dat", + source_url="http://dlib.net/files/shape_predictor_5_face_landmarks.dat.bz2", + compress_type="bz2", + ) + + face_detector = dlib.get_frontal_face_detector() + sp = dlib.shape_predictor(weight_file) + + 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/deepface/models/face_detection/FastMtCnn.py b/deepface/models/face_detection/FastMtCnn.py new file mode 100644 index 0000000..bc792a4 --- /dev/null +++ b/deepface/models/face_detection/FastMtCnn.py @@ -0,0 +1,95 @@ +# built-in dependencies +from typing import Any, Union, List + +# 3rd party dependencies +import cv2 +import numpy as np + +# project dependencies +from deepface.models.Detector import Detector, FacialAreaRegion + + +class FastMtCnnClient(Detector): + """ + Fast MtCnn Detector from github.com/timesler/facenet-pytorch + """ + + 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/deepface/models/face_detection/MediaPipe.py b/deepface/models/face_detection/MediaPipe.py new file mode 100644 index 0000000..61a84fd --- /dev/null +++ b/deepface/models/face_detection/MediaPipe.py @@ -0,0 +1,89 @@ +# built-in dependencies +from typing import Any, List + +# 3rd party dependencies +import numpy as np + +# project dependencies +from deepface.models.Detector import Detector, FacialAreaRegion + + +class MediaPipeClient(Detector): + """ + MediaPipe from google.github.io/mediapipe/solutions/face_detection + """ + + 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=float(confidence), + ) + resp.append(facial_area) + + return resp diff --git a/deepface/models/face_detection/MtCnn.py b/deepface/models/face_detection/MtCnn.py new file mode 100644 index 0000000..014e4a5 --- /dev/null +++ b/deepface/models/face_detection/MtCnn.py @@ -0,0 +1,60 @@ +# built-in dependencies +from typing import List + +# 3rd party dependencies +import numpy as np +from mtcnn import MTCNN + +# project dependencies +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/deepface/models/face_detection/OpenCv.py b/deepface/models/face_detection/OpenCv.py new file mode 100644 index 0000000..4abb6da --- /dev/null +++ b/deepface/models/face_detection/OpenCv.py @@ -0,0 +1,176 @@ +# built-in dependencies +import os +from typing import Any, List + +# 3rd party dependencies +import cv2 +import numpy as np + +#project dependencies +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 = os.path.join(opencv_path, "haarcascade_frontalface_default.xml") + if not os.path.isfile(face_detector_path): + 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 = os.path.join(opencv_path, "haarcascade_eye.xml") + if not os.path.isfile(eye_detector_path): + 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) + """ + return os.path.join(os.path.dirname(cv2.__file__), "data") diff --git a/deepface/models/face_detection/RetinaFace.py b/deepface/models/face_detection/RetinaFace.py new file mode 100644 index 0000000..a3b1468 --- /dev/null +++ b/deepface/models/face_detection/RetinaFace.py @@ -0,0 +1,64 @@ +# built-in dependencies +from typing import List + +# 3rd party dependencies +import numpy as np +from retinaface import RetinaFace as rf + +# project dependencies +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/deepface/models/face_detection/Ssd.py b/deepface/models/face_detection/Ssd.py new file mode 100644 index 0000000..4250888 --- /dev/null +++ b/deepface/models/face_detection/Ssd.py @@ -0,0 +1,133 @@ +# built-in dependencies +from typing import List +from enum import IntEnum + +# 3rd party dependencies +import cv2 +import numpy as np + +# project dependencies +from deepface.models.face_detection import OpenCv +from deepface.commons import weight_utils +from deepface.models.Detector import Detector, FacialAreaRegion +from deepface.commons.logger import Logger + +logger = 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) + """ + + # model structure + output_model = weight_utils.download_weights_if_necessary( + file_name="deploy.prototxt", + source_url="https://github.com/opencv/opencv/raw/3.4.0/samples/dnn/face_detector/deploy.prototxt", + ) + + # pre-trained weights + output_weights = weight_utils.download_weights_if_necessary( + file_name="res10_300x300_ssd_iter_140000.caffemodel", + source_url="https://github.com/opencv/opencv_3rdparty/raw/dnn_samples_face_detector_20170830/res10_300x300_ssd_iter_140000.caffemodel", + ) + + try: + face_detector = cv2.dnn.readNetFromCaffe(output_model, output_weights) + 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 + + return {"face_detector": face_detector, "opencv_module": OpenCv.OpenCvClient()} + + 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 + """ + + # Because cv2.dnn.blobFromImage expects CV_8U (8-bit unsigned integer) values + if img.dtype != np.uint8: + img = img.astype(np.uint8) + + opencv_module: OpenCv.OpenCvClient = self.model["opencv_module"] + + 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() + + class ssd_labels(IntEnum): + img_id = 0 + is_face = 1 + confidence = 2 + left = 3 + top = 4 + right = 5 + bottom = 6 + + faces = detections[0][0] + faces = faces[ + (faces[:, ssd_labels.is_face] == 1) & (faces[:, ssd_labels.confidence] >= 0.90) + ] + margins = [ssd_labels.left, ssd_labels.top, ssd_labels.right, ssd_labels.bottom] + faces[:, margins] = np.int32(faces[:, margins] * 300) + faces[:, margins] = np.int32( + faces[:, margins] * [aspect_ratio_x, aspect_ratio_y, aspect_ratio_x, aspect_ratio_y] + ) + faces[:, [ssd_labels.right, ssd_labels.bottom]] -= faces[ + :, [ssd_labels.left, ssd_labels.top] + ] + + resp = [] + for face in faces: + confidence = float(face[ssd_labels.confidence]) + x, y, w, h = map(int, face[margins]) + detected_face = img[y : y + h, x : 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 = x + int(left_eye[0]), y + int(left_eye[1]) + if right_eye is not None: + right_eye = x + int(right_eye[0]), y + int(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/deepface/models/face_detection/Yolo.py b/deepface/models/face_detection/Yolo.py new file mode 100644 index 0000000..a4f5a46 --- /dev/null +++ b/deepface/models/face_detection/Yolo.py @@ -0,0 +1,94 @@ +# built-in dependencies +from typing import Any, List + +# 3rd party dependencies +import numpy as np + +# project dependencies +from deepface.models.Detector import Detector, FacialAreaRegion +from deepface.commons import weight_utils +from deepface.commons.logger import Logger + +logger = 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 optional 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_file = weight_utils.download_weights_if_necessary( + file_name="yolov8n-face.pt", source_url=WEIGHT_URL + ) + + # Return face_detector + return YOLO(weight_file) + + 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/deepface/models/face_detection/YuNet.py b/deepface/models/face_detection/YuNet.py new file mode 100644 index 0000000..398aed2 --- /dev/null +++ b/deepface/models/face_detection/YuNet.py @@ -0,0 +1,127 @@ +# built-in dependencies +import os +from typing import Any, List + +# 3rd party dependencies +import cv2 +import numpy as np + +# project dependencies +from deepface.commons import weight_utils +from deepface.models.Detector import Detector, FacialAreaRegion +from deepface.commons.logger import Logger + +logger = 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 + weight_file = weight_utils.download_weights_if_necessary( + file_name="face_detection_yunet_2023mar.onnx", + source_url="https://github.com/opencv/opencv_zoo/raw/main/models/face_detection_yunet/face_detection_yunet_2023mar.onnx", + ) + + try: + face_detector = cv2.FaceDetectorYN_create(weight_file, "", (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/deepface/models/face_detection/__init__.py b/deepface/models/face_detection/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/deepface/models/facial_recognition/ArcFace.py b/deepface/models/facial_recognition/ArcFace.py new file mode 100644 index 0000000..596192f --- /dev/null +++ b/deepface/models/facial_recognition/ArcFace.py @@ -0,0 +1,169 @@ +# project dependencies +from deepface.commons import package_utils, weight_utils +from deepface.models.FacialRecognition import FacialRecognition + +from deepface.commons.logger import Logger + +logger = 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) + + # --------------------------------------- + weight_file = weight_utils.download_weights_if_necessary( + file_name="arcface_weights.h5", source_url=url + ) + + model = weight_utils.load_model_weights(model=model, weight_file=weight_file) + # --------------------------------------- + + 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/deepface/models/facial_recognition/DeepID.py b/deepface/models/facial_recognition/DeepID.py new file mode 100644 index 0000000..ea03b4e --- /dev/null +++ b/deepface/models/facial_recognition/DeepID.py @@ -0,0 +1,96 @@ +# project dependencies +from deepface.commons import package_utils, weight_utils +from deepface.models.FacialRecognition import FacialRecognition +from deepface.commons.logger import Logger + +logger = 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) + + # --------------------------------- + + weight_file = weight_utils.download_weights_if_necessary( + file_name="deepid_keras_weights.h5", source_url=url + ) + + model = weight_utils.load_model_weights( + model=model, weight_file=weight_file + ) + + return model diff --git a/deepface/models/facial_recognition/Dlib.py b/deepface/models/facial_recognition/Dlib.py new file mode 100644 index 0000000..3d50521 --- /dev/null +++ b/deepface/models/facial_recognition/Dlib.py @@ -0,0 +1,79 @@ +# built-in dependencies +from typing import List + +# 3rd party dependencies +import numpy as np + +# project dependencies +from deepface.commons import weight_utils +from deepface.models.FacialRecognition import FacialRecognition +from deepface.commons.logger import Logger + +logger = 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. Don't 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 + + weight_file = weight_utils.download_weights_if_necessary( + file_name="dlib_face_recognition_resnet_model_v1.dat", + source_url="http://dlib.net/files/dlib_face_recognition_resnet_model_v1.dat.bz2", + compress_type="bz2", + ) + + self.model = dlib.face_recognition_model_v1(weight_file) + + # return None # classes must return None diff --git a/deepface/models/facial_recognition/Facenet.py b/deepface/models/facial_recognition/Facenet.py new file mode 100644 index 0000000..b1ad37c --- /dev/null +++ b/deepface/models/facial_recognition/Facenet.py @@ -0,0 +1,1696 @@ +# project dependencies +from deepface.commons import package_utils, weight_utils +from deepface.models.FacialRecognition import FacialRecognition +from deepface.commons.logger import Logger + +logger = 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() + + weight_file = weight_utils.download_weights_if_necessary( + file_name="facenet_weights.h5", source_url=url + ) + model = weight_utils.load_model_weights( + model=model, weight_file=weight_file + ) + + 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) + + weight_file = weight_utils.download_weights_if_necessary( + file_name="facenet512_weights.h5", source_url=url + ) + model = weight_utils.load_model_weights( + model=model, weight_file=weight_file + ) + + return model diff --git a/deepface/models/facial_recognition/FbDeepFace.py b/deepface/models/facial_recognition/FbDeepFace.py new file mode 100644 index 0000000..fb41d62 --- /dev/null +++ b/deepface/models/facial_recognition/FbDeepFace.py @@ -0,0 +1,94 @@ +# project dependencies +from deepface.commons import package_utils, weight_utils +from deepface.models.FacialRecognition import FacialRecognition +from deepface.commons.logger import Logger + +logger = 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")) + + # --------------------------------- + + weight_file = weight_utils.download_weights_if_necessary( + file_name="VGGFace2_DeepFace_weights_val-0.9034.h5", source_url=url, compress_type="zip" + ) + + base_model = weight_utils.load_model_weights(model=base_model, weight_file=weight_file) + + # 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/deepface/models/facial_recognition/GhostFaceNet.py b/deepface/models/facial_recognition/GhostFaceNet.py new file mode 100644 index 0000000..37bd728 --- /dev/null +++ b/deepface/models/facial_recognition/GhostFaceNet.py @@ -0,0 +1,306 @@ +# 3rd party dependencies +import tensorflow as tf + +# project dependencies +from deepface.commons import package_utils, weight_utils +from deepface.models.FacialRecognition import FacialRecognition +from deepface.commons.logger import Logger + +logger = 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() + + weight_file = weight_utils.download_weights_if_necessary( + file_name="ghostfacenet_v1.h5", source_url=PRETRAINED_WEIGHTS + ) + + model = weight_utils.load_model_weights( + model=model, weight_file=weight_file + ) + + 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/deepface/models/facial_recognition/OpenFace.py b/deepface/models/facial_recognition/OpenFace.py new file mode 100644 index 0000000..c9c1b7a --- /dev/null +++ b/deepface/models/facial_recognition/OpenFace.py @@ -0,0 +1,394 @@ +# 3rd party dependencies +import tensorflow as tf + +# project dependencies +from deepface.commons import package_utils, weight_utils +from deepface.models.FacialRecognition import FacialRecognition +from deepface.commons.logger import Logger + +logger = 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) + + # ----------------------------------- + + weight_file = weight_utils.download_weights_if_necessary( + file_name="openface_weights.h5", source_url=url + ) + + model = weight_utils.load_model_weights( + model=model, weight_file=weight_file + ) + + # ----------------------------------- + + return model diff --git a/deepface/models/facial_recognition/SFace.py b/deepface/models/facial_recognition/SFace.py new file mode 100644 index 0000000..0f1d421 --- /dev/null +++ b/deepface/models/facial_recognition/SFace.py @@ -0,0 +1,79 @@ +# built-in dependencies +from typing import Any, List + +# 3rd party dependencies +import numpy as np +import cv2 as cv + +# project dependencies +from deepface.commons import weight_utils +from deepface.models.FacialRecognition import FacialRecognition +from deepface.commons.logger import Logger + +logger = 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 + """ + + weight_file = weight_utils.download_weights_if_necessary( + file_name="face_recognition_sface_2021dec.onnx", source_url=url + ) + + model = SFaceWrapper(model_path=weight_file) + + 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/deepface/models/facial_recognition/VGGFace.py b/deepface/models/facial_recognition/VGGFace.py new file mode 100644 index 0000000..56c8a54 --- /dev/null +++ b/deepface/models/facial_recognition/VGGFace.py @@ -0,0 +1,162 @@ +# built-in dependencies +from typing import List + +# 3rd party dependencies +import numpy as np + +# project dependencies +from deepface.commons import package_utils, weight_utils +from deepface.modules import verification +from deepface.models.FacialRecognition import FacialRecognition +from deepface.commons.logger import Logger + +logger = 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() + + weight_file = weight_utils.download_weights_if_necessary( + file_name="vgg_face_weights.h5", source_url=url + ) + + model = weight_utils.load_model_weights( + model=model, weight_file=weight_file + ) + + # 2622d dimensional model + # vgg_face_descriptor = Model(inputs=model.layers[0].input, outputs=model.layers[-2].output) + + # 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/deepface/models/facial_recognition/__init__.py b/deepface/models/facial_recognition/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/deepface/models/spoofing/FasNet.py b/deepface/models/spoofing/FasNet.py new file mode 100644 index 0000000..5eb6f92 --- /dev/null +++ b/deepface/models/spoofing/FasNet.py @@ -0,0 +1,215 @@ +# built-in dependencies +from typing import Union + +# 3rd party dependencies +import cv2 +import numpy as np + +# project dependencies +from deepface.commons import weight_utils +from deepface.commons.logger import Logger + +logger = Logger() + +# pylint: disable=line-too-long, too-few-public-methods, nested-min-max +class Fasnet: + """ + Mini Face Anti Spoofing Net Library from repo: github.com/minivision-ai/Silent-Face-Anti-Spoofing + + Minivision's Silent-Face-Anti-Spoofing Repo licensed under Apache License 2.0 + Ref: github.com/minivision-ai/Silent-Face-Anti-Spoofing/blob/master/src/model_lib/MiniFASNet.py + """ + + def __init__(self): + # pytorch is an opitonal dependency, enforce it to be installed if class imported + try: + import torch + except Exception as err: + raise ValueError( + "You must install torch with `pip install pytorch` command to use face anti spoofing module" + ) from err + + device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") + self.device = device + + # download pre-trained models if not installed yet + first_model_weight_file = weight_utils.download_weights_if_necessary( + file_name="2.7_80x80_MiniFASNetV2.pth", + source_url="https://github.com/minivision-ai/Silent-Face-Anti-Spoofing/raw/master/resources/anti_spoof_models/2.7_80x80_MiniFASNetV2.pth", + ) + + second_model_weight_file = weight_utils.download_weights_if_necessary( + file_name="4_0_0_80x80_MiniFASNetV1SE.pth", + source_url="https://github.com/minivision-ai/Silent-Face-Anti-Spoofing/raw/master/resources/anti_spoof_models/4_0_0_80x80_MiniFASNetV1SE.pth", + ) + + # guarantees Fasnet imported and torch installed + from deepface.models.spoofing import FasNetBackbone + + # Fasnet will use 2 distinct models to predict, then it will find the sum of predictions + # to make a final prediction + + first_model = FasNetBackbone.MiniFASNetV2(conv6_kernel=(5, 5)).to(device) + second_model = FasNetBackbone.MiniFASNetV1SE(conv6_kernel=(5, 5)).to(device) + + # load model weight for first model + state_dict = torch.load(first_model_weight_file, map_location=device) + keys = iter(state_dict) + first_layer_name = keys.__next__() + + if first_layer_name.find("module.") >= 0: + from collections import OrderedDict + + new_state_dict = OrderedDict() + for key, value in state_dict.items(): + name_key = key[7:] + new_state_dict[name_key] = value + first_model.load_state_dict(new_state_dict) + else: + first_model.load_state_dict(state_dict) + + # load model weight for second model + state_dict = torch.load(second_model_weight_file, map_location=device) + keys = iter(state_dict) + first_layer_name = keys.__next__() + + if first_layer_name.find("module.") >= 0: + from collections import OrderedDict + + new_state_dict = OrderedDict() + for key, value in state_dict.items(): + name_key = key[7:] + new_state_dict[name_key] = value + second_model.load_state_dict(new_state_dict) + else: + second_model.load_state_dict(state_dict) + + # evaluate models + _ = first_model.eval() + _ = second_model.eval() + + self.first_model = first_model + self.second_model = second_model + + def analyze(self, img: np.ndarray, facial_area: Union[list, tuple]): + """ + Analyze a given image spoofed or not + Args: + img (np.ndarray): pre loaded image + facial_area (list or tuple): facial rectangle area coordinates with x, y, w, h respectively + Returns: + result (tuple): a result tuple consisting of is_real and score + """ + import torch + import torch.nn.functional as F + + x, y, w, h = facial_area + first_img = crop(img, (x, y, w, h), 2.7, 80, 80) + second_img = crop(img, (x, y, w, h), 4, 80, 80) + + test_transform = Compose( + [ + ToTensor(), + ] + ) + + first_img = test_transform(first_img) + first_img = first_img.unsqueeze(0).to(self.device) + + second_img = test_transform(second_img) + second_img = second_img.unsqueeze(0).to(self.device) + + with torch.no_grad(): + first_result = self.first_model.forward(first_img) + first_result = F.softmax(first_result).cpu().numpy() + + second_result = self.second_model.forward(second_img) + second_result = F.softmax(second_result).cpu().numpy() + + prediction = np.zeros((1, 3)) + prediction += first_result + prediction += second_result + + label = np.argmax(prediction) + is_real = True if label == 1 else False # pylint: disable=simplifiable-if-expression + score = prediction[0][label] / 2 + + return is_real, score + + +# subsdiary classes and functions + + +def to_tensor(pic): + """Convert a ``numpy.ndarray`` to tensor. + + See ``ToTensor`` for more details. + + Args: + pic (PIL Image or numpy.ndarray): Image to be converted to tensor. + + Returns: + Tensor: Converted image. + """ + import torch + + # handle numpy array + # IR image channel=1: modify by lzc --> 20190730 + if pic.ndim == 2: + pic = pic.reshape((pic.shape[0], pic.shape[1], 1)) + + img = torch.from_numpy(pic.transpose((2, 0, 1))) + # backward compatibility + # return img.float().div(255) modify by zkx + return img.float() + + +class Compose: + def __init__(self, transforms): + self.transforms = transforms + + def __call__(self, img): + for t in self.transforms: + img = t(img) + return img + + +class ToTensor: + def __call__(self, pic): + return to_tensor(pic) + + +def _get_new_box(src_w, src_h, bbox, scale): + x = bbox[0] + y = bbox[1] + box_w = bbox[2] + box_h = bbox[3] + scale = min((src_h - 1) / box_h, min((src_w - 1) / box_w, scale)) + new_width = box_w * scale + new_height = box_h * scale + center_x, center_y = box_w / 2 + x, box_h / 2 + y + left_top_x = center_x - new_width / 2 + left_top_y = center_y - new_height / 2 + right_bottom_x = center_x + new_width / 2 + right_bottom_y = center_y + new_height / 2 + if left_top_x < 0: + right_bottom_x -= left_top_x + left_top_x = 0 + if left_top_y < 0: + right_bottom_y -= left_top_y + left_top_y = 0 + if right_bottom_x > src_w - 1: + left_top_x -= right_bottom_x - src_w + 1 + right_bottom_x = src_w - 1 + if right_bottom_y > src_h - 1: + left_top_y -= right_bottom_y - src_h + 1 + right_bottom_y = src_h - 1 + return int(left_top_x), int(left_top_y), int(right_bottom_x), int(right_bottom_y) + + +def crop(org_img, bbox, scale, out_w, out_h): + src_h, src_w, _ = np.shape(org_img) + left_top_x, left_top_y, right_bottom_x, right_bottom_y = _get_new_box(src_w, src_h, bbox, scale) + img = org_img[left_top_y : right_bottom_y + 1, left_top_x : right_bottom_x + 1] + dst_img = cv2.resize(img, (out_w, out_h)) + return dst_img diff --git a/deepface/models/spoofing/FasNetBackbone.py b/deepface/models/spoofing/FasNetBackbone.py new file mode 100644 index 0000000..abfb6ce --- /dev/null +++ b/deepface/models/spoofing/FasNetBackbone.py @@ -0,0 +1,524 @@ +# These classes are copied from Minivision's Silent-Face-Anti-Spoofing Repo +# licensed under Apache License 2.0 +# Ref: github.com/minivision-ai/Silent-Face-Anti-Spoofing/blob/master/src/model_lib/MiniFASNet.py + +# 3rd party dependencies +import torch +from torch.nn import ( + Linear, + Conv2d, + BatchNorm1d, + BatchNorm2d, + PReLU, + ReLU, + Sigmoid, + AdaptiveAvgPool2d, + Sequential, + Module, +) + +# pylint: disable=super-with-arguments, too-many-instance-attributes, unused-argument, redefined-builtin, too-few-public-methods + +keep_dict = { + "1.8M": [ + 32, + 32, + 103, + 103, + 64, + 13, + 13, + 64, + 26, + 26, + 64, + 13, + 13, + 64, + 52, + 52, + 64, + 231, + 231, + 128, + 154, + 154, + 128, + 52, + 52, + 128, + 26, + 26, + 128, + 52, + 52, + 128, + 26, + 26, + 128, + 26, + 26, + 128, + 308, + 308, + 128, + 26, + 26, + 128, + 26, + 26, + 128, + 512, + 512, + ], + "1.8M_": [ + 32, + 32, + 103, + 103, + 64, + 13, + 13, + 64, + 13, + 13, + 64, + 13, + 13, + 64, + 13, + 13, + 64, + 231, + 231, + 128, + 231, + 231, + 128, + 52, + 52, + 128, + 26, + 26, + 128, + 77, + 77, + 128, + 26, + 26, + 128, + 26, + 26, + 128, + 308, + 308, + 128, + 26, + 26, + 128, + 26, + 26, + 128, + 512, + 512, + ], +} + + +def MiniFASNetV2(embedding_size=128, conv6_kernel=(7, 7), drop_p=0.2, num_classes=3, img_channel=3): + return MiniFASNet( + keep_dict["1.8M_"], embedding_size, conv6_kernel, drop_p, num_classes, img_channel + ) + + +def MiniFASNetV1SE( + embedding_size=128, conv6_kernel=(7, 7), drop_p=0.75, num_classes=3, img_channel=3 +): + return MiniFASNetSE( + keep_dict["1.8M"], embedding_size, conv6_kernel, drop_p, num_classes, img_channel + ) + + +class Flatten(Module): + def forward(self, input): + return input.view(input.size(0), -1) + + +class Conv_block(Module): + def __init__(self, in_c, out_c, kernel=(1, 1), stride=(1, 1), padding=(0, 0), groups=1): + super(Conv_block, self).__init__() + self.conv = Conv2d( + in_c, + out_c, + kernel_size=kernel, + groups=groups, + stride=stride, + padding=padding, + bias=False, + ) + self.bn = BatchNorm2d(out_c) + self.prelu = PReLU(out_c) + + def forward(self, x): + x = self.conv(x) + x = self.bn(x) + x = self.prelu(x) + return x + + +class Linear_block(Module): + def __init__(self, in_c, out_c, kernel=(1, 1), stride=(1, 1), padding=(0, 0), groups=1): + super(Linear_block, self).__init__() + self.conv = Conv2d( + in_c, + out_channels=out_c, + kernel_size=kernel, + groups=groups, + stride=stride, + padding=padding, + bias=False, + ) + self.bn = BatchNorm2d(out_c) + + def forward(self, x): + x = self.conv(x) + x = self.bn(x) + return x + + +class Depth_Wise(Module): + def __init__( + self, c1, c2, c3, residual=False, kernel=(3, 3), stride=(2, 2), padding=(1, 1), groups=1 + ): + super(Depth_Wise, self).__init__() + c1_in, c1_out = c1 + c2_in, c2_out = c2 + c3_in, c3_out = c3 + self.conv = Conv_block(c1_in, out_c=c1_out, kernel=(1, 1), padding=(0, 0), stride=(1, 1)) + self.conv_dw = Conv_block( + c2_in, c2_out, groups=c2_in, kernel=kernel, padding=padding, stride=stride + ) + self.project = Linear_block(c3_in, c3_out, kernel=(1, 1), padding=(0, 0), stride=(1, 1)) + self.residual = residual + + def forward(self, x): + if self.residual: + short_cut = x + x = self.conv(x) + x = self.conv_dw(x) + x = self.project(x) + if self.residual: + output = short_cut + x + else: + output = x + return output + + +class Depth_Wise_SE(Module): + def __init__( + self, + c1, + c2, + c3, + residual=False, + kernel=(3, 3), + stride=(2, 2), + padding=(1, 1), + groups=1, + se_reduct=8, + ): + super(Depth_Wise_SE, self).__init__() + c1_in, c1_out = c1 + c2_in, c2_out = c2 + c3_in, c3_out = c3 + self.conv = Conv_block(c1_in, out_c=c1_out, kernel=(1, 1), padding=(0, 0), stride=(1, 1)) + self.conv_dw = Conv_block( + c2_in, c2_out, groups=c2_in, kernel=kernel, padding=padding, stride=stride + ) + self.project = Linear_block(c3_in, c3_out, kernel=(1, 1), padding=(0, 0), stride=(1, 1)) + self.residual = residual + self.se_module = SEModule(c3_out, se_reduct) + + def forward(self, x): + if self.residual: + short_cut = x + x = self.conv(x) + x = self.conv_dw(x) + x = self.project(x) + if self.residual: + x = self.se_module(x) + output = short_cut + x + else: + output = x + return output + + +class SEModule(Module): + def __init__(self, channels, reduction): + super(SEModule, self).__init__() + self.avg_pool = AdaptiveAvgPool2d(1) + self.fc1 = Conv2d(channels, channels // reduction, kernel_size=1, padding=0, bias=False) + self.bn1 = BatchNorm2d(channels // reduction) + self.relu = ReLU(inplace=True) + self.fc2 = Conv2d(channels // reduction, channels, kernel_size=1, padding=0, bias=False) + self.bn2 = BatchNorm2d(channels) + self.sigmoid = Sigmoid() + + def forward(self, x): + module_input = x + x = self.avg_pool(x) + x = self.fc1(x) + x = self.bn1(x) + x = self.relu(x) + x = self.fc2(x) + x = self.bn2(x) + x = self.sigmoid(x) + return module_input * x + + +class Residual(Module): + def __init__(self, c1, c2, c3, num_block, groups, kernel=(3, 3), stride=(1, 1), padding=(1, 1)): + super(Residual, self).__init__() + modules = [] + for i in range(num_block): + c1_tuple = c1[i] + c2_tuple = c2[i] + c3_tuple = c3[i] + modules.append( + Depth_Wise( + c1_tuple, + c2_tuple, + c3_tuple, + residual=True, + kernel=kernel, + padding=padding, + stride=stride, + groups=groups, + ) + ) + self.model = Sequential(*modules) + + def forward(self, x): + return self.model(x) + + +class ResidualSE(Module): + def __init__( + self, + c1, + c2, + c3, + num_block, + groups, + kernel=(3, 3), + stride=(1, 1), + padding=(1, 1), + se_reduct=4, + ): + super(ResidualSE, self).__init__() + modules = [] + for i in range(num_block): + c1_tuple = c1[i] + c2_tuple = c2[i] + c3_tuple = c3[i] + if i == num_block - 1: + modules.append( + Depth_Wise_SE( + c1_tuple, + c2_tuple, + c3_tuple, + residual=True, + kernel=kernel, + padding=padding, + stride=stride, + groups=groups, + se_reduct=se_reduct, + ) + ) + else: + modules.append( + Depth_Wise( + c1_tuple, + c2_tuple, + c3_tuple, + residual=True, + kernel=kernel, + padding=padding, + stride=stride, + groups=groups, + ) + ) + self.model = Sequential(*modules) + + def forward(self, x): + return self.model(x) + + +class MiniFASNet(Module): + def __init__( + self, keep, embedding_size, conv6_kernel=(7, 7), drop_p=0.0, num_classes=3, img_channel=3 + ): + super(MiniFASNet, self).__init__() + self.embedding_size = embedding_size + + self.conv1 = Conv_block(img_channel, keep[0], kernel=(3, 3), stride=(2, 2), padding=(1, 1)) + self.conv2_dw = Conv_block( + keep[0], keep[1], kernel=(3, 3), stride=(1, 1), padding=(1, 1), groups=keep[1] + ) + + c1 = [(keep[1], keep[2])] + c2 = [(keep[2], keep[3])] + c3 = [(keep[3], keep[4])] + + self.conv_23 = Depth_Wise( + c1[0], c2[0], c3[0], kernel=(3, 3), stride=(2, 2), padding=(1, 1), groups=keep[3] + ) + + c1 = [(keep[4], keep[5]), (keep[7], keep[8]), (keep[10], keep[11]), (keep[13], keep[14])] + c2 = [(keep[5], keep[6]), (keep[8], keep[9]), (keep[11], keep[12]), (keep[14], keep[15])] + c3 = [(keep[6], keep[7]), (keep[9], keep[10]), (keep[12], keep[13]), (keep[15], keep[16])] + + self.conv_3 = Residual( + c1, c2, c3, num_block=4, groups=keep[4], kernel=(3, 3), stride=(1, 1), padding=(1, 1) + ) + + c1 = [(keep[16], keep[17])] + c2 = [(keep[17], keep[18])] + c3 = [(keep[18], keep[19])] + + self.conv_34 = Depth_Wise( + c1[0], c2[0], c3[0], kernel=(3, 3), stride=(2, 2), padding=(1, 1), groups=keep[19] + ) + + c1 = [ + (keep[19], keep[20]), + (keep[22], keep[23]), + (keep[25], keep[26]), + (keep[28], keep[29]), + (keep[31], keep[32]), + (keep[34], keep[35]), + ] + c2 = [ + (keep[20], keep[21]), + (keep[23], keep[24]), + (keep[26], keep[27]), + (keep[29], keep[30]), + (keep[32], keep[33]), + (keep[35], keep[36]), + ] + c3 = [ + (keep[21], keep[22]), + (keep[24], keep[25]), + (keep[27], keep[28]), + (keep[30], keep[31]), + (keep[33], keep[34]), + (keep[36], keep[37]), + ] + + self.conv_4 = Residual( + c1, c2, c3, num_block=6, groups=keep[19], kernel=(3, 3), stride=(1, 1), padding=(1, 1) + ) + + c1 = [(keep[37], keep[38])] + c2 = [(keep[38], keep[39])] + c3 = [(keep[39], keep[40])] + + self.conv_45 = Depth_Wise( + c1[0], c2[0], c3[0], kernel=(3, 3), stride=(2, 2), padding=(1, 1), groups=keep[40] + ) + + c1 = [(keep[40], keep[41]), (keep[43], keep[44])] + c2 = [(keep[41], keep[42]), (keep[44], keep[45])] + c3 = [(keep[42], keep[43]), (keep[45], keep[46])] + + self.conv_5 = Residual( + c1, c2, c3, num_block=2, groups=keep[40], kernel=(3, 3), stride=(1, 1), padding=(1, 1) + ) + self.conv_6_sep = Conv_block( + keep[46], keep[47], kernel=(1, 1), stride=(1, 1), padding=(0, 0) + ) + self.conv_6_dw = Linear_block( + keep[47], keep[48], groups=keep[48], kernel=conv6_kernel, stride=(1, 1), padding=(0, 0) + ) + self.conv_6_flatten = Flatten() + self.linear = Linear(512, embedding_size, bias=False) + self.bn = BatchNorm1d(embedding_size) + self.drop = torch.nn.Dropout(p=drop_p) + self.prob = Linear(embedding_size, num_classes, bias=False) + + def forward(self, x): + out = self.conv1(x) + out = self.conv2_dw(out) + out = self.conv_23(out) + out = self.conv_3(out) + out = self.conv_34(out) + out = self.conv_4(out) + out = self.conv_45(out) + out = self.conv_5(out) + out = self.conv_6_sep(out) + out = self.conv_6_dw(out) + out = self.conv_6_flatten(out) + if self.embedding_size != 512: + out = self.linear(out) + out = self.bn(out) + out = self.drop(out) + out = self.prob(out) + return out + + +class MiniFASNetSE(MiniFASNet): + def __init__( + self, keep, embedding_size, conv6_kernel=(7, 7), drop_p=0.75, num_classes=4, img_channel=3 + ): + super(MiniFASNetSE, self).__init__( + keep=keep, + embedding_size=embedding_size, + conv6_kernel=conv6_kernel, + drop_p=drop_p, + num_classes=num_classes, + img_channel=img_channel, + ) + + c1 = [(keep[4], keep[5]), (keep[7], keep[8]), (keep[10], keep[11]), (keep[13], keep[14])] + c2 = [(keep[5], keep[6]), (keep[8], keep[9]), (keep[11], keep[12]), (keep[14], keep[15])] + c3 = [(keep[6], keep[7]), (keep[9], keep[10]), (keep[12], keep[13]), (keep[15], keep[16])] + + self.conv_3 = ResidualSE( + c1, c2, c3, num_block=4, groups=keep[4], kernel=(3, 3), stride=(1, 1), padding=(1, 1) + ) + + c1 = [ + (keep[19], keep[20]), + (keep[22], keep[23]), + (keep[25], keep[26]), + (keep[28], keep[29]), + (keep[31], keep[32]), + (keep[34], keep[35]), + ] + c2 = [ + (keep[20], keep[21]), + (keep[23], keep[24]), + (keep[26], keep[27]), + (keep[29], keep[30]), + (keep[32], keep[33]), + (keep[35], keep[36]), + ] + c3 = [ + (keep[21], keep[22]), + (keep[24], keep[25]), + (keep[27], keep[28]), + (keep[30], keep[31]), + (keep[33], keep[34]), + (keep[36], keep[37]), + ] + + self.conv_4 = ResidualSE( + c1, c2, c3, num_block=6, groups=keep[19], kernel=(3, 3), stride=(1, 1), padding=(1, 1) + ) + + c1 = [(keep[40], keep[41]), (keep[43], keep[44])] + c2 = [(keep[41], keep[42]), (keep[44], keep[45])] + c3 = [(keep[42], keep[43]), (keep[45], keep[46])] + self.conv_5 = ResidualSE( + c1, c2, c3, num_block=2, groups=keep[40], kernel=(3, 3), stride=(1, 1), padding=(1, 1) + ) diff --git a/deepface/models/spoofing/__init__.py b/deepface/models/spoofing/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/deepface/modules/__init__.py b/deepface/modules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/deepface/modules/demography.py b/deepface/modules/demography.py new file mode 100644 index 0000000..b68314b --- /dev/null +++ b/deepface/modules/demography.py @@ -0,0 +1,212 @@ +# 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.models.demography 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, + anti_spoofing: 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). + + anti_spoofing (boolean): Flag to enable anti spoofing (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, + enforce_detection=enforce_detection, + grayscale=False, + align=align, + expand_percentage=expand_percentage, + anti_spoofing=anti_spoofing, + ) + + for img_obj in img_objs: + if anti_spoofing is True and img_obj.get("is_real", True) is False: + raise ValueError("Spoof detected in the given image.") + + 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( + task="facial_attribute", model_name="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( + task="facial_attribute", model_name="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( + task="facial_attribute", model_name="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( + task="facial_attribute", model_name="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/deepface/modules/detection.py b/deepface/modules/detection.py new file mode 100644 index 0000000..46165a9 --- /dev/null +++ b/deepface/modules/detection.py @@ -0,0 +1,410 @@ +# built-in dependencies +from typing import Any, Dict, List, Tuple, Union, Optional + +# 3rd part dependencies +from heapq import nlargest +import numpy as np +import cv2 + +# project dependencies +from deepface.modules import modeling +from deepface.models.Detector import Detector, DetectedFace, FacialAreaRegion +from deepface.commons import image_utils + +from deepface.commons.logger import Logger + +logger = 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, + color_face: str = "rgb", + normalize_face: bool = True, + anti_spoofing: bool = False, + max_faces: Optional[int] = None, +) -> 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): (Deprecated) Flag to convert the output face image to grayscale + (default is False). + + color_face (string): Color to return face image output. Options: 'rgb', 'bgr' or 'gray' + (default is 'rgb'). + + normalize_face (boolean): Flag to enable normalization (divide by 255) of the output + face image output face image normalization (default is True). + + anti_spoofing (boolean): Flag to enable anti spoofing (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. + + - "is_real" (boolean): antispoofing analyze result. this key is just available in the + result only if anti_spoofing is set to True in input arguments. + + - "antispoof_score" (float): score of antispoofing analyze result. this key is + just available in the result only if anti_spoofing is set to True in input arguments. + """ + + 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}") + + height, width, _ = img.shape + + base_region = FacialAreaRegion(x=0, y=0, w=width, h=height, confidence=0) + + if detector_backend == "skip": + face_objs = [DetectedFace(img=img, facial_area=base_region, confidence=0)] + else: + face_objs = detect_faces( + detector_backend=detector_backend, + img=img, + align=align, + expand_percentage=expand_percentage, + max_faces=max_faces, + ) + + # 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: + logger.warn("Parameter grayscale is deprecated. Use color_face instead.") + current_img = cv2.cvtColor(current_img, cv2.COLOR_BGR2GRAY) + else: + if color_face == "rgb": + current_img = current_img[:, :, ::-1] + elif color_face == "bgr": + pass # image is in BGR + elif color_face == "gray": + current_img = cv2.cvtColor(current_img, cv2.COLOR_BGR2GRAY) + else: + raise ValueError(f"The color_face can be rgb, bgr or gray, but it is {color_face}.") + + if normalize_face: + current_img = current_img / 255 # normalize input in [0, 1] + + # cast to int for flask, and do final checks for borders + x = max(0, int(current_region.x)) + y = max(0, int(current_region.y)) + w = min(width - x - 1, int(current_region.w)) + h = min(height - y - 1, int(current_region.h)) + + resp_obj = { + "face": current_img, + "facial_area": { + "x": x, + "y": y, + "w": w, + "h": h, + "left_eye": current_region.left_eye, + "right_eye": current_region.right_eye, + }, + "confidence": round(float(current_region.confidence or 0), 2), + } + + if anti_spoofing is True: + antispoof_model = modeling.build_model(task="spoofing", model_name="Fasnet") + is_real, antispoof_score = antispoof_model.analyze(img=img, facial_area=(x, y, w, h)) + resp_obj["is_real"] = is_real + resp_obj["antispoof_score"] = antispoof_score + + resp_objs.append(resp_obj) + + 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 detect_faces( + detector_backend: str, + img: np.ndarray, + align: bool = True, + expand_percentage: int = 0, + max_faces: Optional[int] = None, +) -> 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. + """ + height, width, _ = img.shape + face_detector: Detector = modeling.build_model( + task="face_detector", model_name=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 + + # If faces are close to the upper boundary, alignment move them outside + # Add a black border around an image to avoid this. + height_border = int(0.5 * height) + width_border = int(0.5 * width) + if align is True: + img = cv2.copyMakeBorder( + img, + height_border, + height_border, + width_border, + width_border, + cv2.BORDER_CONSTANT, + value=[0, 0, 0], # Color of the border (black) + ) + + # find facial areas of given image + facial_areas = face_detector.detect_faces(img) + + if max_faces is not None and max_faces < len(facial_areas): + facial_areas = nlargest( + max_faces, facial_areas, key=lambda facial_area: facial_area.w * facial_area.h + ) + + return [ + expand_and_align_face( + facial_area=facial_area, + img=img, + align=align, + expand_percentage=expand_percentage, + width_border=width_border, + height_border=height_border, + ) + for facial_area in facial_areas + ] + + +def expand_and_align_face( + facial_area: FacialAreaRegion, + img: np.ndarray, + align: bool, + expand_percentage: int, + width_border: int, + height_border: int, +) -> DetectedFace: + 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 = align_img_wrt_eyes(img=img, left_eye=left_eye, right_eye=right_eye) + + rotated_x1, rotated_y1, rotated_x2, rotated_y2 = project_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) + ] + + # restore x, y, le and re before border added + x = x - width_border + y = y - height_border + # w and h will not change + if left_eye is not None: + left_eye = (left_eye[0] - width_border, left_eye[1] - height_border) + if right_eye is not None: + right_eye = (right_eye[0] - width_border, right_eye[1] - height_border) + + return DetectedFace( + img=detected_face, + facial_area=FacialAreaRegion( + x=x, y=y, h=h, w=w, confidence=confidence, left_eye=left_eye, right_eye=right_eye + ), + confidence=confidence, + ) + + +def align_img_wrt_eyes( + 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]))) + + (h, w) = img.shape[:2] + center = (w // 2, h // 2) + M = cv2.getRotationMatrix2D(center, angle, 1.0) + img = cv2.warpAffine( + img, M, (w, h), flags=cv2.INTER_CUBIC, borderMode=cv2.BORDER_CONSTANT, borderValue=(0, 0, 0) + ) + + return img, angle + + +def project_facial_area( + facial_area: Tuple[int, int, int, int], angle: float, size: Tuple[int, int] +) -> Tuple[int, int, int, int]: + """ + Update pre-calculated facial area coordinates after image itself + rotated with respect to the eyes. + 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/deepface/modules/modeling.py b/deepface/modules/modeling.py new file mode 100644 index 0000000..c097c92 --- /dev/null +++ b/deepface/modules/modeling.py @@ -0,0 +1,100 @@ +# built-in dependencies +from typing import Any + +# project dependencies +from deepface.models.facial_recognition import ( + VGGFace, + OpenFace, + FbDeepFace, + DeepID, + ArcFace, + SFace, + Dlib, + Facenet, + GhostFaceNet, +) +from deepface.models.face_detection import ( + FastMtCnn, + MediaPipe, + MtCnn, + OpenCv, + Dlib as DlibDetector, + RetinaFace, + Ssd, + Yolo, + YuNet, + CenterFace, +) +from deepface.models.demography import Age, Gender, Race, Emotion +from deepface.models.spoofing import FasNet + + +def build_model(task: str, model_name: str) -> Any: + """ + This function loads a pre-trained models as singletonish way + Parameters: + task (str): facial_recognition, facial_attribute, face_detector, spoofing + model_name (str): model identifier + - VGG-Face, Facenet, Facenet512, OpenFace, DeepFace, DeepID, Dlib, + ArcFace, SFace, GhostFaceNet for face recognition + - Age, Gender, Emotion, Race for facial attributes + - opencv, mtcnn, ssd, dlib, retinaface, mediapipe, yolov8, yunet, + fastmtcnn or centerface for face detectors + - Fasnet for spoofing + Returns: + built model class + """ + + # singleton design pattern + global cached_models + + models = { + "facial_recognition": { + "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, + }, + "spoofing": { + "Fasnet": FasNet.Fasnet, + }, + "facial_attribute": { + "Emotion": Emotion.EmotionClient, + "Age": Age.ApparentAgeClient, + "Gender": Gender.GenderClient, + "Race": Race.RaceClient, + }, + "face_detector": { + "opencv": OpenCv.OpenCvClient, + "mtcnn": MtCnn.MtCnnClient, + "ssd": Ssd.SsdClient, + "dlib": DlibDetector.DlibClient, + "retinaface": RetinaFace.RetinaFaceClient, + "mediapipe": MediaPipe.MediaPipeClient, + "yolov8": Yolo.YoloClient, + "yunet": YuNet.YuNetClient, + "fastmtcnn": FastMtCnn.FastMtCnnClient, + "centerface": CenterFace.CenterFaceClient, + }, + } + + if models.get(task) is None: + raise ValueError(f"unimplemented task - {task}") + + if not "cached_models" in globals(): + cached_models = {current_task: {} for current_task in models.keys()} + + if cached_models[task].get(model_name) is None: + model = models[task].get(model_name) + if model: + cached_models[task][model_name] = model() + else: + raise ValueError(f"Invalid model_name passed - {task}/{model_name}") + + return cached_models[task][model_name] diff --git a/deepface/modules/preprocessing.py b/deepface/modules/preprocessing.py new file mode 100644 index 0000000..459adba --- /dev/null +++ b/deepface/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/deepface/modules/recognition.py b/deepface/modules/recognition.py new file mode 100644 index 0000000..799dfbc --- /dev/null +++ b/deepface/modules/recognition.py @@ -0,0 +1,417 @@ +# built-in dependencies +import os +import pickle +from typing import List, Union, Optional, Dict, Any, Set +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.logger import Logger + +logger = 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, + refresh_database: bool = True, + anti_spoofing: 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. + + refresh_database (boolean): Synchronizes the images representation (pkl) file with the + directory/db files, if set to false, it will ignore any file changes inside the db_path + directory (default is True). + + anti_spoofing (boolean): Flag to enable anti spoofing (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 + """ + + tic = time.time() + + if not os.path.isdir(db_path): + raise ValueError(f"Passed path {db_path} does not exist!") + + img, _ = image_utils.load_image(img_path) + if img is None: + raise ValueError(f"Passed image path {img_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 = 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 and refresh_database is True: + raise ValueError(f"No item found in {db_path}") + if len(representations) == 0 and refresh_database is False: + raise ValueError(f"Nothing is found in {datastore_path}") + + must_save_pickle = False + new_images, old_images, replaced_images = set(), set(), set() + + if not refresh_database: + logger.info( + f"Could be some changes in {db_path} not tracked." + "Set refresh_database to true to assure that any changes will be tracked." + ) + + # Enforce data consistency amongst on disk images and pickle file + if refresh_database: + new_images = set(storage_images) - set(pickled_images) # images added to storage + old_images = set(pickled_images) - set(storage_images) # images removed from storage + + # detect 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.add(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.update(replaced_images) + old_images.update(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, + anti_spoofing=anti_spoofing, + ) + + resp_obj = [] + + for source_obj in source_objs: + if anti_spoofing is True and source_obj.get("is_real", True) is False: + raise ValueError("Spoof detected in the given image.") + 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: Set[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/deepface/modules/representation.py b/deepface/modules/representation.py new file mode 100644 index 0000000..a147640 --- /dev/null +++ b/deepface/modules/representation.py @@ -0,0 +1,144 @@ +# built-in dependencies +from typing import Any, Dict, List, Union, Optional + +# 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", + anti_spoofing: bool = False, + max_faces: Optional[int] = None, +) -> 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 + + anti_spoofing (boolean): Flag to enable anti spoofing (default is False). + + max_faces (int): Set a limit on the number of faces to be processed (default is None). + + 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( + task="facial_recognition", model_name=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, + anti_spoofing=anti_spoofing, + max_faces=max_faces, + ) + 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[0], "h": img.shape[1]}, + "confidence": 0, + } + ] + # --------------------------------- + + if max_faces is not None and max_faces < len(img_objs): + # sort as largest facial areas come first + img_objs = sorted( + img_objs, + key=lambda img_obj: img_obj["facial_area"]["w"] * img_obj["facial_area"]["h"], + reverse=True, + ) + # discard rest of the items + img_objs = img_objs[0:max_faces] + + for img_obj in img_objs: + if anti_spoofing is True and img_obj.get("is_real", True) is False: + raise ValueError("Spoof detected in the given image.") + 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_objs.append( + { + "embedding": embedding, + "facial_area": region, + "face_confidence": confidence, + } + ) + + return resp_objs diff --git a/deepface/modules/streaming.py b/deepface/modules/streaming.py new file mode 100644 index 0000000..c1a0363 --- /dev/null +++ b/deepface/modules/streaming.py @@ -0,0 +1,1006 @@ +# built-in dependencies +import os +import time +from typing import List, Tuple, Optional +import traceback + +# 3rd party dependencies +import numpy as np +import pandas as pd +import cv2 + +# project dependencies +from deepface import DeepFace +from deepface.commons.logger import Logger + +logger = Logger() + +# dependency configuration +os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2" + + +IDENTIFIED_IMG_SIZE = 112 +TEXT_COLOR = (255, 255, 255) + +# pylint: disable=unused-variable +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, + anti_spoofing: bool = False, +): + """ + 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). + + anti_spoofing (boolean): Flag to enable anti spoofing (default is False). + + 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, anti_spoofing=anti_spoofing + ) + + # 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, anti_spoofing=anti_spoofing + ) + + # 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(task="facial_recognition", 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 *= 255 + target_img = target_img[:, :, ::-1] + else: + target_img = cv2.imread(target_path) + + # resize anyway + target_img = cv2.resize(target_img, (IDENTIFIED_IMG_SIZE, IDENTIFIED_IMG_SIZE)) + + 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(task="facial_attribute", model_name="Age") + logger.info("Age model is just built") + DeepFace.build_model(task="facial_attribute", model_name="Gender") + logger.info("Gender model is just built") + DeepFace.build_model(task="facial_attribute", 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, bool, float]], + anti_spoofing: bool = False, +) -> 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 + also is_real and antispoof_score keys + anti_spoofing (boolean): Flag to enable anti spoofing (default is False). + Returns: + img (np.ndarray): image with highlighted facial areas + """ + for x, y, w, h, is_real, antispoof_score in faces_coordinates: + # highlight facial area with rectangle + + if anti_spoofing is False: + color = (67, 67, 67) + else: + if is_real is True: + color = (0, 255, 0) + else: + color = (0, 0, 255) + cv2.rectangle(img, (x, y), (x + w, y + h), color, 1) + return img + + +def countdown_to_freeze( + img: np.ndarray, + faces_coordinates: List[Tuple[int, int, int, int, bool, float]], + 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, is_real, antispoof_score 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, anti_spoofing: bool = False +) -> List[Tuple[int, int, int, int, bool, float]]: + """ + 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, + anti_spoofing=anti_spoofing, + ) + faces = [ + ( + face_obj["facial_area"]["x"], + face_obj["facial_area"]["y"], + face_obj["facial_area"]["w"], + face_obj["facial_area"]["h"], + face_obj.get("is_real", True), + face_obj.get("antispoof_score", 0), + ) + 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, bool, float]] +) -> 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 also is_real and antispoof_score keys + Returns: + detected_faces (list): list of detected facial area images + """ + detected_faces = [] + for x, y, w, h, is_real, antispoof_score 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, bool, float]], + 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 also is_real and antispoof_score keys + 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, is_real, antispoof_score) 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, bool, float]], + 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 also is_real and antispoof_score keys + 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, is_real, antispoof_score) 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(f"{str(err)} - {traceback.format_exc()}") + 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/deepface/modules/verification.py b/deepface/modules/verification.py new file mode 100644 index 0000000..b6b1002 --- /dev/null +++ b/deepface/modules/verification.py @@ -0,0 +1,382 @@ +# built-in dependencies +import time +from typing import Any, Dict, Optional, 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.logger import Logger + +logger = 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, + threshold: Optional[float] = None, + anti_spoofing: 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). + + 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). + + anti_spoofing (boolean): Flag to enable anti spoofing (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. + + - 'threshold' (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( + task="facial_recognition", model_name=model_name + ) + dims = model.output_shape + + no_facial_area = { + "x": None, + "y": None, + "w": None, + "h": None, + "left_eye": None, + "right_eye": None, + } + + def extract_embeddings_and_facial_areas( + img_path: Union[str, np.ndarray, List[float]], index: int + ) -> Tuple[List[List[float]], List[dict]]: + """ + Extracts facial embeddings and corresponding facial areas from an + image or returns pre-calculated embeddings. + + Depending on the type of img_path, the function either extracts + facial embeddings from the provided image + (via a path or NumPy array) or verifies that the input is a list of + pre-calculated embeddings and validates them. + + Args: + img_path (Union[str, np.ndarray, List[float]]): + - A string representing the file path to an image, + - A NumPy array containing the image data, + - Or a list of pre-calculated embedding values (of type `float`). + index (int): An index value used in error messages and logging + to identify the number of the image. + + Returns: + Tuple[List[List[float]], List[dict]]: + - A list containing lists of facial embeddings for each detected face. + - A list of dictionaries where each dictionary contains facial area information. + """ + if isinstance(img_path, list): + # given image is already pre-calculated embedding + if not all(isinstance(dim, float) for dim in img_path): + raise ValueError( + f"When passing img{index}_path as a list," + " ensure that all its items are of type float." + ) + + if silent is False: + logger.warn( + f"You passed {index}-th image as pre-calculated embeddings." + "Please ensure that embeddings have been calculated" + f" for the {model_name} model." + ) + + if len(img_path) != dims: + raise ValueError( + f"embeddings of {model_name} should have {dims} dimensions," + f" but {index}-th image has {len(img_path)} dimensions input" + ) + + img_embeddings = [img_path] + img_facial_areas = [no_facial_area] + else: + try: + img_embeddings, img_facial_areas = __extract_faces_and_embeddings( + img_path=img_path, + model_name=model_name, + detector_backend=detector_backend, + enforce_detection=enforce_detection, + align=align, + expand_percentage=expand_percentage, + normalization=normalization, + anti_spoofing=anti_spoofing, + ) + except ValueError as err: + raise ValueError(f"Exception while processing img{index}_path") from err + return img_embeddings, img_facial_areas + + img1_embeddings, img1_facial_areas = extract_embeddings_and_facial_areas(img1_path, 1) + img2_embeddings, img2_facial_areas = extract_embeddings_and_facial_areas(img2_path, 2) + + min_distance, min_idx, min_idy = float("inf"), None, None + 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) + if distance < min_distance: + min_distance, min_idx, min_idy = distance, idx, idy + + # find the face pair with minimum distance + threshold = threshold or find_threshold(model_name, distance_metric) + distance = float(min_distance) + facial_areas = ( + no_facial_area if min_idx is None else img1_facial_areas[min_idx], + no_facial_area if min_idy is None else img2_facial_areas[min_idy], + ) + + 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", + anti_spoofing: bool = False, +) -> 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, + anti_spoofing=anti_spoofing, + ) + + # find embeddings for each face + for img_obj in img_objs: + if anti_spoofing is True and img_obj.get("is_real", True) is False: + raise ValueError("Spoof detected in given image.") + 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.dot(source_representation, test_representation) + b = np.linalg.norm(source_representation) + c = np.linalg.norm(test_representation) + return 1 - a / (b * 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) + + return np.linalg.norm(source_representation - test_representation) + + +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) + norm = np.linalg.norm(x) + return x if norm == 0 else x / norm + + +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/icon/benchmarks.jpg b/icon/benchmarks.jpg new file mode 100644 index 0000000..adcd846 Binary files /dev/null and b/icon/benchmarks.jpg differ diff --git a/icon/bmc-button.png b/icon/bmc-button.png new file mode 100644 index 0000000..464bfd9 Binary files /dev/null and b/icon/bmc-button.png differ diff --git a/icon/deepface-and-react.jpg b/icon/deepface-and-react.jpg new file mode 100644 index 0000000..4203a23 Binary files /dev/null and b/icon/deepface-and-react.jpg differ diff --git a/icon/deepface-api.jpg b/icon/deepface-api.jpg new file mode 100644 index 0000000..8ca3fec Binary files /dev/null and b/icon/deepface-api.jpg differ diff --git a/icon/deepface-big-data.jpg b/icon/deepface-big-data.jpg new file mode 100644 index 0000000..5061f1e Binary files /dev/null and b/icon/deepface-big-data.jpg differ diff --git a/icon/deepface-dockerized-v2.jpg b/icon/deepface-dockerized-v2.jpg new file mode 100644 index 0000000..db79929 Binary files /dev/null and b/icon/deepface-dockerized-v2.jpg differ diff --git a/icon/deepface-icon-labeled.png b/icon/deepface-icon-labeled.png new file mode 100644 index 0000000..520887c Binary files /dev/null and b/icon/deepface-icon-labeled.png differ diff --git a/icon/deepface-icon.png b/icon/deepface-icon.png new file mode 100644 index 0000000..d0a3b9b Binary files /dev/null and b/icon/deepface-icon.png differ diff --git a/icon/detector-outputs-20230203.jpg b/icon/detector-outputs-20230203.jpg new file mode 100644 index 0000000..b894e75 Binary files /dev/null and b/icon/detector-outputs-20230203.jpg differ diff --git a/icon/detector-outputs-20240302.jpg b/icon/detector-outputs-20240302.jpg new file mode 100644 index 0000000..b7bf517 Binary files /dev/null and b/icon/detector-outputs-20240302.jpg differ diff --git a/icon/detector-outputs-20240414.jpg b/icon/detector-outputs-20240414.jpg new file mode 100644 index 0000000..15f73fb Binary files /dev/null and b/icon/detector-outputs-20240414.jpg differ diff --git a/icon/detector-portfolio-v5.jpg b/icon/detector-portfolio-v5.jpg new file mode 100644 index 0000000..e35cef1 Binary files /dev/null and b/icon/detector-portfolio-v5.jpg differ diff --git a/icon/detector-portfolio-v6.jpg b/icon/detector-portfolio-v6.jpg new file mode 100644 index 0000000..3fb158a Binary files /dev/null and b/icon/detector-portfolio-v6.jpg differ diff --git a/icon/embedding.jpg b/icon/embedding.jpg new file mode 100644 index 0000000..8555913 Binary files /dev/null and b/icon/embedding.jpg differ diff --git a/icon/face-anti-spoofing.jpg b/icon/face-anti-spoofing.jpg new file mode 100644 index 0000000..bade126 Binary files /dev/null and b/icon/face-anti-spoofing.jpg differ diff --git a/icon/model-portfolio-20240316.jpg b/icon/model-portfolio-20240316.jpg new file mode 100644 index 0000000..7252155 Binary files /dev/null and b/icon/model-portfolio-20240316.jpg differ diff --git a/icon/model-portfolio-v8.jpg b/icon/model-portfolio-v8.jpg new file mode 100644 index 0000000..b359f00 Binary files /dev/null and b/icon/model-portfolio-v8.jpg differ diff --git a/icon/patreon.png b/icon/patreon.png new file mode 100644 index 0000000..21cbfd9 Binary files /dev/null and b/icon/patreon.png differ diff --git a/icon/retinaface-results.jpeg b/icon/retinaface-results.jpeg new file mode 100644 index 0000000..11e5db9 Binary files /dev/null and b/icon/retinaface-results.jpeg differ diff --git a/icon/stock-1.jpg b/icon/stock-1.jpg new file mode 100644 index 0000000..b786042 Binary files /dev/null and b/icon/stock-1.jpg differ diff --git a/icon/stock-2.jpg b/icon/stock-2.jpg new file mode 100644 index 0000000..4e542ea Binary files /dev/null and b/icon/stock-2.jpg differ diff --git a/icon/stock-3.jpg b/icon/stock-3.jpg new file mode 100644 index 0000000..68254cb Binary files /dev/null and b/icon/stock-3.jpg differ diff --git a/icon/stock-6-v2.jpg b/icon/stock-6-v2.jpg new file mode 100644 index 0000000..a7fe5ac Binary files /dev/null and b/icon/stock-6-v2.jpg differ diff --git a/icon/verify-many-faces.jpg b/icon/verify-many-faces.jpg new file mode 100644 index 0000000..66fc890 Binary files /dev/null and b/icon/verify-many-faces.jpg differ diff --git a/package_info.json b/package_info.json new file mode 100644 index 0000000..1dc17ea --- /dev/null +++ b/package_info.json @@ -0,0 +1,3 @@ +{ + "version": "0.0.94" +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8f40c82 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,15 @@ +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 +flask_cors>=4.0.1 +mtcnn>=0.1.0 +retina-face>=0.0.1 +fire>=0.4.0 +gunicorn>=20.1.0 diff --git a/requirements_additional.txt b/requirements_additional.txt new file mode 100644 index 0000000..ea76fde --- /dev/null +++ b/requirements_additional.txt @@ -0,0 +1,6 @@ +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 +torch>=2.1.2 \ No newline at end of file diff --git a/requirements_local b/requirements_local new file mode 100644 index 0000000..e869c3f --- /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/scripts/dockerize.sh b/scripts/dockerize.sh new file mode 100644 index 0000000..f29bed7 --- /dev/null +++ b/scripts/dockerize.sh @@ -0,0 +1,32 @@ +# Dockerfile is in the root +cd .. + +# start docker +# sudo service docker start + +# list current docker packages +# docker container ls -a + +# delete existing deepface packages +# docker rm -f $(docker ps -a -q --filter "ancestor=deepface") + +# build deepface image +docker build -t deepface . + +# copy weights from your local +# docker cp ~/.deepface/weights/. :/root/.deepface/weights/ + +# run the built image +# docker run --net="host" deepface +docker run -p 5005:5000 deepface + +# or pull the pre-built image from docker hub and run it +# docker pull serengil/deepface +# docker run -p 5005:5000 serengil/deepface + +# to access the inside of docker image when it is in running status +# docker exec -it /bin/sh + +# healthcheck +# sleep 3s +# curl localhost:5000 \ No newline at end of file diff --git a/scripts/push-release.sh b/scripts/push-release.sh new file mode 100644 index 0000000..5b3e6fa --- /dev/null +++ b/scripts/push-release.sh @@ -0,0 +1,11 @@ +cd .. + +echo "deleting existing release related files" +rm -rf dist/* +rm -rf build/* + +echo "creating a package for current release - pypi compatible" +python setup.py sdist bdist_wheel + +echo "pushing the release to pypi" +python -m twine upload dist/* \ No newline at end of file diff --git a/scripts/service.sh b/scripts/service.sh new file mode 100755 index 0000000..0d60f2b --- /dev/null +++ b/scripts/service.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +cd ../deepface/api/src + +# run the service with flask - not for production purposes +# python api.py + +# run the service with gunicorn - for prod purposes +gunicorn --workers=1 --timeout=3600 --bind=127.0.0.1:5005 "app:create_app()" \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..3dc0270 --- /dev/null +++ b/setup.py @@ -0,0 +1,38 @@ +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", + license="MIT", + install_requires=requirements, +) diff --git a/tests/dataset/couple.jpg b/tests/dataset/couple.jpg new file mode 100644 index 0000000..1a07d76 Binary files /dev/null and b/tests/dataset/couple.jpg differ diff --git a/tests/dataset/face-recognition-pivot.csv b/tests/dataset/face-recognition-pivot.csv new file mode 100644 index 0000000..7c3f68b --- /dev/null +++ b/tests/dataset/face-recognition-pivot.csv @@ -0,0 +1,281 @@ +file_x,file_y,decision,VGG-Face_cosine,VGG-Face_euclidean,VGG-Face_euclidean_l2,Facenet_cosine,Facenet_euclidean,Facenet_euclidean_l2,OpenFace_cosine,OpenFace_euclidean_l2,DeepFace_cosine,DeepFace_euclidean,DeepFace_euclidean_l2 +deepface/tests/dataset/img38.jpg,deepface/tests/dataset/img39.jpg,Yes,0.2057,0.389,0.6414,0.1601,6.8679,0.5658,0.5925,1.0886,0.2554,61.3336,0.7147 +deepface/tests/dataset/img38.jpg,deepface/tests/dataset/img40.jpg,Yes,0.2117,0.3179,0.6508,0.2739,8.9049,0.7402,0.396,0.8899,0.2685,63.3747,0.7328 +deepface/tests/dataset/img38.jpg,deepface/tests/dataset/img41.jpg,Yes,0.1073,0.2482,0.4632,0.1257,6.1593,0.5014,0.7157,1.1964,0.2452,60.3454,0.7002 +deepface/tests/dataset/img39.jpg,deepface/tests/dataset/img40.jpg,Yes,0.2991,0.4567,0.7734,0.3134,9.3798,0.7917,0.4941,0.9941,0.1703,45.1688,0.5836 +deepface/tests/dataset/img39.jpg,deepface/tests/dataset/img41.jpg,Yes,0.1666,0.3542,0.5772,0.1502,6.6491,0.5481,0.2381,0.6901,0.2194,50.4356,0.6624 +deepface/tests/dataset/img40.jpg,deepface/tests/dataset/img41.jpg,Yes,0.1706,0.3066,0.5841,0.2017,7.6423,0.6352,0.567,1.0649,0.2423,54.2499,0.6961 +deepface/tests/dataset/img3.jpg,deepface/tests/dataset/img12.jpg,Yes,0.2533,0.5199,0.7118,0.4062,11.2632,0.9014,0.1908,0.6178,0.2337,58.8794,0.6837 +deepface/tests/dataset/img3.jpg,deepface/tests/dataset/img53.jpg,Yes,0.1655,0.3567,0.5754,0.184,7.5388,0.6066,0.1465,0.5412,0.243,55.2642,0.6971 +deepface/tests/dataset/img3.jpg,deepface/tests/dataset/img54.jpg,Yes,0.1982,0.4739,0.6297,0.406,11.0618,0.9011,0.1132,0.4758,0.1824,49.7875,0.6041 +deepface/tests/dataset/img3.jpg,deepface/tests/dataset/img55.jpg,Yes,0.1835,0.3742,0.6057,0.1366,6.4168,0.5227,0.1755,0.5924,0.1697,55.179,0.5825 +deepface/tests/dataset/img3.jpg,deepface/tests/dataset/img56.jpg,Yes,0.1652,0.4005,0.5748,0.1833,7.3432,0.6054,0.1803,0.6005,0.2061,59.007,0.642 +deepface/tests/dataset/img12.jpg,deepface/tests/dataset/img53.jpg,Yes,0.372,0.6049,0.8626,0.3933,11.1382,0.8869,0.1068,0.4621,0.1633,48.5516,0.5715 +deepface/tests/dataset/img12.jpg,deepface/tests/dataset/img54.jpg,Yes,0.2153,0.5145,0.6561,0.2694,9.1155,0.734,0.1943,0.6234,0.1881,52.7146,0.6133 +deepface/tests/dataset/img12.jpg,deepface/tests/dataset/img55.jpg,Yes,0.3551,0.5941,0.8428,0.4726,12.0647,0.9722,0.1054,0.4591,0.1265,48.2432,0.5029 +deepface/tests/dataset/img12.jpg,deepface/tests/dataset/img56.jpg,Yes,0.2826,0.565,0.7518,0.4761,11.9569,0.9758,0.1364,0.5224,0.1908,57.6735,0.6177 +deepface/tests/dataset/img53.jpg,deepface/tests/dataset/img54.jpg,Yes,0.3363,0.593,0.8202,0.4627,11.8744,0.962,0.1964,0.6267,0.174,46.6212,0.5898 +deepface/tests/dataset/img53.jpg,deepface/tests/dataset/img55.jpg,Yes,0.187,0.3313,0.6116,0.1625,7.0394,0.5701,0.1312,0.5123,0.1439,52.3132,0.5365 +deepface/tests/dataset/img53.jpg,deepface/tests/dataset/img56.jpg,Yes,0.1385,0.3776,0.5263,0.141,6.4913,0.5311,0.1285,0.507,0.2005,58.0586,0.6332 +deepface/tests/dataset/img54.jpg,deepface/tests/dataset/img55.jpg,Yes,0.3124,0.5756,0.7905,0.4033,10.944,0.8981,0.1738,0.5896,0.1351,49.8255,0.5198 +deepface/tests/dataset/img54.jpg,deepface/tests/dataset/img56.jpg,Yes,0.2571,0.5473,0.717,0.3912,10.6329,0.8846,0.1802,0.6002,0.1648,53.0881,0.574 +deepface/tests/dataset/img55.jpg,deepface/tests/dataset/img56.jpg,Yes,0.2217,0.4543,0.6658,0.1433,6.4387,0.5353,0.1677,0.5792,0.1505,53.6812,0.5486 +deepface/tests/dataset/img1.jpg,deepface/tests/dataset/img2.jpg,Yes,0.2342,0.5033,0.6844,0.2508,8.2369,0.7082,0.0844,0.4109,0.2417,64.2748,0.6952 +deepface/tests/dataset/img1.jpg,deepface/tests/dataset/img4.jpg,Yes,0.2051,0.3916,0.6405,0.2766,8.7946,0.7437,0.1662,0.5766,0.2292,64.7785,0.6771 +deepface/tests/dataset/img1.jpg,deepface/tests/dataset/img5.jpg,Yes,0.2963,0.3948,0.7699,0.2696,8.4689,0.7343,0.0965,0.4393,0.2306,71.6647,0.679 +deepface/tests/dataset/img1.jpg,deepface/tests/dataset/img6.jpg,Yes,0.254,0.4464,0.7128,0.2164,7.7171,0.6579,0.0691,0.3718,0.2365,64.7594,0.6877 +deepface/tests/dataset/img1.jpg,deepface/tests/dataset/img7.jpg,Yes,0.3104,0.4764,0.7879,0.2112,7.5718,0.65,0.1027,0.4531,0.2385,61.371,0.6906 +deepface/tests/dataset/img1.jpg,deepface/tests/dataset/img10.jpg,Yes,0.3363,0.5448,0.8202,0.2129,7.6484,0.6525,0.0661,0.3635,0.2472,65.0668,0.7031 +deepface/tests/dataset/img1.jpg,deepface/tests/dataset/img11.jpg,Yes,0.3083,0.5416,0.7852,0.2042,7.6195,0.639,0.1626,0.5703,0.2001,61.3824,0.6326 +deepface/tests/dataset/img2.jpg,deepface/tests/dataset/img4.jpg,Yes,0.1397,0.3961,0.5285,0.1957,7.351,0.6256,0.2497,0.7066,0.1349,51.5853,0.5194 +deepface/tests/dataset/img2.jpg,deepface/tests/dataset/img5.jpg,Yes,0.1995,0.482,0.6317,0.1574,6.4195,0.561,0.1333,0.5164,0.1583,60.6365,0.5627 +deepface/tests/dataset/img2.jpg,deepface/tests/dataset/img6.jpg,Yes,0.0908,0.3251,0.4261,0.0787,4.625,0.3969,0.0632,0.3556,0.0756,38.218,0.3888 +deepface/tests/dataset/img2.jpg,deepface/tests/dataset/img7.jpg,Yes,0.2,0.4664,0.6325,0.1642,6.6261,0.5731,0.1049,0.4581,0.098,42.1113,0.4428 +deepface/tests/dataset/img2.jpg,deepface/tests/dataset/img10.jpg,Yes,0.2077,0.4862,0.6444,0.1593,6.5693,0.5644,0.0589,0.3431,0.1118,45.9168,0.4729 +deepface/tests/dataset/img2.jpg,deepface/tests/dataset/img11.jpg,Yes,0.2349,0.5235,0.6854,0.1869,7.2485,0.6114,0.1029,0.4536,0.1548,55.617,0.5564 +deepface/tests/dataset/img4.jpg,deepface/tests/dataset/img5.jpg,Yes,0.1991,0.3869,0.6311,0.1199,5.7256,0.4898,0.2891,0.7604,0.1797,64.7925,0.5995 +deepface/tests/dataset/img4.jpg,deepface/tests/dataset/img6.jpg,Yes,0.1937,0.4095,0.6224,0.1772,7.0495,0.5954,0.2199,0.6632,0.1788,59.9202,0.598 +deepface/tests/dataset/img4.jpg,deepface/tests/dataset/img7.jpg,Yes,0.245,0.4526,0.7,0.1663,6.7868,0.5767,0.3435,0.8289,0.1971,61.177,0.6279 +deepface/tests/dataset/img4.jpg,deepface/tests/dataset/img10.jpg,Yes,0.1882,0.4274,0.6136,0.1304,6.0445,0.5107,0.2052,0.6406,0.1239,49.4937,0.4979 +deepface/tests/dataset/img4.jpg,deepface/tests/dataset/img11.jpg,Yes,0.2569,0.5093,0.7168,0.1909,7.4277,0.618,0.2874,0.7582,0.1737,59.8839,0.5894 +deepface/tests/dataset/img5.jpg,deepface/tests/dataset/img6.jpg,Yes,0.1858,0.3915,0.6095,0.1818,6.967,0.6029,0.13,0.5099,0.1742,63.6179,0.5903 +deepface/tests/dataset/img5.jpg,deepface/tests/dataset/img7.jpg,Yes,0.2639,0.4391,0.7264,0.1754,6.7894,0.5923,0.1174,0.4846,0.1523,59.6056,0.5519 +deepface/tests/dataset/img5.jpg,deepface/tests/dataset/img10.jpg,Yes,0.2013,0.4449,0.6344,0.1143,5.525,0.478,0.1228,0.4957,0.1942,66.7805,0.6232 +deepface/tests/dataset/img5.jpg,deepface/tests/dataset/img11.jpg,Yes,0.3348,0.5599,0.8183,0.1975,7.4008,0.6285,0.2071,0.6436,0.1692,63.0817,0.5818 +deepface/tests/dataset/img6.jpg,deepface/tests/dataset/img7.jpg,Yes,0.192,0.4085,0.6196,0.1275,5.892,0.505,0.1004,0.4482,0.094,42.0465,0.4335 +deepface/tests/dataset/img6.jpg,deepface/tests/dataset/img10.jpg,Yes,0.214,0.4593,0.6542,0.1237,5.8374,0.4974,0.0517,0.3216,0.11,46.1197,0.4691 +deepface/tests/dataset/img6.jpg,deepface/tests/dataset/img11.jpg,Yes,0.2755,0.5319,0.7423,0.1772,7.1072,0.5953,0.1383,0.526,0.1771,59.9849,0.5951 +deepface/tests/dataset/img7.jpg,deepface/tests/dataset/img10.jpg,Yes,0.3425,0.5729,0.8276,0.1708,6.8133,0.5845,0.0956,0.4374,0.1552,52.8909,0.5571 +deepface/tests/dataset/img7.jpg,deepface/tests/dataset/img11.jpg,Yes,0.2912,0.5417,0.7632,0.2449,8.3025,0.6998,0.148,0.544,0.1894,60.469,0.6154 +deepface/tests/dataset/img10.jpg,deepface/tests/dataset/img11.jpg,Yes,0.2535,0.5258,0.712,0.1371,6.2509,0.5237,0.0609,0.349,0.1851,60.8244,0.6085 +deepface/tests/dataset/img18.jpg,deepface/tests/dataset/img19.jpg,Yes,0.1043,0.3254,0.4567,0.1248,6.2382,0.4996,0.2563,0.7159,0.1712,60.1675,0.5851 +deepface/tests/dataset/img18.jpg,deepface/tests/dataset/img67.jpg,Yes,0.2197,0.4691,0.6629,0.2387,8.7124,0.6909,0.3072,0.7838,0.1839,58.9528,0.6065 +deepface/tests/dataset/img19.jpg,deepface/tests/dataset/img67.jpg,Yes,0.1466,0.3965,0.5416,0.1321,6.5557,0.514,0.1504,0.5485,0.1517,55.8044,0.5508 +deepface/tests/dataset/img20.jpg,deepface/tests/dataset/img21.jpg,Yes,0.0641,0.2068,0.3581,0.1052,5.4253,0.4586,0.1118,0.4729,0.2209,58.7235,0.6646 +deepface/tests/dataset/img34.jpg,deepface/tests/dataset/img35.jpg,Yes,0.0959,0.2628,0.4381,0.2538,8.7003,0.7124,0.3727,0.8634,0.3244,78.4397,0.8055 +deepface/tests/dataset/img34.jpg,deepface/tests/dataset/img36.jpg,Yes,0.1553,0.2918,0.5573,0.1861,7.5793,0.6101,0.399,0.8933,0.2923,61.625,0.7646 +deepface/tests/dataset/img34.jpg,deepface/tests/dataset/img37.jpg,Yes,0.104,0.2651,0.4562,0.1192,6.0818,0.4882,0.4158,0.912,0.2853,62.1217,0.7554 +deepface/tests/dataset/img35.jpg,deepface/tests/dataset/img36.jpg,Yes,0.2322,0.3945,0.6814,0.2049,7.6366,0.6401,0.38,0.8717,0.2991,74.4219,0.7735 +deepface/tests/dataset/img35.jpg,deepface/tests/dataset/img37.jpg,Yes,0.1684,0.3516,0.5804,0.186,7.2991,0.6099,0.1662,0.5766,0.164,58.1125,0.5727 +deepface/tests/dataset/img36.jpg,deepface/tests/dataset/img37.jpg,Yes,0.1084,0.2715,0.4655,0.1338,6.3075,0.5173,0.2909,0.7627,0.2687,54.7311,0.7331 +deepface/tests/dataset/img22.jpg,deepface/tests/dataset/img23.jpg,Yes,0.3637,0.4569,0.8528,0.3501,9.9752,0.8368,0.1651,0.5746,0.1649,42.2178,0.5742 +deepface/tests/dataset/img13.jpg,deepface/tests/dataset/img14.jpg,Yes,0.086,0.3384,0.4148,0.1104,5.3711,0.47,0.0952,0.4363,0.2043,61.8532,0.6392 +deepface/tests/dataset/img13.jpg,deepface/tests/dataset/img15.jpg,Yes,0.1879,0.5589,0.6131,0.2317,7.9283,0.6808,0.3202,0.8003,0.3665,81.975,0.8562 +deepface/tests/dataset/img13.jpg,deepface/tests/dataset/img57.jpg,Yes,0.1204,0.3952,0.4907,0.1897,7.1445,0.616,0.4599,0.9591,0.3266,82.6217,0.8082 +deepface/tests/dataset/img13.jpg,deepface/tests/dataset/img58.jpg,Yes,0.1748,0.524,0.5913,0.2264,7.7484,0.6729,0.5006,1.0006,0.3476,75.6494,0.8338 +deepface/tests/dataset/img14.jpg,deepface/tests/dataset/img15.jpg,Yes,0.1969,0.571,0.6275,0.2322,7.8197,0.6815,0.3409,0.8257,0.4076,89.3521,0.9029 +deepface/tests/dataset/img14.jpg,deepface/tests/dataset/img57.jpg,Yes,0.1815,0.4206,0.6025,0.128,5.7838,0.5059,0.4251,0.9221,0.3284,84.7328,0.8105 +deepface/tests/dataset/img14.jpg,deepface/tests/dataset/img58.jpg,Yes,0.2071,0.5609,0.6436,0.2125,7.384,0.6519,0.4993,0.9993,0.3848,83.0627,0.8772 +deepface/tests/dataset/img15.jpg,deepface/tests/dataset/img57.jpg,Yes,0.198,0.5753,0.6293,0.2073,7.5025,0.6439,0.3957,0.8896,0.3881,91.551,0.881 +deepface/tests/dataset/img15.jpg,deepface/tests/dataset/img58.jpg,Yes,0.1109,0.4424,0.4709,0.1106,5.4445,0.4702,0.2815,0.7503,0.4153,85.5012,0.9114 +deepface/tests/dataset/img57.jpg,deepface/tests/dataset/img58.jpg,Yes,0.1581,0.5045,0.5624,0.1452,6.2094,0.5389,0.213,0.6528,0.2184,67.7741,0.6609 +deepface/tests/dataset/img29.jpg,deepface/tests/dataset/img30.jpg,Yes,0.142,0.28,0.5329,0.1759,7.1649,0.5931,0.3237,0.8046,0.272,59.7856,0.7375 +deepface/tests/dataset/img29.jpg,deepface/tests/dataset/img31.jpg,Yes,0.1525,0.2777,0.5523,0.1588,6.8613,0.5636,0.5027,1.0027,0.2,49.2171,0.6324 +deepface/tests/dataset/img29.jpg,deepface/tests/dataset/img32.jpg,Yes,0.1807,0.481,0.6011,0.1997,7.8571,0.632,0.4602,0.9594,0.3084,60.7837,0.7854 +deepface/tests/dataset/img29.jpg,deepface/tests/dataset/img33.jpg,Yes,0.1757,0.3177,0.5927,0.2406,8.3798,0.6937,0.3446,0.8302,0.1679,47.9061,0.5795 +deepface/tests/dataset/img30.jpg,deepface/tests/dataset/img31.jpg,Yes,0.1141,0.2453,0.4776,0.1654,6.8805,0.5751,0.3189,0.7986,0.1897,51.344,0.6159 +deepface/tests/dataset/img30.jpg,deepface/tests/dataset/img32.jpg,Yes,0.1567,0.4575,0.5597,0.1757,7.2731,0.5929,0.1712,0.5851,0.242,57.849,0.6957 +deepface/tests/dataset/img30.jpg,deepface/tests/dataset/img33.jpg,Yes,0.1548,0.2997,0.5565,0.2074,7.6356,0.644,0.1744,0.5906,0.2601,61.9643,0.7213 +deepface/tests/dataset/img31.jpg,deepface/tests/dataset/img32.jpg,Yes,0.1402,0.4725,0.5295,0.1009,5.5583,0.4493,0.2098,0.6478,0.2023,51.0814,0.6361 +deepface/tests/dataset/img31.jpg,deepface/tests/dataset/img33.jpg,Yes,0.0895,0.2296,0.4232,0.1873,7.3261,0.6121,0.1871,0.6118,0.229,56.6939,0.6768 +deepface/tests/dataset/img32.jpg,deepface/tests/dataset/img33.jpg,Yes,0.2035,0.4953,0.638,0.2415,8.5176,0.6949,0.2426,0.6965,0.2768,62.1742,0.744 +deepface/tests/dataset/img8.jpg,deepface/tests/dataset/img9.jpg,Yes,0.3147,0.45,0.7933,0.1976,7.3714,0.6287,0.0997,0.4466,0.1695,48.8942,0.5822 +deepface/tests/dataset/img8.jpg,deepface/tests/dataset/img47.jpg,Yes,0.3638,0.4564,0.853,0.1976,7.2952,0.6287,0.0931,0.4314,0.1869,54.8324,0.6114 +deepface/tests/dataset/img8.jpg,deepface/tests/dataset/img48.jpg,Yes,0.3068,0.442,0.7834,0.2593,8.2334,0.7201,0.1319,0.5136,0.2194,55.6994,0.6624 +deepface/tests/dataset/img8.jpg,deepface/tests/dataset/img49.jpg,Yes,0.2353,0.4246,0.686,0.1797,6.8592,0.5996,0.1472,0.5426,0.1904,57.1813,0.617 +deepface/tests/dataset/img8.jpg,deepface/tests/dataset/img50.jpg,Yes,0.3583,0.5144,0.8465,0.24,8.2435,0.6928,0.132,0.5138,0.138,40.4616,0.5253 +deepface/tests/dataset/img8.jpg,deepface/tests/dataset/img51.jpg,Yes,0.3446,0.4498,0.8301,0.1666,6.7177,0.5772,0.1413,0.5317,0.1656,46.6621,0.5756 +deepface/tests/dataset/img9.jpg,deepface/tests/dataset/img47.jpg,Yes,0.3153,0.4374,0.7941,0.1772,6.9625,0.5953,0.1591,0.5641,0.1795,54.801,0.5992 +deepface/tests/dataset/img9.jpg,deepface/tests/dataset/img48.jpg,Yes,0.3537,0.4845,0.8411,0.1723,6.7796,0.5871,0.1234,0.4969,0.1795,52.6507,0.5992 +deepface/tests/dataset/img9.jpg,deepface/tests/dataset/img49.jpg,Yes,0.2072,0.4029,0.6437,0.1954,7.2154,0.6251,0.1529,0.553,0.1311,48.2847,0.5121 +deepface/tests/dataset/img9.jpg,deepface/tests/dataset/img50.jpg,Yes,0.2662,0.4509,0.7296,0.2576,8.5935,0.7177,0.1531,0.5533,0.1205,41.6412,0.491 +deepface/tests/dataset/img9.jpg,deepface/tests/dataset/img51.jpg,Yes,0.3282,0.4507,0.8102,0.2371,8.0755,0.6887,0.1873,0.612,0.1817,51.7388,0.6029 +deepface/tests/dataset/img47.jpg,deepface/tests/dataset/img48.jpg,Yes,0.345,0.4542,0.8307,0.1613,6.4777,0.5679,0.1419,0.5328,0.1649,52.6864,0.5742 +deepface/tests/dataset/img47.jpg,deepface/tests/dataset/img49.jpg,Yes,0.257,0.4382,0.717,0.1944,7.1101,0.6236,0.1089,0.4667,0.2415,66.6307,0.695 +deepface/tests/dataset/img47.jpg,deepface/tests/dataset/img50.jpg,Yes,0.1844,0.3737,0.6073,0.215,7.7872,0.6558,0.1817,0.6029,0.2052,57.2133,0.6406 +deepface/tests/dataset/img47.jpg,deepface/tests/dataset/img51.jpg,Yes,0.1979,0.3274,0.6291,0.1303,5.926,0.5106,0.0939,0.4334,0.1209,44.911,0.4918 +deepface/tests/dataset/img48.jpg,deepface/tests/dataset/img49.jpg,Yes,0.2917,0.4744,0.7639,0.232,7.6321,0.6812,0.1067,0.462,0.2183,61.9241,0.6608 +deepface/tests/dataset/img48.jpg,deepface/tests/dataset/img50.jpg,Yes,0.3985,0.5478,0.8927,0.2745,8.6847,0.7409,0.2245,0.6701,0.2181,55.6337,0.6605 +deepface/tests/dataset/img48.jpg,deepface/tests/dataset/img51.jpg,Yes,0.3408,0.4563,0.8255,0.1586,6.4477,0.5633,0.1734,0.5888,0.2082,55.6445,0.6452 +deepface/tests/dataset/img49.jpg,deepface/tests/dataset/img50.jpg,Yes,0.2073,0.4183,0.6439,0.2437,8.1889,0.6982,0.1738,0.5896,0.1949,57.7545,0.6243 +deepface/tests/dataset/img49.jpg,deepface/tests/dataset/img51.jpg,Yes,0.2694,0.4491,0.7341,0.2076,7.3716,0.6444,0.1414,0.5318,0.2283,62.518,0.6758 +deepface/tests/dataset/img50.jpg,deepface/tests/dataset/img51.jpg,Yes,0.2505,0.4295,0.7079,0.2299,8.07,0.6781,0.1894,0.6155,0.1715,47.5665,0.5857 +deepface/tests/dataset/img16.jpg,deepface/tests/dataset/img17.jpg,Yes,0.2545,0.3759,0.7135,0.1493,6.5661,0.5465,0.2749,0.7414,0.1528,47.8128,0.5528 +deepface/tests/dataset/img16.jpg,deepface/tests/dataset/img59.jpg,Yes,0.1796,0.4352,0.5993,0.3095,9.6361,0.7868,0.4173,0.9136,0.247,61.4867,0.7028 +deepface/tests/dataset/img16.jpg,deepface/tests/dataset/img61.jpg,Yes,0.1779,0.3234,0.5965,0.1863,7.2985,0.6105,0.1407,0.5305,0.1643,53.2032,0.5732 +deepface/tests/dataset/img16.jpg,deepface/tests/dataset/img62.jpg,Yes,0.106,0.2509,0.4604,0.2243,8.1191,0.6698,0.3857,0.8783,0.1953,57.434,0.6249 +deepface/tests/dataset/img17.jpg,deepface/tests/dataset/img59.jpg,Yes,0.2519,0.5106,0.7099,0.2846,9.3099,0.7544,0.3877,0.8806,0.2994,62.5416,0.7739 +deepface/tests/dataset/img17.jpg,deepface/tests/dataset/img61.jpg,Yes,0.2507,0.3495,0.708,0.1992,7.6132,0.6313,0.1867,0.6111,0.2101,58.2095,0.6482 +deepface/tests/dataset/img17.jpg,deepface/tests/dataset/img62.jpg,Yes,0.2533,0.3415,0.7118,0.2672,8.9292,0.731,0.3356,0.8193,0.252,62.3621,0.7099 +deepface/tests/dataset/img59.jpg,deepface/tests/dataset/img61.jpg,Yes,0.192,0.4543,0.6196,0.4417,11.5466,0.9399,0.3558,0.8435,0.1808,54.8373,0.6014 +deepface/tests/dataset/img59.jpg,deepface/tests/dataset/img62.jpg,Yes,0.1123,0.3893,0.4738,0.2974,9.5874,0.7713,0.5393,1.0386,0.1934,55.9836,0.6219 +deepface/tests/dataset/img61.jpg,deepface/tests/dataset/img62.jpg,Yes,0.1251,0.253,0.5002,0.2245,8.1525,0.6701,0.4072,0.9024,0.1757,55.867,0.5928 +deepface/tests/dataset/img26.jpg,deepface/tests/dataset/img27.jpg,Yes,0.3059,0.5758,0.7822,0.3444,9.7537,0.8299,0.1815,0.6026,0.2396,69.4496,0.6922 +deepface/tests/dataset/img26.jpg,deepface/tests/dataset/img28.jpg,Yes,0.343,0.5503,0.8282,0.3556,10.2896,0.8433,0.1662,0.5766,0.205,60.0105,0.6403 +deepface/tests/dataset/img26.jpg,deepface/tests/dataset/img42.jpg,Yes,0.3852,0.542,0.8778,0.3278,9.7855,0.8097,0.2831,0.7524,0.2523,66.2702,0.7104 +deepface/tests/dataset/img26.jpg,deepface/tests/dataset/img43.jpg,Yes,0.3254,0.5271,0.8067,0.2825,8.887,0.7517,0.2876,0.7585,0.3443,79.1342,0.8299 +deepface/tests/dataset/img26.jpg,deepface/tests/dataset/img44.jpg,Yes,0.3645,0.5029,0.8539,0.2248,7.9975,0.6706,0.2646,0.7274,0.2572,68.2216,0.7173 +deepface/tests/dataset/img26.jpg,deepface/tests/dataset/img45.jpg,Yes,0.283,0.4775,0.7523,0.2537,8.5109,0.7124,0.3277,0.8096,0.2726,70.5843,0.7384 +deepface/tests/dataset/img26.jpg,deepface/tests/dataset/img46.jpg,Yes,0.447,0.5967,0.9456,0.4372,11.0907,0.9351,0.3544,0.8419,0.3079,73.7249,0.7848 +deepface/tests/dataset/img27.jpg,deepface/tests/dataset/img28.jpg,Yes,0.2847,0.5707,0.7546,0.2178,7.8688,0.6601,0.1205,0.491,0.232,66.1474,0.6811 +deepface/tests/dataset/img27.jpg,deepface/tests/dataset/img42.jpg,Yes,0.328,0.5946,0.8099,0.2829,8.8485,0.7523,0.3721,0.8627,0.2376,66.8304,0.6893 +deepface/tests/dataset/img27.jpg,deepface/tests/dataset/img43.jpg,Yes,0.3781,0.65,0.8696,0.2827,8.6093,0.7519,0.2004,0.633,0.2924,75.1537,0.7647 +deepface/tests/dataset/img27.jpg,deepface/tests/dataset/img44.jpg,Yes,0.3385,0.5968,0.8229,0.2597,8.3408,0.7207,0.2941,0.7669,0.2314,66.8603,0.6803 +deepface/tests/dataset/img27.jpg,deepface/tests/dataset/img45.jpg,Yes,0.2302,0.5087,0.6785,0.147,6.2958,0.5422,0.2088,0.6463,0.2035,63.0117,0.6379 +deepface/tests/dataset/img27.jpg,deepface/tests/dataset/img46.jpg,Yes,0.3461,0.6141,0.832,0.388,10.1318,0.881,0.264,0.7266,0.2241,65.3424,0.6694 +deepface/tests/dataset/img28.jpg,deepface/tests/dataset/img42.jpg,Yes,0.2442,0.4668,0.6988,0.1991,7.7026,0.631,0.2848,0.7547,0.2583,62.2885,0.7187 +deepface/tests/dataset/img28.jpg,deepface/tests/dataset/img43.jpg,Yes,0.2159,0.4542,0.657,0.2239,8.0122,0.6692,0.2194,0.6624,0.2833,67.7766,0.7527 +deepface/tests/dataset/img28.jpg,deepface/tests/dataset/img44.jpg,Yes,0.2802,0.4883,0.7486,0.1697,7.0317,0.5826,0.2753,0.742,0.2378,61.8227,0.6897 +deepface/tests/dataset/img28.jpg,deepface/tests/dataset/img45.jpg,Yes,0.3044,0.5286,0.7803,0.1768,7.1867,0.5946,0.267,0.7307,0.2683,66.1764,0.7326 +deepface/tests/dataset/img28.jpg,deepface/tests/dataset/img46.jpg,Yes,0.426,0.6222,0.923,0.3338,9.8004,0.817,0.2481,0.7044,0.3072,68.9752,0.7838 +deepface/tests/dataset/img42.jpg,deepface/tests/dataset/img43.jpg,Yes,0.2018,0.4174,0.6353,0.2418,8.227,0.6954,0.1678,0.5794,0.1483,49.1175,0.5446 +deepface/tests/dataset/img42.jpg,deepface/tests/dataset/img44.jpg,Yes,0.1685,0.3458,0.5805,0.119,5.8252,0.4879,0.2432,0.6975,0.0957,39.352,0.4375 +deepface/tests/dataset/img42.jpg,deepface/tests/dataset/img45.jpg,Yes,0.2004,0.4027,0.6331,0.1378,6.2772,0.5251,0.1982,0.6296,0.1742,53.3531,0.5903 +deepface/tests/dataset/img42.jpg,deepface/tests/dataset/img46.jpg,Yes,0.2253,0.4245,0.6713,0.1946,7.4093,0.6239,0.1761,0.5934,0.1568,49.1856,0.5601 +deepface/tests/dataset/img43.jpg,deepface/tests/dataset/img44.jpg,Yes,0.2049,0.4137,0.6402,0.2238,7.7899,0.6691,0.1748,0.5912,0.1553,51.4113,0.5573 +deepface/tests/dataset/img43.jpg,deepface/tests/dataset/img45.jpg,Yes,0.2298,0.4524,0.6779,0.2281,7.8811,0.6754,0.0531,0.3257,0.1801,55.7173,0.6001 +deepface/tests/dataset/img43.jpg,deepface/tests/dataset/img46.jpg,Yes,0.3731,0.5738,0.8638,0.3741,10.0121,0.865,0.1394,0.5281,0.2184,60.1165,0.6609 +deepface/tests/dataset/img44.jpg,deepface/tests/dataset/img45.jpg,Yes,0.1743,0.3671,0.5903,0.1052,5.4022,0.4587,0.1636,0.572,0.1275,46.7067,0.505 +deepface/tests/dataset/img44.jpg,deepface/tests/dataset/img46.jpg,Yes,0.2682,0.4468,0.7324,0.2225,7.7975,0.667,0.1984,0.6299,0.1569,50.7309,0.5602 +deepface/tests/dataset/img45.jpg,deepface/tests/dataset/img46.jpg,Yes,0.2818,0.486,0.7507,0.2239,7.8397,0.6692,0.1379,0.5252,0.193,56.6925,0.6213 +deepface/tests/dataset/img24.jpg,deepface/tests/dataset/img25.jpg,Yes,0.1197,0.2833,0.4893,0.1419,6.4307,0.5327,0.1666,0.5773,0.2083,60.7717,0.6454 +deepface/tests/dataset/img21.jpg,deepface/tests/dataset/img17.jpg,No,0.4907,0.531,0.9907,0.6285,13.4397,1.1212,0.807,1.2704,0.3363,67.5896,0.8201 +deepface/tests/dataset/img23.jpg,deepface/tests/dataset/img47.jpg,No,0.5671,0.563,1.065,0.6961,13.8325,1.1799,0.1334,0.5166,0.2008,56.6182,0.6337 +deepface/tests/dataset/img16.jpg,deepface/tests/dataset/img24.jpg,No,0.6046,0.5757,1.0997,0.9105,16.3487,1.3494,0.2078,0.6447,0.2218,57.4046,0.666 +deepface/tests/dataset/img50.jpg,deepface/tests/dataset/img16.jpg,No,0.7308,0.7317,1.2089,1.0868,17.7134,1.4743,0.3578,0.846,0.2254,57.4293,0.6715 +deepface/tests/dataset/img41.jpg,deepface/tests/dataset/img18.jpg,No,0.4197,0.569,0.9162,0.8173,13.1177,1.2786,0.6457,1.1364,0.3401,75.8425,0.8247 +deepface/tests/dataset/img12.jpg,deepface/tests/dataset/img32.jpg,No,0.7555,0.9708,1.2293,1.0896,18.6004,1.4762,0.4448,0.9432,0.2547,60.7653,0.7138 +deepface/tests/dataset/img51.jpg,deepface/tests/dataset/img26.jpg,No,0.506,0.5807,1.006,0.7329,14.3648,1.2107,0.2928,0.7652,0.2226,61.9764,0.6672 +deepface/tests/dataset/img13.jpg,deepface/tests/dataset/img27.jpg,No,0.688,0.9511,1.1731,0.9559,15.8763,1.3827,0.3366,0.8205,0.2086,63.7428,0.6459 +deepface/tests/dataset/img35.jpg,deepface/tests/dataset/img33.jpg,No,0.2131,0.3838,0.6528,0.5762,12.621,1.0735,0.3323,0.8153,0.2895,74.4074,0.7609 +deepface/tests/dataset/img34.jpg,deepface/tests/dataset/img44.jpg,No,0.7964,0.6879,1.262,0.9531,16.8504,1.3806,0.4968,0.9968,0.2565,63.8992,0.7162 +deepface/tests/dataset/img8.jpg,deepface/tests/dataset/img61.jpg,No,0.8548,0.6996,1.3075,0.9485,16.2825,1.3773,0.6479,1.1383,0.259,64.0582,0.7198 +deepface/tests/dataset/img53.jpg,deepface/tests/dataset/img4.jpg,No,0.5862,0.6454,1.0828,0.8624,16.0416,1.3133,0.3185,0.7982,0.2397,65.712,0.6924 +deepface/tests/dataset/img54.jpg,deepface/tests/dataset/img2.jpg,No,0.6948,0.9246,1.1788,0.9568,16.4217,1.3833,0.3481,0.8344,0.2497,64.7938,0.7067 +deepface/tests/dataset/img43.jpg,deepface/tests/dataset/img24.jpg,No,0.7757,0.7407,1.2456,1.0007,16.8769,1.4147,0.4194,0.9159,0.3961,77.6798,0.8901 +deepface/tests/dataset/img6.jpg,deepface/tests/dataset/img20.jpg,No,0.6784,0.7154,1.1648,0.9864,16.5342,1.4045,0.2043,0.6392,0.2499,67.3658,0.707 +deepface/tests/dataset/img40.jpg,deepface/tests/dataset/img20.jpg,No,0.474,0.4904,0.9736,0.7949,14.8341,1.2609,0.4776,0.9773,0.2192,56.6904,0.6621 +deepface/tests/dataset/img29.jpg,deepface/tests/dataset/img49.jpg,No,0.725,0.7156,1.2041,1.2676,18.7008,1.5922,0.3254,0.8068,0.1968,58.1537,0.6274 +deepface/tests/dataset/img38.jpg,deepface/tests/dataset/img29.jpg,No,0.5496,0.5428,1.0484,1.1766,18.8394,1.534,0.2956,0.769,0.323,68.2188,0.8037 +deepface/tests/dataset/img7.jpg,deepface/tests/dataset/img20.jpg,No,0.7791,0.7506,1.2482,0.945,16.0728,1.3748,0.2922,0.7645,0.2063,58.285,0.6424 +deepface/tests/dataset/img54.jpg,deepface/tests/dataset/img10.jpg,No,0.6852,0.8904,1.1707,0.9223,16.2459,1.3582,0.3508,0.8377,0.2699,67.3228,0.7347 +deepface/tests/dataset/img17.jpg,deepface/tests/dataset/img43.jpg,No,0.7785,0.7344,1.2478,0.8234,15.1735,1.2833,0.8461,1.3009,0.3715,74.2351,0.862 +deepface/tests/dataset/img56.jpg,deepface/tests/dataset/img47.jpg,No,0.5798,0.6885,1.0769,0.9515,16.1507,1.3795,0.2527,0.7109,0.1453,51.4537,0.5391 +deepface/tests/dataset/img10.jpg,deepface/tests/dataset/img15.jpg,No,0.7144,1.0202,1.1953,1.1267,17.5833,1.5012,0.7384,1.2152,0.404,87.858,0.8989 +deepface/tests/dataset/img21.jpg,deepface/tests/dataset/img61.jpg,No,0.5642,0.5883,1.0623,0.7305,14.4227,1.2088,0.5523,1.051,0.3206,73.1845,0.8008 +deepface/tests/dataset/img34.jpg,deepface/tests/dataset/img47.jpg,No,0.6442,0.5952,1.1351,1.0884,17.8754,1.4754,0.6225,1.1158,0.2549,64.7586,0.714 +deepface/tests/dataset/img11.jpg,deepface/tests/dataset/img51.jpg,No,0.5459,0.6938,1.0448,0.7452,14.4984,1.2208,0.1807,0.6012,0.179,58.3078,0.5983 +deepface/tests/dataset/img4.jpg,deepface/tests/dataset/img14.jpg,No,0.7235,0.8162,1.2029,1.0599,16.8526,1.4559,0.4242,0.9211,0.26,72.3704,0.7211 +deepface/tests/dataset/img38.jpg,deepface/tests/dataset/img14.jpg,No,0.5044,0.637,1.0044,0.9856,16.5161,1.404,0.2733,0.7393,0.354,80.6472,0.8415 +deepface/tests/dataset/img19.jpg,deepface/tests/dataset/img47.jpg,No,0.5752,0.6917,1.0726,1.0042,17.1669,1.4172,0.354,0.8414,0.1709,59.1711,0.5846 +deepface/tests/dataset/img55.jpg,deepface/tests/dataset/img14.jpg,No,0.6473,0.7275,1.1378,0.9052,15.7543,1.3455,0.2127,0.6523,0.2293,67.2542,0.6771 +deepface/tests/dataset/img20.jpg,deepface/tests/dataset/img33.jpg,No,0.4886,0.541,0.9885,0.9202,16.051,1.3566,0.6114,1.1058,0.253,62.6318,0.7113 +deepface/tests/dataset/img39.jpg,deepface/tests/dataset/img62.jpg,No,0.4634,0.5606,0.9627,0.8783,16.0858,1.3254,0.7776,1.2471,0.329,70.4788,0.8112 +deepface/tests/dataset/img54.jpg,deepface/tests/dataset/img58.jpg,No,0.6048,0.9477,1.0998,0.8084,15.0301,1.2716,0.6403,1.1316,0.3272,69.1393,0.809 +deepface/tests/dataset/img11.jpg,deepface/tests/dataset/img9.jpg,No,0.6643,0.7784,1.1527,0.899,16.0335,1.3409,0.2452,0.7002,0.1639,56.0631,0.5725 +deepface/tests/dataset/img6.jpg,deepface/tests/dataset/img46.jpg,No,0.5766,0.7054,1.0738,0.9264,15.9036,1.3611,0.1341,0.5179,0.2298,64.5324,0.6779 +deepface/tests/dataset/img7.jpg,deepface/tests/dataset/img59.jpg,No,0.7679,0.8729,1.2393,1.0242,17.2778,1.4312,0.7789,1.2481,0.3103,69.694,0.7878 +deepface/tests/dataset/img7.jpg,deepface/tests/dataset/img35.jpg,No,0.8227,0.8096,1.2827,1.0357,16.7157,1.4392,0.4864,0.9863,0.2401,68.9468,0.693 +deepface/tests/dataset/img5.jpg,deepface/tests/dataset/img19.jpg,No,0.7052,0.752,1.1876,0.9084,16.1781,1.3479,0.2462,0.7016,0.1449,58.8831,0.5384 +deepface/tests/dataset/img55.jpg,deepface/tests/dataset/img8.jpg,No,0.4891,0.5451,0.989,0.7908,14.9832,1.2576,0.2408,0.6939,0.2341,63.666,0.6843 +deepface/tests/dataset/img22.jpg,deepface/tests/dataset/img51.jpg,No,0.5201,0.5378,1.0199,0.6262,13.2133,1.1191,0.1456,0.5397,0.2985,60.8239,0.7726 +deepface/tests/dataset/img4.jpg,deepface/tests/dataset/img15.jpg,No,0.7147,0.9872,1.1956,1.0641,17.2349,1.4588,0.6229,1.1162,0.4049,89.7221,0.8998 +deepface/tests/dataset/img19.jpg,deepface/tests/dataset/img29.jpg,No,0.3605,0.5646,0.8492,0.6901,14.6314,1.1748,0.1803,0.6005,0.2709,71.9655,0.7361 +deepface/tests/dataset/img20.jpg,deepface/tests/dataset/img28.jpg,No,0.5807,0.6843,1.0777,0.8133,15.3844,1.2754,0.1274,0.5048,0.1841,53.6094,0.6067 +deepface/tests/dataset/img55.jpg,deepface/tests/dataset/img13.jpg,No,0.6366,0.8086,1.1283,0.8832,15.8044,1.3291,0.3343,0.8177,0.177,57.373,0.5949 +deepface/tests/dataset/img34.jpg,deepface/tests/dataset/img22.jpg,No,0.7842,0.6655,1.2523,1.137,18.5595,1.508,0.4797,0.9795,0.2457,56.695,0.7011 +deepface/tests/dataset/img67.jpg,deepface/tests/dataset/img58.jpg,No,0.5051,0.8463,1.0051,0.8713,16.0723,1.3201,0.5281,1.0277,0.276,67.6933,0.743 +deepface/tests/dataset/img18.jpg,deepface/tests/dataset/img9.jpg,No,0.7493,0.7683,1.2242,1.0774,17.7057,1.4679,0.5343,1.0337,0.2113,62.0197,0.65 +deepface/tests/dataset/img11.jpg,deepface/tests/dataset/img58.jpg,No,0.7495,1.0309,1.2243,1.0359,16.9461,1.4394,0.6411,1.1324,0.2259,65.3131,0.6721 +deepface/tests/dataset/img18.jpg,deepface/tests/dataset/img42.jpg,No,0.8335,0.8332,1.2911,1.0838,17.9617,1.4723,0.4051,0.9001,0.2449,66.4075,0.6999 +deepface/tests/dataset/img18.jpg,deepface/tests/dataset/img13.jpg,No,0.476,0.7428,0.9757,1.1589,18.2018,1.5224,0.306,0.7823,0.1879,59.4531,0.6129 +deepface/tests/dataset/img4.jpg,deepface/tests/dataset/img32.jpg,No,0.7116,0.8739,1.193,1.0402,17.6777,1.4424,0.6456,1.1363,0.2896,71.6141,0.761 +deepface/tests/dataset/img67.jpg,deepface/tests/dataset/img37.jpg,No,0.4644,0.652,0.9638,0.6683,14.5099,1.1561,0.2355,0.6862,0.2475,61.9234,0.7036 +deepface/tests/dataset/img41.jpg,deepface/tests/dataset/img7.jpg,No,0.8444,0.7812,1.2666,0.9357,16.3278,1.368,0.4702,1.459,0.4919,67.9214,0.7892 +deepface/tests/dataset/img11.jpg,deepface/tests/dataset/img27.jpg,No,0.6496,0.8811,1.1398,0.9364,16.0727,1.3685,0.2416,0.6951,0.2127,66.7336,0.6523 +deepface/tests/dataset/img20.jpg,deepface/tests/dataset/img47.jpg,No,0.6418,0.6011,1.1329,1.0579,16.9991,1.4546,0.31,0.7874,0.1754,54.6104,0.5924 +deepface/tests/dataset/img2.jpg,deepface/tests/dataset/img44.jpg,No,0.4815,0.6806,0.9814,0.7396,14.1679,1.2162,0.2009,0.6338,0.1836,57.4368,0.606 +deepface/tests/dataset/img28.jpg,deepface/tests/dataset/img24.jpg,No,0.7851,0.7588,1.2531,0.9406,16.8964,1.3715,0.5353,1.0347,0.2609,60.6589,0.7224 +deepface/tests/dataset/img67.jpg,deepface/tests/dataset/img43.jpg,No,0.691,0.8328,1.1756,0.9621,16.9417,1.3872,0.3176,0.797,0.3072,72.9213,0.7838 +deepface/tests/dataset/img18.jpg,deepface/tests/dataset/img51.jpg,No,0.668,0.7024,1.1558,1.1051,17.8105,1.4867,0.2508,0.7083,0.1882,58.3932,0.6135 +deepface/tests/dataset/img11.jpg,deepface/tests/dataset/img24.jpg,No,0.79,0.801,1.257,1.1173,18.2579,1.4949,0.3437,0.829,0.3096,74.5014,0.7869 +deepface/tests/dataset/img67.jpg,deepface/tests/dataset/img29.jpg,No,0.5389,0.6762,1.0382,0.8354,16.2507,1.2926,0.1501,0.5479,0.2668,63.7773,0.7305 +deepface/tests/dataset/img29.jpg,deepface/tests/dataset/img59.jpg,No,0.4237,0.6225,0.9205,0.5002,12.4131,1.0002,0.6375,1.1292,0.2637,58.2849,0.7262 +deepface/tests/dataset/img41.jpg,deepface/tests/dataset/img24.jpg,No,0.5431,0.5391,1.0422,1.1194,18.4041,1.4962,0.8286,1.2873,0.4458,74.1332,0.9442 +deepface/tests/dataset/img35.jpg,deepface/tests/dataset/img27.jpg,No,0.821,0.9129,1.2814,0.964,15.9831,1.3885,0.4812,0.9811,0.3061,80.9221,0.7824 +deepface/tests/dataset/img39.jpg,deepface/tests/dataset/img67.jpg,No,0.5513,0.7255,1.0501,0.9839,17.4219,1.4028,0.8181,1.2792,0.2914,66.5717,0.7634 +deepface/tests/dataset/img39.jpg,deepface/tests/dataset/img12.jpg,No,0.6435,0.8102,1.1344,0.7661,15.2245,1.2378,0.7472,1.2224,0.2716,61.7006,0.737 +deepface/tests/dataset/img41.jpg,deepface/tests/dataset/img46.jpg,No,0.8116,0.7634,1.2028,1.1264,17.9427,1.5009,0.9219,1.3578,0.3511,70.3501,0.838 +deepface/tests/dataset/img32.jpg,deepface/tests/dataset/img27.jpg,No,0.7197,0.9593,1.1997,0.7295,14.4944,1.2079,0.5619,1.0601,0.2725,70.5338,0.7382 +deepface/tests/dataset/img40.jpg,deepface/tests/dataset/img11.jpg,No,0.7205,0.7563,1.2004,0.9367,16.3131,1.3687,0.5427,1.0418,0.186,59.4748,0.61 +deepface/tests/dataset/img41.jpg,deepface/tests/dataset/img22.jpg,No,0.5579,0.6466,1.2024,1.0076,17.2122,1.4196,0.7998,1.2648,0.392,65.4579,0.8854 +deepface/tests/dataset/img2.jpg,deepface/tests/dataset/img35.jpg,No,0.8303,0.9037,1.2887,1.0988,17.1897,1.4824,0.498,0.998,0.2992,78.1653,0.7736 +deepface/tests/dataset/img5.jpg,deepface/tests/dataset/img45.jpg,No,0.5247,0.6013,1.0244,0.8827,15.3713,1.3287,0.218,0.6603,0.2322,72.2019,0.6814 +deepface/tests/dataset/img58.jpg,deepface/tests/dataset/img59.jpg,No,0.5937,0.9226,1.0896,0.9931,16.9142,1.4093,0.3525,0.8396,0.3095,68.0277,0.7868 +deepface/tests/dataset/img40.jpg,deepface/tests/dataset/img45.jpg,No,0.772,0.6976,1.2426,1.0516,17.0626,1.4503,0.5487,1.0475,0.2628,63.7285,0.725 +deepface/tests/dataset/img41.jpg,deepface/tests/dataset/img3.jpg,No,0.6417,0.6822,1.1329,0.832,15.8921,1.29,1.0374,1.4404,0.2312,54.5718,0.68 +deepface/tests/dataset/img40.jpg,deepface/tests/dataset/img67.jpg,No,0.4138,0.5942,0.9098,0.948,16.9509,1.3769,0.5121,1.012,0.2455,61.9071,0.7008 +deepface/tests/dataset/img4.jpg,deepface/tests/dataset/img50.jpg,No,0.5776,0.6934,1.0748,0.816,15.3649,1.2775,0.3515,0.8385,0.2072,61.657,0.6437 +deepface/tests/dataset/img67.jpg,deepface/tests/dataset/img47.jpg,No,0.5726,0.692,1.0701,0.9987,17.2907,1.4133,0.4099,0.9054,0.1723,55.0701,0.587 +deepface/tests/dataset/img55.jpg,deepface/tests/dataset/img20.jpg,No,0.684,0.6408,1.1696,0.924,16.3035,1.3594,0.2156,0.6566,0.2111,61.919,0.6498 +deepface/tests/dataset/img13.jpg,deepface/tests/dataset/img33.jpg,No,0.4625,0.7042,0.9617,0.8709,15.4791,1.3198,0.5609,1.0591,0.3643,76.6864,0.8536 +deepface/tests/dataset/img41.jpg,deepface/tests/dataset/img58.jpg,No,0.5732,0.8464,1.0707,0.7511,16.6216,1.4011,0.5091,1.009,0.3653,71.3439,0.8548 +deepface/tests/dataset/img19.jpg,deepface/tests/dataset/img48.jpg,No,0.8186,0.8431,1.2795,1.1082,17.769,1.4888,0.3914,0.8848,0.2363,68.307,0.6875 +deepface/tests/dataset/img18.jpg,deepface/tests/dataset/img49.jpg,No,0.6614,0.7617,1.1501,0.9935,16.5922,1.4096,0.427,0.9241,0.28,73.8384,0.7483 +deepface/tests/dataset/img10.jpg,deepface/tests/dataset/img19.jpg,No,0.603,0.7998,1.0982,0.9508,16.8085,1.379,0.3546,0.8422,0.2352,69.7597,0.6859 +deepface/tests/dataset/img48.jpg,deepface/tests/dataset/img17.jpg,No,0.8174,0.6679,1.2786,0.922,15.8462,1.3579,0.7438,1.2196,0.2545,59.7077,0.7134 +deepface/tests/dataset/img55.jpg,deepface/tests/dataset/img2.jpg,No,0.6454,0.7751,1.1362,1.0674,17.3381,1.4611,0.1279,0.5058,0.1983,61.7554,0.6298 +deepface/tests/dataset/img41.jpg,deepface/tests/dataset/img48.jpg,No,0.7325,0.7072,1.2605,0.8198,15.0575,1.2805,0.9352,1.3676,0.3504,69.8577,0.8371 +deepface/tests/dataset/img30.jpg,deepface/tests/dataset/img44.jpg,No,0.8834,0.7196,1.3292,0.8683,15.5513,1.3178,0.563,1.0611,0.363,75.7833,0.8521 +deepface/tests/dataset/img6.jpg,deepface/tests/dataset/img29.jpg,No,0.7666,0.7464,1.2382,1.0057,17.0345,1.4183,0.3434,0.8287,0.2411,64.6435,0.6943 +deepface/tests/dataset/img19.jpg,deepface/tests/dataset/img26.jpg,No,0.6542,0.7763,1.1439,0.9204,16.7702,1.3568,0.2292,0.677,0.262,73.7273,0.7239 +deepface/tests/dataset/img29.jpg,deepface/tests/dataset/img50.jpg,No,0.6879,0.692,1.1729,1.3134,19.7708,1.6207,0.5038,1.0038,0.2577,54.3931,0.7179 +deepface/tests/dataset/img35.jpg,deepface/tests/dataset/img49.jpg,No,0.8339,0.8186,1.2915,1.2099,17.7753,1.5555,0.5957,1.0915,0.3315,82.3474,0.8142 +deepface/tests/dataset/img22.jpg,deepface/tests/dataset/img28.jpg,No,0.6313,0.7037,1.1236,0.8177,15.5314,1.2789,0.2031,0.6373,0.2271,55.2529,0.6739 +deepface/tests/dataset/img21.jpg,deepface/tests/dataset/img16.jpg,No,0.5678,0.6114,1.0657,0.6376,13.417,1.1293,0.4173,0.9136,0.2696,65.0241,0.7343 +deepface/tests/dataset/img21.jpg,deepface/tests/dataset/img9.jpg,No,0.7653,0.7211,1.2372,1.0502,17.1485,1.4493,0.5726,1.0701,0.3059,68.2225,0.7822 +deepface/tests/dataset/img2.jpg,deepface/tests/dataset/img22.jpg,No,0.6866,0.7895,1.1718,1.0005,16.6324,1.4145,0.1955,0.6253,0.3061,69.9331,0.7824 +deepface/tests/dataset/img12.jpg,deepface/tests/dataset/img29.jpg,No,0.78,0.8337,1.249,1.1016,18.4797,1.4843,0.3404,0.8251,0.3293,67.3331,0.8115 +deepface/tests/dataset/img6.jpg,deepface/tests/dataset/img37.jpg,No,0.7532,0.7788,1.2273,1.0976,17.7567,1.4816,0.2647,0.7275,0.331,74.5559,0.8137 +deepface/tests/dataset/img6.jpg,deepface/tests/dataset/img16.jpg,No,0.7516,0.7581,1.226,1.0332,16.9971,1.4375,0.3815,0.8735,0.2859,72.0572,0.7561 +deepface/tests/dataset/img38.jpg,deepface/tests/dataset/img33.jpg,No,0.4588,0.5085,0.958,1.2465,19.0695,1.5789,0.657,1.1463,0.3722,76.6896,0.8628 +deepface/tests/dataset/img35.jpg,deepface/tests/dataset/img32.jpg,No,0.2651,0.5459,0.7282,0.5427,12.6429,1.0418,0.409,0.9045,0.2546,69.5802,0.7136 +deepface/tests/dataset/img54.jpg,deepface/tests/dataset/img48.jpg,No,0.4528,0.678,0.9516,0.8385,15.166,1.295,0.2238,0.669,0.218,56.5099,0.6603 +deepface/tests/dataset/img1.jpg,deepface/tests/dataset/img23.jpg,No,0.5305,0.5523,1.03,0.7766,14.6983,1.2463,0.1967,0.6272,0.2144,53.347,0.6549 +deepface/tests/dataset/img39.jpg,deepface/tests/dataset/img33.jpg,No,0.5132,0.6067,1.0131,1.1197,17.8246,1.4965,0.2379,0.6898,0.2301,55.7862,0.6783 +deepface/tests/dataset/img3.jpg,deepface/tests/dataset/img48.jpg,No,0.4123,0.5581,0.908,0.7879,14.8183,1.2553,0.2125,0.6519,0.2177,56.6639,0.6598 +deepface/tests/dataset/img43.jpg,deepface/tests/dataset/img25.jpg,No,0.7819,0.7991,1.2505,0.9007,15.601,1.3422,0.4363,0.9341,0.3555,81.219,0.8432 +deepface/tests/dataset/img14.jpg,deepface/tests/dataset/img9.jpg,No,0.7257,0.7829,1.2047,0.8679,15.1696,1.3175,0.5752,1.0725,0.2493,67.0315,0.7061 +deepface/tests/dataset/img6.jpg,deepface/tests/dataset/img47.jpg,No,0.5391,0.6276,1.0383,0.7885,14.6406,1.2558,0.1013,0.4501,0.1756,57.5202,0.5926 +deepface/tests/dataset/img18.jpg,deepface/tests/dataset/img28.jpg,No,0.8293,0.8828,1.2878,1.1151,18.3899,1.4934,0.497,0.997,0.2323,64.8263,0.6816 +deepface/tests/dataset/img7.jpg,deepface/tests/dataset/img57.jpg,No,0.7468,0.815,1.2221,1.1241,17.3821,1.4994,0.6916,1.1761,0.2244,68.912,0.6699 +deepface/tests/dataset/img48.jpg,deepface/tests/dataset/img26.jpg,No,0.5877,0.646,1.0842,0.9734,16.2582,1.3953,0.3102,0.7876,0.2059,60.3497,0.6417 +deepface/tests/dataset/img19.jpg,deepface/tests/dataset/img34.jpg,No,0.2957,0.5193,0.7691,0.5281,12.9854,1.0277,0.5987,1.0943,0.2628,71.5029,0.725 +deepface/tests/dataset/img41.jpg,deepface/tests/dataset/img37.jpg,No,0.4337,0.5351,0.9314,0.8568,16.0356,1.309,0.684,1.1696,0.3654,65.8114,0.8548 +deepface/tests/dataset/img1.jpg,deepface/tests/dataset/img32.jpg,No,0.6985,0.8184,1.182,0.9682,16.9113,1.3915,0.5654,1.0634,0.3173,65.953,0.7967 +deepface/tests/dataset/img12.jpg,deepface/tests/dataset/img57.jpg,No,0.6424,0.8305,1.1335,0.8361,15.6851,1.2931,0.5927,1.0888,0.2943,77.8234,0.7672 +deepface/tests/dataset/img55.jpg,deepface/tests/dataset/img5.jpg,No,0.662,0.6012,1.1507,0.9931,16.5792,1.4093,0.137,0.5234,0.2182,70.8567,0.6606 +deepface/tests/dataset/img47.jpg,deepface/tests/dataset/img61.jpg,No,0.6896,0.603,1.1744,0.98,16.5069,1.4,0.5598,1.0581,0.187,57.8252,0.6115 +deepface/tests/dataset/img33.jpg,deepface/tests/dataset/img49.jpg,No,0.8253,0.7753,1.2848,1.0329,16.5833,1.4373,0.6695,1.1572,0.1992,58.9069,0.6313 +deepface/tests/dataset/img54.jpg,deepface/tests/dataset/img1.jpg,No,0.5922,0.7522,1.0883,0.9398,16.3902,1.371,0.2515,0.7092,0.2836,62.9648,0.7532 +deepface/tests/dataset/img29.jpg,deepface/tests/dataset/img25.jpg,No,0.5458,0.5846,1.0448,0.9074,16.167,1.3472,0.622,1.1153,0.2743,68.4542,0.7407 +deepface/tests/dataset/img55.jpg,deepface/tests/dataset/img67.jpg,No,0.6649,0.7541,1.1531,1.1444,18.95,1.5129,0.3094,0.7866,0.2195,63.9684,0.6625 +deepface/tests/dataset/img53.jpg,deepface/tests/dataset/img30.jpg,No,0.9492,0.7325,1.3778,0.9241,16.5521,1.3595,0.5533,1.052,0.2955,62.208,0.7687 +deepface/tests/dataset/img6.jpg,deepface/tests/dataset/img25.jpg,No,0.8285,0.8131,1.2872,0.8051,14.8877,1.2689,0.4267,0.9238,0.3226,79.803,0.8032 +deepface/tests/dataset/img4.jpg,deepface/tests/dataset/img43.jpg,No,0.6285,0.7443,1.1211,0.838,15.1848,1.2946,0.212,0.6511,0.2685,71.4046,0.7329 +deepface/tests/dataset/img39.jpg,deepface/tests/dataset/img27.jpg,No,0.7176,0.8685,1.198,0.8199,14.9449,1.2805,0.8286,1.2873,0.285,71.6832,0.755 +deepface/tests/dataset/img36.jpg,deepface/tests/dataset/img23.jpg,No,0.6223,0.5866,1.1156,1.0693,17.5747,1.4624,0.4266,0.9237,0.32,58.9248,0.7999 +deepface/tests/dataset/img4.jpg,deepface/tests/dataset/img45.jpg,No,0.6021,0.7106,1.0973,0.9407,16.2744,1.3716,0.2162,0.6576,0.2166,64.3341,0.6582 +deepface/tests/dataset/img38.jpg,deepface/tests/dataset/img19.jpg,No,0.356,0.5607,0.8437,0.9843,17.485,1.403,0.1858,0.6097,0.2867,75.4126,0.7572 +deepface/tests/dataset/img55.jpg,deepface/tests/dataset/img17.jpg,No,0.7135,0.6076,1.1946,0.944,16.691,1.374,0.7449,1.2205,0.2951,70.5113,0.7682 +deepface/tests/dataset/img9.jpg,deepface/tests/dataset/img59.jpg,No,0.8449,0.8766,1.2999,1.1333,18.3376,1.5055,0.8844,1.33,0.3088,67.5783,0.7859 +deepface/tests/dataset/img58.jpg,deepface/tests/dataset/img49.jpg,No,0.5999,0.8901,1.0953,0.9147,15.3098,1.3526,0.4925,0.9925,0.2266,63.0835,0.6733 +deepface/tests/dataset/img56.jpg,deepface/tests/dataset/img59.jpg,No,0.7694,0.9166,1.2405,1.0062,17.304,1.4186,0.8703,1.3193,0.2966,70.5446,0.7702 +deepface/tests/dataset/img4.jpg,deepface/tests/dataset/img8.jpg,No,0.5753,0.6478,1.0727,0.842,15.2912,1.2977,0.3808,0.8727,0.1878,59.2,0.6129 +deepface/tests/dataset/img16.jpg,deepface/tests/dataset/img25.jpg,No,0.5927,0.6271,1.0887,0.9862,16.5907,1.4044,0.286,0.7563,0.1702,56.0079,0.5835 +deepface/tests/dataset/img50.jpg,deepface/tests/dataset/img45.jpg,No,0.5692,0.6912,1.067,0.8581,15.6737,1.3101,0.3278,0.8097,0.2383,60.6426,0.6903 +deepface/tests/dataset/img38.jpg,deepface/tests/dataset/img31.jpg,No,0.4739,0.4751,0.9736,1.1148,18.1862,1.4932,0.6661,1.1542,0.331,70.516,0.8136 +deepface/tests/dataset/img13.jpg,deepface/tests/dataset/img51.jpg,No,0.5639,0.7621,1.062,0.8047,14.7361,1.2686,0.4,0.8945,0.2308,60.6072,0.6795 +deepface/tests/dataset/img1.jpg,deepface/tests/dataset/img33.jpg,No,0.7127,0.6418,1.1939,0.9433,16.1933,1.3736,0.6509,1.1409,0.2684,62.7672,0.7326 +deepface/tests/dataset/img53.jpg,deepface/tests/dataset/img16.jpg,No,0.8344,0.7073,1.2918,0.9023,16.3918,1.3433,0.4153,0.9114,0.3045,65.6394,0.7803 +deepface/tests/dataset/img53.jpg,deepface/tests/dataset/img23.jpg,No,0.4644,0.5199,0.9637,0.7267,14.6939,1.2056,0.1784,0.5973,0.2774,55.6833,0.7448 \ No newline at end of file diff --git a/tests/dataset/img1.jpg b/tests/dataset/img1.jpg new file mode 100644 index 0000000..7ceab4f Binary files /dev/null and b/tests/dataset/img1.jpg differ diff --git a/tests/dataset/img10.jpg b/tests/dataset/img10.jpg new file mode 100644 index 0000000..267d401 Binary files /dev/null and b/tests/dataset/img10.jpg differ diff --git a/tests/dataset/img11.jpg b/tests/dataset/img11.jpg new file mode 100644 index 0000000..0ebe708 Binary files /dev/null and b/tests/dataset/img11.jpg differ diff --git a/tests/dataset/img11_reflection.jpg b/tests/dataset/img11_reflection.jpg new file mode 100644 index 0000000..24b29f2 Binary files /dev/null and b/tests/dataset/img11_reflection.jpg differ diff --git a/tests/dataset/img12.jpg b/tests/dataset/img12.jpg new file mode 100644 index 0000000..e85fac0 Binary files /dev/null and b/tests/dataset/img12.jpg differ diff --git a/tests/dataset/img13.jpg b/tests/dataset/img13.jpg new file mode 100644 index 0000000..74ed70a Binary files /dev/null and b/tests/dataset/img13.jpg differ diff --git a/tests/dataset/img14.jpg b/tests/dataset/img14.jpg new file mode 100644 index 0000000..1f527fb Binary files /dev/null and b/tests/dataset/img14.jpg differ diff --git a/tests/dataset/img15.jpg b/tests/dataset/img15.jpg new file mode 100644 index 0000000..c3e85a3 Binary files /dev/null and b/tests/dataset/img15.jpg differ diff --git a/tests/dataset/img16.jpg b/tests/dataset/img16.jpg new file mode 100644 index 0000000..0774887 Binary files /dev/null and b/tests/dataset/img16.jpg differ diff --git a/tests/dataset/img17.jpg b/tests/dataset/img17.jpg new file mode 100644 index 0000000..7d37ff5 Binary files /dev/null and b/tests/dataset/img17.jpg differ diff --git a/tests/dataset/img18.jpg b/tests/dataset/img18.jpg new file mode 100644 index 0000000..6141407 Binary files /dev/null and b/tests/dataset/img18.jpg differ diff --git a/tests/dataset/img19.jpg b/tests/dataset/img19.jpg new file mode 100644 index 0000000..45433dc Binary files /dev/null and b/tests/dataset/img19.jpg differ diff --git a/tests/dataset/img2.jpg b/tests/dataset/img2.jpg new file mode 100644 index 0000000..05683ea Binary files /dev/null and b/tests/dataset/img2.jpg differ diff --git a/tests/dataset/img20.jpg b/tests/dataset/img20.jpg new file mode 100644 index 0000000..6e85175 Binary files /dev/null and b/tests/dataset/img20.jpg differ diff --git a/tests/dataset/img21.jpg b/tests/dataset/img21.jpg new file mode 100644 index 0000000..1f90aa4 Binary files /dev/null and b/tests/dataset/img21.jpg differ diff --git a/tests/dataset/img22.jpg b/tests/dataset/img22.jpg new file mode 100644 index 0000000..0d18c94 Binary files /dev/null and b/tests/dataset/img22.jpg differ diff --git a/tests/dataset/img23.jpg b/tests/dataset/img23.jpg new file mode 100644 index 0000000..c96eb04 Binary files /dev/null and b/tests/dataset/img23.jpg differ diff --git a/tests/dataset/img24.jpg b/tests/dataset/img24.jpg new file mode 100644 index 0000000..d134ed5 Binary files /dev/null and b/tests/dataset/img24.jpg differ diff --git a/tests/dataset/img25.jpg b/tests/dataset/img25.jpg new file mode 100644 index 0000000..d2d867c Binary files /dev/null and b/tests/dataset/img25.jpg differ diff --git a/tests/dataset/img26.jpg b/tests/dataset/img26.jpg new file mode 100644 index 0000000..cad2093 Binary files /dev/null and b/tests/dataset/img26.jpg differ diff --git a/tests/dataset/img27.jpg b/tests/dataset/img27.jpg new file mode 100644 index 0000000..b0bc802 Binary files /dev/null and b/tests/dataset/img27.jpg differ diff --git a/tests/dataset/img28.jpg b/tests/dataset/img28.jpg new file mode 100644 index 0000000..a643428 Binary files /dev/null and b/tests/dataset/img28.jpg differ diff --git a/tests/dataset/img29.jpg b/tests/dataset/img29.jpg new file mode 100644 index 0000000..8383924 Binary files /dev/null and b/tests/dataset/img29.jpg differ diff --git a/tests/dataset/img3.jpg b/tests/dataset/img3.jpg new file mode 100644 index 0000000..08e8dcc Binary files /dev/null and b/tests/dataset/img3.jpg differ diff --git a/tests/dataset/img30.jpg b/tests/dataset/img30.jpg new file mode 100644 index 0000000..f271132 Binary files /dev/null and b/tests/dataset/img30.jpg differ diff --git a/tests/dataset/img31.jpg b/tests/dataset/img31.jpg new file mode 100644 index 0000000..342d6fd Binary files /dev/null and b/tests/dataset/img31.jpg differ diff --git a/tests/dataset/img32.jpg b/tests/dataset/img32.jpg new file mode 100644 index 0000000..8df9dd7 Binary files /dev/null and b/tests/dataset/img32.jpg differ diff --git a/tests/dataset/img33.jpg b/tests/dataset/img33.jpg new file mode 100644 index 0000000..c6412be Binary files /dev/null and b/tests/dataset/img33.jpg differ diff --git a/tests/dataset/img34.jpg b/tests/dataset/img34.jpg new file mode 100644 index 0000000..47ea382 Binary files /dev/null and b/tests/dataset/img34.jpg differ diff --git a/tests/dataset/img35.jpg b/tests/dataset/img35.jpg new file mode 100644 index 0000000..b20c97d Binary files /dev/null and b/tests/dataset/img35.jpg differ diff --git a/tests/dataset/img36.jpg b/tests/dataset/img36.jpg new file mode 100644 index 0000000..0d4ea95 Binary files /dev/null and b/tests/dataset/img36.jpg differ diff --git a/tests/dataset/img37.jpg b/tests/dataset/img37.jpg new file mode 100644 index 0000000..9026e8c Binary files /dev/null and b/tests/dataset/img37.jpg differ diff --git a/tests/dataset/img38.jpg b/tests/dataset/img38.jpg new file mode 100644 index 0000000..b9bb7f1 Binary files /dev/null and b/tests/dataset/img38.jpg differ diff --git a/tests/dataset/img39.jpg b/tests/dataset/img39.jpg new file mode 100644 index 0000000..751b664 Binary files /dev/null and b/tests/dataset/img39.jpg differ diff --git a/tests/dataset/img4.jpg b/tests/dataset/img4.jpg new file mode 100644 index 0000000..b767bd0 Binary files /dev/null and b/tests/dataset/img4.jpg differ diff --git a/tests/dataset/img40.jpg b/tests/dataset/img40.jpg new file mode 100644 index 0000000..71ebe3b Binary files /dev/null and b/tests/dataset/img40.jpg differ diff --git a/tests/dataset/img41.jpg b/tests/dataset/img41.jpg new file mode 100644 index 0000000..cad0827 Binary files /dev/null and b/tests/dataset/img41.jpg differ diff --git a/tests/dataset/img42.jpg b/tests/dataset/img42.jpg new file mode 100644 index 0000000..1d42636 Binary files /dev/null and b/tests/dataset/img42.jpg differ diff --git a/tests/dataset/img43.jpg b/tests/dataset/img43.jpg new file mode 100644 index 0000000..5aaedfe Binary files /dev/null and b/tests/dataset/img43.jpg differ diff --git a/tests/dataset/img44.jpg b/tests/dataset/img44.jpg new file mode 100644 index 0000000..4cc86d5 Binary files /dev/null and b/tests/dataset/img44.jpg differ diff --git a/tests/dataset/img45.jpg b/tests/dataset/img45.jpg new file mode 100644 index 0000000..2c9da24 Binary files /dev/null and b/tests/dataset/img45.jpg differ diff --git a/tests/dataset/img46.jpg b/tests/dataset/img46.jpg new file mode 100644 index 0000000..5d49cb3 Binary files /dev/null and b/tests/dataset/img46.jpg differ diff --git a/tests/dataset/img47.jpg b/tests/dataset/img47.jpg new file mode 100644 index 0000000..0cc3179 Binary files /dev/null and b/tests/dataset/img47.jpg differ diff --git a/tests/dataset/img48.jpg b/tests/dataset/img48.jpg new file mode 100644 index 0000000..d3ae481 Binary files /dev/null and b/tests/dataset/img48.jpg differ diff --git a/tests/dataset/img49.jpg b/tests/dataset/img49.jpg new file mode 100644 index 0000000..5f6d3c9 Binary files /dev/null and b/tests/dataset/img49.jpg differ diff --git a/tests/dataset/img5.jpg b/tests/dataset/img5.jpg new file mode 100644 index 0000000..29b6a2f Binary files /dev/null and b/tests/dataset/img5.jpg differ diff --git a/tests/dataset/img50.jpg b/tests/dataset/img50.jpg new file mode 100644 index 0000000..3abe0b0 Binary files /dev/null and b/tests/dataset/img50.jpg differ diff --git a/tests/dataset/img51.jpg b/tests/dataset/img51.jpg new file mode 100644 index 0000000..9679499 Binary files /dev/null and b/tests/dataset/img51.jpg differ diff --git a/tests/dataset/img53.jpg b/tests/dataset/img53.jpg new file mode 100644 index 0000000..1487a32 Binary files /dev/null and b/tests/dataset/img53.jpg differ diff --git a/tests/dataset/img54.jpg b/tests/dataset/img54.jpg new file mode 100644 index 0000000..f736571 Binary files /dev/null and b/tests/dataset/img54.jpg differ diff --git a/tests/dataset/img55.jpg b/tests/dataset/img55.jpg new file mode 100644 index 0000000..9ed4612 Binary files /dev/null and b/tests/dataset/img55.jpg differ diff --git a/tests/dataset/img56.jpg b/tests/dataset/img56.jpg new file mode 100644 index 0000000..79ac53f Binary files /dev/null and b/tests/dataset/img56.jpg differ diff --git a/tests/dataset/img57.jpg b/tests/dataset/img57.jpg new file mode 100644 index 0000000..175fef4 Binary files /dev/null and b/tests/dataset/img57.jpg differ diff --git a/tests/dataset/img58.jpg b/tests/dataset/img58.jpg new file mode 100644 index 0000000..dcee1b8 Binary files /dev/null and b/tests/dataset/img58.jpg differ diff --git a/tests/dataset/img59.jpg b/tests/dataset/img59.jpg new file mode 100644 index 0000000..d394873 Binary files /dev/null and b/tests/dataset/img59.jpg differ diff --git a/tests/dataset/img6.jpg b/tests/dataset/img6.jpg new file mode 100644 index 0000000..03dcd3e Binary files /dev/null and b/tests/dataset/img6.jpg differ diff --git a/tests/dataset/img61.jpg b/tests/dataset/img61.jpg new file mode 100644 index 0000000..30c4939 Binary files /dev/null and b/tests/dataset/img61.jpg differ diff --git a/tests/dataset/img62.jpg b/tests/dataset/img62.jpg new file mode 100644 index 0000000..74a7e98 Binary files /dev/null and b/tests/dataset/img62.jpg differ diff --git a/tests/dataset/img67.jpg b/tests/dataset/img67.jpg new file mode 100644 index 0000000..f09699d Binary files /dev/null and b/tests/dataset/img67.jpg differ diff --git a/tests/dataset/img7.jpg b/tests/dataset/img7.jpg new file mode 100644 index 0000000..7cf10cc Binary files /dev/null and b/tests/dataset/img7.jpg differ diff --git a/tests/dataset/img8.jpg b/tests/dataset/img8.jpg new file mode 100644 index 0000000..53a55e9 Binary files /dev/null and b/tests/dataset/img8.jpg differ diff --git a/tests/dataset/img9.jpg b/tests/dataset/img9.jpg new file mode 100644 index 0000000..d4ea20f Binary files /dev/null and b/tests/dataset/img9.jpg differ diff --git a/tests/dataset/master.csv b/tests/dataset/master.csv new file mode 100644 index 0000000..5b05a57 --- /dev/null +++ b/tests/dataset/master.csv @@ -0,0 +1,301 @@ +file_x,file_y,Decision +img20.jpg,img21.jpg,Yes +img16.jpg,img17.jpg,Yes +img3.jpg,img12.jpg,Yes +img22.jpg,img23.jpg,Yes +img24.jpg,img25.jpg,Yes +img1.jpg,img2.jpg,Yes +img1.jpg,img4.jpg,Yes +img1.jpg,img5.jpg,Yes +img1.jpg,img6.jpg,Yes +img1.jpg,img7.jpg,Yes +img1.jpg,img10.jpg,Yes +img1.jpg,img11.jpg,Yes +img2.jpg,img4.jpg,Yes +img2.jpg,img5.jpg,Yes +img2.jpg,img6.jpg,Yes +img2.jpg,img7.jpg,Yes +img2.jpg,img10.jpg,Yes +img2.jpg,img11.jpg,Yes +img4.jpg,img5.jpg,Yes +img4.jpg,img6.jpg,Yes +img4.jpg,img7.jpg,Yes +img4.jpg,img10.jpg,Yes +img4.jpg,img11.jpg,Yes +img5.jpg,img6.jpg,Yes +img5.jpg,img7.jpg,Yes +img5.jpg,img10.jpg,Yes +img5.jpg,img11.jpg,Yes +img6.jpg,img7.jpg,Yes +img6.jpg,img10.jpg,Yes +img6.jpg,img11.jpg,Yes +img7.jpg,img10.jpg,Yes +img7.jpg,img11.jpg,Yes +img10.jpg,img11.jpg,Yes +img13.jpg,img14.jpg,Yes +img13.jpg,img15.jpg,Yes +img14.jpg,img15.jpg,Yes +img18.jpg,img19.jpg,Yes +img8.jpg,img9.jpg,Yes +img20.jpg,img16.jpg,No +img20.jpg,img17.jpg,No +img21.jpg,img16.jpg,No +img21.jpg,img17.jpg,No +img20.jpg,img3.jpg,No +img20.jpg,img12.jpg,No +img21.jpg,img3.jpg,No +img21.jpg,img12.jpg,No +img20.jpg,img22.jpg,No +img20.jpg,img23.jpg,No +img21.jpg,img22.jpg,No +img21.jpg,img23.jpg,No +img20.jpg,img24.jpg,No +img20.jpg,img25.jpg,No +img21.jpg,img24.jpg,No +img21.jpg,img25.jpg,No +img20.jpg,img1.jpg,No +img20.jpg,img2.jpg,No +img20.jpg,img4.jpg,No +img20.jpg,img5.jpg,No +img20.jpg,img6.jpg,No +img20.jpg,img7.jpg,No +img20.jpg,img10.jpg,No +img20.jpg,img11.jpg,No +img21.jpg,img1.jpg,No +img21.jpg,img2.jpg,No +img21.jpg,img4.jpg,No +img21.jpg,img5.jpg,No +img21.jpg,img6.jpg,No +img21.jpg,img7.jpg,No +img21.jpg,img10.jpg,No +img21.jpg,img11.jpg,No +img20.jpg,img13.jpg,No +img20.jpg,img14.jpg,No +img20.jpg,img15.jpg,No +img21.jpg,img13.jpg,No +img21.jpg,img14.jpg,No +img21.jpg,img15.jpg,No +img20.jpg,img18.jpg,No +img20.jpg,img19.jpg,No +img21.jpg,img18.jpg,No +img21.jpg,img19.jpg,No +img20.jpg,img8.jpg,No +img20.jpg,img9.jpg,No +img21.jpg,img8.jpg,No +img21.jpg,img9.jpg,No +img16.jpg,img3.jpg,No +img16.jpg,img12.jpg,No +img17.jpg,img3.jpg,No +img17.jpg,img12.jpg,No +img16.jpg,img22.jpg,No +img16.jpg,img23.jpg,No +img17.jpg,img22.jpg,No +img17.jpg,img23.jpg,No +img16.jpg,img24.jpg,No +img16.jpg,img25.jpg,No +img17.jpg,img24.jpg,No +img17.jpg,img25.jpg,No +img16.jpg,img1.jpg,No +img16.jpg,img2.jpg,No +img16.jpg,img4.jpg,No +img16.jpg,img5.jpg,No +img16.jpg,img6.jpg,No +img16.jpg,img7.jpg,No +img16.jpg,img10.jpg,No +img16.jpg,img11.jpg,No +img17.jpg,img1.jpg,No +img17.jpg,img2.jpg,No +img17.jpg,img4.jpg,No +img17.jpg,img5.jpg,No +img17.jpg,img6.jpg,No +img17.jpg,img7.jpg,No +img17.jpg,img10.jpg,No +img17.jpg,img11.jpg,No +img16.jpg,img13.jpg,No +img16.jpg,img14.jpg,No +img16.jpg,img15.jpg,No +img17.jpg,img13.jpg,No +img17.jpg,img14.jpg,No +img17.jpg,img15.jpg,No +img16.jpg,img18.jpg,No +img16.jpg,img19.jpg,No +img17.jpg,img18.jpg,No +img17.jpg,img19.jpg,No +img16.jpg,img8.jpg,No +img16.jpg,img9.jpg,No +img17.jpg,img8.jpg,No +img17.jpg,img9.jpg,No +img3.jpg,img22.jpg,No +img3.jpg,img23.jpg,No +img12.jpg,img22.jpg,No +img12.jpg,img23.jpg,No +img3.jpg,img24.jpg,No +img3.jpg,img25.jpg,No +img12.jpg,img24.jpg,No +img12.jpg,img25.jpg,No +img3.jpg,img1.jpg,No +img3.jpg,img2.jpg,No +img3.jpg,img4.jpg,No +img3.jpg,img5.jpg,No +img3.jpg,img6.jpg,No +img3.jpg,img7.jpg,No +img3.jpg,img10.jpg,No +img3.jpg,img11.jpg,No +img12.jpg,img1.jpg,No +img12.jpg,img2.jpg,No +img12.jpg,img4.jpg,No +img12.jpg,img5.jpg,No +img12.jpg,img6.jpg,No +img12.jpg,img7.jpg,No +img12.jpg,img10.jpg,No +img12.jpg,img11.jpg,No +img3.jpg,img13.jpg,No +img3.jpg,img14.jpg,No +img3.jpg,img15.jpg,No +img12.jpg,img13.jpg,No +img12.jpg,img14.jpg,No +img12.jpg,img15.jpg,No +img3.jpg,img18.jpg,No +img3.jpg,img19.jpg,No +img12.jpg,img18.jpg,No +img12.jpg,img19.jpg,No +img3.jpg,img8.jpg,No +img3.jpg,img9.jpg,No +img12.jpg,img8.jpg,No +img12.jpg,img9.jpg,No +img22.jpg,img24.jpg,No +img22.jpg,img25.jpg,No +img23.jpg,img24.jpg,No +img23.jpg,img25.jpg,No +img22.jpg,img1.jpg,No +img22.jpg,img2.jpg,No +img22.jpg,img4.jpg,No +img22.jpg,img5.jpg,No +img22.jpg,img6.jpg,No +img22.jpg,img7.jpg,No +img22.jpg,img10.jpg,No +img22.jpg,img11.jpg,No +img23.jpg,img1.jpg,No +img23.jpg,img2.jpg,No +img23.jpg,img4.jpg,No +img23.jpg,img5.jpg,No +img23.jpg,img6.jpg,No +img23.jpg,img7.jpg,No +img23.jpg,img10.jpg,No +img23.jpg,img11.jpg,No +img22.jpg,img13.jpg,No +img22.jpg,img14.jpg,No +img22.jpg,img15.jpg,No +img23.jpg,img13.jpg,No +img23.jpg,img14.jpg,No +img23.jpg,img15.jpg,No +img22.jpg,img18.jpg,No +img22.jpg,img19.jpg,No +img23.jpg,img18.jpg,No +img23.jpg,img19.jpg,No +img22.jpg,img8.jpg,No +img22.jpg,img9.jpg,No +img23.jpg,img8.jpg,No +img23.jpg,img9.jpg,No +img24.jpg,img1.jpg,No +img24.jpg,img2.jpg,No +img24.jpg,img4.jpg,No +img24.jpg,img5.jpg,No +img24.jpg,img6.jpg,No +img24.jpg,img7.jpg,No +img24.jpg,img10.jpg,No +img24.jpg,img11.jpg,No +img25.jpg,img1.jpg,No +img25.jpg,img2.jpg,No +img25.jpg,img4.jpg,No +img25.jpg,img5.jpg,No +img25.jpg,img6.jpg,No +img25.jpg,img7.jpg,No +img25.jpg,img10.jpg,No +img25.jpg,img11.jpg,No +img24.jpg,img13.jpg,No +img24.jpg,img14.jpg,No +img24.jpg,img15.jpg,No +img25.jpg,img13.jpg,No +img25.jpg,img14.jpg,No +img25.jpg,img15.jpg,No +img24.jpg,img18.jpg,No +img24.jpg,img19.jpg,No +img25.jpg,img18.jpg,No +img25.jpg,img19.jpg,No +img24.jpg,img8.jpg,No +img24.jpg,img9.jpg,No +img25.jpg,img8.jpg,No +img25.jpg,img9.jpg,No +img1.jpg,img13.jpg,No +img1.jpg,img14.jpg,No +img1.jpg,img15.jpg,No +img2.jpg,img13.jpg,No +img2.jpg,img14.jpg,No +img2.jpg,img15.jpg,No +img4.jpg,img13.jpg,No +img4.jpg,img14.jpg,No +img4.jpg,img15.jpg,No +img5.jpg,img13.jpg,No +img5.jpg,img14.jpg,No +img5.jpg,img15.jpg,No +img6.jpg,img13.jpg,No +img6.jpg,img14.jpg,No +img6.jpg,img15.jpg,No +img7.jpg,img13.jpg,No +img7.jpg,img14.jpg,No +img7.jpg,img15.jpg,No +img10.jpg,img13.jpg,No +img10.jpg,img14.jpg,No +img10.jpg,img15.jpg,No +img11.jpg,img13.jpg,No +img11.jpg,img14.jpg,No +img11.jpg,img15.jpg,No +img1.jpg,img18.jpg,No +img1.jpg,img19.jpg,No +img2.jpg,img18.jpg,No +img2.jpg,img19.jpg,No +img4.jpg,img18.jpg,No +img4.jpg,img19.jpg,No +img5.jpg,img18.jpg,No +img5.jpg,img19.jpg,No +img6.jpg,img18.jpg,No +img6.jpg,img19.jpg,No +img7.jpg,img18.jpg,No +img7.jpg,img19.jpg,No +img10.jpg,img18.jpg,No +img10.jpg,img19.jpg,No +img11.jpg,img18.jpg,No +img11.jpg,img19.jpg,No +img1.jpg,img8.jpg,No +img1.jpg,img9.jpg,No +img2.jpg,img8.jpg,No +img2.jpg,img9.jpg,No +img4.jpg,img8.jpg,No +img4.jpg,img9.jpg,No +img5.jpg,img8.jpg,No +img5.jpg,img9.jpg,No +img6.jpg,img8.jpg,No +img6.jpg,img9.jpg,No +img7.jpg,img8.jpg,No +img7.jpg,img9.jpg,No +img10.jpg,img8.jpg,No +img10.jpg,img9.jpg,No +img11.jpg,img8.jpg,No +img11.jpg,img9.jpg,No +img13.jpg,img18.jpg,No +img13.jpg,img19.jpg,No +img14.jpg,img18.jpg,No +img14.jpg,img19.jpg,No +img15.jpg,img18.jpg,No +img15.jpg,img19.jpg,No +img13.jpg,img8.jpg,No +img13.jpg,img9.jpg,No +img14.jpg,img8.jpg,No +img14.jpg,img9.jpg,No +img15.jpg,img8.jpg,No +img15.jpg,img9.jpg,No +img18.jpg,img8.jpg,No +img18.jpg,img9.jpg,No +img19.jpg,img8.jpg,No +img19.jpg,img9.jpg,No \ No newline at end of file diff --git a/tests/dataset/selfie-many-people.jpg b/tests/dataset/selfie-many-people.jpg new file mode 100644 index 0000000..a6fed51 Binary files /dev/null and b/tests/dataset/selfie-many-people.jpg differ diff --git a/tests/face-recognition-how.py b/tests/face-recognition-how.py new file mode 100644 index 0000000..cb26f2f --- /dev/null +++ b/tests/face-recognition-how.py @@ -0,0 +1,107 @@ +# 3rd party dependencies +import matplotlib.pyplot as plt +import numpy as np +import cv2 + +# project dependencies +from deepface import DeepFace +from deepface.modules import verification +from deepface.models.FacialRecognition import FacialRecognition +from deepface.commons.logger import Logger + +logger = Logger() + +# ---------------------------------------------- +# build face recognition model + +model_name = "VGG-Face" + +model: FacialRecognition = DeepFace.build_model(task="facial_recognition", model_name=model_name) + +target_size = model.input_shape + +logger.info(f"target_size: {target_size}") + +# ---------------------------------------------- +# load images and find embeddings + +img1 = DeepFace.extract_faces(img_path="dataset/img1.jpg")[0]["face"] +img1 = cv2.resize(img1, target_size) +img1 = np.expand_dims(img1, axis=0) # to (1, 224, 224, 3) +img1_representation = model.forward(img1) + +img2 = DeepFace.extract_faces(img_path="dataset/img3.jpg")[0]["face"] +img2 = cv2.resize(img2, target_size) +img2 = np.expand_dims(img2, axis=0) +img2_representation = model.forward(img2) + +img1_representation = np.array(img1_representation) +img2_representation = np.array(img2_representation) + +# ---------------------------------------------- +# distance between two images - euclidean distance formula +distance_vector = np.square(img1_representation - img2_representation) +current_distance = np.sqrt(distance_vector.sum()) +logger.info(f"Euclidean distance: {current_distance}") + +threshold = verification.find_threshold(model_name=model_name, distance_metric="euclidean") +logger.info(f"Threshold for {model_name}-euclidean pair is {threshold}") + +if current_distance < threshold: + logger.info( + f"This pair is same person because its distance {current_distance}" + f" is less than threshold {threshold}" + ) +else: + logger.info( + f"This pair is different persons because its distance {current_distance}" + f" is greater than threshold {threshold}" + ) +# ---------------------------------------------- +# expand vectors to be shown better in graph + +img1_graph = [] +img2_graph = [] +distance_graph = [] + +for i in range(0, 200): + img1_graph.append(img1_representation) + img2_graph.append(img2_representation) + distance_graph.append(distance_vector) + +img1_graph = np.array(img1_graph) +img2_graph = np.array(img2_graph) +distance_graph = np.array(distance_graph) + +# ---------------------------------------------- +# plotting + +fig = plt.figure() + +ax1 = fig.add_subplot(3, 2, 1) +plt.imshow(img1[0]) +plt.axis("off") + +ax2 = fig.add_subplot(3, 2, 2) +im = plt.imshow(img1_graph, interpolation="nearest", cmap=plt.cm.ocean) +plt.colorbar() + +ax3 = fig.add_subplot(3, 2, 3) +plt.imshow(img2[0]) +plt.axis("off") + +ax4 = fig.add_subplot(3, 2, 4) +im = plt.imshow(img2_graph, interpolation="nearest", cmap=plt.cm.ocean) +plt.colorbar() + +ax5 = fig.add_subplot(3, 2, 5) +plt.text(0.35, 0, f"Distance: {current_distance}") +plt.axis("off") + +ax6 = fig.add_subplot(3, 2, 6) +im = plt.imshow(distance_graph, interpolation="nearest", cmap=plt.cm.ocean) +plt.colorbar() + +plt.show() + +# ---------------------------------------------- diff --git a/tests/overlay.py b/tests/overlay.py new file mode 100644 index 0000000..99cd977 --- /dev/null +++ b/tests/overlay.py @@ -0,0 +1,60 @@ +# 3rd party dependencies +import cv2 +import matplotlib.pyplot as plt + +# project dependencies +from deepface.modules import streaming +from deepface import DeepFace + +img_path = "dataset/img1.jpg" +img = cv2.imread(img_path) + +overlay_img_path = "dataset/img6.jpg" +face_objs = DeepFace.extract_faces(overlay_img_path) +overlay_img = face_objs[0]["face"][:, :, ::-1] * 255 + +overlay_img = cv2.resize(overlay_img, (112, 112)) + +raw_img = img.copy() + +demographies = DeepFace.analyze(img_path=img_path, actions=("age", "gender", "emotion")) +demography = demographies[0] + +x = demography["region"]["x"] +y = demography["region"]["y"] +w = demography["region"]["w"] +h = demography["region"]["h"] + +img = streaming.highlight_facial_areas(img=img, faces_coordinates=[(x, y, w, h)]) + +img = streaming.overlay_emotion( + img=img, + emotion_probas=demography["emotion"], + x=x, + y=y, + w=w, + h=h, +) + +img = streaming.overlay_age_gender( + img=img, + apparent_age=demography["age"], + gender=demography["dominant_gender"][0:1], + x=x, + y=y, + w=w, + h=h, +) + +img = streaming.overlay_identified_face( + img=img, + target_img=overlay_img, + label="angelina", + x=x, + y=y, + w=w, + h=h, +) + +plt.imshow(img[:, :, ::-1]) +plt.show() diff --git a/tests/stream.py b/tests/stream.py new file mode 100644 index 0000000..d0cd3c9 --- /dev/null +++ b/tests/stream.py @@ -0,0 +1,8 @@ +from deepface import DeepFace + +DeepFace.stream("dataset", enable_face_analysis=False, anti_spoofing=True) # opencv +# DeepFace.stream("dataset", detector_backend = 'opencv') +# DeepFace.stream("dataset", detector_backend = 'ssd') +# DeepFace.stream("dataset", detector_backend = 'mtcnn') +# DeepFace.stream("dataset", detector_backend = 'dlib') +# DeepFace.stream("dataset", detector_backend = 'retinaface') diff --git a/tests/test_analyze.py b/tests/test_analyze.py new file mode 100644 index 0000000..bad4426 --- /dev/null +++ b/tests/test_analyze.py @@ -0,0 +1,137 @@ +# 3rd party dependencies +import cv2 + +# project dependencies +from deepface import DeepFace +from deepface.commons.logger import Logger + +logger = Logger() + + +detectors = ["opencv", "mtcnn"] + + +def test_standard_analyze(): + img = "dataset/img4.jpg" + demography_objs = DeepFace.analyze(img, silent=True) + for demography in demography_objs: + logger.debug(demography) + assert demography["age"] > 20 and demography["age"] < 40 + assert demography["dominant_gender"] == "Woman" + logger.info("✅ test standard analyze done") + + +def test_analyze_with_all_actions_as_tuple(): + img = "dataset/img4.jpg" + demography_objs = DeepFace.analyze( + img, actions=("age", "gender", "race", "emotion"), silent=True + ) + + for demography in demography_objs: + logger.debug(f"Demography: {demography}") + age = demography["age"] + gender = demography["dominant_gender"] + race = demography["dominant_race"] + emotion = demography["dominant_emotion"] + logger.debug(f"Age: {age}") + logger.debug(f"Gender: {gender}") + logger.debug(f"Race: {race}") + logger.debug(f"Emotion: {emotion}") + assert demography.get("age") is not None + assert demography.get("dominant_gender") is not None + assert demography.get("dominant_race") is not None + assert demography.get("dominant_emotion") is not None + + logger.info("✅ test analyze for all actions as tuple done") + + +def test_analyze_with_all_actions_as_list(): + img = "dataset/img4.jpg" + demography_objs = DeepFace.analyze( + img, actions=["age", "gender", "race", "emotion"], silent=True + ) + + for demography in demography_objs: + logger.debug(f"Demography: {demography}") + age = demography["age"] + gender = demography["dominant_gender"] + race = demography["dominant_race"] + emotion = demography["dominant_emotion"] + logger.debug(f"Age: {age}") + logger.debug(f"Gender: {gender}") + logger.debug(f"Race: {race}") + logger.debug(f"Emotion: {emotion}") + assert demography.get("age") is not None + assert demography.get("dominant_gender") is not None + assert demography.get("dominant_race") is not None + assert demography.get("dominant_emotion") is not None + + logger.info("✅ test analyze for all actions as array done") + + +def test_analyze_for_some_actions(): + img = "dataset/img4.jpg" + demography_objs = DeepFace.analyze(img, ["age", "gender"], silent=True) + + for demography in demography_objs: + age = demography["age"] + gender = demography["dominant_gender"] + + logger.debug(f"Age: { age }") + logger.debug(f"Gender: {gender}") + + assert demography.get("age") is not None + assert demography.get("dominant_gender") is not None + + # these are not in actions + assert demography.get("dominant_race") is None + assert demography.get("dominant_emotion") is None + + logger.info("✅ test analyze for some actions done") + + +def test_analyze_for_preloaded_image(): + img = cv2.imread("dataset/img1.jpg") + resp_objs = DeepFace.analyze(img, silent=True) + for resp_obj in resp_objs: + logger.debug(resp_obj) + assert resp_obj["age"] > 20 and resp_obj["age"] < 40 + assert resp_obj["dominant_gender"] == "Woman" + + logger.info("✅ test analyze for pre-loaded image done") + + +def test_analyze_for_different_detectors(): + img_paths = [ + "dataset/img1.jpg", + "dataset/img5.jpg", + "dataset/img6.jpg", + "dataset/img8.jpg", + "dataset/img1.jpg", + "dataset/img2.jpg", + "dataset/img1.jpg", + "dataset/img2.jpg", + "dataset/img6.jpg", + "dataset/img6.jpg", + ] + + for img_path in img_paths: + for detector in detectors: + results = DeepFace.analyze( + img_path, actions=("gender",), detector_backend=detector, enforce_detection=False + ) + for result in results: + logger.debug(result) + + # validate keys + assert "gender" in result.keys() + assert "dominant_gender" in result.keys() and result["dominant_gender"] in [ + "Man", + "Woman", + ] + + # validate probabilities + if result["dominant_gender"] == "Man": + assert result["gender"]["Man"] > result["gender"]["Woman"] + else: + assert result["gender"]["Man"] < result["gender"]["Woman"] diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 0000000..ef2db73 --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,229 @@ +# built-in dependencies +import base64 +import unittest + +# project dependencies +from deepface.api.src.app import create_app +from deepface.commons.logger import Logger + +logger = Logger() + + +class TestVerifyEndpoint(unittest.TestCase): + def setUp(self): + app = create_app() + app.config["DEBUG"] = True + app.config["TESTING"] = True + self.app = app.test_client() + + def test_tp_verify(self): + data = { + "img1_path": "dataset/img1.jpg", + "img2_path": "dataset/img2.jpg", + } + response = self.app.post("/verify", json=data) + assert response.status_code == 200 + result = response.json + logger.debug(result) + + assert result.get("verified") is not None + assert result.get("model") is not None + assert result.get("similarity_metric") is not None + assert result.get("detector_backend") is not None + assert result.get("distance") is not None + assert result.get("threshold") is not None + assert result.get("facial_areas") is not None + + assert result.get("verified") is True + + logger.info("✅ true-positive verification api test is done") + + def test_tn_verify(self): + data = { + "img1_path": "dataset/img1.jpg", + "img2_path": "dataset/img2.jpg", + } + response = self.app.post("/verify", json=data) + assert response.status_code == 200 + result = response.json + logger.debug(result) + + assert result.get("verified") is not None + assert result.get("model") is not None + assert result.get("similarity_metric") is not None + assert result.get("detector_backend") is not None + assert result.get("distance") is not None + assert result.get("threshold") is not None + assert result.get("facial_areas") is not None + + assert result.get("verified") is True + + logger.info("✅ true-negative verification api test is done") + + def test_represent(self): + data = { + "img": "dataset/img1.jpg", + } + response = self.app.post("/represent", json=data) + assert response.status_code == 200 + result = response.json + logger.debug(result) + assert result.get("results") is not None + assert isinstance(result["results"], list) is True + assert len(result["results"]) > 0 + for i in result["results"]: + assert i.get("embedding") is not None + assert isinstance(i.get("embedding"), list) is True + assert len(i.get("embedding")) == 4096 + assert i.get("face_confidence") is not None + assert i.get("facial_area") is not None + + logger.info("✅ representation api test is done (for image path)") + + def test_represent_encoded(self): + image_path = "dataset/img1.jpg" + with open(image_path, "rb") as image_file: + encoded_string = "data:image/jpeg;base64," + \ + base64.b64encode(image_file.read()).decode("utf8") + + data = { + "model_name": "Facenet", + "detector_backend": "mtcnn", + "img": encoded_string + } + + response = self.app.post("/represent", json=data) + assert response.status_code == 200 + result = response.json + logger.debug(result) + assert result.get("results") is not None + assert isinstance(result["results"], list) is True + assert len(result["results"]) > 0 + for i in result["results"]: + assert i.get("embedding") is not None + assert isinstance(i.get("embedding"), list) is True + assert len(i.get("embedding")) == 128 + assert i.get("face_confidence") is not None + assert i.get("facial_area") is not None + + logger.info("✅ representation api test is done (for encoded image)") + + def test_represent_url(self): + data = { + "model_name": "Facenet", + "detector_backend": "mtcnn", + "img": "https://github.com/serengil/deepface/blob/master/tests/dataset/couple.jpg?raw=true" + } + + response = self.app.post("/represent", json=data) + assert response.status_code == 200 + result = response.json + logger.debug(result) + assert result.get("results") is not None + assert isinstance(result["results"], list) is True + assert len(result["results"]) == 2 # 2 faces are in the image link + for i in result["results"]: + assert i.get("embedding") is not None + assert isinstance(i.get("embedding"), list) is True + assert len(i.get("embedding")) == 128 + assert i.get("face_confidence") is not None + assert i.get("facial_area") is not None + + logger.info("✅ representation api test is done (for image url)") + + def test_analyze(self): + data = { + "img": "dataset/img1.jpg", + } + response = self.app.post("/analyze", json=data) + assert response.status_code == 200 + result = response.json + logger.debug(result) + assert result.get("results") is not None + assert isinstance(result["results"], list) is True + assert len(result["results"]) > 0 + for i in result["results"]: + assert i.get("age") is not None + assert isinstance(i.get("age"), (int, float)) + assert i.get("dominant_gender") is not None + assert i.get("dominant_gender") in ["Man", "Woman"] + assert i.get("dominant_emotion") is not None + assert i.get("dominant_race") is not None + + logger.info("✅ analyze api test is done") + + def test_analyze_inputformats(self): + image_path = "dataset/couple.jpg" + with open(image_path, "rb") as image_file: + encoded_image = "data:image/jpeg;base64," + \ + base64.b64encode(image_file.read()).decode("utf8") + + image_sources = [ + # image path + image_path, + # image url + f"https://github.com/serengil/deepface/blob/master/tests/{image_path}?raw=true", + # encoded image + encoded_image + ] + + results = [] + for img in image_sources: + data = { + "img": img, + } + response = self.app.post("/analyze", json=data) + + assert response.status_code == 200 + result = response.json + results.append(result) + + assert result.get("results") is not None + assert isinstance(result["results"], list) is True + assert len(result["results"]) > 0 + for i in result["results"]: + assert i.get("age") is not None + assert isinstance(i.get("age"), (int, float)) + assert i.get("dominant_gender") is not None + assert i.get("dominant_gender") in ["Man", "Woman"] + assert i.get("dominant_emotion") is not None + assert i.get("dominant_race") is not None + + assert len(results[0]["results"]) == len(results[1]["results"])\ + and len(results[0]["results"]) == len(results[2]["results"]) + + for i in range(len(results[0]['results'])): + assert results[0]["results"][i]["dominant_emotion"] == results[1]["results"][i]["dominant_emotion"]\ + and results[0]["results"][i]["dominant_emotion"] == results[2]["results"][i]["dominant_emotion"] + + assert results[0]["results"][i]["dominant_gender"] == results[1]["results"][i]["dominant_gender"]\ + and results[0]["results"][i]["dominant_gender"] == results[2]["results"][i]["dominant_gender"] + + assert results[0]["results"][i]["dominant_race"] == results[1]["results"][i]["dominant_race"]\ + and results[0]["results"][i]["dominant_race"] == results[2]["results"][i]["dominant_race"] + + logger.info("✅ different inputs test is done") + + def test_invalid_verify(self): + data = { + "img1_path": "dataset/invalid_1.jpg", + "img2_path": "dataset/invalid_2.jpg", + } + response = self.app.post("/verify", json=data) + assert response.status_code == 400 + logger.info("✅ invalid verification request api test is done") + + def test_invalid_represent(self): + data = { + "img": "dataset/invalid_1.jpg", + } + response = self.app.post("/represent", json=data) + assert response.status_code == 400 + logger.info("✅ invalid represent request api test is done") + + def test_invalid_analyze(self): + data = { + "img": "dataset/invalid.jpg", + } + response = self.app.post("/analyze", json=data) + assert response.status_code == 400 diff --git a/tests/test_commons.py b/tests/test_commons.py new file mode 100644 index 0000000..01a2210 --- /dev/null +++ b/tests/test_commons.py @@ -0,0 +1,252 @@ +# built-in dependencies +import os +from unittest import mock +import pytest + +# project dependencies +from deepface.commons import folder_utils, weight_utils, package_utils +from deepface.commons.logger import Logger + +# pylint: disable=unused-argument + +logger = Logger() + +tf_version = package_utils.get_tf_major_version() + +# conditional imports +if tf_version == 1: + from keras.models import Sequential + from keras.layers import ( + Dropout, + Dense, + ) +else: + from tensorflow.keras.models import Sequential + from tensorflow.keras.layers import ( + Dropout, + Dense, + ) + + +def test_loading_broken_weights(): + home = folder_utils.get_deepface_home() + weight_file = os.path.join(home, ".deepface/weights/vgg_face_weights.h5") + + # construct a dummy model + model = Sequential() + + # Add layers to the model + model.add( + Dense(units=64, activation="relu", input_shape=(100,)) + ) # Input layer with 100 features + model.add(Dropout(0.5)) # Dropout layer to prevent overfitting + model.add(Dense(units=32, activation="relu")) # Hidden layer + model.add(Dense(units=10, activation="softmax")) # Output layer with 10 classes + + # vgg's weights cannot be loaded to this model + with pytest.raises( + ValueError, match="An exception occurred while loading the pre-trained weights from" + ): + model = weight_utils.load_model_weights(model=model, weight_file=weight_file) + + logger.info("✅ test loading broken weight file is done") + + +@mock.patch("deepface.commons.folder_utils.get_deepface_home") # Update with your actual module +@mock.patch("gdown.download") # Mocking gdown's download function +@mock.patch("os.path.isfile") # Mocking os.path.isfile +@mock.patch("os.makedirs") # Mocking os.makedirs to avoid FileNotFoundError +@mock.patch("zipfile.ZipFile") # Mocking the ZipFile class +@mock.patch("bz2.BZ2File") # Mocking the BZ2File class +@mock.patch("builtins.open", new_callable=mock.mock_open()) # Mocking open +class TestDownloadWeightFeature: + def test_download_weights_for_available_file( + self, + mock_open, + mock_zipfile, + mock_bz2file, + mock_makedirs, + mock_isfile, + mock_gdown, + mock_get_deepface_home, + ): + mock_isfile.return_value = True + mock_get_deepface_home.return_value = "/mock/home" + + file_name = "model_weights.h5" + source_url = "http://example.com/model_weights.zip" + + result = weight_utils.download_weights_if_necessary(file_name, source_url) + + assert result == os.path.join("/mock/home", ".deepface/weights", file_name) + + mock_gdown.assert_not_called() + mock_zipfile.assert_not_called() + mock_bz2file.assert_not_called() + logger.info("✅ test download weights for available file is done") + + def test_download_weights_if_necessary_gdown_failure( + self, + mock_open, + mock_zipfile, + mock_bz2file, + mock_makedirs, + mock_isfile, + mock_gdown, + mock_get_deepface_home, + ): + # Setting up the mock return values + mock_get_deepface_home.return_value = "/mock/home" + mock_isfile.return_value = False # Simulate file not being present + + file_name = "model_weights.h5" + source_url = "http://example.com/model_weights.h5" + + # Simulate gdown.download raising an exception + mock_gdown.side_effect = Exception("Download failed!") + + # Call the function and check for ValueError + with pytest.raises( + ValueError, + match=f"⛓️‍💥 An exception occurred while downloading {file_name} from {source_url}.", + ): + weight_utils.download_weights_if_necessary(file_name, source_url) + + logger.info("✅ test for downloading weights while gdown fails done") + + def test_download_weights_if_necessary_no_compression( + self, + mock_open, + mock_zipfile, + mock_bz2file, + mock_makedirs, + mock_isfile, + mock_gdown, + mock_get_deepface_home, + ): + # Setting up the mock return values + mock_get_deepface_home.return_value = "/mock/home" + mock_isfile.return_value = False # Simulate file not being present + + file_name = "model_weights.h5" + source_url = "http://example.com/model_weights.h5" + + # Call the function + result = weight_utils.download_weights_if_necessary(file_name, source_url) + + # Assert that gdown.download was called with the correct parameters + mock_gdown.assert_called_once_with( + source_url, "/mock/home/.deepface/weights/model_weights.h5", quiet=False + ) + + # Assert that the return value is correct + assert result == "/mock/home/.deepface/weights/model_weights.h5" + + # Assert that zipfile.ZipFile and bz2.BZ2File were not called + mock_zipfile.assert_not_called() + mock_bz2file.assert_not_called() + + logger.info("✅ test download weights with no compression is done") + + def test_download_weights_if_necessary_zip( + self, + mock_open, + mock_zipfile, + mock_bz2file, + mock_makedirs, + mock_isfile, + mock_gdown, + mock_get_deepface_home, + ): + # Setting up the mock return values + mock_get_deepface_home.return_value = "/mock/home" + mock_isfile.return_value = False # Simulate file not being present + + file_name = "model_weights.h5" + source_url = "http://example.com/model_weights.zip" + compress_type = "zip" + + # Call the function + result = weight_utils.download_weights_if_necessary(file_name, source_url, compress_type) + + # Assert that gdown.download was called with the correct parameters + mock_gdown.assert_called_once_with( + source_url, "/mock/home/.deepface/weights/model_weights.h5.zip", quiet=False + ) + + # Simulate the unzipping behavior + mock_zipfile.return_value.__enter__.return_value.extractall = mock.Mock() + + # Call the function again to simulate unzipping + with mock_zipfile.return_value as zip_ref: + zip_ref.extractall("/mock/home/.deepface/weights") + + # Assert that the zip file was unzipped correctly + zip_ref.extractall.assert_called_once_with("/mock/home/.deepface/weights") + + # Assert that the return value is correct + assert result == "/mock/home/.deepface/weights/model_weights.h5" + + logger.info("✅ test download weights for zip is done") + + def test_download_weights_if_necessary_bz2( + self, + mock_open, + mock_zipfile, + mock_bz2file, + mock_makedirs, + mock_isfile, + mock_gdown, + mock_get_deepface_home, + ): + + # Setting up the mock return values + mock_get_deepface_home.return_value = "/mock/home" + mock_isfile.return_value = False # Simulate file not being present + + file_name = "model_weights.h5" + source_url = "http://example.com/model_weights.bz2" + compress_type = "bz2" + + # Simulate the download success + mock_gdown.return_value = None + + # Simulate the BZ2 file reading behavior + mock_bz2file.return_value.__enter__.return_value.read.return_value = b"fake data" + + # Call the function under test + result = weight_utils.download_weights_if_necessary(file_name, source_url, compress_type) + + # Assert that gdown.download was called with the correct parameters + mock_gdown.assert_called_once_with( + source_url, "/mock/home/.deepface/weights/model_weights.h5.bz2", quiet=False + ) + + # Ensure open() is called once for writing the decompressed data + mock_open.assert_called_once_with("/mock/home/.deepface/weights/model_weights.h5", "wb") + + # TODO: find a way to check write is called + + # Assert that the return value is correct + assert result == "/mock/home/.deepface/weights/model_weights.h5" + + logger.info("✅ test download weights for bz2 is done") + + def test_download_weights_for_non_supported_compress_type( + self, + mock_open, + mock_zipfile, + mock_bz2file, + mock_makedirs, + mock_isfile, + mock_gdown, + mock_get_deepface_home, + ): + mock_isfile.return_value = False + + file_name = "model_weights.h5" + source_url = "http://example.com/model_weights.bz2" + compress_type = "7z" + with pytest.raises(ValueError, match="unimplemented compress type - 7z"): + _ = weight_utils.download_weights_if_necessary(file_name, source_url, compress_type) + logger.info("✅ test download weights for unsupported compress type is done") diff --git a/tests/test_enforce_detection.py b/tests/test_enforce_detection.py new file mode 100644 index 0000000..5360563 --- /dev/null +++ b/tests/test_enforce_detection.py @@ -0,0 +1,49 @@ +# 3rd party dependencies +import pytest +import numpy as np + +# project dependencies +from deepface import DeepFace +from deepface.commons.logger import Logger + +logger = Logger() + + +def test_enabled_enforce_detection_for_non_facial_input(): + black_img = np.zeros([224, 224, 3]) + + with pytest.raises(ValueError): + DeepFace.represent(img_path=black_img) + + with pytest.raises(ValueError): + DeepFace.verify(img1_path=black_img, img2_path=black_img) + + logger.info("✅ enabled enforce detection with non facial input tests done") + + +def test_disabled_enforce_detection_for_non_facial_input_on_represent(): + black_img = np.zeros([224, 224, 3]) + objs = DeepFace.represent(img_path=black_img, enforce_detection=False) + + assert isinstance(objs, list) + assert len(objs) > 0 + assert isinstance(objs[0], dict) + assert "embedding" in objs[0].keys() + assert "facial_area" in objs[0].keys() + assert isinstance(objs[0]["facial_area"], dict) + assert "x" in objs[0]["facial_area"].keys() + assert "y" in objs[0]["facial_area"].keys() + assert "w" in objs[0]["facial_area"].keys() + assert "h" in objs[0]["facial_area"].keys() + assert isinstance(objs[0]["embedding"], list) + assert len(objs[0]["embedding"]) == 4096 # embedding of VGG-Face + + logger.info("✅ disabled enforce detection with non facial input test for represent tests done") + + +def test_disabled_enforce_detection_for_non_facial_input_on_verify(): + black_img = np.zeros([224, 224, 3]) + obj = DeepFace.verify(img1_path=black_img, img2_path=black_img, enforce_detection=False) + assert isinstance(obj, dict) + + logger.info("✅ disabled enforce detection with non facial input test for verify tests done") diff --git a/tests/test_extract_faces.py b/tests/test_extract_faces.py new file mode 100644 index 0000000..ba05ab4 --- /dev/null +++ b/tests/test_extract_faces.py @@ -0,0 +1,143 @@ +# built-in dependencies +import base64 + +# 3rd party dependencies +import cv2 +import numpy as np +import pytest + +# project dependencies +from deepface import DeepFace +from deepface.commons import image_utils +from deepface.commons.logger import Logger + +logger = Logger() + +detectors = ["opencv", "mtcnn", "ssd"] + + +def test_different_detectors(): + img_path = "dataset/img11.jpg" + img = cv2.imread(img_path) + height, width, _ = img.shape + + for detector in detectors: + img_objs = DeepFace.extract_faces(img_path=img_path, detector_backend=detector) + for img_obj in img_objs: + assert "face" in img_obj.keys() + assert "facial_area" in img_obj.keys() + assert isinstance(img_obj["facial_area"], dict) + assert "x" in img_obj["facial_area"].keys() + assert "y" in img_obj["facial_area"].keys() + assert "w" in img_obj["facial_area"].keys() + assert "h" in img_obj["facial_area"].keys() + # is left eye set with respect to the person instead of observer + assert "left_eye" in img_obj["facial_area"].keys() + assert "right_eye" in img_obj["facial_area"].keys() + right_eye = img_obj["facial_area"]["right_eye"] + left_eye = img_obj["facial_area"]["left_eye"] + + # left eye and right eye must be tuple + assert isinstance(left_eye, tuple) + assert isinstance(right_eye, tuple) + + # right eye should be the right eye of the person + assert left_eye[0] > right_eye[0] + + # left eye and right eye must be int not to have problem in api + assert isinstance(left_eye[0], int) + assert isinstance(left_eye[1], int) + assert isinstance(right_eye[0], int) + assert isinstance(right_eye[1], int) + + # confidence must be float, not numpy not to have problem in api + assert "confidence" in img_obj.keys() + type_conf = type(img_obj["confidence"]) + assert isinstance( + img_obj["confidence"], float + ), f"confidence type must be float but it is {type_conf}" + + # we added black pixeled borders to image because if faces are close to border, + # then alignment moves them to outside of the image. adding this borders may + # cause to miscalculate the facial area. check it is restored correctly. + x = img_obj["facial_area"]["x"] + y = img_obj["facial_area"]["y"] + w = img_obj["facial_area"]["w"] + h = img_obj["facial_area"]["h"] + + assert x < width + assert x + w < width + assert y < height + assert y + h < height + assert left_eye[0] < height + assert right_eye[0] < height + assert left_eye[1] < width + assert right_eye[1] < width + + img = img_obj["face"] + assert img.shape[0] > 0 and img.shape[1] > 0 + logger.info(f"✅ extract_faces for {detector} backend test is done") + + +def test_backends_for_enforced_detection_with_non_facial_inputs(): + black_img = np.zeros([224, 224, 3]) + for detector in detectors: + with pytest.raises(ValueError): + _ = DeepFace.extract_faces(img_path=black_img, detector_backend=detector) + logger.info("✅ extract_faces for enforced detection and non-facial image test is done") + + +def test_backends_for_not_enforced_detection_with_non_facial_inputs(): + black_img = np.zeros([224, 224, 3]) + for detector in detectors: + objs = DeepFace.extract_faces( + img_path=black_img, detector_backend=detector, enforce_detection=False + ) + assert objs[0]["face"].shape == (224, 224, 3) + logger.info("✅ extract_faces for not enforced detection and non-facial image test is done") + + +def test_file_types_while_loading_base64(): + img1_path = "dataset/img47.jpg" + img1_base64 = image_to_base64(image_path=img1_path) + + with pytest.raises(ValueError, match="Input image can be jpg or png, but it is"): + _ = image_utils.load_image_from_base64(uri=img1_base64) + + img2_path = "dataset/img1.jpg" + img2_base64 = image_to_base64(image_path=img2_path) + + img2 = image_utils.load_image_from_base64(uri=img2_base64) + # 3 dimensional image should be loaded + assert len(img2.shape) == 3 + + +def image_to_base64(image_path): + with open(image_path, "rb") as image_file: + encoded_string = base64.b64encode(image_file.read()).decode("utf-8") + return "data:image/jpeg," + encoded_string + + +def test_facial_coordinates_are_in_borders(): + img_path = "dataset/selfie-many-people.jpg" + img = cv2.imread(img_path) + height, width, _ = img.shape + + results = DeepFace.extract_faces(img_path=img_path) + + assert len(results) > 0 + + for result in results: + facial_area = result["facial_area"] + + x = facial_area["x"] + y = facial_area["y"] + w = facial_area["w"] + h = facial_area["h"] + + assert x >= 0 + assert y >= 0 + assert x + w < width + assert y + h < height + + logger.info("✅ facial area coordinates are all in image borders") diff --git a/tests/test_find.py b/tests/test_find.py new file mode 100644 index 0000000..ffea91b --- /dev/null +++ b/tests/test_find.py @@ -0,0 +1,153 @@ +# built-in dependencies +import os + +# 3rd party dependencies +import cv2 +import pandas as pd + +# project dependencies +from deepface import DeepFace +from deepface.modules import verification +from deepface.commons import image_utils +from deepface.commons.logger import Logger + +logger = Logger() + + +threshold = verification.find_threshold(model_name="VGG-Face", distance_metric="cosine") + + +def test_find_with_exact_path(): + img_path = os.path.join("dataset", "img1.jpg") + dfs = DeepFace.find(img_path=img_path, db_path="dataset", silent=True) + assert len(dfs) > 0 + for df in dfs: + assert isinstance(df, pd.DataFrame) + + # one is img1.jpg itself + identity_df = df[df["identity"] == img_path] + assert identity_df.shape[0] > 0 + + # validate reproducability + assert identity_df["distance"].values[0] < threshold + + df = df[df["identity"] != img_path] + logger.debug(df.head()) + assert df.shape[0] > 0 + logger.info("✅ test find for exact path done") + + +def test_find_with_array_input(): + img_path = os.path.join("dataset", "img1.jpg") + img1 = cv2.imread(img_path) + dfs = DeepFace.find(img1, db_path="dataset", silent=True) + assert len(dfs) > 0 + for df in dfs: + assert isinstance(df, pd.DataFrame) + + # one is img1.jpg itself + identity_df = df[df["identity"] == img_path] + assert identity_df.shape[0] > 0 + + # validate reproducability + assert identity_df["distance"].values[0] < threshold + + df = df[df["identity"] != img_path] + logger.debug(df.head()) + assert df.shape[0] > 0 + + logger.info("✅ test find for array input done") + + +def test_find_with_extracted_faces(): + img_path = os.path.join("dataset", "img1.jpg") + face_objs = DeepFace.extract_faces(img_path) + img = face_objs[0]["face"] + dfs = DeepFace.find(img, db_path="dataset", detector_backend="skip", silent=True) + assert len(dfs) > 0 + for df in dfs: + assert isinstance(df, pd.DataFrame) + + # one is img1.jpg itself + identity_df = df[df["identity"] == img_path] + assert identity_df.shape[0] > 0 + + # validate reproducability + assert identity_df["distance"].values[0] < threshold + + df = df[df["identity"] != img_path] + logger.debug(df.head()) + assert df.shape[0] > 0 + logger.info("✅ test find for extracted face input done") + + +def test_filetype_for_find(): + """ + only images as jpg and png can be loaded into database + """ + img_path = os.path.join("dataset", "img1.jpg") + dfs = DeepFace.find(img_path=img_path, db_path="dataset", silent=True) + + df = dfs[0] + + # img47 is webp even though its extension is jpg + assert df[df["identity"] == "dataset/img47.jpg"].shape[0] == 0 + + +def test_filetype_for_find_bulk_embeddings(): + imgs = image_utils.list_images("dataset") + + assert len(imgs) > 0 + + # img47 is webp even though its extension is jpg + assert "dataset/img47.jpg" not in imgs + + +def test_find_without_refresh_database(): + import shutil, hashlib + + img_path = os.path.join("dataset", "img1.jpg") + + # 1. Calculate hash of the .pkl file; + # 2. Move random image to the temporary created directory; + # 3. As a result, there will be a difference between the .pkl file and the disk files; + # 4. If refresh_database=False, then .pkl file should not be updated. + # Recalculate hash and compare it with the hash from pt. 1; + # 5. After successful check, the image will be moved back to the original destination; + + pkl_path = "dataset/ds_model_vggface_detector_opencv_aligned_normalization_base_expand_0.pkl" + with open(pkl_path, "rb") as f: + hash_before = hashlib.sha256(f.read()) + + image_name = "img28.jpg" + tmp_dir = "dataset/temp_image" + os.mkdir(tmp_dir) + shutil.move(os.path.join("dataset", image_name), os.path.join(tmp_dir, image_name)) + + dfs = DeepFace.find(img_path=img_path, db_path="dataset", silent=True, refresh_database=False) + + with open(pkl_path, "rb") as f: + hash_after = hashlib.sha256(f.read()) + + shutil.move(os.path.join(tmp_dir, image_name), os.path.join("dataset", image_name)) + os.rmdir(tmp_dir) + + assert hash_before.hexdigest() == hash_after.hexdigest() + + logger.info("✅ .pkl hashes before and after the recognition process are the same") + + assert len(dfs) > 0 + for df in dfs: + assert isinstance(df, pd.DataFrame) + + # one is img1.jpg itself + identity_df = df[df["identity"] == img_path] + assert identity_df.shape[0] > 0 + + # validate reproducability + assert identity_df["distance"].values[0] < threshold + + df = df[df["identity"] != img_path] + logger.debug(df.head()) + assert df.shape[0] > 0 + logger.info("✅ test find without refresh database done") diff --git a/tests/test_represent.py b/tests/test_represent.py new file mode 100644 index 0000000..085dff2 --- /dev/null +++ b/tests/test_represent.py @@ -0,0 +1,62 @@ +# built-in dependencies +import cv2 + +# project dependencies +from deepface import DeepFace +from deepface.commons.logger import Logger + +logger = Logger() + + +def test_standard_represent(): + img_path = "dataset/img1.jpg" + embedding_objs = DeepFace.represent(img_path) + for embedding_obj in embedding_objs: + embedding = embedding_obj["embedding"] + logger.debug(f"Function returned {len(embedding)} dimensional vector") + assert len(embedding) == 4096 + logger.info("✅ test standard represent function done") + + +def test_represent_for_skipped_detector_backend_with_image_path(): + face_img = "dataset/img5.jpg" + img_objs = DeepFace.represent(img_path=face_img, detector_backend="skip") + assert len(img_objs) >= 1 + img_obj = img_objs[0] + assert "embedding" in img_obj.keys() + assert "facial_area" in img_obj.keys() + assert isinstance(img_obj["facial_area"], dict) + assert "x" in img_obj["facial_area"].keys() + assert "y" in img_obj["facial_area"].keys() + assert "w" in img_obj["facial_area"].keys() + assert "h" in img_obj["facial_area"].keys() + assert "face_confidence" in img_obj.keys() + logger.info("✅ test represent function for skipped detector and image path input backend done") + + +def test_represent_for_skipped_detector_backend_with_preloaded_image(): + face_img = "dataset/img5.jpg" + img = cv2.imread(face_img) + img_objs = DeepFace.represent(img_path=img, detector_backend="skip") + assert len(img_objs) >= 1 + img_obj = img_objs[0] + assert "embedding" in img_obj.keys() + assert "facial_area" in img_obj.keys() + assert isinstance(img_obj["facial_area"], dict) + assert "x" in img_obj["facial_area"].keys() + assert "y" in img_obj["facial_area"].keys() + assert "w" in img_obj["facial_area"].keys() + assert "h" in img_obj["facial_area"].keys() + assert "face_confidence" in img_obj.keys() + logger.info("✅ test represent function for skipped detector and preloaded image done") + + +def test_max_faces(): + # confirm that input image has more than one face + results = DeepFace.represent(img_path="dataset/couple.jpg") + assert len(results) > 1 + + # test it with max faces arg + max_faces = 1 + results = DeepFace.represent(img_path="dataset/couple.jpg", max_faces=max_faces) + assert len(results) == max_faces diff --git a/tests/test_singleton.py b/tests/test_singleton.py new file mode 100644 index 0000000..f2e4ea1 --- /dev/null +++ b/tests/test_singleton.py @@ -0,0 +1,8 @@ +from deepface.commons.logger import Logger + +logger = Logger() + + +def test_singleton_same_object(): + assert Logger() == Logger() + logger.info("✅ id's of instances of \"singletoned\" class Logger are the same") diff --git a/tests/test_verify.py b/tests/test_verify.py new file mode 100644 index 0000000..2a6951b --- /dev/null +++ b/tests/test_verify.py @@ -0,0 +1,190 @@ +# 3rd party dependencies +import pytest +import cv2 + +# project dependencies +from deepface import DeepFace +from deepface.commons.logger import Logger + +logger = Logger() + +models = ["VGG-Face", "Facenet", "Facenet512", "ArcFace", "GhostFaceNet"] +metrics = ["cosine", "euclidean", "euclidean_l2"] +detectors = ["opencv", "mtcnn"] + + +def test_different_facial_recognition_models(): + dataset = [ + ["dataset/img1.jpg", "dataset/img2.jpg", True], + ["dataset/img5.jpg", "dataset/img6.jpg", True], + ["dataset/img6.jpg", "dataset/img7.jpg", True], + ["dataset/img8.jpg", "dataset/img9.jpg", True], + ["dataset/img1.jpg", "dataset/img11.jpg", True], + ["dataset/img2.jpg", "dataset/img11.jpg", True], + ["dataset/img1.jpg", "dataset/img3.jpg", False], + ["dataset/img2.jpg", "dataset/img3.jpg", False], + ["dataset/img6.jpg", "dataset/img8.jpg", False], + ["dataset/img6.jpg", "dataset/img9.jpg", False], + ] + + expected_coverage = 97.53 # human level accuracy on LFW + successful_tests = 0 + unsuccessful_tests = 0 + for model in models: + for metric in metrics: + for instance in dataset: + img1 = instance[0] + img2 = instance[1] + result = instance[2] + + resp_obj = DeepFace.verify(img1, img2, model_name=model, distance_metric=metric) + + prediction = resp_obj["verified"] + distance = round(resp_obj["distance"], 2) + threshold = resp_obj["threshold"] + + if prediction is result: + test_result_label = "✅" + successful_tests += 1 + else: + test_result_label = "❌" + unsuccessful_tests += 1 + + if prediction is True: + classified_label = "same person" + else: + classified_label = "different persons" + + img1_alias = img1.split("/", maxsplit=1)[-1] + img2_alias = img2.split("/", maxsplit=1)[-1] + + logger.debug( + f"{test_result_label} Pair {img1_alias}-{img2_alias}" + f" is {classified_label} based on {model}-{metric}" + f" (Distance: {distance}, Threshold: {threshold})", + ) + + coverage_score = (100 * successful_tests) / (successful_tests + unsuccessful_tests) + assert ( + coverage_score > expected_coverage + ), f"⛔ facial recognition models test failed with {coverage_score} score" + + logger.info(f"✅ facial recognition models test passed with {coverage_score}") + + +def test_different_face_detectors(): + for detector in detectors: + res = DeepFace.verify("dataset/img1.jpg", "dataset/img2.jpg", detector_backend=detector) + assert isinstance(res, dict) + assert "verified" in res.keys() + assert res["verified"] in [True, False] + assert "distance" in res.keys() + assert "threshold" in res.keys() + assert "model" in res.keys() + assert "detector_backend" in res.keys() + assert "similarity_metric" in res.keys() + assert "facial_areas" in res.keys() + assert "img1" in res["facial_areas"].keys() + assert "img2" in res["facial_areas"].keys() + assert "x" in res["facial_areas"]["img1"].keys() + assert "y" in res["facial_areas"]["img1"].keys() + assert "w" in res["facial_areas"]["img1"].keys() + assert "h" in res["facial_areas"]["img1"].keys() + assert "x" in res["facial_areas"]["img2"].keys() + assert "y" in res["facial_areas"]["img2"].keys() + assert "w" in res["facial_areas"]["img2"].keys() + assert "h" in res["facial_areas"]["img2"].keys() + logger.info(f"✅ test verify for {detector} backend done") + + +def test_verify_for_preloaded_image(): + img1 = cv2.imread("dataset/img1.jpg") + img2 = cv2.imread("dataset/img2.jpg") + res = DeepFace.verify(img1, img2) + assert res["verified"] is True + logger.info("✅ test verify for pre-loaded image done") + + +def test_verify_for_precalculated_embeddings(): + model_name = "Facenet" + + img1_path = "dataset/img1.jpg" + img2_path = "dataset/img2.jpg" + + img1_embedding = DeepFace.represent(img_path=img1_path, model_name=model_name)[0]["embedding"] + img2_embedding = DeepFace.represent(img_path=img2_path, model_name=model_name)[0]["embedding"] + + result = DeepFace.verify( + img1_path=img1_embedding, img2_path=img2_embedding, model_name=model_name, silent=True + ) + + assert result["verified"] is True + assert result["distance"] < result["threshold"] + assert result["model"] == model_name + assert result["facial_areas"]["img1"] is not None + assert result["facial_areas"]["img2"] is not None + + assert isinstance(result["facial_areas"]["img1"], dict) + assert isinstance(result["facial_areas"]["img2"], dict) + + assert "x" in result["facial_areas"]["img1"].keys() + assert "y" in result["facial_areas"]["img1"].keys() + assert "w" in result["facial_areas"]["img1"].keys() + assert "h" in result["facial_areas"]["img1"].keys() + assert "left_eye" in result["facial_areas"]["img1"].keys() + assert "right_eye" in result["facial_areas"]["img1"].keys() + + assert "x" in result["facial_areas"]["img2"].keys() + assert "y" in result["facial_areas"]["img2"].keys() + assert "w" in result["facial_areas"]["img2"].keys() + assert "h" in result["facial_areas"]["img2"].keys() + assert "left_eye" in result["facial_areas"]["img2"].keys() + assert "right_eye" in result["facial_areas"]["img2"].keys() + + logger.info("✅ test verify for pre-calculated embeddings done") + + +def test_verify_with_precalculated_embeddings_for_incorrect_model(): + # generate embeddings with VGG (default) + img1_path = "dataset/img1.jpg" + img2_path = "dataset/img2.jpg" + img1_embedding = DeepFace.represent(img_path=img1_path)[0]["embedding"] + img2_embedding = DeepFace.represent(img_path=img2_path)[0]["embedding"] + + with pytest.raises( + ValueError, + match="embeddings of Facenet should have 128 dimensions, but 1-th image has 4096 dimensions input", + ): + _ = DeepFace.verify( + img1_path=img1_embedding, img2_path=img2_embedding, model_name="Facenet", silent=True + ) + + logger.info("✅ test verify with pre-calculated embeddings for incorrect model done") + + +def test_verify_for_broken_embeddings(): + img1_embeddings = ["a", "b", "c"] + img2_embeddings = [1, 2, 3] + + with pytest.raises( + ValueError, + match="When passing img1_path as a list, ensure that all its items are of type float.", + ): + _ = DeepFace.verify(img1_path=img1_embeddings, img2_path=img2_embeddings) + logger.info("✅ test verify for broken embeddings content is done") + + +def test_verify_for_nested_embeddings(): + """ + batch embeddings not supported + """ + img1_embeddings = [[1, 2, 3], [4, 5, 6]] + img2_path = "dataset/img1.jpg" + + with pytest.raises( + ValueError, + match="When passing img1_path as a list, ensure that all its items are of type float", + ): + _ = DeepFace.verify(img1_path=img1_embeddings, img2_path=img2_path) + + logger.info("✅ test verify for nested embeddings is done") diff --git a/tests/test_version.py b/tests/test_version.py new file mode 100644 index 0000000..21e0065 --- /dev/null +++ b/tests/test_version.py @@ -0,0 +1,16 @@ +# built-in dependencies +import json + +# project dependencies +from deepface import DeepFace +from deepface.commons.logger import Logger + +logger = Logger() + + +def test_version(): + with open("../package_info.json", "r", encoding="utf-8") as f: + package_info = json.load(f) + + assert DeepFace.__version__ == package_info["version"] + logger.info("✅ versions are matching in both package_info.json and deepface/__init__.py") diff --git a/tests/visual-test.py b/tests/visual-test.py new file mode 100644 index 0000000..9149bc5 --- /dev/null +++ b/tests/visual-test.py @@ -0,0 +1,110 @@ +# 3rd party dependencies +import matplotlib.pyplot as plt + +# project dependencies +from deepface import DeepFace +from deepface.commons.logger import Logger + +logger = Logger() + +# some models (e.g. Dlib) and detectors (e.g. retinaface) do not have test cases +# because they require to install huge packages +# this module is for local runs + +model_names = [ + "VGG-Face", + "Facenet", + "Facenet512", + "OpenFace", + "DeepFace", + "DeepID", + "Dlib", + "ArcFace", + "SFace", + "GhostFaceNet", +] + +detector_backends = [ + "opencv", + "ssd", + "dlib", + "mtcnn", + "fastmtcnn", + # "mediapipe", # crashed in mac + "retinaface", + "yunet", + "yolov8", + "centerface", +] + +# verification +for model_name in model_names: + obj = DeepFace.verify( + img1_path="dataset/img1.jpg", img2_path="dataset/img2.jpg", model_name=model_name + ) + logger.info(obj) + logger.info("---------------------") + +# represent +for model_name in model_names: + embedding_objs = DeepFace.represent(img_path="dataset/img1.jpg", model_name=model_name) + for embedding_obj in embedding_objs: + embedding = embedding_obj["embedding"] + logger.info(f"{model_name} produced {len(embedding)}D vector") + + +# find +dfs = DeepFace.find( + img_path="dataset/img1.jpg", db_path="dataset", model_name="Facenet", detector_backend="mtcnn" +) +for df in dfs: + logger.info(df) + +expand_areas = [0] +img_paths = ["dataset/img11.jpg", "dataset/img11_reflection.jpg"] +for expand_area in expand_areas: + for img_path in img_paths: + # extract faces + for detector_backend in detector_backends: + face_objs = DeepFace.extract_faces( + img_path=img_path, + detector_backend=detector_backend, + align=True, + expand_percentage=expand_area, + ) + for face_obj in face_objs: + face = face_obj["face"] + logger.info(f"testing {img_path} with {detector_backend}") + logger.info(face_obj["facial_area"]) + logger.info(face_obj["confidence"]) + + # we know opencv sometimes cannot find eyes + if face_obj["facial_area"]["left_eye"] is not None: + assert isinstance(face_obj["facial_area"]["left_eye"], tuple) + assert isinstance(face_obj["facial_area"]["left_eye"][0], int) + assert isinstance(face_obj["facial_area"]["left_eye"][1], int) + + if face_obj["facial_area"]["right_eye"] is not None: + assert isinstance(face_obj["facial_area"]["right_eye"], tuple) + assert isinstance(face_obj["facial_area"]["right_eye"][0], int) + assert isinstance(face_obj["facial_area"]["right_eye"][1], int) + + # left eye is really the left eye of the person + if ( + face_obj["facial_area"]["left_eye"] is not None + and face_obj["facial_area"]["right_eye"] is not None + ): + re_x = face_obj["facial_area"]["right_eye"][0] + le_x = face_obj["facial_area"]["left_eye"][0] + assert re_x < le_x, "right eye must be the right eye of the person" + + type_conf = type(face_obj["confidence"]) + assert isinstance( + face_obj["confidence"], float + ), f"confidence type must be float but it is {type_conf}" + assert face_obj["confidence"] <= 1 + + plt.imshow(face) + plt.axis("off") + plt.show() + logger.info("-----------")