├── .editorconfig ├── .gitignore ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── examples ├── daniel1-daniel2.png ├── daniel1-doc.png ├── daniel1-ronaldinho1.png ├── daniel1.jpg ├── daniel2-doc.png ├── daniel2.jpg ├── doc.png ├── ronaldinho1-doc.png └── ronaldinho1.jpg ├── pyproject.toml ├── requirements.txt ├── setup.cfg ├── setup.py └── src └── facematch ├── __init__.py ├── cmd ├── __init__.py └── cli.py └── face.py /.editorconfig: -------------------------------------------------------------------------------- 1 | # https://editorconfig.org/ 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | end_of_line = lf 11 | charset = utf-8 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # general things to ignore 2 | build/ 3 | dist/ 4 | *.egg-info/ 5 | *.egg 6 | *.py[cod] 7 | __pycache__/ 8 | *.so 9 | *~ 10 | 11 | # due to using tox and pytest 12 | .tox 13 | .cache 14 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Daniel Gatis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include pyproject.toml 2 | 3 | # Include the README 4 | include *.md 5 | 6 | # Include the license file 7 | include LICENSE.txt 8 | 9 | # Include the data files 10 | recursive-include data * 11 | 12 | include requirements.txt 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Facematch 2 | 3 | [](https://pepy.tech/project/facematch) 4 | [](https://pepy.tech/project/facematch/month) 5 | [](https://pepy.tech/project/facematch/week) 6 | [](https://img.shields.io/badge/License-MIT-blue.svg) 7 | 8 | Facematch is a tool to verifies if two photos contain the same person. 9 | 10 |
| input1 | 14 |input2 | 15 |output | 16 |result | 17 |
![]() |
22 | ![]() |
23 | ![]() |
24 | {"match": true, "distance": 0.38913072553055295} | 25 |
![]() |
28 | ![]() |
29 | ![]() |
30 | {"match": true, "distance": 0.5131670729305189} | 31 |
![]() |
34 | ![]() |
35 | ![]() |
36 | {"match": true, "distance": 0.4370069082351905} | 37 |
![]() |
40 | ![]() |
41 | ![]() |
42 | {"match": false, "distance": 0.7838337220196059} | 43 |
![]() |
46 | ![]() |
47 | ![]() |
48 | {"match": false, "distance": 0.8705370438394476} | 49 |
114 |
--------------------------------------------------------------------------------
/examples/daniel1-daniel2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielgatis/facematch/543001e4ec61186b55b630b0b1275e673b2d7726/examples/daniel1-daniel2.png
--------------------------------------------------------------------------------
/examples/daniel1-doc.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielgatis/facematch/543001e4ec61186b55b630b0b1275e673b2d7726/examples/daniel1-doc.png
--------------------------------------------------------------------------------
/examples/daniel1-ronaldinho1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielgatis/facematch/543001e4ec61186b55b630b0b1275e673b2d7726/examples/daniel1-ronaldinho1.png
--------------------------------------------------------------------------------
/examples/daniel1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielgatis/facematch/543001e4ec61186b55b630b0b1275e673b2d7726/examples/daniel1.jpg
--------------------------------------------------------------------------------
/examples/daniel2-doc.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielgatis/facematch/543001e4ec61186b55b630b0b1275e673b2d7726/examples/daniel2-doc.png
--------------------------------------------------------------------------------
/examples/daniel2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielgatis/facematch/543001e4ec61186b55b630b0b1275e673b2d7726/examples/daniel2.jpg
--------------------------------------------------------------------------------
/examples/doc.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielgatis/facematch/543001e4ec61186b55b630b0b1275e673b2d7726/examples/doc.png
--------------------------------------------------------------------------------
/examples/ronaldinho1-doc.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielgatis/facematch/543001e4ec61186b55b630b0b1275e673b2d7726/examples/ronaldinho1-doc.png
--------------------------------------------------------------------------------
/examples/ronaldinho1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielgatis/facematch/543001e4ec61186b55b630b0b1275e673b2d7726/examples/ronaldinho1.jpg
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | # These are the assumed default build requirements from pip:
3 | # https://pip.pypa.io/en/stable/reference/pip/#pep-517-and-518-support
4 | requires = ["setuptools>=40.8.0", "wheel"]
5 | build-backend = "setuptools.build_meta"
6 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | flask>=1.1.2
2 | face-recognition>=1.3.0
3 | waitress>=1.4.4
4 | requests>=2.24.0
5 | numpy>=1.19.1
6 | pillow>=7.2.0
7 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [metadata]
2 | # This includes the license file(s) in the wheel.
3 | # https://wheel.readthedocs.io/en/stable/user_guide.html#including-license-files-in-the-generated-wheel-file
4 | license_files = LICENSE.txt
5 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import pathlib
2 |
3 | from setuptools import find_packages, setup
4 |
5 | here = pathlib.Path(__file__).parent.resolve()
6 |
7 | long_description = (here / "README.md").read_text(encoding="utf-8")
8 |
9 | with open("requirements.txt") as f:
10 | requireds = f.read().splitlines()
11 |
12 | setup(
13 | name="facematch",
14 | version="1.0.1",
15 | description="Verifies if two photos contain the same person",
16 | long_description=long_description,
17 | long_description_content_type="text/markdown",
18 | url="https://github.com/danielgatis/facematch",
19 | author="Daniel Gatis",
20 | author_email="danielgatis@gmail.com",
21 | classifiers=[
22 | "License :: OSI Approved :: MIT License",
23 | "Programming Language :: Python :: 3 :: Only",
24 | ],
25 | keywords="face, recoginition, photo",
26 | package_dir={"": "src"},
27 | packages=find_packages(where="src"),
28 | python_requires=">=3.5, <4",
29 | install_requires=requireds,
30 | entry_points={
31 | "console_scripts": [
32 | "facematch=facematch.cmd.cli:main",
33 | "facematch-server=facematch.cmd.server:main",
34 | ],
35 | },
36 | )
37 |
--------------------------------------------------------------------------------
/src/facematch/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielgatis/facematch/543001e4ec61186b55b630b0b1275e673b2d7726/src/facematch/__init__.py
--------------------------------------------------------------------------------
/src/facematch/cmd/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielgatis/facematch/543001e4ec61186b55b630b0b1275e673b2d7726/src/facematch/cmd/__init__.py
--------------------------------------------------------------------------------
/src/facematch/cmd/cli.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | import json
3 | import sys
4 |
5 | from ..face import EncodingError, match
6 |
7 |
8 | def main():
9 | ap = argparse.ArgumentParser()
10 |
11 | ap.add_argument(
12 | "-t",
13 | "--threshold",
14 | type=float,
15 | default=0.6,
16 | help="How much distance between faces to consider it a match. Lower is more strict. (default=0.6)",
17 | )
18 |
19 | ap.add_argument(
20 | "-o",
21 | "--output",
22 | type=argparse.FileType("wb"),
23 | help="Path to the debug png image.",
24 | )
25 |
26 | ap.add_argument(
27 | "input1", type=argparse.FileType("rb"), help="Path to the first input image.",
28 | )
29 |
30 | ap.add_argument(
31 | "input2", type=argparse.FileType("rb"), help="Path to the second input image.",
32 | )
33 |
34 | args = ap.parse_args()
35 |
36 | try:
37 | result, distance, data = match(
38 | args.input1.read(), args.input2.read(), args.threshold
39 | )
40 | except EncodingError as err:
41 | print(err)
42 | sys.exit(2)
43 |
44 | if args.output:
45 | args.output.write(data)
46 |
47 | if result:
48 | print(json.dumps(dict(match=True, distance=distance)))
49 | sys.exit(0)
50 | else:
51 | print(json.dumps(dict(match=False, distance=distance)))
52 | sys.exit(1)
53 |
54 |
55 | if __name__ == "__main__":
56 | main()
57 |
--------------------------------------------------------------------------------
/src/facematch/face.py:
--------------------------------------------------------------------------------
1 | import io
2 |
3 | import face_recognition
4 | import numpy as np
5 | from PIL import Image, ImageDraw
6 |
7 | RED = (255, 0, 0)
8 | GREEN = (0, 255, 0)
9 | SIZE = 500
10 | HALF_SIZE = 250
11 |
12 | class EncodingError(Exception):
13 | pass
14 |
15 |
16 | def get_concat_h_blank(im1, im2, color=(0, 0, 0)):
17 | dst = Image.new("RGB", (im1.width + im2.width, max(im1.height, im2.height)), color)
18 | dst.paste(im1, (0, 0))
19 | dst.paste(im2, (im1.width, 0))
20 | return dst
21 |
22 |
23 | def features(data):
24 | img = Image.open(io.BytesIO(data))
25 | img = img.convert("RGB")
26 |
27 | w, h = img.size
28 | img = img.resize((SIZE, int(SIZE * (h / w))), Image.ANTIALIAS)
29 |
30 | img_np = np.array(img)
31 | face_encodings = face_recognition.face_encodings(img_np)
32 |
33 | if len(face_encodings) != 1:
34 | raise EncodingError("The image must contain only one face")
35 |
36 | face_locations = face_recognition.face_locations(
37 | img_np, number_of_times_to_upsample=0, model="cnn"
38 | )
39 |
40 | face_images = []
41 | for (top, right, bottom, left) in face_locations:
42 | face_image = Image.fromarray(img_np[top:bottom, left:right])
43 |
44 | w, h = face_image.size
45 | face_image = face_image.resize((HALF_SIZE, int(HALF_SIZE * (h / w))), Image.ANTIALIAS)
46 |
47 | face_images.append(face_image)
48 |
49 | return (face_encodings[0], face_locations[0], face_images[0])
50 |
51 |
52 | def match(data1, data2, threshold=0.6):
53 | try:
54 | face_encoding1, face_location1, face_img1 = features(data1)
55 | except EncodingError as err:
56 | raise EncodingError("The first image must contain only one face")
57 |
58 | try:
59 | face_encoding2, face_location2, face_img2 = features(data2)
60 | except EncodingError as err:
61 | raise EncodingError("The second image must contain only one face")
62 |
63 | distance = face_recognition.face_distance([face_encoding1], face_encoding2)[0]
64 |
65 | result = distance <= threshold
66 | color = GREEN if result else RED
67 |
68 | out = get_concat_h_blank(face_img1, face_img2)
69 | draw = ImageDraw.Draw(out)
70 |
71 | tw, th = draw.textsize(str(distance))
72 | ow, oh = out.size
73 |
74 | draw.text((ow - tw, oh - th), str(distance), color)
75 |
76 | bio = io.BytesIO()
77 | out.save(bio, "PNG")
78 |
79 | return (result, distance, bio.getbuffer())
80 |
--------------------------------------------------------------------------------