// Unit tests for FaceTrackerFunc (nodes/face_tracker_node.hpp): frame-to-frame // track linking and, crucially, cross-cut re-association. Pure, GPU-free, // model-free — drives the node's operator() with hand-built EmbeddedSceneFrames // and inspects the emitted track_ids. // // The behaviour under test: on a camera-angle change (Frame::is_cut) the tracker // parks its tracks instead of destroying them, and revives a parked track_id // when a post-cut detection's raw last-frame-embedding cosine similarity clears // cut_revive_sim. IoU is deliberately driven to 0 across the cut (boxes moved) so // only the embedding path can re-link — exactly the scenario a cut creates. #include #include "config.hpp" #include "nodes/face_tracker_node.hpp" #include "types.hpp" #include namespace { // Unit-norm embedding in the plane of axes i,j at angle whose cosine to // one_hot(i) is cos_t. cosine_similarity(at_sim(i,j,a), at_sim(i,j,b)) works out // to cos(angle diff), letting a test dial the cross-cut similarity precisely. Embedding at_sim(int i, int j, float cos_t) { Embedding e{}; float s = std::sqrt(std::max(0.f, 1.f - cos_t * cos_t)); e[i] = cos_t; e[j] = s; return e; } Embedding axis(int slot) { Embedding e{}; e[slot] = 1.0f; return e; } DetectedFace face_at(float x, float y) { DetectedFace f; f.bbox = cv::Rect2f(x, y, 40.f, 40.f); f.confidence = 0.99f; return f; } // Build a single-face frame at position (x,y) with embedding emb. is_cut marks a // camera-angle change on this frame. EmbeddedSceneFrame frame(double t, float x, float y, const Embedding& emb, bool is_cut = false) { EmbeddedSceneFrame ef; ef.source.timestamp_sec = t; ef.source.is_cut = is_cut; ef.faces = {face_at(x, y)}; ef.crops = {cv::Mat()}; ef.embeddings = {emb}; return ef; } Config tracker_cfg() { Config cfg; cfg.cut_revive_sim = 0.50f; cfg.cut_inactive_max_frames = 5; return cfg; } } // namespace TEST_CASE("track id is stable across ordinary frames", "[face_tracker]") { FaceTrackerFunc ft(tracker_cfg()); Embedding e = axis(0); int id0 = ft(frame(0.0, 10, 10, e)).track_ids[0]; int id1 = ft(frame(1.0, 11, 10, e)).track_ids[0]; // overlaps → same track CHECK(id0 >= 0); CHECK(id1 == id0); } TEST_CASE("cut revives the same track id for a matching identity", "[face_tracker]") { FaceTrackerFunc ft(tracker_cfg()); // Pre-cut: establish a track for a person whose embedding is near-identical // across the cut (sim well above cut_revive_sim), but whose box jumps so IoU // is 0 — the ordinary spatial path cannot re-link it. Embedding pre = at_sim(0, 1, 0.99f); int id_pre = ft(frame(0.0, 10, 10, pre)).track_ids[0]; REQUIRE(id_pre >= 0); Embedding post = at_sim(0, 1, 0.98f); // cos(diff) ≈ 0.9997 > 0.50 auto out = ft(frame(1.0, 300, 300, post, /*is_cut=*/true)); CHECK(out.track_ids[0] == id_pre); // revived, not a fresh id } TEST_CASE("cut starts a fresh track when identity does not match", "[face_tracker]") { FaceTrackerFunc ft(tracker_cfg()); int id_pre = ft(frame(0.0, 10, 10, axis(0))).track_ids[0]; REQUIRE(id_pre >= 0); // Post-cut face is orthogonal (sim 0 < cut_revive_sim) and spatially disjoint // → no revival, brand-new id. auto out = ft(frame(1.0, 300, 300, axis(5), /*is_cut=*/true)); CHECK(out.track_ids[0] != id_pre); CHECK(out.track_ids[0] >= 0); } TEST_CASE("parked track expires after cut_inactive_max_frames", "[face_tracker]") { Config cfg = tracker_cfg(); cfg.cut_inactive_max_frames = 2; FaceTrackerFunc ft(cfg); Embedding person = at_sim(0, 1, 0.99f); int id_pre = ft(frame(0.0, 10, 10, person)).track_ids[0]; REQUIRE(id_pre >= 0); // Cut with an unrelated face parks id_pre; then let the pool age past its // limit with more unrelated, spatially-disjoint faces (each ages the pool by // one). By the time the person returns, id_pre must be gone. ft(frame(1.0, 300, 300, axis(7), /*is_cut=*/true)); // park (age 1) ft(frame(2.0, 300, 300, axis(7))); // age 2 ft(frame(3.0, 300, 300, axis(7))); // age 3 → id_pre dropped auto out = ft(frame(4.0, 10, 10, person)); // same identity returns CHECK(out.track_ids[0] != id_pre); // too late — fresh id } TEST_CASE("eof clears active and parked tracks", "[face_tracker]") { FaceTrackerFunc ft(tracker_cfg()); Embedding person = at_sim(0, 1, 0.99f); int id_pre = ft(frame(0.0, 10, 10, person)).track_ids[0]; ft(frame(1.0, 300, 300, axis(7), /*is_cut=*/true)); // park id_pre EmbeddedSceneFrame eof; eof.source.eof = true; auto out = ft(std::move(eof)); CHECK(out.source.eof); // After eof the pools are empty: the returning identity must get a fresh id, // not the parked one. auto out2 = ft(frame(2.0, 10, 10, person)); CHECK(out2.track_ids[0] != id_pre); }