"""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