#pragma once #include "types.hpp" #include "config.hpp" #include #include #include #include // ── CameraPositionChangeDetectorFunc ────────────────────────────────────────── // KPN node: flags intra-scene camera-angle changes (hard cuts) by comparing each // frame's grayscale histogram to the previous frame's. When the normalised // histogram correlation drops below cut_threshold, the frame is marked with // Frame::is_cut = true. // // This is the pipeline's cut *trigger*: it owns cut detection so a single node // decides when the camera position has changed, and every downstream stage reads // the decision off Frame::is_cut (which rides with the frame's timestamp_sec / // frame_idx). The face_tracker consumes it to re-associate tracks across the cut // rather than blindly resetting; the track_gallery consumes it to bound // promotion to a single physical viewpoint. // // This is the always-on histogram cut — separate from the opt-in TransNetV2 // scene detector, which localises true shot boundaries as Frame::is_scene_boundary. // // The node is a pure pass-through: it forwards the Frame unchanged except for // is_cut, so it slots between frame_source and face_detector without altering the // downstream contract. eof frames are forwarded immediately without processing. // /// TRACES: AR-009 | SR-002 struct CameraPositionChangeDetectorFunc { static constexpr std::string_view label() { return "camera_position_change_detector"; } explicit CameraPositionChangeDetectorFunc(const Config& cfg) : cut_threshold_(cfg.cut_threshold) , want_rgb_hist_(!cfg.scene_xgb_model.empty()) { std::cerr << "[camera_position_change_detector] cut_threshold=" << cut_threshold_ << (want_rgb_hist_ ? " (+rgb_hist for scene detector)" : "") << "\n"; } // 32-bin-per-channel normalised RGB histogram (96 floats), the exact layout // the XGBoost scene detector was trained on (see embedding_dump_node). Only // computed when a scene model is configured, so it costs nothing otherwise. static std::vector rgb_histogram(const cv::Mat& img) { constexpr int kBins = 32; std::vector out(kBins * 3, 0.f); if (img.empty() || img.channels() != 3) return out; float range[] = {0.f, 256.f}; const float* ranges = range; int bins = kBins; for (int c = 0; c < 3; ++c) { // OpenCV BGR → store B,G,R blocks cv::Mat h; cv::calcHist(&img, 1, &c, cv::Mat(), h, 1, &bins, &ranges); cv::normalize(h, h, 1.0, 0.0, cv::NORM_L1); for (int b = 0; b < kBins; ++b) out[c*kBins + b] = h.at(b); } return out; } Frame operator()(Frame f) { if (f.eof) return f; cv::Mat gray; cv::cvtColor(f.image, 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); // Cut score in [0,1]: 0 = identical to previous frame, ~1 = fully // different. Rides on the Frame for the preview HUD / debugging. f.cut_score = static_cast(std::clamp(1.0 - corr, 0.0, 1.0)); f.is_cut = (corr < cut_threshold_); if (f.is_cut) std::cerr << "[camera_position_change_detector] cut at t=" << f.timestamp_sec << "s hist_corr=" << corr << "\n"; } prev_hist_ = hist; prev_hist_valid_ = true; if (want_rgb_hist_) f.rgb_hist = rgb_histogram(f.image); return f; } private: float cut_threshold_; bool want_rgb_hist_{false}; cv::Mat prev_hist_; bool prev_hist_valid_{false}; };