GR-003 — the calibration fit already computed per-actor dedup counts, how many actors are eligible for positive pairs, and a 200-bin histogram of the intra and inter distributions, then discarded all of it to stderr. Nothing persisted, so nobody could audit whether a gallery was any good. The report is written alongside the gallery at build time. That is the right moment: the matcher fits the same sigmoid at analysis time, but by then the answer is per-run and nobody is looking, whereas build time is when a gallery's quality is actually decided. What it surfaces, in order of usefulness: - actors with no usable image — a silent recall ceiling, since the pipeline can never name them and nothing else says why - actors below the positive-pair threshold — not broken, so nothing complains; they just quietly weaken every threshold downstream - near-duplicate references removed, per actor and total - the fitted calibration AND the two distributions behind it That last one is the point. Every threshold in the pipeline is expressed in the probability space this sigmoid defines, so if the distributions overlap heavily the calibration is weak and every downstream decision inherits it — while the gallery still looks fine from the outside. The gallery-derived prior, intra/(intra+inter), is computed and reported but the shipped default of 0.5 is deliberately left alone. The spec records these as disagreeing; now the real value is visible, so the decision can be made on evidence rather than argument. Three tests: a zero-image actor is visible in the report, an under-referenced actor is counted, and the report round-trips through JSON. Suite: 95 cases, 6142 assertions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> TRACES: GR-003 | SR-001
112 lines
4.3 KiB
C++
112 lines
4.3 KiB
C++
// build_gallery — build an actor embedding gallery from a directory of images
|
|
//
|
|
// Gallery directory layout:
|
|
// gallery_root/
|
|
// nm0000093_Brad_Pitt/
|
|
// img1.jpg
|
|
// img2.jpg
|
|
// nm0000129_Cate_Blanchett/
|
|
// ...
|
|
//
|
|
// Usage:
|
|
// build_gallery --root <gallery_root> --output <gallery.json> [options]
|
|
//
|
|
// Options:
|
|
// --detector <path> SCRFD detector model (default: models/scrfd_500m_bnkps.onnx)
|
|
// --arcface <path> ArcFace model (default: models/arcface_w600k_r50.onnx)
|
|
// --conf <f> face detection confidence threshold (default: 0.5)
|
|
// --nms <f> NMS IoU threshold (default: 0.4)
|
|
|
|
#include "gallery/gallery_builder.hpp"
|
|
#include "gallery/gallery_report.hpp"
|
|
#include "gallery/gallery_calibration.hpp"
|
|
#include "gallery/gallery_store.hpp"
|
|
#include "config.hpp"
|
|
|
|
#include <cstring>
|
|
#include <iostream>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
|
|
int main(int argc, char** argv) {
|
|
std::string root_path, output_path;
|
|
std::string detector_model = kDefaultDetectorModel;
|
|
std::string arcface_model = kDefaultArcfaceModel;
|
|
float conf = 0.5f, nms_thr = 0.4f;
|
|
int max_side = 500;
|
|
|
|
for (int i = 1; i < argc; ++i) {
|
|
auto arg = [&](const char* f) { return std::strcmp(argv[i], f) == 0; };
|
|
auto next = [&]() -> std::string {
|
|
if (++i >= argc)
|
|
throw std::runtime_error(std::string("missing arg after ") + argv[i-1]);
|
|
return argv[i];
|
|
};
|
|
try {
|
|
if (arg("--root")) root_path = next();
|
|
else if (arg("--output")) output_path = next();
|
|
else if (arg("--detector")) detector_model = next();
|
|
else if (arg("--arcface")) arcface_model = next();
|
|
else if (arg("--conf")) conf = std::stof(next());
|
|
else if (arg("--nms")) nms_thr = std::stof(next());
|
|
else if (arg("--max-side")) max_side = std::stoi(next());
|
|
else { std::cerr << "[warn] unknown flag: " << argv[i] << "\n"; }
|
|
} catch (const std::exception& e) {
|
|
std::cerr << "Error: " << e.what() << "\n";
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
if (root_path.empty() || output_path.empty()) {
|
|
std::cerr << "Usage: build_gallery --root <dir> --output <gallery.json> "
|
|
"[--detector <path>] [--arcface <path>] [--max-side <N>]\n";
|
|
return 1;
|
|
}
|
|
|
|
BuildConfig cfg;
|
|
cfg.gallery_root = root_path;
|
|
cfg.detector_model = detector_model;
|
|
cfg.arcface_model = arcface_model;
|
|
cfg.detector_conf = conf;
|
|
cfg.detector_nms = nms_thr;
|
|
cfg.max_side = max_side;
|
|
|
|
try {
|
|
ActorGallery gallery = build_gallery(cfg);
|
|
if (gallery.actors.empty()) {
|
|
std::cerr << "No actors built — check your gallery directory.\n";
|
|
return 1;
|
|
}
|
|
save_gallery(output_path, gallery);
|
|
std::cerr << "Gallery saved to: " << output_path << "\n";
|
|
|
|
/// TRACES: GR-003 | SR-001
|
|
// Fit the calibration here and persist what it learned. The matcher
|
|
// fits the same sigmoid at analysis time, but that is the wrong place
|
|
// to audit a gallery from: by then the answer is per-run and nobody is
|
|
// looking. Build time is when the gallery's quality is decided, and a
|
|
// gallery can be quietly bad — heavily overlapping intra/inter
|
|
// distributions, actors with no usable image — while looking fine.
|
|
std::vector<Embedding> flat;
|
|
std::vector<int> flat_actor;
|
|
for (int ai = 0; ai < static_cast<int>(gallery.actors.size()); ++ai)
|
|
for (const auto& e : gallery.actors[ai].embeddings) {
|
|
flat.push_back(e);
|
|
flat_actor.push_back(ai);
|
|
}
|
|
|
|
GalleryCalibrationStats stats;
|
|
GalleryCalibration cal = calibrate_gallery(flat, flat_actor, &stats);
|
|
|
|
const GalleryReport report =
|
|
build_gallery_report(gallery, cal, stats, nullptr, output_path);
|
|
const std::string report_path = gallery_report_path(output_path);
|
|
save_gallery_report(report_path, report);
|
|
std::cerr << "Gallery report saved to: " << report_path << "\n";
|
|
} catch (const std::exception& e) {
|
|
std::cerr << "Fatal: " << e.what() << "\n";
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|