Initial commit: scene-actor-extraction pipeline

Source (KPN++ pipeline nodes, ArcFace embedders, SCRFD/YuNet detectors,
gallery builder), build scripts, and eval artifacts.

- external/KPN as a git submodule (gitea.tourolle.paris/dtourolle/KPN)
- ONNX models tracked via Git LFS (models/*.onnx)
- generated outputs, TensorRT engines, reference repos, and media ignored
This commit is contained in:
2026-06-12 15:29:01 +02:00
commit d753062c6c
50 changed files with 10100 additions and 0 deletions
+118
View File
@@ -0,0 +1,118 @@
#pragma once
#include "types.hpp"
#include <cmath>
#include <iostream>
#include <vector>
// ── GalleryCalibration ────────────────────────────────────────────────────────
// Platt-style sigmoid calibration: P(match) = σ(a · similarity + b)
// where similarity = cosine similarity ∈ [-1, 1] (dot product of L2-normalised
// ArcFace embeddings).
//
// Fitted from intra-class (positive) and inter-class (negative) pairs built
// from the gallery reference embeddings. When calibration is invalid (too few
// positive pairs), the caller should fall back to the raw threshold.
struct GalleryCalibration {
float a{10.f}; // scale (positive → higher similarity → higher probability)
float b{-5.f}; // bias (decision boundary at similarity = -b/a)
bool valid{false};
// P(match | sim) using the balanced-prior calibration.
// Pass log_prior_odds = log(p0/(1-p0)) to adjust for a real base-rate prior p0:
// P(match | sim, p0) = σ(a·sim + b + log(p0/(1-p0)))
float probability(float similarity, float log_prior_odds = 0.f) const {
float z = a * similarity + b + log_prior_odds;
if (z >= 0.f) return 1.f / (1.f + std::exp(-z));
float e = std::exp(z);
return e / (1.f + e);
}
// Similarity at which P(match, prior) == p
float boundary_at(float p = 0.5f, float log_prior_odds = 0.f) const {
return (std::log(p / (1.f - p)) - b - log_prior_odds) / a;
}
};
// Fit a logistic sigmoid to gallery pair similarities.
// Positive pairs: same actor, different reference images.
// Negative pairs: different actors (all cross-actor embedding pairs).
// Class weights balance the (typically skewed) pos/neg ratio.
// Requires ≥2 positive pairs and ≥1 negative pair.
inline GalleryCalibration calibrate_gallery(
const std::vector<Embedding>& flat_emb,
const std::vector<int>& flat_actor)
{
const int n = static_cast<int>(flat_emb.size());
std::vector<float> X; // cosine similarities
std::vector<float> Y; // labels: 1 = same actor, 0 = different
for (int i = 0; i < n; ++i) {
for (int j = i + 1; j < n; ++j) {
float dot = 0.f;
for (int k = 0; k < 512; ++k)
dot += flat_emb[i][k] * flat_emb[j][k];
X.push_back(dot);
Y.push_back(flat_actor[i] == flat_actor[j] ? 1.f : 0.f);
}
}
int n_pos = 0;
for (float y : Y) if (y > 0.5f) ++n_pos;
int n_neg = static_cast<int>(Y.size()) - n_pos;
if (n_pos < 2 || n_neg < 1) {
std::cerr << "[calibration] insufficient pairs (+" << n_pos
<< "/-" << n_neg << ") — calibration skipped\n";
return {};
}
// Class weights to handle pos/neg imbalance
float total = static_cast<float>(Y.size());
float w_pos = total / (2.f * n_pos);
float w_neg = total / (2.f * n_neg);
// Gradient descent logistic regression (2 parameters: a, b)
float a = 10.f, b = -5.f;
constexpr float lr = 0.05f;
constexpr int max_iter = 20000;
constexpr float tol = 1e-7f;
for (int iter = 0; iter < max_iter; ++iter) {
float da = 0.f, db = 0.f;
for (int i = 0; i < static_cast<int>(X.size()); ++i) {
float z = a * X[i] + b;
float sig = (z >= 0.f) ? 1.f / (1.f + std::exp(-z))
: std::exp(z) / (1.f + std::exp(z));
float err = sig - Y[i];
float w = (Y[i] > 0.5f) ? w_pos : w_neg;
da += w * err * X[i];
db += w * err;
}
da /= total;
db /= total;
a -= lr * da;
b -= lr * db;
if (da * da + db * db < tol * tol) break;
}
// Training accuracy at P=0.5 decision boundary
int correct = 0;
for (int i = 0; i < static_cast<int>(X.size()); ++i) {
float z = a * X[i] + b;
float sig = (z >= 0.f) ? 1.f / (1.f + std::exp(-z))
: std::exp(z) / (1.f + std::exp(z));
if ((sig > 0.5f) == (Y[i] > 0.5f)) ++correct;
}
float acc = 100.f * correct / static_cast<float>(Y.size());
GalleryCalibration cal{a, b, true};
std::cerr << "[calibration] sigmoid fitted:"
<< " a=" << a << " b=" << b
<< " boundary(P=0.5)=sim" << cal.boundary_at(0.5f)
<< " pairs=" << Y.size()
<< " (+" << n_pos << "/-" << n_neg << ")"
<< " train_acc=" << acc << "%\n";
return cal;
}