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:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user