Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cosine similarity methods #1337

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion face_recognition/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
__email__ = 'ageitgey@gmail.com'
__version__ = '1.4.0'

from .api import load_image_file, face_locations, batch_face_locations, face_landmarks, face_encodings, compare_faces, face_distance
from .api import load_image_file, face_locations, batch_face_locations, face_landmarks, face_encodings, compare_faces, face_distance, face_cosine_similarity, compare_faces_cosine
37 changes: 37 additions & 0 deletions face_recognition/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,27 @@ def face_distance(face_encodings, face_to_compare):
return np.linalg.norm(face_encodings - face_to_compare, axis=1)


def face_cosine_similarity(face_encodings, face_to_compare):
"""
Given a list of face encodings, compare them to a known encoding and get the cosine similarity
for each comparison face. The cosine similarity tells you the angle ratio of two faces as vectors
in multi-dimensional space, in terms of cosine(theta).

:param face_encodings: List of face encodings to compare
:param face_to_compare: A face encoding to compare against
:return: A numpy ndarray with the corresponding cosine similarity for each face
"""

if len(face_encodings) == 0:
return np.empty((0))

cosine_sims = []
for face_encoding in face_encodings:
cosine_sims.append(np.dot(face_encoding, face_to_compare) / (np.linalg.norm(face_encoding) * np.linalg.norm(face_to_compare)))

return np.array(cosine_sims)


def load_image_file(file, mode='RGB'):
"""
Loads an image file (.jpg, .png, etc) into a numpy array
Expand Down Expand Up @@ -224,3 +245,19 @@ def compare_faces(known_face_encodings, face_encoding_to_check, tolerance=0.6):
:return: A list of True/False values indicating which known_face_encodings match the face encoding to check
"""
return list(face_distance(known_face_encodings, face_encoding_to_check) <= tolerance)


def compare_faces_cosine(known_face_encodings, face_encoding_to_check, tolerance=0.85):
corbanvilla marked this conversation as resolved.
Show resolved Hide resolved
"""
Compare a list of face encodings against a candidate encoding to see if they match
(using cosine similarity formula).

:param known_face_encodings: A list of known face encodings
:param face_encoding_to_check: A single face encoding to compare against the list
:param tolerance: How much distance between faces to consider it a match.
Higher is more strict. 0.8-0.9 is typical best performance.
Since it's cosine(theta), 0.8 => 80% match
:return: A list of True/False values indicating which known_face_encodings match the face encoding to check
"""
return list(face_cosine_similarity(known_face_encodings, face_encoding_to_check) >= tolerance)

86 changes: 86 additions & 0 deletions tests/test_face_recognition.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,92 @@ def test_compare_faces_empty_lists(self):
self.assertEqual(type(match_results), list)
self.assertListEqual(match_results, [])

def test_face_cosine_similarity(self):
img_a1 = api.load_image_file(os.path.join(os.path.dirname(__file__), 'test_images', 'obama.jpg'))
img_a2 = api.load_image_file(os.path.join(os.path.dirname(__file__), 'test_images', 'obama2.jpg'))
img_a3 = api.load_image_file(os.path.join(os.path.dirname(__file__), 'test_images', 'obama3.jpg'))

img_b1 = api.load_image_file(os.path.join(os.path.dirname(__file__), 'test_images', 'biden.jpg'))

face_encoding_a1 = api.face_encodings(img_a1)[0]
face_encoding_a2 = api.face_encodings(img_a2)[0]
face_encoding_a3 = api.face_encodings(img_a3)[0]
face_encoding_b1 = api.face_encodings(img_b1)[0]

faces_to_compare = [
face_encoding_a2,
face_encoding_a3,
face_encoding_b1]

distance_results = api.face_cosine_similarity(faces_to_compare, face_encoding_a1)

# 0.85 is the default face cosine similarity match threshold. So we'll spot-check that the numbers returned
# are above or below that based on if they should match (since the exact numbers could vary).
self.assertEqual(type(distance_results), np.ndarray)
self.assertGreaterEqual(distance_results[0], 0.85)
self.assertGreaterEqual(distance_results[1], 0.85)
self.assertLess(distance_results[2], 0.85)

def test_face_cosine_similarity_empty_lists(self):
img = api.load_image_file(os.path.join(os.path.dirname(__file__), 'test_images', 'biden.jpg'))
face_encoding = api.face_encodings(img)[0]

# empty python list
faces_to_compare = []

distance_results = api.face_cosine_similarity(faces_to_compare, face_encoding)
self.assertEqual(type(distance_results), np.ndarray)
self.assertEqual(len(distance_results), 0)

# empty numpy list
faces_to_compare = np.array([])

distance_results = api.face_distance(faces_to_compare, face_encoding)
self.assertEqual(type(distance_results), np.ndarray)
self.assertEqual(len(distance_results), 0)

def test_compare_faces_cosine(self):
img_a1 = api.load_image_file(os.path.join(os.path.dirname(__file__), 'test_images', 'obama.jpg'))
img_a2 = api.load_image_file(os.path.join(os.path.dirname(__file__), 'test_images', 'obama2.jpg'))
img_a3 = api.load_image_file(os.path.join(os.path.dirname(__file__), 'test_images', 'obama3.jpg'))

img_b1 = api.load_image_file(os.path.join(os.path.dirname(__file__), 'test_images', 'biden.jpg'))

face_encoding_a1 = api.face_encodings(img_a1)[0]
face_encoding_a2 = api.face_encodings(img_a2)[0]
face_encoding_a3 = api.face_encodings(img_a3)[0]
face_encoding_b1 = api.face_encodings(img_b1)[0]

faces_to_compare = [
face_encoding_a2,
face_encoding_a3,
face_encoding_b1]

match_results = api.compare_faces_cosine(faces_to_compare, face_encoding_a1)

self.assertEqual(type(match_results), list)
self.assertTrue(match_results[0])
self.assertTrue(match_results[1])
self.assertFalse(match_results[2])

def test_compare_faces_cosine_empty_lists(self):
img = api.load_image_file(os.path.join(os.path.dirname(__file__), 'test_images', 'biden.jpg'))
face_encoding = api.face_encodings(img)[0]

# empty python list
faces_to_compare = []

match_results = api.compare_faces_cosine(faces_to_compare, face_encoding)
self.assertEqual(type(match_results), list)
self.assertListEqual(match_results, [])

# empty numpy list
faces_to_compare = np.array([])

match_results = api.compare_faces_cosine(faces_to_compare, face_encoding)
self.assertEqual(type(match_results), list)
self.assertListEqual(match_results, [])

def test_command_line_interface_options(self):
target_string = 'Show this message and exit.'
runner = CliRunner()
Expand Down