feat(engine): HDF5-native galleries with embedded calibration; TensorRT backends; scene detection
Gallery format switches from JSON to HDF5 exclusively (JSON read-only kept for back-compat): save_gallery always writes HDF5, and the fitted Platt-sigmoid calibration (a, b, valid, hash) is now embedded directly in the gallery file instead of a sidecar .calib_cache.json — identity_matcher reads it from the loaded gallery and writes back only when the embeddings actually changed (hash mismatch), skipping the O(n^2) refit otherwise. Also includes: TensorRT inference backend support (ort_backend.cpp, trt_backend.cpp), gemm_backend improvements, TransNetV2-based scene-boundary detection wired through frame_source/face_tracker/main, and CMake build target updates for the new sources. Bumps the KPN submodule to feature/persistent-pipeline-reuse (push_blocking backpressure, node_ptr/node_stats introspection, ObjectVariantNodeWrapper for stateful functors) — needed by the optimizer's sae_kpn Python bindings.
This commit is contained in:
@@ -26,10 +26,15 @@ struct FaceDetectorFunc {
|
||||
|
||||
auto faces = detector_->detect(f.image);
|
||||
|
||||
// Drop faces below minimum pixel size (too small for reliable ArcFace alignment)
|
||||
// Drop faces below minimum pixel size (too small for reliable ArcFace
|
||||
// alignment). Note: when dense_scale downscaled the frame, both the
|
||||
// detection coords and min_face_px are in downscaled space — so scale
|
||||
// the threshold down to match, keeping the physical size cutoff constant.
|
||||
const float min_px = (f.bbox_upscale != 1.f)
|
||||
? min_face_px_ / f.bbox_upscale : min_face_px_;
|
||||
faces.erase(
|
||||
std::remove_if(faces.begin(), faces.end(), [&](const DetectedFace& d) {
|
||||
return d.bbox.width < min_face_px_ || d.bbox.height < min_face_px_;
|
||||
return d.bbox.width < min_px || d.bbox.height < min_px;
|
||||
}),
|
||||
faces.end());
|
||||
|
||||
|
||||
@@ -23,6 +23,16 @@
|
||||
//
|
||||
// Unmatched tracks have their frames_missing counter incremented; they are
|
||||
// expired once frames_missing > max_frames_missing.
|
||||
//
|
||||
// Cross-cut re-association. A camera-angle change (Frame::is_cut, set by
|
||||
// camera_position_change_detector) destroys spatial (IoU) continuity — the same
|
||||
// person reappears at a new position — but not identity. On a cut the tracker
|
||||
// does NOT discard its tracks; it parks them in an inactive pool keyed by their
|
||||
// last-frame raw embedding. A post-cut detection whose raw cosine similarity to
|
||||
// a parked track's last-frame embedding is ≥ cut_revive_sim revives that track:
|
||||
// the original track_id, mean embedding and n_frames are restored (only the bbox
|
||||
// jumps to the new detection), so identity continuity survives the cut. Parked
|
||||
// tracks left unrevived for cut_inactive_max_frames are finally dropped.
|
||||
|
||||
struct FaceTrackerFunc {
|
||||
static constexpr std::string_view label() { return "face_tracker"; }
|
||||
@@ -30,6 +40,7 @@ struct FaceTrackerFunc {
|
||||
struct TrackState {
|
||||
cv::Rect2f bbox;
|
||||
Embedding mean_emb{};
|
||||
Embedding last_emb{}; // raw embedding of the most recent matched frame
|
||||
int n_frames{0};
|
||||
int frames_missing{0};
|
||||
};
|
||||
@@ -39,16 +50,21 @@ struct FaceTrackerFunc {
|
||||
, min_iou_(cfg.track_min_iou)
|
||||
, max_embed_dist_(cfg.track_max_embed_dist)
|
||||
, max_missing_(cfg.track_max_frames_missing)
|
||||
, revive_sim_(cfg.cut_revive_sim)
|
||||
, inactive_max_(cfg.cut_inactive_max_frames)
|
||||
{
|
||||
std::cerr << "[face_tracker] alpha=" << alpha_
|
||||
<< " min_iou=" << min_iou_
|
||||
<< " max_embed_dist=" << max_embed_dist_
|
||||
<< " max_missing=" << max_missing_ << "\n";
|
||||
<< " max_missing=" << max_missing_
|
||||
<< " cut_revive_sim=" << revive_sim_
|
||||
<< " cut_inactive_max=" << inactive_max_ << "\n";
|
||||
}
|
||||
|
||||
TrackedSceneFrame operator()(EmbeddedSceneFrame ef) {
|
||||
if (ef.source.eof) {
|
||||
tracks_.clear();
|
||||
inactive_.clear();
|
||||
TrackedSceneFrame out;
|
||||
out.source = std::move(ef.source);
|
||||
return out;
|
||||
@@ -56,11 +72,26 @@ struct FaceTrackerFunc {
|
||||
|
||||
const int n_det = static_cast<int>(ef.embeddings.size());
|
||||
|
||||
// Camera-angle change: park active tracks instead of destroying them so
|
||||
// they can be revived by identity (raw last-frame embedding cosine) once
|
||||
// the same people reappear from the new angle.
|
||||
if (ef.source.is_cut && !tracks_.empty()) {
|
||||
std::cerr << "[face_tracker] cut — clearing " << tracks_.size() << " tracks\n";
|
||||
std::cerr << "[face_tracker] cut — parking " << tracks_.size()
|
||||
<< " track(s) into inactive pool\n";
|
||||
for (auto& [tid, ts] : tracks_) {
|
||||
ts.frames_missing = 0; // repurpose as time-since-parked counter
|
||||
inactive_[tid] = std::move(ts);
|
||||
}
|
||||
tracks_.clear();
|
||||
}
|
||||
|
||||
// Age the inactive pool every frame and drop tracks parked too long.
|
||||
for (auto it = inactive_.begin(); it != inactive_.end(); ) {
|
||||
it->second.frames_missing++;
|
||||
it = (it->second.frames_missing > inactive_max_)
|
||||
? inactive_.erase(it) : std::next(it);
|
||||
}
|
||||
|
||||
// Snapshot active track IDs so the map can be modified safely below
|
||||
std::vector<int> tids;
|
||||
tids.reserve(tracks_.size());
|
||||
@@ -111,6 +142,7 @@ struct FaceTrackerFunc {
|
||||
continue;
|
||||
}
|
||||
update_mean(ts.mean_emb, ts.n_frames, ef.embeddings[di]);
|
||||
ts.last_emb = ef.embeddings[di];
|
||||
ts.bbox = ef.faces[di].bbox;
|
||||
ts.n_frames++;
|
||||
ts.frames_missing = 0;
|
||||
@@ -119,13 +151,34 @@ struct FaceTrackerFunc {
|
||||
out.track_ids[di] = tids[ti];
|
||||
}
|
||||
|
||||
// Create new tracks for unmatched detections
|
||||
// Handle unmatched detections: first try to revive a parked track by
|
||||
// identity (raw last-frame embedding cosine), else start a fresh track.
|
||||
for (int di = 0; di < n_det; ++di) {
|
||||
if (det_matched[di]) continue;
|
||||
int tid = next_id_++;
|
||||
|
||||
int tid = revive_from_inactive(ef.embeddings[di]);
|
||||
if (tid >= 0) {
|
||||
// Restore the parked track: keep its identity statistics
|
||||
// (mean_emb, n_frames), jump the bbox to the new detection.
|
||||
TrackState ts = std::move(inactive_[tid]);
|
||||
inactive_.erase(tid);
|
||||
update_mean(ts.mean_emb, ts.n_frames, ef.embeddings[di]);
|
||||
ts.last_emb = ef.embeddings[di];
|
||||
ts.bbox = ef.faces[di].bbox;
|
||||
ts.n_frames++;
|
||||
ts.frames_missing = 0;
|
||||
tracks_[tid] = std::move(ts);
|
||||
out.track_ids[di] = tid;
|
||||
std::cerr << "[face_tracker] revived track " << tid
|
||||
<< " across cut\n";
|
||||
continue;
|
||||
}
|
||||
|
||||
tid = next_id_++;
|
||||
TrackState ts;
|
||||
ts.bbox = ef.faces[di].bbox;
|
||||
ts.mean_emb = ef.embeddings[di];
|
||||
ts.last_emb = ef.embeddings[di];
|
||||
ts.n_frames = 1;
|
||||
tracks_[tid] = ts;
|
||||
out.track_ids[di] = tid;
|
||||
@@ -141,6 +194,21 @@ struct FaceTrackerFunc {
|
||||
}
|
||||
|
||||
private:
|
||||
// Pick the parked track whose last-frame embedding is most similar to emb,
|
||||
// returning its id if that raw cosine similarity clears revive_sim_, else -1.
|
||||
// The caller removes the returned track from the pool, so a later detection in
|
||||
// the same frame cannot claim it again.
|
||||
int revive_from_inactive(const Embedding& emb) const {
|
||||
int best_tid = -1;
|
||||
float best_sim = revive_sim_; // threshold is the bar to beat (inclusive)
|
||||
for (const auto& [tid, ts] : inactive_) {
|
||||
float sim = cosine_similarity(ts.last_emb, emb);
|
||||
if (sim >= best_sim) { best_sim = sim; best_tid = tid; }
|
||||
// subsequent ties keep the later id; harmless, all clear the threshold
|
||||
}
|
||||
return best_tid;
|
||||
}
|
||||
|
||||
// IoU of two axis-aligned bounding boxes
|
||||
static float iou(const cv::Rect2f& a, const cv::Rect2f& b) {
|
||||
float ix = std::max(0.f, std::min(a.x + a.width, b.x + b.width)
|
||||
@@ -225,9 +293,12 @@ private:
|
||||
}
|
||||
|
||||
std::map<int, TrackState> tracks_;
|
||||
std::map<int, TrackState> inactive_; // parked across a cut, keyed by track id
|
||||
int next_id_{0};
|
||||
float alpha_;
|
||||
float min_iou_;
|
||||
float max_embed_dist_;
|
||||
int max_missing_;
|
||||
float revive_sim_;
|
||||
int inactive_max_;
|
||||
};
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#include "config.hpp"
|
||||
#include "ffmpeg_decoder.hpp"
|
||||
|
||||
#include <opencv2/imgproc.hpp>
|
||||
#include <chrono>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
@@ -27,21 +26,46 @@ struct FrameSourceFunc {
|
||||
static constexpr std::string_view label() { return "frame_source"; }
|
||||
|
||||
explicit FrameSourceFunc(const Config& cfg)
|
||||
: decoder_(std::make_unique<FFmpegDecoder>(cfg.movie_path))
|
||||
: decoder_(std::make_unique<FFmpegDecoder>(
|
||||
cfg.movie_path, /*use_hw=*/true,
|
||||
/*out_scale=*/cfg.scene_detect ? cfg.dense_scale : 1.0f))
|
||||
{
|
||||
sample_interval_sec_ = 1.0 / cfg.sample_fps;
|
||||
next_pos_sec_ = cfg.start_sec;
|
||||
end_sec_ = cfg.end_sec;
|
||||
cut_threshold_ = cfg.cut_threshold;
|
||||
max_decode_fps_ = cfg.max_decode_fps;
|
||||
|
||||
// Dense mode: emit every native-rate frame instead of seeking to each
|
||||
// 1-FPS sample point. Required by the TransNetV2 scene detector, which
|
||||
// needs consecutive frames. A downstream decimator drops back to
|
||||
// sample_fps for the face pipeline. When dense, we advance by the
|
||||
// decoder's frame period (best effort — read_at decodes forward past the
|
||||
// last position, so consecutive small steps yield consecutive frames).
|
||||
dense_ = cfg.scene_detect;
|
||||
double dense_fps = 0.0;
|
||||
if (dense_) {
|
||||
double vfps = decoder_->fps();
|
||||
if (vfps <= 0.0) vfps = 25.0;
|
||||
// Dense decode rate: capped at native fps. A lower scene_decode_fps
|
||||
// decodes fewer frames (big speedup); TransNetV2 still localises cuts
|
||||
// and boundary timestamps stay exact (keyed off real timestamps).
|
||||
dense_fps = (cfg.scene_decode_fps > 0.f)
|
||||
? std::min(static_cast<double>(cfg.scene_decode_fps), vfps)
|
||||
: vfps;
|
||||
dense_step_sec_ = 1.0 / dense_fps;
|
||||
if (cfg.dense_scale > 0.f && cfg.dense_scale < 1.f)
|
||||
bbox_upscale_ = 1.0f / cfg.dense_scale;
|
||||
}
|
||||
|
||||
double total_s = decoder_->duration_sec();
|
||||
double span_s = (end_sec_ > 0 ? std::min(end_sec_, total_s) : total_s)
|
||||
- cfg.start_sec;
|
||||
int n_frames = static_cast<int>(span_s * cfg.sample_fps);
|
||||
double emit_fps = dense_ ? dense_fps : cfg.sample_fps;
|
||||
int n_frames = static_cast<int>(span_s * emit_fps);
|
||||
std::cerr << "[frame_source] decoder=" << decoder_->codec_name()
|
||||
<< " (" << decoder_->hw_backend() << ")"
|
||||
<< " video_fps=" << decoder_->fps()
|
||||
<< (dense_ ? " DENSE@" + std::to_string(dense_fps) + "fps" : "")
|
||||
<< " start=" << cfg.start_sec << "s"
|
||||
<< (end_sec_ > 0 ? " end=" + std::to_string(end_sec_) + "s" : "")
|
||||
<< " sample_fps=" << cfg.sample_fps
|
||||
@@ -97,29 +121,14 @@ struct FrameSourceFunc {
|
||||
return Frame{{}, next_pos_sec_, frame_idx_++, /*eof=*/true};
|
||||
}
|
||||
|
||||
// Cut detection: compare grayscale histogram to previous frame
|
||||
bool is_cut = false;
|
||||
cv::Mat gray;
|
||||
cv::cvtColor(img, gray, cv::COLOR_BGR2GRAY);
|
||||
cv::Mat hist;
|
||||
const int bins = 64;
|
||||
const float range[] = {0.f, 256.f};
|
||||
const float* ranges = range;
|
||||
cv::calcHist(&gray, 1, nullptr, cv::Mat(), hist, 1, &bins, &ranges);
|
||||
cv::normalize(hist, hist, 1.0, 0.0, cv::NORM_L1);
|
||||
|
||||
if (prev_hist_valid_) {
|
||||
double corr = cv::compareHist(prev_hist_, hist, cv::HISTCMP_CORREL);
|
||||
is_cut = (corr < cut_threshold_);
|
||||
if (is_cut)
|
||||
std::cerr << "[frame_source] cut at t=" << next_pos_sec_
|
||||
<< "s hist_corr=" << corr << "\n";
|
||||
}
|
||||
prev_hist_ = hist;
|
||||
prev_hist_valid_ = true;
|
||||
|
||||
Frame f{img, next_pos_sec_, frame_idx_++, /*eof=*/false, is_cut};
|
||||
next_pos_sec_ += sample_interval_sec_;
|
||||
// Cut detection is a downstream concern: camera_position_change_detector
|
||||
// owns the histogram compare and sets Frame::is_cut. The source emits
|
||||
// is_cut=false and only decodes/samples frames.
|
||||
Frame f{img, next_pos_sec_, frame_idx_++, /*eof=*/false, /*is_cut=*/false};
|
||||
// When dense_scale downscaled the decode, tell downstream how to map
|
||||
// detector coordinates back to original video resolution.
|
||||
f.bbox_upscale = bbox_upscale_;
|
||||
next_pos_sec_ += dense_ ? dense_step_sec_ : sample_interval_sec_;
|
||||
if (end_sec_ > 0 && next_pos_sec_ > end_sec_) {
|
||||
hit_eof_ = true;
|
||||
std::cerr << "[frame_source] reached end_sec=" << end_sec_ << "s\n";
|
||||
@@ -130,16 +139,16 @@ struct FrameSourceFunc {
|
||||
private:
|
||||
std::unique_ptr<FFmpegDecoder> decoder_;
|
||||
double sample_interval_sec_{1.0};
|
||||
bool dense_{false};
|
||||
float bbox_upscale_{1.f};
|
||||
double dense_step_sec_{0.04};
|
||||
double next_pos_sec_{0.0};
|
||||
double end_sec_{-1.0};
|
||||
float cut_threshold_{0.70f};
|
||||
float max_decode_fps_{0.f};
|
||||
std::chrono::steady_clock::time_point next_decode_at_{};
|
||||
bool rate_started_{false};
|
||||
int64_t frame_idx_{0};
|
||||
bool hit_eof_{false};
|
||||
cv::Mat prev_hist_;
|
||||
bool prev_hist_valid_{false};
|
||||
double decode_ms_acc_{0.0};
|
||||
int decode_count_{0};
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "inference/similarity.hpp"
|
||||
#include "gallery/gallery_store.hpp"
|
||||
#include "gallery/gallery_calibration.hpp"
|
||||
#include "gallery/track_gallery.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
@@ -51,6 +52,7 @@ struct IdentityMatcherFunc {
|
||||
, threshold_(cfg.match_threshold)
|
||||
, ratio_(cfg.match_ratio)
|
||||
, ratio_ceil_(cfg.match_ratio_ceil)
|
||||
, track_gallery_(cfg)
|
||||
{
|
||||
std::cerr << "[identity_matcher] flattening gallery embeddings...\n";
|
||||
for (int ai = 0; ai < static_cast<int>(gallery_.actors.size()); ++ai) {
|
||||
@@ -63,8 +65,24 @@ struct IdentityMatcherFunc {
|
||||
|
||||
std::cerr << "[identity_matcher] starting calibration ("
|
||||
<< flat_emb_.size() << " embeddings)...\n";
|
||||
bool recomputed = false;
|
||||
cal_ = calibrate_gallery_cached(flat_emb_, flat_actor_,
|
||||
cfg.gallery_path + ".calib_cache.json");
|
||||
gallery_.calib_a, gallery_.calib_b,
|
||||
gallery_.calib_valid, gallery_.calib_hash,
|
||||
cfg.gallery_path + ".calib_cache", recomputed);
|
||||
if (recomputed) {
|
||||
// Persist the freshly-fitted calibration into the gallery file (always
|
||||
// HDF5 — save_gallery rewrites any other extension, see gallery_store.cpp)
|
||||
// so the next run against this same, unchanged gallery skips the O(n^2) fit.
|
||||
ActorGallery to_save = gallery_;
|
||||
to_save.calib_a = cal_.a;
|
||||
to_save.calib_b = cal_.b;
|
||||
to_save.calib_valid = cal_.valid;
|
||||
to_save.calib_hash = hash_gallery_embeddings(flat_emb_, flat_actor_);
|
||||
save_gallery(cfg.gallery_path, to_save);
|
||||
std::cerr << "[identity_matcher] wrote refreshed calibration back to "
|
||||
<< cfg.gallery_path << "\n";
|
||||
}
|
||||
|
||||
if (cal_.valid) {
|
||||
std::cerr << "[identity_matcher] calibrated Bayesian matching"
|
||||
@@ -89,8 +107,23 @@ struct IdentityMatcherFunc {
|
||||
sim_engine_ = make_similarity_engine(host_gallery.data(), n_gallery_, kMaxFaces);
|
||||
}
|
||||
|
||||
// Runtime setter — lets a persistent pipeline be reused across a threshold sweep
|
||||
// without rebuilding the (expensive, gallery-resident) matcher. The gallery,
|
||||
// calibration and GPU sim-engine stay put; only the accept threshold changes.
|
||||
void set_prob_threshold(float t) { prob_threshold_ = t; }
|
||||
|
||||
MatchedSceneFrame operator()(TrackedSceneFrame tf) {
|
||||
if (tf.source.eof) return {std::move(tf.source), {}};
|
||||
if (tf.source.eof) {
|
||||
track_gallery_.clear_tracks();
|
||||
return {std::move(tf.source), {}};
|
||||
}
|
||||
|
||||
// A hard cut changes the camera viewpoint. The face_tracker may revive a
|
||||
// track_id across the cut (identity continuity), but promotion must never
|
||||
// mix embeddings from two viewpoints under one buffer, so we still drop
|
||||
// every diversity buffer here — a revived track simply re-accumulates its
|
||||
// buffer from post-cut frames. Stale cross-cut embeddings are never promoted.
|
||||
if (tf.source.is_cut) track_gallery_.clear_tracks();
|
||||
|
||||
const int n_faces = static_cast<int>(tf.embeddings.size());
|
||||
std::vector<IdentifiedActor> actors;
|
||||
@@ -120,6 +153,14 @@ struct IdentityMatcherFunc {
|
||||
if (sim > best_sim[ai]) best_sim[ai] = sim;
|
||||
}
|
||||
|
||||
// Fold in the per-film annex (CPU-side, tens of embeddings). Promoted
|
||||
// pose-varied views compete for best-of-N exactly like baked refs, so
|
||||
// a face at a pose the gallery lacked can now win its true actor.
|
||||
for (const auto& ae : track_gallery_.annex()) {
|
||||
float sim = cosine_similarity(tf.embeddings[fi], ae.emb);
|
||||
if (sim > best_sim[ae.actor_idx]) best_sim[ae.actor_idx] = sim;
|
||||
}
|
||||
|
||||
int best_actor = -1;
|
||||
int second_actor = -1;
|
||||
float best_s = -std::numeric_limits<float>::max();
|
||||
@@ -155,7 +196,15 @@ struct IdentityMatcherFunc {
|
||||
}
|
||||
|
||||
IdentifiedActor ia;
|
||||
// Map bbox back to original video resolution when dense_scale
|
||||
// downscaled the decoded frame (detection/tracking ran downscaled;
|
||||
// output bboxes must be in original pixel space).
|
||||
ia.bbox = tf.faces[fi].bbox;
|
||||
if (tf.source.bbox_upscale != 1.f) {
|
||||
const float s = tf.source.bbox_upscale;
|
||||
ia.bbox.x *= s; ia.bbox.y *= s;
|
||||
ia.bbox.width *= s; ia.bbox.height *= s;
|
||||
}
|
||||
ia.crop = tf.crops[fi];
|
||||
ia.track_id = tf.track_ids[fi];
|
||||
|
||||
@@ -170,6 +219,14 @@ struct IdentityMatcherFunc {
|
||||
: best_s;
|
||||
}
|
||||
|
||||
// Feed this face into per-film gallery expansion. best_actor/best_s
|
||||
// reflect the actor with the strongest gallery similarity for this
|
||||
// face (annex already folded in above); the track's diversity buffer
|
||||
// keeps the gallery-far views and promotes them once the track is
|
||||
// confirmed. No-op unless --expand-gallery is set.
|
||||
track_gallery_.observe(tf.track_ids[fi], tf.embeddings[fi],
|
||||
best_actor, best_s, accept, tf.crops[fi]);
|
||||
|
||||
actors.push_back(std::move(ia));
|
||||
}
|
||||
|
||||
@@ -189,4 +246,5 @@ private:
|
||||
int n_gallery_{0};
|
||||
|
||||
std::unique_ptr<ISimilarityEngine> sim_engine_;
|
||||
TrackGallery track_gallery_;
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <opencv2/highgui.hpp>
|
||||
#include <opencv2/imgproc.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
|
||||
// ── PreviewNode ───────────────────────────────────────────────────────────────
|
||||
@@ -35,6 +36,7 @@ public:
|
||||
cv::Mat display = mf.source.image.clone();
|
||||
draw_detections(display, mf.actors);
|
||||
draw_hud(display, mf.source.timestamp_sec, mf.actors);
|
||||
draw_cut_meter(display, mf.source.cut_score, mf.source.is_cut);
|
||||
|
||||
// Fit to display width while keeping aspect ratio
|
||||
if (display.cols > max_display_w_) {
|
||||
@@ -132,6 +134,45 @@ private:
|
||||
}
|
||||
}
|
||||
|
||||
// Running cut-score meter (top-right): a horizontal bar whose fill and colour
|
||||
// track the histogram cut score in [0,1] (0 = no change, ~1 = hard cut).
|
||||
// Flashes a red "CUT" tag on frames where the threshold tripped (is_cut).
|
||||
static void draw_cut_meter(cv::Mat& img, float score, bool is_cut) {
|
||||
const int bar_w = 220;
|
||||
const int bar_h = 14;
|
||||
const int pad = 10;
|
||||
const int x0 = img.cols - bar_w - pad;
|
||||
const int y0 = pad + 16; // below any top-left HUD row height
|
||||
|
||||
score = std::clamp(score, 0.f, 1.f);
|
||||
|
||||
// Label
|
||||
char lbl[32];
|
||||
std::snprintf(lbl, sizeof(lbl), "cut %.2f", score);
|
||||
cv::putText(img, lbl, cv::Point(x0, y0 - 4),
|
||||
cv::FONT_HERSHEY_SIMPLEX, 0.5,
|
||||
cv::Scalar(220, 220, 220), 1, cv::LINE_AA);
|
||||
|
||||
// Track (dark) + fill (green→red by score)
|
||||
cv::rectangle(img, cv::Rect(x0, y0, bar_w, bar_h),
|
||||
cv::Scalar(40, 40, 40), cv::FILLED);
|
||||
int fill_w = static_cast<int>(bar_w * score);
|
||||
// BGR: low score → green (0,210,60), high → red (0,0,255)
|
||||
cv::Scalar fill_col(60.0 * (1.f - score),
|
||||
210.0 * (1.f - score),
|
||||
60.0 * (1.f - score) + 255.0 * score);
|
||||
if (fill_w > 0)
|
||||
cv::rectangle(img, cv::Rect(x0, y0, fill_w, bar_h), fill_col, cv::FILLED);
|
||||
cv::rectangle(img, cv::Rect(x0, y0, bar_w, bar_h),
|
||||
cv::Scalar(120, 120, 120), 1);
|
||||
|
||||
if (is_cut) {
|
||||
cv::putText(img, "CUT", cv::Point(x0 - 52, y0 + bar_h),
|
||||
cv::FONT_HERSHEY_DUPLEX, 0.6,
|
||||
cv::Scalar(0, 0, 255), 2, cv::LINE_AA);
|
||||
}
|
||||
}
|
||||
|
||||
static std::string pct(float v) {
|
||||
char buf[8];
|
||||
std::snprintf(buf, sizeof(buf), "%.0f%%", v * 100.f);
|
||||
|
||||
@@ -26,6 +26,10 @@ struct SceneTrackerFunc {
|
||||
std::cerr << "[scene_tracker] extinction_sec=" << extinction_sec_ << "\n";
|
||||
}
|
||||
|
||||
// Runtime setter for pipeline reuse across a sweep. Also clears the active-actor
|
||||
// state so a re-run starts clean (no carry-over from the previous config's film).
|
||||
void set_extinction_sec(double s) { extinction_sec_ = s; active_.clear(); }
|
||||
|
||||
SceneAnnotation operator()(MatchedSceneFrame mf) {
|
||||
if (mf.source.eof) return {0.0, {}, /*eof=*/true};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user