diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000000000000000000000000000000000000..f9fee0bc4138168cff4cc003d93bff704ca5c3b8 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: serengil +patreon: serengil?repo=deepface diff --git a/.github/ISSUE_TEMPLATE/01-report-bug.yaml b/.github/ISSUE_TEMPLATE/01-report-bug.yaml new file mode 100644 index 0000000000000000000000000000000000000000..55d3382d27551d3e4e931a319d7f36723ae80b85 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/01-report-bug.yaml @@ -0,0 +1,86 @@ +name: 'πŸ› Report a bug' +description: 'Use this template to report DeepFace related issues' +title: '[BUG]: ' +labels: + - bug +body: + - type: checkboxes + id: preliminary-checks + attributes: + label: Before You Report a Bug, Please Confirm You Have Done The Following... + description: If any of these required steps are not taken, we may not be able to review your issue. Help us to help you! + options: + - label: I have updated to the latest version of the packages. + required: true + - label: I have searched for both [existing issues](https://github.com/serengil/deepface/issues) and [closed issues](https://github.com/serengil/deepface/issues?q=is%3Aissue+is%3Aclosed) and found none that matched my issue. + required: true + - type: input + id: deepface-version + attributes: + label: DeepFace's version + description: | + Please provide your deepface version with calling the command `python -c "import deepface; print(deepface.__version__)"` in your terminal + placeholder: e.g. v0.0.90 + validations: + required: true + - type: input + id: python-version + attributes: + label: Python version + description: | + Please provide your python programming language's version with calling `python --version` in your terminal + placeholder: e.g. 3.8.5 + validations: + required: true + - type: input + id: os + attributes: + label: Operating System + description: | + Please provide your operation system's details + placeholder: e.g. Windows 10 or Ubuntu 20.04 + validations: + required: false + - type: textarea + id: dependencies + attributes: + label: Dependencies + description: | + Please provide python dependencies with calling `pip freeze` in your terminal, in particular tensorflow's and keras' versions + validations: + required: true + - type: textarea + id: repro-code + attributes: + label: Reproducible example + description: A ***minimal*** code sample which reproduces the issue + render: Python + validations: + required: true + - type: textarea + id: exception-message + attributes: + label: Relevant Log Output + description: Please share the exception message from your terminal if your program is failing + validations: + required: false + - type: textarea + id: expected + attributes: + label: Expected Result + description: What did you expect to happen? + validations: + required: false + - type: textarea + id: actual + attributes: + label: What happened instead? + description: What actually happened? + validations: + required: false + - type: textarea + id: additional + attributes: + label: Additional Info + description: | + Any additional info you'd like to provide. diff --git a/.github/ISSUE_TEMPLATE/02-request-feature.yaml b/.github/ISSUE_TEMPLATE/02-request-feature.yaml new file mode 100644 index 0000000000000000000000000000000000000000..2294bb26bc2a1609f287985d1e2d46b51ce19e54 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/02-request-feature.yaml @@ -0,0 +1,18 @@ +name: '✨ Request a New Feature' +description: 'Use this template to propose a new feature' +title: '[FEATURE]: ' +labels: + - 'enhancement' +body: + - type: textarea + id: description + attributes: + label: Description + description: Explain what your proposed feature would do and why this is useful. + validations: + required: true + - type: textarea + id: additional + attributes: + label: Additional Info + description: Any additional info you'd like to provide. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/03-documentation.yaml b/.github/ISSUE_TEMPLATE/03-documentation.yaml new file mode 100644 index 0000000000000000000000000000000000000000..0383a3c05aef038ff72f6719da96cc9b437ebb23 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/03-documentation.yaml @@ -0,0 +1,18 @@ +name: 'πŸ“ Documentation' +description: 'Use this template to add or improve docs' +title: '[DOC]: ' +labels: + - documentation +body: + - type: textarea + attributes: + label: Suggested Changes + description: What would you like to see happen in the docs? + validations: + required: true + - type: textarea + id: additional + attributes: + label: Additional Info + description: | + Any additional info you'd like to provide. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000000000000000000000000000000000..38d438588b226eb90ac9ff869c214a0c314e3ce3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Ask a question on StackOverflow + about: If you just want to ask a question, consider asking it on StackOverflow! + url: https://stackoverflow.com/questions/tagged/deepface \ No newline at end of file diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000000000000000000000000000000000000..2ff6a964aeb6dd689258c94b6d841043efdc04ac --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,13 @@ +## Tickets + +https://github.com/serengil/deepface/issues/XXX + +### What has been done + +With this PR, ... + +## How to test + +```shell +make lint && make test +``` \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000000000000000000000000000000000000..f90eba9a4be4fab934d6fc8e69a9a018c2a26c62 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,69 @@ +name: Tests and Linting + +on: + push: + paths: + - '.github/workflows/tests.yml' + - 'deepface/**' + - 'tests/**' + - 'api/**' + - 'requirements.txt' + - '.gitignore' + - 'setup.py' + pull_request: + paths: + - '.github/workflows/tests.yml' + - 'deepface/**' + - 'tests/**' + - 'api/**' + - 'requirements.txt' + - '.gitignore' + - 'setup.py' + +jobs: + unit-tests: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.8] + + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pytest + pip install . + + - name: Test with pytest + run: | + cd tests + python -m pytest . -s --disable-warnings + linting: + needs: unit-tests + + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.8] + + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pylint + pip install black + pip install . + + - name: Lint with pylint + run: | + pylint --fail-under=10 deepface/ 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..88ac31fa9641f63c697b7efb042b8ccd1fbea1a8 --- /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 15000 +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/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..908d7e78f2f6f3b0aacc1b2904854656c799e68f --- /dev/null +++ b/README.md @@ -0,0 +1,377 @@ +# deepface + +
+ +[![PyPI Downloads](https://static.pepy.tech/personalized-badge/deepface?period=total&units=international_system&left_color=grey&right_color=blue&left_text=pypi%20downloads)](https://pepy.tech/project/deepface) +[![Conda Downloads](https://img.shields.io/conda/dn/conda-forge/deepface?color=green&label=conda%20downloads)](https://anaconda.org/conda-forge/deepface) +[![Stars](https://img.shields.io/github/stars/serengil/deepface?color=yellow&style=flat&label=%E2%AD%90%20stars)](https://github.com/serengil/deepface/stargazers) +[![License](http://img.shields.io/:license-MIT-green.svg?style=flat)](https://github.com/serengil/deepface/blob/master/LICENSE) +[![Tests](https://github.com/serengil/deepface/actions/workflows/tests.yml/badge.svg)](https://github.com/serengil/deepface/actions/workflows/tests.yml) + +[![Blog](https://img.shields.io/:blog-sefiks.com-blue.svg?style=flat&logo=wordpress)](https://sefiks.com) +[![YouTube](https://img.shields.io/:youtube-@sefiks-red.svg?style=flat&logo=youtube)](https://www.youtube.com/@sefiks?sub_confirmation=1) +[![Twitter](https://img.shields.io/:follow-@serengil-blue.svg?style=flat&logo=twitter)](https://twitter.com/intent/user?screen_name=serengil) +[![Support me on Patreon](https://img.shields.io/endpoint.svg?url=https%3A%2F%2Fshieldsio-patreon.vercel.app%2Fapi%3Fusername%3Dserengil%26type%3Dpatrons&style=flat)](https://www.patreon.com/serengil?repo=deepface) +[![GitHub Sponsors](https://img.shields.io/github/sponsors/serengil?logo=GitHub&color=lightgray)](https://github.com/sponsors/serengil) + +[![DOI](http://img.shields.io/:DOI-10.1109/ASYU50717.2020.9259802-blue.svg?style=flat)](https://doi.org/10.1109/ASYU50717.2020.9259802) +[![DOI](http://img.shields.io/:DOI-10.1109/ICEET53442.2021.9659697-blue.svg?style=flat)](https://doi.org/10.1109/ICEET53442.2021.9659697) + +
+ +

+ +Deepface is a lightweight [face recognition](https://sefiks.com/2018/08/06/deep-face-recognition-with-keras/) and facial attribute analysis ([age](https://sefiks.com/2019/02/13/apparent-age-and-gender-prediction-in-keras/), [gender](https://sefiks.com/2019/02/13/apparent-age-and-gender-prediction-in-keras/), [emotion](https://sefiks.com/2018/01/01/facial-expression-recognition-with-keras/) and [race](https://sefiks.com/2019/11/11/race-and-ethnicity-prediction-in-keras/)) framework for python. It is a hybrid face recognition framework wrapping **state-of-the-art** models: [`VGG-Face`](https://sefiks.com/2018/08/06/deep-face-recognition-with-keras/), [`FaceNet`](https://sefiks.com/2018/09/03/face-recognition-with-facenet-in-keras/), [`OpenFace`](https://sefiks.com/2019/07/21/face-recognition-with-openface-in-keras/), [`DeepFace`](https://sefiks.com/2020/02/17/face-recognition-with-facebook-deepface-in-keras/), [`DeepID`](https://sefiks.com/2020/06/16/face-recognition-with-deepid-in-keras/), [`ArcFace`](https://sefiks.com/2020/12/14/deep-face-recognition-with-arcface-in-keras-and-python/), [`Dlib`](https://sefiks.com/2020/07/11/face-recognition-with-dlib-in-python/), `SFace` and `GhostFaceNet`. + +Experiments show that human beings have 97.53% accuracy on facial recognition tasks whereas those models already reached and passed that accuracy level. + +## Installation [![PyPI](https://img.shields.io/pypi/v/deepface.svg)](https://pypi.org/project/deepface/) [![Conda](https://img.shields.io/conda/vn/conda-forge/deepface.svg)](https://anaconda.org/conda-forge/deepface) + +The easiest way to install deepface is to download it from [`PyPI`](https://pypi.org/project/deepface/). It's going to install the library itself and its prerequisites as well. + +```shell +$ pip install deepface +``` + +Secondly, DeepFace is also available at [`Conda`](https://anaconda.org/conda-forge/deepface). You can alternatively install the package via conda. + +```shell +$ conda install -c conda-forge deepface +``` + +Thirdly, you can install deepface from its source code. + +```shell +$ git clone https://github.com/serengil/deepface.git +$ cd deepface +$ pip install -e . +``` + +Then you will be able to import the library and use its functionalities. + +```python +from deepface import DeepFace +``` + +**Facial Recognition** - [`Demo`](https://youtu.be/WnUVYQP4h44) + +A modern [**face recognition pipeline**](https://sefiks.com/2020/05/01/a-gentle-introduction-to-face-recognition-in-deep-learning/) consists of 5 common stages: [detect](https://sefiks.com/2020/08/25/deep-face-detection-with-opencv-in-python/), [align](https://sefiks.com/2020/02/23/face-alignment-for-face-recognition-in-python-within-opencv/), [normalize](https://sefiks.com/2020/11/20/facial-landmarks-for-face-recognition-with-dlib/), [represent](https://sefiks.com/2018/08/06/deep-face-recognition-with-keras/) and [verify](https://sefiks.com/2020/05/22/fine-tuning-the-threshold-in-face-recognition/). While Deepface handles all these common stages in the background, you don’t need to acquire in-depth knowledge about all the processes behind it. You can just call its verification, find or analysis function with a single line of code. + +**Face Verification** - [`Demo`](https://youtu.be/KRCvkNCOphE) + +This function verifies face pairs as same person or different persons. It expects exact image paths as inputs. Passing numpy or base64 encoded images is also welcome. Then, it is going to return a dictionary and you should check just its verified key. + +```python +result = DeepFace.verify(img1_path = "img1.jpg", img2_path = "img2.jpg") +``` + +

+ +**Face recognition** - [`Demo`](https://youtu.be/Hrjp-EStM_s) + +[Face recognition](https://sefiks.com/2020/05/25/large-scale-face-recognition-for-deep-learning/) requires applying face verification many times. Herein, deepface has an out-of-the-box find function to handle this action. It's going to look for the identity of input image in the database path and it will return list of pandas data frame as output. Meanwhile, facial embeddings of the facial database are stored in a pickle file to be searched faster in next time. Result is going to be the size of faces appearing in the source image. Besides, target images in the database can have many faces as well. + + +```python +dfs = DeepFace.find(img_path = "img1.jpg", db_path = "C:/workspace/my_db") +``` + +

+ +**Embeddings** + +Face recognition models basically represent facial images as multi-dimensional vectors. Sometimes, you need those embedding vectors directly. DeepFace comes with a dedicated representation function. Represent function returns a list of embeddings. Result is going to be the size of faces appearing in the image path. + +```python +embedding_objs = DeepFace.represent(img_path = "img.jpg") +``` + +This function returns an array as embedding. The size of the embedding array would be different based on the model name. For instance, VGG-Face is the default model and it represents facial images as 4096 dimensional vectors. + +```python +embedding = embedding_objs[0]["embedding"] +assert isinstance(embedding, list) +assert model_name == "VGG-Face" and len(embedding) == 4096 +``` + +Here, embedding is also [plotted](https://sefiks.com/2020/05/01/a-gentle-introduction-to-face-recognition-in-deep-learning/) with 4096 slots horizontally. Each slot is corresponding to a dimension value in the embedding vector and dimension value is explained in the colorbar on the right. Similar to 2D barcodes, vertical dimension stores no information in the illustration. + +

+ +**Face recognition models** - [`Demo`](https://youtu.be/i_MOwvhbLdI) + +Deepface is a **hybrid** face recognition package. It currently wraps many **state-of-the-art** face recognition models: [`VGG-Face`](https://sefiks.com/2018/08/06/deep-face-recognition-with-keras/) , [`FaceNet`](https://sefiks.com/2018/09/03/face-recognition-with-facenet-in-keras/), [`OpenFace`](https://sefiks.com/2019/07/21/face-recognition-with-openface-in-keras/), [`DeepFace`](https://sefiks.com/2020/02/17/face-recognition-with-facebook-deepface-in-keras/), [`DeepID`](https://sefiks.com/2020/06/16/face-recognition-with-deepid-in-keras/), [`ArcFace`](https://sefiks.com/2020/12/14/deep-face-recognition-with-arcface-in-keras-and-python/), [`Dlib`](https://sefiks.com/2020/07/11/face-recognition-with-dlib-in-python/), `SFace` and `GhostFaceNet`. The default configuration uses VGG-Face model. + +```python +models = [ + "VGG-Face", + "Facenet", + "Facenet512", + "OpenFace", + "DeepFace", + "DeepID", + "ArcFace", + "Dlib", + "SFace", + "GhostFaceNet", +] + +#face verification +result = DeepFace.verify(img1_path = "img1.jpg", + img2_path = "img2.jpg", + model_name = models[0] +) + +#face recognition +dfs = DeepFace.find(img_path = "img1.jpg", + db_path = "C:/workspace/my_db", + model_name = models[1] +) + +#embeddings +embedding_objs = DeepFace.represent(img_path = "img.jpg", + model_name = models[2] +) +``` + +

+ +FaceNet, VGG-Face, ArcFace and Dlib are [overperforming](https://youtu.be/i_MOwvhbLdI) ones based on experiments. You can find out the scores of those models below on [Labeled Faces in the Wild](https://sefiks.com/2020/08/27/labeled-faces-in-the-wild-for-face-recognition/) set declared by its creators. + +| Model | Declared LFW Score | +| -------------- | ------------------ | +| VGG-Face | 98.9% | +| Facenet | 99.2% | +| Facenet512 | 99.6% | +| OpenFace | 92.9% | +| DeepID | 97.4% | +| Dlib | 99.3 % | +| SFace | 99.5% | +| ArcFace | 99.5% | +| GhostFaceNet | 99.7% | +| *Human-beings* | *97.5%* | + +Conducting experiments with those models within DeepFace may reveal disparities compared to the original studies, owing to the adoption of distinct detection or normalization techniques. Furthermore, some models have been released solely with their backbones, lacking pre-trained weights. Thus, we are utilizing their re-implementations instead of the original pre-trained weights. + +**Similarity** + +Face recognition models are regular [convolutional neural networks](https://sefiks.com/2018/03/23/convolutional-autoencoder-clustering-images-with-neural-networks/) and they are responsible to represent faces as vectors. We expect that a face pair of same person should be [more similar](https://sefiks.com/2020/05/22/fine-tuning-the-threshold-in-face-recognition/) than a face pair of different persons. + +Similarity could be calculated by different metrics such as [Cosine Similarity](https://sefiks.com/2018/08/13/cosine-similarity-in-machine-learning/), Euclidean Distance and L2 form. The default configuration uses cosine similarity. + +```python +metrics = ["cosine", "euclidean", "euclidean_l2"] + +#face verification +result = DeepFace.verify(img1_path = "img1.jpg", + img2_path = "img2.jpg", + distance_metric = metrics[1] +) + +#face recognition +dfs = DeepFace.find(img_path = "img1.jpg", + db_path = "C:/workspace/my_db", + distance_metric = metrics[2] +) +``` + +Euclidean L2 form [seems](https://youtu.be/i_MOwvhbLdI) to be more stable than cosine and regular Euclidean distance based on experiments. + +**Facial Attribute Analysis** - [`Demo`](https://youtu.be/GT2UeN85BdA) + +Deepface also comes with a strong facial attribute analysis module including [`age`](https://sefiks.com/2019/02/13/apparent-age-and-gender-prediction-in-keras/), [`gender`](https://sefiks.com/2019/02/13/apparent-age-and-gender-prediction-in-keras/), [`facial expression`](https://sefiks.com/2018/01/01/facial-expression-recognition-with-keras/) (including angry, fear, neutral, sad, disgust, happy and surprise) and [`race`](https://sefiks.com/2019/11/11/race-and-ethnicity-prediction-in-keras/) (including asian, white, middle eastern, indian, latino and black) predictions. Result is going to be the size of faces appearing in the source image. + +```python +objs = DeepFace.analyze(img_path = "img4.jpg", + actions = ['age', 'gender', 'race', 'emotion'] +) +``` + +

+ +Age model got Β± 4.65 MAE; gender model got 97.44% accuracy, 96.29% precision and 95.05% recall as mentioned in its [tutorial](https://sefiks.com/2019/02/13/apparent-age-and-gender-prediction-in-keras/). + + +**Face Detectors** - [`Demo`](https://youtu.be/GZ2p2hj2H5k) + +Face detection and alignment are important early stages of a modern face recognition pipeline. Experiments show that just alignment increases the face recognition accuracy almost 1%. [`OpenCV`](https://sefiks.com/2020/02/23/face-alignment-for-face-recognition-in-python-within-opencv/), [`Ssd`](https://sefiks.com/2020/08/25/deep-face-detection-with-opencv-in-python/), [`Dlib`](https://sefiks.com/2020/07/11/face-recognition-with-dlib-in-python/), [`MtCnn`](https://sefiks.com/2020/09/09/deep-face-detection-with-mtcnn-in-python/), `Faster MTCNN`, [`RetinaFace`](https://sefiks.com/2021/04/27/deep-face-detection-with-retinaface-in-python/), [`MediaPipe`](https://sefiks.com/2022/01/14/deep-face-detection-with-mediapipe/), `Yolo`, `YuNet` and `CenterFace` detectors are wrapped in deepface. + +

+ +All deepface functions accept an optional detector backend input argument. You can switch among those detectors with this argument. OpenCV is the default detector. + +```python +backends = [ + 'opencv', + 'ssd', + 'dlib', + 'mtcnn', + 'fastmtcnn', + 'retinaface', + 'mediapipe', + 'yolov8', + 'yunet', + 'centerface', +] + +#face verification +obj = DeepFace.verify(img1_path = "img1.jpg", + img2_path = "img2.jpg", + detector_backend = backends[0] +) + +#face recognition +dfs = DeepFace.find(img_path = "img.jpg", + db_path = "my_db", + detector_backend = backends[1] +) + +#embeddings +embedding_objs = DeepFace.represent(img_path = "img.jpg", + detector_backend = backends[2] +) + +#facial analysis +demographies = DeepFace.analyze(img_path = "img4.jpg", + detector_backend = backends[3] +) + +#face detection and alignment +face_objs = DeepFace.extract_faces(img_path = "img.jpg", + detector_backend = backends[4] +) +``` + +Face recognition models are actually CNN models and they expect standard sized inputs. So, resizing is required before representation. To avoid deformation, deepface adds black padding pixels according to the target size argument after detection and alignment. + +

+ +[RetinaFace](https://sefiks.com/2021/04/27/deep-face-detection-with-retinaface-in-python/) and [MTCNN](https://sefiks.com/2020/09/09/deep-face-detection-with-mtcnn-in-python/) seem to overperform in detection and alignment stages but they are much slower. If the speed of your pipeline is more important, then you should use opencv or ssd. On the other hand, if you consider the accuracy, then you should use retinaface or mtcnn. + +The performance of RetinaFace is very satisfactory even in the crowd as seen in the following illustration. Besides, it comes with an incredible facial landmark detection performance. Highlighted red points show some facial landmarks such as eyes, nose and mouth. That's why, alignment score of RetinaFace is high as well. + +

+
The Yellow Angels - Fenerbahce Women's Volleyball Team +

+ +You can find out more about RetinaFace on this [repo](https://github.com/serengil/retinaface). + +**Real Time Analysis** - [`Demo`](https://youtu.be/-c9sSJcx6wI) + +You can run deepface for real time videos as well. Stream function will access your webcam and apply both face recognition and facial attribute analysis. The function starts to analyze a frame if it can focus a face sequentially 5 frames. Then, it shows results 5 seconds. + +```python +DeepFace.stream(db_path = "C:/User/Sefik/Desktop/database") +``` + +

+ +Even though face recognition is based on one-shot learning, you can use multiple face pictures of a person as well. You should rearrange your directory structure as illustrated below. + +```bash +user +β”œβ”€β”€ database +β”‚ β”œβ”€β”€ Alice +β”‚ β”‚ β”œβ”€β”€ Alice1.jpg +β”‚ β”‚ β”œβ”€β”€ Alice2.jpg +β”‚ β”œβ”€β”€ Bob +β”‚ β”‚ β”œβ”€β”€ Bob.jpg +``` + +**API** - [`Demo`](https://youtu.be/HeKCQ6U9XmI) + +DeepFace serves an API as well - see [`api folder`](https://github.com/serengil/deepface/tree/master/deepface/api/src) for more details. You can clone deepface source code and run the api with the following command. It will use gunicorn server to get a rest service up. In this way, you can call deepface from an external system such as mobile app or web. + +```shell +cd scripts +./service.sh +``` + +

+ +Face recognition, facial attribute analysis and vector representation functions are covered in the API. You are expected to call these functions as http post methods. Default service endpoints will be `http://localhost:5000/verify` for face recognition, `http://localhost:5000/analyze` for facial attribute analysis, and `http://localhost:5000/represent` for vector representation. You can pass input images as exact image paths on your environment, base64 encoded strings or images on web. [Here](https://github.com/serengil/deepface/tree/master/deepface/api/postman), you can find a postman project to find out how these methods should be called. + +**Dockerized Service** + +You can deploy the deepface api on a kubernetes cluster with docker. The following [shell script](https://github.com/serengil/deepface/blob/master/scripts/dockerize.sh) will serve deepface on `localhost:5000`. You need to re-configure the [Dockerfile](https://github.com/serengil/deepface/blob/master/Dockerfile) if you want to change the port. Then, even if you do not have a development environment, you will be able to consume deepface services such as verify and analyze. You can also access the inside of the docker image to run deepface related commands. Please follow the instructions in the [shell script](https://github.com/serengil/deepface/blob/master/scripts/dockerize.sh). + +```shell +cd scripts +./dockerize.sh +``` + +

+ +**Command Line Interface** - [`Demo`](https://youtu.be/PKKTAr3ts2s) + +DeepFace comes with a command line interface as well. You are able to access its functions in command line as shown below. The command deepface expects the function name as 1st argument and function arguments thereafter. + +```shell +#face verification +$ deepface verify -img1_path tests/dataset/img1.jpg -img2_path tests/dataset/img2.jpg + +#facial analysis +$ deepface analyze -img_path tests/dataset/img1.jpg +``` + +You can also run these commands if you are running deepface with docker. Please follow the instructions in the [shell script](https://github.com/serengil/deepface/blob/master/scripts/dockerize.sh#L17). + +## Contribution + +Pull requests are more than welcome! If you are planning to contribute a large patch, please create an issue first to get any upfront questions or design decisions out of the way first. + +Before creating a PR, you should run the unit tests and linting locally by running `make test && make lint` command. Once a PR sent, GitHub test workflow will be run automatically and unit test and linting jobs will be available in [GitHub actions](https://github.com/serengil/deepface/actions) before approval. + +## Support + +There are many ways to support a project - starring⭐️ the GitHub repo is just one πŸ™ + +You can also support this work on [Patreon](https://www.patreon.com/serengil?repo=deepface) or [GitHub Sponsors](https://github.com/sponsors/serengil). + +
+ + + +## Citation + +Please cite deepface in your publications if it helps your research - see [`CITATIONS`](https://github.com/serengil/deepface/blob/master/CITATION.md) for more details. Here are its BibTex entries: + +If you use deepface in your research for facial recogntion purposes, please cite 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://ieeexplore.ieee.org/document/9259802}, + organization = {IEEE} +} +``` + +If you use deepface in your research for facial attribute analysis purposes such as age, gender, emotion or ethnicity prediction, please cite this publication. + +```BibTeX +@inproceedings{serengil2021lightface, + title = {HyperExtended LightFace: A Facial Attribute Analysis Framework}, + author = {Serengil, Sefik Ilkin and Ozpinar, Alper}, + booktitle = {2021 International Conference on Engineering and Emerging Technologies (ICEET)}, + pages = {1-4}, + year = {2021}, + doi = {10.1109/ICEET53442.2021.9659697}, + url = {https://ieeexplore.ieee.org/document/9659697}, + organization = {IEEE} +} +``` + +Also, if you use deepface in your GitHub projects, please add `deepface` in the `requirements.txt`. + +## Licence + +DeepFace is licensed under the MIT License - see [`LICENSE`](https://github.com/serengil/deepface/blob/master/LICENSE) for more details. + +DeepFace wraps some external face recognition models: [VGG-Face](http://www.robots.ox.ac.uk/~vgg/software/vgg_face/), [Facenet](https://github.com/davidsandberg/facenet/blob/master/LICENSE.md), [OpenFace](https://github.com/iwantooxxoox/Keras-OpenFace/blob/master/LICENSE), [DeepFace](https://github.com/swghosh/DeepFace), [DeepID](https://github.com/Ruoyiran/DeepID/blob/master/LICENSE.md), [ArcFace](https://github.com/leondgarse/Keras_insightface/blob/master/LICENSE), [Dlib](https://github.com/davisking/dlib/blob/master/dlib/LICENSE.txt), [SFace](https://github.com/opencv/opencv_zoo/blob/master/models/face_recognition_sface/LICENSE) and [GhostFaceNet](https://github.com/HamadYA/GhostFaceNets/blob/main/LICENSE). Besides, age, gender and race / ethnicity models were trained on the backbone of VGG-Face with transfer learning. Similarly, DeepFace wraps many face detectors: [OpenCv](https://github.com/opencv/opencv/blob/4.x/LICENSE), [Ssd](https://github.com/opencv/opencv/blob/master/LICENSE), [Dlib](https://github.com/davisking/dlib/blob/master/LICENSE.txt), [MtCnn](https://github.com/ipazc/mtcnn/blob/master/LICENSE), [Fast MtCnn](https://github.com/timesler/facenet-pytorch/blob/master/LICENSE.md), [RetinaFace](https://github.com/serengil/retinaface/blob/master/LICENSE), [MediaPipe](https://github.com/google/mediapipe/blob/master/LICENSE), [YuNet](https://github.com/ShiqiYu/libfacedetection/blob/master/LICENSE), [Yolo](https://github.com/derronqi/yolov8-face/blob/main/LICENSE) and [CenterFace](https://github.com/Star-Clouds/CenterFace/blob/master/LICENSE). License types will be inherited when you intend to utilize those models. Please check the license types of those models for production purposes. + +DeepFace [logo](https://thenounproject.com/term/face-recognition/2965879/) is created by [Adrien Coquet](https://thenounproject.com/coquet_adrien/) and it is licensed under [Creative Commons: By Attribution 3.0 License](https://creativecommons.org/licenses/by/3.0/). 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..cb29852c35f70cd6c2f909ec2972c41a29dd1d5b --- /dev/null +++ b/deepface/DeepFace.py @@ -0,0 +1,585 @@ +# common dependencies +import os +import warnings +import logging +from typing import Any, Dict, List, Union, Optional +from deepface.commons.os_path import os_path + +# this has to be set before importing tensorflow +os.environ["TF_USE_LEGACY_KERAS"] = "1" + +# pylint: disable=wrong-import-position + +# 3rd party dependencies +import numpy as np +import pandas as pd +import tensorflow as tf + +# package dependencies +from deepface.commons import package_utils, folder_utils +from deepface.commons import logger as log +from deepface.modules import ( + modeling, + representation, + verification, + recognition, + demography, + detection, + streaming, + preprocessing, + cloudservice, +) +from deepface import __version__ + +logger = log.get_singletonish_logger() + +# ----------------------------------- +# configurations for dependencies + +# users should install tf_keras package if they are using tf 2.16 or later versions +package_utils.validate_for_keras3() + +warnings.filterwarnings("ignore") +os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3" +tf_version = package_utils.get_tf_major_version() +if tf_version == 2: + tf.get_logger().setLevel(logging.ERROR) +# ----------------------------------- + +# create required folders if necessary to store model weights +folder_utils.initialize_folder() + + +def build_model(model_name: str) -> Any: + """ + This function builds a deepface model + Args: + model_name (string): face recognition or facial attribute model + VGG-Face, Facenet, OpenFace, DeepFace, DeepID for face recognition + Age, Gender, Emotion, Race for facial attributes + Returns: + built_model + """ + return modeling.build_model(model_name=model_name) + + +def verify( + img1_path: Union[str, np.ndarray, List[float]], + img2_path: Union[str, np.ndarray, List[float]], + model_name: str = "VGG-Face", + detector_backend: str = "opencv", + distance_metric: str = "cosine", + enforce_detection: bool = True, + align: bool = True, + expand_percentage: int = 0, + normalization: str = "base", + silent: bool = False, +) -> Dict[str, Any]: + """ + Verify if an image pair represents the same person or different persons. + Args: + img1_path (str or np.ndarray or List[float]): Path to the first image. + Accepts exact image path as a string, numpy array (BGR), base64 encoded images + or pre-calculated embeddings. + + img2_path (str or np.ndarray or List[float]): Path to the second image. + Accepts exact image path as a string, numpy array (BGR), base64 encoded images + or pre-calculated embeddings. + + model_name (str): Model for face recognition. Options: VGG-Face, Facenet, Facenet512, + OpenFace, DeepFace, DeepID, Dlib, ArcFace, SFace and GhostFaceNet (default is VGG-Face). + + detector_backend (string): face detector backend. Options: 'opencv', 'retinaface', + 'mtcnn', 'ssd', 'dlib', 'mediapipe', 'yolov8', 'centerface' or 'skip' + (default is opencv). + + distance_metric (string): Metric for measuring similarity. Options: 'cosine', + 'euclidean', 'euclidean_l2' (default is cosine). + + enforce_detection (boolean): If no face is detected in an image, raise an exception. + Set to False to avoid the exception for low-resolution images (default is True). + + align (bool): Flag to enable face alignment (default is True). + + expand_percentage (int): expand detected facial area with a percentage (default is 0). + + normalization (string): Normalize the input image before feeding it to the model. + Options: base, raw, Facenet, Facenet2018, VGGFace, VGGFace2, ArcFace (default is base) + + silent (boolean): Suppress or allow some log messages for a quieter analysis process + (default is False). + + Returns: + result (dict): A dictionary containing verification results with following keys. + + - 'verified' (bool): Indicates whether the images represent the same person (True) + or different persons (False). + + - 'distance' (float): The distance measure between the face vectors. + A lower distance indicates higher similarity. + + - 'max_threshold_to_verify' (float): The maximum threshold used for verification. + If the distance is below this threshold, the images are considered a match. + + - 'model' (str): The chosen face recognition model. + + - 'distance_metric' (str): The chosen similarity metric for measuring distances. + + - 'facial_areas' (dict): Rectangular regions of interest for faces in both images. + - 'img1': {'x': int, 'y': int, 'w': int, 'h': int} + Region of interest for the first image. + - 'img2': {'x': int, 'y': int, 'w': int, 'h': int} + Region of interest for the second image. + + - 'time' (float): Time taken for the verification process in seconds. + """ + + return verification.verify( + img1_path=img1_path, + img2_path=img2_path, + model_name=model_name, + detector_backend=detector_backend, + distance_metric=distance_metric, + enforce_detection=enforce_detection, + align=align, + expand_percentage=expand_percentage, + normalization=normalization, + silent=silent, + ) + + +def analyze( + img_path: Union[str, np.ndarray], + actions: Union[tuple, list] = ("emotion", "age", "gender", "race"), + enforce_detection: bool = True, + detector_backend: str = "opencv", + align: bool = True, + expand_percentage: int = 0, + silent: bool = False, +) -> List[Dict[str, Any]]: + """ + Analyze facial attributes such as age, gender, emotion, and race in the provided image. + Args: + img_path (str or np.ndarray): The exact path to the image, a numpy array in BGR format, + or a base64 encoded image. If the source image contains multiple faces, the result will + include information for each detected face. + + actions (tuple): Attributes to analyze. The default is ('age', 'gender', 'emotion', 'race'). + You can exclude some of these attributes from the analysis if needed. + + enforce_detection (boolean): If no face is detected in an image, raise an exception. + Set to False to avoid the exception for low-resolution images (default is True). + + detector_backend (string): face detector backend. Options: 'opencv', 'retinaface', + 'mtcnn', 'ssd', 'dlib', 'mediapipe', 'yolov8', 'centerface' or 'skip' + (default is opencv). + + distance_metric (string): Metric for measuring similarity. Options: 'cosine', + 'euclidean', 'euclidean_l2' (default is cosine). + + align (boolean): Perform alignment based on the eye positions (default is True). + + expand_percentage (int): expand detected facial area with a percentage (default is 0). + + silent (boolean): Suppress or allow some log messages for a quieter analysis process + (default is False). + + Returns: + results (List[Dict[str, Any]]): A list of dictionaries, where each dictionary represents + the analysis results for a detected face. Each dictionary in the list contains the + following keys: + + - 'region' (dict): Represents the rectangular region of the detected face in the image. + - 'x': x-coordinate of the top-left corner of the face. + - 'y': y-coordinate of the top-left corner of the face. + - 'w': Width of the detected face region. + - 'h': Height of the detected face region. + + - 'age' (float): Estimated age of the detected face. + + - 'face_confidence' (float): Confidence score for the detected face. + Indicates the reliability of the face detection. + + - 'dominant_gender' (str): The dominant gender in the detected face. + Either "Man" or "Woman". + + - 'gender' (dict): Confidence scores for each gender category. + - 'Man': Confidence score for the male gender. + - 'Woman': Confidence score for the female gender. + + - 'dominant_emotion' (str): The dominant emotion in the detected face. + Possible values include "sad," "angry," "surprise," "fear," "happy," + "disgust," and "neutral" + + - 'emotion' (dict): Confidence scores for each emotion category. + - 'sad': Confidence score for sadness. + - 'angry': Confidence score for anger. + - 'surprise': Confidence score for surprise. + - 'fear': Confidence score for fear. + - 'happy': Confidence score for happiness. + - 'disgust': Confidence score for disgust. + - 'neutral': Confidence score for neutrality. + + - 'dominant_race' (str): The dominant race in the detected face. + Possible values include "indian," "asian," "latino hispanic," + "black," "middle eastern," and "white." + + - 'race' (dict): Confidence scores for each race category. + - 'indian': Confidence score for Indian ethnicity. + - 'asian': Confidence score for Asian ethnicity. + - 'latino hispanic': Confidence score for Latino/Hispanic ethnicity. + - 'black': Confidence score for Black ethnicity. + - 'middle eastern': Confidence score for Middle Eastern ethnicity. + - 'white': Confidence score for White ethnicity. + """ + return demography.analyze( + img_path=img_path, + actions=actions, + enforce_detection=enforce_detection, + detector_backend=detector_backend, + align=align, + expand_percentage=expand_percentage, + silent=silent, + ) + + +def find( + img_path: Union[str, np.ndarray], + db_path: str, + model_name: str = "VGG-Face", + distance_metric: str = "cosine", + enforce_detection: bool = True, + detector_backend: str = "opencv", + align: bool = True, + expand_percentage: int = 0, + threshold: Optional[float] = None, + normalization: str = "base", + silent: bool = False, +) -> List[pd.DataFrame]: + """ + Identify individuals in a database + Args: + img_path (str or np.ndarray): The exact path to the image, a numpy array in BGR format, + or a base64 encoded image. If the source image contains multiple faces, the result will + include information for each detected face. + + db_path (string): Path to the folder containing image files. All detected faces + in the database will be considered in the decision-making process. + + model_name (str): Model for face recognition. Options: VGG-Face, Facenet, Facenet512, + OpenFace, DeepFace, DeepID, Dlib, ArcFace, SFace and GhostFaceNet (default is VGG-Face). + + distance_metric (string): Metric for measuring similarity. Options: 'cosine', + 'euclidean', 'euclidean_l2' (default is cosine). + + enforce_detection (boolean): If no face is detected in an image, raise an exception. + Set to False to avoid the exception for low-resolution images (default is True). + + detector_backend (string): face detector backend. Options: 'opencv', 'retinaface', + 'mtcnn', 'ssd', 'dlib', 'mediapipe', 'yolov8', 'centerface' or 'skip' + (default is opencv). + + align (boolean): Perform alignment based on the eye positions (default is True). + + expand_percentage (int): expand detected facial area with a percentage (default is 0). + + threshold (float): Specify a threshold to determine whether a pair represents the same + person or different individuals. This threshold is used for comparing distances. + If left unset, default pre-tuned threshold values will be applied based on the specified + model name and distance metric (default is None). + + normalization (string): Normalize the input image before feeding it to the model. + Options: base, raw, Facenet, Facenet2018, VGGFace, VGGFace2, ArcFace (default is base). + + silent (boolean): Suppress or allow some log messages for a quieter analysis process + (default is False). + + Returns: + results (List[pd.DataFrame]): A list of pandas dataframes. Each dataframe corresponds + to the identity information for an individual detected in the source image. + The DataFrame columns include: + + - 'identity': Identity label of the detected individual. + + - 'target_x', 'target_y', 'target_w', 'target_h': Bounding box coordinates of the + target face in the database. + + - 'source_x', 'source_y', 'source_w', 'source_h': Bounding box coordinates of the + detected face in the source image. + + - 'threshold': threshold to determine a pair whether same person or different persons + + - 'distance': Similarity score between the faces based on the + specified model and distance metric + """ + return recognition.find( + img_path=img_path, + db_path=db_path, + model_name=model_name, + distance_metric=distance_metric, + enforce_detection=enforce_detection, + detector_backend=detector_backend, + align=align, + expand_percentage=expand_percentage, + threshold=threshold, + normalization=normalization, + silent=silent, + ) + + +def represent( + img_path: Union[str, np.ndarray], + model_name: str = "VGG-Face", + enforce_detection: bool = True, + detector_backend: str = "opencv", + align: bool = True, + expand_percentage: int = 0, + normalization: str = "base", +) -> List[Dict[str, Any]]: + """ + Represent facial images as multi-dimensional vector embeddings. + + Args: + img_path (str or np.ndarray): The exact path to the image, a numpy array in BGR format, + or a base64 encoded image. If the source image contains multiple faces, the result will + include information for each detected face. + + model_name (str): Model for face recognition. Options: VGG-Face, Facenet, Facenet512, + OpenFace, DeepFace, DeepID, Dlib, ArcFace, SFace and GhostFaceNet + (default is VGG-Face.). + + enforce_detection (boolean): If no face is detected in an image, raise an exception. + Default is True. Set to False to avoid the exception for low-resolution images + (default is True). + + detector_backend (string): face detector backend. Options: 'opencv', 'retinaface', + 'mtcnn', 'ssd', 'dlib', 'mediapipe', 'yolov8', 'centerface' or 'skip' + (default is opencv). + + align (boolean): Perform alignment based on the eye positions (default is True). + + expand_percentage (int): expand detected facial area with a percentage (default is 0). + + normalization (string): Normalize the input image before feeding it to the model. + Default is base. Options: base, raw, Facenet, Facenet2018, VGGFace, VGGFace2, ArcFace + (default is base). + + Returns: + results (List[Dict[str, Any]]): A list of dictionaries, each containing the + following fields: + + - embedding (List[float]): Multidimensional vector representing facial features. + The number of dimensions varies based on the reference model + (e.g., FaceNet returns 128 dimensions, VGG-Face returns 4096 dimensions). + + - facial_area (dict): Detected facial area by face detection in dictionary format. + Contains 'x' and 'y' as the left-corner point, and 'w' and 'h' + as the width and height. If `detector_backend` is set to 'skip', it represents + the full image area and is nonsensical. + + - face_confidence (float): Confidence score of face detection. If `detector_backend` is set + to 'skip', the confidence will be 0 and is nonsensical. + """ + return representation.represent( + img_path=img_path, + model_name=model_name, + enforce_detection=enforce_detection, + detector_backend=detector_backend, + align=align, + expand_percentage=expand_percentage, + normalization=normalization, + ) + + +def stream( + db_path: str = "", + model_name: str = "VGG-Face", + detector_backend: str = "opencv", + distance_metric: str = "cosine", + enable_face_analysis: bool = True, + source: Any = 0, + time_threshold: int = 5, + frame_threshold: int = 5, +) -> None: + """ + Run real time face recognition and facial attribute analysis + + Args: + db_path (string): Path to the folder containing image files. All detected faces + in the database will be considered in the decision-making process. + + model_name (str): Model for face recognition. Options: VGG-Face, Facenet, Facenet512, + OpenFace, DeepFace, DeepID, Dlib, ArcFace, SFace and GhostFaceNet (default is VGG-Face). + + detector_backend (string): face detector backend. Options: 'opencv', 'retinaface', + 'mtcnn', 'ssd', 'dlib', 'mediapipe', 'yolov8', 'centerface' or 'skip' + (default is opencv). + + distance_metric (string): Metric for measuring similarity. Options: 'cosine', + 'euclidean', 'euclidean_l2' (default is cosine). + + enable_face_analysis (bool): Flag to enable face analysis (default is True). + + source (Any): The source for the video stream (default is 0, which represents the + default camera). + + time_threshold (int): The time threshold (in seconds) for face recognition (default is 5). + + frame_threshold (int): The frame threshold for face recognition (default is 5). + Returns: + None + """ + + time_threshold = max(time_threshold, 1) + frame_threshold = max(frame_threshold, 1) + + streaming.analysis( + db_path=db_path, + model_name=model_name, + detector_backend=detector_backend, + distance_metric=distance_metric, + enable_face_analysis=enable_face_analysis, + source=source, + time_threshold=time_threshold, + frame_threshold=frame_threshold, + ) + + +def extract_faces( + img_path: Union[str, np.ndarray], + detector_backend: str = "opencv", + enforce_detection: bool = True, + align: bool = True, + expand_percentage: int = 0, + grayscale: bool = False, +) -> List[Dict[str, Any]]: + """ + Extract faces from a given image + + Args: + img_path (str or np.ndarray): Path to the first image. Accepts exact image path + as a string, numpy array (BGR), or base64 encoded images. + + detector_backend (string): face detector backend. Options: 'opencv', 'retinaface', + 'mtcnn', 'ssd', 'dlib', 'mediapipe', 'yolov8', 'centerface' or 'skip' + (default is opencv). + + enforce_detection (boolean): If no face is detected in an image, raise an exception. + Set to False to avoid the exception for low-resolution images (default is True). + + align (bool): Flag to enable face alignment (default is True). + + expand_percentage (int): expand detected facial area with a percentage (default is 0). + + grayscale (boolean): Flag to convert the image to grayscale before + processing (default is False). + + Returns: + results (List[Dict[str, Any]]): A list of dictionaries, where each dictionary contains: + + - "face" (np.ndarray): The detected face as a NumPy array. + + - "facial_area" (Dict[str, Any]): The detected face's regions as a dictionary containing: + - keys 'x', 'y', 'w', 'h' with int values + - keys 'left_eye', 'right_eye' with a tuple of 2 ints as values. left and right eyes + are eyes on the left and right respectively with respect to the person itself + instead of observer. + + - "confidence" (float): The confidence score associated with the detected face. + """ + + return detection.extract_faces( + img_path=img_path, + detector_backend=detector_backend, + enforce_detection=enforce_detection, + align=align, + expand_percentage=expand_percentage, + grayscale=grayscale, + ) + + +def cli() -> None: + """ + command line interface function will be offered in this block + """ + import fire + + fire.Fire() + + +# deprecated function(s) + + +def detectFace( + img_path: Union[str, np.ndarray], + target_size: tuple = (224, 224), + detector_backend: str = "opencv", + enforce_detection: bool = True, + align: bool = True, +) -> Union[np.ndarray, None]: + """ + Deprecated face detection function. Use extract_faces for same functionality. + + Args: + img_path (str or np.ndarray): Path to the first image. Accepts exact image path + as a string, numpy array (BGR), or base64 encoded images. + + target_size (tuple): final shape of facial image. black pixels will be + added to resize the image (default is (224, 224)). + + detector_backend (string): face detector backend. Options: 'opencv', 'retinaface', + 'mtcnn', 'ssd', 'dlib', 'mediapipe', 'yolov8', 'centerface' or 'skip' + (default is opencv). + + enforce_detection (boolean): If no face is detected in an image, raise an exception. + Set to False to avoid the exception for low-resolution images (default is True). + + align (bool): Flag to enable face alignment (default is True). + + Returns: + img (np.ndarray): detected (and aligned) facial area image as numpy array + """ + logger.warn("Function detectFace is deprecated. Use extract_faces instead.") + face_objs = extract_faces( + img_path=img_path, + detector_backend=detector_backend, + enforce_detection=enforce_detection, + align=align, + grayscale=False, + ) + extracted_face = None + if len(face_objs) > 0: + extracted_face = face_objs[0]["face"] + extracted_face = preprocessing.resize_image(img=extracted_face, target_size=target_size) + return extracted_face + + +def sync_datasets(): + # Set the local directories + base_dir = os_path.get_main_directory() + + missing_dir = os.path.join(base_dir, 'mafqoud', 'images', 'missing_people') + founded_dir = os.path.join(base_dir, 'mafqoud', 'images', 'founded_people') + + # Ensure the directories exist + os.makedirs(missing_dir, exist_ok=True) + os.makedirs(founded_dir, exist_ok=True) + + missing_people = cloudservice.sync_folder('missing_people', missing_dir) + + founded_people = cloudservice.sync_folder('founded_people', founded_dir) + +def delete_pkls(): + # Set the local directories + base_dir = os_path.get_main_directory() + + missing_dir = os.path.join(base_dir, 'mafqoud', 'images', 'missing_people') + founded_dir = os.path.join(base_dir, 'mafqoud', 'images', 'founded_people') + + # Ensure the directories exist + os.makedirs(missing_dir, exist_ok=True) + os.makedirs(founded_dir, exist_ok=True) + + cloudservice.delete_pkl_files(missing_dir) + cloudservice.delete_pkl_files(founded_dir) + diff --git a/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..eb5d942d7e72321dc9f8f0dde055ef8d318fc4fb --- /dev/null +++ b/deepface/api/src/modules/core/routes.py @@ -0,0 +1,207 @@ +from flask import Blueprint, request , jsonify +from deepface.api.src.modules.core import service +from deepface.commons.logger import Logger +from deepface.commons.os_path import os_path +import json +import os + +logger = Logger(module="api/src/routes.py") + +blueprint = Blueprint("routes", __name__) + + +@blueprint.route("/") +def home(): + return "

Welcome to DeepFace API!

" + + +@blueprint.route("/represent", methods=["POST"]) +def represent(): + input_args = request.get_json() + + if input_args is None: + return {"message": "empty input set passed"} + + img_path = input_args.get("img") or input_args.get("img_path") + if img_path is None: + return {"message": "you must pass img_path input"} + + model_name = input_args.get("model_name", "VGG-Face") + detector_backend = input_args.get("detector_backend", "opencv") + enforce_detection = input_args.get("enforce_detection", True) + align = input_args.get("align", True) + + obj = service.represent( + img_path=img_path, + model_name=model_name, + detector_backend=detector_backend, + enforce_detection=enforce_detection, + align=align, + ) + + logger.debug(obj) + + return obj + + +@blueprint.route("/verify", methods=["POST"]) +def verify(): + input_args = request.get_json() + + if input_args is None: + return {"message": "empty input set passed"} + + img1_path = input_args.get("img1") or input_args.get("img1_path") + img2_path = input_args.get("img2") or input_args.get("img2_path") + + if img1_path is None: + return {"message": "you must pass img1_path input"} + + if img2_path is None: + return {"message": "you must pass img2_path input"} + + model_name = input_args.get("model_name", "VGG-Face") + detector_backend = input_args.get("detector_backend", "opencv") + enforce_detection = input_args.get("enforce_detection", True) + distance_metric = input_args.get("distance_metric", "cosine") + align = input_args.get("align", True) + + verification = service.verify( + img1_path=img1_path, + img2_path=img2_path, + model_name=model_name, + detector_backend=detector_backend, + distance_metric=distance_metric, + align=align, + enforce_detection=enforce_detection, + ) + + logger.debug(verification) + + return verification + + +@blueprint.route("/analyze", methods=["POST"]) +def analyze(): + input_args = request.get_json() + + if input_args is None: + return {"message": "empty input set passed"} + + img_path = input_args.get("img") or input_args.get("img_path") + if img_path is None: + return {"message": "you must pass img_path input"} + + detector_backend = input_args.get("detector_backend", "opencv") + enforce_detection = input_args.get("enforce_detection", True) + align = input_args.get("align", True) + actions = input_args.get("actions", ["age", "gender", "emotion", "race"]) + + demographies = service.analyze( + img_path=img_path, + actions=actions, + detector_backend=detector_backend, + enforce_detection=enforce_detection, + align=align, + ) + + logger.debug(demographies) + + return demographies + +@blueprint.route("/find", methods=["POST"]) +def find(): + input_args = request.get_json() + + if input_args is None: + response = jsonify({'error': 'empty input set passed'}) + response.status_code = 500 + return response + + img_name = input_args.get("img") or input_args.get("img_name") + img_type = input_args.get("img_type") + + if img_name is None: + response = jsonify({'error': 'you must pass img_name input'}) + response.status_code = 404 + return response + + if img_type == "missing" or img_type == "missing_person" or img_type == "missing_people" or img_type == "missing person" or img_type == "missing people" : + + img_path = os.path.join( os_path.get_main_directory() , 'mafqoud' , 'images' , "missing_people" , img_name) + db_path = os.path.join( os_path.get_main_directory() , 'mafqoud' , 'images' , "founded_people") + + elif img_type == "founded" or img_type == "founded_person" or img_type == "founded_people" or img_type == "founded person" or img_type == "founded people" : + + img_path = os.path.join( os_path.get_main_directory() , 'mafqoud' , 'images' , "founded_people" , img_name) + db_path = os.path.join( os_path.get_main_directory() , 'mafqoud' , 'images' , "missing_people") + + else : + + response = jsonify({'error': 'the type of the image is not correct and it should be one of those : ( missing , missing_people , missing_people , missing person , missing people ) or ( founded , founded_people , founded_people , founded person , founded people )'}) + response.status_code = 400 + return response + + print(img_path) + if not os.path.exists(img_path) or not os.path.isfile(img_path): + # If the image does not exist, return a JSON response with status code 404 + response = jsonify({'error': 'Image not found'}) + response.status_code = 404 + return response + + + model_name = input_args.get("model_name", "Facenet512") + detector_backend = input_args.get("detector_backend", "mtcnn") + enforce_detection = input_args.get("enforce_detection", True) + distance_metric = input_args.get("distance_metric", "euclidean_l2") + align = input_args.get("align", True) + + if img_name is None: + return {"message": "you must pass img1_path input"} + + if db_path is None: + dataset_path = os.path.join(path.get_parent_path(), 'dataset') + if img_type == "missing_person": + img_path = os.path.join(dataset_path, 'missing_people', img_name) + db_path = os.path.join(dataset_path, 'founded_people') + elif img_type == "founded_people": + img_path = os.path.join(dataset_path, 'founded_people', img_name) + db_path = os.path.join(dataset_path, 'missing_people') + + results = service.find( + img_path=img_path, + db_path=db_path, + model_name=model_name, + detector_backend=detector_backend, + distance_metric=distance_metric, + align=align, + enforce_detection=enforce_detection, + ) + + # Calculate similarity_percentage for each row + results[0]['similarity_percentage'] =100 - ((results[0]['distance'] / results[0]['threshold']) * 100) + + data = [] + for _, row in results[0].iterrows(): + data.append({ + "identity": row['identity'], + "similarity_percentage": row['similarity_percentage'] + }) + + json_data = json.dumps(data, indent=4) + + + logger.debug(json_data) + return json_data + + +@blueprint.route("/dataset/sync", methods=["GET"]) +def sync_datasets(): + result = service.sync_datasets() + return jsonify(result) + + +@blueprint.route("/delete/pkls", methods=["GET"]) +def delete_pkls(): + result = service.delete_pkls() + return jsonify(result) \ No newline at end of file diff --git a/deepface/api/src/modules/core/service.py b/deepface/api/src/modules/core/service.py new file mode 100644 index 0000000000000000000000000000000000000000..121a8b842292a305332094b3e333cf21a5ddc94e --- /dev/null +++ b/deepface/api/src/modules/core/service.py @@ -0,0 +1,84 @@ +from deepface import DeepFace + +# pylint: disable=broad-except + + +def represent(img_path, model_name, detector_backend, enforce_detection, align): + try: + result = {} + embedding_objs = DeepFace.represent( + img_path=img_path, + model_name=model_name, + detector_backend=detector_backend, + enforce_detection=enforce_detection, + align=align, + ) + result["results"] = embedding_objs + return result + except Exception as err: + return {"error": f"Exception while representing: {str(err)}"}, 400 + + +def verify( + img1_path, img2_path, model_name, detector_backend, distance_metric, enforce_detection, align +): + try: + obj = DeepFace.verify( + img1_path=img1_path, + img2_path=img2_path, + model_name=model_name, + detector_backend=detector_backend, + distance_metric=distance_metric, + align=align, + enforce_detection=enforce_detection, + ) + return obj + except Exception as err: + return {"error": f"Exception while verifying: {str(err)}"}, 400 + + +def analyze(img_path, actions, detector_backend, enforce_detection, align): + try: + result = {} + demographies = DeepFace.analyze( + img_path=img_path, + actions=actions, + detector_backend=detector_backend, + enforce_detection=enforce_detection, + align=align, + silent=True, + ) + result["results"] = demographies + return result + except Exception as err: + return {"error": f"Exception while analyzing: {str(err)}"}, 400 + +def find(img_path, db_path, model_name, detector_backend, distance_metric, enforce_detection, align): + try: + obj = DeepFace.find( + img_path=img_path, + db_path=db_path, + model_name=model_name, + detector_backend=detector_backend, + distance_metric=distance_metric, + align=align, + enforce_detection=enforce_detection, + ) + return obj + except Exception as err: + return {"error": f"Exception while Findind: {str(err)}"}, 400 + + +def sync_datasets(): + try: + DeepFace.sync_datasets() + return {'data': 'synced successfully'}, 200 + except Exception as e: + return {'error': str(e)}, 400 + +def delete_pkls(): + try: + DeepFace.delete_pkls() + return {'data': 'pkl files deleted successfully'}, 200 + except Exception as e: + return {'error': str(e)}, 400 \ No newline at end of file diff --git a/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..7a56708491b226a42e026c701c499607e225ba17 --- /dev/null +++ b/deepface/modules/cloudservice.py @@ -0,0 +1,154 @@ +# from flask import Flask, jsonify +# import cloudinary +# import cloudinary.api +# import cloudinary.uploader +# import os +# import glob +# import requests +# from dotenv import load_dotenv +# from deepface.commons.logger import Logger + +# logger = Logger(module="modules/cloudservice.py") + +# load_dotenv() + +# # Configure Cloudinary +# cloudinary.config( +# cloud_name=os.getenv('CLOUDINARY_CLOUD_NAME'), +# api_key=os.getenv('CLOUDINARY_API_KEY'), +# api_secret=os.getenv('CLOUDINARY_API_SECRET') +# ) + + +# def fetch_cloudinary_images(folder_name): + +# resources = [] + +# res = cloudinary.api.resources(type='upload', resource_type='image', prefix=f'mafqoud/images/{folder_name}') +# resources.extend(res.get('resources', [])) +# return resources + +# def download_image(url, local_path): +# response = requests.get(url, stream=True) +# if response.status_code == 200: +# with open(local_path, 'wb') as file: +# for chunk in response: +# file.write(chunk) + +# def sync_folder(folder_name, local_dir): +# cloudinary_images = fetch_cloudinary_images(folder_name) +# cloudinary_urls = {img['secure_url']: img['public_id'] for img in cloudinary_images} + +# # Download new images and track downloaded image paths +# downloaded_paths = [] +# for img in cloudinary_images: +# url = img['secure_url'] +# public_id = img['public_id'] +# file_name = url.split('/')[-1] # Get the actual file name +# local_path = os.path.join(local_dir, file_name) +# print(local_path) + +# if not os.path.exists(local_path): +# download_image(url, local_path) +# downloaded_paths.append(local_path) + +# # Remove old images +# local_images = [os.path.join(local_dir, f) for f in os.listdir(local_dir) if os.path.isfile(os.path.join(local_dir, f))] +# for local_path in local_images: +# if local_path not in downloaded_paths and (local_path.endswith('.jpg') or local_path.endswith('.jpeg') or local_path.endswith('.png')): +# os.remove(local_path) + +# def delete_pkl_files(directory): +# """Delete all .pkl files in the specified directory.""" +# pkl_files = glob.glob(os.path.join(directory, '*.pkl')) +# for pkl_file in pkl_files: +# os.remove(pkl_file) + + +from flask import Flask, jsonify +import cloudinary +import cloudinary.api +import cloudinary.uploader +import os +import glob +import requests +from dotenv import load_dotenv +from deepface.commons.logger import Logger +from deepface import DeepFace # Assuming this is the correct import for training + +logger = Logger(module="modules/cloudservice.py") + +load_dotenv() + +# Configure Cloudinary +cloudinary.config( + cloud_name=os.getenv('CLOUDINARY_CLOUD_NAME'), + api_key=os.getenv('CLOUDINARY_API_KEY'), + api_secret=os.getenv('CLOUDINARY_API_SECRET') +) + + +def fetch_cloudinary_images(folder_name): + resources = [] + next_cursor = None + + while True: + if next_cursor: + res = cloudinary.api.resources( + type='upload', + resource_type='image', + prefix=f'mafqoud/images/{folder_name}', + max_results=500, + next_cursor=next_cursor + ) + else: + res = cloudinary.api.resources( + type='upload', + resource_type='image', + prefix=f'mafqoud/images/{folder_name}', + max_results=500 + ) + + resources.extend(res.get('resources', [])) + next_cursor = res.get('next_cursor') + + if not next_cursor: + break + + return resources + +def download_image(url, local_path): + response = requests.get(url, stream=True) + if response.status_code == 200: + with open(local_path, 'wb') as file: + for chunk in response: + file.write(chunk) + +def sync_folder(folder_name, local_dir): + cloudinary_images = fetch_cloudinary_images(folder_name) + cloudinary_urls = {img['secure_url']: img['public_id'] for img in cloudinary_images} + + # Download new images and track downloaded image paths + downloaded_paths = [] + for img in cloudinary_images: + url = img['secure_url'] + public_id = img['public_id'] + file_name = url.split('/')[-1] # Get the actual file name + local_path = os.path.join(local_dir, file_name) + print(local_path) + + if not os.path.exists(local_path): + download_image(url, local_path) + downloaded_paths.append(local_path) + + # Remove old images + local_images = [os.path.join(local_dir, f) for f in os.listdir(local_dir) if os.path.isfile(os.path.join(local_dir, f))] + for local_path in local_images: + if local_path not in downloaded_paths and (local_path.endswith('.jpg') or local_path.endswith('.jpeg') or local_path.endswith('.png')): + os.remove(local_path) + +def delete_pkl_files(directory): + """Delete all .pkl files in the specified directory.""" + pkl_files = glob.glob(os.path.join(directory, '*.pkl')) + for pkl_file in pkl_files: + os.remove(pkl_file) diff --git a/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("-----------")