Files
scene-actor-extraction/src/build_gallery.cpp
T
dtourolleandClaude Opus 5 1dfd6fea11 feat: gallery build report
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
2026-07-31 15:30:46 +02:00

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;
}