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.
Files changed (28) hide show
  1. {face_engine-3.1.0/face_engine.egg-info → face_engine-3.2.0}/PKG-INFO +29 -1
  2. {face_engine-3.1.0 → face_engine-3.2.0}/README.rst +28 -0
  3. {face_engine-3.1.0 → face_engine-3.2.0}/face_engine/__init__.py +1 -1
  4. {face_engine-3.1.0 → face_engine-3.2.0}/face_engine/core.py +63 -5
  5. {face_engine-3.1.0 → face_engine-3.2.0}/face_engine/exceptions.py +4 -0
  6. {face_engine-3.1.0 → face_engine-3.2.0}/face_engine/tools.py +4 -0
  7. {face_engine-3.1.0 → face_engine-3.2.0/face_engine.egg-info}/PKG-INFO +29 -1
  8. {face_engine-3.1.0 → face_engine-3.2.0}/face_engine.egg-info/SOURCES.txt +2 -1
  9. {face_engine-3.1.0 → face_engine-3.2.0}/tests/test_face_engine.py +89 -21
  10. {face_engine-3.1.0 → face_engine-3.2.0}/tests/test_face_engine_persistence.py +12 -0
  11. face_engine-3.2.0/tests/test_tools.py +34 -0
  12. {face_engine-3.1.0 → face_engine-3.2.0}/LICENSE +0 -0
  13. {face_engine-3.1.0 → face_engine-3.2.0}/MANIFEST.in +0 -0
  14. {face_engine-3.1.0 → face_engine-3.2.0}/docs/source/conf.py +0 -0
  15. {face_engine-3.1.0 → face_engine-3.2.0}/face_engine/fetching.py +0 -0
  16. {face_engine-3.1.0 → face_engine-3.2.0}/face_engine/models/__init__.py +0 -0
  17. {face_engine-3.1.0 → face_engine-3.2.0}/face_engine/models/_onnx.py +0 -0
  18. {face_engine-3.1.0 → face_engine-3.2.0}/face_engine/models/basic_estimator.py +0 -0
  19. {face_engine-3.1.0 → face_engine-3.2.0}/face_engine/models/dlib_models.py +0 -0
  20. {face_engine-3.1.0 → face_engine-3.2.0}/face_engine/models/insightface_models.py +0 -0
  21. {face_engine-3.1.0 → face_engine-3.2.0}/face_engine/models/minifasnet.py +0 -0
  22. {face_engine-3.1.0 → face_engine-3.2.0}/face_engine.egg-info/dependency_links.txt +0 -0
  23. {face_engine-3.1.0 → face_engine-3.2.0}/face_engine.egg-info/requires.txt +0 -0
  24. {face_engine-3.1.0 → face_engine-3.2.0}/face_engine.egg-info/top_level.txt +0 -0
  25. {face_engine-3.1.0 → face_engine-3.2.0}/pyproject.toml +0 -0
  26. {face_engine-3.1.0 → face_engine-3.2.0}/setup.cfg +0 -0
  27. {face_engine-3.1.0 → face_engine-3.2.0}/tests/test_face_engine_models.py +0 -0
  28. {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.1.0
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.1.0"
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__, logger
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 and abstract fallback for unknown 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
- logger.warning("%s model '%s' not found!", kind.capitalize(), name)
35
- return "abstract_" + kind
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.1.0
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
 
@@ -22,4 +22,5 @@ face_engine/models/minifasnet.py
22
22
  tests/test_face_engine.py
23
23
  tests/test_face_engine_models.py
24
24
  tests/test_face_engine_persistence.py
25
- tests/test_fetching.py
25
+ tests/test_fetching.py
26
+ tests/test_tools.py
@@ -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 test_explicit_init_with_not_existing_models(self):
52
- logging.disable(logging.WARNING)
53
- engine = FaceEngine(
54
- detector="my_detector", embedder="my_embedder", estimator="my_estimator"
55
- )
56
- self.assertEqual(engine.detector, "abstract_detector")
57
- self.assertEqual(engine.embedder, "abstract_embedder")
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 test_setters_with_not_existing_models(self):
99
- logging.disable(logging.WARNING)
100
- self.empty_engine.detector = "my_detector"
101
- self.empty_engine.embedder = "my_embedder"
102
- self.empty_engine.estimator = "my_estimator"
103
- self.assertNotEqual(self.empty_engine.detector, "my_detector")
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 test_setter_antispoof_with_not_existing_model(self):
150
- logging.disable(logging.WARNING)
151
- self.test_engine.antispoof = "my_antispoof"
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