Files
scene-actor-extraction/tests/test_face_utils.cpp
T
dtourolle 76df2f66aa test: add Catch2 unit test suite (gallery, calibration, tracking, similarity)
GPU-free, model-free tests for the pure logic: gallery HDF5 save/load
round-trips (actors, embeddings, embedded calibration) and legacy JSON
read back-compat; the calibration sigmoid fit, boundary inversion, and the
in-memory hash-keyed cache reuse/staleness; TrackGallery's diversity-buffer
eviction, novelty/spread safety gates, and promotion; FaceTracker's IoU/
embedding association and cross-cut track revival; and the GEMM similarity
backend (forced to CPU so the suite runs without a GPU).

Verified: all 39 test cases / 1640 assertions pass (cmake -DSAE_BUILD_TESTS=ON).
2026-07-19 19:10:57 +02:00

77 lines
2.6 KiB
C++

// Unit tests for the geometric/numeric helpers in types.hpp and face_utils.hpp:
// cosine_similarity and the ArcFace 5-point alignment transform. GPU-free,
// model-free.
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include "face_utils.hpp"
#include "types.hpp"
#include <array>
#include <cmath>
using Catch::Matchers::WithinAbs;
TEST_CASE("cosine_similarity of a unit vector with itself is 1", "[types]") {
std::array<float, 512> raw{};
raw[3] = 2.f; raw[7] = -1.f;
Embedding e = l2_normalise(raw.data());
CHECK_THAT(cosine_similarity(e, e), WithinAbs(1.0f, 1e-6f));
}
TEST_CASE("cosine_similarity of orthogonal vectors is 0", "[types]") {
Embedding a{}, b{};
a[0] = 1.f;
b[1] = 1.f;
CHECK_THAT(cosine_similarity(a, b), WithinAbs(0.0f, 1e-6f));
}
TEST_CASE("cosine_similarity of opposite vectors is -1", "[types]") {
Embedding a{}, b{};
a[5] = 1.f;
b[5] = -1.f;
CHECK_THAT(cosine_similarity(a, b), WithinAbs(-1.0f, 1e-6f));
}
TEST_CASE("align_face maps the reference landmarks onto the 112x112 canvas", "[face_utils]") {
// Build a synthetic image where the five landmarks sit at known positions.
// Feeding align_face the *reference* positions themselves should yield an
// (near-)identity similarity transform, so the output is 112x112.
cv::Mat img(200, 200, CV_8UC3, cv::Scalar(0, 0, 0));
std::array<cv::Point2f, 5> lm;
for (int i = 0; i < 5; ++i) {
lm[i] = {kArcFaceRef[i][0], kArcFaceRef[i][1]};
cv::circle(img, lm[i], 2, cv::Scalar(255, 255, 255), -1);
}
cv::Mat crop = align_face(img, lm);
REQUIRE_FALSE(crop.empty());
CHECK(crop.cols == 112);
CHECK(crop.rows == 112);
}
TEST_CASE("align_face is translation-equivariant", "[face_utils]") {
// Shifting all landmarks by a constant offset must still produce a valid
// 112x112 crop (the similarity transform absorbs the translation).
cv::Mat img(300, 300, CV_8UC3, cv::Scalar(30, 30, 30));
std::array<cv::Point2f, 5> lm;
const float dx = 100.f, dy = 80.f;
for (int i = 0; i < 5; ++i)
lm[i] = {kArcFaceRef[i][0] + dx, kArcFaceRef[i][1] + dy};
cv::Mat crop = align_face(img, lm);
REQUIRE_FALSE(crop.empty());
CHECK(crop.cols == 112);
CHECK(crop.rows == 112);
}
TEST_CASE("align_face returns empty on degenerate (collinear) landmarks", "[face_utils]") {
// All five landmarks identical → the affine fit is degenerate.
cv::Mat img(200, 200, CV_8UC3, cv::Scalar(0, 0, 0));
std::array<cv::Point2f, 5> lm;
for (auto& p : lm) p = {50.f, 50.f};
cv::Mat crop = align_face(img, lm);
CHECK(crop.empty());
}