|
|
|
@@ -0,0 +1,495 @@
|
|
|
|
|
#pragma once
|
|
|
|
|
/// TRACES: GR-003 | SR-001
|
|
|
|
|
///
|
|
|
|
|
/// The gallery build report — what the gallery *is*, written next to it.
|
|
|
|
|
///
|
|
|
|
|
/// A gallery is a silent artefact: it loads, it scores, it never complains. The
|
|
|
|
|
/// two ways it fails are both invisible from the outside.
|
|
|
|
|
///
|
|
|
|
|
/// 1. **An actor with zero usable images can never be recognised.** They are
|
|
|
|
|
/// dropped at build time (`gallery_builder.cpp` skips a directory whose
|
|
|
|
|
/// images all fail detection or alignment), so afterwards nothing in the
|
|
|
|
|
/// file records that they were ever meant to be there. Every scene they
|
|
|
|
|
/// appear in is a guaranteed miss, and recall is capped at a number nobody
|
|
|
|
|
/// computed. This is the single most useful line in the report.
|
|
|
|
|
/// 2. **A gallery can be quietly bad and look fine.** The Platt sigmoid
|
|
|
|
|
/// (AR-023) is fitted from two distributions — intra-class (same actor,
|
|
|
|
|
/// different reference) and inter-class (different actors) similarity — and
|
|
|
|
|
/// *every* threshold in the pipeline is expressed in the probability space
|
|
|
|
|
/// that fit defines (AR-024): identity acceptance, track association,
|
|
|
|
|
/// expansion admission, cluster merging. If those two distributions overlap
|
|
|
|
|
/// heavily the fit is weak, and every downstream decision silently inherits
|
|
|
|
|
/// that weakness while still reporting confident-looking probabilities. The
|
|
|
|
|
/// fit already computes the distributions and throws them away; emitting
|
|
|
|
|
/// them is what makes the quality of the whole probability space auditable
|
|
|
|
|
/// instead of assumed.
|
|
|
|
|
///
|
|
|
|
|
/// The report is therefore a build artefact, not a debug aid: it is the only
|
|
|
|
|
/// place the recall ceiling and the calibration's conditioning are written down.
|
|
|
|
|
///
|
|
|
|
|
/// **On the histograms being in cosine space.** They bin raw similarity, and
|
|
|
|
|
/// that is not an AR-024 violation: no decision is taken here. These two
|
|
|
|
|
/// distributions are the *input* the calibration is fitted from — they cannot be
|
|
|
|
|
/// expressed in the probability space the calibration defines, because that
|
|
|
|
|
/// space is their output. GR-003 asks for exactly this ("the intra/inter
|
|
|
|
|
/// distributions behind it"), for the same reason GR-008 characterises an
|
|
|
|
|
/// actor's reference spread in the metric space: shape is a property of the
|
|
|
|
|
/// metric, decisions are a property of the probability.
|
|
|
|
|
#include "gallery/gallery_calibration.hpp"
|
|
|
|
|
#include "gallery/embedder_stamp.hpp"
|
|
|
|
|
#include "types.hpp"
|
|
|
|
|
|
|
|
|
|
#include <nlohmann/json.hpp>
|
|
|
|
|
|
|
|
|
|
#include <algorithm>
|
|
|
|
|
#include <cmath>
|
|
|
|
|
#include <cstdint>
|
|
|
|
|
#include <fstream>
|
|
|
|
|
#include <stdexcept>
|
|
|
|
|
#include <string>
|
|
|
|
|
#include <vector>
|
|
|
|
|
|
|
|
|
|
/// TRACES: GR-003 | SR-001
|
|
|
|
|
///
|
|
|
|
|
/// Per-actor image accounting from the build pass, including the actors that
|
|
|
|
|
/// produced nothing and were therefore dropped from the gallery.
|
|
|
|
|
///
|
|
|
|
|
/// Filled by `build_gallery()`. It has to be collected there and cannot be
|
|
|
|
|
/// recovered later: by the time a gallery exists, an actor with no usable image
|
|
|
|
|
/// is indistinguishable from an actor who was never requested.
|
|
|
|
|
struct GalleryBuildAudit {
|
|
|
|
|
struct ActorImages {
|
|
|
|
|
std::string imdb_id;
|
|
|
|
|
std::string name;
|
|
|
|
|
int images_seen = 0; // candidate image files in the actor's directory
|
|
|
|
|
int images_used = 0; // ...that yielded an embedding
|
|
|
|
|
int unreadable = 0; // cv::imread failed
|
|
|
|
|
int no_face = 0; // detector found nothing
|
|
|
|
|
int align_failed = 0; // 5-point warp failed
|
|
|
|
|
};
|
|
|
|
|
std::vector<ActorImages> actors; // every directory seen, in build order
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/// TRACES: GR-003 | SR-001
|
|
|
|
|
struct GalleryReport {
|
|
|
|
|
// One row per actor the build considered. Actors with references == 0 are
|
|
|
|
|
// the zero-usable-image case: present in the source tree, absent from the
|
|
|
|
|
// gallery, unrecognisable for the life of the file.
|
|
|
|
|
struct Actor {
|
|
|
|
|
std::string imdb_id;
|
|
|
|
|
std::string name;
|
|
|
|
|
int images_seen = -1; // -1 = unknown (report built without a build audit)
|
|
|
|
|
int references = 0; // embeddings stored in the gallery
|
|
|
|
|
int distinct_references = 0; // ...after near-duplicate removal
|
|
|
|
|
int duplicates_removed = 0;
|
|
|
|
|
bool eligible_for_positive_pairs = false;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// The two distributions the sigmoid is fitted from, as the fit itself saw
|
|
|
|
|
// them: counts per similarity bin over [sim_min, sim_max].
|
|
|
|
|
struct Distributions {
|
|
|
|
|
int bins = 0;
|
|
|
|
|
float sim_min = -1.f;
|
|
|
|
|
float sim_max = 1.f;
|
|
|
|
|
std::vector<double> intra; // same actor, different reference image
|
|
|
|
|
std::vector<double> inter; // different actors
|
|
|
|
|
double intra_pairs = 0.0;
|
|
|
|
|
double inter_pairs = 0.0;
|
|
|
|
|
double intra_mean = 0.0;
|
|
|
|
|
double inter_mean = 0.0;
|
|
|
|
|
// Normalised histogram intersection, Σ_b min(p_intra[b], p_inter[b]).
|
|
|
|
|
// 0 = perfectly separated, 1 = indistinguishable. This is the number
|
|
|
|
|
// that says whether the calibration — and so every threshold expressed
|
|
|
|
|
// in its probability space — rests on anything.
|
|
|
|
|
double overlap = 0.0;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// GR-003 / AR-023 open question, reported but NOT applied. The spec asks for
|
|
|
|
|
// a gallery-derived prior of intra/(intra+inter); the shipped default is
|
|
|
|
|
// 0.5. Persisting the distributions makes the real value computable, so the
|
|
|
|
|
// decision can be taken on evidence rather than left implicit. Behaviour is
|
|
|
|
|
// unchanged: `applied` is always false here.
|
|
|
|
|
struct Prior {
|
|
|
|
|
double derived = 0.0; // intra_pairs / (intra_pairs + inter_pairs)
|
|
|
|
|
double derived_log_odds = 0.0; // log(p/(1-p)), the term AR-023 would add
|
|
|
|
|
float configured_default = 0.5f;
|
|
|
|
|
bool applied = false;
|
|
|
|
|
std::string note;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
std::string schema{"sae.gallery_report/1"};
|
|
|
|
|
std::string gallery_path;
|
|
|
|
|
EmbedderStamp embedder;
|
|
|
|
|
|
|
|
|
|
// ── Summary ──────────────────────────────────────────────────────────────
|
|
|
|
|
int actors_total = 0; // considered (gallery + zero-usable)
|
|
|
|
|
int actors_in_gallery = 0;
|
|
|
|
|
int actors_zero_usable = 0;
|
|
|
|
|
int actors_below_positive_threshold = 0;
|
|
|
|
|
int64_t embeddings_total = 0;
|
|
|
|
|
int64_t distinct_embeddings_total = 0;
|
|
|
|
|
int64_t duplicates_removed_total = 0;
|
|
|
|
|
double mean_embeddings_per_actor = 0.0; // over actors in the gallery
|
|
|
|
|
int min_embeddings_for_positive_pairs = 0;
|
|
|
|
|
float dedup_similarity_threshold = 0.f;
|
|
|
|
|
|
|
|
|
|
// ── Calibration ──────────────────────────────────────────────────────────
|
|
|
|
|
float calib_a = 10.f;
|
|
|
|
|
float calib_b = -5.f;
|
|
|
|
|
bool calib_valid = false;
|
|
|
|
|
uint64_t calib_hash = 0;
|
|
|
|
|
double calib_train_accuracy_pct = 0.0;
|
|
|
|
|
float calib_boundary_p50 = 0.f; // similarity at which P(match) = 0.5
|
|
|
|
|
|
|
|
|
|
Distributions distributions;
|
|
|
|
|
Prior prior;
|
|
|
|
|
|
|
|
|
|
std::vector<Actor> actors;
|
|
|
|
|
// Names duplicated out of `actors` so the two failure modes are greppable
|
|
|
|
|
// without a JSON query. These are the lines a human reads first.
|
|
|
|
|
std::vector<std::string> zero_usable;
|
|
|
|
|
std::vector<std::string> below_positive_threshold;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/// TRACES: GR-003 | SR-001
|
|
|
|
|
///
|
|
|
|
|
/// Assembles the report from the three things that know a piece of the answer:
|
|
|
|
|
/// the gallery itself (who is in it, with how many references), the calibration
|
|
|
|
|
/// stats (dedup, eligibility, the two distributions), and the build audit (who
|
|
|
|
|
/// was considered and produced nothing). The audit is optional — a report built
|
|
|
|
|
/// from a stored gallery simply cannot know about the actors that never made it.
|
|
|
|
|
///
|
|
|
|
|
/// `stats` is indexed by actor index, so the flat arrays handed to
|
|
|
|
|
/// `calibrate_gallery()` must have used the gallery's own actor ordering.
|
|
|
|
|
inline GalleryReport build_gallery_report(const ActorGallery& gallery,
|
|
|
|
|
const GalleryCalibration& cal,
|
|
|
|
|
const GalleryCalibrationStats& stats,
|
|
|
|
|
const GalleryBuildAudit* audit = nullptr,
|
|
|
|
|
const std::string& gallery_path = "",
|
|
|
|
|
float configured_prior = 0.5f)
|
|
|
|
|
{
|
|
|
|
|
GalleryReport r;
|
|
|
|
|
r.gallery_path = gallery_path;
|
|
|
|
|
r.embedder = gallery.embedder;
|
|
|
|
|
|
|
|
|
|
r.calib_a = cal.a;
|
|
|
|
|
r.calib_b = cal.b;
|
|
|
|
|
r.calib_valid = cal.valid;
|
|
|
|
|
r.calib_hash = gallery.calib_hash;
|
|
|
|
|
r.calib_train_accuracy_pct = stats.train_accuracy_pct;
|
|
|
|
|
r.calib_boundary_p50 = cal.boundary_at(0.5f);
|
|
|
|
|
|
|
|
|
|
r.min_embeddings_for_positive_pairs = stats.min_embeddings_for_positive;
|
|
|
|
|
r.dedup_similarity_threshold = stats.dedup_sim_threshold;
|
|
|
|
|
|
|
|
|
|
auto audit_for = [&](const ActorGallery::Actor& a) -> const GalleryBuildAudit::ActorImages* {
|
|
|
|
|
if (!audit) return nullptr;
|
|
|
|
|
for (const auto& e : audit->actors) {
|
|
|
|
|
if (!a.imdb_id.empty() && e.imdb_id == a.imdb_id) return &e;
|
|
|
|
|
if (a.imdb_id.empty() && e.name == a.name) return &e;
|
|
|
|
|
}
|
|
|
|
|
return nullptr;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
for (size_t i = 0; i < gallery.actors.size(); ++i) {
|
|
|
|
|
const auto& ga = gallery.actors[i];
|
|
|
|
|
GalleryReport::Actor row;
|
|
|
|
|
row.imdb_id = ga.imdb_id;
|
|
|
|
|
row.name = ga.name;
|
|
|
|
|
row.references = static_cast<int>(ga.embeddings.size());
|
|
|
|
|
if (const auto* au = audit_for(ga)) row.images_seen = au->images_seen;
|
|
|
|
|
|
|
|
|
|
if (i < stats.distinct_per_actor.size()) {
|
|
|
|
|
row.distinct_references = stats.distinct_per_actor[i];
|
|
|
|
|
row.duplicates_removed = stats.duplicates_removed_per_actor[i];
|
|
|
|
|
row.eligible_for_positive_pairs = stats.eligible[i] != 0;
|
|
|
|
|
} else {
|
|
|
|
|
// No calibration stats for this actor (the fit never saw them).
|
|
|
|
|
// Report the raw count rather than a fabricated distinct count.
|
|
|
|
|
row.distinct_references = row.references;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
r.embeddings_total += row.references;
|
|
|
|
|
r.distinct_embeddings_total += row.distinct_references;
|
|
|
|
|
r.duplicates_removed_total += row.duplicates_removed;
|
|
|
|
|
if (!row.eligible_for_positive_pairs) {
|
|
|
|
|
++r.actors_below_positive_threshold;
|
|
|
|
|
r.below_positive_threshold.push_back(row.name);
|
|
|
|
|
}
|
|
|
|
|
r.actors.push_back(std::move(row));
|
|
|
|
|
}
|
|
|
|
|
r.actors_in_gallery = static_cast<int>(gallery.actors.size());
|
|
|
|
|
|
|
|
|
|
// Actors the build considered and could not use at all. They are not in the
|
|
|
|
|
// gallery, so this is the only record that they exist.
|
|
|
|
|
if (audit) {
|
|
|
|
|
for (const auto& e : audit->actors) {
|
|
|
|
|
if (e.images_used > 0) continue;
|
|
|
|
|
GalleryReport::Actor row;
|
|
|
|
|
row.imdb_id = e.imdb_id;
|
|
|
|
|
row.name = e.name;
|
|
|
|
|
row.images_seen = e.images_seen;
|
|
|
|
|
row.references = 0;
|
|
|
|
|
r.zero_usable.push_back(e.name);
|
|
|
|
|
r.actors.push_back(std::move(row));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
r.actors_zero_usable = static_cast<int>(r.zero_usable.size());
|
|
|
|
|
r.actors_total = r.actors_in_gallery + r.actors_zero_usable;
|
|
|
|
|
r.mean_embeddings_per_actor =
|
|
|
|
|
r.actors_in_gallery > 0
|
|
|
|
|
? static_cast<double>(r.embeddings_total) / r.actors_in_gallery
|
|
|
|
|
: 0.0;
|
|
|
|
|
|
|
|
|
|
// ── The two distributions, straight out of the fit ───────────────────────
|
|
|
|
|
auto& d = r.distributions;
|
|
|
|
|
d.bins = stats.hist_bins;
|
|
|
|
|
d.sim_min = -1.f;
|
|
|
|
|
d.sim_max = 1.f;
|
|
|
|
|
d.intra = stats.intra_hist;
|
|
|
|
|
d.inter = stats.inter_hist;
|
|
|
|
|
d.intra_pairs = stats.n_intra_pairs;
|
|
|
|
|
d.inter_pairs = stats.n_inter_pairs;
|
|
|
|
|
|
|
|
|
|
if (d.bins > 0) {
|
|
|
|
|
const double bin_w = (d.sim_max - d.sim_min) / d.bins;
|
|
|
|
|
double si = 0.0, se = 0.0;
|
|
|
|
|
for (int b = 0; b < d.bins; ++b) {
|
|
|
|
|
const double centre = d.sim_min + (b + 0.5) * bin_w;
|
|
|
|
|
si += d.intra[b] * centre;
|
|
|
|
|
se += d.inter[b] * centre;
|
|
|
|
|
}
|
|
|
|
|
if (d.intra_pairs > 0.0) d.intra_mean = si / d.intra_pairs;
|
|
|
|
|
if (d.inter_pairs > 0.0) d.inter_mean = se / d.inter_pairs;
|
|
|
|
|
if (d.intra_pairs > 0.0 && d.inter_pairs > 0.0) {
|
|
|
|
|
double ov = 0.0;
|
|
|
|
|
for (int b = 0; b < d.bins; ++b)
|
|
|
|
|
ov += std::min(d.intra[b] / d.intra_pairs, d.inter[b] / d.inter_pairs);
|
|
|
|
|
d.overlap = ov;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── The prior AR-023 leaves open — computed, reported, not applied ────────
|
|
|
|
|
r.prior.configured_default = configured_prior;
|
|
|
|
|
r.prior.applied = false;
|
|
|
|
|
const double pair_total = d.intra_pairs + d.inter_pairs;
|
|
|
|
|
if (pair_total > 0.0) {
|
|
|
|
|
r.prior.derived = d.intra_pairs / pair_total;
|
|
|
|
|
const double p = std::clamp(r.prior.derived, 1e-12, 1.0 - 1e-12);
|
|
|
|
|
r.prior.derived_log_odds = std::log(p / (1.0 - p));
|
|
|
|
|
}
|
|
|
|
|
r.prior.note =
|
|
|
|
|
"AR-023 specifies a gallery-derived prior of intra/(intra+inter); the shipped "
|
|
|
|
|
"match_prior default is 0.5 (calibrated sigmoid used directly). The derived value "
|
|
|
|
|
"is the base rate of same-actor pairs among ALL enumerated gallery pairs, so it "
|
|
|
|
|
"falls as the cast grows (roughly (k-1)/((k-1)+(A-1)k) for A actors with k "
|
|
|
|
|
"references each) — it is a property of gallery size as much as of the embedder. "
|
|
|
|
|
"Reported here as evidence; NOT applied. Behaviour is unchanged until the choice "
|
|
|
|
|
"is recorded in the spec.";
|
|
|
|
|
|
|
|
|
|
return r;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── JSON ─────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/// TRACES: GR-003 | SR-001
|
|
|
|
|
inline nlohmann::json gallery_report_to_json(const GalleryReport& r) {
|
|
|
|
|
nlohmann::json j;
|
|
|
|
|
j["schema"] = r.schema;
|
|
|
|
|
j["gallery_path"] = r.gallery_path;
|
|
|
|
|
j["embedder"] = {{"model_name", r.embedder.model_name},
|
|
|
|
|
{"model_sha256", r.embedder.model_sha256},
|
|
|
|
|
{"embed_dim", r.embedder.embed_dim}};
|
|
|
|
|
|
|
|
|
|
j["summary"] = {
|
|
|
|
|
{"actors_total", r.actors_total},
|
|
|
|
|
{"actors_in_gallery", r.actors_in_gallery},
|
|
|
|
|
{"actors_zero_usable", r.actors_zero_usable},
|
|
|
|
|
{"actors_below_positive_threshold", r.actors_below_positive_threshold},
|
|
|
|
|
{"embeddings_total", r.embeddings_total},
|
|
|
|
|
{"distinct_embeddings_total", r.distinct_embeddings_total},
|
|
|
|
|
{"duplicates_removed_total", r.duplicates_removed_total},
|
|
|
|
|
{"mean_embeddings_per_actor", r.mean_embeddings_per_actor},
|
|
|
|
|
{"min_embeddings_for_positive_pairs", r.min_embeddings_for_positive_pairs},
|
|
|
|
|
{"dedup_similarity_threshold", r.dedup_similarity_threshold}};
|
|
|
|
|
|
|
|
|
|
j["calibration"] = {
|
|
|
|
|
{"a", r.calib_a},
|
|
|
|
|
{"b", r.calib_b},
|
|
|
|
|
{"valid", r.calib_valid},
|
|
|
|
|
{"hash", r.calib_hash},
|
|
|
|
|
{"train_accuracy_pct", r.calib_train_accuracy_pct},
|
|
|
|
|
{"boundary_p50", r.calib_boundary_p50}};
|
|
|
|
|
|
|
|
|
|
const auto& d = r.distributions;
|
|
|
|
|
j["distributions"] = {
|
|
|
|
|
{"bins", d.bins},
|
|
|
|
|
{"sim_min", d.sim_min},
|
|
|
|
|
{"sim_max", d.sim_max},
|
|
|
|
|
{"intra", d.intra},
|
|
|
|
|
{"inter", d.inter},
|
|
|
|
|
{"intra_pairs", d.intra_pairs},
|
|
|
|
|
{"inter_pairs", d.inter_pairs},
|
|
|
|
|
{"intra_mean", d.intra_mean},
|
|
|
|
|
{"inter_mean", d.inter_mean},
|
|
|
|
|
{"overlap", d.overlap}};
|
|
|
|
|
|
|
|
|
|
j["prior"] = {
|
|
|
|
|
{"derived", r.prior.derived},
|
|
|
|
|
{"derived_log_odds", r.prior.derived_log_odds},
|
|
|
|
|
{"configured_default", r.prior.configured_default},
|
|
|
|
|
{"applied", r.prior.applied},
|
|
|
|
|
{"note", r.prior.note}};
|
|
|
|
|
|
|
|
|
|
j["zero_usable"] = r.zero_usable;
|
|
|
|
|
j["below_positive_threshold"] = r.below_positive_threshold;
|
|
|
|
|
|
|
|
|
|
j["actors"] = nlohmann::json::array();
|
|
|
|
|
for (const auto& a : r.actors) {
|
|
|
|
|
j["actors"].push_back({
|
|
|
|
|
{"imdb_id", a.imdb_id},
|
|
|
|
|
{"name", a.name},
|
|
|
|
|
{"images_seen", a.images_seen},
|
|
|
|
|
{"references", a.references},
|
|
|
|
|
{"distinct_references", a.distinct_references},
|
|
|
|
|
{"duplicates_removed", a.duplicates_removed},
|
|
|
|
|
{"eligible_for_positive_pairs", a.eligible_for_positive_pairs}});
|
|
|
|
|
}
|
|
|
|
|
return j;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// TRACES: GR-003 | SR-001
|
|
|
|
|
inline GalleryReport gallery_report_from_json(const nlohmann::json& j) {
|
|
|
|
|
GalleryReport r;
|
|
|
|
|
r.schema = j.value("schema", std::string{});
|
|
|
|
|
r.gallery_path = j.value("gallery_path", std::string{});
|
|
|
|
|
if (j.contains("embedder")) {
|
|
|
|
|
const auto& je = j.at("embedder");
|
|
|
|
|
r.embedder.model_name = je.value("model_name", "");
|
|
|
|
|
r.embedder.model_sha256 = je.value("model_sha256", "");
|
|
|
|
|
r.embedder.embed_dim = je.value("embed_dim", 512);
|
|
|
|
|
}
|
|
|
|
|
if (j.contains("summary")) {
|
|
|
|
|
const auto& s = j.at("summary");
|
|
|
|
|
r.actors_total = s.value("actors_total", 0);
|
|
|
|
|
r.actors_in_gallery = s.value("actors_in_gallery", 0);
|
|
|
|
|
r.actors_zero_usable = s.value("actors_zero_usable", 0);
|
|
|
|
|
r.actors_below_positive_threshold = s.value("actors_below_positive_threshold", 0);
|
|
|
|
|
r.embeddings_total = s.value("embeddings_total", int64_t{0});
|
|
|
|
|
r.distinct_embeddings_total = s.value("distinct_embeddings_total", int64_t{0});
|
|
|
|
|
r.duplicates_removed_total = s.value("duplicates_removed_total", int64_t{0});
|
|
|
|
|
r.mean_embeddings_per_actor = s.value("mean_embeddings_per_actor", 0.0);
|
|
|
|
|
r.min_embeddings_for_positive_pairs = s.value("min_embeddings_for_positive_pairs", 0);
|
|
|
|
|
r.dedup_similarity_threshold = s.value("dedup_similarity_threshold", 0.f);
|
|
|
|
|
}
|
|
|
|
|
if (j.contains("calibration")) {
|
|
|
|
|
const auto& c = j.at("calibration");
|
|
|
|
|
r.calib_a = c.value("a", 10.f);
|
|
|
|
|
r.calib_b = c.value("b", -5.f);
|
|
|
|
|
r.calib_valid = c.value("valid", false);
|
|
|
|
|
r.calib_hash = c.value("hash", uint64_t{0});
|
|
|
|
|
r.calib_train_accuracy_pct = c.value("train_accuracy_pct", 0.0);
|
|
|
|
|
r.calib_boundary_p50 = c.value("boundary_p50", 0.f);
|
|
|
|
|
}
|
|
|
|
|
if (j.contains("distributions")) {
|
|
|
|
|
const auto& d = j.at("distributions");
|
|
|
|
|
r.distributions.bins = d.value("bins", 0);
|
|
|
|
|
r.distributions.sim_min = d.value("sim_min", -1.f);
|
|
|
|
|
r.distributions.sim_max = d.value("sim_max", 1.f);
|
|
|
|
|
r.distributions.intra = d.value("intra", std::vector<double>{});
|
|
|
|
|
r.distributions.inter = d.value("inter", std::vector<double>{});
|
|
|
|
|
r.distributions.intra_pairs = d.value("intra_pairs", 0.0);
|
|
|
|
|
r.distributions.inter_pairs = d.value("inter_pairs", 0.0);
|
|
|
|
|
r.distributions.intra_mean = d.value("intra_mean", 0.0);
|
|
|
|
|
r.distributions.inter_mean = d.value("inter_mean", 0.0);
|
|
|
|
|
r.distributions.overlap = d.value("overlap", 0.0);
|
|
|
|
|
}
|
|
|
|
|
if (j.contains("prior")) {
|
|
|
|
|
const auto& p = j.at("prior");
|
|
|
|
|
r.prior.derived = p.value("derived", 0.0);
|
|
|
|
|
r.prior.derived_log_odds = p.value("derived_log_odds", 0.0);
|
|
|
|
|
r.prior.configured_default = p.value("configured_default", 0.5f);
|
|
|
|
|
r.prior.applied = p.value("applied", false);
|
|
|
|
|
r.prior.note = p.value("note", "");
|
|
|
|
|
}
|
|
|
|
|
r.zero_usable = j.value("zero_usable", std::vector<std::string>{});
|
|
|
|
|
r.below_positive_threshold = j.value("below_positive_threshold", std::vector<std::string>{});
|
|
|
|
|
if (j.contains("actors")) {
|
|
|
|
|
for (const auto& ja : j.at("actors")) {
|
|
|
|
|
GalleryReport::Actor a;
|
|
|
|
|
a.imdb_id = ja.value("imdb_id", "");
|
|
|
|
|
a.name = ja.value("name", "");
|
|
|
|
|
a.images_seen = ja.value("images_seen", -1);
|
|
|
|
|
a.references = ja.value("references", 0);
|
|
|
|
|
a.distinct_references = ja.value("distinct_references", 0);
|
|
|
|
|
a.duplicates_removed = ja.value("duplicates_removed", 0);
|
|
|
|
|
a.eligible_for_positive_pairs = ja.value("eligible_for_positive_pairs", false);
|
|
|
|
|
r.actors.push_back(std::move(a));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return r;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// "<dir>/cast.h5" → "<dir>/cast.report.json". A known gallery extension is
|
|
|
|
|
// replaced rather than appended to, so the report sits beside the gallery under
|
|
|
|
|
// the same stem.
|
|
|
|
|
inline std::string gallery_report_path(const std::string& gallery_path) {
|
|
|
|
|
auto slash = gallery_path.find_last_of("/\\");
|
|
|
|
|
auto dot = gallery_path.find_last_of('.');
|
|
|
|
|
std::string stem =
|
|
|
|
|
(dot != std::string::npos && (slash == std::string::npos || dot > slash))
|
|
|
|
|
? gallery_path.substr(0, dot)
|
|
|
|
|
: gallery_path;
|
|
|
|
|
return stem + ".report.json";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// TRACES: GR-003 | SR-001
|
|
|
|
|
inline void save_gallery_report(const std::string& path, const GalleryReport& r) {
|
|
|
|
|
std::ofstream out(path);
|
|
|
|
|
if (!out.is_open())
|
|
|
|
|
throw std::runtime_error("save_gallery_report: cannot write " + path);
|
|
|
|
|
out << gallery_report_to_json(r).dump(2) << "\n";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// TRACES: GR-003 | SR-001
|
|
|
|
|
inline GalleryReport load_gallery_report(const std::string& path) {
|
|
|
|
|
std::ifstream in(path);
|
|
|
|
|
if (!in.is_open())
|
|
|
|
|
throw std::runtime_error("load_gallery_report: cannot open " + path);
|
|
|
|
|
nlohmann::json j;
|
|
|
|
|
in >> j;
|
|
|
|
|
return gallery_report_from_json(j);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// TRACES: GR-003 | SR-001
|
|
|
|
|
///
|
|
|
|
|
/// The report's headline, on stderr, at build time. The file is the audit trail;
|
|
|
|
|
/// this is what stops a bad gallery from being shipped without anyone noticing.
|
|
|
|
|
inline void log_gallery_report(const GalleryReport& r) {
|
|
|
|
|
std::cerr << "[gallery-report] " << r.actors_in_gallery << " actors / "
|
|
|
|
|
<< r.embeddings_total << " embeddings"
|
|
|
|
|
<< " (mean " << r.mean_embeddings_per_actor << " per actor)\n";
|
|
|
|
|
if (r.actors_zero_usable > 0) {
|
|
|
|
|
std::cerr << "[gallery-report] WARNING: " << r.actors_zero_usable
|
|
|
|
|
<< " actor(s) have NO usable image — they can never be recognised:\n";
|
|
|
|
|
for (const auto& n : r.zero_usable) std::cerr << " - " << n << "\n";
|
|
|
|
|
}
|
|
|
|
|
if (r.actors_below_positive_threshold > 0) {
|
|
|
|
|
std::cerr << "[gallery-report] " << r.actors_below_positive_threshold
|
|
|
|
|
<< " actor(s) below " << r.min_embeddings_for_positive_pairs
|
|
|
|
|
<< " distinct references — they contribute no positive pairs and "
|
|
|
|
|
"weaken the calibration\n";
|
|
|
|
|
}
|
|
|
|
|
if (r.duplicates_removed_total > 0)
|
|
|
|
|
std::cerr << "[gallery-report] " << r.duplicates_removed_total
|
|
|
|
|
<< " near-duplicate reference(s) removed\n";
|
|
|
|
|
std::cerr << "[gallery-report] calibration valid=" << r.calib_valid
|
|
|
|
|
<< " a=" << r.calib_a << " b=" << r.calib_b
|
|
|
|
|
<< " intra/inter overlap=" << r.distributions.overlap
|
|
|
|
|
<< " (intra mean=" << r.distributions.intra_mean
|
|
|
|
|
<< ", inter mean=" << r.distributions.inter_mean << ")\n";
|
|
|
|
|
std::cerr << "[gallery-report] gallery-derived prior would be "
|
|
|
|
|
<< r.prior.derived << " (log-odds " << r.prior.derived_log_odds
|
|
|
|
|
<< "); shipped default " << r.prior.configured_default
|
|
|
|
|
<< " is in force — reported, not applied\n";
|
|
|
|
|
}
|