face-engine 3.1.0__tar.gz → 3.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {face_engine-3.1.0/face_engine.egg-info → face_engine-3.2.0}/PKG-INFO +29 -1
- {face_engine-3.1.0 → face_engine-3.2.0}/README.rst +28 -0
- {face_engine-3.1.0 → face_engine-3.2.0}/face_engine/__init__.py +1 -1
- {face_engine-3.1.0 → face_engine-3.2.0}/face_engine/core.py +63 -5
- {face_engine-3.1.0 → face_engine-3.2.0}/face_engine/exceptions.py +4 -0
- {face_engine-3.1.0 → face_engine-3.2.0}/face_engine/tools.py +4 -0
- {face_engine-3.1.0 → face_engine-3.2.0/face_engine.egg-info}/PKG-INFO +29 -1
- {face_engine-3.1.0 → face_engine-3.2.0}/face_engine.egg-info/SOURCES.txt +2 -1
- {face_engine-3.1.0 → face_engine-3.2.0}/tests/test_face_engine.py +89 -21
- {face_engine-3.1.0 → face_engine-3.2.0}/tests/test_face_engine_persistence.py +12 -0
- face_engine-3.2.0/tests/test_tools.py +34 -0
- {face_engine-3.1.0 → face_engine-3.2.0}/LICENSE +0 -0
- {face_engine-3.1.0 → face_engine-3.2.0}/MANIFEST.in +0 -0
- {face_engine-3.1.0 → face_engine-3.2.0}/docs/source/conf.py +0 -0
- {face_engine-3.1.0 → face_engine-3.2.0}/face_engine/fetching.py +0 -0
- {face_engine-3.1.0 → face_engine-3.2.0}/face_engine/models/__init__.py +0 -0
- {face_engine-3.1.0 → face_engine-3.2.0}/face_engine/models/_onnx.py +0 -0
- {face_engine-3.1.0 → face_engine-3.2.0}/face_engine/models/basic_estimator.py +0 -0
- {face_engine-3.1.0 → face_engine-3.2.0}/face_engine/models/dlib_models.py +0 -0
- {face_engine-3.1.0 → face_engine-3.2.0}/face_engine/models/insightface_models.py +0 -0
- {face_engine-3.1.0 → face_engine-3.2.0}/face_engine/models/minifasnet.py +0 -0
- {face_engine-3.1.0 → face_engine-3.2.0}/face_engine.egg-info/dependency_links.txt +0 -0
- {face_engine-3.1.0 → face_engine-3.2.0}/face_engine.egg-info/requires.txt +0 -0
- {face_engine-3.1.0 → face_engine-3.2.0}/face_engine.egg-info/top_level.txt +0 -0
- {face_engine-3.1.0 → face_engine-3.2.0}/pyproject.toml +0 -0
- {face_engine-3.1.0 → face_engine-3.2.0}/setup.cfg +0 -0
- {face_engine-3.1.0 → face_engine-3.2.0}/tests/test_face_engine_models.py +0 -0
- {face_engine-3.1.0 → face_engine-3.2.0}/tests/test_fetching.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: face-engine
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.2.0
|
|
4
4
|
Summary: Face Recognition Engine
|
|
5
5
|
Author-email: Daniyar Kussainov <ohw0sseug@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -131,6 +131,22 @@ already present with the ``[insightface]`` extra). It is effective against
|
|
|
131
131
|
printed photos and basic screen replays; it is **not** a certified
|
|
132
132
|
(ISO/IEC 30107-3) liveness solution.
|
|
133
133
|
|
|
134
|
+
Face verification
|
|
135
|
+
-----------------
|
|
136
|
+
|
|
137
|
+
Since 3.2 the engine has a model-agnostic 1:1 verification primitive —
|
|
138
|
+
cosine similarity between two embeddings of the same embedder:
|
|
139
|
+
|
|
140
|
+
.. code-block:: python
|
|
141
|
+
|
|
142
|
+
>>> bbs, extra = engine.find_faces(image, limit=1)
|
|
143
|
+
>>> source = engine.compute_embeddings(image, bbs, **extra)[0]
|
|
144
|
+
>>> engine.compare(source, target)
|
|
145
|
+
0.83
|
|
146
|
+
|
|
147
|
+
``compare`` returns the raw score in ``[-1, 1]``; accept/reject
|
|
148
|
+
thresholding is left to the caller and should be calibrated per embedder.
|
|
149
|
+
|
|
134
150
|
Model weights licensing
|
|
135
151
|
-----------------------
|
|
136
152
|
|
|
@@ -144,6 +160,18 @@ come with their own terms:
|
|
|
144
160
|
ONNX exports of the `Silent-Face-Anti-Spoofing`_ checkpoints, reproducible
|
|
145
161
|
with ``extra/export_minifasnet.py``.
|
|
146
162
|
|
|
163
|
+
Changes in 3.2
|
|
164
|
+
--------------
|
|
165
|
+
|
|
166
|
+
* Unknown explicit model names now raise ``ModelNotFoundError`` instead of
|
|
167
|
+
warning and falling back to the abstract no-op models. This applies to the
|
|
168
|
+
``FaceEngine`` constructor, the model property setters, and
|
|
169
|
+
``load_engine``. Empty names keep the installed-backend fallback chains.
|
|
170
|
+
* New ``engine.compare(source, target)`` — cosine similarity between two
|
|
171
|
+
embeddings for 1:1 verification (raw score, no thresholding).
|
|
172
|
+
* ``tools.imread`` now reads raw ``bytes`` image content, as its
|
|
173
|
+
documentation always claimed.
|
|
174
|
+
|
|
147
175
|
Breaking changes in 3.0
|
|
148
176
|
-----------------------
|
|
149
177
|
|
|
@@ -92,6 +92,22 @@ already present with the ``[insightface]`` extra). It is effective against
|
|
|
92
92
|
printed photos and basic screen replays; it is **not** a certified
|
|
93
93
|
(ISO/IEC 30107-3) liveness solution.
|
|
94
94
|
|
|
95
|
+
Face verification
|
|
96
|
+
-----------------
|
|
97
|
+
|
|
98
|
+
Since 3.2 the engine has a model-agnostic 1:1 verification primitive —
|
|
99
|
+
cosine similarity between two embeddings of the same embedder:
|
|
100
|
+
|
|
101
|
+
.. code-block:: python
|
|
102
|
+
|
|
103
|
+
>>> bbs, extra = engine.find_faces(image, limit=1)
|
|
104
|
+
>>> source = engine.compute_embeddings(image, bbs, **extra)[0]
|
|
105
|
+
>>> engine.compare(source, target)
|
|
106
|
+
0.83
|
|
107
|
+
|
|
108
|
+
``compare`` returns the raw score in ``[-1, 1]``; accept/reject
|
|
109
|
+
thresholding is left to the caller and should be calibrated per embedder.
|
|
110
|
+
|
|
95
111
|
Model weights licensing
|
|
96
112
|
-----------------------
|
|
97
113
|
|
|
@@ -105,6 +121,18 @@ come with their own terms:
|
|
|
105
121
|
ONNX exports of the `Silent-Face-Anti-Spoofing`_ checkpoints, reproducible
|
|
106
122
|
with ``extra/export_minifasnet.py``.
|
|
107
123
|
|
|
124
|
+
Changes in 3.2
|
|
125
|
+
--------------
|
|
126
|
+
|
|
127
|
+
* Unknown explicit model names now raise ``ModelNotFoundError`` instead of
|
|
128
|
+
warning and falling back to the abstract no-op models. This applies to the
|
|
129
|
+
``FaceEngine`` constructor, the model property setters, and
|
|
130
|
+
``load_engine``. Empty names keep the installed-backend fallback chains.
|
|
131
|
+
* New ``engine.compare(source, target)`` — cosine similarity between two
|
|
132
|
+
embeddings for 1:1 verification (raw score, no thresholding).
|
|
133
|
+
* ``tools.imread`` now reads raw ``bytes`` image content, as its
|
|
134
|
+
documentation always claimed.
|
|
135
|
+
|
|
108
136
|
Breaking changes in 3.0
|
|
109
137
|
-----------------------
|
|
110
138
|
|
|
@@ -3,7 +3,7 @@ Face Recognition Engine
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
__all__ = ["logger", "RESOURCES", "FaceEngine", "_models", "load_engine"]
|
|
6
|
-
__version__ = "3.
|
|
6
|
+
__version__ = "3.2.0"
|
|
7
7
|
__author__ = "Daniyar Kussainov"
|
|
8
8
|
__license__ = "Apache License, Version 2.0"
|
|
9
9
|
__copyright__ = "Copyright 2019-2026 Daniyar Kussainov"
|
|
@@ -8,8 +8,8 @@ import os
|
|
|
8
8
|
|
|
9
9
|
import numpy as np
|
|
10
10
|
|
|
11
|
-
from . import __version__
|
|
12
|
-
from .exceptions import FaceNotFoundError
|
|
11
|
+
from . import __version__
|
|
12
|
+
from .exceptions import FaceNotFoundError, ModelNotFoundError
|
|
13
13
|
from .models import _models
|
|
14
14
|
from .tools import imread
|
|
15
15
|
|
|
@@ -20,10 +20,29 @@ _ESTIMATOR_DEFAULTS = ("basic",)
|
|
|
20
20
|
# antispoof is opt-in: the default is the abstract no-op model
|
|
21
21
|
_ANTISPOOF_DEFAULTS = ()
|
|
22
22
|
|
|
23
|
+
# in-tree optional models: absence from the registry means the backend
|
|
24
|
+
# dependency failed to import (tools.import_module swallows it), not that
|
|
25
|
+
# a plugin-module import was forgotten
|
|
26
|
+
_OPTIONAL_BACKENDS = {
|
|
27
|
+
"scrfd": "insightface",
|
|
28
|
+
"scrfd_antelopev2": "insightface",
|
|
29
|
+
"retina_face": "insightface",
|
|
30
|
+
"arcface": "insightface",
|
|
31
|
+
"arcface_antelopev2": "insightface",
|
|
32
|
+
"hog": "dlib",
|
|
33
|
+
"mmod": "dlib",
|
|
34
|
+
"resnet": "dlib",
|
|
35
|
+
"minifasnet": "onnxruntime",
|
|
36
|
+
}
|
|
37
|
+
|
|
23
38
|
|
|
24
39
|
def _resolve(name: Optional[str], defaults: Tuple[str, ...], kind: str) -> str:
|
|
25
40
|
"""Resolve model name to a registered one, with installed-backend
|
|
26
|
-
fallbacks for empty names
|
|
41
|
+
fallbacks for empty names.
|
|
42
|
+
|
|
43
|
+
Explicit unknown names raise :class:`~.exceptions.ModelNotFoundError`
|
|
44
|
+
(since 3.2; previously they warned and fell back to the abstract no-op).
|
|
45
|
+
"""
|
|
27
46
|
|
|
28
47
|
if not name:
|
|
29
48
|
for candidate in defaults:
|
|
@@ -31,8 +50,17 @@ def _resolve(name: Optional[str], defaults: Tuple[str, ...], kind: str) -> str:
|
|
|
31
50
|
return candidate
|
|
32
51
|
return "abstract_" + kind
|
|
33
52
|
if name not in _models:
|
|
34
|
-
|
|
35
|
-
|
|
53
|
+
if name in _OPTIONAL_BACKENDS:
|
|
54
|
+
raise ModelNotFoundError(
|
|
55
|
+
"%s model '%s' is not registered because its backend "
|
|
56
|
+
"dependency '%s' is not installed"
|
|
57
|
+
% (kind.capitalize(), name, _OPTIONAL_BACKENDS[name])
|
|
58
|
+
)
|
|
59
|
+
raise ModelNotFoundError(
|
|
60
|
+
"%s model '%s' is not registered; make sure the module "
|
|
61
|
+
"providing it is imported. Registered models: %s"
|
|
62
|
+
% (kind.capitalize(), name, sorted(_models))
|
|
63
|
+
)
|
|
36
64
|
return name
|
|
37
65
|
|
|
38
66
|
|
|
@@ -61,6 +89,8 @@ def load_engine(filename: str) -> "FaceEngine":
|
|
|
61
89
|
:rtype: :class:`.FaceEngine`
|
|
62
90
|
|
|
63
91
|
:raises RuntimeError: on legacy (pre-3.0) pickle files
|
|
92
|
+
:raises ModelNotFoundError: when the file names a model that is not
|
|
93
|
+
registered (e.g. its plugin module is not imported)
|
|
64
94
|
"""
|
|
65
95
|
|
|
66
96
|
with open(filename, "rb") as file:
|
|
@@ -498,3 +528,31 @@ class FaceEngine:
|
|
|
498
528
|
|
|
499
529
|
embeddings = self._embedder.compute_embeddings(image, bounding_boxes, **kwargs)
|
|
500
530
|
return embeddings
|
|
531
|
+
|
|
532
|
+
def compare(self, source: np.ndarray, target: np.ndarray) -> float:
|
|
533
|
+
"""Compare two face embeddings by cosine similarity.
|
|
534
|
+
|
|
535
|
+
The 1:1 verification primitive: returns the raw similarity score
|
|
536
|
+
in ``[-1, 1]`` (1 — same direction, 0 — orthogonal, -1 — opposite;
|
|
537
|
+
0 also for zero-norm inputs). Both embeddings must come from the
|
|
538
|
+
same embedder model. Accept/reject thresholding is left to the
|
|
539
|
+
caller and should be calibrated per embedder.
|
|
540
|
+
|
|
541
|
+
:param source: embedding vector with shape (embedding_dim,)
|
|
542
|
+
or (1, embedding_dim)
|
|
543
|
+
:type source: numpy.ndarray
|
|
544
|
+
|
|
545
|
+
:param target: embedding vector with shape (embedding_dim,)
|
|
546
|
+
or (1, embedding_dim)
|
|
547
|
+
:type target: numpy.ndarray
|
|
548
|
+
|
|
549
|
+
:returns: cosine similarity score
|
|
550
|
+
:rtype: float
|
|
551
|
+
"""
|
|
552
|
+
|
|
553
|
+
source = np.asarray(source, dtype=np.float64).ravel()
|
|
554
|
+
target = np.asarray(target, dtype=np.float64).ravel()
|
|
555
|
+
denominator = float(np.linalg.norm(source) * np.linalg.norm(target))
|
|
556
|
+
if denominator == 0.0:
|
|
557
|
+
return 0.0
|
|
558
|
+
return float(np.dot(source, target) / denominator)
|
|
@@ -7,5 +7,9 @@ class FaceNotFoundError(Exception):
|
|
|
7
7
|
"""Raised when the face is not found in the image"""
|
|
8
8
|
|
|
9
9
|
|
|
10
|
+
class ModelNotFoundError(Exception):
|
|
11
|
+
"""Raised when an explicit model name is not in the registry"""
|
|
12
|
+
|
|
13
|
+
|
|
10
14
|
class TrainError(Exception):
|
|
11
15
|
"""Raised when the fit(train) process is failed"""
|
|
@@ -58,6 +58,10 @@ def imread(uri, mode=None):
|
|
|
58
58
|
data = _download_url(uri)
|
|
59
59
|
uri = io.BytesIO(data)
|
|
60
60
|
|
|
61
|
+
# raw image bytes: PIL treats a bare bytes object as a file path
|
|
62
|
+
if isinstance(uri, bytes):
|
|
63
|
+
uri = io.BytesIO(uri)
|
|
64
|
+
|
|
61
65
|
image = Image.open(uri)
|
|
62
66
|
if mode:
|
|
63
67
|
image = image.convert(mode)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: face-engine
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.2.0
|
|
4
4
|
Summary: Face Recognition Engine
|
|
5
5
|
Author-email: Daniyar Kussainov <ohw0sseug@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -131,6 +131,22 @@ already present with the ``[insightface]`` extra). It is effective against
|
|
|
131
131
|
printed photos and basic screen replays; it is **not** a certified
|
|
132
132
|
(ISO/IEC 30107-3) liveness solution.
|
|
133
133
|
|
|
134
|
+
Face verification
|
|
135
|
+
-----------------
|
|
136
|
+
|
|
137
|
+
Since 3.2 the engine has a model-agnostic 1:1 verification primitive —
|
|
138
|
+
cosine similarity between two embeddings of the same embedder:
|
|
139
|
+
|
|
140
|
+
.. code-block:: python
|
|
141
|
+
|
|
142
|
+
>>> bbs, extra = engine.find_faces(image, limit=1)
|
|
143
|
+
>>> source = engine.compute_embeddings(image, bbs, **extra)[0]
|
|
144
|
+
>>> engine.compare(source, target)
|
|
145
|
+
0.83
|
|
146
|
+
|
|
147
|
+
``compare`` returns the raw score in ``[-1, 1]``; accept/reject
|
|
148
|
+
thresholding is left to the caller and should be calibrated per embedder.
|
|
149
|
+
|
|
134
150
|
Model weights licensing
|
|
135
151
|
-----------------------
|
|
136
152
|
|
|
@@ -144,6 +160,18 @@ come with their own terms:
|
|
|
144
160
|
ONNX exports of the `Silent-Face-Anti-Spoofing`_ checkpoints, reproducible
|
|
145
161
|
with ``extra/export_minifasnet.py``.
|
|
146
162
|
|
|
163
|
+
Changes in 3.2
|
|
164
|
+
--------------
|
|
165
|
+
|
|
166
|
+
* Unknown explicit model names now raise ``ModelNotFoundError`` instead of
|
|
167
|
+
warning and falling back to the abstract no-op models. This applies to the
|
|
168
|
+
``FaceEngine`` constructor, the model property setters, and
|
|
169
|
+
``load_engine``. Empty names keep the installed-backend fallback chains.
|
|
170
|
+
* New ``engine.compare(source, target)`` — cosine similarity between two
|
|
171
|
+
embeddings for 1:1 verification (raw score, no thresholding).
|
|
172
|
+
* ``tools.imread`` now reads raw ``bytes`` image content, as its
|
|
173
|
+
documentation always claimed.
|
|
174
|
+
|
|
147
175
|
Breaking changes in 3.0
|
|
148
176
|
-----------------------
|
|
149
177
|
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import logging
|
|
2
1
|
import unittest
|
|
3
2
|
|
|
4
3
|
import numpy as np
|
|
5
4
|
|
|
6
5
|
from face_engine import FaceEngine
|
|
7
|
-
from face_engine.exceptions import FaceNotFoundError, TrainError
|
|
6
|
+
from face_engine.exceptions import FaceNotFoundError, ModelNotFoundError, TrainError
|
|
8
7
|
from face_engine.models import Antispoof, Detector, Embedder, Estimator
|
|
9
8
|
from face_engine.tools import imread
|
|
10
9
|
from tests import TestCase, dlib, insightface, onnxruntime
|
|
@@ -48,14 +47,13 @@ class TestFaceEngine(TestCase):
|
|
|
48
47
|
self.assertEqual(self.empty_engine.embedder, "abstract_embedder")
|
|
49
48
|
self.assertEqual(self.empty_engine.estimator, "abstract_estimator")
|
|
50
49
|
|
|
51
|
-
def
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
self.
|
|
57
|
-
|
|
58
|
-
self.assertEqual(engine.estimator, "abstract_estimator")
|
|
50
|
+
def test_explicit_init_with_not_existing_models_raises(self):
|
|
51
|
+
with self.assertRaises(ModelNotFoundError):
|
|
52
|
+
FaceEngine(detector="my_detector")
|
|
53
|
+
with self.assertRaises(ModelNotFoundError):
|
|
54
|
+
FaceEngine(embedder="my_embedder")
|
|
55
|
+
with self.assertRaises(ModelNotFoundError):
|
|
56
|
+
FaceEngine(estimator="my_estimator")
|
|
59
57
|
|
|
60
58
|
@unittest.skipUnless(dlib, "dlib package is not installed")
|
|
61
59
|
def test_setters_with_dlib_models(self):
|
|
@@ -95,16 +93,19 @@ class TestFaceEngine(TestCase):
|
|
|
95
93
|
self.assertIsInstance(self.test_engine._estimator, Estimator)
|
|
96
94
|
self.assertEqual(self.test_engine.estimator, "abstract_estimator")
|
|
97
95
|
|
|
98
|
-
def
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
self.
|
|
103
|
-
self.
|
|
96
|
+
def test_setters_with_not_existing_models_raise(self):
|
|
97
|
+
with self.assertRaises(ModelNotFoundError) as context:
|
|
98
|
+
self.empty_engine.detector = "my_detector"
|
|
99
|
+
# the message names the model and hints at the plugin import
|
|
100
|
+
self.assertIn("my_detector", str(context.exception))
|
|
101
|
+
self.assertIn("imported", str(context.exception))
|
|
102
|
+
with self.assertRaises(ModelNotFoundError):
|
|
103
|
+
self.empty_engine.embedder = "my_embedder"
|
|
104
|
+
with self.assertRaises(ModelNotFoundError):
|
|
105
|
+
self.empty_engine.estimator = "my_estimator"
|
|
106
|
+
# the failed assignments left the previous models in place
|
|
104
107
|
self.assertEqual(self.empty_engine.detector, "abstract_detector")
|
|
105
|
-
self.assertNotEqual(self.empty_engine.embedder, "my_embedder")
|
|
106
108
|
self.assertEqual(self.empty_engine.embedder, "abstract_embedder")
|
|
107
|
-
self.assertNotEqual(self.empty_engine.estimator, "my_estimator")
|
|
108
109
|
self.assertEqual(self.empty_engine.estimator, "abstract_estimator")
|
|
109
110
|
|
|
110
111
|
@unittest.skipUnless(insightface, "insightface package is not installed")
|
|
@@ -146,11 +147,25 @@ class TestFaceEngine(TestCase):
|
|
|
146
147
|
# antispoof is opt-in regardless of installed backends
|
|
147
148
|
self.assertEqual(self.test_engine.antispoof, "abstract_antispoof")
|
|
148
149
|
|
|
149
|
-
def
|
|
150
|
-
|
|
151
|
-
|
|
150
|
+
def test_setter_antispoof_with_not_existing_model_raises(self):
|
|
151
|
+
with self.assertRaises(ModelNotFoundError):
|
|
152
|
+
self.test_engine.antispoof = "my_antispoof"
|
|
152
153
|
self.assertEqual(self.test_engine.antispoof, "abstract_antispoof")
|
|
153
154
|
|
|
155
|
+
def test_unregistered_in_tree_model_names_missing_backend(self):
|
|
156
|
+
# simulate an install where minifasnet's backend failed to import
|
|
157
|
+
from unittest import mock
|
|
158
|
+
|
|
159
|
+
from face_engine import core
|
|
160
|
+
|
|
161
|
+
with mock.patch.dict(core._models):
|
|
162
|
+
core._models.pop("minifasnet", None)
|
|
163
|
+
with self.assertRaises(ModelNotFoundError) as context:
|
|
164
|
+
core._resolve("minifasnet", (), "antispoof")
|
|
165
|
+
# the hint names the missing dependency, not a module import
|
|
166
|
+
self.assertIn("onnxruntime", str(context.exception))
|
|
167
|
+
self.assertNotIn("imported", str(context.exception))
|
|
168
|
+
|
|
154
169
|
def test_check_liveness_with_abstract_raises(self):
|
|
155
170
|
with self.assertRaises(NotImplementedError):
|
|
156
171
|
self.test_engine.check_liveness(
|
|
@@ -179,6 +194,59 @@ class TestFaceEngine(TestCase):
|
|
|
179
194
|
self.assertGreater(len(scores), 1)
|
|
180
195
|
self.assertTrue(all(0.0 <= s <= 1.0 for s in scores))
|
|
181
196
|
|
|
197
|
+
def test_compare_identical_embeddings(self):
|
|
198
|
+
embedding = np.array([0.5, -0.25, 0.75])
|
|
199
|
+
self.assertAlmostEqual(self.test_engine.compare(embedding, embedding), 1.0)
|
|
200
|
+
|
|
201
|
+
def test_compare_orthogonal_embeddings(self):
|
|
202
|
+
source = np.array([1.0, 0.0])
|
|
203
|
+
target = np.array([0.0, 1.0])
|
|
204
|
+
self.assertAlmostEqual(self.test_engine.compare(source, target), 0.0)
|
|
205
|
+
|
|
206
|
+
def test_compare_opposite_embeddings(self):
|
|
207
|
+
embedding = np.array([0.5, -0.25, 0.75])
|
|
208
|
+
self.assertAlmostEqual(self.test_engine.compare(embedding, -embedding), -1.0)
|
|
209
|
+
|
|
210
|
+
def test_compare_zero_vector_scores_zero(self):
|
|
211
|
+
source = np.zeros(3)
|
|
212
|
+
target = np.array([1.0, 2.0, 3.0])
|
|
213
|
+
self.assertEqual(self.test_engine.compare(source, target), 0.0)
|
|
214
|
+
|
|
215
|
+
def test_compare_accepts_single_row_matrices(self):
|
|
216
|
+
# compute_embeddings returns (n_faces, dim); a single-face row
|
|
217
|
+
# must be comparable without an explicit squeeze
|
|
218
|
+
source = np.array([[1.0, 2.0, 3.0]])
|
|
219
|
+
target = np.array([1.0, 2.0, 3.0])
|
|
220
|
+
self.assertAlmostEqual(self.test_engine.compare(source, target), 1.0)
|
|
221
|
+
|
|
222
|
+
def test_compare_returns_python_float(self):
|
|
223
|
+
source = np.array([1.0, 2.0], dtype=np.float32)
|
|
224
|
+
target = np.array([2.0, 1.0], dtype=np.float32)
|
|
225
|
+
self.assertIsInstance(self.test_engine.compare(source, target), float)
|
|
226
|
+
|
|
227
|
+
def _assert_same_face_scores_higher(self, engine):
|
|
228
|
+
image1 = imread(self.bubbles1)
|
|
229
|
+
bbs1, extra1 = engine.find_faces(image1, limit=1)
|
|
230
|
+
same1 = engine.compute_embeddings(image1, bbs1, **extra1)[0]
|
|
231
|
+
image2 = imread(self.bubbles2)
|
|
232
|
+
bbs2, extra2 = engine.find_faces(image2, limit=1)
|
|
233
|
+
same2 = engine.compute_embeddings(image2, bbs2, **extra2)[0]
|
|
234
|
+
image3 = imread(self.drive)
|
|
235
|
+
bbs3, extra3 = engine.find_faces(image3, limit=1)
|
|
236
|
+
other = engine.compute_embeddings(image3, bbs3, **extra3)[0]
|
|
237
|
+
same_score = engine.compare(same1, same2)
|
|
238
|
+
other_score = engine.compare(same1, other)
|
|
239
|
+
self.assertGreater(same_score, other_score)
|
|
240
|
+
|
|
241
|
+
@unittest.skipUnless(dlib, "dlib package is not installed")
|
|
242
|
+
def test_compare_same_face_scores_higher_dlib(self):
|
|
243
|
+
self._assert_same_face_scores_higher(self.dlib_engine)
|
|
244
|
+
|
|
245
|
+
@unittest.skipUnless(insightface, "insightface package is not installed")
|
|
246
|
+
def test_compare_same_face_scores_higher_insightface(self):
|
|
247
|
+
engine = FaceEngine(detector="scrfd", embedder="arcface")
|
|
248
|
+
self._assert_same_face_scores_higher(engine)
|
|
249
|
+
|
|
182
250
|
@unittest.skipUnless(dlib, "dlib package is not installed")
|
|
183
251
|
def test_fit_bubbles(self):
|
|
184
252
|
images = [self.bubbles1, self.bubbles2]
|
|
@@ -8,6 +8,7 @@ import numpy as np
|
|
|
8
8
|
from tests import TestCase, dlib, insightface
|
|
9
9
|
|
|
10
10
|
from face_engine import FaceEngine, load_engine
|
|
11
|
+
from face_engine.exceptions import ModelNotFoundError
|
|
11
12
|
from face_engine.models.basic_estimator import BasicEstimator
|
|
12
13
|
|
|
13
14
|
|
|
@@ -85,6 +86,17 @@ class TestFaceEnginePersistence(TestCase):
|
|
|
85
86
|
np.testing.assert_array_equal(bbs, restored_bbs)
|
|
86
87
|
self.assertEqual(class_names, restored_class_names)
|
|
87
88
|
|
|
89
|
+
def test_load_engine_unregistered_model_raises(self):
|
|
90
|
+
self.test_engine.save(self.filename)
|
|
91
|
+
with open(self.filename) as file:
|
|
92
|
+
data = json.load(file)
|
|
93
|
+
# e.g. a plugin model whose module is not imported on this install
|
|
94
|
+
data["detector"] = "unregistered_plugin_detector"
|
|
95
|
+
with open(self.filename, "w") as file:
|
|
96
|
+
json.dump(data, file)
|
|
97
|
+
with self.assertRaises(ModelNotFoundError):
|
|
98
|
+
load_engine(self.filename)
|
|
99
|
+
|
|
88
100
|
def test_load_engine_legacy_pickle_raises(self):
|
|
89
101
|
with open("test_engine.p", "wb") as file:
|
|
90
102
|
pickle.dump({"detector": "hog"}, file)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import unittest
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
from tests import TestCase
|
|
7
|
+
|
|
8
|
+
from face_engine.tools import imread
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestImread(TestCase):
|
|
12
|
+
|
|
13
|
+
def test_imread_file_path(self):
|
|
14
|
+
image = imread(self.bubbles1)
|
|
15
|
+
self.assertEqual(image.dtype, np.uint8)
|
|
16
|
+
self.assertEqual(image.ndim, 3)
|
|
17
|
+
|
|
18
|
+
def test_imread_bytes(self):
|
|
19
|
+
with open(self.bubbles1, "rb") as file:
|
|
20
|
+
data = file.read()
|
|
21
|
+
np.testing.assert_array_equal(imread(data), imread(self.bubbles1))
|
|
22
|
+
|
|
23
|
+
def test_imread_file_object(self):
|
|
24
|
+
with open(self.bubbles1, "rb") as file:
|
|
25
|
+
data = file.read()
|
|
26
|
+
np.testing.assert_array_equal(imread(io.BytesIO(data)), imread(self.bubbles1))
|
|
27
|
+
|
|
28
|
+
def test_imread_mode_conversion(self):
|
|
29
|
+
image = imread(self.bubbles1, mode="L")
|
|
30
|
+
self.assertEqual(image.ndim, 2)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
if __name__ == "__main__":
|
|
34
|
+
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|