face-engine 3.0.0__tar.gz → 3.1.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.0.0/face_engine.egg-info → face_engine-3.1.0}/PKG-INFO +31 -7
- {face_engine-3.0.0 → face_engine-3.1.0}/README.rst +30 -6
- {face_engine-3.0.0 → face_engine-3.1.0}/face_engine/__init__.py +1 -1
- {face_engine-3.0.0 → face_engine-3.1.0}/face_engine/core.py +73 -2
- {face_engine-3.0.0 → face_engine-3.1.0}/face_engine/fetching.py +3 -0
- {face_engine-3.0.0 → face_engine-3.1.0}/face_engine/models/__init__.py +24 -1
- face_engine-3.1.0/face_engine/models/_onnx.py +19 -0
- {face_engine-3.0.0 → face_engine-3.1.0}/face_engine/models/insightface_models.py +1 -13
- face_engine-3.1.0/face_engine/models/minifasnet.py +127 -0
- {face_engine-3.0.0 → face_engine-3.1.0}/face_engine/tools.py +1 -1
- {face_engine-3.0.0 → face_engine-3.1.0/face_engine.egg-info}/PKG-INFO +31 -7
- {face_engine-3.0.0 → face_engine-3.1.0}/face_engine.egg-info/SOURCES.txt +2 -0
- {face_engine-3.0.0 → face_engine-3.1.0}/tests/test_face_engine.py +40 -2
- {face_engine-3.0.0 → face_engine-3.1.0}/tests/test_face_engine_models.py +38 -2
- {face_engine-3.0.0 → face_engine-3.1.0}/tests/test_face_engine_persistence.py +12 -0
- {face_engine-3.0.0 → face_engine-3.1.0}/LICENSE +0 -0
- {face_engine-3.0.0 → face_engine-3.1.0}/MANIFEST.in +0 -0
- {face_engine-3.0.0 → face_engine-3.1.0}/docs/source/conf.py +0 -0
- {face_engine-3.0.0 → face_engine-3.1.0}/face_engine/exceptions.py +0 -0
- {face_engine-3.0.0 → face_engine-3.1.0}/face_engine/models/basic_estimator.py +0 -0
- {face_engine-3.0.0 → face_engine-3.1.0}/face_engine/models/dlib_models.py +0 -0
- {face_engine-3.0.0 → face_engine-3.1.0}/face_engine.egg-info/dependency_links.txt +0 -0
- {face_engine-3.0.0 → face_engine-3.1.0}/face_engine.egg-info/requires.txt +0 -0
- {face_engine-3.0.0 → face_engine-3.1.0}/face_engine.egg-info/top_level.txt +0 -0
- {face_engine-3.0.0 → face_engine-3.1.0}/pyproject.toml +0 -0
- {face_engine-3.0.0 → face_engine-3.1.0}/setup.cfg +0 -0
- {face_engine-3.0.0 → face_engine-3.1.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.1.0
|
|
4
4
|
Summary: Face Recognition Engine
|
|
5
5
|
Author-email: Daniyar Kussainov <ohw0sseug@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -68,23 +68,25 @@ FaceEngine is supported only on Python 3.11 and above.
|
|
|
68
68
|
Models
|
|
69
69
|
------
|
|
70
70
|
|
|
71
|
-
FaceEngine is built on top of
|
|
72
|
-
and ``
|
|
73
|
-
of these models.
|
|
71
|
+
FaceEngine is built on top of four model interfaces ``Detector``, ``Embedder``,
|
|
72
|
+
``Estimator`` and ``Antispoof`` (see `models`_), and leans on user provided
|
|
73
|
+
implementations of these models.
|
|
74
74
|
|
|
75
75
|
The default backend is `insightface`_ (the ``[insightface]`` extra), with
|
|
76
76
|
these bundled model implementations:
|
|
77
77
|
|
|
78
|
-
======================= ========== ============
|
|
78
|
+
======================= ========== ============ ==========================
|
|
79
79
|
name role model pack notes
|
|
80
|
-
======================= ========== ============
|
|
80
|
+
======================= ========== ============ ==========================
|
|
81
81
|
``scrfd`` detector buffalo_l default; deprecated alias
|
|
82
82
|
``retina_face``
|
|
83
83
|
``arcface`` embedder buffalo_l default, 512-d
|
|
84
84
|
``scrfd_antelopev2`` detector antelopev2 opt-in
|
|
85
85
|
``arcface_antelopev2`` embedder antelopev2 strongest insightface
|
|
86
86
|
embedder, 512-d
|
|
87
|
-
|
|
87
|
+
``minifasnet`` antispoof -- passive anti-spoofing,
|
|
88
|
+
opt-in
|
|
89
|
+
======================= ========== ============ ==========================
|
|
88
90
|
|
|
89
91
|
.. note::
|
|
90
92
|
The ``[insightface]`` extra installs the CPU build of onnxruntime.
|
|
@@ -111,6 +113,24 @@ To work with your own custom models you have to implement required
|
|
|
111
113
|
`models`_ and import it. FaceEngine models are used to register all inheriting
|
|
112
114
|
imported subclasses (subclass registration `PEP 487`_).
|
|
113
115
|
|
|
116
|
+
Face anti-spoofing
|
|
117
|
+
------------------
|
|
118
|
+
|
|
119
|
+
Since 3.1 the engine has an opt-in liveness (presentation attack detection)
|
|
120
|
+
step powered by the ``Antispoof`` model interface:
|
|
121
|
+
|
|
122
|
+
.. code-block:: python
|
|
123
|
+
|
|
124
|
+
>>> engine = FaceEngine(antispoof="minifasnet")
|
|
125
|
+
>>> engine.check_liveness('bubbles1.jpg')
|
|
126
|
+
array([0.971], dtype=float32)
|
|
127
|
+
|
|
128
|
+
The bundled ``minifasnet`` model is an ensemble of the two released
|
|
129
|
+
`Silent-Face-Anti-Spoofing`_ MiniFASNet models (requires ``onnxruntime``,
|
|
130
|
+
already present with the ``[insightface]`` extra). It is effective against
|
|
131
|
+
printed photos and basic screen replays; it is **not** a certified
|
|
132
|
+
(ISO/IEC 30107-3) liveness solution.
|
|
133
|
+
|
|
114
134
|
Model weights licensing
|
|
115
135
|
-----------------------
|
|
116
136
|
|
|
@@ -120,6 +140,9 @@ come with their own terms:
|
|
|
120
140
|
* insightface model packs (``buffalo_l``, ``antelopev2``) are available for
|
|
121
141
|
**non-commercial research purposes only** (see `insightface`_).
|
|
122
142
|
* dlib model files have their own terms, see `dlib-models`_.
|
|
143
|
+
* ``minifasnet`` model weights are **Apache-2.0** (usable commercially) —
|
|
144
|
+
ONNX exports of the `Silent-Face-Anti-Spoofing`_ checkpoints, reproducible
|
|
145
|
+
with ``extra/export_minifasnet.py``.
|
|
123
146
|
|
|
124
147
|
Breaking changes in 3.0
|
|
125
148
|
-----------------------
|
|
@@ -139,6 +162,7 @@ For more information read the full `documentation`_.
|
|
|
139
162
|
|
|
140
163
|
.. _models: https://github.com/guesswh0/face_engine/blob/master/face_engine/models/__init__.py
|
|
141
164
|
.. _insightface: https://github.com/deepinsight/insightface
|
|
165
|
+
.. _Silent-Face-Anti-Spoofing: https://github.com/minivision-ai/Silent-Face-Anti-Spoofing
|
|
142
166
|
.. _dlib python api: http://dlib.net/python/index.html
|
|
143
167
|
.. _files: https://github.com/davisking/dlib-models
|
|
144
168
|
.. _dlib-models: https://github.com/davisking/dlib-models
|
|
@@ -29,23 +29,25 @@ FaceEngine is supported only on Python 3.11 and above.
|
|
|
29
29
|
Models
|
|
30
30
|
------
|
|
31
31
|
|
|
32
|
-
FaceEngine is built on top of
|
|
33
|
-
and ``
|
|
34
|
-
of these models.
|
|
32
|
+
FaceEngine is built on top of four model interfaces ``Detector``, ``Embedder``,
|
|
33
|
+
``Estimator`` and ``Antispoof`` (see `models`_), and leans on user provided
|
|
34
|
+
implementations of these models.
|
|
35
35
|
|
|
36
36
|
The default backend is `insightface`_ (the ``[insightface]`` extra), with
|
|
37
37
|
these bundled model implementations:
|
|
38
38
|
|
|
39
|
-
======================= ========== ============
|
|
39
|
+
======================= ========== ============ ==========================
|
|
40
40
|
name role model pack notes
|
|
41
|
-
======================= ========== ============
|
|
41
|
+
======================= ========== ============ ==========================
|
|
42
42
|
``scrfd`` detector buffalo_l default; deprecated alias
|
|
43
43
|
``retina_face``
|
|
44
44
|
``arcface`` embedder buffalo_l default, 512-d
|
|
45
45
|
``scrfd_antelopev2`` detector antelopev2 opt-in
|
|
46
46
|
``arcface_antelopev2`` embedder antelopev2 strongest insightface
|
|
47
47
|
embedder, 512-d
|
|
48
|
-
|
|
48
|
+
``minifasnet`` antispoof -- passive anti-spoofing,
|
|
49
|
+
opt-in
|
|
50
|
+
======================= ========== ============ ==========================
|
|
49
51
|
|
|
50
52
|
.. note::
|
|
51
53
|
The ``[insightface]`` extra installs the CPU build of onnxruntime.
|
|
@@ -72,6 +74,24 @@ To work with your own custom models you have to implement required
|
|
|
72
74
|
`models`_ and import it. FaceEngine models are used to register all inheriting
|
|
73
75
|
imported subclasses (subclass registration `PEP 487`_).
|
|
74
76
|
|
|
77
|
+
Face anti-spoofing
|
|
78
|
+
------------------
|
|
79
|
+
|
|
80
|
+
Since 3.1 the engine has an opt-in liveness (presentation attack detection)
|
|
81
|
+
step powered by the ``Antispoof`` model interface:
|
|
82
|
+
|
|
83
|
+
.. code-block:: python
|
|
84
|
+
|
|
85
|
+
>>> engine = FaceEngine(antispoof="minifasnet")
|
|
86
|
+
>>> engine.check_liveness('bubbles1.jpg')
|
|
87
|
+
array([0.971], dtype=float32)
|
|
88
|
+
|
|
89
|
+
The bundled ``minifasnet`` model is an ensemble of the two released
|
|
90
|
+
`Silent-Face-Anti-Spoofing`_ MiniFASNet models (requires ``onnxruntime``,
|
|
91
|
+
already present with the ``[insightface]`` extra). It is effective against
|
|
92
|
+
printed photos and basic screen replays; it is **not** a certified
|
|
93
|
+
(ISO/IEC 30107-3) liveness solution.
|
|
94
|
+
|
|
75
95
|
Model weights licensing
|
|
76
96
|
-----------------------
|
|
77
97
|
|
|
@@ -81,6 +101,9 @@ come with their own terms:
|
|
|
81
101
|
* insightface model packs (``buffalo_l``, ``antelopev2``) are available for
|
|
82
102
|
**non-commercial research purposes only** (see `insightface`_).
|
|
83
103
|
* dlib model files have their own terms, see `dlib-models`_.
|
|
104
|
+
* ``minifasnet`` model weights are **Apache-2.0** (usable commercially) —
|
|
105
|
+
ONNX exports of the `Silent-Face-Anti-Spoofing`_ checkpoints, reproducible
|
|
106
|
+
with ``extra/export_minifasnet.py``.
|
|
84
107
|
|
|
85
108
|
Breaking changes in 3.0
|
|
86
109
|
-----------------------
|
|
@@ -100,6 +123,7 @@ For more information read the full `documentation`_.
|
|
|
100
123
|
|
|
101
124
|
.. _models: https://github.com/guesswh0/face_engine/blob/master/face_engine/models/__init__.py
|
|
102
125
|
.. _insightface: https://github.com/deepinsight/insightface
|
|
126
|
+
.. _Silent-Face-Anti-Spoofing: https://github.com/minivision-ai/Silent-Face-Anti-Spoofing
|
|
103
127
|
.. _dlib python api: http://dlib.net/python/index.html
|
|
104
128
|
.. _files: https://github.com/davisking/dlib-models
|
|
105
129
|
.. _dlib-models: https://github.com/davisking/dlib-models
|
|
@@ -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.1.0"
|
|
7
7
|
__author__ = "Daniyar Kussainov"
|
|
8
8
|
__license__ = "Apache License, Version 2.0"
|
|
9
9
|
__copyright__ = "Copyright 2019-2026 Daniyar Kussainov"
|
|
@@ -17,6 +17,8 @@ from .tools import imread
|
|
|
17
17
|
_DETECTOR_DEFAULTS = ("scrfd", "hog")
|
|
18
18
|
_EMBEDDER_DEFAULTS = ("arcface", "resnet")
|
|
19
19
|
_ESTIMATOR_DEFAULTS = ("basic",)
|
|
20
|
+
# antispoof is opt-in: the default is the abstract no-op model
|
|
21
|
+
_ANTISPOOF_DEFAULTS = ()
|
|
20
22
|
|
|
21
23
|
|
|
22
24
|
def _resolve(name: Optional[str], defaults: Tuple[str, ...], kind: str) -> str:
|
|
@@ -84,6 +86,8 @@ def load_engine(filename: str) -> "FaceEngine":
|
|
|
84
86
|
detector=data.get("detector"),
|
|
85
87
|
embedder=data.get("embedder"),
|
|
86
88
|
estimator=data.get("estimator"),
|
|
89
|
+
# absent in files saved by face-engine < 3.1
|
|
90
|
+
antispoof=data.get("antispoof"),
|
|
87
91
|
)
|
|
88
92
|
engine.n_classes = data.get("n_classes", 0)
|
|
89
93
|
engine.n_samples = data.get("n_samples", 0)
|
|
@@ -107,6 +111,7 @@ class FaceEngine:
|
|
|
107
111
|
* detector (str) -- face detector model to use
|
|
108
112
|
* embedder (str) -- face embedder model to use
|
|
109
113
|
* estimator (str) -- face estimator model to use
|
|
114
|
+
* antispoof (str) -- face anti-spoofing model to use (opt-in)
|
|
110
115
|
"""
|
|
111
116
|
|
|
112
117
|
def __init__(self, **kwargs: Any) -> None:
|
|
@@ -116,6 +121,8 @@ class FaceEngine:
|
|
|
116
121
|
self.detector = kwargs.get("detector")
|
|
117
122
|
self.embedder = kwargs.get("embedder")
|
|
118
123
|
self.estimator = kwargs.get("estimator")
|
|
124
|
+
# opt-in anti-spoofing model, abstract no-op by default
|
|
125
|
+
self.antispoof = kwargs.get("antispoof")
|
|
119
126
|
# keep last fitted number of classes and samples
|
|
120
127
|
self.n_classes = 0
|
|
121
128
|
self.n_samples = 0
|
|
@@ -191,6 +198,28 @@ class FaceEngine:
|
|
|
191
198
|
Estimator = _models.get(_resolve(name, _ESTIMATOR_DEFAULTS, "estimator"))
|
|
192
199
|
self._estimator = Estimator()
|
|
193
200
|
|
|
201
|
+
@property
|
|
202
|
+
def antispoof(self) -> str:
|
|
203
|
+
"""
|
|
204
|
+
:return: antispoof model name
|
|
205
|
+
:rtype: str
|
|
206
|
+
"""
|
|
207
|
+
|
|
208
|
+
return self._antispoof.name
|
|
209
|
+
|
|
210
|
+
@antispoof.setter
|
|
211
|
+
def antispoof(self, name: Optional[str]) -> None:
|
|
212
|
+
"""Face anti-spoofing model to use:
|
|
213
|
+
- 'minifasnet': MiniFASNet passive anti-spoofing ensemble
|
|
214
|
+
- by default the abstract no-op model is used
|
|
215
|
+
|
|
216
|
+
:param name: antispoof model name
|
|
217
|
+
:type name: str
|
|
218
|
+
"""
|
|
219
|
+
|
|
220
|
+
Antispoof = _models.get(_resolve(name, _ANTISPOOF_DEFAULTS, "antispoof"))
|
|
221
|
+
self._antispoof = Antispoof()
|
|
222
|
+
|
|
194
223
|
def save(self, filename: str) -> None:
|
|
195
224
|
"""Save engine object state to the file as JSON.
|
|
196
225
|
|
|
@@ -205,12 +234,19 @@ class FaceEngine:
|
|
|
205
234
|
if self.n_samples > 0:
|
|
206
235
|
self._estimator.save(os.path.dirname(filename))
|
|
207
236
|
|
|
208
|
-
# everything beyond the
|
|
237
|
+
# everything beyond the models and counters goes to "extra"
|
|
209
238
|
extra = {
|
|
210
239
|
key: value
|
|
211
240
|
for key, value in self.__dict__.items()
|
|
212
241
|
if key
|
|
213
|
-
not in (
|
|
242
|
+
not in (
|
|
243
|
+
"_detector",
|
|
244
|
+
"_embedder",
|
|
245
|
+
"_estimator",
|
|
246
|
+
"_antispoof",
|
|
247
|
+
"n_classes",
|
|
248
|
+
"n_samples",
|
|
249
|
+
)
|
|
214
250
|
}
|
|
215
251
|
data = {
|
|
216
252
|
"format": "face-engine",
|
|
@@ -219,6 +255,7 @@ class FaceEngine:
|
|
|
219
255
|
"detector": self.detector,
|
|
220
256
|
"embedder": self.embedder,
|
|
221
257
|
"estimator": self.estimator,
|
|
258
|
+
"antispoof": self.antispoof,
|
|
222
259
|
"n_classes": self.n_classes,
|
|
223
260
|
"n_samples": self.n_samples,
|
|
224
261
|
}
|
|
@@ -405,6 +442,40 @@ class FaceEngine:
|
|
|
405
442
|
bbs = bbs / ([width, height] * 2)
|
|
406
443
|
return bbs, extra
|
|
407
444
|
|
|
445
|
+
def check_liveness(
|
|
446
|
+
self,
|
|
447
|
+
image: Union[str, bytes, os.PathLike, np.ndarray],
|
|
448
|
+
bounding_boxes: Optional[np.ndarray] = None,
|
|
449
|
+
) -> np.ndarray:
|
|
450
|
+
"""Predict live-face probabilities for faces in the image.
|
|
451
|
+
|
|
452
|
+
Antispoof's :meth:`~face_engine.models.Antispoof.predict`
|
|
453
|
+
wrapping method. When ``bounding_boxes`` is not given, faces are
|
|
454
|
+
located with :meth:`.find_faces` first.
|
|
455
|
+
|
|
456
|
+
.. note::
|
|
457
|
+
The default abstract antispoof model raises
|
|
458
|
+
``NotImplementedError`` — set :attr:`antispoof` first
|
|
459
|
+
(e.g. ``engine.antispoof = "minifasnet"``).
|
|
460
|
+
|
|
461
|
+
:param image: RGB image content or image file uri.
|
|
462
|
+
:type image: Union[str, bytes, file, os.PathLike, numpy.ndarray]
|
|
463
|
+
|
|
464
|
+
:param bounding_boxes: face bounding boxes, found when omitted
|
|
465
|
+
:type bounding_boxes: numpy.ndarray
|
|
466
|
+
|
|
467
|
+
:returns: live-face probabilities in [0, 1] with shape (n_faces,)
|
|
468
|
+
:rtype: numpy.ndarray
|
|
469
|
+
|
|
470
|
+
:raises: FaceNotFoundError
|
|
471
|
+
"""
|
|
472
|
+
|
|
473
|
+
if not hasattr(image, "shape"):
|
|
474
|
+
image = imread(image)
|
|
475
|
+
if bounding_boxes is None:
|
|
476
|
+
bounding_boxes, _ = self.find_faces(image)
|
|
477
|
+
return self._antispoof.predict(image, bounding_boxes)
|
|
478
|
+
|
|
408
479
|
def compute_embeddings(
|
|
409
480
|
self, image: np.ndarray, bounding_boxes: np.ndarray, **kwargs: Any
|
|
410
481
|
) -> np.ndarray:
|
|
@@ -27,6 +27,9 @@ KNOWN_HASHES = {
|
|
|
27
27
|
# insightface model packs (v0.7 github release assets)
|
|
28
28
|
"buffalo_l.zip": "80ffe37d8a5940d59a7384c201a2a38d4741f2f3c51eef46ebb28218a7b0ca2f", # noqa: E501
|
|
29
29
|
"antelopev2.zip": "8e182f14fc6e80b3bfa375b33eb6cff7ee05d8ef7633e738d1c89021dcf0c5c5", # noqa: E501
|
|
30
|
+
# minifasnet anti-spoofing onnx exports, see extra/export_minifasnet.py
|
|
31
|
+
"2.7_80x80_MiniFASNetV2.onnx": "3052e9d1d97270f5d9f197bed4f039cae23b8c14d6282e879ca3a63073792a97", # noqa: E501
|
|
32
|
+
"4_0_0_80x80_MiniFASNetV1SE.onnx": "2348be428f787149bf28dca49f802bd7ab9280ff3333475f76df1aafd2dada4f", # noqa: E501
|
|
30
33
|
# test assets (https://github.com/guesswh0/storage)
|
|
31
34
|
"test.zip": "7e967899f1a106908798d750f81d3a966422300b479757b600eaf3ea4d96723b",
|
|
32
35
|
"train.zip": "d61d21d6b0198df9b2f045de784924984177042a595a1a5fd42c3771b2b24b15",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
FaceEngine models API.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
__all__ = ["Detector", "Embedder", "Estimator", "_models"]
|
|
5
|
+
__all__ = ["Detector", "Embedder", "Estimator", "Antispoof", "_models"]
|
|
6
6
|
|
|
7
7
|
from face_engine.tools import import_package
|
|
8
8
|
|
|
@@ -88,6 +88,29 @@ class Embedder(Model):
|
|
|
88
88
|
raise NotImplementedError()
|
|
89
89
|
|
|
90
90
|
|
|
91
|
+
class Antispoof(Model):
|
|
92
|
+
"""Face anti-spoofing (presentation attack detection) model base class.
|
|
93
|
+
|
|
94
|
+
Estimates whether the faces in the image belong to live persons or
|
|
95
|
+
to presentation attacks (printed photos, screen replays, masks).
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
def predict(self, image, bounding_boxes):
|
|
99
|
+
"""Predict live-face probabilities for given bounding boxes.
|
|
100
|
+
|
|
101
|
+
:param image: RGB image with shape (rows, cols, 3)
|
|
102
|
+
:type image: numpy.ndarray
|
|
103
|
+
|
|
104
|
+
:param bounding_boxes: bounding boxes with shape (n_faces, 4)
|
|
105
|
+
:type bounding_boxes: numpy.ndarray
|
|
106
|
+
|
|
107
|
+
:returns: live-face probabilities in [0, 1] with shape (n_faces,)
|
|
108
|
+
:rtype: numpy.ndarray
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
raise NotImplementedError()
|
|
112
|
+
|
|
113
|
+
|
|
91
114
|
class Estimator(Model):
|
|
92
115
|
"""Estimator model base class. Used to make predictions for face
|
|
93
116
|
embedding vectors.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Shared helpers for onnxruntime backed models.
|
|
2
|
+
|
|
3
|
+
Underscore-prefixed so ``import_package`` skips it: importing it must stay
|
|
4
|
+
an explicit choice of modules that already require onnxruntime.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import onnxruntime
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _providers():
|
|
11
|
+
"""Cuda when available, cpu otherwise.
|
|
12
|
+
|
|
13
|
+
Insightface requests CUDAExecutionProvider unconditionally, making
|
|
14
|
+
onnxruntime warn on every non-CUDA machine.
|
|
15
|
+
"""
|
|
16
|
+
available = onnxruntime.get_available_providers()
|
|
17
|
+
return [
|
|
18
|
+
p for p in ("CUDAExecutionProvider", "CPUExecutionProvider") if p in available
|
|
19
|
+
]
|
|
@@ -2,7 +2,6 @@ import os
|
|
|
2
2
|
import warnings
|
|
3
3
|
|
|
4
4
|
import numpy as np
|
|
5
|
-
import onnxruntime
|
|
6
5
|
from insightface.model_zoo import model_zoo
|
|
7
6
|
from insightface.utils import face_align
|
|
8
7
|
|
|
@@ -10,6 +9,7 @@ from face_engine import RESOURCES
|
|
|
10
9
|
from face_engine.exceptions import FaceNotFoundError
|
|
11
10
|
from face_engine.fetching import fetch_file
|
|
12
11
|
from face_engine.models import Detector, Embedder
|
|
12
|
+
from face_engine.models._onnx import _providers
|
|
13
13
|
|
|
14
14
|
# model packs from the insightface v0.7 release assets
|
|
15
15
|
# (storage.insightface.ai is no longer available);
|
|
@@ -17,18 +17,6 @@ from face_engine.models import Detector, Embedder
|
|
|
17
17
|
_PACK_URL = "https://github.com/deepinsight/insightface/releases/download/v0.7/{}.zip"
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
def _providers():
|
|
21
|
-
"""Cuda when available, cpu otherwise.
|
|
22
|
-
|
|
23
|
-
Insightface requests CUDAExecutionProvider unconditionally, making
|
|
24
|
-
onnxruntime warn on every non-CUDA machine.
|
|
25
|
-
"""
|
|
26
|
-
available = onnxruntime.get_available_providers()
|
|
27
|
-
return [
|
|
28
|
-
p for p in ("CUDAExecutionProvider", "CPUExecutionProvider") if p in available
|
|
29
|
-
]
|
|
30
|
-
|
|
31
|
-
|
|
32
20
|
def _fetch_pack(pack, filename):
|
|
33
21
|
"""Fetch insightface model pack and return path to the model file."""
|
|
34
22
|
extract_dir = os.path.join(RESOURCES, "models", pack)
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import onnxruntime
|
|
5
|
+
|
|
6
|
+
from face_engine import RESOURCES
|
|
7
|
+
from face_engine.fetching import fetch_file
|
|
8
|
+
from face_engine.models import Antispoof
|
|
9
|
+
from face_engine.models._onnx import _providers
|
|
10
|
+
|
|
11
|
+
# apache-2.0 onnx exports, see extra/export_minifasnet.py
|
|
12
|
+
_ASSET_URL = "https://github.com/guesswh0/face_engine/releases/download/v3.0.0/{}"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _softmax(logits):
|
|
16
|
+
exp = np.exp(logits - logits.max(axis=1, keepdims=True))
|
|
17
|
+
return exp / exp.sum(axis=1, keepdims=True)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _resize_bilinear(image, out_h, out_w):
|
|
21
|
+
"""cv2.resize(INTER_LINEAR) equivalent, to avoid an opencv dependency."""
|
|
22
|
+
in_h, in_w = image.shape[:2]
|
|
23
|
+
ys = (np.arange(out_h) + 0.5) * in_h / out_h - 0.5
|
|
24
|
+
xs = (np.arange(out_w) + 0.5) * in_w / out_w - 0.5
|
|
25
|
+
y0 = np.floor(ys).astype(int)
|
|
26
|
+
x0 = np.floor(xs).astype(int)
|
|
27
|
+
wy = (ys - y0)[:, None, None]
|
|
28
|
+
wx = (xs - x0)[None, :, None]
|
|
29
|
+
y0c, y1c = np.clip(y0, 0, in_h - 1), np.clip(y0 + 1, 0, in_h - 1)
|
|
30
|
+
x0c, x1c = np.clip(x0, 0, in_w - 1), np.clip(x0 + 1, 0, in_w - 1)
|
|
31
|
+
# gather the sampled rows first, cast only the gathered data
|
|
32
|
+
top_rows = image[y0c].astype(np.float32)
|
|
33
|
+
bottom_rows = image[y1c].astype(np.float32)
|
|
34
|
+
top = top_rows[:, x0c] * (1 - wx) + top_rows[:, x1c] * wx
|
|
35
|
+
bottom = bottom_rows[:, x0c] * (1 - wx) + bottom_rows[:, x1c] * wx
|
|
36
|
+
# official pipeline resizes uint8 images, so round like cv2 does
|
|
37
|
+
return np.rint(top * (1 - wy) + bottom * wy)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class MiniFASNetAntispoof(Antispoof, name="minifasnet"):
|
|
41
|
+
"""MiniFASNet passive anti-spoofing ensemble (Silent-Face-Anti-Spoofing).
|
|
42
|
+
|
|
43
|
+
Ensemble of the two released 80x80 models, each run on its own
|
|
44
|
+
scale-cropped patch around the face; the averaged softmax gives the
|
|
45
|
+
live-face probability.
|
|
46
|
+
|
|
47
|
+
.. note::
|
|
48
|
+
* both code and model weights are Apache-2.0 — usable in
|
|
49
|
+
commercial products.
|
|
50
|
+
* effective against printed photos and basic screen replays;
|
|
51
|
+
not a certified (ISO/IEC 30107-3) liveness solution.
|
|
52
|
+
|
|
53
|
+
References:
|
|
54
|
+
1. https://github.com/minivision-ai/Silent-Face-Anti-Spoofing
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
# filename -> official crop scale (each model has its own)
|
|
58
|
+
files = (
|
|
59
|
+
("2.7_80x80_MiniFASNetV2.onnx", 2.7),
|
|
60
|
+
("4_0_0_80x80_MiniFASNetV1SE.onnx", 4.0),
|
|
61
|
+
)
|
|
62
|
+
input_size = (80, 80)
|
|
63
|
+
|
|
64
|
+
def __init__(self):
|
|
65
|
+
extract_dir = os.path.join(RESOURCES, "models", "minifasnet")
|
|
66
|
+
self._sessions = []
|
|
67
|
+
for filename, scale in self.files:
|
|
68
|
+
fetch_file(_ASSET_URL.format(filename), extract_dir)
|
|
69
|
+
session = onnxruntime.InferenceSession(
|
|
70
|
+
os.path.join(extract_dir, filename), providers=_providers()
|
|
71
|
+
)
|
|
72
|
+
self._sessions.append((session, scale))
|
|
73
|
+
|
|
74
|
+
def predict(self, image, bounding_boxes):
|
|
75
|
+
n_faces = len(bounding_boxes)
|
|
76
|
+
if n_faces == 0:
|
|
77
|
+
return np.empty(0, dtype=np.float32)
|
|
78
|
+
|
|
79
|
+
# models are trained on BGR images
|
|
80
|
+
image = image[..., ::-1]
|
|
81
|
+
live_probs = np.zeros(n_faces, dtype=np.float32)
|
|
82
|
+
for session, scale in self._sessions:
|
|
83
|
+
batch = np.stack([self._patch(image, bb, scale) for bb in bounding_boxes])
|
|
84
|
+
logits = session.run(None, {"input": batch})[0]
|
|
85
|
+
# softmax classes are (fake, live, fake)
|
|
86
|
+
live_probs += _softmax(logits)[:, 1]
|
|
87
|
+
return live_probs / len(self._sessions)
|
|
88
|
+
|
|
89
|
+
def _patch(self, image, bounding_box, scale):
|
|
90
|
+
"""Scale-cropped face patch as float32 CHW tensor in [0, 255].
|
|
91
|
+
|
|
92
|
+
Reproduces the official CropImage box math: aspect-preserving
|
|
93
|
+
scaled box around the face, shifted back inside the image, then
|
|
94
|
+
resized to the model input size (no normalization by design —
|
|
95
|
+
the models were trained on raw 0-255 inputs).
|
|
96
|
+
"""
|
|
97
|
+
src_h, src_w = image.shape[:2]
|
|
98
|
+
left, upper, right, lower = bounding_box
|
|
99
|
+
box_w, box_h = right - left + 1, lower - upper + 1
|
|
100
|
+
|
|
101
|
+
scale = min((src_h - 1) / box_h, (src_w - 1) / box_w, scale)
|
|
102
|
+
new_w, new_h = box_w * scale, box_h * scale
|
|
103
|
+
center_x, center_y = left + box_w / 2, upper + box_h / 2
|
|
104
|
+
|
|
105
|
+
left_top_x = center_x - new_w / 2
|
|
106
|
+
left_top_y = center_y - new_h / 2
|
|
107
|
+
right_bottom_x = center_x + new_w / 2
|
|
108
|
+
right_bottom_y = center_y + new_h / 2
|
|
109
|
+
if left_top_x < 0:
|
|
110
|
+
right_bottom_x -= left_top_x
|
|
111
|
+
left_top_x = 0
|
|
112
|
+
if left_top_y < 0:
|
|
113
|
+
right_bottom_y -= left_top_y
|
|
114
|
+
left_top_y = 0
|
|
115
|
+
if right_bottom_x > src_w - 1:
|
|
116
|
+
left_top_x -= right_bottom_x - src_w + 1
|
|
117
|
+
right_bottom_x = src_w - 1
|
|
118
|
+
if right_bottom_y > src_h - 1:
|
|
119
|
+
left_top_y -= right_bottom_y - src_h + 1
|
|
120
|
+
right_bottom_y = src_h - 1
|
|
121
|
+
|
|
122
|
+
patch = image[
|
|
123
|
+
int(left_top_y) : int(right_bottom_y) + 1,
|
|
124
|
+
int(left_top_x) : int(right_bottom_x) + 1,
|
|
125
|
+
]
|
|
126
|
+
patch = _resize_bilinear(patch, *self.input_size)
|
|
127
|
+
return patch.transpose((2, 0, 1)).astype(np.float32)
|
|
@@ -80,7 +80,7 @@ def import_module(filepath):
|
|
|
80
80
|
spec.loader.exec_module(module)
|
|
81
81
|
except ImportError:
|
|
82
82
|
# skip raising on project [optional] default models
|
|
83
|
-
if path.stem not in ["dlib_models", "insightface_models"]:
|
|
83
|
+
if path.stem not in ["dlib_models", "insightface_models", "minifasnet"]:
|
|
84
84
|
raise
|
|
85
85
|
|
|
86
86
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: face-engine
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.1.0
|
|
4
4
|
Summary: Face Recognition Engine
|
|
5
5
|
Author-email: Daniyar Kussainov <ohw0sseug@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -68,23 +68,25 @@ FaceEngine is supported only on Python 3.11 and above.
|
|
|
68
68
|
Models
|
|
69
69
|
------
|
|
70
70
|
|
|
71
|
-
FaceEngine is built on top of
|
|
72
|
-
and ``
|
|
73
|
-
of these models.
|
|
71
|
+
FaceEngine is built on top of four model interfaces ``Detector``, ``Embedder``,
|
|
72
|
+
``Estimator`` and ``Antispoof`` (see `models`_), and leans on user provided
|
|
73
|
+
implementations of these models.
|
|
74
74
|
|
|
75
75
|
The default backend is `insightface`_ (the ``[insightface]`` extra), with
|
|
76
76
|
these bundled model implementations:
|
|
77
77
|
|
|
78
|
-
======================= ========== ============
|
|
78
|
+
======================= ========== ============ ==========================
|
|
79
79
|
name role model pack notes
|
|
80
|
-
======================= ========== ============
|
|
80
|
+
======================= ========== ============ ==========================
|
|
81
81
|
``scrfd`` detector buffalo_l default; deprecated alias
|
|
82
82
|
``retina_face``
|
|
83
83
|
``arcface`` embedder buffalo_l default, 512-d
|
|
84
84
|
``scrfd_antelopev2`` detector antelopev2 opt-in
|
|
85
85
|
``arcface_antelopev2`` embedder antelopev2 strongest insightface
|
|
86
86
|
embedder, 512-d
|
|
87
|
-
|
|
87
|
+
``minifasnet`` antispoof -- passive anti-spoofing,
|
|
88
|
+
opt-in
|
|
89
|
+
======================= ========== ============ ==========================
|
|
88
90
|
|
|
89
91
|
.. note::
|
|
90
92
|
The ``[insightface]`` extra installs the CPU build of onnxruntime.
|
|
@@ -111,6 +113,24 @@ To work with your own custom models you have to implement required
|
|
|
111
113
|
`models`_ and import it. FaceEngine models are used to register all inheriting
|
|
112
114
|
imported subclasses (subclass registration `PEP 487`_).
|
|
113
115
|
|
|
116
|
+
Face anti-spoofing
|
|
117
|
+
------------------
|
|
118
|
+
|
|
119
|
+
Since 3.1 the engine has an opt-in liveness (presentation attack detection)
|
|
120
|
+
step powered by the ``Antispoof`` model interface:
|
|
121
|
+
|
|
122
|
+
.. code-block:: python
|
|
123
|
+
|
|
124
|
+
>>> engine = FaceEngine(antispoof="minifasnet")
|
|
125
|
+
>>> engine.check_liveness('bubbles1.jpg')
|
|
126
|
+
array([0.971], dtype=float32)
|
|
127
|
+
|
|
128
|
+
The bundled ``minifasnet`` model is an ensemble of the two released
|
|
129
|
+
`Silent-Face-Anti-Spoofing`_ MiniFASNet models (requires ``onnxruntime``,
|
|
130
|
+
already present with the ``[insightface]`` extra). It is effective against
|
|
131
|
+
printed photos and basic screen replays; it is **not** a certified
|
|
132
|
+
(ISO/IEC 30107-3) liveness solution.
|
|
133
|
+
|
|
114
134
|
Model weights licensing
|
|
115
135
|
-----------------------
|
|
116
136
|
|
|
@@ -120,6 +140,9 @@ come with their own terms:
|
|
|
120
140
|
* insightface model packs (``buffalo_l``, ``antelopev2``) are available for
|
|
121
141
|
**non-commercial research purposes only** (see `insightface`_).
|
|
122
142
|
* dlib model files have their own terms, see `dlib-models`_.
|
|
143
|
+
* ``minifasnet`` model weights are **Apache-2.0** (usable commercially) —
|
|
144
|
+
ONNX exports of the `Silent-Face-Anti-Spoofing`_ checkpoints, reproducible
|
|
145
|
+
with ``extra/export_minifasnet.py``.
|
|
123
146
|
|
|
124
147
|
Breaking changes in 3.0
|
|
125
148
|
-----------------------
|
|
@@ -139,6 +162,7 @@ For more information read the full `documentation`_.
|
|
|
139
162
|
|
|
140
163
|
.. _models: https://github.com/guesswh0/face_engine/blob/master/face_engine/models/__init__.py
|
|
141
164
|
.. _insightface: https://github.com/deepinsight/insightface
|
|
165
|
+
.. _Silent-Face-Anti-Spoofing: https://github.com/minivision-ai/Silent-Face-Anti-Spoofing
|
|
142
166
|
.. _dlib python api: http://dlib.net/python/index.html
|
|
143
167
|
.. _files: https://github.com/davisking/dlib-models
|
|
144
168
|
.. _dlib-models: https://github.com/davisking/dlib-models
|
|
@@ -14,9 +14,11 @@ face_engine.egg-info/dependency_links.txt
|
|
|
14
14
|
face_engine.egg-info/requires.txt
|
|
15
15
|
face_engine.egg-info/top_level.txt
|
|
16
16
|
face_engine/models/__init__.py
|
|
17
|
+
face_engine/models/_onnx.py
|
|
17
18
|
face_engine/models/basic_estimator.py
|
|
18
19
|
face_engine/models/dlib_models.py
|
|
19
20
|
face_engine/models/insightface_models.py
|
|
21
|
+
face_engine/models/minifasnet.py
|
|
20
22
|
tests/test_face_engine.py
|
|
21
23
|
tests/test_face_engine_models.py
|
|
22
24
|
tests/test_face_engine_persistence.py
|
|
@@ -5,9 +5,9 @@ import numpy as np
|
|
|
5
5
|
|
|
6
6
|
from face_engine import FaceEngine
|
|
7
7
|
from face_engine.exceptions import FaceNotFoundError, TrainError
|
|
8
|
-
from face_engine.models import Detector, Embedder, Estimator
|
|
8
|
+
from face_engine.models import Antispoof, Detector, Embedder, Estimator
|
|
9
9
|
from face_engine.tools import imread
|
|
10
|
-
from tests import TestCase, dlib, insightface
|
|
10
|
+
from tests import TestCase, dlib, insightface, onnxruntime
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class TestFaceEngine(TestCase):
|
|
@@ -140,6 +140,44 @@ class TestFaceEngine(TestCase):
|
|
|
140
140
|
self.assertIsInstance(self.test_engine.detector, str)
|
|
141
141
|
self.assertIsInstance(self.test_engine.embedder, str)
|
|
142
142
|
self.assertIsInstance(self.test_engine.estimator, str)
|
|
143
|
+
self.assertIsInstance(self.test_engine.antispoof, str)
|
|
144
|
+
|
|
145
|
+
def test_init_default_antispoof_is_abstract(self):
|
|
146
|
+
# antispoof is opt-in regardless of installed backends
|
|
147
|
+
self.assertEqual(self.test_engine.antispoof, "abstract_antispoof")
|
|
148
|
+
|
|
149
|
+
def test_setter_antispoof_with_not_existing_model(self):
|
|
150
|
+
logging.disable(logging.WARNING)
|
|
151
|
+
self.test_engine.antispoof = "my_antispoof"
|
|
152
|
+
self.assertEqual(self.test_engine.antispoof, "abstract_antispoof")
|
|
153
|
+
|
|
154
|
+
def test_check_liveness_with_abstract_raises(self):
|
|
155
|
+
with self.assertRaises(NotImplementedError):
|
|
156
|
+
self.test_engine.check_liveness(
|
|
157
|
+
imread(self.bubbles1), np.array([[278, 132, 618, 471]])
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
@unittest.skipUnless(onnxruntime, "onnxruntime package is not installed")
|
|
161
|
+
def test_setter_antispoof_minifasnet(self):
|
|
162
|
+
self.test_engine.antispoof = "minifasnet"
|
|
163
|
+
self.assertEqual(self.test_engine.antispoof, "minifasnet")
|
|
164
|
+
self.assertIsInstance(self.test_engine._antispoof, Antispoof)
|
|
165
|
+
|
|
166
|
+
@unittest.skipUnless(onnxruntime, "onnxruntime package is not installed")
|
|
167
|
+
def test_check_liveness_with_bounding_boxes(self):
|
|
168
|
+
self.test_engine.antispoof = "minifasnet"
|
|
169
|
+
scores = self.test_engine.check_liveness(
|
|
170
|
+
imread(self.bubbles1), np.array([[278, 132, 618, 471]])
|
|
171
|
+
)
|
|
172
|
+
self.assertEqual(scores.shape, (1,))
|
|
173
|
+
self.assertTrue(0.0 <= scores[0] <= 1.0)
|
|
174
|
+
|
|
175
|
+
@unittest.skipUnless(insightface, "insightface package is not installed")
|
|
176
|
+
def test_check_liveness_finds_faces(self):
|
|
177
|
+
engine = FaceEngine(antispoof="minifasnet")
|
|
178
|
+
scores = engine.check_liveness(self.family)
|
|
179
|
+
self.assertGreater(len(scores), 1)
|
|
180
|
+
self.assertTrue(all(0.0 <= s <= 1.0 for s in scores))
|
|
143
181
|
|
|
144
182
|
@unittest.skipUnless(dlib, "dlib package is not installed")
|
|
145
183
|
def test_fit_bubbles(self):
|
|
@@ -5,10 +5,10 @@ import numpy as np
|
|
|
5
5
|
|
|
6
6
|
from extra.dataset import load_dataset
|
|
7
7
|
from face_engine.exceptions import FaceNotFoundError
|
|
8
|
-
from face_engine.models import Detector, Embedder, Estimator
|
|
8
|
+
from face_engine.models import Detector, Embedder, Estimator, _models
|
|
9
9
|
from face_engine.models.basic_estimator import BasicEstimator
|
|
10
10
|
from face_engine.tools import imread
|
|
11
|
-
from tests import TestCase, dlib, insightface
|
|
11
|
+
from tests import TestCase, dlib, insightface, onnxruntime
|
|
12
12
|
|
|
13
13
|
if dlib:
|
|
14
14
|
from face_engine.models.dlib_models import HOGDetector, MMODDetector, ResNetEmbedder
|
|
@@ -160,6 +160,42 @@ class TestArcFaceAntelopeV2Embedder(TestInsightFaceEmbedder):
|
|
|
160
160
|
self.embedder = ArcFaceAntelopeV2Embedder()
|
|
161
161
|
|
|
162
162
|
|
|
163
|
+
@unittest.skipUnless(onnxruntime, "onnxruntime package is not installed")
|
|
164
|
+
class TestMiniFASNetAntispoof(TestCase):
|
|
165
|
+
|
|
166
|
+
def setUp(self):
|
|
167
|
+
from face_engine.models.minifasnet import MiniFASNetAntispoof
|
|
168
|
+
|
|
169
|
+
self.antispoof = MiniFASNetAntispoof()
|
|
170
|
+
self.image = imread(self.bubbles1)
|
|
171
|
+
self.bbs = np.array([[278, 132, 618, 471]])
|
|
172
|
+
|
|
173
|
+
def test_registered_under_canonical_name(self):
|
|
174
|
+
self.assertIs(_models["minifasnet"], type(self.antispoof))
|
|
175
|
+
|
|
176
|
+
def test_predict_return_data_shape_and_range(self):
|
|
177
|
+
scores = self.antispoof.predict(self.image, self.bbs)
|
|
178
|
+
self.assertIsInstance(scores, np.ndarray)
|
|
179
|
+
self.assertEqual(scores.shape, (1,))
|
|
180
|
+
self.assertTrue(0.0 <= scores[0] <= 1.0)
|
|
181
|
+
|
|
182
|
+
def test_predict_live_photo_scores_high(self):
|
|
183
|
+
scores = self.antispoof.predict(self.image, self.bbs)
|
|
184
|
+
self.assertGreater(scores[0], 0.5)
|
|
185
|
+
|
|
186
|
+
def test_predict_multiple_faces(self):
|
|
187
|
+
image = imread(self.family)
|
|
188
|
+
bbs = np.array(
|
|
189
|
+
[[100, 100, 200, 220], [300, 120, 400, 240], [500, 110, 600, 230]]
|
|
190
|
+
)
|
|
191
|
+
scores = self.antispoof.predict(image, bbs)
|
|
192
|
+
self.assertEqual(scores.shape, (3,))
|
|
193
|
+
|
|
194
|
+
def test_predict_empty_bounding_boxes(self):
|
|
195
|
+
scores = self.antispoof.predict(self.image, np.empty((0, 4)))
|
|
196
|
+
self.assertEqual(scores.shape, (0,))
|
|
197
|
+
|
|
198
|
+
|
|
163
199
|
class TestEstimator(TestCase):
|
|
164
200
|
"""Test cases to test estimators.
|
|
165
201
|
|
|
@@ -53,6 +53,18 @@ class TestFaceEnginePersistence(TestCase):
|
|
|
53
53
|
self.assertEqual(engine.detector, self.test_engine.detector)
|
|
54
54
|
self.assertEqual(engine.embedder, self.test_engine.embedder)
|
|
55
55
|
self.assertEqual(engine.estimator, self.test_engine.estimator)
|
|
56
|
+
self.assertEqual(engine.antispoof, self.test_engine.antispoof)
|
|
57
|
+
|
|
58
|
+
def test_load_engine_pre31_file_without_antispoof(self):
|
|
59
|
+
self.test_engine.save(self.filename)
|
|
60
|
+
with open(self.filename) as file:
|
|
61
|
+
data = json.load(file)
|
|
62
|
+
# files saved by face-engine 3.0 have no antispoof key
|
|
63
|
+
del data["antispoof"]
|
|
64
|
+
with open(self.filename, "w") as file:
|
|
65
|
+
json.dump(data, file)
|
|
66
|
+
engine = load_engine(self.filename)
|
|
67
|
+
self.assertEqual(engine.antispoof, "abstract_antispoof")
|
|
56
68
|
|
|
57
69
|
@unittest.skipUnless(dlib or insightface, "no face recognition backend installed")
|
|
58
70
|
def test_load_engine_with_estimator_state(self):
|
|
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
|