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.
Files changed (27) hide show
  1. {face_engine-3.0.0/face_engine.egg-info → face_engine-3.1.0}/PKG-INFO +31 -7
  2. {face_engine-3.0.0 → face_engine-3.1.0}/README.rst +30 -6
  3. {face_engine-3.0.0 → face_engine-3.1.0}/face_engine/__init__.py +1 -1
  4. {face_engine-3.0.0 → face_engine-3.1.0}/face_engine/core.py +73 -2
  5. {face_engine-3.0.0 → face_engine-3.1.0}/face_engine/fetching.py +3 -0
  6. {face_engine-3.0.0 → face_engine-3.1.0}/face_engine/models/__init__.py +24 -1
  7. face_engine-3.1.0/face_engine/models/_onnx.py +19 -0
  8. {face_engine-3.0.0 → face_engine-3.1.0}/face_engine/models/insightface_models.py +1 -13
  9. face_engine-3.1.0/face_engine/models/minifasnet.py +127 -0
  10. {face_engine-3.0.0 → face_engine-3.1.0}/face_engine/tools.py +1 -1
  11. {face_engine-3.0.0 → face_engine-3.1.0/face_engine.egg-info}/PKG-INFO +31 -7
  12. {face_engine-3.0.0 → face_engine-3.1.0}/face_engine.egg-info/SOURCES.txt +2 -0
  13. {face_engine-3.0.0 → face_engine-3.1.0}/tests/test_face_engine.py +40 -2
  14. {face_engine-3.0.0 → face_engine-3.1.0}/tests/test_face_engine_models.py +38 -2
  15. {face_engine-3.0.0 → face_engine-3.1.0}/tests/test_face_engine_persistence.py +12 -0
  16. {face_engine-3.0.0 → face_engine-3.1.0}/LICENSE +0 -0
  17. {face_engine-3.0.0 → face_engine-3.1.0}/MANIFEST.in +0 -0
  18. {face_engine-3.0.0 → face_engine-3.1.0}/docs/source/conf.py +0 -0
  19. {face_engine-3.0.0 → face_engine-3.1.0}/face_engine/exceptions.py +0 -0
  20. {face_engine-3.0.0 → face_engine-3.1.0}/face_engine/models/basic_estimator.py +0 -0
  21. {face_engine-3.0.0 → face_engine-3.1.0}/face_engine/models/dlib_models.py +0 -0
  22. {face_engine-3.0.0 → face_engine-3.1.0}/face_engine.egg-info/dependency_links.txt +0 -0
  23. {face_engine-3.0.0 → face_engine-3.1.0}/face_engine.egg-info/requires.txt +0 -0
  24. {face_engine-3.0.0 → face_engine-3.1.0}/face_engine.egg-info/top_level.txt +0 -0
  25. {face_engine-3.0.0 → face_engine-3.1.0}/pyproject.toml +0 -0
  26. {face_engine-3.0.0 → face_engine-3.1.0}/setup.cfg +0 -0
  27. {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.0.0
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 three model interfaces ``Detector``, ``Embedder``
72
- and ``Estimator`` (see `models`_), and leans on user provided implementations
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 three model interfaces ``Detector``, ``Embedder``
33
- and ``Estimator`` (see `models`_), and leans on user provided implementations
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.0.0"
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 model trio and counters goes to "extra"
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 ("_detector", "_embedder", "_estimator", "n_classes", "n_samples")
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.0.0
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 three model interfaces ``Detector``, ``Embedder``
72
- and ``Estimator`` (see `models`_), and leans on user provided implementations
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