diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..29b8acd44e7e7c0142f5232f83b648cbc1379242
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,16 @@
+**/__pycache__
+**/.DS_Store
+build/
+dist/
+Pipfile
+Pipfile.lock
+.mypy_cache/
+.idea/
+deepface.egg-info/
+tests/dataset/*.pkl
+tests/*.ipynb
+tests/*.csv
+*.pyc
+**/.coverage
+**/.coverage.*
+.env
\ No newline at end of file
diff --git a/.pylintrc b/.pylintrc
new file mode 100644
index 0000000000000000000000000000000000000000..e44c3350e5107289fe6b6bc092cbea2043e1780c
--- /dev/null
+++ b/.pylintrc
@@ -0,0 +1,641 @@
+[MAIN]
+
+# Analyse import fallback blocks. This can be used to support both Python 2 and
+# 3 compatible code, which means that the block might have code that exists
+# only in one or another interpreter, leading to false positives when analysed.
+analyse-fallback-blocks=no
+
+# Load and enable all available extensions. Use --list-extensions to see a list
+# all available extensions.
+#enable-all-extensions=
+
+# In error mode, messages with a category besides ERROR or FATAL are
+# suppressed, and no reports are done by default. Error mode is compatible with
+# disabling specific errors.
+#errors-only=
+
+# Always return a 0 (non-error) status code, even if lint errors are found.
+# This is primarily useful in continuous integration scripts.
+#exit-zero=
+
+# A comma-separated list of package or module names from where C extensions may
+# be loaded. Extensions are loading into the active Python interpreter and may
+# run arbitrary code.
+extension-pkg-allow-list=
+
+# A comma-separated list of package or module names from where C extensions may
+# be loaded. Extensions are loading into the active Python interpreter and may
+# run arbitrary code. (This is an alternative name to extension-pkg-allow-list
+# for backward compatibility.)
+extension-pkg-whitelist=
+
+# Return non-zero exit code if any of these messages/categories are detected,
+# even if score is above --fail-under value. Syntax same as enable. Messages
+# specified are enabled, while categories only check already-enabled messages.
+fail-on=
+
+# Specify a score threshold under which the program will exit with error.
+fail-under=10
+
+# Interpret the stdin as a python script, whose filename needs to be passed as
+# the module_or_package argument.
+#from-stdin=
+
+# Files or directories to be skipped. They should be base names, not paths.
+ignore=CVS
+
+# Add files or directories matching the regular expressions patterns to the
+# ignore-list. The regex matches against paths and can be in Posix or Windows
+# format. Because '\' represents the directory delimiter on Windows systems, it
+# can't be used as an escape character.
+ignore-paths=
+
+# Files or directories matching the regular expression patterns are skipped.
+# The regex matches against base names, not paths. The default value ignores
+# Emacs file locks
+ignore-patterns=^\.#
+
+# List of module names for which member attributes should not be checked
+# (useful for modules/projects where namespaces are manipulated during runtime
+# and thus existing member attributes cannot be deduced by static analysis). It
+# supports qualified module names, as well as Unix pattern matching.
+ignored-modules=
+
+# Python code to execute, usually for sys.path manipulation such as
+# pygtk.require().
+#init-hook=
+
+# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
+# number of processors available to use, and will cap the count on Windows to
+# avoid hangs.
+jobs=1
+
+# Control the amount of potential inferred values when inferring a single
+# object. This can help the performance when dealing with large functions or
+# complex, nested conditions.
+limit-inference-results=100
+
+# List of plugins (as comma separated values of python module names) to load,
+# usually to register additional checkers.
+load-plugins=
+
+# Pickle collected data for later comparisons.
+persistent=yes
+
+# Minimum Python version to use for version dependent checks. Will default to
+# the version used to run pylint.
+py-version=3.9
+
+# Discover python modules and packages in the file system subtree.
+recursive=no
+
+# When enabled, pylint would attempt to guess common misconfiguration and emit
+# user-friendly hints instead of false-positive error messages.
+suggestion-mode=yes
+
+# Allow loading of arbitrary C extensions. Extensions are imported into the
+# active Python interpreter and may run arbitrary code.
+unsafe-load-any-extension=no
+
+# In verbose mode, extra non-checker-related info will be displayed.
+#verbose=
+
+
+[BASIC]
+
+# Naming style matching correct argument names.
+argument-naming-style=snake_case
+
+# Regular expression matching correct argument names. Overrides argument-
+# naming-style. If left empty, argument names will be checked with the set
+# naming style.
+#argument-rgx=
+
+# Naming style matching correct attribute names.
+attr-naming-style=snake_case
+
+# Regular expression matching correct attribute names. Overrides attr-naming-
+# style. If left empty, attribute names will be checked with the set naming
+# style.
+#attr-rgx=
+
+# Bad variable names which should always be refused, separated by a comma.
+bad-names=foo,
+ bar,
+ baz,
+ toto,
+ tutu,
+ tata
+
+# Bad variable names regexes, separated by a comma. If names match any regex,
+# they will always be refused
+bad-names-rgxs=
+
+# Naming style matching correct class attribute names.
+class-attribute-naming-style=any
+
+# Regular expression matching correct class attribute names. Overrides class-
+# attribute-naming-style. If left empty, class attribute names will be checked
+# with the set naming style.
+#class-attribute-rgx=
+
+# Naming style matching correct class constant names.
+class-const-naming-style=UPPER_CASE
+
+# Regular expression matching correct class constant names. Overrides class-
+# const-naming-style. If left empty, class constant names will be checked with
+# the set naming style.
+#class-const-rgx=
+
+# Naming style matching correct class names.
+class-naming-style=PascalCase
+
+# Regular expression matching correct class names. Overrides class-naming-
+# style. If left empty, class names will be checked with the set naming style.
+#class-rgx=
+
+# Naming style matching correct constant names.
+const-naming-style=UPPER_CASE
+
+# Regular expression matching correct constant names. Overrides const-naming-
+# style. If left empty, constant names will be checked with the set naming
+# style.
+#const-rgx=
+
+# Minimum line length for functions/classes that require docstrings, shorter
+# ones are exempt.
+docstring-min-length=-1
+
+# Naming style matching correct function names.
+function-naming-style=snake_case
+
+# Regular expression matching correct function names. Overrides function-
+# naming-style. If left empty, function names will be checked with the set
+# naming style.
+#function-rgx=
+
+# Good variable names which should always be accepted, separated by a comma.
+good-names=i,
+ j,
+ k,
+ ex,
+ Run,
+ _
+
+# Good variable names regexes, separated by a comma. If names match any regex,
+# they will always be accepted
+good-names-rgxs=
+
+# Include a hint for the correct naming format with invalid-name.
+include-naming-hint=no
+
+# Naming style matching correct inline iteration names.
+inlinevar-naming-style=any
+
+# Regular expression matching correct inline iteration names. Overrides
+# inlinevar-naming-style. If left empty, inline iteration names will be checked
+# with the set naming style.
+#inlinevar-rgx=
+
+# Naming style matching correct method names.
+method-naming-style=snake_case
+
+# Regular expression matching correct method names. Overrides method-naming-
+# style. If left empty, method names will be checked with the set naming style.
+#method-rgx=
+
+# Naming style matching correct module names.
+module-naming-style=snake_case
+
+# Regular expression matching correct module names. Overrides module-naming-
+# style. If left empty, module names will be checked with the set naming style.
+#module-rgx=
+
+# Colon-delimited sets of names that determine each other's naming style when
+# the name regexes allow several styles.
+name-group=
+
+# Regular expression which should only match function or class names that do
+# not require a docstring.
+no-docstring-rgx=^_
+
+# List of decorators that produce properties, such as abc.abstractproperty. Add
+# to this list to register other decorators that produce valid properties.
+# These decorators are taken in consideration only for invalid-name.
+property-classes=abc.abstractproperty
+
+# Regular expression matching correct type variable names. If left empty, type
+# variable names will be checked with the set naming style.
+#typevar-rgx=
+
+# Naming style matching correct variable names.
+variable-naming-style=snake_case
+
+# Regular expression matching correct variable names. Overrides variable-
+# naming-style. If left empty, variable names will be checked with the set
+# naming style.
+#variable-rgx=
+
+
+[CLASSES]
+
+# Warn about protected attribute access inside special methods
+check-protected-access-in-special-methods=no
+
+# List of method names used to declare (i.e. assign) instance attributes.
+defining-attr-methods=__init__,
+ __new__,
+ setUp,
+ __post_init__
+
+# List of member names, which should be excluded from the protected access
+# warning.
+exclude-protected=_asdict,
+ _fields,
+ _replace,
+ _source,
+ _make
+
+# List of valid names for the first argument in a class method.
+valid-classmethod-first-arg=cls
+
+# List of valid names for the first argument in a metaclass class method.
+valid-metaclass-classmethod-first-arg=cls
+
+
+[DESIGN]
+
+# List of regular expressions of class ancestor names to ignore when counting
+# public methods (see R0903)
+exclude-too-few-public-methods=
+
+# List of qualified class names to ignore when counting class parents (see
+# R0901)
+ignored-parents=
+
+# Maximum number of arguments for function / method.
+max-args=5
+
+# Maximum number of attributes for a class (see R0902).
+max-attributes=7
+
+# Maximum number of boolean expressions in an if statement (see R0916).
+max-bool-expr=5
+
+# Maximum number of branch for function / method body.
+max-branches=12
+
+# Maximum number of locals for function / method body.
+max-locals=15
+
+# Maximum number of parents for a class (see R0901).
+max-parents=7
+
+# Maximum number of public methods for a class (see R0904).
+max-public-methods=20
+
+# Maximum number of return / yield for function / method body.
+max-returns=6
+
+# Maximum number of statements in function / method body.
+max-statements=50
+
+# Minimum number of public methods for a class (see R0903).
+min-public-methods=2
+
+
+[EXCEPTIONS]
+
+# Exceptions that will emit a warning when caught.
+overgeneral-exceptions=BaseException,
+ Exception
+
+
+[FORMAT]
+
+# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
+expected-line-ending-format=
+
+# Regexp for a line that is allowed to be longer than the limit.
+ignore-long-lines=^\s*(# )??$
+
+# Number of spaces of indent required inside a hanging or continued line.
+indent-after-paren=4
+
+# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
+# tab).
+indent-string=' '
+
+# Maximum number of characters on a single line.
+max-line-length=100
+
+# Maximum number of lines in a module.
+max-module-lines=1000
+
+# Allow the body of a class to be on the same line as the declaration if body
+# contains single statement.
+single-line-class-stmt=no
+
+# Allow the body of an if to be on the same line as the test if there is no
+# else.
+single-line-if-stmt=no
+
+
+[IMPORTS]
+
+# List of modules that can be imported at any level, not just the top level
+# one.
+allow-any-import-level=
+
+# Allow wildcard imports from modules that define __all__.
+allow-wildcard-with-all=no
+
+# Deprecated modules which should not be used, separated by a comma.
+deprecated-modules=
+
+# Output a graph (.gv or any supported image format) of external dependencies
+# to the given file (report RP0402 must not be disabled).
+ext-import-graph=
+
+# Output a graph (.gv or any supported image format) of all (i.e. internal and
+# external) dependencies to the given file (report RP0402 must not be
+# disabled).
+import-graph=
+
+# Output a graph (.gv or any supported image format) of internal dependencies
+# to the given file (report RP0402 must not be disabled).
+int-import-graph=
+
+# Force import order to recognize a module as part of the standard
+# compatibility libraries.
+known-standard-library=
+
+# Force import order to recognize a module as part of a third party library.
+known-third-party=enchant
+
+# Couples of modules and preferred modules, separated by a comma.
+preferred-modules=
+
+
+[LOGGING]
+
+# The type of string formatting that logging methods do. `old` means using %
+# formatting, `new` is for `{}` formatting.
+logging-format-style=old
+
+# Logging modules to check that the string format arguments are in logging
+# function parameter format.
+logging-modules=logging
+
+
+[MESSAGES CONTROL]
+
+# Only show warnings with the listed confidence levels. Leave empty to show
+# all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE,
+# UNDEFINED.
+confidence=HIGH,
+ CONTROL_FLOW,
+ INFERENCE,
+ INFERENCE_FAILURE,
+ UNDEFINED
+
+# Disable the message, report, category or checker with the given id(s). You
+# can either give multiple identifiers separated by comma (,) or put this
+# option multiple times (only on the command line, not in the configuration
+# file where it should appear only once). You can also use "--disable=all" to
+# disable everything first and then re-enable specific checks. For example, if
+# you want to run only the similarities checker, you can use "--disable=all
+# --enable=similarities". If you want to run only the classes checker, but have
+# no Warning level messages displayed, use "--disable=all --enable=classes
+# --disable=W".
+disable=raw-checker-failed,
+ bad-inline-option,
+ locally-disabled,
+ file-ignored,
+ suppressed-message,
+ useless-suppression,
+ deprecated-pragma,
+ use-symbolic-message-instead,
+ import-error,
+ invalid-name,
+ missing-module-docstring,
+ missing-function-docstring,
+ missing-class-docstring,
+ too-many-arguments,
+ too-many-locals,
+ too-many-branches,
+ too-many-statements,
+ global-variable-undefined,
+ import-outside-toplevel,
+ singleton-comparison,
+ too-many-lines,
+ duplicate-code,
+ bare-except,
+ cyclic-import,
+ global-statement,
+ no-member,
+ no-name-in-module,
+ unrecognized-option,
+ consider-using-dict-items,
+ consider-iterating-dictionary,
+ unexpected-keyword-arg
+
+# Enable the message, report, category or checker with the given id(s). You can
+# either give multiple identifier separated by comma (,) or put this option
+# multiple time (only on the command line, not in the configuration file where
+# it should appear only once). See also the "--disable" option for examples.
+enable=c-extension-no-member
+
+
+[METHOD_ARGS]
+
+# List of qualified names (i.e., library.method) which require a timeout
+# parameter e.g. 'requests.api.get,requests.api.post'
+timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request
+
+
+[MISCELLANEOUS]
+
+# List of note tags to take in consideration, separated by a comma.
+notes=FIXME,
+ XXX,
+ TODO
+
+# Regular expression of note tags to take in consideration.
+notes-rgx=
+
+
+[REFACTORING]
+
+# Maximum number of nested blocks for function / method body
+max-nested-blocks=5
+
+# Complete name of functions that never returns. When checking for
+# inconsistent-return-statements if a never returning function is called then
+# it will be considered as an explicit return statement and no message will be
+# printed.
+never-returning-functions=sys.exit,argparse.parse_error
+
+
+[REPORTS]
+
+# Python expression which should return a score less than or equal to 10. You
+# have access to the variables 'fatal', 'error', 'warning', 'refactor',
+# 'convention', and 'info' which contain the number of messages in each
+# category, as well as 'statement' which is the total number of statements
+# analyzed. This score is used by the global evaluation report (RP0004).
+evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10))
+
+# Template used to display messages. This is a python new-style format string
+# used to format the message information. See doc for all details.
+msg-template=
+
+# Set the output format. Available formats are text, parseable, colorized, json
+# and msvs (visual studio). You can also give a reporter class, e.g.
+# mypackage.mymodule.MyReporterClass.
+#output-format=
+
+# Tells whether to display a full report or only the messages.
+reports=no
+
+# Activate the evaluation score.
+score=yes
+
+
+[SIMILARITIES]
+
+# Comments are removed from the similarity computation
+ignore-comments=yes
+
+# Docstrings are removed from the similarity computation
+ignore-docstrings=yes
+
+# Imports are removed from the similarity computation
+ignore-imports=yes
+
+# Signatures are removed from the similarity computation
+ignore-signatures=yes
+
+# Minimum lines number of a similarity.
+min-similarity-lines=4
+
+
+[SPELLING]
+
+# Limits count of emitted suggestions for spelling mistakes.
+max-spelling-suggestions=4
+
+# Spelling dictionary name. Available dictionaries: none. To make it work,
+# install the 'python-enchant' package.
+spelling-dict=
+
+# List of comma separated words that should be considered directives if they
+# appear at the beginning of a comment and should not be checked.
+spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy:
+
+# List of comma separated words that should not be checked.
+spelling-ignore-words=
+
+# A path to a file that contains the private dictionary; one word per line.
+spelling-private-dict-file=
+
+# Tells whether to store unknown words to the private dictionary (see the
+# --spelling-private-dict-file option) instead of raising a message.
+spelling-store-unknown-words=no
+
+
+[STRING]
+
+# This flag controls whether inconsistent-quotes generates a warning when the
+# character used as a quote delimiter is used inconsistently within a module.
+check-quote-consistency=no
+
+# This flag controls whether the implicit-str-concat should generate a warning
+# on implicit string concatenation in sequences defined over several lines.
+check-str-concat-over-line-jumps=no
+
+
+[TYPECHECK]
+
+# List of decorators that produce context managers, such as
+# contextlib.contextmanager. Add to this list to register other decorators that
+# produce valid context managers.
+contextmanager-decorators=contextlib.contextmanager
+
+# List of members which are set dynamically and missed by pylint inference
+# system, and so shouldn't trigger E1101 when accessed. Python regular
+# expressions are accepted.
+generated-members=
+
+# Tells whether to warn about missing members when the owner of the attribute
+# is inferred to be None.
+ignore-none=yes
+
+# This flag controls whether pylint should warn about no-member and similar
+# checks whenever an opaque object is returned when inferring. The inference
+# can return multiple potential results while evaluating a Python object, but
+# some branches might not be evaluated, which results in partial inference. In
+# that case, it might be useful to still emit no-member and other checks for
+# the rest of the inferred objects.
+ignore-on-opaque-inference=yes
+
+# List of symbolic message names to ignore for Mixin members.
+ignored-checks-for-mixins=no-member,
+ not-async-context-manager,
+ not-context-manager,
+ attribute-defined-outside-init
+
+# List of class names for which member attributes should not be checked (useful
+# for classes with dynamically set attributes). This supports the use of
+# qualified names.
+ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace
+
+# Show a hint with possible names when a member name was not found. The aspect
+# of finding the hint is based on edit distance.
+missing-member-hint=yes
+
+# The minimum edit distance a name should have in order to be considered a
+# similar match for a missing member name.
+missing-member-hint-distance=1
+
+# The total number of similar names that should be taken in consideration when
+# showing a hint for a missing member.
+missing-member-max-choices=1
+
+# Regex pattern to define which classes are considered mixins.
+mixin-class-rgx=.*[Mm]ixin
+
+# List of decorators that change the signature of a decorated function.
+signature-mutators=
+
+
+[VARIABLES]
+
+# List of additional names supposed to be defined in builtins. Remember that
+# you should avoid defining new builtins when possible.
+additional-builtins=
+
+# Tells whether unused global variables should be treated as a violation.
+allow-global-unused-variables=yes
+
+# List of names allowed to shadow builtins
+allowed-redefined-builtins=
+
+# List of strings which can identify a callback function by name. A callback
+# name must start or end with one of those strings.
+callbacks=cb_,
+ _cb
+
+# A regular expression matching the name of dummy variables (i.e. expected to
+# not be used).
+dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
+
+# Argument names that match this expression will be ignored.
+ignored-argument-names=_.*|^ignored_|^unused_
+
+# Tells whether we should check for unused import in __init__ files.
+init-import=no
+
+# List of qualified module names which can have objects that can redefine
+# builtins.
+redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000000000000000000000000000000000000..40792689659e6267ccb988c0156437925a923118
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,18 @@
+{
+ "python.linting.pylintEnabled": true,
+ "python.linting.enabled": true,
+ "python.linting.pylintUseMinimalCheckers": false,
+ "editor.formatOnSave": true,
+ "editor.renderWhitespace": "all",
+ "files.autoSave": "afterDelay",
+ "python.analysis.typeCheckingMode": "basic",
+ "python.formatting.provider": "black",
+ "python.formatting.blackArgs": ["--line-length=100"],
+ "editor.fontWeight": "normal",
+ "python.analysis.extraPaths": [
+ "./deepface"
+ ],
+ "black-formatter.args": [
+ "--line-length=100"
+ ]
+}
diff --git a/CITATION.md b/CITATION.md
new file mode 100644
index 0000000000000000000000000000000000000000..4384442cf9ed6e4ea6644955835e0a19af586330
--- /dev/null
+++ b/CITATION.md
@@ -0,0 +1,41 @@
+## 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 the this publication.
+
+```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://doi.org/10.1109/ASYU50717.2020.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 or face detection purposes, 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://doi.org/10.1109/ICEET53442.2021.9659697},
+ organization = {IEEE}
+}
+```
+
+### 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 0000000000000000000000000000000000000000..90bd6927f2ae6d6ac964acd38b549d785f4c08b0
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,56 @@
+# 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
+
+# -----------------------------------
+# 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:15000", "app:create_app()"]
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..2b0f9fbb7d024c081d12ea0418b02a0969c5d35a
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2019 Sefik Ilkin Serengil
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000000000000000000000000000000000000..cb8e9aeb40d45d5c58a014b95cc19eb7237a7908
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,8 @@
+test:
+ cd tests && python -m pytest . -s --disable-warnings
+
+lint:
+ python -m pylint deepface/ --fail-under=10
+
+coverage:
+ pip install pytest-cov && cd tests && python -m pytest --cov=deepface
\ No newline at end of file
diff --git a/Train.py b/Train.py
new file mode 100644
index 0000000000000000000000000000000000000000..4ad7bc8847148bf242801204469afbf50647c446
--- /dev/null
+++ b/Train.py
@@ -0,0 +1,55 @@
+
+# from deepface import DeepFace
+# import os
+# models = [
+# "VGG-Face",
+# "Facenet",
+# "Facenet512",
+# "OpenFace",
+# "DeepFace",
+# "DeepID",
+# "ArcFace",
+# "Dlib",
+# "SFace",
+# ]
+
+# metrics = ["cosine", "euclidean", "euclidean_l2"]
+
+# backends = [
+# 'opencv',
+# 'ssd',
+# 'dlib',
+# 'mtcnn',
+# 'retinaface',
+# 'mediapipe',
+# 'yolov8',
+# 'yunet',
+# 'fastmtcnn',
+# ]
+
+# # df = DeepFace.find(img_path='F:/projects/python/mafqoud/dataset/missing_people/m0.jpg'
+# # , db_path='F:/projects/python/mafqoud/dataset/founded_people'
+# # , enforce_detection = True
+# # , model_name = models[2]
+# # , distance_metric = metrics[2]
+# # , detector_backend = backends[3])
+
+# DeepFace.stream(db_path = "F:/deepface")
+
+# base_dir = os.path.abspath(os.path.dirname(__file__))
+# # base_dir = "f:\\"
+# founded_dir = os.path.join(base_dir, 'mafqoud', 'images', 'founded_people')
+# def get_main_directory():
+# path = os.path.abspath(__file__)
+# drive, _ = os.path.splitdrive(path)
+# if not drive.endswith(os.path.sep):
+# drive += os.path.sep
+# return drive
+
+# base_dir = get_main_directory()
+# missing_dir = os.path.join(base_dir, 'mafqoud', 'images', 'missing_people')
+# print(missing_dir)
+
+# print(base_dir)
+# print(missing_dir)
+# print(founded_dir)
\ No newline at end of file
diff --git a/deepface/DeepFace.py b/deepface/DeepFace.py
new file mode 100644
index 0000000000000000000000000000000000000000..a03089739ce292dec7529d07b0f3b296ae010e01
--- /dev/null
+++ b/deepface/DeepFace.py
@@ -0,0 +1,572 @@
+# common dependencies
+import os
+import warnings
+import logging
+from typing import Any, Dict, List, Union, Optional
+from deepface.commons.os_path import os_path
+
+# this has to be set before importing tensorflow
+os.environ["TF_USE_LEGACY_KERAS"] = "1"
+
+# pylint: disable=wrong-import-position
+
+# 3rd party dependencies
+import numpy as np
+import pandas as pd
+import tensorflow as tf
+
+# package dependencies
+from deepface.commons import package_utils, folder_utils
+from deepface.commons import logger as log
+from deepface.modules import (
+ modeling,
+ representation,
+ verification,
+ recognition,
+ demography,
+ detection,
+ streaming,
+ preprocessing,
+ cloudservice,
+)
+from deepface import __version__
+
+logger = log.get_singletonish_logger()
+
+# -----------------------------------
+# configurations for dependencies
+
+# users should install tf_keras package if they are using tf 2.16 or later versions
+package_utils.validate_for_keras3()
+
+warnings.filterwarnings("ignore")
+os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3"
+tf_version = package_utils.get_tf_major_version()
+if tf_version == 2:
+ tf.get_logger().setLevel(logging.ERROR)
+# -----------------------------------
+
+# create required folders if necessary to store model weights
+folder_utils.initialize_folder()
+
+
+def build_model(model_name: str) -> Any:
+ """
+ This function builds a deepface model
+ Args:
+ model_name (string): face recognition or facial attribute model
+ VGG-Face, Facenet, OpenFace, DeepFace, DeepID for face recognition
+ Age, Gender, Emotion, Race for facial attributes
+ Returns:
+ built_model
+ """
+ return modeling.build_model(model_name=model_name)
+
+
+def verify(
+ img1_path: Union[str, np.ndarray, List[float]],
+ img2_path: Union[str, np.ndarray, List[float]],
+ model_name: str = "VGG-Face",
+ detector_backend: str = "opencv",
+ distance_metric: str = "cosine",
+ enforce_detection: bool = True,
+ align: bool = True,
+ expand_percentage: int = 0,
+ normalization: str = "base",
+ silent: bool = False,
+) -> Dict[str, Any]:
+ """
+ Verify if an image pair represents the same person or different persons.
+ Args:
+ img1_path (str or np.ndarray or List[float]): Path to the first image.
+ Accepts exact image path as a string, numpy array (BGR), base64 encoded images
+ or pre-calculated embeddings.
+
+ img2_path (str or np.ndarray or List[float]): Path to the second image.
+ Accepts exact image path as a string, numpy array (BGR), base64 encoded images
+ or pre-calculated embeddings.
+
+ model_name (str): Model for face recognition. Options: VGG-Face, Facenet, Facenet512,
+ OpenFace, DeepFace, DeepID, Dlib, ArcFace, SFace and GhostFaceNet (default is VGG-Face).
+
+ detector_backend (string): face detector backend. Options: 'opencv', 'retinaface',
+ 'mtcnn', 'ssd', 'dlib', 'mediapipe', 'yolov8', 'centerface' or 'skip'
+ (default is opencv).
+
+ distance_metric (string): Metric for measuring similarity. Options: 'cosine',
+ 'euclidean', 'euclidean_l2' (default is cosine).
+
+ enforce_detection (boolean): If no face is detected in an image, raise an exception.
+ Set to False to avoid the exception for low-resolution images (default is True).
+
+ align (bool): Flag to enable face alignment (default is True).
+
+ expand_percentage (int): expand detected facial area with a percentage (default is 0).
+
+ normalization (string): Normalize the input image before feeding it to the model.
+ Options: base, raw, Facenet, Facenet2018, VGGFace, VGGFace2, ArcFace (default is base)
+
+ silent (boolean): Suppress or allow some log messages for a quieter analysis process
+ (default is False).
+
+ Returns:
+ result (dict): A dictionary containing verification results with following keys.
+
+ - 'verified' (bool): Indicates whether the images represent the same person (True)
+ or different persons (False).
+
+ - 'distance' (float): The distance measure between the face vectors.
+ A lower distance indicates higher similarity.
+
+ - 'max_threshold_to_verify' (float): The maximum threshold used for verification.
+ If the distance is below this threshold, the images are considered a match.
+
+ - 'model' (str): The chosen face recognition model.
+
+ - 'distance_metric' (str): The chosen similarity metric for measuring distances.
+
+ - 'facial_areas' (dict): Rectangular regions of interest for faces in both images.
+ - 'img1': {'x': int, 'y': int, 'w': int, 'h': int}
+ Region of interest for the first image.
+ - 'img2': {'x': int, 'y': int, 'w': int, 'h': int}
+ Region of interest for the second image.
+
+ - 'time' (float): Time taken for the verification process in seconds.
+ """
+
+ return verification.verify(
+ img1_path=img1_path,
+ img2_path=img2_path,
+ model_name=model_name,
+ detector_backend=detector_backend,
+ distance_metric=distance_metric,
+ enforce_detection=enforce_detection,
+ align=align,
+ expand_percentage=expand_percentage,
+ normalization=normalization,
+ silent=silent,
+ )
+
+
+def analyze(
+ img_path: Union[str, np.ndarray],
+ actions: Union[tuple, list] = ("emotion", "age", "gender", "race"),
+ enforce_detection: bool = True,
+ detector_backend: str = "opencv",
+ align: bool = True,
+ expand_percentage: int = 0,
+ silent: bool = False,
+) -> List[Dict[str, Any]]:
+ """
+ Analyze facial attributes such as age, gender, emotion, and race in the provided image.
+ Args:
+ img_path (str or np.ndarray): The exact path to the image, a numpy array in BGR format,
+ or a base64 encoded image. If the source image contains multiple faces, the result will
+ include information for each detected face.
+
+ actions (tuple): Attributes to analyze. The default is ('age', 'gender', 'emotion', 'race').
+ You can exclude some of these attributes from the analysis if needed.
+
+ enforce_detection (boolean): If no face is detected in an image, raise an exception.
+ Set to False to avoid the exception for low-resolution images (default is True).
+
+ detector_backend (string): face detector backend. Options: 'opencv', 'retinaface',
+ 'mtcnn', 'ssd', 'dlib', 'mediapipe', 'yolov8', 'centerface' or 'skip'
+ (default is opencv).
+
+ distance_metric (string): Metric for measuring similarity. Options: 'cosine',
+ 'euclidean', 'euclidean_l2' (default is cosine).
+
+ align (boolean): Perform alignment based on the eye positions (default is True).
+
+ expand_percentage (int): expand detected facial area with a percentage (default is 0).
+
+ silent (boolean): Suppress or allow some log messages for a quieter analysis process
+ (default is False).
+
+ Returns:
+ results (List[Dict[str, Any]]): A list of dictionaries, where each dictionary represents
+ the analysis results for a detected face. Each dictionary in the list contains the
+ following keys:
+
+ - 'region' (dict): Represents the rectangular region of the detected face in the image.
+ - 'x': x-coordinate of the top-left corner of the face.
+ - 'y': y-coordinate of the top-left corner of the face.
+ - 'w': Width of the detected face region.
+ - 'h': Height of the detected face region.
+
+ - 'age' (float): Estimated age of the detected face.
+
+ - 'face_confidence' (float): Confidence score for the detected face.
+ Indicates the reliability of the face detection.
+
+ - 'dominant_gender' (str): The dominant gender in the detected face.
+ Either "Man" or "Woman".
+
+ - 'gender' (dict): Confidence scores for each gender category.
+ - 'Man': Confidence score for the male gender.
+ - 'Woman': Confidence score for the female gender.
+
+ - 'dominant_emotion' (str): The dominant emotion in the detected face.
+ Possible values include "sad," "angry," "surprise," "fear," "happy,"
+ "disgust," and "neutral"
+
+ - 'emotion' (dict): Confidence scores for each emotion category.
+ - 'sad': Confidence score for sadness.
+ - 'angry': Confidence score for anger.
+ - 'surprise': Confidence score for surprise.
+ - 'fear': Confidence score for fear.
+ - 'happy': Confidence score for happiness.
+ - 'disgust': Confidence score for disgust.
+ - 'neutral': Confidence score for neutrality.
+
+ - 'dominant_race' (str): The dominant race in the detected face.
+ Possible values include "indian," "asian," "latino hispanic,"
+ "black," "middle eastern," and "white."
+
+ - 'race' (dict): Confidence scores for each race category.
+ - 'indian': Confidence score for Indian ethnicity.
+ - 'asian': Confidence score for Asian ethnicity.
+ - 'latino hispanic': Confidence score for Latino/Hispanic ethnicity.
+ - 'black': Confidence score for Black ethnicity.
+ - 'middle eastern': Confidence score for Middle Eastern ethnicity.
+ - 'white': Confidence score for White ethnicity.
+ """
+ return demography.analyze(
+ img_path=img_path,
+ actions=actions,
+ enforce_detection=enforce_detection,
+ detector_backend=detector_backend,
+ align=align,
+ expand_percentage=expand_percentage,
+ silent=silent,
+ )
+
+
+def find(
+ img_path: Union[str, np.ndarray],
+ db_path: str,
+ model_name: str = "VGG-Face",
+ distance_metric: str = "cosine",
+ enforce_detection: bool = True,
+ detector_backend: str = "opencv",
+ align: bool = True,
+ expand_percentage: int = 0,
+ threshold: Optional[float] = None,
+ normalization: str = "base",
+ silent: bool = False,
+) -> List[pd.DataFrame]:
+ """
+ Identify individuals in a database
+ Args:
+ img_path (str or np.ndarray): The exact path to the image, a numpy array in BGR format,
+ or a base64 encoded image. If the source image contains multiple faces, the result will
+ include information for each detected face.
+
+ db_path (string): Path to the folder containing image files. All detected faces
+ in the database will be considered in the decision-making process.
+
+ model_name (str): Model for face recognition. Options: VGG-Face, Facenet, Facenet512,
+ OpenFace, DeepFace, DeepID, Dlib, ArcFace, SFace and GhostFaceNet (default is VGG-Face).
+
+ distance_metric (string): Metric for measuring similarity. Options: 'cosine',
+ 'euclidean', 'euclidean_l2' (default is cosine).
+
+ enforce_detection (boolean): If no face is detected in an image, raise an exception.
+ Set to False to avoid the exception for low-resolution images (default is True).
+
+ detector_backend (string): face detector backend. Options: 'opencv', 'retinaface',
+ 'mtcnn', 'ssd', 'dlib', 'mediapipe', 'yolov8', 'centerface' or 'skip'
+ (default is opencv).
+
+ align (boolean): Perform alignment based on the eye positions (default is True).
+
+ expand_percentage (int): expand detected facial area with a percentage (default is 0).
+
+ threshold (float): Specify a threshold to determine whether a pair represents the same
+ person or different individuals. This threshold is used for comparing distances.
+ If left unset, default pre-tuned threshold values will be applied based on the specified
+ model name and distance metric (default is None).
+
+ normalization (string): Normalize the input image before feeding it to the model.
+ Options: base, raw, Facenet, Facenet2018, VGGFace, VGGFace2, ArcFace (default is base).
+
+ silent (boolean): Suppress or allow some log messages for a quieter analysis process
+ (default is False).
+
+ Returns:
+ results (List[pd.DataFrame]): A list of pandas dataframes. Each dataframe corresponds
+ to the identity information for an individual detected in the source image.
+ The DataFrame columns include:
+
+ - 'identity': Identity label of the detected individual.
+
+ - 'target_x', 'target_y', 'target_w', 'target_h': Bounding box coordinates of the
+ target face in the database.
+
+ - 'source_x', 'source_y', 'source_w', 'source_h': Bounding box coordinates of the
+ detected face in the source image.
+
+ - 'threshold': threshold to determine a pair whether same person or different persons
+
+ - 'distance': Similarity score between the faces based on the
+ specified model and distance metric
+ """
+ return recognition.find(
+ img_path=img_path,
+ db_path=db_path,
+ model_name=model_name,
+ distance_metric=distance_metric,
+ enforce_detection=enforce_detection,
+ detector_backend=detector_backend,
+ align=align,
+ expand_percentage=expand_percentage,
+ threshold=threshold,
+ normalization=normalization,
+ silent=silent,
+ )
+
+
+def represent(
+ img_path: Union[str, np.ndarray],
+ model_name: str = "VGG-Face",
+ enforce_detection: bool = True,
+ detector_backend: str = "opencv",
+ align: bool = True,
+ expand_percentage: int = 0,
+ normalization: str = "base",
+) -> List[Dict[str, Any]]:
+ """
+ Represent facial images as multi-dimensional vector embeddings.
+
+ Args:
+ img_path (str or np.ndarray): The exact path to the image, a numpy array in BGR format,
+ or a base64 encoded image. If the source image contains multiple faces, the result will
+ include information for each detected face.
+
+ model_name (str): Model for face recognition. Options: VGG-Face, Facenet, Facenet512,
+ OpenFace, DeepFace, DeepID, Dlib, ArcFace, SFace and GhostFaceNet
+ (default is VGG-Face.).
+
+ enforce_detection (boolean): If no face is detected in an image, raise an exception.
+ Default is True. Set to False to avoid the exception for low-resolution images
+ (default is True).
+
+ detector_backend (string): face detector backend. Options: 'opencv', 'retinaface',
+ 'mtcnn', 'ssd', 'dlib', 'mediapipe', 'yolov8', 'centerface' or 'skip'
+ (default is opencv).
+
+ align (boolean): Perform alignment based on the eye positions (default is True).
+
+ expand_percentage (int): expand detected facial area with a percentage (default is 0).
+
+ normalization (string): Normalize the input image before feeding it to the model.
+ Default is base. Options: base, raw, Facenet, Facenet2018, VGGFace, VGGFace2, ArcFace
+ (default is base).
+
+ Returns:
+ results (List[Dict[str, Any]]): A list of dictionaries, each containing the
+ following fields:
+
+ - embedding (List[float]): Multidimensional vector representing facial features.
+ The number of dimensions varies based on the reference model
+ (e.g., FaceNet returns 128 dimensions, VGG-Face returns 4096 dimensions).
+
+ - facial_area (dict): Detected facial area by face detection in dictionary format.
+ Contains 'x' and 'y' as the left-corner point, and 'w' and 'h'
+ as the width and height. If `detector_backend` is set to 'skip', it represents
+ the full image area and is nonsensical.
+
+ - face_confidence (float): Confidence score of face detection. If `detector_backend` is set
+ to 'skip', the confidence will be 0 and is nonsensical.
+ """
+ return representation.represent(
+ img_path=img_path,
+ model_name=model_name,
+ enforce_detection=enforce_detection,
+ detector_backend=detector_backend,
+ align=align,
+ expand_percentage=expand_percentage,
+ normalization=normalization,
+ )
+
+
+def stream(
+ db_path: str = "",
+ model_name: str = "VGG-Face",
+ detector_backend: str = "opencv",
+ distance_metric: str = "cosine",
+ enable_face_analysis: bool = True,
+ source: Any = 0,
+ time_threshold: int = 5,
+ frame_threshold: int = 5,
+) -> None:
+ """
+ Run real time face recognition and facial attribute analysis
+
+ Args:
+ db_path (string): Path to the folder containing image files. All detected faces
+ in the database will be considered in the decision-making process.
+
+ model_name (str): Model for face recognition. Options: VGG-Face, Facenet, Facenet512,
+ OpenFace, DeepFace, DeepID, Dlib, ArcFace, SFace and GhostFaceNet (default is VGG-Face).
+
+ detector_backend (string): face detector backend. Options: 'opencv', 'retinaface',
+ 'mtcnn', 'ssd', 'dlib', 'mediapipe', 'yolov8', 'centerface' or 'skip'
+ (default is opencv).
+
+ distance_metric (string): Metric for measuring similarity. Options: 'cosine',
+ 'euclidean', 'euclidean_l2' (default is cosine).
+
+ enable_face_analysis (bool): Flag to enable face analysis (default is True).
+
+ source (Any): The source for the video stream (default is 0, which represents the
+ default camera).
+
+ time_threshold (int): The time threshold (in seconds) for face recognition (default is 5).
+
+ frame_threshold (int): The frame threshold for face recognition (default is 5).
+ Returns:
+ None
+ """
+
+ time_threshold = max(time_threshold, 1)
+ frame_threshold = max(frame_threshold, 1)
+
+ streaming.analysis(
+ db_path=db_path,
+ model_name=model_name,
+ detector_backend=detector_backend,
+ distance_metric=distance_metric,
+ enable_face_analysis=enable_face_analysis,
+ source=source,
+ time_threshold=time_threshold,
+ frame_threshold=frame_threshold,
+ )
+
+
+def extract_faces(
+ img_path: Union[str, np.ndarray],
+ detector_backend: str = "opencv",
+ enforce_detection: bool = True,
+ align: bool = True,
+ expand_percentage: int = 0,
+ grayscale: bool = False,
+) -> List[Dict[str, Any]]:
+ """
+ Extract faces from a given image
+
+ Args:
+ img_path (str or np.ndarray): Path to the first image. Accepts exact image path
+ as a string, numpy array (BGR), or base64 encoded images.
+
+ detector_backend (string): face detector backend. Options: 'opencv', 'retinaface',
+ 'mtcnn', 'ssd', 'dlib', 'mediapipe', 'yolov8', 'centerface' or 'skip'
+ (default is opencv).
+
+ enforce_detection (boolean): If no face is detected in an image, raise an exception.
+ Set to False to avoid the exception for low-resolution images (default is True).
+
+ align (bool): Flag to enable face alignment (default is True).
+
+ expand_percentage (int): expand detected facial area with a percentage (default is 0).
+
+ grayscale (boolean): Flag to convert the image to grayscale before
+ processing (default is False).
+
+ Returns:
+ results (List[Dict[str, Any]]): A list of dictionaries, where each dictionary contains:
+
+ - "face" (np.ndarray): The detected face as a NumPy array.
+
+ - "facial_area" (Dict[str, Any]): The detected face's regions as a dictionary containing:
+ - keys 'x', 'y', 'w', 'h' with int values
+ - keys 'left_eye', 'right_eye' with a tuple of 2 ints as values. left and right eyes
+ are eyes on the left and right respectively with respect to the person itself
+ instead of observer.
+
+ - "confidence" (float): The confidence score associated with the detected face.
+ """
+
+ return detection.extract_faces(
+ img_path=img_path,
+ detector_backend=detector_backend,
+ enforce_detection=enforce_detection,
+ align=align,
+ expand_percentage=expand_percentage,
+ grayscale=grayscale,
+ )
+
+
+def cli() -> None:
+ """
+ command line interface function will be offered in this block
+ """
+ import fire
+
+ fire.Fire()
+
+
+# deprecated function(s)
+
+
+def detectFace(
+ img_path: Union[str, np.ndarray],
+ target_size: tuple = (224, 224),
+ detector_backend: str = "opencv",
+ enforce_detection: bool = True,
+ align: bool = True,
+) -> Union[np.ndarray, None]:
+ """
+ Deprecated face detection function. Use extract_faces for same functionality.
+
+ Args:
+ img_path (str or np.ndarray): Path to the first image. Accepts exact image path
+ as a string, numpy array (BGR), or base64 encoded images.
+
+ target_size (tuple): final shape of facial image. black pixels will be
+ added to resize the image (default is (224, 224)).
+
+ detector_backend (string): face detector backend. Options: 'opencv', 'retinaface',
+ 'mtcnn', 'ssd', 'dlib', 'mediapipe', 'yolov8', 'centerface' or 'skip'
+ (default is opencv).
+
+ enforce_detection (boolean): If no face is detected in an image, raise an exception.
+ Set to False to avoid the exception for low-resolution images (default is True).
+
+ align (bool): Flag to enable face alignment (default is True).
+
+ Returns:
+ img (np.ndarray): detected (and aligned) facial area image as numpy array
+ """
+ logger.warn("Function detectFace is deprecated. Use extract_faces instead.")
+ face_objs = extract_faces(
+ img_path=img_path,
+ detector_backend=detector_backend,
+ enforce_detection=enforce_detection,
+ align=align,
+ grayscale=False,
+ )
+ extracted_face = None
+ if len(face_objs) > 0:
+ extracted_face = face_objs[0]["face"]
+ extracted_face = preprocessing.resize_image(img=extracted_face, target_size=target_size)
+ return extracted_face
+
+
+def sync_datasets():
+ # Set the local directories
+ base_dir = os_path.get_main_directory()
+
+ missing_dir = os.path.join(base_dir, 'mafqoud', 'images', 'missing_people')
+ founded_dir = os.path.join(base_dir, 'mafqoud', 'images', 'founded_people')
+
+ # Ensure the directories exist
+ os.makedirs(missing_dir, exist_ok=True)
+ os.makedirs(founded_dir, exist_ok=True)
+
+ cloudservice.delete_pkl_files(missing_dir)
+ missing_people = cloudservice.sync_folder('missing_people', missing_dir)
+ cloudservice.delete_pkl_files(founded_dir)
+ founded_people = cloudservice.sync_folder('founded_people', founded_dir)
+
diff --git a/deepface/__init__.py b/deepface/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..69d27b416452135a4da763cc8c98a8872648ee34
--- /dev/null
+++ b/deepface/__init__.py
@@ -0,0 +1 @@
+__version__ = "0.0.90"
diff --git a/deepface/api/__init__.py b/deepface/api/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
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 0000000000000000000000000000000000000000..0cbb0a3886499a37bc54b4a61b780eccbdd22f96
--- /dev/null
+++ b/deepface/api/postman/deepface-api.postman_collection.json
@@ -0,0 +1,102 @@
+{
+ "info": {
+ "_postman_id": "4c0b144e-4294-4bdd-8072-bcb326b1fed2",
+ "name": "deepface-api",
+ "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
+ },
+ "item": [
+ {
+ "name": "Represent",
+ "request": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"model_name\": \"Facenet\",\n \"img\": \"/Users/sefik/Desktop/deepface/tests/dataset/img1.jpg\"\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://127.0.0.1:5000/represent",
+ "protocol": "http",
+ "host": [
+ "127",
+ "0",
+ "0",
+ "1"
+ ],
+ "port": "5000",
+ "path": [
+ "represent"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Face verification",
+ "request": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": " {\n \t\"img1_path\": \"/Users/sefik/Desktop/deepface/tests/dataset/img1.jpg\",\n \"img2_path\": \"/Users/sefik/Desktop/deepface/tests/dataset/img2.jpg\",\n \"model_name\": \"Facenet\",\n \"detector_backend\": \"mtcnn\",\n \"distance_metric\": \"euclidean\"\n }",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://127.0.0.1:5000/verify",
+ "protocol": "http",
+ "host": [
+ "127",
+ "0",
+ "0",
+ "1"
+ ],
+ "port": "5000",
+ "path": [
+ "verify"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Face analysis",
+ "request": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"img_path\": \"/Users/sefik/Desktop/deepface/tests/dataset/couple.jpg\",\n \"actions\": [\"age\", \"gender\", \"emotion\", \"race\"]\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://127.0.0.1:5000/analyze",
+ "protocol": "http",
+ "host": [
+ "127",
+ "0",
+ "0",
+ "1"
+ ],
+ "port": "5000",
+ "path": [
+ "analyze"
+ ]
+ }
+ },
+ "response": []
+ }
+ ]
+}
\ No newline at end of file
diff --git a/deepface/api/src/__init__.py b/deepface/api/src/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/deepface/api/src/api.py b/deepface/api/src/api.py
new file mode 100644
index 0000000000000000000000000000000000000000..70ccb512210c5b9882c84c073b6359674591d08a
--- /dev/null
+++ b/deepface/api/src/api.py
@@ -0,0 +1,10 @@
+import argparse
+import app
+import os
+
+if __name__ == "__main__":
+ deepface_app = app.create_app()
+ parser = argparse.ArgumentParser()
+ parser.add_argument("-p", "--port", type=int, default=int(os.getenv('DEFAULT_PORT')), help="Port of serving api")
+ args = parser.parse_args()
+ deepface_app.run(host="0.0.0.0", port=args.port)
diff --git a/deepface/api/src/app.py b/deepface/api/src/app.py
new file mode 100644
index 0000000000000000000000000000000000000000..69ec52f5cea3175717e4d68924e52d34d23a1d68
--- /dev/null
+++ b/deepface/api/src/app.py
@@ -0,0 +1,11 @@
+# 3rd parth dependencies
+from flask import Flask
+from deepface.api.src.modules.core.routes import blueprint
+
+
+def create_app():
+ app = Flask(__name__)
+ app.register_blueprint(blueprint)
+ print(app.url_map)
+ return app
+
diff --git a/deepface/api/src/modules/__init__.py b/deepface/api/src/modules/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/deepface/api/src/modules/core/__init__.py b/deepface/api/src/modules/core/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/deepface/api/src/modules/core/routes.py b/deepface/api/src/modules/core/routes.py
new file mode 100644
index 0000000000000000000000000000000000000000..36b9f27d067dc7e6eba41b13f5ab92f9b83721eb
--- /dev/null
+++ b/deepface/api/src/modules/core/routes.py
@@ -0,0 +1,201 @@
+from flask import Blueprint, request , jsonify
+from deepface.api.src.modules.core import service
+from deepface.commons.logger import Logger
+from deepface.commons.os_path import os_path
+import json
+import os
+
+logger = Logger(module="api/src/routes.py")
+
+blueprint = Blueprint("routes", __name__)
+
+
+@blueprint.route("/")
+def home():
+ return "
Welcome to DeepFace API!
"
+
+
+@blueprint.route("/represent", methods=["POST"])
+def represent():
+ input_args = request.get_json()
+
+ if input_args is None:
+ return {"message": "empty input set passed"}
+
+ img_path = input_args.get("img") or input_args.get("img_path")
+ if img_path is None:
+ return {"message": "you must pass img_path input"}
+
+ model_name = input_args.get("model_name", "VGG-Face")
+ detector_backend = input_args.get("detector_backend", "opencv")
+ enforce_detection = input_args.get("enforce_detection", True)
+ align = input_args.get("align", True)
+
+ obj = service.represent(
+ img_path=img_path,
+ model_name=model_name,
+ detector_backend=detector_backend,
+ enforce_detection=enforce_detection,
+ align=align,
+ )
+
+ logger.debug(obj)
+
+ return obj
+
+
+@blueprint.route("/verify", methods=["POST"])
+def verify():
+ input_args = request.get_json()
+
+ if input_args is None:
+ return {"message": "empty input set passed"}
+
+ img1_path = input_args.get("img1") or input_args.get("img1_path")
+ img2_path = input_args.get("img2") or input_args.get("img2_path")
+
+ if img1_path is None:
+ return {"message": "you must pass img1_path input"}
+
+ if img2_path is None:
+ return {"message": "you must pass img2_path input"}
+
+ model_name = input_args.get("model_name", "VGG-Face")
+ detector_backend = input_args.get("detector_backend", "opencv")
+ enforce_detection = input_args.get("enforce_detection", True)
+ distance_metric = input_args.get("distance_metric", "cosine")
+ align = input_args.get("align", True)
+
+ verification = service.verify(
+ img1_path=img1_path,
+ img2_path=img2_path,
+ model_name=model_name,
+ detector_backend=detector_backend,
+ distance_metric=distance_metric,
+ align=align,
+ enforce_detection=enforce_detection,
+ )
+
+ logger.debug(verification)
+
+ return verification
+
+
+@blueprint.route("/analyze", methods=["POST"])
+def analyze():
+ input_args = request.get_json()
+
+ if input_args is None:
+ return {"message": "empty input set passed"}
+
+ img_path = input_args.get("img") or input_args.get("img_path")
+ if img_path is None:
+ return {"message": "you must pass img_path input"}
+
+ detector_backend = input_args.get("detector_backend", "opencv")
+ enforce_detection = input_args.get("enforce_detection", True)
+ align = input_args.get("align", True)
+ actions = input_args.get("actions", ["age", "gender", "emotion", "race"])
+
+ demographies = service.analyze(
+ img_path=img_path,
+ actions=actions,
+ detector_backend=detector_backend,
+ enforce_detection=enforce_detection,
+ align=align,
+ )
+
+ logger.debug(demographies)
+
+ return demographies
+
+@blueprint.route("/find", methods=["POST"])
+def find():
+ input_args = request.get_json()
+
+ if input_args is None:
+ response = jsonify({'error': 'empty input set passed'})
+ response.status_code = 500
+ return response
+
+ img_name = input_args.get("img") or input_args.get("img_name")
+ img_type = input_args.get("img_type")
+
+ if img_name is None:
+ response = jsonify({'error': 'you must pass img_name input'})
+ response.status_code = 404
+ return response
+
+ if img_type == "missing" or img_type == "missing_person" or img_type == "missing_people" or img_type == "missing person" or img_type == "missing people" :
+
+ img_path = os.path.join( os_path.get_main_directory() , 'mafqoud' , 'images' , "missing_people" , img_name)
+ db_path = os.path.join( os_path.get_main_directory() , 'mafqoud' , 'images' , "founded_people")
+
+ elif img_type == "founded" or img_type == "founded_person" or img_type == "founded_people" or img_type == "founded person" or img_type == "founded people" :
+
+ img_path = os.path.join( os_path.get_main_directory() , 'mafqoud' , 'images' , "founded_people" , img_name)
+ db_path = os.path.join( os_path.get_main_directory() , 'mafqoud' , 'images' , "missing_people")
+
+ else :
+
+ response = jsonify({'error': 'the type of the image is not correct and it should be one of those : ( missing , missing_people , missing_people , missing person , missing people ) or ( founded , founded_people , founded_people , founded person , founded people )'})
+ response.status_code = 400
+ return response
+
+
+ if not os.path.exists(img_path) or not os.path.isfile(img_path):
+ # If the image does not exist, return a JSON response with status code 404
+ response = jsonify({'error': 'Image not found'})
+ response.status_code = 404
+ return response
+
+
+ model_name = input_args.get("model_name", "Facenet512")
+ detector_backend = input_args.get("detector_backend", "mtcnn")
+ enforce_detection = input_args.get("enforce_detection", True)
+ distance_metric = input_args.get("distance_metric", "euclidean_l2")
+ align = input_args.get("align", True)
+
+ if img_name is None:
+ return {"message": "you must pass img1_path input"}
+
+ if db_path is None:
+ dataset_path = os.path.join(path.get_parent_path(), 'dataset')
+ if img_type == "missing_person":
+ img_path = os.path.join(dataset_path, 'missing_people', img_name)
+ db_path = os.path.join(dataset_path, 'founded_people')
+ elif img_type == "founded_people":
+ img_path = os.path.join(dataset_path, 'founded_people', img_name)
+ db_path = os.path.join(dataset_path, 'missing_people')
+
+ results = service.find(
+ img_path=img_path,
+ db_path=db_path,
+ model_name=model_name,
+ detector_backend=detector_backend,
+ distance_metric=distance_metric,
+ align=align,
+ enforce_detection=enforce_detection,
+ )
+
+ # Calculate similarity_percentage for each row
+ results[0]['similarity_percentage'] =100 - ((results[0]['distance'] / results[0]['threshold']) * 100)
+
+ data = []
+ for _, row in results[0].iterrows():
+ data.append({
+ "identity": row['identity'],
+ "similarity_percentage": row['similarity_percentage']
+ })
+
+ json_data = json.dumps(data, indent=4)
+
+
+ logger.debug(json_data)
+ return json_data
+
+
+@blueprint.route("/dataset/sync", methods=["GET"])
+def sync_datasets():
+ result = service.sync_datasets()
+ return jsonify(result)
\ No newline at end of file
diff --git a/deepface/api/src/modules/core/service.py b/deepface/api/src/modules/core/service.py
new file mode 100644
index 0000000000000000000000000000000000000000..bb09383f5d4a86d7205c1171950f6fbde01d756b
--- /dev/null
+++ b/deepface/api/src/modules/core/service.py
@@ -0,0 +1,77 @@
+from deepface import DeepFace
+
+# pylint: disable=broad-except
+
+
+def represent(img_path, model_name, detector_backend, enforce_detection, align):
+ try:
+ result = {}
+ embedding_objs = DeepFace.represent(
+ img_path=img_path,
+ model_name=model_name,
+ detector_backend=detector_backend,
+ enforce_detection=enforce_detection,
+ align=align,
+ )
+ result["results"] = embedding_objs
+ return result
+ except Exception as err:
+ return {"error": f"Exception while representing: {str(err)}"}, 400
+
+
+def verify(
+ img1_path, img2_path, model_name, detector_backend, distance_metric, enforce_detection, align
+):
+ try:
+ obj = DeepFace.verify(
+ img1_path=img1_path,
+ img2_path=img2_path,
+ model_name=model_name,
+ detector_backend=detector_backend,
+ distance_metric=distance_metric,
+ align=align,
+ enforce_detection=enforce_detection,
+ )
+ return obj
+ except Exception as err:
+ return {"error": f"Exception while verifying: {str(err)}"}, 400
+
+
+def analyze(img_path, actions, detector_backend, enforce_detection, align):
+ try:
+ result = {}
+ demographies = DeepFace.analyze(
+ img_path=img_path,
+ actions=actions,
+ detector_backend=detector_backend,
+ enforce_detection=enforce_detection,
+ align=align,
+ silent=True,
+ )
+ result["results"] = demographies
+ return result
+ except Exception as err:
+ return {"error": f"Exception while analyzing: {str(err)}"}, 400
+
+def find(img_path, db_path, model_name, detector_backend, distance_metric, enforce_detection, align):
+ try:
+ obj = DeepFace.find(
+ img_path=img_path,
+ db_path=db_path,
+ model_name=model_name,
+ detector_backend=detector_backend,
+ distance_metric=distance_metric,
+ align=align,
+ enforce_detection=enforce_detection,
+ )
+ return obj
+ except Exception as err:
+ return {"error": f"Exception while Findind: {str(err)}"}, 400
+
+
+def sync_datasets():
+ try:
+ DeepFace.sync_datasets()
+ return {'data': 'synced successfully'}, 200
+ except Exception as e:
+ return {'error': str(e)}, 400
\ No newline at end of file
diff --git a/deepface/basemodels/ArcFace.py b/deepface/basemodels/ArcFace.py
new file mode 100644
index 0000000000000000000000000000000000000000..43dd4247ccdbc8f52b4e9803425f16a700120ba4
--- /dev/null
+++ b/deepface/basemodels/ArcFace.py
@@ -0,0 +1,179 @@
+import os
+import gdown
+from deepface.commons import package_utils, folder_utils
+from deepface.models.FacialRecognition import FacialRecognition
+
+from deepface.commons import logger as log
+
+logger = log.get_singletonish_logger()
+
+# pylint: disable=unsubscriptable-object
+
+# --------------------------------
+# dependency configuration
+
+tf_version = package_utils.get_tf_major_version()
+
+if tf_version == 1:
+ from keras.models import Model
+ from keras.engine import training
+ from keras.layers import (
+ ZeroPadding2D,
+ Input,
+ Conv2D,
+ BatchNormalization,
+ PReLU,
+ Add,
+ Dropout,
+ Flatten,
+ Dense,
+ )
+else:
+ from tensorflow.keras.models import Model
+ from tensorflow.python.keras.engine import training
+ from tensorflow.keras.layers import (
+ ZeroPadding2D,
+ Input,
+ Conv2D,
+ BatchNormalization,
+ PReLU,
+ Add,
+ Dropout,
+ Flatten,
+ Dense,
+ )
+
+# pylint: disable=too-few-public-methods
+class ArcFaceClient(FacialRecognition):
+ """
+ ArcFace model class
+ """
+
+ def __init__(self):
+ self.model = load_model()
+ self.model_name = "ArcFace"
+ self.input_shape = (112, 112)
+ self.output_shape = 512
+
+
+def load_model(
+ url="https://github.com/serengil/deepface_models/releases/download/v1.0/arcface_weights.h5",
+) -> Model:
+ """
+ Construct ArcFace model, download its weights and load
+ Returns:
+ model (Model)
+ """
+ base_model = ResNet34()
+ inputs = base_model.inputs[0]
+ arcface_model = base_model.outputs[0]
+ arcface_model = BatchNormalization(momentum=0.9, epsilon=2e-5)(arcface_model)
+ arcface_model = Dropout(0.4)(arcface_model)
+ arcface_model = Flatten()(arcface_model)
+ arcface_model = Dense(512, activation=None, use_bias=True, kernel_initializer="glorot_normal")(
+ arcface_model
+ )
+ embedding = BatchNormalization(momentum=0.9, epsilon=2e-5, name="embedding", scale=True)(
+ arcface_model
+ )
+ model = Model(inputs, embedding, name=base_model.name)
+
+ # ---------------------------------------
+ # check the availability of pre-trained weights
+
+ home = folder_utils.get_deepface_home()
+
+ file_name = "arcface_weights.h5"
+ output = home + "/.deepface/weights/" + file_name
+
+ if os.path.isfile(output) != True:
+
+ logger.info(f"{file_name} will be downloaded to {output}")
+ gdown.download(url, output, quiet=False)
+
+ # ---------------------------------------
+
+ model.load_weights(output)
+
+ return model
+
+
+def ResNet34() -> Model:
+ """
+ ResNet34 model
+ Returns:
+ model (Model)
+ """
+ img_input = Input(shape=(112, 112, 3))
+
+ x = ZeroPadding2D(padding=1, name="conv1_pad")(img_input)
+ x = Conv2D(
+ 64, 3, strides=1, use_bias=False, kernel_initializer="glorot_normal", name="conv1_conv"
+ )(x)
+ x = BatchNormalization(axis=3, epsilon=2e-5, momentum=0.9, name="conv1_bn")(x)
+ x = PReLU(shared_axes=[1, 2], name="conv1_prelu")(x)
+ x = stack_fn(x)
+
+ model = training.Model(img_input, x, name="ResNet34")
+
+ return model
+
+
+def block1(x, filters, kernel_size=3, stride=1, conv_shortcut=True, name=None):
+ bn_axis = 3
+
+ if conv_shortcut:
+ shortcut = Conv2D(
+ filters,
+ 1,
+ strides=stride,
+ use_bias=False,
+ kernel_initializer="glorot_normal",
+ name=name + "_0_conv",
+ )(x)
+ shortcut = BatchNormalization(
+ axis=bn_axis, epsilon=2e-5, momentum=0.9, name=name + "_0_bn"
+ )(shortcut)
+ else:
+ shortcut = x
+
+ x = BatchNormalization(axis=bn_axis, epsilon=2e-5, momentum=0.9, name=name + "_1_bn")(x)
+ x = ZeroPadding2D(padding=1, name=name + "_1_pad")(x)
+ x = Conv2D(
+ filters,
+ 3,
+ strides=1,
+ kernel_initializer="glorot_normal",
+ use_bias=False,
+ name=name + "_1_conv",
+ )(x)
+ x = BatchNormalization(axis=bn_axis, epsilon=2e-5, momentum=0.9, name=name + "_2_bn")(x)
+ x = PReLU(shared_axes=[1, 2], name=name + "_1_prelu")(x)
+
+ x = ZeroPadding2D(padding=1, name=name + "_2_pad")(x)
+ x = Conv2D(
+ filters,
+ kernel_size,
+ strides=stride,
+ kernel_initializer="glorot_normal",
+ use_bias=False,
+ name=name + "_2_conv",
+ )(x)
+ x = BatchNormalization(axis=bn_axis, epsilon=2e-5, momentum=0.9, name=name + "_3_bn")(x)
+
+ x = Add(name=name + "_add")([shortcut, x])
+ return x
+
+
+def stack1(x, filters, blocks, stride1=2, name=None):
+ x = block1(x, filters, stride=stride1, name=name + "_block1")
+ for i in range(2, blocks + 1):
+ x = block1(x, filters, conv_shortcut=False, name=name + "_block" + str(i))
+ return x
+
+
+def stack_fn(x):
+ x = stack1(x, 64, 3, name="conv2")
+ x = stack1(x, 128, 4, name="conv3")
+ x = stack1(x, 256, 6, name="conv4")
+ return stack1(x, 512, 3, name="conv5")
diff --git a/deepface/basemodels/DeepID.py b/deepface/basemodels/DeepID.py
new file mode 100644
index 0000000000000000000000000000000000000000..b68b37915e7d34ac4d18e7e833e68e886d38b932
--- /dev/null
+++ b/deepface/basemodels/DeepID.py
@@ -0,0 +1,99 @@
+import os
+import gdown
+from deepface.commons import package_utils, folder_utils
+from deepface.models.FacialRecognition import FacialRecognition
+from deepface.commons import logger as log
+
+logger = log.get_singletonish_logger()
+
+tf_version = package_utils.get_tf_major_version()
+
+if tf_version == 1:
+ from keras.models import Model
+ from keras.layers import (
+ Conv2D,
+ Activation,
+ Input,
+ Add,
+ MaxPooling2D,
+ Flatten,
+ Dense,
+ Dropout,
+ )
+else:
+ from tensorflow.keras.models import Model
+ from tensorflow.keras.layers import (
+ Conv2D,
+ Activation,
+ Input,
+ Add,
+ MaxPooling2D,
+ Flatten,
+ Dense,
+ Dropout,
+ )
+
+# pylint: disable=line-too-long
+
+
+# -------------------------------------
+
+# pylint: disable=too-few-public-methods
+class DeepIdClient(FacialRecognition):
+ """
+ DeepId model class
+ """
+
+ def __init__(self):
+ self.model = load_model()
+ self.model_name = "DeepId"
+ self.input_shape = (47, 55)
+ self.output_shape = 160
+
+
+def load_model(
+ url="https://github.com/serengil/deepface_models/releases/download/v1.0/deepid_keras_weights.h5",
+) -> Model:
+ """
+ Construct DeepId model, download its weights and load
+ """
+
+ myInput = Input(shape=(55, 47, 3))
+
+ x = Conv2D(20, (4, 4), name="Conv1", activation="relu", input_shape=(55, 47, 3))(myInput)
+ x = MaxPooling2D(pool_size=2, strides=2, name="Pool1")(x)
+ x = Dropout(rate=0.99, name="D1")(x)
+
+ x = Conv2D(40, (3, 3), name="Conv2", activation="relu")(x)
+ x = MaxPooling2D(pool_size=2, strides=2, name="Pool2")(x)
+ x = Dropout(rate=0.99, name="D2")(x)
+
+ x = Conv2D(60, (3, 3), name="Conv3", activation="relu")(x)
+ x = MaxPooling2D(pool_size=2, strides=2, name="Pool3")(x)
+ x = Dropout(rate=0.99, name="D3")(x)
+
+ x1 = Flatten()(x)
+ fc11 = Dense(160, name="fc11")(x1)
+
+ x2 = Conv2D(80, (2, 2), name="Conv4", activation="relu")(x)
+ x2 = Flatten()(x2)
+ fc12 = Dense(160, name="fc12")(x2)
+
+ y = Add()([fc11, fc12])
+ y = Activation("relu", name="deepid")(y)
+
+ model = Model(inputs=[myInput], outputs=y)
+
+ # ---------------------------------
+
+ home = folder_utils.get_deepface_home()
+
+ if os.path.isfile(home + "/.deepface/weights/deepid_keras_weights.h5") != True:
+ logger.info("deepid_keras_weights.h5 will be downloaded...")
+
+ output = home + "/.deepface/weights/deepid_keras_weights.h5"
+ gdown.download(url, output, quiet=False)
+
+ model.load_weights(home + "/.deepface/weights/deepid_keras_weights.h5")
+
+ return model
diff --git a/deepface/basemodels/Dlib.py b/deepface/basemodels/Dlib.py
new file mode 100644
index 0000000000000000000000000000000000000000..f13b7cad1f4c79c5f805b0da4d5fc3a03ee82f7f
--- /dev/null
+++ b/deepface/basemodels/Dlib.py
@@ -0,0 +1,89 @@
+from typing import List
+import os
+import bz2
+import gdown
+import numpy as np
+from deepface.commons import folder_utils
+from deepface.models.FacialRecognition import FacialRecognition
+from deepface.commons import logger as log
+
+logger = log.get_singletonish_logger()
+
+# pylint: disable=too-few-public-methods
+
+
+class DlibClient(FacialRecognition):
+ """
+ Dlib model class
+ """
+
+ def __init__(self):
+ self.model = DlibResNet()
+ self.model_name = "Dlib"
+ self.input_shape = (150, 150)
+ self.output_shape = 128
+
+ def forward(self, img: np.ndarray) -> List[float]:
+ """
+ Find embeddings with Dlib model.
+ This model necessitates the override of the forward method
+ because it is not a keras model.
+ Args:
+ img (np.ndarray): pre-loaded image in BGR
+ Returns
+ embeddings (list): multi-dimensional vector
+ """
+ # return self.model.predict(img)[0].tolist()
+
+ # extract_faces returns 4 dimensional images
+ if len(img.shape) == 4:
+ img = img[0]
+
+ # bgr to rgb
+ img = img[:, :, ::-1] # bgr to rgb
+
+ # img is in scale of [0, 1] but expected [0, 255]
+ if img.max() <= 1:
+ img = img * 255
+
+ img = img.astype(np.uint8)
+
+ img_representation = self.model.model.compute_face_descriptor(img)
+ img_representation = np.array(img_representation)
+ img_representation = np.expand_dims(img_representation, axis=0)
+ return img_representation[0].tolist()
+
+
+class DlibResNet:
+ def __init__(self):
+
+ ## this is not a must dependency. do not import it in the global level.
+ try:
+ import dlib
+ except ModuleNotFoundError as e:
+ raise ImportError(
+ "Dlib is an optional dependency, ensure the library is installed."
+ "Please install using 'pip install dlib' "
+ ) from e
+
+ home = folder_utils.get_deepface_home()
+ weight_file = home + "/.deepface/weights/dlib_face_recognition_resnet_model_v1.dat"
+
+ # download pre-trained model if it does not exist
+ if os.path.isfile(weight_file) != True:
+ logger.info("dlib_face_recognition_resnet_model_v1.dat is going to be downloaded")
+
+ file_name = "dlib_face_recognition_resnet_model_v1.dat.bz2"
+ url = f"http://dlib.net/files/{file_name}"
+ output = f"{home}/.deepface/weights/{file_name}"
+ gdown.download(url, output, quiet=False)
+
+ zipfile = bz2.BZ2File(output)
+ data = zipfile.read()
+ newfilepath = output[:-4] # discard .bz2 extension
+ with open(newfilepath, "wb") as f:
+ f.write(data)
+
+ self.model = dlib.face_recognition_model_v1(weight_file)
+
+ # return None # classes must return None
diff --git a/deepface/basemodels/Facenet.py b/deepface/basemodels/Facenet.py
new file mode 100644
index 0000000000000000000000000000000000000000..bcbb7b718fcb995663876ad7c22c614e4f77e5c8
--- /dev/null
+++ b/deepface/basemodels/Facenet.py
@@ -0,0 +1,1715 @@
+import os
+import gdown
+from deepface.commons import package_utils, folder_utils
+from deepface.models.FacialRecognition import FacialRecognition
+from deepface.commons import logger as log
+
+logger = log.get_singletonish_logger()
+
+# --------------------------------
+# dependency configuration
+
+tf_version = package_utils.get_tf_major_version()
+
+if tf_version == 1:
+ from keras.models import Model
+ from keras.layers import Activation
+ from keras.layers import BatchNormalization
+ from keras.layers import Concatenate
+ from keras.layers import Conv2D
+ from keras.layers import Dense
+ from keras.layers import Dropout
+ from keras.layers import GlobalAveragePooling2D
+ from keras.layers import Input
+ from keras.layers import Lambda
+ from keras.layers import MaxPooling2D
+ from keras.layers import add
+ from keras import backend as K
+else:
+ from tensorflow.keras.models import Model
+ from tensorflow.keras.layers import Activation
+ from tensorflow.keras.layers import BatchNormalization
+ from tensorflow.keras.layers import Concatenate
+ from tensorflow.keras.layers import Conv2D
+ from tensorflow.keras.layers import Dense
+ from tensorflow.keras.layers import Dropout
+ from tensorflow.keras.layers import GlobalAveragePooling2D
+ from tensorflow.keras.layers import Input
+ from tensorflow.keras.layers import Lambda
+ from tensorflow.keras.layers import MaxPooling2D
+ from tensorflow.keras.layers import add
+ from tensorflow.keras import backend as K
+
+# --------------------------------
+
+# pylint: disable=too-few-public-methods
+class FaceNet128dClient(FacialRecognition):
+ """
+ FaceNet-128d model class
+ """
+
+ def __init__(self):
+ self.model = load_facenet128d_model()
+ self.model_name = "FaceNet-128d"
+ self.input_shape = (160, 160)
+ self.output_shape = 128
+
+
+class FaceNet512dClient(FacialRecognition):
+ """
+ FaceNet-1512d model class
+ """
+
+ def __init__(self):
+ self.model = load_facenet512d_model()
+ self.model_name = "FaceNet-512d"
+ self.input_shape = (160, 160)
+ self.output_shape = 512
+
+
+def scaling(x, scale):
+ return x * scale
+
+
+def InceptionResNetV1(dimension: int = 128) -> Model:
+ """
+ InceptionResNetV1 model heavily inspired from
+ github.com/davidsandberg/facenet/blob/master/src/models/inception_resnet_v1.py
+ As mentioned in Sandberg's repo's readme, pre-trained models are using Inception ResNet v1
+ Besides training process is documented at
+ sefiks.com/2018/09/03/face-recognition-with-facenet-in-keras/
+
+ Args:
+ dimension (int): number of dimensions in the embedding layer
+ Returns:
+ model (Model)
+ """
+
+ inputs = Input(shape=(160, 160, 3))
+ x = Conv2D(32, 3, strides=2, padding="valid", use_bias=False, name="Conv2d_1a_3x3")(inputs)
+ x = BatchNormalization(
+ axis=3, momentum=0.995, epsilon=0.001, scale=False, name="Conv2d_1a_3x3_BatchNorm"
+ )(x)
+ x = Activation("relu", name="Conv2d_1a_3x3_Activation")(x)
+ x = Conv2D(32, 3, strides=1, padding="valid", use_bias=False, name="Conv2d_2a_3x3")(x)
+ x = BatchNormalization(
+ axis=3, momentum=0.995, epsilon=0.001, scale=False, name="Conv2d_2a_3x3_BatchNorm"
+ )(x)
+ x = Activation("relu", name="Conv2d_2a_3x3_Activation")(x)
+ x = Conv2D(64, 3, strides=1, padding="same", use_bias=False, name="Conv2d_2b_3x3")(x)
+ x = BatchNormalization(
+ axis=3, momentum=0.995, epsilon=0.001, scale=False, name="Conv2d_2b_3x3_BatchNorm"
+ )(x)
+ x = Activation("relu", name="Conv2d_2b_3x3_Activation")(x)
+ x = MaxPooling2D(3, strides=2, name="MaxPool_3a_3x3")(x)
+ x = Conv2D(80, 1, strides=1, padding="valid", use_bias=False, name="Conv2d_3b_1x1")(x)
+ x = BatchNormalization(
+ axis=3, momentum=0.995, epsilon=0.001, scale=False, name="Conv2d_3b_1x1_BatchNorm"
+ )(x)
+ x = Activation("relu", name="Conv2d_3b_1x1_Activation")(x)
+ x = Conv2D(192, 3, strides=1, padding="valid", use_bias=False, name="Conv2d_4a_3x3")(x)
+ x = BatchNormalization(
+ axis=3, momentum=0.995, epsilon=0.001, scale=False, name="Conv2d_4a_3x3_BatchNorm"
+ )(x)
+ x = Activation("relu", name="Conv2d_4a_3x3_Activation")(x)
+ x = Conv2D(256, 3, strides=2, padding="valid", use_bias=False, name="Conv2d_4b_3x3")(x)
+ x = BatchNormalization(
+ axis=3, momentum=0.995, epsilon=0.001, scale=False, name="Conv2d_4b_3x3_BatchNorm"
+ )(x)
+ x = Activation("relu", name="Conv2d_4b_3x3_Activation")(x)
+
+ # 5x Block35 (Inception-ResNet-A block):
+ branch_0 = Conv2D(
+ 32, 1, strides=1, padding="same", use_bias=False, name="Block35_1_Branch_0_Conv2d_1x1"
+ )(x)
+ branch_0 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block35_1_Branch_0_Conv2d_1x1_BatchNorm",
+ )(branch_0)
+ branch_0 = Activation("relu", name="Block35_1_Branch_0_Conv2d_1x1_Activation")(branch_0)
+ branch_1 = Conv2D(
+ 32, 1, strides=1, padding="same", use_bias=False, name="Block35_1_Branch_1_Conv2d_0a_1x1"
+ )(x)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block35_1_Branch_1_Conv2d_0a_1x1_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block35_1_Branch_1_Conv2d_0a_1x1_Activation")(branch_1)
+ branch_1 = Conv2D(
+ 32, 3, strides=1, padding="same", use_bias=False, name="Block35_1_Branch_1_Conv2d_0b_3x3"
+ )(branch_1)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block35_1_Branch_1_Conv2d_0b_3x3_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block35_1_Branch_1_Conv2d_0b_3x3_Activation")(branch_1)
+ branch_2 = Conv2D(
+ 32, 1, strides=1, padding="same", use_bias=False, name="Block35_1_Branch_2_Conv2d_0a_1x1"
+ )(x)
+ branch_2 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block35_1_Branch_2_Conv2d_0a_1x1_BatchNorm",
+ )(branch_2)
+ branch_2 = Activation("relu", name="Block35_1_Branch_2_Conv2d_0a_1x1_Activation")(branch_2)
+ branch_2 = Conv2D(
+ 32, 3, strides=1, padding="same", use_bias=False, name="Block35_1_Branch_2_Conv2d_0b_3x3"
+ )(branch_2)
+ branch_2 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block35_1_Branch_2_Conv2d_0b_3x3_BatchNorm",
+ )(branch_2)
+ branch_2 = Activation("relu", name="Block35_1_Branch_2_Conv2d_0b_3x3_Activation")(branch_2)
+ branch_2 = Conv2D(
+ 32, 3, strides=1, padding="same", use_bias=False, name="Block35_1_Branch_2_Conv2d_0c_3x3"
+ )(branch_2)
+ branch_2 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block35_1_Branch_2_Conv2d_0c_3x3_BatchNorm",
+ )(branch_2)
+ branch_2 = Activation("relu", name="Block35_1_Branch_2_Conv2d_0c_3x3_Activation")(branch_2)
+ branches = [branch_0, branch_1, branch_2]
+ mixed = Concatenate(axis=3, name="Block35_1_Concatenate")(branches)
+ up = Conv2D(256, 1, strides=1, padding="same", use_bias=True, name="Block35_1_Conv2d_1x1")(
+ mixed
+ )
+ up = Lambda(scaling, output_shape=K.int_shape(up)[1:], arguments={"scale": 0.17})(up)
+ x = add([x, up])
+ x = Activation("relu", name="Block35_1_Activation")(x)
+
+ branch_0 = Conv2D(
+ 32, 1, strides=1, padding="same", use_bias=False, name="Block35_2_Branch_0_Conv2d_1x1"
+ )(x)
+ branch_0 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block35_2_Branch_0_Conv2d_1x1_BatchNorm",
+ )(branch_0)
+ branch_0 = Activation("relu", name="Block35_2_Branch_0_Conv2d_1x1_Activation")(branch_0)
+ branch_1 = Conv2D(
+ 32, 1, strides=1, padding="same", use_bias=False, name="Block35_2_Branch_1_Conv2d_0a_1x1"
+ )(x)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block35_2_Branch_1_Conv2d_0a_1x1_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block35_2_Branch_1_Conv2d_0a_1x1_Activation")(branch_1)
+ branch_1 = Conv2D(
+ 32, 3, strides=1, padding="same", use_bias=False, name="Block35_2_Branch_1_Conv2d_0b_3x3"
+ )(branch_1)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block35_2_Branch_1_Conv2d_0b_3x3_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block35_2_Branch_1_Conv2d_0b_3x3_Activation")(branch_1)
+ branch_2 = Conv2D(
+ 32, 1, strides=1, padding="same", use_bias=False, name="Block35_2_Branch_2_Conv2d_0a_1x1"
+ )(x)
+ branch_2 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block35_2_Branch_2_Conv2d_0a_1x1_BatchNorm",
+ )(branch_2)
+ branch_2 = Activation("relu", name="Block35_2_Branch_2_Conv2d_0a_1x1_Activation")(branch_2)
+ branch_2 = Conv2D(
+ 32, 3, strides=1, padding="same", use_bias=False, name="Block35_2_Branch_2_Conv2d_0b_3x3"
+ )(branch_2)
+ branch_2 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block35_2_Branch_2_Conv2d_0b_3x3_BatchNorm",
+ )(branch_2)
+ branch_2 = Activation("relu", name="Block35_2_Branch_2_Conv2d_0b_3x3_Activation")(branch_2)
+ branch_2 = Conv2D(
+ 32, 3, strides=1, padding="same", use_bias=False, name="Block35_2_Branch_2_Conv2d_0c_3x3"
+ )(branch_2)
+ branch_2 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block35_2_Branch_2_Conv2d_0c_3x3_BatchNorm",
+ )(branch_2)
+ branch_2 = Activation("relu", name="Block35_2_Branch_2_Conv2d_0c_3x3_Activation")(branch_2)
+ branches = [branch_0, branch_1, branch_2]
+ mixed = Concatenate(axis=3, name="Block35_2_Concatenate")(branches)
+ up = Conv2D(256, 1, strides=1, padding="same", use_bias=True, name="Block35_2_Conv2d_1x1")(
+ mixed
+ )
+ up = Lambda(scaling, output_shape=K.int_shape(up)[1:], arguments={"scale": 0.17})(up)
+ x = add([x, up])
+ x = Activation("relu", name="Block35_2_Activation")(x)
+
+ branch_0 = Conv2D(
+ 32, 1, strides=1, padding="same", use_bias=False, name="Block35_3_Branch_0_Conv2d_1x1"
+ )(x)
+ branch_0 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block35_3_Branch_0_Conv2d_1x1_BatchNorm",
+ )(branch_0)
+ branch_0 = Activation("relu", name="Block35_3_Branch_0_Conv2d_1x1_Activation")(branch_0)
+ branch_1 = Conv2D(
+ 32, 1, strides=1, padding="same", use_bias=False, name="Block35_3_Branch_1_Conv2d_0a_1x1"
+ )(x)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block35_3_Branch_1_Conv2d_0a_1x1_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block35_3_Branch_1_Conv2d_0a_1x1_Activation")(branch_1)
+ branch_1 = Conv2D(
+ 32, 3, strides=1, padding="same", use_bias=False, name="Block35_3_Branch_1_Conv2d_0b_3x3"
+ )(branch_1)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block35_3_Branch_1_Conv2d_0b_3x3_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block35_3_Branch_1_Conv2d_0b_3x3_Activation")(branch_1)
+ branch_2 = Conv2D(
+ 32, 1, strides=1, padding="same", use_bias=False, name="Block35_3_Branch_2_Conv2d_0a_1x1"
+ )(x)
+ branch_2 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block35_3_Branch_2_Conv2d_0a_1x1_BatchNorm",
+ )(branch_2)
+ branch_2 = Activation("relu", name="Block35_3_Branch_2_Conv2d_0a_1x1_Activation")(branch_2)
+ branch_2 = Conv2D(
+ 32, 3, strides=1, padding="same", use_bias=False, name="Block35_3_Branch_2_Conv2d_0b_3x3"
+ )(branch_2)
+ branch_2 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block35_3_Branch_2_Conv2d_0b_3x3_BatchNorm",
+ )(branch_2)
+ branch_2 = Activation("relu", name="Block35_3_Branch_2_Conv2d_0b_3x3_Activation")(branch_2)
+ branch_2 = Conv2D(
+ 32, 3, strides=1, padding="same", use_bias=False, name="Block35_3_Branch_2_Conv2d_0c_3x3"
+ )(branch_2)
+ branch_2 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block35_3_Branch_2_Conv2d_0c_3x3_BatchNorm",
+ )(branch_2)
+ branch_2 = Activation("relu", name="Block35_3_Branch_2_Conv2d_0c_3x3_Activation")(branch_2)
+ branches = [branch_0, branch_1, branch_2]
+ mixed = Concatenate(axis=3, name="Block35_3_Concatenate")(branches)
+ up = Conv2D(256, 1, strides=1, padding="same", use_bias=True, name="Block35_3_Conv2d_1x1")(
+ mixed
+ )
+ up = Lambda(scaling, output_shape=K.int_shape(up)[1:], arguments={"scale": 0.17})(up)
+ x = add([x, up])
+ x = Activation("relu", name="Block35_3_Activation")(x)
+
+ branch_0 = Conv2D(
+ 32, 1, strides=1, padding="same", use_bias=False, name="Block35_4_Branch_0_Conv2d_1x1"
+ )(x)
+ branch_0 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block35_4_Branch_0_Conv2d_1x1_BatchNorm",
+ )(branch_0)
+ branch_0 = Activation("relu", name="Block35_4_Branch_0_Conv2d_1x1_Activation")(branch_0)
+ branch_1 = Conv2D(
+ 32, 1, strides=1, padding="same", use_bias=False, name="Block35_4_Branch_1_Conv2d_0a_1x1"
+ )(x)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block35_4_Branch_1_Conv2d_0a_1x1_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block35_4_Branch_1_Conv2d_0a_1x1_Activation")(branch_1)
+ branch_1 = Conv2D(
+ 32, 3, strides=1, padding="same", use_bias=False, name="Block35_4_Branch_1_Conv2d_0b_3x3"
+ )(branch_1)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block35_4_Branch_1_Conv2d_0b_3x3_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block35_4_Branch_1_Conv2d_0b_3x3_Activation")(branch_1)
+ branch_2 = Conv2D(
+ 32, 1, strides=1, padding="same", use_bias=False, name="Block35_4_Branch_2_Conv2d_0a_1x1"
+ )(x)
+ branch_2 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block35_4_Branch_2_Conv2d_0a_1x1_BatchNorm",
+ )(branch_2)
+ branch_2 = Activation("relu", name="Block35_4_Branch_2_Conv2d_0a_1x1_Activation")(branch_2)
+ branch_2 = Conv2D(
+ 32, 3, strides=1, padding="same", use_bias=False, name="Block35_4_Branch_2_Conv2d_0b_3x3"
+ )(branch_2)
+ branch_2 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block35_4_Branch_2_Conv2d_0b_3x3_BatchNorm",
+ )(branch_2)
+ branch_2 = Activation("relu", name="Block35_4_Branch_2_Conv2d_0b_3x3_Activation")(branch_2)
+ branch_2 = Conv2D(
+ 32, 3, strides=1, padding="same", use_bias=False, name="Block35_4_Branch_2_Conv2d_0c_3x3"
+ )(branch_2)
+ branch_2 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block35_4_Branch_2_Conv2d_0c_3x3_BatchNorm",
+ )(branch_2)
+ branch_2 = Activation("relu", name="Block35_4_Branch_2_Conv2d_0c_3x3_Activation")(branch_2)
+ branches = [branch_0, branch_1, branch_2]
+ mixed = Concatenate(axis=3, name="Block35_4_Concatenate")(branches)
+ up = Conv2D(256, 1, strides=1, padding="same", use_bias=True, name="Block35_4_Conv2d_1x1")(
+ mixed
+ )
+ up = Lambda(scaling, output_shape=K.int_shape(up)[1:], arguments={"scale": 0.17})(up)
+ x = add([x, up])
+ x = Activation("relu", name="Block35_4_Activation")(x)
+
+ branch_0 = Conv2D(
+ 32, 1, strides=1, padding="same", use_bias=False, name="Block35_5_Branch_0_Conv2d_1x1"
+ )(x)
+ branch_0 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block35_5_Branch_0_Conv2d_1x1_BatchNorm",
+ )(branch_0)
+ branch_0 = Activation("relu", name="Block35_5_Branch_0_Conv2d_1x1_Activation")(branch_0)
+ branch_1 = Conv2D(
+ 32, 1, strides=1, padding="same", use_bias=False, name="Block35_5_Branch_1_Conv2d_0a_1x1"
+ )(x)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block35_5_Branch_1_Conv2d_0a_1x1_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block35_5_Branch_1_Conv2d_0a_1x1_Activation")(branch_1)
+ branch_1 = Conv2D(
+ 32, 3, strides=1, padding="same", use_bias=False, name="Block35_5_Branch_1_Conv2d_0b_3x3"
+ )(branch_1)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block35_5_Branch_1_Conv2d_0b_3x3_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block35_5_Branch_1_Conv2d_0b_3x3_Activation")(branch_1)
+ branch_2 = Conv2D(
+ 32, 1, strides=1, padding="same", use_bias=False, name="Block35_5_Branch_2_Conv2d_0a_1x1"
+ )(x)
+ branch_2 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block35_5_Branch_2_Conv2d_0a_1x1_BatchNorm",
+ )(branch_2)
+ branch_2 = Activation("relu", name="Block35_5_Branch_2_Conv2d_0a_1x1_Activation")(branch_2)
+ branch_2 = Conv2D(
+ 32, 3, strides=1, padding="same", use_bias=False, name="Block35_5_Branch_2_Conv2d_0b_3x3"
+ )(branch_2)
+ branch_2 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block35_5_Branch_2_Conv2d_0b_3x3_BatchNorm",
+ )(branch_2)
+ branch_2 = Activation("relu", name="Block35_5_Branch_2_Conv2d_0b_3x3_Activation")(branch_2)
+ branch_2 = Conv2D(
+ 32, 3, strides=1, padding="same", use_bias=False, name="Block35_5_Branch_2_Conv2d_0c_3x3"
+ )(branch_2)
+ branch_2 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block35_5_Branch_2_Conv2d_0c_3x3_BatchNorm",
+ )(branch_2)
+ branch_2 = Activation("relu", name="Block35_5_Branch_2_Conv2d_0c_3x3_Activation")(branch_2)
+ branches = [branch_0, branch_1, branch_2]
+ mixed = Concatenate(axis=3, name="Block35_5_Concatenate")(branches)
+ up = Conv2D(256, 1, strides=1, padding="same", use_bias=True, name="Block35_5_Conv2d_1x1")(
+ mixed
+ )
+ up = Lambda(scaling, output_shape=K.int_shape(up)[1:], arguments={"scale": 0.17})(up)
+ x = add([x, up])
+ x = Activation("relu", name="Block35_5_Activation")(x)
+
+ # Mixed 6a (Reduction-A block):
+ branch_0 = Conv2D(
+ 384, 3, strides=2, padding="valid", use_bias=False, name="Mixed_6a_Branch_0_Conv2d_1a_3x3"
+ )(x)
+ branch_0 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Mixed_6a_Branch_0_Conv2d_1a_3x3_BatchNorm",
+ )(branch_0)
+ branch_0 = Activation("relu", name="Mixed_6a_Branch_0_Conv2d_1a_3x3_Activation")(branch_0)
+ branch_1 = Conv2D(
+ 192, 1, strides=1, padding="same", use_bias=False, name="Mixed_6a_Branch_1_Conv2d_0a_1x1"
+ )(x)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Mixed_6a_Branch_1_Conv2d_0a_1x1_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Mixed_6a_Branch_1_Conv2d_0a_1x1_Activation")(branch_1)
+ branch_1 = Conv2D(
+ 192, 3, strides=1, padding="same", use_bias=False, name="Mixed_6a_Branch_1_Conv2d_0b_3x3"
+ )(branch_1)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Mixed_6a_Branch_1_Conv2d_0b_3x3_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Mixed_6a_Branch_1_Conv2d_0b_3x3_Activation")(branch_1)
+ branch_1 = Conv2D(
+ 256, 3, strides=2, padding="valid", use_bias=False, name="Mixed_6a_Branch_1_Conv2d_1a_3x3"
+ )(branch_1)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Mixed_6a_Branch_1_Conv2d_1a_3x3_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Mixed_6a_Branch_1_Conv2d_1a_3x3_Activation")(branch_1)
+ branch_pool = MaxPooling2D(
+ 3, strides=2, padding="valid", name="Mixed_6a_Branch_2_MaxPool_1a_3x3"
+ )(x)
+ branches = [branch_0, branch_1, branch_pool]
+ x = Concatenate(axis=3, name="Mixed_6a")(branches)
+
+ # 10x Block17 (Inception-ResNet-B block):
+ branch_0 = Conv2D(
+ 128, 1, strides=1, padding="same", use_bias=False, name="Block17_1_Branch_0_Conv2d_1x1"
+ )(x)
+ branch_0 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block17_1_Branch_0_Conv2d_1x1_BatchNorm",
+ )(branch_0)
+ branch_0 = Activation("relu", name="Block17_1_Branch_0_Conv2d_1x1_Activation")(branch_0)
+ branch_1 = Conv2D(
+ 128, 1, strides=1, padding="same", use_bias=False, name="Block17_1_Branch_1_Conv2d_0a_1x1"
+ )(x)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block17_1_Branch_1_Conv2d_0a_1x1_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block17_1_Branch_1_Conv2d_0a_1x1_Activation")(branch_1)
+ branch_1 = Conv2D(
+ 128,
+ [1, 7],
+ strides=1,
+ padding="same",
+ use_bias=False,
+ name="Block17_1_Branch_1_Conv2d_0b_1x7",
+ )(branch_1)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block17_1_Branch_1_Conv2d_0b_1x7_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block17_1_Branch_1_Conv2d_0b_1x7_Activation")(branch_1)
+ branch_1 = Conv2D(
+ 128,
+ [7, 1],
+ strides=1,
+ padding="same",
+ use_bias=False,
+ name="Block17_1_Branch_1_Conv2d_0c_7x1",
+ )(branch_1)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block17_1_Branch_1_Conv2d_0c_7x1_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block17_1_Branch_1_Conv2d_0c_7x1_Activation")(branch_1)
+ branches = [branch_0, branch_1]
+ mixed = Concatenate(axis=3, name="Block17_1_Concatenate")(branches)
+ up = Conv2D(896, 1, strides=1, padding="same", use_bias=True, name="Block17_1_Conv2d_1x1")(
+ mixed
+ )
+ up = Lambda(scaling, output_shape=K.int_shape(up)[1:], arguments={"scale": 0.1})(up)
+ x = add([x, up])
+ x = Activation("relu", name="Block17_1_Activation")(x)
+
+ branch_0 = Conv2D(
+ 128, 1, strides=1, padding="same", use_bias=False, name="Block17_2_Branch_0_Conv2d_1x1"
+ )(x)
+ branch_0 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block17_2_Branch_0_Conv2d_1x1_BatchNorm",
+ )(branch_0)
+ branch_0 = Activation("relu", name="Block17_2_Branch_0_Conv2d_1x1_Activation")(branch_0)
+ branch_1 = Conv2D(
+ 128, 1, strides=1, padding="same", use_bias=False, name="Block17_2_Branch_2_Conv2d_0a_1x1"
+ )(x)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block17_2_Branch_2_Conv2d_0a_1x1_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block17_2_Branch_2_Conv2d_0a_1x1_Activation")(branch_1)
+ branch_1 = Conv2D(
+ 128,
+ [1, 7],
+ strides=1,
+ padding="same",
+ use_bias=False,
+ name="Block17_2_Branch_2_Conv2d_0b_1x7",
+ )(branch_1)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block17_2_Branch_2_Conv2d_0b_1x7_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block17_2_Branch_2_Conv2d_0b_1x7_Activation")(branch_1)
+ branch_1 = Conv2D(
+ 128,
+ [7, 1],
+ strides=1,
+ padding="same",
+ use_bias=False,
+ name="Block17_2_Branch_2_Conv2d_0c_7x1",
+ )(branch_1)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block17_2_Branch_2_Conv2d_0c_7x1_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block17_2_Branch_2_Conv2d_0c_7x1_Activation")(branch_1)
+ branches = [branch_0, branch_1]
+ mixed = Concatenate(axis=3, name="Block17_2_Concatenate")(branches)
+ up = Conv2D(896, 1, strides=1, padding="same", use_bias=True, name="Block17_2_Conv2d_1x1")(
+ mixed
+ )
+ up = Lambda(scaling, output_shape=K.int_shape(up)[1:], arguments={"scale": 0.1})(up)
+ x = add([x, up])
+ x = Activation("relu", name="Block17_2_Activation")(x)
+
+ branch_0 = Conv2D(
+ 128, 1, strides=1, padding="same", use_bias=False, name="Block17_3_Branch_0_Conv2d_1x1"
+ )(x)
+ branch_0 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block17_3_Branch_0_Conv2d_1x1_BatchNorm",
+ )(branch_0)
+ branch_0 = Activation("relu", name="Block17_3_Branch_0_Conv2d_1x1_Activation")(branch_0)
+ branch_1 = Conv2D(
+ 128, 1, strides=1, padding="same", use_bias=False, name="Block17_3_Branch_3_Conv2d_0a_1x1"
+ )(x)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block17_3_Branch_3_Conv2d_0a_1x1_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block17_3_Branch_3_Conv2d_0a_1x1_Activation")(branch_1)
+ branch_1 = Conv2D(
+ 128,
+ [1, 7],
+ strides=1,
+ padding="same",
+ use_bias=False,
+ name="Block17_3_Branch_3_Conv2d_0b_1x7",
+ )(branch_1)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block17_3_Branch_3_Conv2d_0b_1x7_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block17_3_Branch_3_Conv2d_0b_1x7_Activation")(branch_1)
+ branch_1 = Conv2D(
+ 128,
+ [7, 1],
+ strides=1,
+ padding="same",
+ use_bias=False,
+ name="Block17_3_Branch_3_Conv2d_0c_7x1",
+ )(branch_1)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block17_3_Branch_3_Conv2d_0c_7x1_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block17_3_Branch_3_Conv2d_0c_7x1_Activation")(branch_1)
+ branches = [branch_0, branch_1]
+ mixed = Concatenate(axis=3, name="Block17_3_Concatenate")(branches)
+ up = Conv2D(896, 1, strides=1, padding="same", use_bias=True, name="Block17_3_Conv2d_1x1")(
+ mixed
+ )
+ up = Lambda(scaling, output_shape=K.int_shape(up)[1:], arguments={"scale": 0.1})(up)
+ x = add([x, up])
+ x = Activation("relu", name="Block17_3_Activation")(x)
+
+ branch_0 = Conv2D(
+ 128, 1, strides=1, padding="same", use_bias=False, name="Block17_4_Branch_0_Conv2d_1x1"
+ )(x)
+ branch_0 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block17_4_Branch_0_Conv2d_1x1_BatchNorm",
+ )(branch_0)
+ branch_0 = Activation("relu", name="Block17_4_Branch_0_Conv2d_1x1_Activation")(branch_0)
+ branch_1 = Conv2D(
+ 128, 1, strides=1, padding="same", use_bias=False, name="Block17_4_Branch_4_Conv2d_0a_1x1"
+ )(x)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block17_4_Branch_4_Conv2d_0a_1x1_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block17_4_Branch_4_Conv2d_0a_1x1_Activation")(branch_1)
+ branch_1 = Conv2D(
+ 128,
+ [1, 7],
+ strides=1,
+ padding="same",
+ use_bias=False,
+ name="Block17_4_Branch_4_Conv2d_0b_1x7",
+ )(branch_1)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block17_4_Branch_4_Conv2d_0b_1x7_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block17_4_Branch_4_Conv2d_0b_1x7_Activation")(branch_1)
+ branch_1 = Conv2D(
+ 128,
+ [7, 1],
+ strides=1,
+ padding="same",
+ use_bias=False,
+ name="Block17_4_Branch_4_Conv2d_0c_7x1",
+ )(branch_1)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block17_4_Branch_4_Conv2d_0c_7x1_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block17_4_Branch_4_Conv2d_0c_7x1_Activation")(branch_1)
+ branches = [branch_0, branch_1]
+ mixed = Concatenate(axis=3, name="Block17_4_Concatenate")(branches)
+ up = Conv2D(896, 1, strides=1, padding="same", use_bias=True, name="Block17_4_Conv2d_1x1")(
+ mixed
+ )
+ up = Lambda(scaling, output_shape=K.int_shape(up)[1:], arguments={"scale": 0.1})(up)
+ x = add([x, up])
+ x = Activation("relu", name="Block17_4_Activation")(x)
+
+ branch_0 = Conv2D(
+ 128, 1, strides=1, padding="same", use_bias=False, name="Block17_5_Branch_0_Conv2d_1x1"
+ )(x)
+ branch_0 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block17_5_Branch_0_Conv2d_1x1_BatchNorm",
+ )(branch_0)
+ branch_0 = Activation("relu", name="Block17_5_Branch_0_Conv2d_1x1_Activation")(branch_0)
+ branch_1 = Conv2D(
+ 128, 1, strides=1, padding="same", use_bias=False, name="Block17_5_Branch_5_Conv2d_0a_1x1"
+ )(x)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block17_5_Branch_5_Conv2d_0a_1x1_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block17_5_Branch_5_Conv2d_0a_1x1_Activation")(branch_1)
+ branch_1 = Conv2D(
+ 128,
+ [1, 7],
+ strides=1,
+ padding="same",
+ use_bias=False,
+ name="Block17_5_Branch_5_Conv2d_0b_1x7",
+ )(branch_1)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block17_5_Branch_5_Conv2d_0b_1x7_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block17_5_Branch_5_Conv2d_0b_1x7_Activation")(branch_1)
+ branch_1 = Conv2D(
+ 128,
+ [7, 1],
+ strides=1,
+ padding="same",
+ use_bias=False,
+ name="Block17_5_Branch_5_Conv2d_0c_7x1",
+ )(branch_1)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block17_5_Branch_5_Conv2d_0c_7x1_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block17_5_Branch_5_Conv2d_0c_7x1_Activation")(branch_1)
+ branches = [branch_0, branch_1]
+ mixed = Concatenate(axis=3, name="Block17_5_Concatenate")(branches)
+ up = Conv2D(896, 1, strides=1, padding="same", use_bias=True, name="Block17_5_Conv2d_1x1")(
+ mixed
+ )
+ up = Lambda(scaling, output_shape=K.int_shape(up)[1:], arguments={"scale": 0.1})(up)
+ x = add([x, up])
+ x = Activation("relu", name="Block17_5_Activation")(x)
+
+ branch_0 = Conv2D(
+ 128, 1, strides=1, padding="same", use_bias=False, name="Block17_6_Branch_0_Conv2d_1x1"
+ )(x)
+ branch_0 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block17_6_Branch_0_Conv2d_1x1_BatchNorm",
+ )(branch_0)
+ branch_0 = Activation("relu", name="Block17_6_Branch_0_Conv2d_1x1_Activation")(branch_0)
+ branch_1 = Conv2D(
+ 128, 1, strides=1, padding="same", use_bias=False, name="Block17_6_Branch_6_Conv2d_0a_1x1"
+ )(x)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block17_6_Branch_6_Conv2d_0a_1x1_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block17_6_Branch_6_Conv2d_0a_1x1_Activation")(branch_1)
+ branch_1 = Conv2D(
+ 128,
+ [1, 7],
+ strides=1,
+ padding="same",
+ use_bias=False,
+ name="Block17_6_Branch_6_Conv2d_0b_1x7",
+ )(branch_1)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block17_6_Branch_6_Conv2d_0b_1x7_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block17_6_Branch_6_Conv2d_0b_1x7_Activation")(branch_1)
+ branch_1 = Conv2D(
+ 128,
+ [7, 1],
+ strides=1,
+ padding="same",
+ use_bias=False,
+ name="Block17_6_Branch_6_Conv2d_0c_7x1",
+ )(branch_1)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block17_6_Branch_6_Conv2d_0c_7x1_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block17_6_Branch_6_Conv2d_0c_7x1_Activation")(branch_1)
+ branches = [branch_0, branch_1]
+ mixed = Concatenate(axis=3, name="Block17_6_Concatenate")(branches)
+ up = Conv2D(896, 1, strides=1, padding="same", use_bias=True, name="Block17_6_Conv2d_1x1")(
+ mixed
+ )
+ up = Lambda(scaling, output_shape=K.int_shape(up)[1:], arguments={"scale": 0.1})(up)
+ x = add([x, up])
+ x = Activation("relu", name="Block17_6_Activation")(x)
+
+ branch_0 = Conv2D(
+ 128, 1, strides=1, padding="same", use_bias=False, name="Block17_7_Branch_0_Conv2d_1x1"
+ )(x)
+ branch_0 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block17_7_Branch_0_Conv2d_1x1_BatchNorm",
+ )(branch_0)
+ branch_0 = Activation("relu", name="Block17_7_Branch_0_Conv2d_1x1_Activation")(branch_0)
+ branch_1 = Conv2D(
+ 128, 1, strides=1, padding="same", use_bias=False, name="Block17_7_Branch_7_Conv2d_0a_1x1"
+ )(x)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block17_7_Branch_7_Conv2d_0a_1x1_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block17_7_Branch_7_Conv2d_0a_1x1_Activation")(branch_1)
+ branch_1 = Conv2D(
+ 128,
+ [1, 7],
+ strides=1,
+ padding="same",
+ use_bias=False,
+ name="Block17_7_Branch_7_Conv2d_0b_1x7",
+ )(branch_1)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block17_7_Branch_7_Conv2d_0b_1x7_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block17_7_Branch_7_Conv2d_0b_1x7_Activation")(branch_1)
+ branch_1 = Conv2D(
+ 128,
+ [7, 1],
+ strides=1,
+ padding="same",
+ use_bias=False,
+ name="Block17_7_Branch_7_Conv2d_0c_7x1",
+ )(branch_1)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block17_7_Branch_7_Conv2d_0c_7x1_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block17_7_Branch_7_Conv2d_0c_7x1_Activation")(branch_1)
+ branches = [branch_0, branch_1]
+ mixed = Concatenate(axis=3, name="Block17_7_Concatenate")(branches)
+ up = Conv2D(896, 1, strides=1, padding="same", use_bias=True, name="Block17_7_Conv2d_1x1")(
+ mixed
+ )
+ up = Lambda(scaling, output_shape=K.int_shape(up)[1:], arguments={"scale": 0.1})(up)
+ x = add([x, up])
+ x = Activation("relu", name="Block17_7_Activation")(x)
+
+ branch_0 = Conv2D(
+ 128, 1, strides=1, padding="same", use_bias=False, name="Block17_8_Branch_0_Conv2d_1x1"
+ )(x)
+ branch_0 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block17_8_Branch_0_Conv2d_1x1_BatchNorm",
+ )(branch_0)
+ branch_0 = Activation("relu", name="Block17_8_Branch_0_Conv2d_1x1_Activation")(branch_0)
+ branch_1 = Conv2D(
+ 128, 1, strides=1, padding="same", use_bias=False, name="Block17_8_Branch_8_Conv2d_0a_1x1"
+ )(x)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block17_8_Branch_8_Conv2d_0a_1x1_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block17_8_Branch_8_Conv2d_0a_1x1_Activation")(branch_1)
+ branch_1 = Conv2D(
+ 128,
+ [1, 7],
+ strides=1,
+ padding="same",
+ use_bias=False,
+ name="Block17_8_Branch_8_Conv2d_0b_1x7",
+ )(branch_1)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block17_8_Branch_8_Conv2d_0b_1x7_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block17_8_Branch_8_Conv2d_0b_1x7_Activation")(branch_1)
+ branch_1 = Conv2D(
+ 128,
+ [7, 1],
+ strides=1,
+ padding="same",
+ use_bias=False,
+ name="Block17_8_Branch_8_Conv2d_0c_7x1",
+ )(branch_1)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block17_8_Branch_8_Conv2d_0c_7x1_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block17_8_Branch_8_Conv2d_0c_7x1_Activation")(branch_1)
+ branches = [branch_0, branch_1]
+ mixed = Concatenate(axis=3, name="Block17_8_Concatenate")(branches)
+ up = Conv2D(896, 1, strides=1, padding="same", use_bias=True, name="Block17_8_Conv2d_1x1")(
+ mixed
+ )
+ up = Lambda(scaling, output_shape=K.int_shape(up)[1:], arguments={"scale": 0.1})(up)
+ x = add([x, up])
+ x = Activation("relu", name="Block17_8_Activation")(x)
+
+ branch_0 = Conv2D(
+ 128, 1, strides=1, padding="same", use_bias=False, name="Block17_9_Branch_0_Conv2d_1x1"
+ )(x)
+ branch_0 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block17_9_Branch_0_Conv2d_1x1_BatchNorm",
+ )(branch_0)
+ branch_0 = Activation("relu", name="Block17_9_Branch_0_Conv2d_1x1_Activation")(branch_0)
+ branch_1 = Conv2D(
+ 128, 1, strides=1, padding="same", use_bias=False, name="Block17_9_Branch_9_Conv2d_0a_1x1"
+ )(x)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block17_9_Branch_9_Conv2d_0a_1x1_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block17_9_Branch_9_Conv2d_0a_1x1_Activation")(branch_1)
+ branch_1 = Conv2D(
+ 128,
+ [1, 7],
+ strides=1,
+ padding="same",
+ use_bias=False,
+ name="Block17_9_Branch_9_Conv2d_0b_1x7",
+ )(branch_1)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block17_9_Branch_9_Conv2d_0b_1x7_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block17_9_Branch_9_Conv2d_0b_1x7_Activation")(branch_1)
+ branch_1 = Conv2D(
+ 128,
+ [7, 1],
+ strides=1,
+ padding="same",
+ use_bias=False,
+ name="Block17_9_Branch_9_Conv2d_0c_7x1",
+ )(branch_1)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block17_9_Branch_9_Conv2d_0c_7x1_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block17_9_Branch_9_Conv2d_0c_7x1_Activation")(branch_1)
+ branches = [branch_0, branch_1]
+ mixed = Concatenate(axis=3, name="Block17_9_Concatenate")(branches)
+ up = Conv2D(896, 1, strides=1, padding="same", use_bias=True, name="Block17_9_Conv2d_1x1")(
+ mixed
+ )
+ up = Lambda(scaling, output_shape=K.int_shape(up)[1:], arguments={"scale": 0.1})(up)
+ x = add([x, up])
+ x = Activation("relu", name="Block17_9_Activation")(x)
+
+ branch_0 = Conv2D(
+ 128, 1, strides=1, padding="same", use_bias=False, name="Block17_10_Branch_0_Conv2d_1x1"
+ )(x)
+ branch_0 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block17_10_Branch_0_Conv2d_1x1_BatchNorm",
+ )(branch_0)
+ branch_0 = Activation("relu", name="Block17_10_Branch_0_Conv2d_1x1_Activation")(branch_0)
+ branch_1 = Conv2D(
+ 128, 1, strides=1, padding="same", use_bias=False, name="Block17_10_Branch_10_Conv2d_0a_1x1"
+ )(x)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block17_10_Branch_10_Conv2d_0a_1x1_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block17_10_Branch_10_Conv2d_0a_1x1_Activation")(branch_1)
+ branch_1 = Conv2D(
+ 128,
+ [1, 7],
+ strides=1,
+ padding="same",
+ use_bias=False,
+ name="Block17_10_Branch_10_Conv2d_0b_1x7",
+ )(branch_1)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block17_10_Branch_10_Conv2d_0b_1x7_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block17_10_Branch_10_Conv2d_0b_1x7_Activation")(branch_1)
+ branch_1 = Conv2D(
+ 128,
+ [7, 1],
+ strides=1,
+ padding="same",
+ use_bias=False,
+ name="Block17_10_Branch_10_Conv2d_0c_7x1",
+ )(branch_1)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block17_10_Branch_10_Conv2d_0c_7x1_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block17_10_Branch_10_Conv2d_0c_7x1_Activation")(branch_1)
+ branches = [branch_0, branch_1]
+ mixed = Concatenate(axis=3, name="Block17_10_Concatenate")(branches)
+ up = Conv2D(896, 1, strides=1, padding="same", use_bias=True, name="Block17_10_Conv2d_1x1")(
+ mixed
+ )
+ up = Lambda(scaling, output_shape=K.int_shape(up)[1:], arguments={"scale": 0.1})(up)
+ x = add([x, up])
+ x = Activation("relu", name="Block17_10_Activation")(x)
+
+ # Mixed 7a (Reduction-B block): 8 x 8 x 2080
+ branch_0 = Conv2D(
+ 256, 1, strides=1, padding="same", use_bias=False, name="Mixed_7a_Branch_0_Conv2d_0a_1x1"
+ )(x)
+ branch_0 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Mixed_7a_Branch_0_Conv2d_0a_1x1_BatchNorm",
+ )(branch_0)
+ branch_0 = Activation("relu", name="Mixed_7a_Branch_0_Conv2d_0a_1x1_Activation")(branch_0)
+ branch_0 = Conv2D(
+ 384, 3, strides=2, padding="valid", use_bias=False, name="Mixed_7a_Branch_0_Conv2d_1a_3x3"
+ )(branch_0)
+ branch_0 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Mixed_7a_Branch_0_Conv2d_1a_3x3_BatchNorm",
+ )(branch_0)
+ branch_0 = Activation("relu", name="Mixed_7a_Branch_0_Conv2d_1a_3x3_Activation")(branch_0)
+ branch_1 = Conv2D(
+ 256, 1, strides=1, padding="same", use_bias=False, name="Mixed_7a_Branch_1_Conv2d_0a_1x1"
+ )(x)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Mixed_7a_Branch_1_Conv2d_0a_1x1_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Mixed_7a_Branch_1_Conv2d_0a_1x1_Activation")(branch_1)
+ branch_1 = Conv2D(
+ 256, 3, strides=2, padding="valid", use_bias=False, name="Mixed_7a_Branch_1_Conv2d_1a_3x3"
+ )(branch_1)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Mixed_7a_Branch_1_Conv2d_1a_3x3_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Mixed_7a_Branch_1_Conv2d_1a_3x3_Activation")(branch_1)
+ branch_2 = Conv2D(
+ 256, 1, strides=1, padding="same", use_bias=False, name="Mixed_7a_Branch_2_Conv2d_0a_1x1"
+ )(x)
+ branch_2 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Mixed_7a_Branch_2_Conv2d_0a_1x1_BatchNorm",
+ )(branch_2)
+ branch_2 = Activation("relu", name="Mixed_7a_Branch_2_Conv2d_0a_1x1_Activation")(branch_2)
+ branch_2 = Conv2D(
+ 256, 3, strides=1, padding="same", use_bias=False, name="Mixed_7a_Branch_2_Conv2d_0b_3x3"
+ )(branch_2)
+ branch_2 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Mixed_7a_Branch_2_Conv2d_0b_3x3_BatchNorm",
+ )(branch_2)
+ branch_2 = Activation("relu", name="Mixed_7a_Branch_2_Conv2d_0b_3x3_Activation")(branch_2)
+ branch_2 = Conv2D(
+ 256, 3, strides=2, padding="valid", use_bias=False, name="Mixed_7a_Branch_2_Conv2d_1a_3x3"
+ )(branch_2)
+ branch_2 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Mixed_7a_Branch_2_Conv2d_1a_3x3_BatchNorm",
+ )(branch_2)
+ branch_2 = Activation("relu", name="Mixed_7a_Branch_2_Conv2d_1a_3x3_Activation")(branch_2)
+ branch_pool = MaxPooling2D(
+ 3, strides=2, padding="valid", name="Mixed_7a_Branch_3_MaxPool_1a_3x3"
+ )(x)
+ branches = [branch_0, branch_1, branch_2, branch_pool]
+ x = Concatenate(axis=3, name="Mixed_7a")(branches)
+
+ # 5x Block8 (Inception-ResNet-C block):
+
+ branch_0 = Conv2D(
+ 192, 1, strides=1, padding="same", use_bias=False, name="Block8_1_Branch_0_Conv2d_1x1"
+ )(x)
+ branch_0 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block8_1_Branch_0_Conv2d_1x1_BatchNorm",
+ )(branch_0)
+ branch_0 = Activation("relu", name="Block8_1_Branch_0_Conv2d_1x1_Activation")(branch_0)
+ branch_1 = Conv2D(
+ 192, 1, strides=1, padding="same", use_bias=False, name="Block8_1_Branch_1_Conv2d_0a_1x1"
+ )(x)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block8_1_Branch_1_Conv2d_0a_1x1_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block8_1_Branch_1_Conv2d_0a_1x1_Activation")(branch_1)
+ branch_1 = Conv2D(
+ 192,
+ [1, 3],
+ strides=1,
+ padding="same",
+ use_bias=False,
+ name="Block8_1_Branch_1_Conv2d_0b_1x3",
+ )(branch_1)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block8_1_Branch_1_Conv2d_0b_1x3_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block8_1_Branch_1_Conv2d_0b_1x3_Activation")(branch_1)
+ branch_1 = Conv2D(
+ 192,
+ [3, 1],
+ strides=1,
+ padding="same",
+ use_bias=False,
+ name="Block8_1_Branch_1_Conv2d_0c_3x1",
+ )(branch_1)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block8_1_Branch_1_Conv2d_0c_3x1_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block8_1_Branch_1_Conv2d_0c_3x1_Activation")(branch_1)
+ branches = [branch_0, branch_1]
+ mixed = Concatenate(axis=3, name="Block8_1_Concatenate")(branches)
+ up = Conv2D(1792, 1, strides=1, padding="same", use_bias=True, name="Block8_1_Conv2d_1x1")(
+ mixed
+ )
+ up = Lambda(scaling, output_shape=K.int_shape(up)[1:], arguments={"scale": 0.2})(up)
+ x = add([x, up])
+ x = Activation("relu", name="Block8_1_Activation")(x)
+
+ branch_0 = Conv2D(
+ 192, 1, strides=1, padding="same", use_bias=False, name="Block8_2_Branch_0_Conv2d_1x1"
+ )(x)
+ branch_0 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block8_2_Branch_0_Conv2d_1x1_BatchNorm",
+ )(branch_0)
+ branch_0 = Activation("relu", name="Block8_2_Branch_0_Conv2d_1x1_Activation")(branch_0)
+ branch_1 = Conv2D(
+ 192, 1, strides=1, padding="same", use_bias=False, name="Block8_2_Branch_2_Conv2d_0a_1x1"
+ )(x)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block8_2_Branch_2_Conv2d_0a_1x1_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block8_2_Branch_2_Conv2d_0a_1x1_Activation")(branch_1)
+ branch_1 = Conv2D(
+ 192,
+ [1, 3],
+ strides=1,
+ padding="same",
+ use_bias=False,
+ name="Block8_2_Branch_2_Conv2d_0b_1x3",
+ )(branch_1)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block8_2_Branch_2_Conv2d_0b_1x3_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block8_2_Branch_2_Conv2d_0b_1x3_Activation")(branch_1)
+ branch_1 = Conv2D(
+ 192,
+ [3, 1],
+ strides=1,
+ padding="same",
+ use_bias=False,
+ name="Block8_2_Branch_2_Conv2d_0c_3x1",
+ )(branch_1)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block8_2_Branch_2_Conv2d_0c_3x1_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block8_2_Branch_2_Conv2d_0c_3x1_Activation")(branch_1)
+ branches = [branch_0, branch_1]
+ mixed = Concatenate(axis=3, name="Block8_2_Concatenate")(branches)
+ up = Conv2D(1792, 1, strides=1, padding="same", use_bias=True, name="Block8_2_Conv2d_1x1")(
+ mixed
+ )
+ up = Lambda(scaling, output_shape=K.int_shape(up)[1:], arguments={"scale": 0.2})(up)
+ x = add([x, up])
+ x = Activation("relu", name="Block8_2_Activation")(x)
+
+ branch_0 = Conv2D(
+ 192, 1, strides=1, padding="same", use_bias=False, name="Block8_3_Branch_0_Conv2d_1x1"
+ )(x)
+ branch_0 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block8_3_Branch_0_Conv2d_1x1_BatchNorm",
+ )(branch_0)
+ branch_0 = Activation("relu", name="Block8_3_Branch_0_Conv2d_1x1_Activation")(branch_0)
+ branch_1 = Conv2D(
+ 192, 1, strides=1, padding="same", use_bias=False, name="Block8_3_Branch_3_Conv2d_0a_1x1"
+ )(x)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block8_3_Branch_3_Conv2d_0a_1x1_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block8_3_Branch_3_Conv2d_0a_1x1_Activation")(branch_1)
+ branch_1 = Conv2D(
+ 192,
+ [1, 3],
+ strides=1,
+ padding="same",
+ use_bias=False,
+ name="Block8_3_Branch_3_Conv2d_0b_1x3",
+ )(branch_1)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block8_3_Branch_3_Conv2d_0b_1x3_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block8_3_Branch_3_Conv2d_0b_1x3_Activation")(branch_1)
+ branch_1 = Conv2D(
+ 192,
+ [3, 1],
+ strides=1,
+ padding="same",
+ use_bias=False,
+ name="Block8_3_Branch_3_Conv2d_0c_3x1",
+ )(branch_1)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block8_3_Branch_3_Conv2d_0c_3x1_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block8_3_Branch_3_Conv2d_0c_3x1_Activation")(branch_1)
+ branches = [branch_0, branch_1]
+ mixed = Concatenate(axis=3, name="Block8_3_Concatenate")(branches)
+ up = Conv2D(1792, 1, strides=1, padding="same", use_bias=True, name="Block8_3_Conv2d_1x1")(
+ mixed
+ )
+ up = Lambda(scaling, output_shape=K.int_shape(up)[1:], arguments={"scale": 0.2})(up)
+ x = add([x, up])
+ x = Activation("relu", name="Block8_3_Activation")(x)
+
+ branch_0 = Conv2D(
+ 192, 1, strides=1, padding="same", use_bias=False, name="Block8_4_Branch_0_Conv2d_1x1"
+ )(x)
+ branch_0 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block8_4_Branch_0_Conv2d_1x1_BatchNorm",
+ )(branch_0)
+ branch_0 = Activation("relu", name="Block8_4_Branch_0_Conv2d_1x1_Activation")(branch_0)
+ branch_1 = Conv2D(
+ 192, 1, strides=1, padding="same", use_bias=False, name="Block8_4_Branch_4_Conv2d_0a_1x1"
+ )(x)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block8_4_Branch_4_Conv2d_0a_1x1_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block8_4_Branch_4_Conv2d_0a_1x1_Activation")(branch_1)
+ branch_1 = Conv2D(
+ 192,
+ [1, 3],
+ strides=1,
+ padding="same",
+ use_bias=False,
+ name="Block8_4_Branch_4_Conv2d_0b_1x3",
+ )(branch_1)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block8_4_Branch_4_Conv2d_0b_1x3_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block8_4_Branch_4_Conv2d_0b_1x3_Activation")(branch_1)
+ branch_1 = Conv2D(
+ 192,
+ [3, 1],
+ strides=1,
+ padding="same",
+ use_bias=False,
+ name="Block8_4_Branch_4_Conv2d_0c_3x1",
+ )(branch_1)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block8_4_Branch_4_Conv2d_0c_3x1_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block8_4_Branch_4_Conv2d_0c_3x1_Activation")(branch_1)
+ branches = [branch_0, branch_1]
+ mixed = Concatenate(axis=3, name="Block8_4_Concatenate")(branches)
+ up = Conv2D(1792, 1, strides=1, padding="same", use_bias=True, name="Block8_4_Conv2d_1x1")(
+ mixed
+ )
+ up = Lambda(scaling, output_shape=K.int_shape(up)[1:], arguments={"scale": 0.2})(up)
+ x = add([x, up])
+ x = Activation("relu", name="Block8_4_Activation")(x)
+
+ branch_0 = Conv2D(
+ 192, 1, strides=1, padding="same", use_bias=False, name="Block8_5_Branch_0_Conv2d_1x1"
+ )(x)
+ branch_0 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block8_5_Branch_0_Conv2d_1x1_BatchNorm",
+ )(branch_0)
+ branch_0 = Activation("relu", name="Block8_5_Branch_0_Conv2d_1x1_Activation")(branch_0)
+ branch_1 = Conv2D(
+ 192, 1, strides=1, padding="same", use_bias=False, name="Block8_5_Branch_5_Conv2d_0a_1x1"
+ )(x)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block8_5_Branch_5_Conv2d_0a_1x1_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block8_5_Branch_5_Conv2d_0a_1x1_Activation")(branch_1)
+ branch_1 = Conv2D(
+ 192,
+ [1, 3],
+ strides=1,
+ padding="same",
+ use_bias=False,
+ name="Block8_5_Branch_5_Conv2d_0b_1x3",
+ )(branch_1)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block8_5_Branch_5_Conv2d_0b_1x3_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block8_5_Branch_5_Conv2d_0b_1x3_Activation")(branch_1)
+ branch_1 = Conv2D(
+ 192,
+ [3, 1],
+ strides=1,
+ padding="same",
+ use_bias=False,
+ name="Block8_5_Branch_5_Conv2d_0c_3x1",
+ )(branch_1)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block8_5_Branch_5_Conv2d_0c_3x1_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block8_5_Branch_5_Conv2d_0c_3x1_Activation")(branch_1)
+ branches = [branch_0, branch_1]
+ mixed = Concatenate(axis=3, name="Block8_5_Concatenate")(branches)
+ up = Conv2D(1792, 1, strides=1, padding="same", use_bias=True, name="Block8_5_Conv2d_1x1")(
+ mixed
+ )
+ up = Lambda(scaling, output_shape=K.int_shape(up)[1:], arguments={"scale": 0.2})(up)
+ x = add([x, up])
+ x = Activation("relu", name="Block8_5_Activation")(x)
+
+ branch_0 = Conv2D(
+ 192, 1, strides=1, padding="same", use_bias=False, name="Block8_6_Branch_0_Conv2d_1x1"
+ )(x)
+ branch_0 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block8_6_Branch_0_Conv2d_1x1_BatchNorm",
+ )(branch_0)
+ branch_0 = Activation("relu", name="Block8_6_Branch_0_Conv2d_1x1_Activation")(branch_0)
+ branch_1 = Conv2D(
+ 192, 1, strides=1, padding="same", use_bias=False, name="Block8_6_Branch_1_Conv2d_0a_1x1"
+ )(x)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block8_6_Branch_1_Conv2d_0a_1x1_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block8_6_Branch_1_Conv2d_0a_1x1_Activation")(branch_1)
+ branch_1 = Conv2D(
+ 192,
+ [1, 3],
+ strides=1,
+ padding="same",
+ use_bias=False,
+ name="Block8_6_Branch_1_Conv2d_0b_1x3",
+ )(branch_1)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block8_6_Branch_1_Conv2d_0b_1x3_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block8_6_Branch_1_Conv2d_0b_1x3_Activation")(branch_1)
+ branch_1 = Conv2D(
+ 192,
+ [3, 1],
+ strides=1,
+ padding="same",
+ use_bias=False,
+ name="Block8_6_Branch_1_Conv2d_0c_3x1",
+ )(branch_1)
+ branch_1 = BatchNormalization(
+ axis=3,
+ momentum=0.995,
+ epsilon=0.001,
+ scale=False,
+ name="Block8_6_Branch_1_Conv2d_0c_3x1_BatchNorm",
+ )(branch_1)
+ branch_1 = Activation("relu", name="Block8_6_Branch_1_Conv2d_0c_3x1_Activation")(branch_1)
+ branches = [branch_0, branch_1]
+ mixed = Concatenate(axis=3, name="Block8_6_Concatenate")(branches)
+ up = Conv2D(1792, 1, strides=1, padding="same", use_bias=True, name="Block8_6_Conv2d_1x1")(
+ mixed
+ )
+ up = Lambda(scaling, output_shape=K.int_shape(up)[1:], arguments={"scale": 1})(up)
+ x = add([x, up])
+
+ # Classification block
+ x = GlobalAveragePooling2D(name="AvgPool")(x)
+ x = Dropout(1.0 - 0.8, name="Dropout")(x)
+ # Bottleneck
+ x = Dense(dimension, use_bias=False, name="Bottleneck")(x)
+ x = BatchNormalization(momentum=0.995, epsilon=0.001, scale=False, name="Bottleneck_BatchNorm")(
+ x
+ )
+
+ # Create model
+ model = Model(inputs, x, name="inception_resnet_v1")
+
+ return model
+
+
+def load_facenet128d_model(
+ url="https://github.com/serengil/deepface_models/releases/download/v1.0/facenet_weights.h5",
+) -> Model:
+ """
+ Construct FaceNet-128d model, download weights and then load weights
+ Args:
+ dimension (int): construct FaceNet-128d or FaceNet-512d models
+ Returns:
+ model (Model)
+ """
+ model = InceptionResNetV1()
+
+ # -----------------------------------
+
+ home = folder_utils.get_deepface_home()
+
+ if os.path.isfile(home + "/.deepface/weights/facenet_weights.h5") != True:
+ logger.info("facenet_weights.h5 will be downloaded...")
+
+ output = home + "/.deepface/weights/facenet_weights.h5"
+ gdown.download(url, output, quiet=False)
+
+ # -----------------------------------
+
+ model.load_weights(home + "/.deepface/weights/facenet_weights.h5")
+
+ # -----------------------------------
+
+ return model
+
+
+def load_facenet512d_model(
+ url="https://github.com/serengil/deepface_models/releases/download/v1.0/facenet512_weights.h5",
+) -> Model:
+ """
+ Construct FaceNet-512d model, download its weights and load
+ Returns:
+ model (Model)
+ """
+
+ model = InceptionResNetV1(dimension=512)
+
+ # -------------------------
+
+ home = folder_utils.get_deepface_home()
+
+ if os.path.isfile(home + "/.deepface/weights/facenet512_weights.h5") != True:
+ logger.info("facenet512_weights.h5 will be downloaded...")
+
+ output = home + "/.deepface/weights/facenet512_weights.h5"
+ gdown.download(url, output, quiet=False)
+
+ # -------------------------
+
+ model.load_weights(home + "/.deepface/weights/facenet512_weights.h5")
+
+ # -------------------------
+
+ return model
diff --git a/deepface/basemodels/FbDeepFace.py b/deepface/basemodels/FbDeepFace.py
new file mode 100644
index 0000000000000000000000000000000000000000..f51a5112809314a9166fee965c1d07703db5bfb7
--- /dev/null
+++ b/deepface/basemodels/FbDeepFace.py
@@ -0,0 +1,105 @@
+import os
+import zipfile
+import gdown
+from deepface.commons import package_utils, folder_utils
+from deepface.models.FacialRecognition import FacialRecognition
+from deepface.commons import logger as log
+
+logger = log.get_singletonish_logger()
+
+# --------------------------------
+# dependency configuration
+
+tf_major = package_utils.get_tf_major_version()
+tf_minor = package_utils.get_tf_minor_version()
+
+if tf_major == 1:
+ from keras.models import Model, Sequential
+ from keras.layers import (
+ Convolution2D,
+ MaxPooling2D,
+ Flatten,
+ Dense,
+ Dropout,
+ )
+else:
+ from tensorflow.keras.models import Model, Sequential
+ from tensorflow.keras.layers import (
+ Convolution2D,
+ MaxPooling2D,
+ Flatten,
+ Dense,
+ Dropout,
+ )
+
+
+# -------------------------------------
+# pylint: disable=line-too-long, too-few-public-methods
+class DeepFaceClient(FacialRecognition):
+ """
+ Fb's DeepFace model class
+ """
+
+ def __init__(self):
+ # DeepFace requires tf 2.12 or less
+ if tf_major == 2 and tf_minor > 12:
+ # Ref: https://github.com/serengil/deepface/pull/1079
+ raise ValueError(
+ "DeepFace model requires LocallyConnected2D but it is no longer supported"
+ f" after tf 2.12 but you have {tf_major}.{tf_minor}. You need to downgrade your tf."
+ )
+
+ self.model = load_model()
+ self.model_name = "DeepFace"
+ self.input_shape = (152, 152)
+ self.output_shape = 4096
+
+
+def load_model(
+ url="https://github.com/swghosh/DeepFace/releases/download/weights-vggface2-2d-aligned/VGGFace2_DeepFace_weights_val-0.9034.h5.zip",
+) -> Model:
+ """
+ Construct DeepFace model, download its weights and load
+ """
+ # we have some checks for this dependency in the init of client
+ # putting this in global causes library initialization
+ if tf_major == 1:
+ from keras.layers import LocallyConnected2D
+ else:
+ from tensorflow.keras.layers import LocallyConnected2D
+
+ base_model = Sequential()
+ base_model.add(
+ Convolution2D(32, (11, 11), activation="relu", name="C1", input_shape=(152, 152, 3))
+ )
+ base_model.add(MaxPooling2D(pool_size=3, strides=2, padding="same", name="M2"))
+ base_model.add(Convolution2D(16, (9, 9), activation="relu", name="C3"))
+ base_model.add(LocallyConnected2D(16, (9, 9), activation="relu", name="L4"))
+ base_model.add(LocallyConnected2D(16, (7, 7), strides=2, activation="relu", name="L5"))
+ base_model.add(LocallyConnected2D(16, (5, 5), activation="relu", name="L6"))
+ base_model.add(Flatten(name="F0"))
+ base_model.add(Dense(4096, activation="relu", name="F7"))
+ base_model.add(Dropout(rate=0.5, name="D0"))
+ base_model.add(Dense(8631, activation="softmax", name="F8"))
+
+ # ---------------------------------
+
+ home = folder_utils.get_deepface_home()
+
+ if os.path.isfile(home + "/.deepface/weights/VGGFace2_DeepFace_weights_val-0.9034.h5") != True:
+ logger.info("VGGFace2_DeepFace_weights_val-0.9034.h5 will be downloaded...")
+
+ output = home + "/.deepface/weights/VGGFace2_DeepFace_weights_val-0.9034.h5.zip"
+
+ gdown.download(url, output, quiet=False)
+
+ # unzip VGGFace2_DeepFace_weights_val-0.9034.h5.zip
+ with zipfile.ZipFile(output, "r") as zip_ref:
+ zip_ref.extractall(home + "/.deepface/weights/")
+
+ base_model.load_weights(home + "/.deepface/weights/VGGFace2_DeepFace_weights_val-0.9034.h5")
+
+ # drop F8 and D0. F7 is the representation layer.
+ deepface_model = Model(inputs=base_model.layers[0].input, outputs=base_model.layers[-3].output)
+
+ return deepface_model
diff --git a/deepface/basemodels/GhostFaceNet.py b/deepface/basemodels/GhostFaceNet.py
new file mode 100644
index 0000000000000000000000000000000000000000..1917833845d03a4a106129e537891a2bce46b4e2
--- /dev/null
+++ b/deepface/basemodels/GhostFaceNet.py
@@ -0,0 +1,312 @@
+# built-in dependencies
+import os
+
+# 3rd party dependencies
+import gdown
+import tensorflow as tf
+
+# project dependencies
+from deepface.commons import package_utils, folder_utils
+from deepface.models.FacialRecognition import FacialRecognition
+from deepface.commons import logger as log
+
+logger = log.get_singletonish_logger()
+
+tf_major = package_utils.get_tf_major_version()
+if tf_major == 1:
+ import keras
+ from keras import backend as K
+ from keras.models import Model
+ from keras.layers import (
+ Activation,
+ Add,
+ BatchNormalization,
+ Concatenate,
+ Conv2D,
+ DepthwiseConv2D,
+ GlobalAveragePooling2D,
+ Input,
+ Reshape,
+ Multiply,
+ ReLU,
+ PReLU,
+ )
+else:
+ from tensorflow import keras
+ from tensorflow.keras import backend as K
+ from tensorflow.keras.models import Model
+ from tensorflow.keras.layers import (
+ Activation,
+ Add,
+ BatchNormalization,
+ Concatenate,
+ Conv2D,
+ DepthwiseConv2D,
+ GlobalAveragePooling2D,
+ Input,
+ Reshape,
+ Multiply,
+ ReLU,
+ PReLU,
+ )
+
+
+# pylint: disable=line-too-long, too-few-public-methods, no-else-return, unsubscriptable-object, comparison-with-callable
+PRETRAINED_WEIGHTS = "https://github.com/HamadYA/GhostFaceNets/releases/download/v1.2/GhostFaceNet_W1.3_S1_ArcFace.h5"
+
+
+class GhostFaceNetClient(FacialRecognition):
+ """
+ GhostFaceNet model (GhostFaceNetV1 backbone)
+ Repo: https://github.com/HamadYA/GhostFaceNets
+ Pre-trained weights: https://github.com/HamadYA/GhostFaceNets/releases/tag/v1.2
+ GhostFaceNet_W1.3_S1_ArcFace.h5 ~ 16.5MB
+ Author declared that this backbone and pre-trained weights got 99.7667% accuracy on LFW
+ """
+
+ def __init__(self):
+ self.model_name = "GhostFaceNet"
+ self.input_shape = (112, 112)
+ self.output_shape = 512
+ self.model = load_model()
+
+
+def load_model():
+ model = GhostFaceNetV1()
+
+ home = folder_utils.get_deepface_home()
+ output = home + "/.deepface/weights/ghostfacenet_v1.h5"
+
+ if os.path.isfile(output) is not True:
+ logger.info(f"Pre-trained weights is downloaded from {PRETRAINED_WEIGHTS} to {output}")
+ gdown.download(PRETRAINED_WEIGHTS, output, quiet=False)
+ logger.info(f"Pre-trained weights is just downloaded to {output}")
+
+ model.load_weights(output)
+
+ return model
+
+
+def GhostFaceNetV1() -> Model:
+ """
+ Build GhostFaceNetV1 model. Refactored from
+ github.com/HamadYA/GhostFaceNets/blob/main/backbones/ghost_model.py
+ Returns:
+ model (Model)
+ """
+ inputs = Input(shape=(112, 112, 3))
+
+ out_channel = 20
+
+ nn = Conv2D(
+ out_channel,
+ (3, 3),
+ strides=1,
+ padding="same",
+ use_bias=False,
+ kernel_initializer=keras.initializers.VarianceScaling(
+ scale=2.0, mode="fan_out", distribution="truncated_normal"
+ ),
+ )(inputs)
+
+ nn = BatchNormalization(axis=-1)(nn)
+ nn = Activation("relu")(nn)
+
+ dwkernels = [3, 3, 3, 5, 5, 3, 3, 3, 3, 3, 3, 5, 5, 5, 5, 5]
+ exps = [20, 64, 92, 92, 156, 312, 260, 240, 240, 624, 872, 872, 1248, 1248, 1248, 664]
+ outs = [20, 32, 32, 52, 52, 104, 104, 104, 104, 144, 144, 208, 208, 208, 208, 208]
+ strides_set = [1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1]
+ reductions = [0, 0, 0, 24, 40, 0, 0, 0, 0, 156, 220, 220, 0, 312, 0, 168]
+
+ pre_out = out_channel
+ for dwk, stride, exp, out, reduction in zip(dwkernels, strides_set, exps, outs, reductions):
+ shortcut = not (out == pre_out and stride == 1)
+ nn = ghost_bottleneck(nn, dwk, stride, exp, out, reduction, shortcut)
+ pre_out = out
+
+ nn = Conv2D(
+ 664,
+ (1, 1),
+ strides=(1, 1),
+ padding="valid",
+ use_bias=False,
+ kernel_initializer=keras.initializers.VarianceScaling(
+ scale=2.0, mode="fan_out", distribution="truncated_normal"
+ ),
+ )(nn)
+ nn = BatchNormalization(axis=-1)(nn)
+ nn = Activation("relu")(nn)
+
+ xx = Model(inputs=inputs, outputs=nn, name="GhostFaceNetV1")
+
+ # post modelling
+ inputs = xx.inputs[0]
+ nn = xx.outputs[0]
+
+ nn = keras.layers.DepthwiseConv2D(nn.shape[1], use_bias=False, name="GDC_dw")(nn)
+ nn = keras.layers.BatchNormalization(momentum=0.99, epsilon=0.001, name="GDC_batchnorm")(nn)
+ nn = keras.layers.Conv2D(
+ 512, 1, use_bias=True, kernel_initializer="glorot_normal", name="GDC_conv"
+ )(nn)
+ nn = keras.layers.Flatten(name="GDC_flatten")(nn)
+
+ embedding = keras.layers.BatchNormalization(
+ momentum=0.99, epsilon=0.001, scale=True, name="pre_embedding"
+ )(nn)
+ embedding_fp32 = keras.layers.Activation("linear", dtype="float32", name="embedding")(embedding)
+
+ model = keras.models.Model(inputs, embedding_fp32, name=xx.name)
+ model = replace_relu_with_prelu(model=model)
+ return model
+
+
+def se_module(inputs, reduction):
+ """
+ Refactored from github.com/HamadYA/GhostFaceNets/blob/main/backbones/ghost_model.py
+ """
+ # get the channel axis
+ channel_axis = 1 if K.image_data_format() == "channels_first" else -1
+ # filters = channel axis shape
+ filters = inputs.shape[channel_axis]
+
+ # from None x H x W x C to None x C
+ se = GlobalAveragePooling2D()(inputs)
+
+ # Reshape None x C to None 1 x 1 x C
+ se = Reshape((1, 1, filters))(se)
+
+ # Squeeze by using C*se_ratio. The size will be 1 x 1 x C*se_ratio
+ se = Conv2D(
+ reduction,
+ kernel_size=1,
+ use_bias=True,
+ kernel_initializer=keras.initializers.VarianceScaling(
+ scale=2.0, mode="fan_out", distribution="truncated_normal"
+ ),
+ )(se)
+ se = Activation("relu")(se)
+
+ # Excitation using C filters. The size will be 1 x 1 x C
+ se = Conv2D(
+ filters,
+ kernel_size=1,
+ use_bias=True,
+ kernel_initializer=keras.initializers.VarianceScaling(
+ scale=2.0, mode="fan_out", distribution="truncated_normal"
+ ),
+ )(se)
+ se = Activation("hard_sigmoid")(se)
+
+ return Multiply()([inputs, se])
+
+
+def ghost_module(inputs, out, convkernel=1, dwkernel=3, add_activation=True):
+ """
+ Refactored from github.com/HamadYA/GhostFaceNets/blob/main/backbones/ghost_model.py
+ """
+ conv_out_channel = out // 2
+ cc = Conv2D(
+ conv_out_channel,
+ convkernel,
+ use_bias=False,
+ strides=(1, 1),
+ padding="same",
+ kernel_initializer=keras.initializers.VarianceScaling(
+ scale=2.0, mode="fan_out", distribution="truncated_normal"
+ ),
+ )(inputs)
+ cc = BatchNormalization(axis=-1)(cc)
+ if add_activation:
+ cc = Activation("relu")(cc)
+
+ nn = DepthwiseConv2D(
+ dwkernel,
+ 1,
+ padding="same",
+ use_bias=False,
+ depthwise_initializer=keras.initializers.VarianceScaling(
+ scale=2.0, mode="fan_out", distribution="truncated_normal"
+ ),
+ )(cc)
+ nn = BatchNormalization(axis=-1)(nn)
+ if add_activation:
+ nn = Activation("relu")(nn)
+ return Concatenate()([cc, nn])
+
+
+def ghost_bottleneck(inputs, dwkernel, strides, exp, out, reduction, shortcut=True):
+ """
+ Refactored from github.com/HamadYA/GhostFaceNets/blob/main/backbones/ghost_model.py
+ """
+ nn = ghost_module(inputs, exp, add_activation=True)
+ if strides > 1:
+ # Extra depth conv if strides higher than 1
+ nn = DepthwiseConv2D(
+ dwkernel,
+ strides,
+ padding="same",
+ use_bias=False,
+ depthwise_initializer=keras.initializers.VarianceScaling(
+ scale=2.0, mode="fan_out", distribution="truncated_normal"
+ ),
+ )(nn)
+ nn = BatchNormalization(axis=-1)(nn)
+
+ if reduction > 0:
+ # Squeeze and excite
+ nn = se_module(nn, reduction)
+
+ # Point-wise linear projection
+ nn = ghost_module(nn, out, add_activation=False) # ghost2 = GhostModule(exp, out, relu=False)
+
+ if shortcut:
+ xx = DepthwiseConv2D(
+ dwkernel,
+ strides,
+ padding="same",
+ use_bias=False,
+ depthwise_initializer=keras.initializers.VarianceScaling(
+ scale=2.0, mode="fan_out", distribution="truncated_normal"
+ ),
+ )(inputs)
+ xx = BatchNormalization(axis=-1)(xx)
+ xx = Conv2D(
+ out,
+ (1, 1),
+ strides=(1, 1),
+ padding="valid",
+ use_bias=False,
+ kernel_initializer=keras.initializers.VarianceScaling(
+ scale=2.0, mode="fan_out", distribution="truncated_normal"
+ ),
+ )(xx)
+ xx = BatchNormalization(axis=-1)(xx)
+ else:
+ xx = inputs
+ return Add()([xx, nn])
+
+
+def replace_relu_with_prelu(model) -> Model:
+ """
+ Replaces relu activation function in the built model with prelu.
+ Refactored from github.com/HamadYA/GhostFaceNets/blob/main/backbones/ghost_model.py
+ Args:
+ model (Model): built model with relu activation functions
+ Returns
+ model (Model): built model with prelu activation functions
+ """
+
+ def convert_relu(layer):
+ if isinstance(layer, ReLU) or (
+ isinstance(layer, Activation) and layer.activation == keras.activations.relu
+ ):
+ layer_name = layer.name.replace("_relu", "_prelu")
+ return PReLU(
+ shared_axes=[1, 2],
+ alpha_initializer=tf.initializers.Constant(0.25),
+ name=layer_name,
+ )
+ return layer
+
+ input_tensors = keras.layers.Input(model.input_shape[1:])
+ return keras.models.clone_model(model, input_tensors=input_tensors, clone_function=convert_relu)
diff --git a/deepface/basemodels/OpenFace.py b/deepface/basemodels/OpenFace.py
new file mode 100644
index 0000000000000000000000000000000000000000..cc335b6cda30ef5965a0f57058715ddc7840099c
--- /dev/null
+++ b/deepface/basemodels/OpenFace.py
@@ -0,0 +1,397 @@
+import os
+import gdown
+import tensorflow as tf
+from deepface.commons import package_utils, folder_utils
+from deepface.models.FacialRecognition import FacialRecognition
+from deepface.commons import logger as log
+
+logger = log.get_singletonish_logger()
+
+tf_version = package_utils.get_tf_major_version()
+if tf_version == 1:
+ from keras.models import Model
+ from keras.layers import Conv2D, ZeroPadding2D, Input, concatenate
+ from keras.layers import Dense, Activation, Lambda, Flatten, BatchNormalization
+ from keras.layers import MaxPooling2D, AveragePooling2D
+ from keras import backend as K
+else:
+ from tensorflow.keras.models import Model
+ from tensorflow.keras.layers import Conv2D, ZeroPadding2D, Input, concatenate
+ from tensorflow.keras.layers import Dense, Activation, Lambda, Flatten, BatchNormalization
+ from tensorflow.keras.layers import MaxPooling2D, AveragePooling2D
+ from tensorflow.keras import backend as K
+
+# pylint: disable=unnecessary-lambda
+
+# ---------------------------------------
+
+# pylint: disable=too-few-public-methods
+class OpenFaceClient(FacialRecognition):
+ """
+ OpenFace model class
+ """
+
+ def __init__(self):
+ self.model = load_model()
+ self.model_name = "OpenFace"
+ self.input_shape = (96, 96)
+ self.output_shape = 128
+
+
+def load_model(
+ url="https://github.com/serengil/deepface_models/releases/download/v1.0/openface_weights.h5",
+) -> Model:
+ """
+ Consturct OpenFace model, download its weights and load
+ Returns:
+ model (Model)
+ """
+ myInput = Input(shape=(96, 96, 3))
+
+ x = ZeroPadding2D(padding=(3, 3), input_shape=(96, 96, 3))(myInput)
+ x = Conv2D(64, (7, 7), strides=(2, 2), name="conv1")(x)
+ x = BatchNormalization(axis=3, epsilon=0.00001, name="bn1")(x)
+ x = Activation("relu")(x)
+ x = ZeroPadding2D(padding=(1, 1))(x)
+ x = MaxPooling2D(pool_size=3, strides=2)(x)
+ x = Lambda(lambda x: tf.nn.lrn(x, alpha=1e-4, beta=0.75), name="lrn_1")(x)
+ x = Conv2D(64, (1, 1), name="conv2")(x)
+ x = BatchNormalization(axis=3, epsilon=0.00001, name="bn2")(x)
+ x = Activation("relu")(x)
+ x = ZeroPadding2D(padding=(1, 1))(x)
+ x = Conv2D(192, (3, 3), name="conv3")(x)
+ x = BatchNormalization(axis=3, epsilon=0.00001, name="bn3")(x)
+ x = Activation("relu")(x)
+ x = Lambda(lambda x: tf.nn.lrn(x, alpha=1e-4, beta=0.75), name="lrn_2")(x) # x is equal added
+ x = ZeroPadding2D(padding=(1, 1))(x)
+ x = MaxPooling2D(pool_size=3, strides=2)(x)
+
+ # Inception3a
+ inception_3a_3x3 = Conv2D(96, (1, 1), name="inception_3a_3x3_conv1")(x)
+ inception_3a_3x3 = BatchNormalization(axis=3, epsilon=0.00001, name="inception_3a_3x3_bn1")(
+ inception_3a_3x3
+ )
+ inception_3a_3x3 = Activation("relu")(inception_3a_3x3)
+ inception_3a_3x3 = ZeroPadding2D(padding=(1, 1))(inception_3a_3x3)
+ inception_3a_3x3 = Conv2D(128, (3, 3), name="inception_3a_3x3_conv2")(inception_3a_3x3)
+ inception_3a_3x3 = BatchNormalization(axis=3, epsilon=0.00001, name="inception_3a_3x3_bn2")(
+ inception_3a_3x3
+ )
+ inception_3a_3x3 = Activation("relu")(inception_3a_3x3)
+
+ inception_3a_5x5 = Conv2D(16, (1, 1), name="inception_3a_5x5_conv1")(x)
+ inception_3a_5x5 = BatchNormalization(axis=3, epsilon=0.00001, name="inception_3a_5x5_bn1")(
+ inception_3a_5x5
+ )
+ inception_3a_5x5 = Activation("relu")(inception_3a_5x5)
+ inception_3a_5x5 = ZeroPadding2D(padding=(2, 2))(inception_3a_5x5)
+ inception_3a_5x5 = Conv2D(32, (5, 5), name="inception_3a_5x5_conv2")(inception_3a_5x5)
+ inception_3a_5x5 = BatchNormalization(axis=3, epsilon=0.00001, name="inception_3a_5x5_bn2")(
+ inception_3a_5x5
+ )
+ inception_3a_5x5 = Activation("relu")(inception_3a_5x5)
+
+ inception_3a_pool = MaxPooling2D(pool_size=3, strides=2)(x)
+ inception_3a_pool = Conv2D(32, (1, 1), name="inception_3a_pool_conv")(inception_3a_pool)
+ inception_3a_pool = BatchNormalization(axis=3, epsilon=0.00001, name="inception_3a_pool_bn")(
+ inception_3a_pool
+ )
+ inception_3a_pool = Activation("relu")(inception_3a_pool)
+ inception_3a_pool = ZeroPadding2D(padding=((3, 4), (3, 4)))(inception_3a_pool)
+
+ inception_3a_1x1 = Conv2D(64, (1, 1), name="inception_3a_1x1_conv")(x)
+ inception_3a_1x1 = BatchNormalization(axis=3, epsilon=0.00001, name="inception_3a_1x1_bn")(
+ inception_3a_1x1
+ )
+ inception_3a_1x1 = Activation("relu")(inception_3a_1x1)
+
+ inception_3a = concatenate(
+ [inception_3a_3x3, inception_3a_5x5, inception_3a_pool, inception_3a_1x1], axis=3
+ )
+
+ # Inception3b
+ inception_3b_3x3 = Conv2D(96, (1, 1), name="inception_3b_3x3_conv1")(inception_3a)
+ inception_3b_3x3 = BatchNormalization(axis=3, epsilon=0.00001, name="inception_3b_3x3_bn1")(
+ inception_3b_3x3
+ )
+ inception_3b_3x3 = Activation("relu")(inception_3b_3x3)
+ inception_3b_3x3 = ZeroPadding2D(padding=(1, 1))(inception_3b_3x3)
+ inception_3b_3x3 = Conv2D(128, (3, 3), name="inception_3b_3x3_conv2")(inception_3b_3x3)
+ inception_3b_3x3 = BatchNormalization(axis=3, epsilon=0.00001, name="inception_3b_3x3_bn2")(
+ inception_3b_3x3
+ )
+ inception_3b_3x3 = Activation("relu")(inception_3b_3x3)
+
+ inception_3b_5x5 = Conv2D(32, (1, 1), name="inception_3b_5x5_conv1")(inception_3a)
+ inception_3b_5x5 = BatchNormalization(axis=3, epsilon=0.00001, name="inception_3b_5x5_bn1")(
+ inception_3b_5x5
+ )
+ inception_3b_5x5 = Activation("relu")(inception_3b_5x5)
+ inception_3b_5x5 = ZeroPadding2D(padding=(2, 2))(inception_3b_5x5)
+ inception_3b_5x5 = Conv2D(64, (5, 5), name="inception_3b_5x5_conv2")(inception_3b_5x5)
+ inception_3b_5x5 = BatchNormalization(axis=3, epsilon=0.00001, name="inception_3b_5x5_bn2")(
+ inception_3b_5x5
+ )
+ inception_3b_5x5 = Activation("relu")(inception_3b_5x5)
+
+ inception_3b_pool = Lambda(lambda x: x**2, name="power2_3b")(inception_3a)
+ inception_3b_pool = AveragePooling2D(pool_size=(3, 3), strides=(3, 3))(inception_3b_pool)
+ inception_3b_pool = Lambda(lambda x: x * 9, name="mult9_3b")(inception_3b_pool)
+ inception_3b_pool = Lambda(lambda x: K.sqrt(x), name="sqrt_3b")(inception_3b_pool)
+ inception_3b_pool = Conv2D(64, (1, 1), name="inception_3b_pool_conv")(inception_3b_pool)
+ inception_3b_pool = BatchNormalization(axis=3, epsilon=0.00001, name="inception_3b_pool_bn")(
+ inception_3b_pool
+ )
+ inception_3b_pool = Activation("relu")(inception_3b_pool)
+ inception_3b_pool = ZeroPadding2D(padding=(4, 4))(inception_3b_pool)
+
+ inception_3b_1x1 = Conv2D(64, (1, 1), name="inception_3b_1x1_conv")(inception_3a)
+ inception_3b_1x1 = BatchNormalization(axis=3, epsilon=0.00001, name="inception_3b_1x1_bn")(
+ inception_3b_1x1
+ )
+ inception_3b_1x1 = Activation("relu")(inception_3b_1x1)
+
+ inception_3b = concatenate(
+ [inception_3b_3x3, inception_3b_5x5, inception_3b_pool, inception_3b_1x1], axis=3
+ )
+
+ # Inception3c
+ inception_3c_3x3 = Conv2D(128, (1, 1), strides=(1, 1), name="inception_3c_3x3_conv1")(
+ inception_3b
+ )
+ inception_3c_3x3 = BatchNormalization(axis=3, epsilon=0.00001, name="inception_3c_3x3_bn1")(
+ inception_3c_3x3
+ )
+ inception_3c_3x3 = Activation("relu")(inception_3c_3x3)
+ inception_3c_3x3 = ZeroPadding2D(padding=(1, 1))(inception_3c_3x3)
+ inception_3c_3x3 = Conv2D(256, (3, 3), strides=(2, 2), name="inception_3c_3x3_conv" + "2")(
+ inception_3c_3x3
+ )
+ inception_3c_3x3 = BatchNormalization(
+ axis=3, epsilon=0.00001, name="inception_3c_3x3_bn" + "2"
+ )(inception_3c_3x3)
+ inception_3c_3x3 = Activation("relu")(inception_3c_3x3)
+
+ inception_3c_5x5 = Conv2D(32, (1, 1), strides=(1, 1), name="inception_3c_5x5_conv1")(
+ inception_3b
+ )
+ inception_3c_5x5 = BatchNormalization(axis=3, epsilon=0.00001, name="inception_3c_5x5_bn1")(
+ inception_3c_5x5
+ )
+ inception_3c_5x5 = Activation("relu")(inception_3c_5x5)
+ inception_3c_5x5 = ZeroPadding2D(padding=(2, 2))(inception_3c_5x5)
+ inception_3c_5x5 = Conv2D(64, (5, 5), strides=(2, 2), name="inception_3c_5x5_conv" + "2")(
+ inception_3c_5x5
+ )
+ inception_3c_5x5 = BatchNormalization(
+ axis=3, epsilon=0.00001, name="inception_3c_5x5_bn" + "2"
+ )(inception_3c_5x5)
+ inception_3c_5x5 = Activation("relu")(inception_3c_5x5)
+
+ inception_3c_pool = MaxPooling2D(pool_size=3, strides=2)(inception_3b)
+ inception_3c_pool = ZeroPadding2D(padding=((0, 1), (0, 1)))(inception_3c_pool)
+
+ inception_3c = concatenate([inception_3c_3x3, inception_3c_5x5, inception_3c_pool], axis=3)
+
+ # inception 4a
+ inception_4a_3x3 = Conv2D(96, (1, 1), strides=(1, 1), name="inception_4a_3x3_conv" + "1")(
+ inception_3c
+ )
+ inception_4a_3x3 = BatchNormalization(
+ axis=3, epsilon=0.00001, name="inception_4a_3x3_bn" + "1"
+ )(inception_4a_3x3)
+ inception_4a_3x3 = Activation("relu")(inception_4a_3x3)
+ inception_4a_3x3 = ZeroPadding2D(padding=(1, 1))(inception_4a_3x3)
+ inception_4a_3x3 = Conv2D(192, (3, 3), strides=(1, 1), name="inception_4a_3x3_conv" + "2")(
+ inception_4a_3x3
+ )
+ inception_4a_3x3 = BatchNormalization(
+ axis=3, epsilon=0.00001, name="inception_4a_3x3_bn" + "2"
+ )(inception_4a_3x3)
+ inception_4a_3x3 = Activation("relu")(inception_4a_3x3)
+
+ inception_4a_5x5 = Conv2D(32, (1, 1), strides=(1, 1), name="inception_4a_5x5_conv1")(
+ inception_3c
+ )
+ inception_4a_5x5 = BatchNormalization(axis=3, epsilon=0.00001, name="inception_4a_5x5_bn1")(
+ inception_4a_5x5
+ )
+ inception_4a_5x5 = Activation("relu")(inception_4a_5x5)
+ inception_4a_5x5 = ZeroPadding2D(padding=(2, 2))(inception_4a_5x5)
+ inception_4a_5x5 = Conv2D(64, (5, 5), strides=(1, 1), name="inception_4a_5x5_conv" + "2")(
+ inception_4a_5x5
+ )
+ inception_4a_5x5 = BatchNormalization(
+ axis=3, epsilon=0.00001, name="inception_4a_5x5_bn" + "2"
+ )(inception_4a_5x5)
+ inception_4a_5x5 = Activation("relu")(inception_4a_5x5)
+
+ inception_4a_pool = Lambda(lambda x: x**2, name="power2_4a")(inception_3c)
+ inception_4a_pool = AveragePooling2D(pool_size=(3, 3), strides=(3, 3))(inception_4a_pool)
+ inception_4a_pool = Lambda(lambda x: x * 9, name="mult9_4a")(inception_4a_pool)
+ inception_4a_pool = Lambda(lambda x: K.sqrt(x), name="sqrt_4a")(inception_4a_pool)
+
+ inception_4a_pool = Conv2D(128, (1, 1), strides=(1, 1), name="inception_4a_pool_conv" + "")(
+ inception_4a_pool
+ )
+ inception_4a_pool = BatchNormalization(
+ axis=3, epsilon=0.00001, name="inception_4a_pool_bn" + ""
+ )(inception_4a_pool)
+ inception_4a_pool = Activation("relu")(inception_4a_pool)
+ inception_4a_pool = ZeroPadding2D(padding=(2, 2))(inception_4a_pool)
+
+ inception_4a_1x1 = Conv2D(256, (1, 1), strides=(1, 1), name="inception_4a_1x1_conv" + "")(
+ inception_3c
+ )
+ inception_4a_1x1 = BatchNormalization(axis=3, epsilon=0.00001, name="inception_4a_1x1_bn" + "")(
+ inception_4a_1x1
+ )
+ inception_4a_1x1 = Activation("relu")(inception_4a_1x1)
+
+ inception_4a = concatenate(
+ [inception_4a_3x3, inception_4a_5x5, inception_4a_pool, inception_4a_1x1], axis=3
+ )
+
+ # inception4e
+ inception_4e_3x3 = Conv2D(160, (1, 1), strides=(1, 1), name="inception_4e_3x3_conv" + "1")(
+ inception_4a
+ )
+ inception_4e_3x3 = BatchNormalization(
+ axis=3, epsilon=0.00001, name="inception_4e_3x3_bn" + "1"
+ )(inception_4e_3x3)
+ inception_4e_3x3 = Activation("relu")(inception_4e_3x3)
+ inception_4e_3x3 = ZeroPadding2D(padding=(1, 1))(inception_4e_3x3)
+ inception_4e_3x3 = Conv2D(256, (3, 3), strides=(2, 2), name="inception_4e_3x3_conv" + "2")(
+ inception_4e_3x3
+ )
+ inception_4e_3x3 = BatchNormalization(
+ axis=3, epsilon=0.00001, name="inception_4e_3x3_bn" + "2"
+ )(inception_4e_3x3)
+ inception_4e_3x3 = Activation("relu")(inception_4e_3x3)
+
+ inception_4e_5x5 = Conv2D(64, (1, 1), strides=(1, 1), name="inception_4e_5x5_conv" + "1")(
+ inception_4a
+ )
+ inception_4e_5x5 = BatchNormalization(
+ axis=3, epsilon=0.00001, name="inception_4e_5x5_bn" + "1"
+ )(inception_4e_5x5)
+ inception_4e_5x5 = Activation("relu")(inception_4e_5x5)
+ inception_4e_5x5 = ZeroPadding2D(padding=(2, 2))(inception_4e_5x5)
+ inception_4e_5x5 = Conv2D(128, (5, 5), strides=(2, 2), name="inception_4e_5x5_conv" + "2")(
+ inception_4e_5x5
+ )
+ inception_4e_5x5 = BatchNormalization(
+ axis=3, epsilon=0.00001, name="inception_4e_5x5_bn" + "2"
+ )(inception_4e_5x5)
+ inception_4e_5x5 = Activation("relu")(inception_4e_5x5)
+
+ inception_4e_pool = MaxPooling2D(pool_size=3, strides=2)(inception_4a)
+ inception_4e_pool = ZeroPadding2D(padding=((0, 1), (0, 1)))(inception_4e_pool)
+
+ inception_4e = concatenate([inception_4e_3x3, inception_4e_5x5, inception_4e_pool], axis=3)
+
+ # inception5a
+ inception_5a_3x3 = Conv2D(96, (1, 1), strides=(1, 1), name="inception_5a_3x3_conv" + "1")(
+ inception_4e
+ )
+ inception_5a_3x3 = BatchNormalization(
+ axis=3, epsilon=0.00001, name="inception_5a_3x3_bn" + "1"
+ )(inception_5a_3x3)
+ inception_5a_3x3 = Activation("relu")(inception_5a_3x3)
+ inception_5a_3x3 = ZeroPadding2D(padding=(1, 1))(inception_5a_3x3)
+ inception_5a_3x3 = Conv2D(384, (3, 3), strides=(1, 1), name="inception_5a_3x3_conv" + "2")(
+ inception_5a_3x3
+ )
+ inception_5a_3x3 = BatchNormalization(
+ axis=3, epsilon=0.00001, name="inception_5a_3x3_bn" + "2"
+ )(inception_5a_3x3)
+ inception_5a_3x3 = Activation("relu")(inception_5a_3x3)
+
+ inception_5a_pool = Lambda(lambda x: x**2, name="power2_5a")(inception_4e)
+ inception_5a_pool = AveragePooling2D(pool_size=(3, 3), strides=(3, 3))(inception_5a_pool)
+ inception_5a_pool = Lambda(lambda x: x * 9, name="mult9_5a")(inception_5a_pool)
+ inception_5a_pool = Lambda(lambda x: K.sqrt(x), name="sqrt_5a")(inception_5a_pool)
+
+ inception_5a_pool = Conv2D(96, (1, 1), strides=(1, 1), name="inception_5a_pool_conv" + "")(
+ inception_5a_pool
+ )
+ inception_5a_pool = BatchNormalization(
+ axis=3, epsilon=0.00001, name="inception_5a_pool_bn" + ""
+ )(inception_5a_pool)
+ inception_5a_pool = Activation("relu")(inception_5a_pool)
+ inception_5a_pool = ZeroPadding2D(padding=(1, 1))(inception_5a_pool)
+
+ inception_5a_1x1 = Conv2D(256, (1, 1), strides=(1, 1), name="inception_5a_1x1_conv" + "")(
+ inception_4e
+ )
+ inception_5a_1x1 = BatchNormalization(axis=3, epsilon=0.00001, name="inception_5a_1x1_bn" + "")(
+ inception_5a_1x1
+ )
+ inception_5a_1x1 = Activation("relu")(inception_5a_1x1)
+
+ inception_5a = concatenate([inception_5a_3x3, inception_5a_pool, inception_5a_1x1], axis=3)
+
+ # inception_5b
+ inception_5b_3x3 = Conv2D(96, (1, 1), strides=(1, 1), name="inception_5b_3x3_conv" + "1")(
+ inception_5a
+ )
+ inception_5b_3x3 = BatchNormalization(
+ axis=3, epsilon=0.00001, name="inception_5b_3x3_bn" + "1"
+ )(inception_5b_3x3)
+ inception_5b_3x3 = Activation("relu")(inception_5b_3x3)
+ inception_5b_3x3 = ZeroPadding2D(padding=(1, 1))(inception_5b_3x3)
+ inception_5b_3x3 = Conv2D(384, (3, 3), strides=(1, 1), name="inception_5b_3x3_conv" + "2")(
+ inception_5b_3x3
+ )
+ inception_5b_3x3 = BatchNormalization(
+ axis=3, epsilon=0.00001, name="inception_5b_3x3_bn" + "2"
+ )(inception_5b_3x3)
+ inception_5b_3x3 = Activation("relu")(inception_5b_3x3)
+
+ inception_5b_pool = MaxPooling2D(pool_size=3, strides=2)(inception_5a)
+
+ inception_5b_pool = Conv2D(96, (1, 1), strides=(1, 1), name="inception_5b_pool_conv" + "")(
+ inception_5b_pool
+ )
+ inception_5b_pool = BatchNormalization(
+ axis=3, epsilon=0.00001, name="inception_5b_pool_bn" + ""
+ )(inception_5b_pool)
+ inception_5b_pool = Activation("relu")(inception_5b_pool)
+
+ inception_5b_pool = ZeroPadding2D(padding=(1, 1))(inception_5b_pool)
+
+ inception_5b_1x1 = Conv2D(256, (1, 1), strides=(1, 1), name="inception_5b_1x1_conv" + "")(
+ inception_5a
+ )
+ inception_5b_1x1 = BatchNormalization(axis=3, epsilon=0.00001, name="inception_5b_1x1_bn" + "")(
+ inception_5b_1x1
+ )
+ inception_5b_1x1 = Activation("relu")(inception_5b_1x1)
+
+ inception_5b = concatenate([inception_5b_3x3, inception_5b_pool, inception_5b_1x1], axis=3)
+
+ av_pool = AveragePooling2D(pool_size=(3, 3), strides=(1, 1))(inception_5b)
+ reshape_layer = Flatten()(av_pool)
+ dense_layer = Dense(128, name="dense_layer")(reshape_layer)
+ norm_layer = Lambda(lambda x: K.l2_normalize(x, axis=1), name="norm_layer")(dense_layer)
+
+ # Final Model
+ model = Model(inputs=[myInput], outputs=norm_layer)
+
+ # -----------------------------------
+
+ home = folder_utils.get_deepface_home()
+
+ if os.path.isfile(home + "/.deepface/weights/openface_weights.h5") != True:
+ logger.info("openface_weights.h5 will be downloaded...")
+
+ output = home + "/.deepface/weights/openface_weights.h5"
+ gdown.download(url, output, quiet=False)
+
+ # -----------------------------------
+
+ model.load_weights(home + "/.deepface/weights/openface_weights.h5")
+
+ # -----------------------------------
+
+ return model
diff --git a/deepface/basemodels/SFace.py b/deepface/basemodels/SFace.py
new file mode 100644
index 0000000000000000000000000000000000000000..ddd9360f17f3822c4c0d41d7bcc2ddce56b23e0b
--- /dev/null
+++ b/deepface/basemodels/SFace.py
@@ -0,0 +1,87 @@
+# built-in dependencies
+import os
+from typing import Any, List
+
+# 3rd party dependencies
+import numpy as np
+import cv2 as cv
+import gdown
+
+# project dependencies
+from deepface.commons import folder_utils
+from deepface.models.FacialRecognition import FacialRecognition
+from deepface.commons import logger as log
+
+logger = log.get_singletonish_logger()
+
+# pylint: disable=line-too-long, too-few-public-methods
+
+
+class SFaceClient(FacialRecognition):
+ """
+ SFace model class
+ """
+
+ def __init__(self):
+ self.model = load_model()
+ self.model_name = "SFace"
+ self.input_shape = (112, 112)
+ self.output_shape = 128
+
+ def forward(self, img: np.ndarray) -> List[float]:
+ """
+ Find embeddings with SFace model
+ This model necessitates the override of the forward method
+ because it is not a keras model.
+ Args:
+ img (np.ndarray): pre-loaded image in BGR
+ Returns
+ embeddings (list): multi-dimensional vector
+ """
+ # return self.model.predict(img)[0].tolist()
+
+ # revert the image to original format and preprocess using the model
+ input_blob = (img[0] * 255).astype(np.uint8)
+
+ embeddings = self.model.model.feature(input_blob)
+
+ return embeddings[0].tolist()
+
+
+def load_model(
+ url="https://github.com/opencv/opencv_zoo/raw/main/models/face_recognition_sface/face_recognition_sface_2021dec.onnx",
+) -> Any:
+ """
+ Construct SFace model, download its weights and load
+ """
+
+ home = folder_utils.get_deepface_home()
+
+ file_name = home + "/.deepface/weights/face_recognition_sface_2021dec.onnx"
+
+ if not os.path.isfile(file_name):
+
+ logger.info("sface weights will be downloaded...")
+
+ gdown.download(url, file_name, quiet=False)
+
+ model = SFaceWrapper(model_path=file_name)
+
+ return model
+
+
+class SFaceWrapper:
+ def __init__(self, model_path):
+ """
+ SFace wrapper covering model construction, layer infos and predict
+ """
+ try:
+ self.model = cv.FaceRecognizerSF.create(
+ model=model_path, config="", backend_id=0, target_id=0
+ )
+ except Exception as err:
+ raise ValueError(
+ "Exception while calling opencv.FaceRecognizerSF module."
+ + "This is an optional dependency."
+ + "You can install it as pip install opencv-contrib-python."
+ ) from err
diff --git a/deepface/basemodels/VGGFace.py b/deepface/basemodels/VGGFace.py
new file mode 100644
index 0000000000000000000000000000000000000000..819150e33bb3ac28c03c87e2dd615aa85534715d
--- /dev/null
+++ b/deepface/basemodels/VGGFace.py
@@ -0,0 +1,160 @@
+from typing import List
+import os
+import gdown
+import numpy as np
+from deepface.commons import package_utils, folder_utils
+from deepface.modules import verification
+from deepface.models.FacialRecognition import FacialRecognition
+from deepface.commons import logger as log
+
+logger = log.get_singletonish_logger()
+
+# ---------------------------------------
+
+tf_version = package_utils.get_tf_major_version()
+if tf_version == 1:
+ from keras.models import Model, Sequential
+ from keras.layers import (
+ Convolution2D,
+ ZeroPadding2D,
+ MaxPooling2D,
+ Flatten,
+ Dropout,
+ Activation,
+ )
+else:
+ from tensorflow.keras.models import Model, Sequential
+ from tensorflow.keras.layers import (
+ Convolution2D,
+ ZeroPadding2D,
+ MaxPooling2D,
+ Flatten,
+ Dropout,
+ Activation,
+ )
+
+# ---------------------------------------
+
+# pylint: disable=too-few-public-methods
+class VggFaceClient(FacialRecognition):
+ """
+ VGG-Face model class
+ """
+
+ def __init__(self):
+ self.model = load_model()
+ self.model_name = "VGG-Face"
+ self.input_shape = (224, 224)
+ self.output_shape = 4096
+
+ def forward(self, img: np.ndarray) -> List[float]:
+ """
+ Generates embeddings using the VGG-Face model.
+ This method incorporates an additional normalization layer,
+ necessitating the override of the forward method.
+
+ Args:
+ img (np.ndarray): pre-loaded image in BGR
+ Returns
+ embeddings (list): multi-dimensional vector
+ """
+ # model.predict causes memory issue when it is called in a for loop
+ # embedding = model.predict(img, verbose=0)[0].tolist()
+
+ # having normalization layer in descriptor troubles for some gpu users (e.g. issue 957, 966)
+ # instead we are now calculating it with traditional way not with keras backend
+ embedding = self.model(img, training=False).numpy()[0].tolist()
+ embedding = verification.l2_normalize(embedding)
+ return embedding.tolist()
+
+
+def base_model() -> Sequential:
+ """
+ Base model of VGG-Face being used for classification - not to find embeddings
+ Returns:
+ model (Sequential): model was trained to classify 2622 identities
+ """
+ model = Sequential()
+ model.add(ZeroPadding2D((1, 1), input_shape=(224, 224, 3)))
+ model.add(Convolution2D(64, (3, 3), activation="relu"))
+ model.add(ZeroPadding2D((1, 1)))
+ model.add(Convolution2D(64, (3, 3), activation="relu"))
+ model.add(MaxPooling2D((2, 2), strides=(2, 2)))
+
+ model.add(ZeroPadding2D((1, 1)))
+ model.add(Convolution2D(128, (3, 3), activation="relu"))
+ model.add(ZeroPadding2D((1, 1)))
+ model.add(Convolution2D(128, (3, 3), activation="relu"))
+ model.add(MaxPooling2D((2, 2), strides=(2, 2)))
+
+ model.add(ZeroPadding2D((1, 1)))
+ model.add(Convolution2D(256, (3, 3), activation="relu"))
+ model.add(ZeroPadding2D((1, 1)))
+ model.add(Convolution2D(256, (3, 3), activation="relu"))
+ model.add(ZeroPadding2D((1, 1)))
+ model.add(Convolution2D(256, (3, 3), activation="relu"))
+ model.add(MaxPooling2D((2, 2), strides=(2, 2)))
+
+ model.add(ZeroPadding2D((1, 1)))
+ model.add(Convolution2D(512, (3, 3), activation="relu"))
+ model.add(ZeroPadding2D((1, 1)))
+ model.add(Convolution2D(512, (3, 3), activation="relu"))
+ model.add(ZeroPadding2D((1, 1)))
+ model.add(Convolution2D(512, (3, 3), activation="relu"))
+ model.add(MaxPooling2D((2, 2), strides=(2, 2)))
+
+ model.add(ZeroPadding2D((1, 1)))
+ model.add(Convolution2D(512, (3, 3), activation="relu"))
+ model.add(ZeroPadding2D((1, 1)))
+ model.add(Convolution2D(512, (3, 3), activation="relu"))
+ model.add(ZeroPadding2D((1, 1)))
+ model.add(Convolution2D(512, (3, 3), activation="relu"))
+ model.add(MaxPooling2D((2, 2), strides=(2, 2)))
+
+ model.add(Convolution2D(4096, (7, 7), activation="relu"))
+ model.add(Dropout(0.5))
+ model.add(Convolution2D(4096, (1, 1), activation="relu"))
+ model.add(Dropout(0.5))
+ model.add(Convolution2D(2622, (1, 1)))
+ model.add(Flatten())
+ model.add(Activation("softmax"))
+
+ return model
+
+
+def load_model(
+ url="https://github.com/serengil/deepface_models/releases/download/v1.0/vgg_face_weights.h5",
+) -> Model:
+ """
+ Final VGG-Face model being used for finding embeddings
+ Returns:
+ model (Model): returning 4096 dimensional vectors
+ """
+
+ model = base_model()
+
+ home = folder_utils.get_deepface_home()
+ output = home + "/.deepface/weights/vgg_face_weights.h5"
+
+ if os.path.isfile(output) != True:
+ logger.info("vgg_face_weights.h5 will be downloaded...")
+ gdown.download(url, output, quiet=False)
+
+ model.load_weights(output)
+
+ # 2622d dimensional model
+ # vgg_face_descriptor = Model(inputs=model.layers[0].input, outputs=model.layers[-2].output)
+
+ # 4096 dimensional model offers 6% to 14% increasement on accuracy!
+ # - softmax causes underfitting
+ # - added normalization layer to avoid underfitting with euclidean
+ # as described here: https://github.com/serengil/deepface/issues/944
+ base_model_output = Sequential()
+ base_model_output = Flatten()(model.layers[-5].output)
+ # keras backend's l2 normalization layer troubles some gpu users (e.g. issue 957, 966)
+ # base_model_output = Lambda(lambda x: K.l2_normalize(x, axis=1), name="norm_layer")(
+ # base_model_output
+ # )
+ vgg_face_descriptor = Model(inputs=model.input, outputs=base_model_output)
+
+ return vgg_face_descriptor
diff --git a/deepface/basemodels/__init__.py b/deepface/basemodels/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/deepface/commons/__init__.py b/deepface/commons/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/deepface/commons/constant.py b/deepface/commons/constant.py
new file mode 100644
index 0000000000000000000000000000000000000000..22f63499f051c89d0bae3e7325aae8b46c7b678a
--- /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 0000000000000000000000000000000000000000..ef5fbc71bb11415199813e2f51c5dbaf6ad07640
--- /dev/null
+++ b/deepface/commons/folder_utils.py
@@ -0,0 +1,35 @@
+import os
+from pathlib import Path
+from deepface.commons import logger as log
+
+logger = log.get_singletonish_logger()
+
+
+def initialize_folder() -> None:
+ """
+ Initialize the folder for storing model weights.
+
+ Raises:
+ OSError: if the folder cannot be created.
+ """
+ home = get_deepface_home()
+ deepface_home_path = home + "/.deepface"
+ weights_path = deepface_home_path + "/weights"
+
+ if not os.path.exists(deepface_home_path):
+ os.makedirs(deepface_home_path, exist_ok=True)
+ logger.info(f"Directory {home}/.deepface created")
+
+ if not os.path.exists(weights_path):
+ os.makedirs(weights_path, exist_ok=True)
+ logger.info(f"Directory {home}/.deepface/weights created")
+
+
+def get_deepface_home() -> str:
+ """
+ Get the home directory for storing model weights
+
+ Returns:
+ str: the home directory.
+ """
+ return str(os.getenv("DEEPFACE_HOME", default=str(Path.home())))
diff --git a/deepface/commons/image_utils.py b/deepface/commons/image_utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..c25e41167fcc70604ae64bce47a3d86531dd846f
--- /dev/null
+++ b/deepface/commons/image_utils.py
@@ -0,0 +1,149 @@
+# built-in dependencies
+import os
+import io
+from typing import List, Union, Tuple
+import hashlib
+import base64
+from pathlib import Path
+
+# 3rd party dependencies
+import requests
+import numpy as np
+import cv2
+from PIL import Image
+
+
+def list_images(path: str) -> List[str]:
+ """
+ List images in a given path
+ Args:
+ path (str): path's location
+ Returns:
+ images (list): list of exact image paths
+ """
+ images = []
+ for r, _, f in os.walk(path):
+ for file in f:
+ exact_path = os.path.join(r, file)
+
+ _, ext = os.path.splitext(exact_path)
+ ext_lower = ext.lower()
+
+ if ext_lower not in {".jpg", ".jpeg", ".png"}:
+ continue
+
+ with Image.open(exact_path) as img: # lazy
+ if img.format.lower() in ["jpeg", "png"]:
+ images.append(exact_path)
+ return images
+
+
+def find_image_hash(file_path: str) -> str:
+ """
+ Find the hash of given image file with its properties
+ finding the hash of image content is costly operation
+ Args:
+ file_path (str): exact image path
+ Returns:
+ hash (str): digest with sha1 algorithm
+ """
+ file_stats = os.stat(file_path)
+
+ # some properties
+ file_size = file_stats.st_size
+ creation_time = file_stats.st_ctime
+ modification_time = file_stats.st_mtime
+
+ properties = f"{file_size}-{creation_time}-{modification_time}"
+
+ hasher = hashlib.sha1()
+ hasher.update(properties.encode("utf-8"))
+ return hasher.hexdigest()
+
+
+def load_image(img: Union[str, np.ndarray]) -> Tuple[np.ndarray, str]:
+ """
+ Load image from path, url, base64 or numpy array.
+ Args:
+ img: a path, url, base64 or numpy array.
+ Returns:
+ image (numpy array): the loaded image in BGR format
+ image name (str): image name itself
+ """
+
+ # The image is already a numpy array
+ if isinstance(img, np.ndarray):
+ return img, "numpy array"
+
+ if isinstance(img, Path):
+ img = str(img)
+
+ if not isinstance(img, str):
+ raise ValueError(f"img must be numpy array or str but it is {type(img)}")
+
+ # The image is a base64 string
+ if img.startswith("data:image/"):
+ return load_image_from_base64(img), "base64 encoded string"
+
+ # The image is a url
+ if img.lower().startswith("http://") or img.lower().startswith("https://"):
+ return load_image_from_web(url=img), img
+
+ # The image is a path
+ if os.path.isfile(img) is not True:
+ raise ValueError(f"Confirm that {img} exists")
+
+ # image must be a file on the system then
+
+ # image name must have english characters
+ if img.isascii() is False:
+ raise ValueError(f"Input image must not have non-english characters - {img}")
+
+ img_obj_bgr = cv2.imread(img)
+ # img_obj_rgb = cv2.cvtColor(img_obj_bgr, cv2.COLOR_BGR2RGB)
+ return img_obj_bgr, img
+
+
+def load_image_from_base64(uri: str) -> np.ndarray:
+ """
+ Load image from base64 string.
+ Args:
+ uri: a base64 string.
+ Returns:
+ numpy array: the loaded image.
+ """
+
+ encoded_data_parts = uri.split(",")
+
+ if len(encoded_data_parts) < 2:
+ raise ValueError("format error in base64 encoded string")
+
+ encoded_data = encoded_data_parts[1]
+ decoded_bytes = base64.b64decode(encoded_data)
+
+ # similar to find functionality, we are just considering these extensions
+ # content type is safer option than file extension
+ with Image.open(io.BytesIO(decoded_bytes)) as img:
+ file_type = img.format.lower()
+ if file_type not in ["jpeg", "png"]:
+ raise ValueError(f"input image can be jpg or png, but it is {file_type}")
+
+ nparr = np.fromstring(decoded_bytes, np.uint8)
+ img_bgr = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
+ # img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
+ return img_bgr
+
+
+def load_image_from_web(url: str) -> np.ndarray:
+ """
+ Loading an image from web
+ Args:
+ url: link for the image
+ Returns:
+ img (np.ndarray): equivalent to pre-loaded image from opencv (BGR format)
+ """
+ response = requests.get(url, stream=True, timeout=60)
+ response.raise_for_status()
+ image_array = np.asarray(bytearray(response.raw.read()), dtype=np.uint8)
+ img = cv2.imdecode(image_array, cv2.IMREAD_COLOR)
+ return img
diff --git a/deepface/commons/logger.py b/deepface/commons/logger.py
new file mode 100644
index 0000000000000000000000000000000000000000..45339532d1c1392bfc3a14ff01ca7ec5dbbf8385
--- /dev/null
+++ b/deepface/commons/logger.py
@@ -0,0 +1,54 @@
+import os
+import logging
+from datetime import datetime
+
+# pylint: disable=broad-except
+class Logger:
+ def __init__(self, module=None):
+ self.module = module
+ log_level = os.environ.get("DEEPFACE_LOG_LEVEL", str(logging.INFO))
+ try:
+ self.log_level = int(log_level)
+ except Exception as err:
+ self.dump_log(
+ f"Exception while parsing $DEEPFACE_LOG_LEVEL."
+ f"Expected int but it is {log_level} ({str(err)})."
+ "Setting app log level to info."
+ )
+ self.log_level = logging.INFO
+
+ def info(self, message):
+ if self.log_level <= logging.INFO:
+ self.dump_log(f"{message}")
+
+ def debug(self, message):
+ if self.log_level <= logging.DEBUG:
+ self.dump_log(f"🕷️ {message}")
+
+ def warn(self, message):
+ if self.log_level <= logging.WARNING:
+ self.dump_log(f"⚠️ {message}")
+
+ def error(self, message):
+ if self.log_level <= logging.ERROR:
+ self.dump_log(f"🔴 {message}")
+
+ def critical(self, message):
+ if self.log_level <= logging.CRITICAL:
+ self.dump_log(f"💥 {message}")
+
+ def dump_log(self, message):
+ print(f"{str(datetime.now())[2:-7]} - {message}")
+
+
+def get_singletonish_logger():
+ # singleton design pattern
+ global model_obj
+
+ if not "model_obj" in globals():
+ model_obj = {}
+
+ if "logger" not in model_obj.keys():
+ model_obj["logger"] = Logger(module="Singleton")
+
+ return model_obj["logger"]
diff --git a/deepface/commons/os_path.py b/deepface/commons/os_path.py
new file mode 100644
index 0000000000000000000000000000000000000000..954bcf81d42fa5d3590048991909d7a6cd1fc53b
--- /dev/null
+++ b/deepface/commons/os_path.py
@@ -0,0 +1,10 @@
+import os
+
+class os_path :
+
+ def get_main_directory():
+ path = os.path.abspath(__file__)
+ drive, _ = os.path.splitdrive(path)
+ if not drive.endswith(os.path.sep):
+ drive += os.path.sep
+ return drive
\ No newline at end of file
diff --git a/deepface/commons/package_utils.py b/deepface/commons/package_utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..c35db8bc55114b58068d4eb3705c58e6b17030f3
--- /dev/null
+++ b/deepface/commons/package_utils.py
@@ -0,0 +1,46 @@
+# 3rd party dependencies
+import tensorflow as tf
+
+# package dependencies
+from deepface.commons import logger as log
+
+logger = log.get_singletonish_logger()
+
+
+def get_tf_major_version() -> int:
+ """
+ Find tensorflow's major version
+ Returns
+ major_version (int)
+ """
+ return int(tf.__version__.split(".", maxsplit=1)[0])
+
+
+def get_tf_minor_version() -> int:
+ """
+ Find tensorflow's minor version
+ Returns
+ minor_version (int)
+ """
+ return int(tf.__version__.split(".", maxsplit=-1)[1])
+
+
+def validate_for_keras3():
+ tf_major = get_tf_major_version()
+ tf_minor = get_tf_minor_version()
+
+ # tf_keras is a must dependency after tf 2.16
+ if tf_major == 1 or (tf_major == 2 and tf_minor < 16):
+ return
+
+ try:
+ import tf_keras
+
+ logger.debug(f"tf_keras is already available - {tf_keras.__version__}")
+ except ImportError as err:
+ # you may consider to install that package here
+ raise ValueError(
+ f"You have tensorflow {tf.__version__} and this requires "
+ "tf-keras package. Please run `pip install tf-keras` "
+ "or downgrade your tensorflow."
+ ) from err
diff --git a/deepface/commons/path.py b/deepface/commons/path.py
new file mode 100644
index 0000000000000000000000000000000000000000..30abbb198e9f3dbb0266485663cd4bb0a12686f2
--- /dev/null
+++ b/deepface/commons/path.py
@@ -0,0 +1,9 @@
+import os
+
+class path :
+
+def get_parent_path(path,levels=1):
+ for _ in range(levels):
+ path = os.path.dirname(path)
+ return path
+
diff --git a/deepface/detectors/CenterFace.py b/deepface/detectors/CenterFace.py
new file mode 100644
index 0000000000000000000000000000000000000000..84db3345c977841d9b2ca69aa60cdd0f2a6d7219
--- /dev/null
+++ b/deepface/detectors/CenterFace.py
@@ -0,0 +1,217 @@
+# built-in dependencies
+import os
+from typing import List
+
+# 3rd party dependencies
+import numpy as np
+import cv2
+import gdown
+
+# project dependencies
+from deepface.commons import folder_utils
+from deepface.models.Detector import Detector, FacialAreaRegion
+from deepface.commons import logger as log
+
+logger = log.get_singletonish_logger()
+
+# pylint: disable=c-extension-no-member
+
+WEIGHTS_URL = "https://github.com/Star-Clouds/CenterFace/raw/master/models/onnx/centerface.onnx"
+
+
+class CenterFaceClient(Detector):
+ def __init__(self):
+ # BUG: model must be flushed for each call
+ # self.model = self.build_model()
+ pass
+
+ def build_model(self):
+ """
+ Download pre-trained weights of CenterFace model if necessary and load built model
+ """
+ weights_path = f"{folder_utils.get_deepface_home()}/.deepface/weights/centerface.onnx"
+ if not os.path.isfile(weights_path):
+ logger.info(f"Downloading CenterFace weights from {WEIGHTS_URL} to {weights_path}...")
+ try:
+ gdown.download(WEIGHTS_URL, weights_path, quiet=False)
+ except Exception as err:
+ raise ValueError(
+ f"Exception while downloading CenterFace weights from {WEIGHTS_URL}."
+ f"You may consider to download it to {weights_path} manually."
+ ) from err
+ logger.info(f"CenterFace model is just downloaded to {os.path.basename(weights_path)}")
+
+ return CenterFace(weight_path=weights_path)
+
+ def detect_faces(self, img: np.ndarray) -> List["FacialAreaRegion"]:
+ """
+ Detect and align face with CenterFace
+
+ Args:
+ img (np.ndarray): pre-loaded image as numpy array
+
+ Returns:
+ results (List[FacialAreaRegion]): A list of FacialAreaRegion objects
+ """
+ resp = []
+
+ threshold = float(os.getenv("CENTERFACE_THRESHOLD", "0.80"))
+
+ # BUG: model causes problematic results from 2nd call if it is not flushed
+ # detections, landmarks = self.model.forward(
+ # img, img.shape[0], img.shape[1], threshold=threshold
+ # )
+ detections, landmarks = self.build_model().forward(
+ img, img.shape[0], img.shape[1], threshold=threshold
+ )
+
+ for i, detection in enumerate(detections):
+ boxes, confidence = detection[:4], detection[4]
+
+ x = boxes[0]
+ y = boxes[1]
+ w = boxes[2] - x
+ h = boxes[3] - y
+
+ landmark = landmarks[i]
+
+ right_eye = (int(landmark[0]), int(landmark[1]))
+ left_eye = (int(landmark[2]), int(landmark[3]))
+ # nose = (int(landmark[4]), int(landmark [5]))
+ # mouth_right = (int(landmark[6]), int(landmark [7]))
+ # mouth_left = (int(landmark[8]), int(landmark [9]))
+
+ facial_area = FacialAreaRegion(
+ x=int(x),
+ y=int(y),
+ w=int(w),
+ h=int(h),
+ left_eye=left_eye,
+ right_eye=right_eye,
+ confidence=min(max(0, float(confidence)), 1.0),
+ )
+ resp.append(facial_area)
+
+ return resp
+
+
+class CenterFace:
+ """
+ This class is heavily inspired from
+ github.com/Star-Clouds/CenterFace/blob/master/prj-python/centerface.py
+ """
+
+ def __init__(self, weight_path: str):
+ self.net = cv2.dnn.readNetFromONNX(weight_path)
+ self.img_h_new, self.img_w_new, self.scale_h, self.scale_w = 0, 0, 0, 0
+
+ def forward(self, img, height, width, threshold=0.5):
+ self.img_h_new, self.img_w_new, self.scale_h, self.scale_w = self.transform(height, width)
+ return self.inference_opencv(img, threshold)
+
+ def inference_opencv(self, img, threshold):
+ blob = cv2.dnn.blobFromImage(
+ img,
+ scalefactor=1.0,
+ size=(self.img_w_new, self.img_h_new),
+ mean=(0, 0, 0),
+ swapRB=True,
+ crop=False,
+ )
+ self.net.setInput(blob)
+ heatmap, scale, offset, lms = self.net.forward(["537", "538", "539", "540"])
+ return self.postprocess(heatmap, lms, offset, scale, threshold)
+
+ def transform(self, h, w):
+ img_h_new, img_w_new = int(np.ceil(h / 32) * 32), int(np.ceil(w / 32) * 32)
+ scale_h, scale_w = img_h_new / h, img_w_new / w
+ return img_h_new, img_w_new, scale_h, scale_w
+
+ def postprocess(self, heatmap, lms, offset, scale, threshold):
+ dets, lms = self.decode(
+ heatmap, scale, offset, lms, (self.img_h_new, self.img_w_new), threshold=threshold
+ )
+ if len(dets) > 0:
+ dets[:, 0:4:2], dets[:, 1:4:2] = (
+ dets[:, 0:4:2] / self.scale_w,
+ dets[:, 1:4:2] / self.scale_h,
+ )
+ lms[:, 0:10:2], lms[:, 1:10:2] = (
+ lms[:, 0:10:2] / self.scale_w,
+ lms[:, 1:10:2] / self.scale_h,
+ )
+ else:
+ dets = np.empty(shape=[0, 5], dtype=np.float32)
+ lms = np.empty(shape=[0, 10], dtype=np.float32)
+ return dets, lms
+
+ def decode(self, heatmap, scale, offset, landmark, size, threshold=0.1):
+ heatmap = np.squeeze(heatmap)
+ scale0, scale1 = scale[0, 0, :, :], scale[0, 1, :, :]
+ offset0, offset1 = offset[0, 0, :, :], offset[0, 1, :, :]
+ c0, c1 = np.where(heatmap > threshold)
+ boxes, lms = [], []
+ if len(c0) > 0:
+ # pylint:disable=consider-using-enumerate
+ for i in range(len(c0)):
+ s0, s1 = np.exp(scale0[c0[i], c1[i]]) * 4, np.exp(scale1[c0[i], c1[i]]) * 4
+ o0, o1 = offset0[c0[i], c1[i]], offset1[c0[i], c1[i]]
+ s = heatmap[c0[i], c1[i]]
+ x1, y1 = max(0, (c1[i] + o1 + 0.5) * 4 - s1 / 2), max(
+ 0, (c0[i] + o0 + 0.5) * 4 - s0 / 2
+ )
+ x1, y1 = min(x1, size[1]), min(y1, size[0])
+ boxes.append([x1, y1, min(x1 + s1, size[1]), min(y1 + s0, size[0]), s])
+ lm = []
+ for j in range(5):
+ lm.append(landmark[0, j * 2 + 1, c0[i], c1[i]] * s1 + x1)
+ lm.append(landmark[0, j * 2, c0[i], c1[i]] * s0 + y1)
+ lms.append(lm)
+ boxes = np.asarray(boxes, dtype=np.float32)
+ keep = self.nms(boxes[:, :4], boxes[:, 4], 0.3)
+ boxes = boxes[keep, :]
+ lms = np.asarray(lms, dtype=np.float32)
+ lms = lms[keep, :]
+ return boxes, lms
+
+ def nms(self, boxes, scores, nms_thresh):
+ x1 = boxes[:, 0]
+ y1 = boxes[:, 1]
+ x2 = boxes[:, 2]
+ y2 = boxes[:, 3]
+ areas = (x2 - x1 + 1) * (y2 - y1 + 1)
+ order = np.argsort(scores)[::-1]
+ num_detections = boxes.shape[0]
+ suppressed = np.zeros((num_detections,), dtype=bool)
+
+ keep = []
+ for _i in range(num_detections):
+ i = order[_i]
+ if suppressed[i]:
+ continue
+ keep.append(i)
+
+ ix1 = x1[i]
+ iy1 = y1[i]
+ ix2 = x2[i]
+ iy2 = y2[i]
+ iarea = areas[i]
+
+ for _j in range(_i + 1, num_detections):
+ j = order[_j]
+ if suppressed[j]:
+ continue
+
+ xx1 = max(ix1, x1[j])
+ yy1 = max(iy1, y1[j])
+ xx2 = min(ix2, x2[j])
+ yy2 = min(iy2, y2[j])
+ w = max(0, xx2 - xx1 + 1)
+ h = max(0, yy2 - yy1 + 1)
+
+ inter = w * h
+ ovr = inter / (iarea + areas[j] - inter)
+ if ovr >= nms_thresh:
+ suppressed[j] = True
+
+ return keep
diff --git a/deepface/detectors/DetectorWrapper.py b/deepface/detectors/DetectorWrapper.py
new file mode 100644
index 0000000000000000000000000000000000000000..9a5c11b21a7537f4ee734def884f975fddde7920
--- /dev/null
+++ b/deepface/detectors/DetectorWrapper.py
@@ -0,0 +1,204 @@
+from typing import Any, List, Tuple
+import numpy as np
+from deepface.modules import detection
+from deepface.models.Detector import Detector, DetectedFace, FacialAreaRegion
+from deepface.detectors import (
+ FastMtCnn,
+ MediaPipe,
+ MtCnn,
+ OpenCv,
+ Dlib,
+ RetinaFace,
+ Ssd,
+ Yolo,
+ YuNet,
+ CenterFace,
+)
+from deepface.commons import logger as log
+
+logger = log.get_singletonish_logger()
+
+
+def build_model(detector_backend: str) -> Any:
+ """
+ Build a face detector model
+ Args:
+ detector_backend (str): backend detector name
+ Returns:
+ built detector (Any)
+ """
+ global face_detector_obj # singleton design pattern
+
+ backends = {
+ "opencv": OpenCv.OpenCvClient,
+ "mtcnn": MtCnn.MtCnnClient,
+ "ssd": Ssd.SsdClient,
+ "dlib": Dlib.DlibClient,
+ "retinaface": RetinaFace.RetinaFaceClient,
+ "mediapipe": MediaPipe.MediaPipeClient,
+ "yolov8": Yolo.YoloClient,
+ "yunet": YuNet.YuNetClient,
+ "fastmtcnn": FastMtCnn.FastMtCnnClient,
+ "centerface": CenterFace.CenterFaceClient,
+ }
+
+ if not "face_detector_obj" in globals():
+ face_detector_obj = {}
+
+ built_models = list(face_detector_obj.keys())
+ if detector_backend not in built_models:
+ face_detector = backends.get(detector_backend)
+
+ if face_detector:
+ face_detector = face_detector()
+ face_detector_obj[detector_backend] = face_detector
+ else:
+ raise ValueError("invalid detector_backend passed - " + detector_backend)
+
+ return face_detector_obj[detector_backend]
+
+
+def detect_faces(
+ detector_backend: str, img: np.ndarray, align: bool = True, expand_percentage: int = 0
+) -> List[DetectedFace]:
+ """
+ Detect face(s) from a given image
+ Args:
+ detector_backend (str): detector name
+
+ img (np.ndarray): pre-loaded image
+
+ align (bool): enable or disable alignment after detection
+
+ expand_percentage (int): expand detected facial area with a percentage (default is 0).
+
+ Returns:
+ results (List[DetectedFace]): A list of DetectedFace objects
+ where each object contains:
+
+ - img (np.ndarray): The detected face as a NumPy array.
+
+ - facial_area (FacialAreaRegion): The facial area region represented as x, y, w, h,
+ left_eye and right eye. left eye and right eye are eyes on the left and right
+ with respect to the person instead of observer.
+
+ - confidence (float): The confidence score associated with the detected face.
+ """
+ face_detector: Detector = build_model(detector_backend)
+
+ # validate expand percentage score
+ if expand_percentage < 0:
+ logger.warn(
+ f"Expand percentage cannot be negative but you set it to {expand_percentage}."
+ "Overwritten it to 0."
+ )
+ expand_percentage = 0
+
+ # find facial areas of given image
+ facial_areas = face_detector.detect_faces(img)
+
+ results = []
+ for facial_area in facial_areas:
+ x = facial_area.x
+ y = facial_area.y
+ w = facial_area.w
+ h = facial_area.h
+ left_eye = facial_area.left_eye
+ right_eye = facial_area.right_eye
+ confidence = facial_area.confidence
+
+ if expand_percentage > 0:
+ # Expand the facial region height and width by the provided percentage
+ # ensuring that the expanded region stays within img.shape limits
+ expanded_w = w + int(w * expand_percentage / 100)
+ expanded_h = h + int(h * expand_percentage / 100)
+
+ x = max(0, x - int((expanded_w - w) / 2))
+ y = max(0, y - int((expanded_h - h) / 2))
+ w = min(img.shape[1] - x, expanded_w)
+ h = min(img.shape[0] - y, expanded_h)
+
+ # extract detected face unaligned
+ detected_face = img[int(y) : int(y + h), int(x) : int(x + w)]
+
+ # align original image, then find projection of detected face area after alignment
+ if align is True: # and left_eye is not None and right_eye is not None:
+ aligned_img, angle = detection.align_face(
+ img=img, left_eye=left_eye, right_eye=right_eye
+ )
+ rotated_x1, rotated_y1, rotated_x2, rotated_y2 = rotate_facial_area(
+ facial_area=(x, y, x + w, y + h), angle=angle, size=(img.shape[0], img.shape[1])
+ )
+ detected_face = aligned_img[
+ int(rotated_y1) : int(rotated_y2), int(rotated_x1) : int(rotated_x2)
+ ]
+
+ result = DetectedFace(
+ img=detected_face,
+ facial_area=FacialAreaRegion(
+ x=x, y=y, h=h, w=w, confidence=confidence, left_eye=left_eye, right_eye=right_eye
+ ),
+ confidence=confidence,
+ )
+ results.append(result)
+ return results
+
+
+def rotate_facial_area(
+ facial_area: Tuple[int, int, int, int], angle: float, size: Tuple[int, int]
+) -> Tuple[int, int, int, int]:
+ """
+ Rotate the facial area around its center.
+ Inspried from the work of @UmutDeniz26 - github.com/serengil/retinaface/pull/80
+
+ Args:
+ facial_area (tuple of int): Representing the (x1, y1, x2, y2) of the facial area.
+ x2 is equal to x1 + w1, and y2 is equal to y1 + h1
+ angle (float): Angle of rotation in degrees. Its sign determines the direction of rotation.
+ Note that angles > 360 degrees are normalized to the range [0, 360).
+ size (tuple of int): Tuple representing the size of the image (width, height).
+
+ Returns:
+ rotated_coordinates (tuple of int): Representing the new coordinates
+ (x1, y1, x2, y2) or (x1, y1, x1+w1, y1+h1) of the rotated facial area.
+ """
+
+ # Normalize the witdh of the angle so we don't have to
+ # worry about rotations greater than 360 degrees.
+ # We workaround the quirky behavior of the modulo operator
+ # for negative angle values.
+ direction = 1 if angle >= 0 else -1
+ angle = abs(angle) % 360
+ if angle == 0:
+ return facial_area
+
+ # Angle in radians
+ angle = angle * np.pi / 180
+
+ height, weight = size
+
+ # Translate the facial area to the center of the image
+ x = (facial_area[0] + facial_area[2]) / 2 - weight / 2
+ y = (facial_area[1] + facial_area[3]) / 2 - height / 2
+
+ # Rotate the facial area
+ x_new = x * np.cos(angle) + y * direction * np.sin(angle)
+ y_new = -x * direction * np.sin(angle) + y * np.cos(angle)
+
+ # Translate the facial area back to the original position
+ x_new = x_new + weight / 2
+ y_new = y_new + height / 2
+
+ # Calculate projected coordinates after alignment
+ x1 = x_new - (facial_area[2] - facial_area[0]) / 2
+ y1 = y_new - (facial_area[3] - facial_area[1]) / 2
+ x2 = x_new + (facial_area[2] - facial_area[0]) / 2
+ y2 = y_new + (facial_area[3] - facial_area[1]) / 2
+
+ # validate projected coordinates are in image's boundaries
+ x1 = max(int(x1), 0)
+ y1 = max(int(y1), 0)
+ x2 = min(int(x2), weight)
+ y2 = min(int(y2), height)
+
+ return (x1, y1, x2, y2)
diff --git a/deepface/detectors/Dlib.py b/deepface/detectors/Dlib.py
new file mode 100644
index 0000000000000000000000000000000000000000..0699efbb3f4e5ab7c6d86fb3c59dc2b567f5246f
--- /dev/null
+++ b/deepface/detectors/Dlib.py
@@ -0,0 +1,114 @@
+from typing import List
+import os
+import bz2
+import gdown
+import numpy as np
+from deepface.commons import folder_utils
+from deepface.models.Detector import Detector, FacialAreaRegion
+from deepface.commons import logger as log
+
+logger = log.get_singletonish_logger()
+
+
+class DlibClient(Detector):
+ def __init__(self):
+ self.model = self.build_model()
+
+ def build_model(self) -> dict:
+ """
+ Build a dlib hog face detector model
+ Returns:
+ model (Any)
+ """
+ home = folder_utils.get_deepface_home()
+
+ # this is not a must dependency. do not import it in the global level.
+ try:
+ import dlib
+ except ModuleNotFoundError as e:
+ raise ImportError(
+ "Dlib is an optional detector, ensure the library is installed."
+ "Please install using 'pip install dlib' "
+ ) from e
+
+ # check required file exists in the home/.deepface/weights folder
+ if os.path.isfile(home + "/.deepface/weights/shape_predictor_5_face_landmarks.dat") != True:
+
+ file_name = "shape_predictor_5_face_landmarks.dat.bz2"
+ logger.info(f"{file_name} is going to be downloaded")
+
+ url = f"http://dlib.net/files/{file_name}"
+ output = f"{home}/.deepface/weights/{file_name}"
+
+ gdown.download(url, output, quiet=False)
+
+ zipfile = bz2.BZ2File(output)
+ data = zipfile.read()
+ newfilepath = output[:-4] # discard .bz2 extension
+ with open(newfilepath, "wb") as f:
+ f.write(data)
+
+ face_detector = dlib.get_frontal_face_detector()
+ sp = dlib.shape_predictor(home + "/.deepface/weights/shape_predictor_5_face_landmarks.dat")
+
+ detector = {}
+ detector["face_detector"] = face_detector
+ detector["sp"] = sp
+ return detector
+
+ def detect_faces(self, img: np.ndarray) -> List[FacialAreaRegion]:
+ """
+ Detect and align face with dlib
+
+ Args:
+ img (np.ndarray): pre-loaded image as numpy array
+
+ Returns:
+ results (List[FacialAreaRegion]): A list of FacialAreaRegion objects
+ """
+ resp = []
+
+ face_detector = self.model["face_detector"]
+
+ # note that, by design, dlib's fhog face detector scores are >0 but not capped at 1
+ detections, scores, _ = face_detector.run(img, 1)
+
+ if len(detections) > 0:
+
+ for idx, detection in enumerate(detections):
+ left = detection.left()
+ right = detection.right()
+ top = detection.top()
+ bottom = detection.bottom()
+
+ y = int(max(0, top))
+ h = int(min(bottom, img.shape[0]) - y)
+ x = int(max(0, left))
+ w = int(min(right, img.shape[1]) - x)
+
+ shape = self.model["sp"](img, detection)
+
+ right_eye = (
+ int((shape.part(2).x + shape.part(3).x) // 2),
+ int((shape.part(2).y + shape.part(3).y) // 2),
+ )
+ left_eye = (
+ int((shape.part(0).x + shape.part(1).x) // 2),
+ int((shape.part(0).y + shape.part(1).y) // 2),
+ )
+
+ # never saw confidence higher than +3.5 github.com/davisking/dlib/issues/761
+ confidence = scores[idx]
+
+ facial_area = FacialAreaRegion(
+ x=x,
+ y=y,
+ w=w,
+ h=h,
+ left_eye=left_eye,
+ right_eye=right_eye,
+ confidence=min(max(0, confidence), 1.0),
+ )
+ resp.append(facial_area)
+
+ return resp
diff --git a/deepface/detectors/FastMtCnn.py b/deepface/detectors/FastMtCnn.py
new file mode 100644
index 0000000000000000000000000000000000000000..ee8e69c124f87b2f801c9ec0c89d334a1edfd8f5
--- /dev/null
+++ b/deepface/detectors/FastMtCnn.py
@@ -0,0 +1,89 @@
+from typing import Any, Union, List
+import cv2
+import numpy as np
+from deepface.models.Detector import Detector, FacialAreaRegion
+
+# Link -> https://github.com/timesler/facenet-pytorch
+# Examples https://www.kaggle.com/timesler/guide-to-mtcnn-in-facenet-pytorch
+
+
+class FastMtCnnClient(Detector):
+ def __init__(self):
+ self.model = self.build_model()
+
+ def detect_faces(self, img: np.ndarray) -> List[FacialAreaRegion]:
+ """
+ Detect and align face with mtcnn
+
+ Args:
+ img (np.ndarray): pre-loaded image as numpy array
+
+ Returns:
+ results (List[FacialAreaRegion]): A list of FacialAreaRegion objects
+ """
+ resp = []
+
+ img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # mtcnn expects RGB but OpenCV read BGR
+ detections = self.model.detect(
+ img_rgb, landmarks=True
+ ) # returns boundingbox, prob, landmark
+ if (
+ detections is not None
+ and len(detections) > 0
+ and not any(detection is None for detection in detections) # issue 1043
+ ):
+ for regions, confidence, eyes in zip(*detections):
+ x, y, w, h = xyxy_to_xywh(regions)
+ right_eye = eyes[0]
+ left_eye = eyes[1]
+
+ left_eye = tuple(int(i) for i in left_eye)
+ right_eye = tuple(int(i) for i in right_eye)
+
+ facial_area = FacialAreaRegion(
+ x=x,
+ y=y,
+ w=w,
+ h=h,
+ left_eye=left_eye,
+ right_eye=right_eye,
+ confidence=confidence,
+ )
+ resp.append(facial_area)
+
+ return resp
+
+ def build_model(self) -> Any:
+ """
+ Build a fast mtcnn face detector model
+ Returns:
+ model (Any)
+ """
+ # this is not a must dependency. do not import it in the global level.
+ try:
+ from facenet_pytorch import MTCNN as fast_mtcnn
+ import torch
+ except ModuleNotFoundError as e:
+ raise ImportError(
+ "FastMtcnn is an optional detector, ensure the library is installed."
+ "Please install using 'pip install facenet-pytorch' "
+ ) from e
+
+ device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
+ face_detector = fast_mtcnn(device=device)
+
+ return face_detector
+
+
+def xyxy_to_xywh(regions: Union[list, tuple]) -> tuple:
+ """
+ Convert (x1, y1, x2, y2) format to (x, y, w, h) format.
+ Args:
+ regions (list or tuple): facial area coordinates as x, y, x+w, y+h
+ Returns:
+ regions (tuple): facial area coordinates as x, y, w, h
+ """
+ x, y, x_plus_w, y_plus_h = regions[0], regions[1], regions[2], regions[3]
+ w = x_plus_w - x
+ h = y_plus_h - y
+ return (x, y, w, h)
diff --git a/deepface/detectors/MediaPipe.py b/deepface/detectors/MediaPipe.py
new file mode 100644
index 0000000000000000000000000000000000000000..099b0d40b158a28d5f0091136e1959d54ff18470
--- /dev/null
+++ b/deepface/detectors/MediaPipe.py
@@ -0,0 +1,76 @@
+from typing import Any, List
+import numpy as np
+from deepface.models.Detector import Detector, FacialAreaRegion
+
+# Link - https://google.github.io/mediapipe/solutions/face_detection
+
+
+class MediaPipeClient(Detector):
+ def __init__(self):
+ self.model = self.build_model()
+
+ def build_model(self) -> Any:
+ """
+ Build a mediapipe face detector model
+ Returns:
+ model (Any)
+ """
+ # this is not a must dependency. do not import it in the global level.
+ try:
+ import mediapipe as mp
+ except ModuleNotFoundError as e:
+ raise ImportError(
+ "MediaPipe is an optional detector, ensure the library is installed."
+ "Please install using 'pip install mediapipe' "
+ ) from e
+
+ mp_face_detection = mp.solutions.face_detection
+ face_detection = mp_face_detection.FaceDetection(min_detection_confidence=0.7)
+ return face_detection
+
+ def detect_faces(self, img: np.ndarray) -> List[FacialAreaRegion]:
+ """
+ Detect and align face with mediapipe
+
+ Args:
+ img (np.ndarray): pre-loaded image as numpy array
+
+ Returns:
+ results (List[FacialAreaRegion]): A list of FacialAreaRegion objects
+ """
+ resp = []
+
+ img_width = img.shape[1]
+ img_height = img.shape[0]
+
+ results = self.model.process(img)
+
+ # If no face has been detected, return an empty list
+ if results.detections is None:
+ return resp
+
+ # Extract the bounding box, the landmarks and the confidence score
+ for current_detection in results.detections:
+ (confidence,) = current_detection.score
+
+ bounding_box = current_detection.location_data.relative_bounding_box
+ landmarks = current_detection.location_data.relative_keypoints
+
+ x = int(bounding_box.xmin * img_width)
+ w = int(bounding_box.width * img_width)
+ y = int(bounding_box.ymin * img_height)
+ h = int(bounding_box.height * img_height)
+
+ right_eye = (int(landmarks[0].x * img_width), int(landmarks[0].y * img_height))
+ left_eye = (int(landmarks[1].x * img_width), int(landmarks[1].y * img_height))
+ # nose = (int(landmarks[2].x * img_width), int(landmarks[2].y * img_height))
+ # mouth = (int(landmarks[3].x * img_width), int(landmarks[3].y * img_height))
+ # right_ear = (int(landmarks[4].x * img_width), int(landmarks[4].y * img_height))
+ # left_ear = (int(landmarks[5].x * img_width), int(landmarks[5].y * img_height))
+
+ facial_area = FacialAreaRegion(
+ x=x, y=y, w=w, h=h, left_eye=left_eye, right_eye=right_eye, confidence=confidence
+ )
+ resp.append(facial_area)
+
+ return resp
diff --git a/deepface/detectors/MtCnn.py b/deepface/detectors/MtCnn.py
new file mode 100644
index 0000000000000000000000000000000000000000..527c9e5bf17078603b1d68051f217f461c364b5b
--- /dev/null
+++ b/deepface/detectors/MtCnn.py
@@ -0,0 +1,55 @@
+from typing import List
+import numpy as np
+from mtcnn import MTCNN
+from deepface.models.Detector import Detector, FacialAreaRegion
+
+# pylint: disable=too-few-public-methods
+class MtCnnClient(Detector):
+ """
+ Class to cover common face detection functionalitiy for MtCnn backend
+ """
+
+ def __init__(self):
+ self.model = MTCNN()
+
+ def detect_faces(self, img: np.ndarray) -> List[FacialAreaRegion]:
+ """
+ Detect and align face with mtcnn
+
+ Args:
+ img (np.ndarray): pre-loaded image as numpy array
+
+ Returns:
+ results (List[FacialAreaRegion]): A list of FacialAreaRegion objects
+ """
+
+ resp = []
+
+ # mtcnn expects RGB but OpenCV read BGR
+ # img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
+ img_rgb = img[:, :, ::-1]
+ detections = self.model.detect_faces(img_rgb)
+
+ if detections is not None and len(detections) > 0:
+
+ for current_detection in detections:
+ x, y, w, h = current_detection["box"]
+ confidence = current_detection["confidence"]
+ # mtcnn detector assigns left eye with respect to the observer
+ # but we are setting it with respect to the person itself
+ left_eye = current_detection["keypoints"]["right_eye"]
+ right_eye = current_detection["keypoints"]["left_eye"]
+
+ facial_area = FacialAreaRegion(
+ x=x,
+ y=y,
+ w=w,
+ h=h,
+ left_eye=left_eye,
+ right_eye=right_eye,
+ confidence=confidence,
+ )
+
+ resp.append(facial_area)
+
+ return resp
diff --git a/deepface/detectors/OpenCv.py b/deepface/detectors/OpenCv.py
new file mode 100644
index 0000000000000000000000000000000000000000..9b59d7da206f270a5e93428ba423845198c26e49
--- /dev/null
+++ b/deepface/detectors/OpenCv.py
@@ -0,0 +1,178 @@
+import os
+from typing import Any, List
+import cv2
+import numpy as np
+from deepface.models.Detector import Detector, FacialAreaRegion
+
+
+class OpenCvClient(Detector):
+ """
+ Class to cover common face detection functionalitiy for OpenCv backend
+ """
+
+ def __init__(self):
+ self.model = self.build_model()
+
+ def build_model(self):
+ """
+ Build opencv's face and eye detector models
+ Returns:
+ model (dict): including face_detector and eye_detector keys
+ """
+ detector = {}
+ detector["face_detector"] = self.__build_cascade("haarcascade")
+ detector["eye_detector"] = self.__build_cascade("haarcascade_eye")
+ return detector
+
+ def detect_faces(self, img: np.ndarray) -> List[FacialAreaRegion]:
+ """
+ Detect and align face with opencv
+
+ Args:
+ img (np.ndarray): pre-loaded image as numpy array
+
+ Returns:
+ results (List[FacialAreaRegion]): A list of FacialAreaRegion objects
+ """
+ resp = []
+
+ detected_face = None
+
+ faces = []
+ try:
+ # faces = detector["face_detector"].detectMultiScale(img, 1.3, 5)
+
+ # note that, by design, opencv's haarcascade scores are >0 but not capped at 1
+ faces, _, scores = self.model["face_detector"].detectMultiScale3(
+ img, 1.1, 10, outputRejectLevels=True
+ )
+ except:
+ pass
+
+ if len(faces) > 0:
+ for (x, y, w, h), confidence in zip(faces, scores):
+ detected_face = img[int(y) : int(y + h), int(x) : int(x + w)]
+ left_eye, right_eye = self.find_eyes(img=detected_face)
+
+ # eyes found in the detected face instead image itself
+ # detected face's coordinates should be added
+ if left_eye is not None:
+ left_eye = (int(x + left_eye[0]), int(y + left_eye[1]))
+ if right_eye is not None:
+ right_eye = (int(x + right_eye[0]), int(y + right_eye[1]))
+
+ facial_area = FacialAreaRegion(
+ x=x,
+ y=y,
+ w=w,
+ h=h,
+ left_eye=left_eye,
+ right_eye=right_eye,
+ confidence=(100 - confidence) / 100,
+ )
+ resp.append(facial_area)
+
+ return resp
+
+ def find_eyes(self, img: np.ndarray) -> tuple:
+ """
+ Find the left and right eye coordinates of given image
+ Args:
+ img (np.ndarray): given image
+ Returns:
+ left and right eye (tuple)
+ """
+ left_eye = None
+ right_eye = None
+
+ # if image has unexpectedly 0 dimension then skip alignment
+ if img.shape[0] == 0 or img.shape[1] == 0:
+ return left_eye, right_eye
+
+ detected_face_gray = cv2.cvtColor(
+ img, cv2.COLOR_BGR2GRAY
+ ) # eye detector expects gray scale image
+
+ eyes = self.model["eye_detector"].detectMultiScale(detected_face_gray, 1.1, 10)
+
+ # ----------------------------------------------------------------
+
+ # opencv eye detection module is not strong. it might find more than 2 eyes!
+ # besides, it returns eyes with different order in each call (issue 435)
+ # this is an important issue because opencv is the default detector and ssd also uses this
+ # find the largest 2 eye. Thanks to @thelostpeace
+
+ eyes = sorted(eyes, key=lambda v: abs(v[2] * v[3]), reverse=True)
+
+ # ----------------------------------------------------------------
+ if len(eyes) >= 2:
+ # decide left and right eye
+
+ eye_1 = eyes[0]
+ eye_2 = eyes[1]
+
+ if eye_1[0] < eye_2[0]:
+ right_eye = eye_1
+ left_eye = eye_2
+ else:
+ right_eye = eye_2
+ left_eye = eye_1
+
+ # -----------------------
+ # find center of eyes
+ left_eye = (
+ int(left_eye[0] + (left_eye[2] / 2)),
+ int(left_eye[1] + (left_eye[3] / 2)),
+ )
+ right_eye = (
+ int(right_eye[0] + (right_eye[2] / 2)),
+ int(right_eye[1] + (right_eye[3] / 2)),
+ )
+ return left_eye, right_eye
+
+ def __build_cascade(self, model_name="haarcascade") -> Any:
+ """
+ Build a opencv face&eye detector models
+ Returns:
+ model (Any)
+ """
+ opencv_path = self.__get_opencv_path()
+ if model_name == "haarcascade":
+ face_detector_path = opencv_path + "haarcascade_frontalface_default.xml"
+ if os.path.isfile(face_detector_path) != True:
+ raise ValueError(
+ "Confirm that opencv is installed on your environment! Expected path ",
+ face_detector_path,
+ " violated.",
+ )
+ detector = cv2.CascadeClassifier(face_detector_path)
+
+ elif model_name == "haarcascade_eye":
+ eye_detector_path = opencv_path + "haarcascade_eye.xml"
+ if os.path.isfile(eye_detector_path) != True:
+ raise ValueError(
+ "Confirm that opencv is installed on your environment! Expected path ",
+ eye_detector_path,
+ " violated.",
+ )
+ detector = cv2.CascadeClassifier(eye_detector_path)
+
+ else:
+ raise ValueError(f"unimplemented model_name for build_cascade - {model_name}")
+
+ return detector
+
+ def __get_opencv_path(self) -> str:
+ """
+ Returns where opencv installed
+ Returns:
+ installation_path (str)
+ """
+ opencv_home = cv2.__file__
+ folders = opencv_home.split(os.path.sep)[0:-1]
+
+ path = folders[0]
+ for folder in folders[1:]:
+ path = path + "/" + folder
+
+ return path + "/data/"
diff --git a/deepface/detectors/RetinaFace.py b/deepface/detectors/RetinaFace.py
new file mode 100644
index 0000000000000000000000000000000000000000..a21a7931837f1099650bb440c7db1d82dea6629b
--- /dev/null
+++ b/deepface/detectors/RetinaFace.py
@@ -0,0 +1,59 @@
+from typing import List
+import numpy as np
+from retinaface import RetinaFace as rf
+from deepface.models.Detector import Detector, FacialAreaRegion
+
+# pylint: disable=too-few-public-methods
+class RetinaFaceClient(Detector):
+ def __init__(self):
+ self.model = rf.build_model()
+
+ def detect_faces(self, img: np.ndarray) -> List[FacialAreaRegion]:
+ """
+ Detect and align face with retinaface
+
+ Args:
+ img (np.ndarray): pre-loaded image as numpy array
+
+ Returns:
+ results (List[FacialAreaRegion]): A list of FacialAreaRegion objects
+ """
+ resp = []
+
+ obj = rf.detect_faces(img, model=self.model, threshold=0.9)
+
+ if not isinstance(obj, dict):
+ return resp
+
+ for face_idx in obj.keys():
+ identity = obj[face_idx]
+ detection = identity["facial_area"]
+
+ y = detection[1]
+ h = detection[3] - y
+ x = detection[0]
+ w = detection[2] - x
+
+ # retinaface sets left and right eyes with respect to the person
+ left_eye = identity["landmarks"]["left_eye"]
+ right_eye = identity["landmarks"]["right_eye"]
+
+ # eyes are list of float, need to cast them tuple of int
+ left_eye = tuple(int(i) for i in left_eye)
+ right_eye = tuple(int(i) for i in right_eye)
+
+ confidence = identity["score"]
+
+ facial_area = FacialAreaRegion(
+ x=x,
+ y=y,
+ w=w,
+ h=h,
+ left_eye=left_eye,
+ right_eye=right_eye,
+ confidence=confidence,
+ )
+
+ resp.append(facial_area)
+
+ return resp
diff --git a/deepface/detectors/Ssd.py b/deepface/detectors/Ssd.py
new file mode 100644
index 0000000000000000000000000000000000000000..a8d68eb0e0c57dab8b370e5b9bbbb409de256262
--- /dev/null
+++ b/deepface/detectors/Ssd.py
@@ -0,0 +1,153 @@
+from typing import List
+import os
+import gdown
+import cv2
+import pandas as pd
+import numpy as np
+from deepface.detectors import OpenCv
+from deepface.commons import folder_utils
+from deepface.models.Detector import Detector, FacialAreaRegion
+from deepface.commons import logger as log
+
+logger = log.get_singletonish_logger()
+
+# pylint: disable=line-too-long, c-extension-no-member
+
+
+class SsdClient(Detector):
+ def __init__(self):
+ self.model = self.build_model()
+
+ def build_model(self) -> dict:
+ """
+ Build a ssd detector model
+ Returns:
+ model (dict)
+ """
+
+ home = folder_utils.get_deepface_home()
+
+ # model structure
+ if os.path.isfile(home + "/.deepface/weights/deploy.prototxt") != True:
+
+ logger.info("deploy.prototxt will be downloaded...")
+
+ url = "https://github.com/opencv/opencv/raw/3.4.0/samples/dnn/face_detector/deploy.prototxt"
+
+ output = home + "/.deepface/weights/deploy.prototxt"
+
+ gdown.download(url, output, quiet=False)
+
+ # pre-trained weights
+ if (
+ os.path.isfile(home + "/.deepface/weights/res10_300x300_ssd_iter_140000.caffemodel")
+ != True
+ ):
+
+ logger.info("res10_300x300_ssd_iter_140000.caffemodel will be downloaded...")
+
+ url = "https://github.com/opencv/opencv_3rdparty/raw/dnn_samples_face_detector_20170830/res10_300x300_ssd_iter_140000.caffemodel"
+
+ output = home + "/.deepface/weights/res10_300x300_ssd_iter_140000.caffemodel"
+
+ gdown.download(url, output, quiet=False)
+
+ try:
+ face_detector = cv2.dnn.readNetFromCaffe(
+ home + "/.deepface/weights/deploy.prototxt",
+ home + "/.deepface/weights/res10_300x300_ssd_iter_140000.caffemodel",
+ )
+ except Exception as err:
+ raise ValueError(
+ "Exception while calling opencv.dnn module."
+ + "This is an optional dependency."
+ + "You can install it as pip install opencv-contrib-python."
+ ) from err
+
+ detector = {}
+ detector["face_detector"] = face_detector
+ detector["opencv_module"] = OpenCv.OpenCvClient()
+
+ return detector
+
+ def detect_faces(self, img: np.ndarray) -> List[FacialAreaRegion]:
+ """
+ Detect and align face with ssd
+
+ Args:
+ img (np.ndarray): pre-loaded image as numpy array
+
+ Returns:
+ results (List[FacialAreaRegion]): A list of FacialAreaRegion objects
+ """
+ opencv_module: OpenCv.OpenCvClient = self.model["opencv_module"]
+
+ resp = []
+
+ detected_face = None
+
+ ssd_labels = ["img_id", "is_face", "confidence", "left", "top", "right", "bottom"]
+
+ target_size = (300, 300)
+
+ original_size = img.shape
+
+ current_img = cv2.resize(img, target_size)
+
+ aspect_ratio_x = original_size[1] / target_size[1]
+ aspect_ratio_y = original_size[0] / target_size[0]
+
+ imageBlob = cv2.dnn.blobFromImage(image=current_img)
+
+ face_detector = self.model["face_detector"]
+ face_detector.setInput(imageBlob)
+ detections = face_detector.forward()
+
+ detections_df = pd.DataFrame(detections[0][0], columns=ssd_labels)
+
+ detections_df = detections_df[detections_df["is_face"] == 1] # 0: background, 1: face
+ detections_df = detections_df[detections_df["confidence"] >= 0.90]
+
+ detections_df["left"] = (detections_df["left"] * 300).astype(int)
+ detections_df["bottom"] = (detections_df["bottom"] * 300).astype(int)
+ detections_df["right"] = (detections_df["right"] * 300).astype(int)
+ detections_df["top"] = (detections_df["top"] * 300).astype(int)
+
+ if detections_df.shape[0] > 0:
+
+ for _, instance in detections_df.iterrows():
+
+ left = instance["left"]
+ right = instance["right"]
+ bottom = instance["bottom"]
+ top = instance["top"]
+ confidence = instance["confidence"]
+
+ x = int(left * aspect_ratio_x)
+ y = int(top * aspect_ratio_y)
+ w = int(right * aspect_ratio_x) - int(left * aspect_ratio_x)
+ h = int(bottom * aspect_ratio_y) - int(top * aspect_ratio_y)
+
+ detected_face = img[int(y) : int(y + h), int(x) : int(x + w)]
+
+ left_eye, right_eye = opencv_module.find_eyes(detected_face)
+
+ # eyes found in the detected face instead image itself
+ # detected face's coordinates should be added
+ if left_eye is not None:
+ left_eye = (int(x + left_eye[0]), int(y + left_eye[1]))
+ if right_eye is not None:
+ right_eye = (int(x + right_eye[0]), int(y + right_eye[1]))
+
+ facial_area = FacialAreaRegion(
+ x=x,
+ y=y,
+ w=w,
+ h=h,
+ left_eye=left_eye,
+ right_eye=right_eye,
+ confidence=confidence,
+ )
+ resp.append(facial_area)
+
+ return resp
diff --git a/deepface/detectors/Yolo.py b/deepface/detectors/Yolo.py
new file mode 100644
index 0000000000000000000000000000000000000000..5ff1c8079b7b1453573564889c74bec5131f7110
--- /dev/null
+++ b/deepface/detectors/Yolo.py
@@ -0,0 +1,101 @@
+import os
+from typing import Any, List
+import numpy as np
+import gdown
+from deepface.models.Detector import Detector, FacialAreaRegion
+from deepface.commons import folder_utils
+from deepface.commons import logger as log
+
+logger = log.get_singletonish_logger()
+
+# Model's weights paths
+PATH = "/.deepface/weights/yolov8n-face.pt"
+
+# Google Drive URL from repo (https://github.com/derronqi/yolov8-face) ~6MB
+WEIGHT_URL = "https://drive.google.com/uc?id=1qcr9DbgsX3ryrz2uU8w4Xm3cOrRywXqb"
+
+
+class YoloClient(Detector):
+ def __init__(self):
+ self.model = self.build_model()
+
+ def build_model(self) -> Any:
+ """
+ Build a yolo detector model
+ Returns:
+ model (Any)
+ """
+
+ # Import the Ultralytics YOLO model
+ try:
+ from ultralytics import YOLO
+ except ModuleNotFoundError as e:
+ raise ImportError(
+ "Yolo is an optional detector, ensure the library is installed. \
+ Please install using 'pip install ultralytics' "
+ ) from e
+
+ weight_path = f"{folder_utils.get_deepface_home()}{PATH}"
+
+ # Download the model's weights if they don't exist
+ if not os.path.isfile(weight_path):
+ logger.info(f"Downloading Yolo weights from {WEIGHT_URL} to {weight_path}...")
+ try:
+ gdown.download(WEIGHT_URL, weight_path, quiet=False)
+ except Exception as err:
+ raise ValueError(
+ f"Exception while downloading Yolo weights from {WEIGHT_URL}."
+ f"You may consider to download it to {weight_path} manually."
+ ) from err
+ logger.info(f"Yolo model is just downloaded to {os.path.basename(weight_path)}")
+
+ # Return face_detector
+ return YOLO(weight_path)
+
+ def detect_faces(self, img: np.ndarray) -> List[FacialAreaRegion]:
+ """
+ Detect and align face with yolo
+
+ Args:
+ img (np.ndarray): pre-loaded image as numpy array
+
+ Returns:
+ results (List[FacialAreaRegion]): A list of FacialAreaRegion objects
+ """
+ resp = []
+
+ # Detect faces
+ results = self.model.predict(img, verbose=False, show=False, conf=0.25)[0]
+
+ # For each face, extract the bounding box, the landmarks and confidence
+ for result in results:
+
+ if result.boxes is None or result.keypoints is None:
+ continue
+
+ # Extract the bounding box and the confidence
+ x, y, w, h = result.boxes.xywh.tolist()[0]
+ confidence = result.boxes.conf.tolist()[0]
+
+ # right_eye_conf = result.keypoints.conf[0][0]
+ # left_eye_conf = result.keypoints.conf[0][1]
+ right_eye = result.keypoints.xy[0][0].tolist()
+ left_eye = result.keypoints.xy[0][1].tolist()
+
+ # eyes are list of float, need to cast them tuple of int
+ left_eye = tuple(int(i) for i in left_eye)
+ right_eye = tuple(int(i) for i in right_eye)
+
+ x, y, w, h = int(x - w / 2), int(y - h / 2), int(w), int(h)
+ facial_area = FacialAreaRegion(
+ x=x,
+ y=y,
+ w=w,
+ h=h,
+ left_eye=left_eye,
+ right_eye=right_eye,
+ confidence=confidence,
+ )
+ resp.append(facial_area)
+
+ return resp
diff --git a/deepface/detectors/YuNet.py b/deepface/detectors/YuNet.py
new file mode 100644
index 0000000000000000000000000000000000000000..c43b80f026641a7d0a94b8fc129ea2310604c4c0
--- /dev/null
+++ b/deepface/detectors/YuNet.py
@@ -0,0 +1,133 @@
+# built-in dependencies
+import os
+from typing import Any, List
+
+# 3rd party dependencies
+import cv2
+import numpy as np
+import gdown
+
+# project dependencies
+from deepface.commons import folder_utils
+from deepface.models.Detector import Detector, FacialAreaRegion
+from deepface.commons import logger as log
+
+logger = log.get_singletonish_logger()
+
+
+class YuNetClient(Detector):
+ def __init__(self):
+ self.model = self.build_model()
+
+ def build_model(self) -> Any:
+ """
+ Build a yunet detector model
+ Returns:
+ model (Any)
+ """
+
+ opencv_version = cv2.__version__.split(".")
+ if not len(opencv_version) >= 2:
+ raise ValueError(
+ f"OpenCv's version must have major and minor values but it is {opencv_version}"
+ )
+
+ opencv_version_major = int(opencv_version[0])
+ opencv_version_minor = int(opencv_version[1])
+
+ if opencv_version_major < 4 or (opencv_version_major == 4 and opencv_version_minor < 8):
+ # min requirement: https://github.com/opencv/opencv_zoo/issues/172
+ raise ValueError(f"YuNet requires opencv-python >= 4.8 but you have {cv2.__version__}")
+
+ # pylint: disable=C0301
+ url = "https://github.com/opencv/opencv_zoo/raw/main/models/face_detection_yunet/face_detection_yunet_2023mar.onnx"
+ file_name = "face_detection_yunet_2023mar.onnx"
+ home = folder_utils.get_deepface_home()
+ if os.path.isfile(home + f"/.deepface/weights/{file_name}") is False:
+ logger.info(f"{file_name} will be downloaded...")
+ output = home + f"/.deepface/weights/{file_name}"
+ gdown.download(url, output, quiet=False)
+
+ try:
+ face_detector = cv2.FaceDetectorYN_create(
+ home + f"/.deepface/weights/{file_name}", "", (0, 0)
+ )
+ except Exception as err:
+ raise ValueError(
+ "Exception while calling opencv.FaceDetectorYN_create module."
+ + "This is an optional dependency."
+ + "You can install it as pip install opencv-contrib-python."
+ ) from err
+ return face_detector
+
+ def detect_faces(self, img: np.ndarray) -> List[FacialAreaRegion]:
+ """
+ Detect and align face with yunet
+
+ Args:
+ img (np.ndarray): pre-loaded image as numpy array
+
+ Returns:
+ results (List[FacialAreaRegion]): A list of FacialAreaRegion objects
+ """
+ # FaceDetector.detect_faces does not support score_threshold parameter.
+ # We can set it via environment variable.
+ score_threshold = float(os.environ.get("yunet_score_threshold", "0.9"))
+ resp = []
+ faces = []
+ height, width = img.shape[0], img.shape[1]
+ # resize image if it is too large (Yunet fails to detect faces on large input sometimes)
+ # I picked 640 as a threshold because it is the default value of max_size in Yunet.
+ resized = False
+ r = 1 # resize factor
+ if height > 640 or width > 640:
+ r = 640.0 / max(height, width)
+ img = cv2.resize(img, (int(width * r), int(height * r)))
+ height, width = img.shape[0], img.shape[1]
+ resized = True
+ self.model.setInputSize((width, height))
+ self.model.setScoreThreshold(score_threshold)
+ _, faces = self.model.detect(img)
+ if faces is None:
+ return resp
+ for face in faces:
+ # pylint: disable=W0105
+ """
+ The detection output faces is a two-dimension array of type CV_32F,
+ whose rows are the detected face instances, columns are the location
+ of a face and 5 facial landmarks.
+ The format of each row is as follows:
+ x1, y1, w, h, x_re, y_re, x_le, y_le, x_nt, y_nt,
+ x_rcm, y_rcm, x_lcm, y_lcm,
+ where x1, y1, w, h are the top-left coordinates, width and height of
+ the face bounding box,
+ {x, y}_{re, le, nt, rcm, lcm} stands for the coordinates of right eye,
+ left eye, nose tip, the right corner and left corner of the mouth respectively.
+ """
+ (x, y, w, h, x_le, y_le, x_re, y_re) = list(map(int, face[:8]))
+
+ # YuNet returns negative coordinates if it thinks part of the detected face
+ # is outside the frame.
+ x = max(x, 0)
+ y = max(y, 0)
+ if resized:
+ x, y, w, h = int(x / r), int(y / r), int(w / r), int(h / r)
+ x_re, y_re, x_le, y_le = (
+ int(x_re / r),
+ int(y_re / r),
+ int(x_le / r),
+ int(y_le / r),
+ )
+ confidence = float(face[-1])
+
+ facial_area = FacialAreaRegion(
+ x=x,
+ y=y,
+ w=w,
+ h=h,
+ confidence=confidence,
+ left_eye=(x_re, y_re),
+ right_eye=(x_le, y_le),
+ )
+ resp.append(facial_area)
+ return resp
diff --git a/deepface/detectors/__init__.py b/deepface/detectors/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/deepface/extendedmodels/Age.py b/deepface/extendedmodels/Age.py
new file mode 100644
index 0000000000000000000000000000000000000000..2e99995ae93c7c5215d7a7038f91782d4ca022c1
--- /dev/null
+++ b/deepface/extendedmodels/Age.py
@@ -0,0 +1,92 @@
+import os
+import gdown
+import numpy as np
+from deepface.basemodels import VGGFace
+from deepface.commons import package_utils, folder_utils
+from deepface.models.Demography import Demography
+from deepface.commons import logger as log
+
+logger = log.get_singletonish_logger()
+
+# ----------------------------------------
+# dependency configurations
+
+tf_version = package_utils.get_tf_major_version()
+
+if tf_version == 1:
+ from keras.models import Model, Sequential
+ from keras.layers import Convolution2D, Flatten, Activation
+else:
+ from tensorflow.keras.models import Model, Sequential
+ from tensorflow.keras.layers import Convolution2D, Flatten, Activation
+
+# ----------------------------------------
+
+# pylint: disable=too-few-public-methods
+class ApparentAgeClient(Demography):
+ """
+ Age model class
+ """
+
+ def __init__(self):
+ self.model = load_model()
+ self.model_name = "Age"
+
+ def predict(self, img: np.ndarray) -> np.float64:
+ age_predictions = self.model.predict(img, verbose=0)[0, :]
+ return find_apparent_age(age_predictions)
+
+
+def load_model(
+ url="https://github.com/serengil/deepface_models/releases/download/v1.0/age_model_weights.h5",
+) -> Model:
+ """
+ Construct age model, download its weights and load
+ Returns:
+ model (Model)
+ """
+
+ model = VGGFace.base_model()
+
+ # --------------------------
+
+ classes = 101
+ base_model_output = Sequential()
+ base_model_output = Convolution2D(classes, (1, 1), name="predictions")(model.layers[-4].output)
+ base_model_output = Flatten()(base_model_output)
+ base_model_output = Activation("softmax")(base_model_output)
+
+ # --------------------------
+
+ age_model = Model(inputs=model.input, outputs=base_model_output)
+
+ # --------------------------
+
+ # load weights
+
+ home = folder_utils.get_deepface_home()
+
+ if os.path.isfile(home + "/.deepface/weights/age_model_weights.h5") != True:
+ logger.info("age_model_weights.h5 will be downloaded...")
+
+ output = home + "/.deepface/weights/age_model_weights.h5"
+ gdown.download(url, output, quiet=False)
+
+ age_model.load_weights(home + "/.deepface/weights/age_model_weights.h5")
+
+ return age_model
+
+ # --------------------------
+
+
+def find_apparent_age(age_predictions: np.ndarray) -> np.float64:
+ """
+ Find apparent age prediction from a given probas of ages
+ Args:
+ age_predictions (?)
+ Returns:
+ apparent_age (float)
+ """
+ output_indexes = np.array(list(range(0, 101)))
+ apparent_age = np.sum(age_predictions * output_indexes)
+ return apparent_age
diff --git a/deepface/extendedmodels/Emotion.py b/deepface/extendedmodels/Emotion.py
new file mode 100644
index 0000000000000000000000000000000000000000..e0b93bff2324bc2adda67ba854c89aa974e1d416
--- /dev/null
+++ b/deepface/extendedmodels/Emotion.py
@@ -0,0 +1,106 @@
+# built-in dependencies
+import os
+
+# 3rd party dependencies
+import gdown
+import numpy as np
+import cv2
+
+# project dependencies
+from deepface.commons import package_utils, folder_utils
+from deepface.models.Demography import Demography
+from deepface.commons import logger as log
+
+logger = log.get_singletonish_logger()
+
+# -------------------------------------------
+# pylint: disable=line-too-long
+# -------------------------------------------
+# dependency configuration
+tf_version = package_utils.get_tf_major_version()
+
+if tf_version == 1:
+ from keras.models import Sequential
+ from keras.layers import Conv2D, MaxPooling2D, AveragePooling2D, Flatten, Dense, Dropout
+else:
+ from tensorflow.keras.models import Sequential
+ from tensorflow.keras.layers import (
+ Conv2D,
+ MaxPooling2D,
+ AveragePooling2D,
+ Flatten,
+ Dense,
+ Dropout,
+ )
+# -------------------------------------------
+
+# Labels for the emotions that can be detected by the model.
+labels = ["angry", "disgust", "fear", "happy", "sad", "surprise", "neutral"]
+
+# pylint: disable=too-few-public-methods
+class EmotionClient(Demography):
+ """
+ Emotion model class
+ """
+
+ def __init__(self):
+ self.model = load_model()
+ self.model_name = "Emotion"
+
+ def predict(self, img: np.ndarray) -> np.ndarray:
+ img_gray = cv2.cvtColor(img[0], cv2.COLOR_BGR2GRAY)
+ img_gray = cv2.resize(img_gray, (48, 48))
+ img_gray = np.expand_dims(img_gray, axis=0)
+
+ emotion_predictions = self.model.predict(img_gray, verbose=0)[0, :]
+ return emotion_predictions
+
+
+def load_model(
+ url="https://github.com/serengil/deepface_models/releases/download/v1.0/facial_expression_model_weights.h5",
+) -> Sequential:
+ """
+ Consruct emotion model, download and load weights
+ """
+
+ num_classes = 7
+
+ model = Sequential()
+
+ # 1st convolution layer
+ model.add(Conv2D(64, (5, 5), activation="relu", input_shape=(48, 48, 1)))
+ model.add(MaxPooling2D(pool_size=(5, 5), strides=(2, 2)))
+
+ # 2nd convolution layer
+ model.add(Conv2D(64, (3, 3), activation="relu"))
+ model.add(Conv2D(64, (3, 3), activation="relu"))
+ model.add(AveragePooling2D(pool_size=(3, 3), strides=(2, 2)))
+
+ # 3rd convolution layer
+ model.add(Conv2D(128, (3, 3), activation="relu"))
+ model.add(Conv2D(128, (3, 3), activation="relu"))
+ model.add(AveragePooling2D(pool_size=(3, 3), strides=(2, 2)))
+
+ model.add(Flatten())
+
+ # fully connected neural networks
+ model.add(Dense(1024, activation="relu"))
+ model.add(Dropout(0.2))
+ model.add(Dense(1024, activation="relu"))
+ model.add(Dropout(0.2))
+
+ model.add(Dense(num_classes, activation="softmax"))
+
+ # ----------------------------
+
+ home = folder_utils.get_deepface_home()
+
+ if os.path.isfile(home + "/.deepface/weights/facial_expression_model_weights.h5") != True:
+ logger.info("facial_expression_model_weights.h5 will be downloaded...")
+
+ output = home + "/.deepface/weights/facial_expression_model_weights.h5"
+ gdown.download(url, output, quiet=False)
+
+ model.load_weights(home + "/.deepface/weights/facial_expression_model_weights.h5")
+
+ return model
diff --git a/deepface/extendedmodels/Gender.py b/deepface/extendedmodels/Gender.py
new file mode 100644
index 0000000000000000000000000000000000000000..84cb4465eacadcebb562629c6a94b7244ef42596
--- /dev/null
+++ b/deepface/extendedmodels/Gender.py
@@ -0,0 +1,84 @@
+# built-in dependencies
+import os
+
+# 3rd party dependencies
+import gdown
+import numpy as np
+
+# project dependencies
+from deepface.basemodels import VGGFace
+from deepface.commons import package_utils, folder_utils
+from deepface.models.Demography import Demography
+from deepface.commons import logger as log
+
+logger = log.get_singletonish_logger()
+
+# -------------------------------------
+# pylint: disable=line-too-long
+# -------------------------------------
+# dependency configurations
+
+tf_version = package_utils.get_tf_major_version()
+if tf_version == 1:
+ from keras.models import Model, Sequential
+ from keras.layers import Convolution2D, Flatten, Activation
+else:
+ from tensorflow.keras.models import Model, Sequential
+ from tensorflow.keras.layers import Convolution2D, Flatten, Activation
+# -------------------------------------
+
+# Labels for the genders that can be detected by the model.
+labels = ["Woman", "Man"]
+
+# pylint: disable=too-few-public-methods
+class GenderClient(Demography):
+ """
+ Gender model class
+ """
+
+ def __init__(self):
+ self.model = load_model()
+ self.model_name = "Gender"
+
+ def predict(self, img: np.ndarray) -> np.ndarray:
+ return self.model.predict(img, verbose=0)[0, :]
+
+
+def load_model(
+ url="https://github.com/serengil/deepface_models/releases/download/v1.0/gender_model_weights.h5",
+) -> Model:
+ """
+ Construct gender model, download its weights and load
+ Returns:
+ model (Model)
+ """
+
+ model = VGGFace.base_model()
+
+ # --------------------------
+
+ classes = 2
+ base_model_output = Sequential()
+ base_model_output = Convolution2D(classes, (1, 1), name="predictions")(model.layers[-4].output)
+ base_model_output = Flatten()(base_model_output)
+ base_model_output = Activation("softmax")(base_model_output)
+
+ # --------------------------
+
+ gender_model = Model(inputs=model.input, outputs=base_model_output)
+
+ # --------------------------
+
+ # load weights
+
+ home = folder_utils.get_deepface_home()
+
+ if os.path.isfile(home + "/.deepface/weights/gender_model_weights.h5") != True:
+ logger.info("gender_model_weights.h5 will be downloaded...")
+
+ output = home + "/.deepface/weights/gender_model_weights.h5"
+ gdown.download(url, output, quiet=False)
+
+ gender_model.load_weights(home + "/.deepface/weights/gender_model_weights.h5")
+
+ return gender_model
diff --git a/deepface/extendedmodels/Race.py b/deepface/extendedmodels/Race.py
new file mode 100644
index 0000000000000000000000000000000000000000..1943dea9ebb4fae37fc86de4a541917c0b6131be
--- /dev/null
+++ b/deepface/extendedmodels/Race.py
@@ -0,0 +1,81 @@
+# built-in dependencies
+import os
+
+# 3rd party dependencies
+import gdown
+import numpy as np
+
+# project dependencies
+from deepface.basemodels import VGGFace
+from deepface.commons import package_utils, folder_utils
+from deepface.models.Demography import Demography
+from deepface.commons import logger as log
+
+logger = log.get_singletonish_logger()
+
+# --------------------------
+# pylint: disable=line-too-long
+# --------------------------
+# dependency configurations
+tf_version = package_utils.get_tf_major_version()
+
+if tf_version == 1:
+ from keras.models import Model, Sequential
+ from keras.layers import Convolution2D, Flatten, Activation
+else:
+ from tensorflow.keras.models import Model, Sequential
+ from tensorflow.keras.layers import Convolution2D, Flatten, Activation
+# --------------------------
+# Labels for the ethnic phenotypes that can be detected by the model.
+labels = ["asian", "indian", "black", "white", "middle eastern", "latino hispanic"]
+
+# pylint: disable=too-few-public-methods
+class RaceClient(Demography):
+ """
+ Race model class
+ """
+
+ def __init__(self):
+ self.model = load_model()
+ self.model_name = "Race"
+
+ def predict(self, img: np.ndarray) -> np.ndarray:
+ return self.model.predict(img, verbose=0)[0, :]
+
+
+def load_model(
+ url="https://github.com/serengil/deepface_models/releases/download/v1.0/race_model_single_batch.h5",
+) -> Model:
+ """
+ Construct race model, download its weights and load
+ """
+
+ model = VGGFace.base_model()
+
+ # --------------------------
+
+ classes = 6
+ base_model_output = Sequential()
+ base_model_output = Convolution2D(classes, (1, 1), name="predictions")(model.layers[-4].output)
+ base_model_output = Flatten()(base_model_output)
+ base_model_output = Activation("softmax")(base_model_output)
+
+ # --------------------------
+
+ race_model = Model(inputs=model.input, outputs=base_model_output)
+
+ # --------------------------
+
+ # load weights
+
+ home = folder_utils.get_deepface_home()
+
+ if os.path.isfile(home + "/.deepface/weights/race_model_single_batch.h5") != True:
+ logger.info("race_model_single_batch.h5 will be downloaded...")
+
+ output = home + "/.deepface/weights/race_model_single_batch.h5"
+ gdown.download(url, output, quiet=False)
+
+ race_model.load_weights(home + "/.deepface/weights/race_model_single_batch.h5")
+
+ return race_model
diff --git a/deepface/extendedmodels/__init__.py b/deepface/extendedmodels/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/deepface/models/Demography.py b/deepface/models/Demography.py
new file mode 100644
index 0000000000000000000000000000000000000000..ad9392029307f7539095e2d0372df1cec81ec0c0
--- /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 0000000000000000000000000000000000000000..0a1e48d29b791cf5f0159f451a74a85bdc603432
--- /dev/null
+++ b/deepface/models/Detector.py
@@ -0,0 +1,89 @@
+from typing import List, Tuple, Optional
+from abc import ABC, abstractmethod
+import numpy as np
+
+# Notice that all facial detector models must be inherited from this class
+
+
+# pylint: disable=unnecessary-pass, too-few-public-methods
+class Detector(ABC):
+ @abstractmethod
+ def detect_faces(self, img: np.ndarray) -> List["FacialAreaRegion"]:
+ """
+ Interface for detect and align face
+
+ Args:
+ img (np.ndarray): pre-loaded image as numpy array
+
+ Returns:
+ results (List[FacialAreaRegion]): A list of FacialAreaRegion objects
+ where each object contains:
+
+ - facial_area (FacialAreaRegion): The facial area region represented
+ as x, y, w, h, left_eye and right_eye. left eye and right eye are
+ eyes on the left and right respectively with respect to the person
+ instead of observer.
+ """
+ pass
+
+
+class FacialAreaRegion:
+ x: int
+ y: int
+ w: int
+ h: int
+ left_eye: Tuple[int, int]
+ right_eye: Tuple[int, int]
+ confidence: float
+
+ def __init__(
+ self,
+ x: int,
+ y: int,
+ w: int,
+ h: int,
+ left_eye: Optional[Tuple[int, int]] = None,
+ right_eye: Optional[Tuple[int, int]] = None,
+ confidence: Optional[float] = None,
+ ):
+ """
+ Initialize a Face object.
+
+ Args:
+ x (int): The x-coordinate of the top-left corner of the bounding box.
+ y (int): The y-coordinate of the top-left corner of the bounding box.
+ w (int): The width of the bounding box.
+ h (int): The height of the bounding box.
+ left_eye (tuple): The coordinates (x, y) of the left eye with respect to
+ the person instead of observer. Default is None.
+ right_eye (tuple): The coordinates (x, y) of the right eye with respect to
+ the person instead of observer. Default is None.
+ confidence (float, optional): Confidence score associated with the face detection.
+ Default is None.
+ """
+ self.x = x
+ self.y = y
+ self.w = w
+ self.h = h
+ self.left_eye = left_eye
+ self.right_eye = right_eye
+ self.confidence = confidence
+
+
+class DetectedFace:
+ img: np.ndarray
+ facial_area: FacialAreaRegion
+ confidence: float
+
+ def __init__(self, img: np.ndarray, facial_area: FacialAreaRegion, confidence: float):
+ """
+ Initialize detected face object.
+
+ Args:
+ img (np.ndarray): detected face image as numpy array
+ facial_area (FacialAreaRegion): detected face's metadata (e.g. bounding box)
+ confidence (float): confidence score for face detection
+ """
+ self.img = img
+ self.facial_area = facial_area
+ self.confidence = confidence
diff --git a/deepface/models/FacialRecognition.py b/deepface/models/FacialRecognition.py
new file mode 100644
index 0000000000000000000000000000000000000000..a6ee7b59d3693cb63775d180a3a0089ca7b3ed53
--- /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 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/deepface/modules/__init__.py b/deepface/modules/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/deepface/modules/cloudservice.py b/deepface/modules/cloudservice.py
new file mode 100644
index 0000000000000000000000000000000000000000..f7963abc734cc897a0847aac2657e54659d33528
--- /dev/null
+++ b/deepface/modules/cloudservice.py
@@ -0,0 +1,68 @@
+from flask import Flask, jsonify
+import cloudinary
+import cloudinary.api
+import cloudinary.uploader
+import os
+import glob
+import requests
+from dotenv import load_dotenv
+from deepface.commons.logger import Logger
+
+logger = Logger(module="modules/cloudservice.py")
+
+load_dotenv()
+
+# Configure Cloudinary
+cloudinary.config(
+ cloud_name=os.getenv('CLOUDINARY_CLOUD_NAME'),
+ api_key=os.getenv('CLOUDINARY_API_KEY'),
+ api_secret=os.getenv('CLOUDINARY_API_SECRET')
+)
+
+
+def fetch_cloudinary_images(folder_name):
+
+ resources = []
+
+ res = cloudinary.api.resources(type='upload', resource_type='image', prefix=f'mafqoud/images/{folder_name}')
+ resources.extend(res.get('resources', []))
+ return resources
+
+def download_image(url, local_path):
+ response = requests.get(url, stream=True)
+ if response.status_code == 200:
+ with open(local_path, 'wb') as file:
+ for chunk in response:
+ file.write(chunk)
+
+def sync_folder(folder_name, local_dir):
+ cloudinary_images = fetch_cloudinary_images(folder_name)
+ cloudinary_urls = {img['secure_url']: img['public_id'] for img in cloudinary_images}
+
+ # Download new images and track downloaded image paths
+ downloaded_paths = []
+ for img in cloudinary_images:
+ url = img['secure_url']
+ public_id = img['public_id']
+ file_name = url.split('/')[-1] # Get the actual file name
+ local_path = os.path.join(local_dir, file_name)
+
+
+ if not os.path.exists(local_path):
+ download_image(url, local_path)
+ downloaded_paths.append(local_path)
+
+ # Remove old images
+ local_images = [os.path.join(local_dir, f) for f in os.listdir(local_dir) if os.path.isfile(os.path.join(local_dir, f))]
+ for local_path in local_images:
+ if local_path not in downloaded_paths:
+ os.remove(local_path)
+
+def delete_pkl_files(directory):
+ """Delete all .pkl files in the specified directory."""
+ pkl_files = glob.glob(os.path.join(directory, '*.pkl'))
+ for pkl_file in pkl_files:
+ os.remove(pkl_file)
+
+
+
diff --git a/deepface/modules/demography.py b/deepface/modules/demography.py
new file mode 100644
index 0000000000000000000000000000000000000000..f11f71d1ee63e4df954d85b55ee0278b03d76da0
--- /dev/null
+++ b/deepface/modules/demography.py
@@ -0,0 +1,197 @@
+# built-in dependencies
+from typing import Any, Dict, List, Union
+
+# 3rd party dependencies
+import numpy as np
+from tqdm import tqdm
+
+# project dependencies
+from deepface.modules import modeling, detection, preprocessing
+from deepface.extendedmodels import Gender, Race, Emotion
+
+
+def analyze(
+ img_path: Union[str, np.ndarray],
+ actions: Union[tuple, list] = ("emotion", "age", "gender", "race"),
+ enforce_detection: bool = True,
+ detector_backend: str = "opencv",
+ align: bool = True,
+ expand_percentage: int = 0,
+ silent: bool = False,
+) -> List[Dict[str, Any]]:
+ """
+ Analyze facial attributes such as age, gender, emotion, and race in the provided image.
+
+ Args:
+ img_path (str or np.ndarray): The exact path to the image, a numpy array in BGR format,
+ or a base64 encoded image. If the source image contains multiple faces, the result will
+ include information for each detected face.
+
+ actions (tuple): Attributes to analyze. The default is ('age', 'gender', 'emotion', 'race').
+ You can exclude some of these attributes from the analysis if needed.
+
+ enforce_detection (boolean): If no face is detected in an image, raise an exception.
+ Set to False to avoid the exception for low-resolution images (default is True).
+
+ detector_backend (string): face detector backend. Options: 'opencv', 'retinaface',
+ 'mtcnn', 'ssd', 'dlib', 'mediapipe', 'yolov8', 'centerface' or 'skip'
+ (default is opencv).
+
+ distance_metric (string): Metric for measuring similarity. Options: 'cosine',
+ 'euclidean', 'euclidean_l2' (default is cosine).
+
+ align (boolean): Perform alignment based on the eye positions (default is True).
+
+ expand_percentage (int): expand detected facial area with a percentage (default is 0).
+
+ silent (boolean): Suppress or allow some log messages for a quieter analysis process
+ (default is False).
+
+ Returns:
+ results (List[Dict[str, Any]]): A list of dictionaries, where each dictionary represents
+ the analysis results for a detected face.
+
+ Each dictionary in the list contains the following keys:
+
+ - 'region' (dict): Represents the rectangular region of the detected face in the image.
+ - 'x': x-coordinate of the top-left corner of the face.
+ - 'y': y-coordinate of the top-left corner of the face.
+ - 'w': Width of the detected face region.
+ - 'h': Height of the detected face region.
+
+ - 'age' (float): Estimated age of the detected face.
+
+ - 'face_confidence' (float): Confidence score for the detected face.
+ Indicates the reliability of the face detection.
+
+ - 'dominant_gender' (str): The dominant gender in the detected face.
+ Either "Man" or "Woman."
+
+ - 'gender' (dict): Confidence scores for each gender category.
+ - 'Man': Confidence score for the male gender.
+ - 'Woman': Confidence score for the female gender.
+
+ - 'dominant_emotion' (str): The dominant emotion in the detected face.
+ Possible values include "sad," "angry," "surprise," "fear," "happy,"
+ "disgust," and "neutral."
+
+ - 'emotion' (dict): Confidence scores for each emotion category.
+ - 'sad': Confidence score for sadness.
+ - 'angry': Confidence score for anger.
+ - 'surprise': Confidence score for surprise.
+ - 'fear': Confidence score for fear.
+ - 'happy': Confidence score for happiness.
+ - 'disgust': Confidence score for disgust.
+ - 'neutral': Confidence score for neutrality.
+
+ - 'dominant_race' (str): The dominant race in the detected face.
+ Possible values include "indian," "asian," "latino hispanic,"
+ "black," "middle eastern," and "white."
+
+ - 'race' (dict): Confidence scores for each race category.
+ - 'indian': Confidence score for Indian ethnicity.
+ - 'asian': Confidence score for Asian ethnicity.
+ - 'latino hispanic': Confidence score for Latino/Hispanic ethnicity.
+ - 'black': Confidence score for Black ethnicity.
+ - 'middle eastern': Confidence score for Middle Eastern ethnicity.
+ - 'white': Confidence score for White ethnicity.
+ """
+
+ # if actions is passed as tuple with single item, interestingly it becomes str here
+ if isinstance(actions, str):
+ actions = (actions,)
+
+ # check if actions is not an iterable or empty.
+ if not hasattr(actions, "__getitem__") or not actions:
+ raise ValueError("`actions` must be a list of strings.")
+
+ actions = list(actions)
+
+ # For each action, check if it is valid
+ for action in actions:
+ if action not in ("emotion", "age", "gender", "race"):
+ raise ValueError(
+ f"Invalid action passed ({repr(action)})). "
+ "Valid actions are `emotion`, `age`, `gender`, `race`."
+ )
+ # ---------------------------------
+ resp_objects = []
+
+ img_objs = detection.extract_faces(
+ img_path=img_path,
+ detector_backend=detector_backend,
+ grayscale=False,
+ enforce_detection=enforce_detection,
+ align=align,
+ expand_percentage=expand_percentage,
+ )
+
+ for img_obj in img_objs:
+ img_content = img_obj["face"]
+ img_region = img_obj["facial_area"]
+ img_confidence = img_obj["confidence"]
+ if img_content.shape[0] == 0 or img_content.shape[1] == 0:
+ continue
+
+ # rgb to bgr
+ img_content = img_content[:, :, ::-1]
+
+ # resize input image
+ img_content = preprocessing.resize_image(img=img_content, target_size=(224, 224))
+
+ obj = {}
+ # facial attribute analysis
+ pbar = tqdm(
+ range(0, len(actions)),
+ desc="Finding actions",
+ disable=silent if len(actions) > 1 else True,
+ )
+ for index in pbar:
+ action = actions[index]
+ pbar.set_description(f"Action: {action}")
+
+ if action == "emotion":
+ emotion_predictions = modeling.build_model("Emotion").predict(img_content)
+ sum_of_predictions = emotion_predictions.sum()
+
+ obj["emotion"] = {}
+ for i, emotion_label in enumerate(Emotion.labels):
+ emotion_prediction = 100 * emotion_predictions[i] / sum_of_predictions
+ obj["emotion"][emotion_label] = emotion_prediction
+
+ obj["dominant_emotion"] = Emotion.labels[np.argmax(emotion_predictions)]
+
+ elif action == "age":
+ apparent_age = modeling.build_model("Age").predict(img_content)
+ # int cast is for exception - object of type 'float32' is not JSON serializable
+ obj["age"] = int(apparent_age)
+
+ elif action == "gender":
+ gender_predictions = modeling.build_model("Gender").predict(img_content)
+ obj["gender"] = {}
+ for i, gender_label in enumerate(Gender.labels):
+ gender_prediction = 100 * gender_predictions[i]
+ obj["gender"][gender_label] = gender_prediction
+
+ obj["dominant_gender"] = Gender.labels[np.argmax(gender_predictions)]
+
+ elif action == "race":
+ race_predictions = modeling.build_model("Race").predict(img_content)
+ sum_of_predictions = race_predictions.sum()
+
+ obj["race"] = {}
+ for i, race_label in enumerate(Race.labels):
+ race_prediction = 100 * race_predictions[i] / sum_of_predictions
+ obj["race"][race_label] = race_prediction
+
+ obj["dominant_race"] = Race.labels[np.argmax(race_predictions)]
+
+ # -----------------------------
+ # mention facial areas
+ obj["region"] = img_region
+ # include image confidence
+ obj["face_confidence"] = img_confidence
+
+ resp_objects.append(obj)
+
+ return resp_objects
diff --git a/deepface/modules/detection.py b/deepface/modules/detection.py
new file mode 100644
index 0000000000000000000000000000000000000000..ad4b28874796bd0015088abdf271ab33b4b1b404
--- /dev/null
+++ b/deepface/modules/detection.py
@@ -0,0 +1,160 @@
+# built-in dependencies
+from typing import Any, Dict, List, Tuple, Union
+
+# 3rd part dependencies
+import numpy as np
+import cv2
+from PIL import Image
+
+# project dependencies
+from deepface.models.Detector import DetectedFace, FacialAreaRegion
+from deepface.detectors import DetectorWrapper
+from deepface.commons import image_utils
+from deepface.commons import logger as log
+
+logger = log.get_singletonish_logger()
+
+# pylint: disable=no-else-raise
+
+
+def extract_faces(
+ img_path: Union[str, np.ndarray],
+ detector_backend: str = "opencv",
+ enforce_detection: bool = True,
+ align: bool = True,
+ expand_percentage: int = 0,
+ grayscale: bool = False,
+) -> List[Dict[str, Any]]:
+ """
+ Extract faces from a given image
+
+ Args:
+ img_path (str or np.ndarray): Path to the first image. Accepts exact image path
+ as a string, numpy array (BGR), or base64 encoded images.
+
+ detector_backend (string): face detector backend. Options: 'opencv', 'retinaface',
+ 'mtcnn', 'ssd', 'dlib', 'mediapipe', 'yolov8', 'centerface' or 'skip'
+ (default is opencv)
+
+ enforce_detection (boolean): If no face is detected in an image, raise an exception.
+ Default is True. Set to False to avoid the exception for low-resolution images.
+
+ align (bool): Flag to enable face alignment (default is True).
+
+ expand_percentage (int): expand detected facial area with a percentage
+
+ grayscale (boolean): Flag to convert the image to grayscale before
+ processing (default is False).
+
+ Returns:
+ results (List[Dict[str, Any]]): A list of dictionaries, where each dictionary contains:
+
+ - "face" (np.ndarray): The detected face as a NumPy array in RGB format.
+
+ - "facial_area" (Dict[str, Any]): The detected face's regions as a dictionary containing:
+ - keys 'x', 'y', 'w', 'h' with int values
+ - keys 'left_eye', 'right_eye' with a tuple of 2 ints as values.
+ left eye and right eye are eyes on the left and right respectively with respect
+ to the person itself instead of observer.
+
+ - "confidence" (float): The confidence score associated with the detected face.
+ """
+
+ resp_objs = []
+
+ # img might be path, base64 or numpy array. Convert it to numpy whatever it is.
+ img, img_name = image_utils.load_image(img_path)
+
+ if img is None:
+ raise ValueError(f"Exception while loading {img_name}")
+
+ base_region = FacialAreaRegion(x=0, y=0, w=img.shape[1], h=img.shape[0], confidence=0)
+
+ if detector_backend == "skip":
+ face_objs = [DetectedFace(img=img, facial_area=base_region, confidence=0)]
+ else:
+ face_objs = DetectorWrapper.detect_faces(
+ detector_backend=detector_backend,
+ img=img,
+ align=align,
+ expand_percentage=expand_percentage,
+ )
+
+ # in case of no face found
+ if len(face_objs) == 0 and enforce_detection is True:
+ if img_name is not None:
+ raise ValueError(
+ f"Face could not be detected in {img_name}."
+ "Please confirm that the picture is a face photo "
+ "or consider to set enforce_detection param to False."
+ )
+ else:
+ raise ValueError(
+ "Face could not be detected. Please confirm that the picture is a face photo "
+ "or consider to set enforce_detection param to False."
+ )
+
+ if len(face_objs) == 0 and enforce_detection is False:
+ face_objs = [DetectedFace(img=img, facial_area=base_region, confidence=0)]
+
+ for face_obj in face_objs:
+ current_img = face_obj.img
+ current_region = face_obj.facial_area
+
+ if current_img.shape[0] == 0 or current_img.shape[1] == 0:
+ continue
+
+ if grayscale is True:
+ current_img = cv2.cvtColor(current_img, cv2.COLOR_BGR2GRAY)
+
+ current_img = current_img / 255 # normalize input in [0, 1]
+
+ resp_objs.append(
+ {
+ "face": current_img[:, :, ::-1],
+ "facial_area": {
+ "x": int(current_region.x),
+ "y": int(current_region.y),
+ "w": int(current_region.w),
+ "h": int(current_region.h),
+ "left_eye": current_region.left_eye,
+ "right_eye": current_region.right_eye,
+ },
+ "confidence": round(current_region.confidence, 2),
+ }
+ )
+
+ if len(resp_objs) == 0 and enforce_detection == True:
+ raise ValueError(
+ f"Exception while extracting faces from {img_name}."
+ "Consider to set enforce_detection arg to False."
+ )
+
+ return resp_objs
+
+
+def align_face(
+ img: np.ndarray,
+ left_eye: Union[list, tuple],
+ right_eye: Union[list, tuple],
+) -> Tuple[np.ndarray, float]:
+ """
+ Align a given image horizantally with respect to their left and right eye locations
+ Args:
+ img (np.ndarray): pre-loaded image with detected face
+ left_eye (list or tuple): coordinates of left eye with respect to the person itself
+ right_eye(list or tuple): coordinates of right eye with respect to the person itself
+ Returns:
+ img (np.ndarray): aligned facial image
+ """
+ # if eye could not be detected for the given image, return image itself
+ if left_eye is None or right_eye is None:
+ return img, 0
+
+ # sometimes unexpectedly detected images come with nil dimensions
+ if img.shape[0] == 0 or img.shape[1] == 0:
+ return img, 0
+
+ angle = float(np.degrees(np.arctan2(left_eye[1] - right_eye[1], left_eye[0] - right_eye[0])))
+ img = np.array(Image.fromarray(img).rotate(angle))
+ return img, angle
diff --git a/deepface/modules/modeling.py b/deepface/modules/modeling.py
new file mode 100644
index 0000000000000000000000000000000000000000..b40dcb5c249f1c1c15526fc98b1c75861fe3d347
--- /dev/null
+++ b/deepface/modules/modeling.py
@@ -0,0 +1,61 @@
+# built-in dependencies
+from typing import Any
+
+# project dependencies
+from deepface.basemodels import (
+ VGGFace,
+ OpenFace,
+ FbDeepFace,
+ DeepID,
+ ArcFace,
+ SFace,
+ Dlib,
+ Facenet,
+ GhostFaceNet
+)
+from deepface.extendedmodels import Age, Gender, Race, Emotion
+
+
+def build_model(model_name: str) -> Any:
+ """
+ This function builds a deepface model
+ Parameters:
+ model_name (string): face recognition or facial attribute model
+ VGG-Face, Facenet, OpenFace, DeepFace, DeepID for face recognition
+ Age, Gender, Emotion, Race for facial attributes
+
+ Returns:
+ built model class
+ """
+
+ # singleton design pattern
+ global model_obj
+
+ models = {
+ "VGG-Face": VGGFace.VggFaceClient,
+ "OpenFace": OpenFace.OpenFaceClient,
+ "Facenet": Facenet.FaceNet128dClient,
+ "Facenet512": Facenet.FaceNet512dClient,
+ "DeepFace": FbDeepFace.DeepFaceClient,
+ "DeepID": DeepID.DeepIdClient,
+ "Dlib": Dlib.DlibClient,
+ "ArcFace": ArcFace.ArcFaceClient,
+ "SFace": SFace.SFaceClient,
+ "GhostFaceNet": GhostFaceNet.GhostFaceNetClient,
+ "Emotion": Emotion.EmotionClient,
+ "Age": Age.ApparentAgeClient,
+ "Gender": Gender.GenderClient,
+ "Race": Race.RaceClient,
+ }
+
+ if not "model_obj" in globals():
+ model_obj = {}
+
+ if not model_name in model_obj.keys():
+ model = models.get(model_name)
+ if model:
+ model_obj[model_name] = model()
+ else:
+ raise ValueError(f"Invalid model_name passed - {model_name}")
+
+ return model_obj[model_name]
diff --git a/deepface/modules/preprocessing.py b/deepface/modules/preprocessing.py
new file mode 100644
index 0000000000000000000000000000000000000000..459adbab3e0dff3459e4ae21f413ece226bbf3b8
--- /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 0000000000000000000000000000000000000000..011863445b882455e04da9d25d9a93bd7b60bb8a
--- /dev/null
+++ b/deepface/modules/recognition.py
@@ -0,0 +1,391 @@
+# built-in dependencies
+import os
+import pickle
+from typing import List, Union, Optional, Dict, Any
+import time
+
+# 3rd party dependencies
+import numpy as np
+import pandas as pd
+from tqdm import tqdm
+
+# project dependencies
+from deepface.commons import image_utils
+from deepface.modules import representation, detection, verification
+from deepface.commons import logger as log
+
+logger = log.get_singletonish_logger()
+
+
+def find(
+ img_path: Union[str, np.ndarray],
+ db_path: str,
+ model_name: str = "VGG-Face",
+ distance_metric: str = "cosine",
+ enforce_detection: bool = True,
+ detector_backend: str = "opencv",
+ align: bool = True,
+ expand_percentage: int = 0,
+ threshold: Optional[float] = None,
+ normalization: str = "base",
+ silent: bool = False,
+) -> List[pd.DataFrame]:
+ """
+ Identify individuals in a database
+
+ Args:
+ img_path (str or np.ndarray): The exact path to the image, a numpy array in BGR format,
+ or a base64 encoded image. If the source image contains multiple faces, the result will
+ include information for each detected face.
+
+ db_path (string): Path to the folder containing image files. All detected faces
+ in the database will be considered in the decision-making process.
+
+ model_name (str): Model for face recognition. Options: VGG-Face, Facenet, Facenet512,
+ OpenFace, DeepFace, DeepID, Dlib, ArcFace, SFace and GhostFaceNet (default is VGG-Face).
+
+ distance_metric (string): Metric for measuring similarity. Options: 'cosine',
+ 'euclidean', 'euclidean_l2'.
+
+ enforce_detection (boolean): If no face is detected in an image, raise an exception.
+ Default is True. Set to False to avoid the exception for low-resolution images.
+
+ detector_backend (string): face detector backend. Options: 'opencv', 'retinaface',
+ 'mtcnn', 'ssd', 'dlib', 'mediapipe', 'yolov8', 'centerface' or 'skip'.
+
+ align (boolean): Perform alignment based on the eye positions.
+
+ expand_percentage (int): expand detected facial area with a percentage (default is 0).
+
+ threshold (float): Specify a threshold to determine whether a pair represents the same
+ person or different individuals. This threshold is used for comparing distances.
+ If left unset, default pre-tuned threshold values will be applied based on the specified
+ model name and distance metric (default is None).
+
+ normalization (string): Normalize the input image before feeding it to the model.
+ Default is base. Options: base, raw, Facenet, Facenet2018, VGGFace, VGGFace2, ArcFace
+
+ silent (boolean): Suppress or allow some log messages for a quieter analysis process.
+
+ Returns:
+ results (List[pd.DataFrame]): A list of pandas dataframes. Each dataframe corresponds
+ to the identity information for an individual detected in the source image.
+ The DataFrame columns include:
+
+ - 'identity': Identity label of the detected individual.
+
+ - 'target_x', 'target_y', 'target_w', 'target_h': Bounding box coordinates of the
+ target face in the database.
+
+ - 'source_x', 'source_y', 'source_w', 'source_h': Bounding box coordinates of the
+ detected face in the source image.
+
+ - 'threshold': threshold to determine a pair whether same person or different persons
+
+ - 'distance': Similarity score between the faces based on the
+ specified model and distance metric
+ """
+
+ tic = time.time()
+
+ if os.path.isdir(db_path) is not True:
+ raise ValueError("Passed db_path does not exist!")
+
+ file_parts = [
+ "ds",
+ "model",
+ model_name,
+ "detector",
+ detector_backend,
+ "aligned" if align else "unaligned",
+ "normalization",
+ normalization,
+ "expand",
+ str(expand_percentage),
+ ]
+
+ file_name = "_".join(file_parts) + ".pkl"
+ file_name = file_name.replace("-", "").lower()
+
+ datastore_path = os.path.join(db_path, file_name)
+ representations = []
+
+ # required columns for representations
+ df_cols = [
+ "identity",
+ "hash",
+ "embedding",
+ "target_x",
+ "target_y",
+ "target_w",
+ "target_h",
+ ]
+
+ # Ensure the proper pickle file exists
+ if not os.path.exists(datastore_path):
+ with open(datastore_path, "wb") as f:
+ pickle.dump([], f)
+
+ # Load the representations from the pickle file
+ with open(datastore_path, "rb") as f:
+ representations = pickle.load(f)
+
+ # check each item of representations list has required keys
+ for i, current_representation in enumerate(representations):
+ missing_keys = list(set(df_cols) - set(current_representation.keys()))
+ if len(missing_keys) > 0:
+ raise ValueError(
+ f"{i}-th item does not have some required keys - {missing_keys}."
+ f"Consider to delete {datastore_path}"
+ )
+
+ # embedded images
+ pickled_images = [representation["identity"] for representation in representations]
+
+ # Get the list of images on storage
+ storage_images = image_utils.list_images(path=db_path)
+
+ if len(storage_images) == 0:
+ raise ValueError(f"No item found in {db_path}")
+
+ # Enforce data consistency amongst on disk images and pickle file
+ must_save_pickle = False
+ new_images = list(set(storage_images) - set(pickled_images)) # images added to storage
+ old_images = list(set(pickled_images) - set(storage_images)) # images removed from storage
+
+ # detect replaced images
+ replaced_images = []
+ for current_representation in representations:
+ identity = current_representation["identity"]
+ if identity in old_images:
+ continue
+ alpha_hash = current_representation["hash"]
+ beta_hash = image_utils.find_image_hash(identity)
+ if alpha_hash != beta_hash:
+ logger.debug(f"Even though {identity} represented before, it's replaced later.")
+ replaced_images.append(identity)
+
+ if not silent and (len(new_images) > 0 or len(old_images) > 0 or len(replaced_images) > 0):
+ logger.info(
+ f"Found {len(new_images)} newly added image(s)"
+ f", {len(old_images)} removed image(s)"
+ f", {len(replaced_images)} replaced image(s)."
+ )
+
+ # append replaced images into both old and new images. these will be dropped and re-added.
+ new_images = new_images + replaced_images
+ old_images = old_images + replaced_images
+
+ # remove old images first
+ if len(old_images) > 0:
+ representations = [rep for rep in representations if rep["identity"] not in old_images]
+ must_save_pickle = True
+
+ # find representations for new images
+ if len(new_images) > 0:
+ representations += __find_bulk_embeddings(
+ employees=new_images,
+ model_name=model_name,
+ detector_backend=detector_backend,
+ enforce_detection=enforce_detection,
+ align=align,
+ expand_percentage=expand_percentage,
+ normalization=normalization,
+ silent=silent,
+ ) # add new images
+ must_save_pickle = True
+
+ if must_save_pickle:
+ with open(datastore_path, "wb") as f:
+ pickle.dump(representations, f)
+ if not silent:
+ logger.info(f"There are now {len(representations)} representations in {file_name}")
+
+ # Should we have no representations bailout
+ if len(representations) == 0:
+ if not silent:
+ toc = time.time()
+ logger.info(f"find function duration {toc - tic} seconds")
+ return []
+
+ # ----------------------------
+ # now, we got representations for facial database
+ df = pd.DataFrame(representations)
+
+ if silent is False:
+ logger.info(f"Searching {img_path} in {df.shape[0]} length datastore")
+
+ # img path might have more than once face
+ source_objs = detection.extract_faces(
+ img_path=img_path,
+ detector_backend=detector_backend,
+ grayscale=False,
+ enforce_detection=enforce_detection,
+ align=align,
+ expand_percentage=expand_percentage,
+ )
+
+ resp_obj = []
+
+ for source_obj in source_objs:
+ source_img = source_obj["face"]
+ source_region = source_obj["facial_area"]
+ target_embedding_obj = representation.represent(
+ img_path=source_img,
+ model_name=model_name,
+ enforce_detection=enforce_detection,
+ detector_backend="skip",
+ align=align,
+ normalization=normalization,
+ )
+
+ target_representation = target_embedding_obj[0]["embedding"]
+
+ result_df = df.copy() # df will be filtered in each img
+ result_df["source_x"] = source_region["x"]
+ result_df["source_y"] = source_region["y"]
+ result_df["source_w"] = source_region["w"]
+ result_df["source_h"] = source_region["h"]
+
+ distances = []
+ for _, instance in df.iterrows():
+ source_representation = instance["embedding"]
+ if source_representation is None:
+ distances.append(float("inf")) # no representation for this image
+ continue
+
+ target_dims = len(list(target_representation))
+ source_dims = len(list(source_representation))
+ if target_dims != source_dims:
+ raise ValueError(
+ "Source and target embeddings must have same dimensions but "
+ + f"{target_dims}:{source_dims}. Model structure may change"
+ + " after pickle created. Delete the {file_name} and re-run."
+ )
+
+ distance = verification.find_distance(
+ source_representation, target_representation, distance_metric
+ )
+
+ distances.append(distance)
+
+ # ---------------------------
+ target_threshold = threshold or verification.find_threshold(model_name, distance_metric)
+
+ result_df["threshold"] = target_threshold
+ result_df["distance"] = distances
+
+ result_df = result_df.drop(columns=["embedding"])
+ # pylint: disable=unsubscriptable-object
+ result_df = result_df[result_df["distance"] <= target_threshold]
+ result_df = result_df.sort_values(by=["distance"], ascending=True).reset_index(drop=True)
+
+ resp_obj.append(result_df)
+
+ # -----------------------------------
+
+ if not silent:
+ toc = time.time()
+ logger.info(f"find function duration {toc - tic} seconds")
+
+ return resp_obj
+
+
+def __find_bulk_embeddings(
+ employees: List[str],
+ model_name: str = "VGG-Face",
+ detector_backend: str = "opencv",
+ enforce_detection: bool = True,
+ align: bool = True,
+ expand_percentage: int = 0,
+ normalization: str = "base",
+ silent: bool = False,
+) -> List[Dict["str", Any]]:
+ """
+ Find embeddings of a list of images
+
+ Args:
+ employees (list): list of exact image paths
+
+ model_name (str): Model for face recognition. Options: VGG-Face, Facenet, Facenet512,
+ OpenFace, DeepFace, DeepID, Dlib, ArcFace, SFace and GhostFaceNet (default is VGG-Face).
+
+ detector_backend (str): face detector model name
+
+ enforce_detection (bool): set this to False if you
+ want to proceed when you cannot detect any face
+
+ align (bool): enable or disable alignment of image
+ before feeding to facial recognition model
+
+ expand_percentage (int): expand detected facial area with a
+ percentage (default is 0).
+
+ normalization (bool): normalization technique
+
+ silent (bool): enable or disable informative logging
+ Returns:
+ representations (list): pivot list of dict with
+ image name, hash, embedding and detected face area's coordinates
+ """
+ representations = []
+ for employee in tqdm(
+ employees,
+ desc="Finding representations",
+ disable=silent,
+ ):
+ file_hash = image_utils.find_image_hash(employee)
+
+ try:
+ img_objs = detection.extract_faces(
+ img_path=employee,
+ detector_backend=detector_backend,
+ grayscale=False,
+ enforce_detection=enforce_detection,
+ align=align,
+ expand_percentage=expand_percentage,
+ )
+
+ except ValueError as err:
+ logger.error(f"Exception while extracting faces from {employee}: {str(err)}")
+ img_objs = []
+
+ if len(img_objs) == 0:
+ representations.append(
+ {
+ "identity": employee,
+ "hash": file_hash,
+ "embedding": None,
+ "target_x": 0,
+ "target_y": 0,
+ "target_w": 0,
+ "target_h": 0,
+ }
+ )
+ else:
+ for img_obj in img_objs:
+ img_content = img_obj["face"]
+ img_region = img_obj["facial_area"]
+ embedding_obj = representation.represent(
+ img_path=img_content,
+ model_name=model_name,
+ enforce_detection=enforce_detection,
+ detector_backend="skip",
+ align=align,
+ normalization=normalization,
+ )
+
+ img_representation = embedding_obj[0]["embedding"]
+ representations.append(
+ {
+ "identity": employee,
+ "hash": file_hash,
+ "embedding": img_representation,
+ "target_x": img_region["x"],
+ "target_y": img_region["y"],
+ "target_w": img_region["w"],
+ "target_h": img_region["h"],
+ }
+ )
+
+ return representations
diff --git a/deepface/modules/representation.py b/deepface/modules/representation.py
new file mode 100644
index 0000000000000000000000000000000000000000..9e8a1a6974f31962f3fc68a6f5261822132c09a6
--- /dev/null
+++ b/deepface/modules/representation.py
@@ -0,0 +1,120 @@
+# built-in dependencies
+from typing import Any, Dict, List, Union
+
+# 3rd party dependencies
+import numpy as np
+
+# project dependencies
+from deepface.commons import image_utils
+from deepface.modules import modeling, detection, preprocessing
+from deepface.models.FacialRecognition import FacialRecognition
+
+
+def represent(
+ img_path: Union[str, np.ndarray],
+ model_name: str = "VGG-Face",
+ enforce_detection: bool = True,
+ detector_backend: str = "opencv",
+ align: bool = True,
+ expand_percentage: int = 0,
+ normalization: str = "base",
+) -> List[Dict[str, Any]]:
+ """
+ Represent facial images as multi-dimensional vector embeddings.
+
+ Args:
+ img_path (str or np.ndarray): The exact path to the image, a numpy array in BGR format,
+ or a base64 encoded image. If the source image contains multiple faces, the result will
+ include information for each detected face.
+
+ model_name (str): Model for face recognition. Options: VGG-Face, Facenet, Facenet512,
+ OpenFace, DeepFace, DeepID, Dlib, ArcFace, SFace and GhostFaceNet
+
+ enforce_detection (boolean): If no face is detected in an image, raise an exception.
+ Default is True. Set to False to avoid the exception for low-resolution images.
+
+ detector_backend (string): face detector backend. Options: 'opencv', 'retinaface',
+ 'mtcnn', 'ssd', 'dlib', 'mediapipe', 'yolov8', 'centerface' or 'skip'.
+
+ align (boolean): Perform alignment based on the eye positions.
+
+ expand_percentage (int): expand detected facial area with a percentage (default is 0).
+
+ normalization (string): Normalize the input image before feeding it to the model.
+ Default is base. Options: base, raw, Facenet, Facenet2018, VGGFace, VGGFace2, ArcFace
+
+ Returns:
+ results (List[Dict[str, Any]]): A list of dictionaries, each containing the
+ following fields:
+
+ - embedding (List[float]): Multidimensional vector representing facial features.
+ The number of dimensions varies based on the reference model
+ (e.g., FaceNet returns 128 dimensions, VGG-Face returns 4096 dimensions).
+ - facial_area (dict): Detected facial area by face detection in dictionary format.
+ Contains 'x' and 'y' as the left-corner point, and 'w' and 'h'
+ as the width and height. If `detector_backend` is set to 'skip', it represents
+ the full image area and is nonsensical.
+ - face_confidence (float): Confidence score of face detection. If `detector_backend` is set
+ to 'skip', the confidence will be 0 and is nonsensical.
+ """
+ resp_objs = []
+
+ model: FacialRecognition = modeling.build_model(model_name)
+
+ # ---------------------------------
+ # we have run pre-process in verification. so, this can be skipped if it is coming from verify.
+ target_size = model.input_shape
+ if detector_backend != "skip":
+ img_objs = detection.extract_faces(
+ img_path=img_path,
+ detector_backend=detector_backend,
+ grayscale=False,
+ enforce_detection=enforce_detection,
+ align=align,
+ expand_percentage=expand_percentage,
+ )
+ else: # skip
+ # Try load. If load error, will raise exception internal
+ img, _ = image_utils.load_image(img_path)
+
+ if len(img.shape) != 3:
+ raise ValueError(f"Input img must be 3 dimensional but it is {img.shape}")
+
+ # make dummy region and confidence to keep compatibility with `extract_faces`
+ img_objs = [
+ {
+ "face": img,
+ "facial_area": {"x": 0, "y": 0, "w": img.shape[1], "h": img.shape[2]},
+ "confidence": 0,
+ }
+ ]
+ # ---------------------------------
+
+ for img_obj in img_objs:
+ img = img_obj["face"]
+
+ # rgb to bgr
+ img = img[:, :, ::-1]
+
+ region = img_obj["facial_area"]
+ confidence = img_obj["confidence"]
+
+ # resize to expected shape of ml model
+ img = preprocessing.resize_image(
+ img=img,
+ # thanks to DeepId (!)
+ target_size=(target_size[1], target_size[0]),
+ )
+
+ # custom normalization
+ img = preprocessing.normalize_input(img=img, normalization=normalization)
+
+ embedding = model.forward(img)
+
+ resp_obj = {}
+ resp_obj["embedding"] = embedding
+ resp_obj["facial_area"] = region
+ resp_obj["face_confidence"] = confidence
+ resp_objs.append(resp_obj)
+
+ return resp_objs
diff --git a/deepface/modules/streaming.py b/deepface/modules/streaming.py
new file mode 100644
index 0000000000000000000000000000000000000000..95d05d91c68a64a4c9fd2adc47484f5da0faf130
--- /dev/null
+++ b/deepface/modules/streaming.py
@@ -0,0 +1,980 @@
+# built-in dependencies
+import os
+import time
+from typing import List, Tuple, Optional
+
+# 3rd party dependencies
+import numpy as np
+import pandas as pd
+import cv2
+
+# project dependencies
+from deepface import DeepFace
+from deepface.commons import logger as log
+
+logger = log.get_singletonish_logger()
+
+# dependency configuration
+os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2"
+
+
+IDENTIFIED_IMG_SIZE = 112
+TEXT_COLOR = (255, 255, 255)
+
+
+def analysis(
+ db_path: str,
+ model_name="VGG-Face",
+ detector_backend="opencv",
+ distance_metric="cosine",
+ enable_face_analysis=True,
+ source=0,
+ time_threshold=5,
+ frame_threshold=5,
+):
+ """
+ Run real time face recognition and facial attribute analysis
+
+ Args:
+ db_path (string): Path to the folder containing image files. All detected faces
+ in the database will be considered in the decision-making process.
+
+ model_name (str): Model for face recognition. Options: VGG-Face, Facenet, Facenet512,
+ OpenFace, DeepFace, DeepID, Dlib, ArcFace, SFace and GhostFaceNet (default is VGG-Face).
+
+ detector_backend (string): face detector backend. Options: 'opencv', 'retinaface',
+ 'mtcnn', 'ssd', 'dlib', 'mediapipe', 'yolov8', 'centerface' or 'skip'
+ (default is opencv).
+
+ distance_metric (string): Metric for measuring similarity. Options: 'cosine',
+ 'euclidean', 'euclidean_l2' (default is cosine).
+
+ enable_face_analysis (bool): Flag to enable face analysis (default is True).
+
+ source (Any): The source for the video stream (default is 0, which represents the
+ default camera).
+
+ time_threshold (int): The time threshold (in seconds) for face recognition (default is 5).
+
+ frame_threshold (int): The frame threshold for face recognition (default is 5).
+ Returns:
+ None
+ """
+ # initialize models
+ build_demography_models(enable_face_analysis=enable_face_analysis)
+ build_facial_recognition_model(model_name=model_name)
+ # call a dummy find function for db_path once to create embeddings before starting webcam
+ _ = search_identity(
+ detected_face=np.zeros([224, 224, 3]),
+ db_path=db_path,
+ detector_backend=detector_backend,
+ distance_metric=distance_metric,
+ model_name=model_name,
+ )
+
+ freezed_img = None
+ freeze = False
+ num_frames_with_faces = 0
+ tic = time.time()
+
+ cap = cv2.VideoCapture(source) # webcam
+ while True:
+ has_frame, img = cap.read()
+ if not has_frame:
+ break
+
+ # we are adding some figures into img such as identified facial image, age, gender
+ # that is why, we need raw image itself to make analysis
+ raw_img = img.copy()
+
+ faces_coordinates = []
+ if freeze is False:
+ faces_coordinates = grab_facial_areas(img=img, detector_backend=detector_backend)
+
+ # we will pass img to analyze modules (identity, demography) and add some illustrations
+ # that is why, we will not be able to extract detected face from img clearly
+ detected_faces = extract_facial_areas(img=img, faces_coordinates=faces_coordinates)
+
+ img = highlight_facial_areas(img=img, faces_coordinates=faces_coordinates)
+ img = countdown_to_freeze(
+ img=img,
+ faces_coordinates=faces_coordinates,
+ frame_threshold=frame_threshold,
+ num_frames_with_faces=num_frames_with_faces,
+ )
+
+ num_frames_with_faces = num_frames_with_faces + 1 if len(faces_coordinates) else 0
+
+ freeze = num_frames_with_faces > 0 and num_frames_with_faces % frame_threshold == 0
+ if freeze:
+ # add analyze results into img - derive from raw_img
+ img = highlight_facial_areas(img=raw_img, faces_coordinates=faces_coordinates)
+
+ # age, gender and emotion analysis
+ img = perform_demography_analysis(
+ enable_face_analysis=enable_face_analysis,
+ img=raw_img,
+ faces_coordinates=faces_coordinates,
+ detected_faces=detected_faces,
+ )
+ # facial recogntion analysis
+ img = perform_facial_recognition(
+ img=img,
+ faces_coordinates=faces_coordinates,
+ detected_faces=detected_faces,
+ db_path=db_path,
+ detector_backend=detector_backend,
+ distance_metric=distance_metric,
+ model_name=model_name,
+ )
+
+ # freeze the img after analysis
+ freezed_img = img.copy()
+
+ # start counter for freezing
+ tic = time.time()
+ logger.info("freezed")
+
+ elif freeze is True and time.time() - tic > time_threshold:
+ freeze = False
+ freezed_img = None
+ # reset counter for freezing
+ tic = time.time()
+ logger.info("freeze released")
+
+ freezed_img = countdown_to_release(img=freezed_img, tic=tic, time_threshold=time_threshold)
+
+ cv2.imshow("img", img if freezed_img is None else freezed_img)
+
+ if cv2.waitKey(1) & 0xFF == ord("q"): # press q to quit
+ break
+
+ # kill open cv things
+ cap.release()
+ cv2.destroyAllWindows()
+
+
+def build_facial_recognition_model(model_name: str) -> None:
+ """
+ Build facial recognition model
+ Args:
+ model_name (str): Model for face recognition. Options: VGG-Face, Facenet, Facenet512,
+ OpenFace, DeepFace, DeepID, Dlib, ArcFace, SFace and GhostFaceNet (default is VGG-Face).
+ Returns
+ input_shape (tuple): input shape of given facial recognitio n model.
+ """
+ _ = DeepFace.build_model(model_name=model_name)
+ logger.info(f"{model_name} is built")
+
+
+def search_identity(
+ detected_face: np.ndarray,
+ db_path: str,
+ model_name: str,
+ detector_backend: str,
+ distance_metric: str,
+) -> Tuple[Optional[str], Optional[np.ndarray]]:
+ """
+ Search an identity in facial database.
+ Args:
+ detected_face (np.ndarray): extracted individual facial image
+ db_path (string): Path to the folder containing image files. All detected faces
+ in the database will be considered in the decision-making process.
+ model_name (str): Model for face recognition. Options: VGG-Face, Facenet, Facenet512,
+ OpenFace, DeepFace, DeepID, Dlib, ArcFace, SFace and GhostFaceNet (default is VGG-Face).
+ detector_backend (string): face detector backend. Options: 'opencv', 'retinaface',
+ 'mtcnn', 'ssd', 'dlib', 'mediapipe', 'yolov8', 'centerface' or 'skip'
+ (default is opencv).
+ distance_metric (string): Metric for measuring similarity. Options: 'cosine',
+ 'euclidean', 'euclidean_l2' (default is cosine).
+ Returns:
+ result (tuple): result consisting of following objects
+ identified image path (str)
+ identified image itself (np.ndarray)
+ """
+ target_path = None
+ try:
+ dfs = DeepFace.find(
+ img_path=detected_face,
+ db_path=db_path,
+ model_name=model_name,
+ detector_backend=detector_backend,
+ distance_metric=distance_metric,
+ enforce_detection=False,
+ silent=True,
+ )
+ except ValueError as err:
+ if f"No item found in {db_path}" in str(err):
+ logger.warn(
+ f"No item is found in {db_path}."
+ "So, no facial recognition analysis will be performed."
+ )
+ dfs = []
+ else:
+ raise err
+ if len(dfs) == 0:
+ # you may consider to return unknown person's image here
+ return None, None
+
+ # detected face is coming from parent, safe to access 1st index
+ df = dfs[0]
+
+ if df.shape[0] == 0:
+ return None, None
+
+ candidate = df.iloc[0]
+ target_path = candidate["identity"]
+ logger.info(f"Hello, {target_path}")
+
+ # load found identity image - extracted if possible
+ target_objs = DeepFace.extract_faces(
+ img_path=target_path,
+ detector_backend=detector_backend,
+ enforce_detection=False,
+ align=True,
+ )
+
+ # extract facial area of the identified image if and only if it has one face
+ # otherwise, show image as is
+ if len(target_objs) == 1:
+ # extract 1st item directly
+ target_obj = target_objs[0]
+ target_img = target_obj["face"]
+ target_img = cv2.resize(target_img, (IDENTIFIED_IMG_SIZE, IDENTIFIED_IMG_SIZE))
+ target_img *= 255
+ target_img = target_img[:, :, ::-1]
+ else:
+ target_img = cv2.imread(target_path)
+
+ return target_path.split("/")[-1], target_img
+
+
+def build_demography_models(enable_face_analysis: bool) -> None:
+ """
+ Build demography analysis models
+ Args:
+ enable_face_analysis (bool): Flag to enable face analysis (default is True).
+ Returns:
+ None
+ """
+ if enable_face_analysis is False:
+ return
+ DeepFace.build_model(model_name="Age")
+ logger.info("Age model is just built")
+ DeepFace.build_model(model_name="Gender")
+ logger.info("Gender model is just built")
+ DeepFace.build_model(model_name="Emotion")
+ logger.info("Emotion model is just built")
+
+
+def highlight_facial_areas(
+ img: np.ndarray, faces_coordinates: List[Tuple[int, int, int, int]]
+) -> np.ndarray:
+ """
+ Highlight detected faces with rectangles in the given image
+ Args:
+ img (np.ndarray): image itself
+ faces_coordinates (list): list of face coordinates as tuple with x, y, w and h
+ Returns:
+ img (np.ndarray): image with highlighted facial areas
+ """
+ for x, y, w, h in faces_coordinates:
+ # highlight facial area with rectangle
+ cv2.rectangle(img, (x, y), (x + w, y + h), (67, 67, 67), 1)
+ return img
+
+
+def countdown_to_freeze(
+ img: np.ndarray,
+ faces_coordinates: List[Tuple[int, int, int, int]],
+ frame_threshold: int,
+ num_frames_with_faces: int,
+) -> np.ndarray:
+ """
+ Highlight time to freeze in the image's facial areas
+ Args:
+ img (np.ndarray): image itself
+ faces_coordinates (list): list of face coordinates as tuple with x, y, w and h
+ frame_threshold (int): how many sequantial frames required with face(s) to freeze
+ num_frames_with_faces (int): how many sequantial frames do we have with face(s)
+ Returns:
+ img (np.ndarray): image with counter values
+ """
+ for x, y, w, h in faces_coordinates:
+ cv2.putText(
+ img,
+ str(frame_threshold - (num_frames_with_faces % frame_threshold)),
+ (int(x + w / 4), int(y + h / 1.5)),
+ cv2.FONT_HERSHEY_SIMPLEX,
+ 4,
+ (255, 255, 255),
+ 2,
+ )
+ return img
+
+
+def countdown_to_release(
+ img: Optional[np.ndarray], tic: float, time_threshold: int
+) -> Optional[np.ndarray]:
+ """
+ Highlight time to release the freezing in the image top left area
+ Args:
+ img (np.ndarray): image itself
+ tic (float): time specifying when freezing started
+ time_threshold (int): freeze time threshold
+ Returns:
+ img (np.ndarray): image with time to release the freezing
+ """
+ # do not take any action if it is not frozen yet
+ if img is None:
+ return img
+ toc = time.time()
+ time_left = int(time_threshold - (toc - tic) + 1)
+ cv2.rectangle(img, (10, 10), (90, 50), (67, 67, 67), -10)
+ cv2.putText(
+ img,
+ str(time_left),
+ (40, 40),
+ cv2.FONT_HERSHEY_SIMPLEX,
+ 1,
+ (255, 255, 255),
+ 1,
+ )
+ return img
+
+
+def grab_facial_areas(
+ img: np.ndarray, detector_backend: str, threshold: int = 130
+) -> List[Tuple[int, int, int, int]]:
+ """
+ Find facial area coordinates in the given image
+ Args:
+ img (np.ndarray): image itself
+ detector_backend (string): face detector backend. Options: 'opencv', 'retinaface',
+ 'mtcnn', 'ssd', 'dlib', 'mediapipe', 'yolov8', 'centerface' or 'skip'
+ (default is opencv).
+ threshold (int): threshold for facial area, discard smaller ones
+ Returns
+ result (list): list of tuple with x, y, w and h coordinates
+ """
+ try:
+ face_objs = DeepFace.extract_faces(
+ img_path=img,
+ detector_backend=detector_backend,
+ # you may consider to extract with larger expanding value
+ expand_percentage=0,
+ )
+ faces = [
+ (
+ face_obj["facial_area"]["x"],
+ face_obj["facial_area"]["y"],
+ face_obj["facial_area"]["w"],
+ face_obj["facial_area"]["h"],
+ )
+ for face_obj in face_objs
+ if face_obj["facial_area"]["w"] > threshold
+ ]
+ return faces
+ except: # to avoid exception if no face detected
+ return []
+
+
+def extract_facial_areas(
+ img: np.ndarray, faces_coordinates: List[Tuple[int, int, int, int]]
+) -> List[np.ndarray]:
+ """
+ Extract facial areas as numpy array from given image
+ Args:
+ img (np.ndarray): image itself
+ faces_coordinates (list): list of facial area coordinates as tuple with
+ x, y, w and h values
+ Returns:
+ detected_faces (list): list of detected facial area images
+ """
+ detected_faces = []
+ for x, y, w, h in faces_coordinates:
+ detected_face = img[int(y) : int(y + h), int(x) : int(x + w)]
+ detected_faces.append(detected_face)
+ return detected_faces
+
+
+def perform_facial_recognition(
+ img: np.ndarray,
+ detected_faces: List[np.ndarray],
+ faces_coordinates: List[Tuple[int, int, int, int]],
+ db_path: str,
+ detector_backend: str,
+ distance_metric: str,
+ model_name: str,
+) -> np.ndarray:
+ """
+ Perform facial recognition
+ Args:
+ img (np.ndarray): image itself
+ detected_faces (list): list of extracted detected face images as numpy
+ faces_coordinates (list): list of facial area coordinates as tuple with
+ x, y, w and h values
+ db_path (string): Path to the folder containing image files. All detected faces
+ in the database will be considered in the decision-making process.
+ detector_backend (string): face detector backend. Options: 'opencv', 'retinaface',
+ 'mtcnn', 'ssd', 'dlib', 'mediapipe', 'yolov8', 'centerface' or 'skip'
+ (default is opencv).
+ distance_metric (string): Metric for measuring similarity. Options: 'cosine',
+ 'euclidean', 'euclidean_l2' (default is cosine).
+ model_name (str): Model for face recognition. Options: VGG-Face, Facenet, Facenet512,
+ OpenFace, DeepFace, DeepID, Dlib, ArcFace, SFace and GhostFaceNet (default is VGG-Face).
+ Returns:
+ img (np.ndarray): image with identified face informations
+ """
+ for idx, (x, y, w, h) in enumerate(faces_coordinates):
+ detected_face = detected_faces[idx]
+ target_label, target_img = search_identity(
+ detected_face=detected_face,
+ db_path=db_path,
+ detector_backend=detector_backend,
+ distance_metric=distance_metric,
+ model_name=model_name,
+ )
+ if target_label is None:
+ continue
+
+ img = overlay_identified_face(
+ img=img,
+ target_img=target_img,
+ label=target_label,
+ x=x,
+ y=y,
+ w=w,
+ h=h,
+ )
+
+ return img
+
+
+def perform_demography_analysis(
+ enable_face_analysis: bool,
+ img: np.ndarray,
+ faces_coordinates: List[Tuple[int, int, int, int]],
+ detected_faces: List[np.ndarray],
+) -> np.ndarray:
+ """
+ Perform demography analysis on given image
+ Args:
+ enable_face_analysis (bool): Flag to enable face analysis.
+ img (np.ndarray): image itself
+ faces_coordinates (list): list of face coordinates as tuple with
+ x, y, w and h values
+ detected_faces (list): list of extracted detected face images as numpy
+ Returns:
+ img (np.ndarray): image with analyzed demography information
+ """
+ if enable_face_analysis is False:
+ return img
+ for idx, (x, y, w, h) in enumerate(faces_coordinates):
+ detected_face = detected_faces[idx]
+ demographies = DeepFace.analyze(
+ img_path=detected_face,
+ actions=("age", "gender", "emotion"),
+ detector_backend="skip",
+ enforce_detection=False,
+ silent=True,
+ )
+
+ if len(demographies) == 0:
+ continue
+
+ # safe to access 1st index because detector backend is skip
+ demography = demographies[0]
+
+ img = overlay_emotion(img=img, emotion_probas=demography["emotion"], x=x, y=y, w=w, h=h)
+ img = overlay_age_gender(
+ img=img,
+ apparent_age=demography["age"],
+ gender=demography["dominant_gender"][0:1], # M or W
+ x=x,
+ y=y,
+ w=w,
+ h=h,
+ )
+ return img
+
+
+def overlay_identified_face(
+ img: np.ndarray,
+ target_img: np.ndarray,
+ label: str,
+ x: int,
+ y: int,
+ w: int,
+ h: int,
+) -> np.ndarray:
+ """
+ Overlay the identified face onto image itself
+ Args:
+ img (np.ndarray): image itself
+ target_img (np.ndarray): identified face's image
+ label (str): name of the identified face
+ x (int): x coordinate of the face on the given image
+ y (int): y coordinate of the face on the given image
+ w (int): w coordinate of the face on the given image
+ h (int): h coordinate of the face on the given image
+ Returns:
+ img (np.ndarray): image with overlayed identity
+ """
+ try:
+ if y - IDENTIFIED_IMG_SIZE > 0 and x + w + IDENTIFIED_IMG_SIZE < img.shape[1]:
+ # top right
+ img[
+ y - IDENTIFIED_IMG_SIZE : y,
+ x + w : x + w + IDENTIFIED_IMG_SIZE,
+ ] = target_img
+
+ overlay = img.copy()
+ opacity = 0.4
+ cv2.rectangle(
+ img,
+ (x + w, y),
+ (x + w + IDENTIFIED_IMG_SIZE, y + 20),
+ (46, 200, 255),
+ cv2.FILLED,
+ )
+ cv2.addWeighted(
+ overlay,
+ opacity,
+ img,
+ 1 - opacity,
+ 0,
+ img,
+ )
+
+ cv2.putText(
+ img,
+ label,
+ (x + w, y + 10),
+ cv2.FONT_HERSHEY_SIMPLEX,
+ 0.5,
+ TEXT_COLOR,
+ 1,
+ )
+
+ # connect face and text
+ cv2.line(
+ img,
+ (x + int(w / 2), y),
+ (x + 3 * int(w / 4), y - int(IDENTIFIED_IMG_SIZE / 2)),
+ (67, 67, 67),
+ 1,
+ )
+ cv2.line(
+ img,
+ (x + 3 * int(w / 4), y - int(IDENTIFIED_IMG_SIZE / 2)),
+ (x + w, y - int(IDENTIFIED_IMG_SIZE / 2)),
+ (67, 67, 67),
+ 1,
+ )
+
+ elif y + h + IDENTIFIED_IMG_SIZE < img.shape[0] and x - IDENTIFIED_IMG_SIZE > 0:
+ # bottom left
+ img[
+ y + h : y + h + IDENTIFIED_IMG_SIZE,
+ x - IDENTIFIED_IMG_SIZE : x,
+ ] = target_img
+
+ overlay = img.copy()
+ opacity = 0.4
+ cv2.rectangle(
+ img,
+ (x - IDENTIFIED_IMG_SIZE, y + h - 20),
+ (x, y + h),
+ (46, 200, 255),
+ cv2.FILLED,
+ )
+ cv2.addWeighted(
+ overlay,
+ opacity,
+ img,
+ 1 - opacity,
+ 0,
+ img,
+ )
+
+ cv2.putText(
+ img,
+ label,
+ (x - IDENTIFIED_IMG_SIZE, y + h - 10),
+ cv2.FONT_HERSHEY_SIMPLEX,
+ 0.5,
+ TEXT_COLOR,
+ 1,
+ )
+
+ # connect face and text
+ cv2.line(
+ img,
+ (x + int(w / 2), y + h),
+ (
+ x + int(w / 2) - int(w / 4),
+ y + h + int(IDENTIFIED_IMG_SIZE / 2),
+ ),
+ (67, 67, 67),
+ 1,
+ )
+ cv2.line(
+ img,
+ (
+ x + int(w / 2) - int(w / 4),
+ y + h + int(IDENTIFIED_IMG_SIZE / 2),
+ ),
+ (x, y + h + int(IDENTIFIED_IMG_SIZE / 2)),
+ (67, 67, 67),
+ 1,
+ )
+
+ elif y - IDENTIFIED_IMG_SIZE > 0 and x - IDENTIFIED_IMG_SIZE > 0:
+ # top left
+ img[y - IDENTIFIED_IMG_SIZE : y, x - IDENTIFIED_IMG_SIZE : x] = target_img
+
+ overlay = img.copy()
+ opacity = 0.4
+ cv2.rectangle(
+ img,
+ (x - IDENTIFIED_IMG_SIZE, y),
+ (x, y + 20),
+ (46, 200, 255),
+ cv2.FILLED,
+ )
+ cv2.addWeighted(
+ overlay,
+ opacity,
+ img,
+ 1 - opacity,
+ 0,
+ img,
+ )
+
+ cv2.putText(
+ img,
+ label,
+ (x - IDENTIFIED_IMG_SIZE, y + 10),
+ cv2.FONT_HERSHEY_SIMPLEX,
+ 0.5,
+ TEXT_COLOR,
+ 1,
+ )
+
+ # connect face and text
+ cv2.line(
+ img,
+ (x + int(w / 2), y),
+ (
+ x + int(w / 2) - int(w / 4),
+ y - int(IDENTIFIED_IMG_SIZE / 2),
+ ),
+ (67, 67, 67),
+ 1,
+ )
+ cv2.line(
+ img,
+ (
+ x + int(w / 2) - int(w / 4),
+ y - int(IDENTIFIED_IMG_SIZE / 2),
+ ),
+ (x, y - int(IDENTIFIED_IMG_SIZE / 2)),
+ (67, 67, 67),
+ 1,
+ )
+
+ elif (
+ x + w + IDENTIFIED_IMG_SIZE < img.shape[1]
+ and y + h + IDENTIFIED_IMG_SIZE < img.shape[0]
+ ):
+ # bottom righ
+ img[
+ y + h : y + h + IDENTIFIED_IMG_SIZE,
+ x + w : x + w + IDENTIFIED_IMG_SIZE,
+ ] = target_img
+
+ overlay = img.copy()
+ opacity = 0.4
+ cv2.rectangle(
+ img,
+ (x + w, y + h - 20),
+ (x + w + IDENTIFIED_IMG_SIZE, y + h),
+ (46, 200, 255),
+ cv2.FILLED,
+ )
+ cv2.addWeighted(
+ overlay,
+ opacity,
+ img,
+ 1 - opacity,
+ 0,
+ img,
+ )
+
+ cv2.putText(
+ img,
+ label,
+ (x + w, y + h - 10),
+ cv2.FONT_HERSHEY_SIMPLEX,
+ 0.5,
+ TEXT_COLOR,
+ 1,
+ )
+
+ # connect face and text
+ cv2.line(
+ img,
+ (x + int(w / 2), y + h),
+ (
+ x + int(w / 2) + int(w / 4),
+ y + h + int(IDENTIFIED_IMG_SIZE / 2),
+ ),
+ (67, 67, 67),
+ 1,
+ )
+ cv2.line(
+ img,
+ (
+ x + int(w / 2) + int(w / 4),
+ y + h + int(IDENTIFIED_IMG_SIZE / 2),
+ ),
+ (x + w, y + h + int(IDENTIFIED_IMG_SIZE / 2)),
+ (67, 67, 67),
+ 1,
+ )
+ else:
+ logger.info("cannot put facial recognition info on the image")
+ except Exception as err: # pylint: disable=broad-except
+ logger.error(str(err))
+ return img
+
+
+def overlay_emotion(
+ img: np.ndarray, emotion_probas: dict, x: int, y: int, w: int, h: int
+) -> np.ndarray:
+ """
+ Overlay the analyzed emotion of face onto image itself
+ Args:
+ img (np.ndarray): image itself
+ emotion_probas (dict): probability of different emotionas dictionary
+ x (int): x coordinate of the face on the given image
+ y (int): y coordinate of the face on the given image
+ w (int): w coordinate of the face on the given image
+ h (int): h coordinate of the face on the given image
+ Returns:
+ img (np.ndarray): image with overlay emotion analsis results
+ """
+ emotion_df = pd.DataFrame(emotion_probas.items(), columns=["emotion", "score"])
+ emotion_df = emotion_df.sort_values(by=["score"], ascending=False).reset_index(drop=True)
+
+ # background of mood box
+
+ # transparency
+ overlay = img.copy()
+ opacity = 0.4
+
+ # put gray background to the right of the detected image
+ if x + w + IDENTIFIED_IMG_SIZE < img.shape[1]:
+ cv2.rectangle(
+ img,
+ (x + w, y),
+ (x + w + IDENTIFIED_IMG_SIZE, y + h),
+ (64, 64, 64),
+ cv2.FILLED,
+ )
+ cv2.addWeighted(overlay, opacity, img, 1 - opacity, 0, img)
+
+ # put gray background to the left of the detected image
+ elif x - IDENTIFIED_IMG_SIZE > 0:
+ cv2.rectangle(
+ img,
+ (x - IDENTIFIED_IMG_SIZE, y),
+ (x, y + h),
+ (64, 64, 64),
+ cv2.FILLED,
+ )
+ cv2.addWeighted(overlay, opacity, img, 1 - opacity, 0, img)
+
+ for index, instance in emotion_df.iterrows():
+ current_emotion = instance["emotion"]
+ emotion_label = f"{current_emotion} "
+ emotion_score = instance["score"] / 100
+
+ filled_bar_x = 35 # this is the size if an emotion is 100%
+ bar_x = int(filled_bar_x * emotion_score)
+
+ if x + w + IDENTIFIED_IMG_SIZE < img.shape[1]:
+
+ text_location_y = y + 20 + (index + 1) * 20
+ text_location_x = x + w
+
+ if text_location_y < y + h:
+ cv2.putText(
+ img,
+ emotion_label,
+ (text_location_x, text_location_y),
+ cv2.FONT_HERSHEY_SIMPLEX,
+ 0.5,
+ (255, 255, 255),
+ 1,
+ )
+
+ cv2.rectangle(
+ img,
+ (x + w + 70, y + 13 + (index + 1) * 20),
+ (
+ x + w + 70 + bar_x,
+ y + 13 + (index + 1) * 20 + 5,
+ ),
+ (255, 255, 255),
+ cv2.FILLED,
+ )
+
+ elif x - IDENTIFIED_IMG_SIZE > 0:
+
+ text_location_y = y + 20 + (index + 1) * 20
+ text_location_x = x - IDENTIFIED_IMG_SIZE
+
+ if text_location_y <= y + h:
+ cv2.putText(
+ img,
+ emotion_label,
+ (text_location_x, text_location_y),
+ cv2.FONT_HERSHEY_SIMPLEX,
+ 0.5,
+ (255, 255, 255),
+ 1,
+ )
+
+ cv2.rectangle(
+ img,
+ (
+ x - IDENTIFIED_IMG_SIZE + 70,
+ y + 13 + (index + 1) * 20,
+ ),
+ (
+ x - IDENTIFIED_IMG_SIZE + 70 + bar_x,
+ y + 13 + (index + 1) * 20 + 5,
+ ),
+ (255, 255, 255),
+ cv2.FILLED,
+ )
+
+ return img
+
+
+def overlay_age_gender(
+ img: np.ndarray, apparent_age: float, gender: str, x: int, y: int, w: int, h: int
+) -> np.ndarray:
+ """
+ Overlay the analyzed age and gender of face onto image itself
+ Args:
+ img (np.ndarray): image itself
+ apparent_age (float): analyzed apparent age
+ gender (str): analyzed gender
+ x (int): x coordinate of the face on the given image
+ y (int): y coordinate of the face on the given image
+ w (int): w coordinate of the face on the given image
+ h (int): h coordinate of the face on the given image
+ Returns:
+ img (np.ndarray): image with overlay age and gender analsis results
+ """
+ logger.debug(f"{apparent_age} years old {gender}")
+ analysis_report = f"{int(apparent_age)} {gender}"
+
+ info_box_color = (46, 200, 255)
+
+ # show its age and gender on the top of the image
+ if y - IDENTIFIED_IMG_SIZE + int(IDENTIFIED_IMG_SIZE / 5) > 0:
+
+ triangle_coordinates = np.array(
+ [
+ (x + int(w / 2), y),
+ (
+ x + int(w / 2) - int(w / 10),
+ y - int(IDENTIFIED_IMG_SIZE / 3),
+ ),
+ (
+ x + int(w / 2) + int(w / 10),
+ y - int(IDENTIFIED_IMG_SIZE / 3),
+ ),
+ ]
+ )
+
+ cv2.drawContours(
+ img,
+ [triangle_coordinates],
+ 0,
+ info_box_color,
+ -1,
+ )
+
+ cv2.rectangle(
+ img,
+ (
+ x + int(w / 5),
+ y - IDENTIFIED_IMG_SIZE + int(IDENTIFIED_IMG_SIZE / 5),
+ ),
+ (x + w - int(w / 5), y - int(IDENTIFIED_IMG_SIZE / 3)),
+ info_box_color,
+ cv2.FILLED,
+ )
+
+ cv2.putText(
+ img,
+ analysis_report,
+ (x + int(w / 3.5), y - int(IDENTIFIED_IMG_SIZE / 2.1)),
+ cv2.FONT_HERSHEY_SIMPLEX,
+ 1,
+ (0, 111, 255),
+ 2,
+ )
+
+ # show its age and gender on the top of the image
+ elif y + h + IDENTIFIED_IMG_SIZE - int(IDENTIFIED_IMG_SIZE / 5) < img.shape[0]:
+
+ triangle_coordinates = np.array(
+ [
+ (x + int(w / 2), y + h),
+ (
+ x + int(w / 2) - int(w / 10),
+ y + h + int(IDENTIFIED_IMG_SIZE / 3),
+ ),
+ (
+ x + int(w / 2) + int(w / 10),
+ y + h + int(IDENTIFIED_IMG_SIZE / 3),
+ ),
+ ]
+ )
+
+ cv2.drawContours(
+ img,
+ [triangle_coordinates],
+ 0,
+ info_box_color,
+ -1,
+ )
+
+ cv2.rectangle(
+ img,
+ (x + int(w / 5), y + h + int(IDENTIFIED_IMG_SIZE / 3)),
+ (
+ x + w - int(w / 5),
+ y + h + IDENTIFIED_IMG_SIZE - int(IDENTIFIED_IMG_SIZE / 5),
+ ),
+ info_box_color,
+ cv2.FILLED,
+ )
+
+ cv2.putText(
+ img,
+ analysis_report,
+ (x + int(w / 3.5), y + h + int(IDENTIFIED_IMG_SIZE / 1.5)),
+ cv2.FONT_HERSHEY_SIMPLEX,
+ 1,
+ (0, 111, 255),
+ 2,
+ )
+
+ return img
diff --git a/deepface/modules/verification.py b/deepface/modules/verification.py
new file mode 100644
index 0000000000000000000000000000000000000000..6bb5248f4fa525cd2e3761af1fd92c737542aa6d
--- /dev/null
+++ b/deepface/modules/verification.py
@@ -0,0 +1,374 @@
+# built-in dependencies
+import time
+from typing import Any, Dict, Union, List, Tuple
+
+# 3rd party dependencies
+import numpy as np
+
+# project dependencies
+from deepface.modules import representation, detection, modeling
+from deepface.models.FacialRecognition import FacialRecognition
+from deepface.commons import logger as log
+
+logger = log.get_singletonish_logger()
+
+
+def verify(
+ img1_path: Union[str, np.ndarray, List[float]],
+ img2_path: Union[str, np.ndarray, List[float]],
+ model_name: str = "VGG-Face",
+ detector_backend: str = "opencv",
+ distance_metric: str = "cosine",
+ enforce_detection: bool = True,
+ align: bool = True,
+ expand_percentage: int = 0,
+ normalization: str = "base",
+ silent: bool = False,
+) -> Dict[str, Any]:
+ """
+ Verify if an image pair represents the same person or different persons.
+
+ The verification function converts facial images to vectors and calculates the similarity
+ between those vectors. Vectors of images of the same person should exhibit higher similarity
+ (or lower distance) than vectors of images of different persons.
+
+ Args:
+ img1_path (str or np.ndarray or List[float]): Path to the first image.
+ Accepts exact image path as a string, numpy array (BGR), base64 encoded images
+ or pre-calculated embeddings.
+
+ img2_path (str or np.ndarray or or List[float]): Path to the second image.
+ Accepts exact image path as a string, numpy array (BGR), base64 encoded images
+ or pre-calculated embeddings.
+
+ model_name (str): Model for face recognition. Options: VGG-Face, Facenet, Facenet512,
+ OpenFace, DeepFace, DeepID, Dlib, ArcFace, SFace and GhostFaceNet (default is VGG-Face).
+
+ detector_backend (string): face detector backend. Options: 'opencv', 'retinaface',
+ 'mtcnn', 'ssd', 'dlib', 'mediapipe', 'yolov8', 'centerface' or 'skip'
+ (default is opencv)
+
+ distance_metric (string): Metric for measuring similarity. Options: 'cosine',
+ 'euclidean', 'euclidean_l2' (default is cosine).
+
+ enforce_detection (boolean): If no face is detected in an image, raise an exception.
+ Set to False to avoid the exception for low-resolution images (default is True).
+
+ align (bool): Flag to enable face alignment (default is True).
+
+ expand_percentage (int): expand detected facial area with a percentage (default is 0).
+
+ normalization (string): Normalize the input image before feeding it to the model.
+ Options: base, raw, Facenet, Facenet2018, VGGFace, VGGFace2, ArcFace (default is base)
+
+ silent (boolean): Suppress or allow some log messages for a quieter analysis process
+ (default is False).
+
+ Returns:
+ result (dict): A dictionary containing verification results.
+
+ - 'verified' (bool): Indicates whether the images represent the same person (True)
+ or different persons (False).
+
+ - 'distance' (float): The distance measure between the face vectors.
+ A lower distance indicates higher similarity.
+
+ - 'max_threshold_to_verify' (float): The maximum threshold used for verification.
+ If the distance is below this threshold, the images are considered a match.
+
+ - 'model' (str): The chosen face recognition model.
+
+ - 'similarity_metric' (str): The chosen similarity metric for measuring distances.
+
+ - 'facial_areas' (dict): Rectangular regions of interest for faces in both images.
+ - 'img1': {'x': int, 'y': int, 'w': int, 'h': int}
+ Region of interest for the first image.
+ - 'img2': {'x': int, 'y': int, 'w': int, 'h': int}
+ Region of interest for the second image.
+
+ - 'time' (float): Time taken for the verification process in seconds.
+ """
+
+ tic = time.time()
+
+ model: FacialRecognition = modeling.build_model(model_name)
+ dims = model.output_shape
+
+ # extract faces from img1
+ if isinstance(img1_path, list):
+ # given image is already pre-calculated embedding
+ if not all(isinstance(dim, float) for dim in img1_path):
+ raise ValueError(
+ "When passing img1_path as a list, ensure that all its items are of type float."
+ )
+
+ if silent is False:
+ logger.warn(
+ "You passed 1st image as pre-calculated embeddings."
+ f"Please ensure that embeddings have been calculated for the {model_name} model."
+ )
+
+ if len(img1_path) != dims:
+ raise ValueError(
+ f"embeddings of {model_name} should have {dims} dimensions,"
+ f" but it has {len(img1_path)} dimensions input"
+ )
+
+ img1_embeddings = [img1_path]
+ img1_facial_areas = [None]
+ else:
+ try:
+ img1_embeddings, img1_facial_areas = __extract_faces_and_embeddings(
+ img_path=img1_path,
+ model_name=model_name,
+ detector_backend=detector_backend,
+ enforce_detection=enforce_detection,
+ align=align,
+ expand_percentage=expand_percentage,
+ normalization=normalization,
+ )
+ except ValueError as err:
+ raise ValueError("Exception while processing img1_path") from err
+
+ # extract faces from img2
+ if isinstance(img2_path, list):
+ # given image is already pre-calculated embedding
+ if not all(isinstance(dim, float) for dim in img2_path):
+ raise ValueError(
+ "When passing img2_path as a list, ensure that all its items are of type float."
+ )
+
+ if silent is False:
+ logger.warn(
+ "You passed 2nd image as pre-calculated embeddings."
+ f"Please ensure that embeddings have been calculated for the {model_name} model."
+ )
+
+ if len(img2_path) != dims:
+ raise ValueError(
+ f"embeddings of {model_name} should have {dims} dimensions,"
+ f" but it has {len(img2_path)} dimensions input"
+ )
+
+ img2_embeddings = [img2_path]
+ img2_facial_areas = [None]
+ else:
+ try:
+ img2_embeddings, img2_facial_areas = __extract_faces_and_embeddings(
+ img_path=img2_path,
+ model_name=model_name,
+ detector_backend=detector_backend,
+ enforce_detection=enforce_detection,
+ align=align,
+ expand_percentage=expand_percentage,
+ normalization=normalization,
+ )
+ except ValueError as err:
+ raise ValueError("Exception while processing img2_path") from err
+
+ no_facial_area = {
+ "x": None,
+ "y": None,
+ "w": None,
+ "h": None,
+ "left_eye": None,
+ "right_eye": None,
+ }
+
+ distances = []
+ facial_areas = []
+ for idx, img1_embedding in enumerate(img1_embeddings):
+ for idy, img2_embedding in enumerate(img2_embeddings):
+ distance = find_distance(img1_embedding, img2_embedding, distance_metric)
+ distances.append(distance)
+ facial_areas.append(
+ (img1_facial_areas[idx] or no_facial_area, img2_facial_areas[idy] or no_facial_area)
+ )
+
+ # find the face pair with minimum distance
+ threshold = find_threshold(model_name, distance_metric)
+ distance = float(min(distances)) # best distance
+ facial_areas = facial_areas[np.argmin(distances)]
+
+ toc = time.time()
+
+ resp_obj = {
+ "verified": distance <= threshold,
+ "distance": distance,
+ "threshold": threshold,
+ "model": model_name,
+ "detector_backend": detector_backend,
+ "similarity_metric": distance_metric,
+ "facial_areas": {"img1": facial_areas[0], "img2": facial_areas[1]},
+ "time": round(toc - tic, 2),
+ }
+
+ return resp_obj
+
+
+def __extract_faces_and_embeddings(
+ img_path: Union[str, np.ndarray],
+ model_name: str = "VGG-Face",
+ detector_backend: str = "opencv",
+ enforce_detection: bool = True,
+ align: bool = True,
+ expand_percentage: int = 0,
+ normalization: str = "base",
+) -> Tuple[List[List[float]], List[dict]]:
+ """
+ Extract facial areas and find corresponding embeddings for given image
+ Returns:
+ embeddings (List[float])
+ facial areas (List[dict])
+ """
+ embeddings = []
+ facial_areas = []
+
+ img_objs = detection.extract_faces(
+ img_path=img_path,
+ detector_backend=detector_backend,
+ grayscale=False,
+ enforce_detection=enforce_detection,
+ align=align,
+ expand_percentage=expand_percentage,
+ )
+
+ # find embeddings for each face
+ for img_obj in img_objs:
+ img_embedding_obj = representation.represent(
+ img_path=img_obj["face"],
+ model_name=model_name,
+ enforce_detection=enforce_detection,
+ detector_backend="skip",
+ align=align,
+ normalization=normalization,
+ )
+ # already extracted face given, safe to access its 1st item
+ img_embedding = img_embedding_obj[0]["embedding"]
+ embeddings.append(img_embedding)
+ facial_areas.append(img_obj["facial_area"])
+
+ return embeddings, facial_areas
+
+
+def find_cosine_distance(
+ source_representation: Union[np.ndarray, list], test_representation: Union[np.ndarray, list]
+) -> np.float64:
+ """
+ Find cosine distance between two given vectors
+ Args:
+ source_representation (np.ndarray or list): 1st vector
+ test_representation (np.ndarray or list): 2nd vector
+ Returns
+ distance (np.float64): calculated cosine distance
+ """
+ if isinstance(source_representation, list):
+ source_representation = np.array(source_representation)
+
+ if isinstance(test_representation, list):
+ test_representation = np.array(test_representation)
+
+ a = np.matmul(np.transpose(source_representation), test_representation)
+ b = np.sum(np.multiply(source_representation, source_representation))
+ c = np.sum(np.multiply(test_representation, test_representation))
+ return 1 - (a / (np.sqrt(b) * np.sqrt(c)))
+
+
+def find_euclidean_distance(
+ source_representation: Union[np.ndarray, list], test_representation: Union[np.ndarray, list]
+) -> np.float64:
+ """
+ Find euclidean distance between two given vectors
+ Args:
+ source_representation (np.ndarray or list): 1st vector
+ test_representation (np.ndarray or list): 2nd vector
+ Returns
+ distance (np.float64): calculated euclidean distance
+ """
+ if isinstance(source_representation, list):
+ source_representation = np.array(source_representation)
+
+ if isinstance(test_representation, list):
+ test_representation = np.array(test_representation)
+
+ euclidean_distance = source_representation - test_representation
+ euclidean_distance = np.sum(np.multiply(euclidean_distance, euclidean_distance))
+ euclidean_distance = np.sqrt(euclidean_distance)
+ return euclidean_distance
+
+
+def l2_normalize(x: Union[np.ndarray, list]) -> np.ndarray:
+ """
+ Normalize input vector with l2
+ Args:
+ x (np.ndarray or list): given vector
+ Returns:
+ y (np.ndarray): l2 normalized vector
+ """
+ if isinstance(x, list):
+ x = np.array(x)
+ return x / np.sqrt(np.sum(np.multiply(x, x)))
+
+
+def find_distance(
+ alpha_embedding: Union[np.ndarray, list],
+ beta_embedding: Union[np.ndarray, list],
+ distance_metric: str,
+) -> np.float64:
+ """
+ Wrapper to find distance between vectors according to the given distance metric
+ Args:
+ source_representation (np.ndarray or list): 1st vector
+ test_representation (np.ndarray or list): 2nd vector
+ Returns
+ distance (np.float64): calculated cosine distance
+ """
+ if distance_metric == "cosine":
+ distance = find_cosine_distance(alpha_embedding, beta_embedding)
+ elif distance_metric == "euclidean":
+ distance = find_euclidean_distance(alpha_embedding, beta_embedding)
+ elif distance_metric == "euclidean_l2":
+ distance = find_euclidean_distance(
+ l2_normalize(alpha_embedding), l2_normalize(beta_embedding)
+ )
+ else:
+ raise ValueError("Invalid distance_metric passed - ", distance_metric)
+ return distance
+
+
+def find_threshold(model_name: str, distance_metric: str) -> float:
+ """
+ Retrieve pre-tuned threshold values for a model and distance metric pair
+ Args:
+ model_name (str): Model for face recognition. Options: VGG-Face, Facenet, Facenet512,
+ OpenFace, DeepFace, DeepID, Dlib, ArcFace, SFace and GhostFaceNet (default is VGG-Face).
+ distance_metric (str): distance metric name. Options are cosine, euclidean
+ and euclidean_l2.
+ Returns:
+ threshold (float): threshold value for that model name and distance metric
+ pair. Distances less than this threshold will be classified same person.
+ """
+
+ base_threshold = {"cosine": 0.40, "euclidean": 0.55, "euclidean_l2": 0.75}
+
+ thresholds = {
+ # "VGG-Face": {"cosine": 0.40, "euclidean": 0.60, "euclidean_l2": 0.86}, # 2622d
+ "VGG-Face": {
+ "cosine": 0.68,
+ "euclidean": 1.17,
+ "euclidean_l2": 1.17,
+ }, # 4096d - tuned with LFW
+ "Facenet": {"cosine": 0.40, "euclidean": 10, "euclidean_l2": 0.80},
+ "Facenet512": {"cosine": 0.30, "euclidean": 23.56, "euclidean_l2": 1.04},
+ "ArcFace": {"cosine": 0.68, "euclidean": 4.15, "euclidean_l2": 1.13},
+ "Dlib": {"cosine": 0.07, "euclidean": 0.6, "euclidean_l2": 0.4},
+ "SFace": {"cosine": 0.593, "euclidean": 10.734, "euclidean_l2": 1.055},
+ "OpenFace": {"cosine": 0.10, "euclidean": 0.55, "euclidean_l2": 0.55},
+ "DeepFace": {"cosine": 0.23, "euclidean": 64, "euclidean_l2": 0.64},
+ "DeepID": {"cosine": 0.015, "euclidean": 45, "euclidean_l2": 0.17},
+ "GhostFaceNet": {"cosine": 0.65, "euclidean": 35.71, "euclidean_l2": 1.10},
+ }
+
+ threshold = thresholds.get(model_name, base_threshold).get(distance_metric, 0.4)
+
+ return threshold
diff --git a/icon/deepface-api.jpg b/icon/deepface-api.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..8ca3fec14b88d0aa0e35dd5d68e54935d49da8da
Binary files /dev/null and b/icon/deepface-api.jpg differ
diff --git a/icon/deepface-dockerized-v2.jpg b/icon/deepface-dockerized-v2.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..db79929b1199c2f5512be1763c328b82268e9972
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 0000000000000000000000000000000000000000..520887c9d5b962877be457c70e26907bb14bffa6
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 0000000000000000000000000000000000000000..d0a3b9b9a2bc61eefd8508db4aa23a83c5633191
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 0000000000000000000000000000000000000000..b894e75e19114dbe2958bfb6919c6e2a285919d9
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 0000000000000000000000000000000000000000..b7bf51779fc167974a535b4f8af6fe113f61100f
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 0000000000000000000000000000000000000000..15f73fb4b69d9e53b9a53aa6e015346abcdf9175
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 0000000000000000000000000000000000000000..e35cef1578389d097f041ccd5bf680309865fab8
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 0000000000000000000000000000000000000000..3fb158a76c2f63a933bd5447bb690d59510d908c
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 0000000000000000000000000000000000000000..85559132800dcb370b1d53c90b703d61ba79cef3
Binary files /dev/null and b/icon/embedding.jpg differ
diff --git a/icon/model-portfolio-20240316.jpg b/icon/model-portfolio-20240316.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..7252155fc8926c563f4f5ce8180503b3aa645364
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 0000000000000000000000000000000000000000..b359f00ed9f03b2ecbf1de34be4c6b60e44d3b4a
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 0000000000000000000000000000000000000000..21cbfd96a49413a0c59e0855aa33425fe1df0673
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 0000000000000000000000000000000000000000..11e5db9002ebfa8a377fbb8fe8c9f98f499eb54f
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 0000000000000000000000000000000000000000..b786042db273075bedf51a0a311d3bf15101c0c6
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 0000000000000000000000000000000000000000..4e542ea76759975ea3a6e8c4679ab1103d39441c
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 0000000000000000000000000000000000000000..68254cb71834305848b3c1f08c372a1cdccfb716
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 0000000000000000000000000000000000000000..a7fe5ac090f770d08de5f015ff1ec25c27edd250
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 0000000000000000000000000000000000000000..66fc890b8885186a4b1a8e3b3710324638ff33ef
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 0000000000000000000000000000000000000000..36c186393773faad2a29563b5a2868ae268f9c96
--- /dev/null
+++ b/package_info.json
@@ -0,0 +1,3 @@
+{
+ "version": "0.0.90"
+}
\ No newline at end of file
diff --git a/railway.toml b/railway.toml
new file mode 100644
index 0000000000000000000000000000000000000000..4b1fac87ef4b579daad2f94c057f7a4ba14b1097
--- /dev/null
+++ b/railway.toml
@@ -0,0 +1,2 @@
+[env]
+port = 8080
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..0d1fefc5fdafa1ef09b193b827fd21de96ab9723
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,16 @@
+requests>=2.27.1
+numpy>=1.14.0
+pandas>=0.23.4
+gdown>=3.10.1
+tqdm>=4.30.0
+Pillow>=5.2.0
+opencv-python>=4.5.5.64
+tensorflow>=1.9.0
+keras>=2.2.0
+Flask>=1.1.2
+mtcnn>=0.1.0
+retina-face>=0.0.1
+fire>=0.4.0
+gunicorn>=20.1.0
+cloudinary>=1.40.0
+python-dotenv>=1.0.1
diff --git a/requirements_additional.txt b/requirements_additional.txt
new file mode 100644
index 0000000000000000000000000000000000000000..0344661d6387c28115da5949aa75bf4dd31d5c1a
--- /dev/null
+++ b/requirements_additional.txt
@@ -0,0 +1,5 @@
+opencv-contrib-python>=4.3.0.36
+mediapipe>=0.8.7.3
+dlib>=19.20.0
+ultralytics>=8.0.122
+facenet-pytorch>=2.5.3
\ No newline at end of file
diff --git a/requirements_local b/requirements_local
new file mode 100644
index 0000000000000000000000000000000000000000..e869c3f4053c7f1fb74814e0783e9057f37b9fc4
--- /dev/null
+++ b/requirements_local
@@ -0,0 +1,6 @@
+numpy==1.22.3
+pandas==2.0.3
+Pillow==9.0.0
+opencv-python==4.9.0.80
+tensorflow==2.9.0
+keras==2.9.0
diff --git a/scripts/dockerize.sh b/scripts/dockerize.sh
new file mode 100644
index 0000000000000000000000000000000000000000..0d5ac6e00c4d11f5a13e48724a2453300f91f494
--- /dev/null
+++ b/scripts/dockerize.sh
@@ -0,0 +1,27 @@
+# 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 image
+docker run --net="host" 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 0000000000000000000000000000000000000000..5b3e6fac53e5f78e3230f1ee9c00cb8f7218ad22
--- /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 100644
index 0000000000000000000000000000000000000000..a8ac03243cc7891ea937f576a3ab5a08108e1d44
--- /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=0.0.0.0:5000 "app:create_app()"
\ No newline at end of file
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000000000000000000000000000000000000..6424cecf31ddf8110307952e38e0da9ff2c5c68a
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,37 @@
+import json
+import setuptools
+
+with open("README.md", "r", encoding="utf-8") as fh:
+ long_description = fh.read()
+
+with open("requirements.txt", "r", encoding="utf-8") as f:
+ requirements = f.read().split("\n")
+
+with open("package_info.json", "r", encoding="utf-8") as f:
+ package_info = json.load(f)
+
+setuptools.setup(
+ name="deepface",
+ version=package_info["version"],
+ author="Sefik Ilkin Serengil",
+ author_email="serengil@gmail.com",
+ description=(
+ "A Lightweight Face Recognition and Facial Attribute Analysis Framework"
+ " (Age, Gender, Emotion, Race) for Python"
+ ),
+ data_files=[("", ["README.md", "requirements.txt", "package_info.json"])],
+ long_description=long_description,
+ long_description_content_type="text/markdown",
+ url="https://github.com/serengil/deepface",
+ packages=setuptools.find_packages(),
+ classifiers=[
+ "Programming Language :: Python :: 3",
+ "License :: OSI Approved :: MIT License",
+ "Operating System :: OS Independent",
+ ],
+ entry_points={
+ "console_scripts": ["deepface = deepface.DeepFace:cli"],
+ },
+ python_requires=">=3.7",
+ install_requires=requirements,
+)
diff --git a/tests/face-recognition-how.py b/tests/face-recognition-how.py
new file mode 100644
index 0000000000000000000000000000000000000000..2c92f28c120d91ff56317306c6ce0428ac934a9a
--- /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 import logger as log
+
+logger = log.get_singletonish_logger()
+
+# ----------------------------------------------
+# build face recognition model
+
+model_name = "VGG-Face"
+
+model: FacialRecognition = DeepFace.build_model(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 0000000000000000000000000000000000000000..99cd977f6ca31c5d7a2ffcd60aced1ebdf75a6b1
--- /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 0000000000000000000000000000000000000000..6c041bfed7bc10b33a0ae29f5f13192ae750e182
--- /dev/null
+++ b/tests/stream.py
@@ -0,0 +1,8 @@
+from deepface import DeepFace
+
+DeepFace.stream("dataset") #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 0000000000000000000000000000000000000000..3e3d3e12ed36987748d0263a092a26102aa7a314
--- /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 import logger as log
+
+logger = log.get_singletonish_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 0000000000000000000000000000000000000000..0eeafc8ae3c8f732d43bcd433335470e9bcb8802
--- /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 import logger as log
+
+logger = log.get_singletonish_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_enforce_detection.py b/tests/test_enforce_detection.py
new file mode 100644
index 0000000000000000000000000000000000000000..db73ce16454b1efa34ea07ea4a726216137a1fe2
--- /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 import logger as log
+
+logger = log.get_singletonish_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 0000000000000000000000000000000000000000..eac1e82dc79ea6d7ad73721959b04802845d109e
--- /dev/null
+++ b/tests/test_extract_faces.py
@@ -0,0 +1,78 @@
+# built-in dependencies
+import base64
+
+# 3rd party dependencies
+import numpy as np
+import pytest
+
+# project dependencies
+from deepface import DeepFace
+from deepface.commons import image_utils
+from deepface.commons import logger as log
+
+logger = log.get_singletonish_logger()
+
+detectors = ["opencv", "mtcnn"]
+
+
+def test_different_detectors():
+ for detector in detectors:
+ img_objs = DeepFace.extract_faces(img_path="dataset/img11.jpg", 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"]
+ assert left_eye[0] > right_eye[0]
+ assert "confidence" in img_obj.keys()
+
+ 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
diff --git a/tests/test_find.py b/tests/test_find.py
new file mode 100644
index 0000000000000000000000000000000000000000..83d9964f52abf9576ac89b02d38e1e6fa213b1f4
--- /dev/null
+++ b/tests/test_find.py
@@ -0,0 +1,103 @@
+# 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 import logger as log
+
+logger = log.get_singletonish_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
diff --git a/tests/test_represent.py b/tests/test_represent.py
new file mode 100644
index 0000000000000000000000000000000000000000..97878c587ee7f01bdf200a12a586afbdd6e675f4
--- /dev/null
+++ b/tests/test_represent.py
@@ -0,0 +1,50 @@
+# built-in dependencies
+import cv2
+
+# project dependencies
+from deepface import DeepFace
+from deepface.commons import logger as log
+
+logger = log.get_singletonish_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")
diff --git a/tests/test_verify.py b/tests/test_verify.py
new file mode 100644
index 0000000000000000000000000000000000000000..01e7e4a6060d44d281b7aabfa8280fe062595e53
--- /dev/null
+++ b/tests/test_verify.py
@@ -0,0 +1,155 @@
+# 3rd party dependencies
+import pytest
+import cv2
+
+# project dependencies
+from deepface import DeepFace
+from deepface.commons import logger as log
+
+logger = log.get_singletonish_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
+
+ logger.info("✅ test verify for pre-calculated embeddings done")
+
+
+def test_verify_with_precalculated_embeddings_for_incorrect_model():
+ # generate embeddings with VGG (default)
+ img1_path = "dataset/img1.jpg"
+ img2_path = "dataset/img2.jpg"
+ img1_embedding = DeepFace.represent(img_path=img1_path)[0]["embedding"]
+ img2_embedding = DeepFace.represent(img_path=img2_path)[0]["embedding"]
+
+ with pytest.raises(
+ ValueError,
+ match="embeddings of Facenet should have 128 dimensions, but it has 4096 dimensions input",
+ ):
+ _ = DeepFace.verify(
+ img1_path=img1_embedding, img2_path=img2_embedding, model_name="Facenet", silent=True
+ )
+
+ logger.info("✅ test verify with pre-calculated embeddings for incorrect model done")
+
+
+def test_verify_for_broken_embeddings():
+ img1_embeddings = ["a", "b", "c"]
+ img2_embeddings = [1, 2, 3]
+
+ with pytest.raises(
+ ValueError,
+ match="When passing img1_path as a list, ensure that all its items are of type float.",
+ ):
+ _ = DeepFace.verify(img1_path=img1_embeddings, img2_path=img2_embeddings)
+ logger.info("✅ test verify for broken embeddings content is done")
diff --git a/tests/test_version.py b/tests/test_version.py
new file mode 100644
index 0000000000000000000000000000000000000000..08e3bbf8b28442abaa43a1225b83fdb5ebf7431c
--- /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 import logger as log
+
+logger = log.get_singletonish_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 0000000000000000000000000000000000000000..eb016b52debbf0355025d872827aca662b754abf
--- /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 import logger as log
+
+logger = log.get_singletonish_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("-----------")