#!/opt/homebrew/bin/python3.11 """Unit tests for face_tracker.py""" import sys import json import math import unittest import numpy as np from typing import Dict, List, Set sys.path.insert(0, "scripts/utils") from face_tracker import ( calculate_bbox_iou, calculate_bbox_distance, calculate_embedding_similarity, match_faces, track_faces, ) class TestBboxIoU(unittest.TestCase): def test_identical_bboxes(self): bbox = {"x": 100, "y": 100, "width": 200, "height": 200} self.assertAlmostEqual(calculate_bbox_iou(bbox, bbox), 1.0) def test_no_overlap(self): b1 = {"x": 0, "y": 0, "width": 100, "height": 100} b2 = {"x": 200, "y": 200, "width": 100, "height": 100} self.assertEqual(calculate_bbox_iou(b1, b2), 0.0) def test_partial_overlap(self): b1 = {"x": 0, "y": 0, "width": 100, "height": 100} b2 = {"x": 50, "y": 50, "width": 100, "height": 100} iou = calculate_bbox_iou(b1, b2) self.assertGreater(iou, 0.0) self.assertLess(iou, 1.0) self.assertAlmostEqual(iou, 2500 / (10000 + 10000 - 2500)) def test_contained(self): b1 = {"x": 0, "y": 0, "width": 200, "height": 200} b2 = {"x": 50, "y": 50, "width": 50, "height": 50} iou = calculate_bbox_iou(b1, b2) self.assertGreater(iou, 0.0) self.assertLess(iou, 1.0) def test_zero_area(self): b1 = {"x": 0, "y": 0, "width": 0, "height": 100} b2 = {"x": 0, "y": 0, "width": 100, "height": 100} self.assertEqual(calculate_bbox_iou(b1, b2), 0.0) class TestBboxDistance(unittest.TestCase): def test_same_center(self): bbox = {"x": 100, "y": 100, "width": 100, "height": 100} self.assertAlmostEqual(calculate_bbox_distance(bbox, bbox), 0.0) def test_horizontal_shift(self): b1 = {"x": 0, "y": 0, "width": 100, "height": 100} b2 = {"x": 100, "y": 0, "width": 100, "height": 100} self.assertAlmostEqual(calculate_bbox_distance(b1, b2), 100.0) def test_diagonal_shift(self): b1 = {"x": 0, "y": 0, "width": 100, "height": 100} b2 = {"x": 100, "y": 100, "width": 100, "height": 100} expected = math.sqrt(100**2 + 100**2) self.assertAlmostEqual(calculate_bbox_distance(b1, b2), expected) class TestEmbeddingSimilarity(unittest.TestCase): def test_identical(self): emb = [1.0, 0.0, 0.0] self.assertAlmostEqual(calculate_embedding_similarity(emb, emb), 1.0) def test_opposite(self): self.assertAlmostEqual( calculate_embedding_similarity([1.0, 0.0], [-1.0, 0.0]), -1.0 ) def test_orthogonal(self): self.assertAlmostEqual( calculate_embedding_similarity([1.0, 0.0], [0.0, 1.0]), 0.0 ) def test_partial_match(self): sim = calculate_embedding_similarity([1.0, 0.0, 0.0], [0.5, 0.5, 0.0]) self.assertGreater(sim, 0.5) self.assertLess(sim, 1.0) def test_none_embedding(self): self.assertEqual(calculate_embedding_similarity(None, [1.0, 0.0]), 0.0) self.assertEqual(calculate_embedding_similarity([1.0, 0.0], None), 0.0) def test_zero_norm(self): self.assertEqual(calculate_embedding_similarity([0.0, 0.0], [1.0, 0.0]), 0.0) class TestMatchFaces(unittest.TestCase): def make_face(self, x, y, w, h, embedding=None, c=1.0): f = {"x": x, "y": y, "width": w, "height": h, "confidence": c} if embedding: f["embedding"] = embedding return f def test_no_previous(self): curr = [self.make_face(0, 0, 100, 100)] result = match_faces(curr, []) self.assertEqual(result, {0: -1}) def test_same_position_match(self): curr = [self.make_face(0, 0, 100, 100, embedding=[1.0, 0.0])] prev = [self.make_face(0, 0, 100, 100, embedding=[1.0, 0.0])] result = match_faces(curr, prev) self.assertEqual(result, {0: 0}) def test_reject_low_sim_low_iou(self): curr = [self.make_face(300, 300, 100, 100, embedding=[0.0, 1.0])] prev = [self.make_face(0, 0, 100, 100, embedding=[1.0, 0.0])] result = match_faces(curr, prev) self.assertEqual(result, {0: -1}) def test_match_by_position_only(self): curr = [self.make_face(0, 0, 100, 100, embedding=[0.0, 1.0])] prev = [self.make_face(0, 0, 100, 100, embedding=[1.0, 0.0])] result = match_faces(curr, prev) self.assertEqual(result, {0: 0}) def test_reject_size_change(self): curr = [self.make_face(0, 0, 10, 10, embedding=[0.3, 0.7])] prev = [self.make_face(0, 0, 100, 100, embedding=[0.7, 0.3])] result = match_faces(curr, prev) self.assertEqual(result, {0: -1}) def test_cut_boundary_split(self): curr = [self.make_face(0, 0, 100, 100)] prev = [self.make_face(0, 0, 100, 100)] def test_edge_exit_reject(self): curr = [self.make_face(200, 200, 100, 100, embedding=[0.3, 0.7])] prev = [self.make_face(0, 0, 100, 100, embedding=[0.7, 0.3])] result = match_faces(curr, prev) self.assertEqual(result, {0: -1}) def test_cut_boundary_split(self): curr = [self.make_face(0, 0, 100, 100)] prev = [self.make_face(0, 0, 100, 100)] result = match_faces(curr, prev, cut_boundaries={5}, prev_frame=4, curr_frame=6) self.assertEqual(result, {0: -1}) def test_edge_exit_reject(self): curr = [self.make_face(200, 200, 100, 100, embedding=[0.3, 0.7])] prev = [self.make_face(0, 0, 100, 100, embedding=[0.7, 0.3])] result = match_faces(curr, prev) self.assertEqual(result, {0: -1}) def test_multiple_faces_no_conflict(self): curr = [ self.make_face(0, 0, 100, 100, embedding=[1.0, 0.0, 0.0]), self.make_face(200, 200, 100, 100, embedding=[0.0, 1.0, 0.0]), ] prev = [ self.make_face(0, 0, 100, 100, embedding=[1.0, 0.0, 0.0]), self.make_face(200, 200, 100, 100, embedding=[0.0, 1.0, 0.0]), ] result = match_faces(curr, prev) self.assertEqual(result, {0: 0, 1: 1}) class TestTrackFaces(unittest.TestCase): def make_face_data(self, frames_data: Dict[int, List[Dict]]) -> Dict: frames = {} for fnum, face_list in frames_data.items(): frames[str(fnum)] = {"faces": face_list} return {"frames": frames, "metadata": {"fps": 25.0}} def test_single_frame(self): data = self.make_face_data({ 0: [{"x": 0, "y": 0, "width": 100, "height": 100, "confidence": 1.0}] }) result = track_faces(data) traces = result.get("traces", {}) self.assertEqual(len(traces), 1) t = traces.get("0", {}) self.assertEqual(t["start_frame"], 0) self.assertEqual(t["end_frame"], 0) def test_face_appears_disappears(self): data = self.make_face_data({ 0: [{"x": 0, "y": 0, "width": 100, "height": 100, "confidence": 1.0}], 1: [], 2: [{"x": 100, "y": 100, "width": 100, "height": 100, "confidence": 1.0}], }) result = track_faces(data) traces = result.get("traces", {}) self.assertEqual(len(traces), 2) def test_same_face_stable(self): face = {"x": 50, "y": 50, "width": 100, "height": 100, "confidence": 1.0} data = self.make_face_data({ 0: [dict(face)], 1: [dict(face)], 2: [dict(face)], }) result = track_faces(data) traces = result.get("traces", {}) self.assertEqual(len(traces), 1) t = list(traces.values())[0] self.assertEqual(t["start_frame"], 0) self.assertEqual(t["end_frame"], 2) def test_cut_splits_trace(self): data = self.make_face_data({ 0: [{"x": 50, "y": 50, "width": 100, "height": 100, "confidence": 1.0}], 1: [{"x": 50, "y": 50, "width": 100, "height": 100, "confidence": 1.0}], }) result = track_faces(data, cut_boundaries={1}) self.assertEqual(len(result["traces"]), 2) def test_trace_stats_present(self): data = self.make_face_data({ 0: [{"x": 0, "y": 0, "width": 100, "height": 100, "confidence": 0.9}], }) result = track_faces(data) stats = result["metadata"]["trace_stats"] self.assertIn("total_traces", stats) self.assertIn("active_traces", stats) self.assertIn("long_traces", stats) if __name__ == "__main__": unittest.main()