File size: 3,041 Bytes
9e89884
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
"""Tests for the face + expression detector.

The detector depends only on numpy and OpenCV's cascade interface. We inject
fake cascades so the tests are deterministic and need no real camera/images.
"""

import numpy as np
import pytest

from face_detection.detector import Face, FaceDetector


class FakeCascade:
    """Mimics cv2.CascadeClassifier: returns canned detections per call."""

    def __init__(self, *detections):
        # Each call to detectMultiScale pops the next canned result.
        self._results = list(detections)
        self.calls = []

    def detectMultiScale(self, image, **kwargs):  # noqa: N802 (cv2 naming)
        self.calls.append(image.shape)
        result = self._results.pop(0) if self._results else []
        return np.array(result, dtype=int) if len(result) else np.empty((0, 4), int)


def gray_frame(h=200, w=200):
    return np.zeros((h, w), dtype=np.uint8)


def color_frame(h=200, w=200):
    return np.zeros((h, w, 3), dtype=np.uint8)


def test_no_face_returns_empty_list():
    det = FaceDetector(face_cascade=FakeCascade([]), smile_cascade=FakeCascade())
    assert det.detect(gray_frame()) == []


def test_single_face_neutral_when_no_smile():
    faces = FakeCascade([(10, 20, 50, 50)])
    smiles = FakeCascade([])  # no smile inside the face ROI
    det = FaceDetector(face_cascade=faces, smile_cascade=smiles)

    result = det.detect(gray_frame())

    assert len(result) == 1
    assert isinstance(result[0], Face)
    assert result[0].bbox == (10, 20, 50, 50)
    assert result[0].expression == "neutral"


def test_single_face_happy_when_smile_detected():
    faces = FakeCascade([(10, 20, 50, 50)])
    smiles = FakeCascade([(5, 30, 20, 10)])  # a smile within the ROI
    det = FaceDetector(face_cascade=faces, smile_cascade=smiles)

    result = det.detect(gray_frame())

    assert result[0].expression == "happy"


def test_multiple_faces_detected():
    faces = FakeCascade([(0, 0, 40, 40), (100, 100, 30, 30)])
    # One smile lookup per detected face; first happy, second neutral.
    smiles = FakeCascade([(1, 1, 5, 5)], [])
    det = FaceDetector(face_cascade=faces, smile_cascade=smiles)

    result = det.detect(gray_frame())

    assert len(result) == 2
    assert result[0].expression == "happy"
    assert result[1].expression == "neutral"


def test_color_frame_is_converted_to_gray_before_detection():
    faces = FakeCascade([(10, 20, 50, 50)])
    det = FaceDetector(face_cascade=faces, smile_cascade=FakeCascade([]))

    det.detect(color_frame())

    # The image handed to the cascade must be 2D (grayscale).
    assert len(faces.calls[0]) == 2


def test_face_center_helper():
    face = Face(bbox=(10, 20, 40, 60), expression="neutral")
    assert face.center == (30, 50)  # (10 + 40/2, 20 + 60/2)


def test_largest_face_picks_biggest_bbox():
    small = Face(bbox=(0, 0, 10, 10), expression="neutral")
    big = Face(bbox=(0, 0, 80, 80), expression="happy")
    assert FaceDetector.largest([small, big]) is big
    assert FaceDetector.largest([]) is None