Ctrl K
ComunidadGuías…

tests/test_face_match.py

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""人脸比对(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()