"""人脸比对(SFace):模型的取得、余弦、不可用时的退路。真模型不进测试,不出网。""" import hashlib import tempfile import unittest from pathlib import Path from unittest.mock import patch from peach import face_detect, face_match from peach.face_detect import Face, FaceModelUnavailable class EnsureModelTests(unittest.TestCase): """与 YuNet 同一条取法:固定地址、校验 sha256,取不到就抛。""" def test_a_matching_local_model_is_reused_without_network(self): with tempfile.TemporaryDirectory() as temp: target = Path(temp).resolve() / "sface.onnx" payload = b"sface-bytes" target.write_bytes(payload) with patch.object(face_match, "MODEL_SHA256", hashlib.sha256(payload).hexdigest()), \ patch.object(face_detect, "read_url") as opener: self.assertEqual(face_match.ensure_model(target), target) opener.assert_not_called() def test_a_missing_model_without_network_is_reported_not_guessed(self): with tempfile.TemporaryDirectory() as temp: target = Path(temp).resolve() / "sface.onnx" with self.assertRaises(FaceModelUnavailable) as caught: face_match.ensure_model(target, allow_download=False) self.assertIn("人脸比对模型", str(caught.exception)) def test_the_pinned_model_is_the_lfs_object(self): self.assertEqual(len(face_match.MODEL_SHA256), 64) self.assertIn("media.githubusercontent.com", face_match.MODEL_URL) self.assertEqual(face_match.COSINE_THRESHOLD, 0.363) class ModelLoadingTests(unittest.TestCase): def setUp(self): temporary = tempfile.TemporaryDirectory() self.addCleanup(temporary.cleanup) self.model = Path(temporary.name).resolve() / "中文模型" / "比对.onnx" self.model.parent.mkdir() self.payload = b"verified-sface-model" self.model.write_bytes(self.payload) digest = patch.object(face_match, "MODEL_SHA256", hashlib.sha256(self.payload).hexdigest()) digest.start() self.addCleanup(digest.stop) def test_unicode_model_path_is_read_as_onnx_bytes(self): import cv2 import numpy detector = object() with patch.object(cv2, "FaceRecognizerSF") as recognizer: embedder = face_match.FaceEmbedder(self.model, detector=detector, allow_download=False) args = recognizer.create.call_args.args self.assertEqual(args[0], "ONNX") self.assertIsInstance(args[1], numpy.ndarray) self.assertEqual((args[1].dtype, args[1].ndim, args[1].tobytes()), (numpy.dtype("uint8"), 1, self.payload)) self.assertEqual((args[2].dtype, args[2].size), (numpy.dtype("uint8"), 0)) self.assertEqual(len(args), 3) self.assertEqual(embedder.model_path, self.model) self.assertIs(embedder._recognizer, recognizer.create.return_value) self.assertIs(embedder._detector, detector) def test_model_read_error_after_verification_reaches_the_caller(self): import cv2 failure = PermissionError(13, "model read denied") with patch.object(Path, "read_bytes", side_effect=[self.payload, failure]), \ patch.object(cv2, "FaceRecognizerSF") as recognizer, \ self.assertRaises(PermissionError) as caught: face_match.FaceEmbedder(self.model, detector=object(), allow_download=False) self.assertIs(caught.exception, failure) recognizer.create.assert_not_called() def test_an_onnx_import_error_reaches_the_caller(self): import cv2 failure = cv2.error("invalid ONNX buffer") with patch.object(cv2, "FaceRecognizerSF") as recognizer, self.assertRaises(cv2.error) as caught: recognizer.create.side_effect = failure face_match.FaceEmbedder(self.model, detector=object(), allow_download=False) self.assertIs(caught.exception, failure) def test_mismatched_model_bytes_never_reach_opencv(self): import cv2 self.model.write_bytes(b"partial-model") with patch.object(cv2, "FaceRecognizerSF") as recognizer, self.assertRaises(FaceModelUnavailable): face_match.FaceEmbedder(self.model, detector=object(), allow_download=False) recognizer.create.assert_not_called() class CosineTests(unittest.TestCase): def test_same_direction_is_one_and_orthogonal_is_zero(self): self.assertAlmostEqual(face_match.cosine((1, 2, 3), (2, 4, 6)), 1.0) self.assertAlmostEqual(face_match.cosine((1, 0), (0, 1)), 0.0) self.assertEqual(face_match.cosine((0, 0), (1, 1)), 0.0) class MatcherTests(unittest.TestCase): def test_an_unavailable_model_is_reported_once_and_compares_nothing(self): built = [] def broken(): built.append(1) raise FaceModelUnavailable("缺少人脸比对模型:sface.onnx") with patch.object(face_match, "FaceEmbedder", side_effect=broken): matcher = face_match.FaceMatcher() self.assertEqual(built, [], "用不上就不去取模型") self.assertIsNone(matcher.embedding(b"anything")) self.assertIsNone(matcher.embedding(b"again")) self.assertEqual(built, [1]) self.assertIn("缺少人脸比对模型", matcher.unavailable) def test_a_face_without_landmarks_gives_no_feature(self): """摆正要五个关键点;框里没带就不比,不拿没摆正的脸去算分。""" import numpy class Detector: def detect(self, _image): return [Face(cx=0.5, cy=0.5, width=0.3, height=0.3, score=0.9)] embedder = face_match.FaceEmbedder.__new__(face_match.FaceEmbedder) embedder._detector = Detector() embedder._recognizer = None self.assertIsNone(embedder.embed(numpy.zeros((400, 400, 3), numpy.uint8))) class SmallFaceCropTests(unittest.TestCase): """全身照上的小脸先裁出来放大再摆正;大脸和裁出来检不到的都照整图走。""" POINTS = (0.4, 0.4, 0.6, 0.4, 0.5, 0.5, 0.42, 0.6, 0.58, 0.6) def embedder(self, faces_on_crop): import cv2 import numpy seen = [] class Detector: def __init__(self, first): self.first = first def detect(self, image): seen.append(image.shape[:2]) return self.first if len(seen) == 1 else faces_on_crop class Recognizer: def alignCrop(self, image, _box): seen.append(("aligned", image.shape[:2])) return image def feature(self, _aligned): return numpy.ones((1, 4), numpy.float32) embedder = face_match.FaceEmbedder.__new__(face_match.FaceEmbedder) embedder._cv2 = cv2 embedder._recognizer = Recognizer() return embedder, Detector, seen def run_on(self, face_width, faces_on_crop): import numpy embedder, Detector, seen = self.embedder(faces_on_crop) embedder._detector = Detector([Face(cx=0.5, cy=0.2, width=face_width, height=face_width, score=0.9, landmarks=self.POINTS)]) feature = embedder.embed(numpy.zeros((1000, 800, 3), numpy.uint8)) return feature, seen def test_a_small_face_is_aligned_on_the_enlarged_crop(self): side = face_match.FACE_CROP_SIDE feature, seen = self.run_on(0.1, [Face(cx=0.5, cy=0.5, width=0.5, height=0.5, score=0.9, landmarks=self.POINTS)]) self.assertEqual(feature, (1.0, 1.0, 1.0, 1.0)) self.assertEqual(seen, [(1000, 800), (side, side), ("aligned", (side, side))]) def test_a_large_face_is_aligned_on_the_whole_picture(self): _feature, seen = self.run_on(0.2, []) self.assertEqual(seen, [(1000, 800), ("aligned", (1000, 800))]) def test_a_crop_without_a_usable_face_falls_back_to_the_whole_picture(self): _feature, seen = self.run_on(0.1, [Face(cx=0.5, cy=0.5, width=0.5, height=0.5, score=0.9)]) side = face_match.FACE_CROP_SIDE self.assertEqual(seen, [(1000, 800), (side, side), ("aligned", (1000, 800))]) if __name__ == "__main__": unittest.main()