use crate::kernels::features::{match_features, Feature};
use crate::kernels::geometry::{js_hypot, Rect};
use crate::kernels::motion::{
audit_translation, refine_patches, translation_hypotheses, Gray, MatchPoints, Patch, Point,
};
pub struct CandidateKeyframe<'a> {
pub features: &'a [Feature],
pub gray: Gray<'a>,
pub patches: &'a [Patch<'a>],
pub x: f64,
pub y: f64,
pub canonical_idx: u32,
pub dx: f64,
pub dy: f64,
}
#[derive(Clone, Copy)]
pub struct AuditedCandidate {
pub keyframe_index: usize,
pub guess_x: f64,
pub guess_y: f64,
pub support: u32,
pub unique: u32,
pub hyp_confidence: f64,
pub ambiguous: bool,
pub analysis_error: f64,
}
pub fn evaluate_candidates_audit(
keyframes: &[CandidateKeyframe<'_>],
features: &[Feature],
gray: Gray<'_>,
roi: Rect,
factor: f64,
) -> Vec<AuditedCandidate> {
let mut out = Vec::new();
for (ki, k) in keyframes.iter().enumerate() {
let raw_matches = match_features(k.features, features, true);
let match_points: Vec<MatchPoints> = raw_matches
.iter()
.map(|m| {
let a = &k.features[m.a as usize];
let b = &features[m.b as usize];
MatchPoints {
ax: a.x as f64,
ay: a.y as f64,
bx: b.x as f64,
by: b.y as f64,
unique: m.unique,
}
})
.collect();
let models = translation_hypotheses(&match_points, 16);
for m in models.iter().take(8) {
if m.support < 6 {
continue;
}
let audit = audit_translation(k.gray, gray, m.x, m.y, Some(roi), factor > 1.0);
if audit.overlap < 0.22
|| !audit.error.is_finite()
|| (audit.mismatch > 0.12 && audit.agreement < 0.5)
{
continue;
}
out.push(AuditedCandidate {
keyframe_index: ki,
guess_x: m.x * factor,
guess_y: m.y * factor,
support: m.support,
unique: m.unique,
hyp_confidence: m.confidence,
ambiguous: m.ambiguous,
analysis_error: audit.error,
});
}
}
out
}
#[derive(Clone, Copy)]
pub struct RefinedCandidate {
pub keyframe_index: usize,
pub x: i32,
pub y: i32,
pub support: u32,
pub unique: u32,
pub ambiguous: bool,
pub error: f64,
pub analysis_error: f64,
pub strong: bool,
}
pub fn evaluate_candidates_refine(
audited: &[AuditedCandidate],
keyframes: &[CandidateKeyframe<'_>],
native: Gray<'_>,
region: Rect,
radius: i32,
) -> Vec<RefinedCandidate> {
let mut out = Vec::new();
for c in audited {
let refined = refine_patches(
keyframes[c.keyframe_index].patches,
native,
region,
Point {
x: c.guess_x,
y: c.guess_y,
},
radius,
);
if !refined.error.is_finite() || refined.error > 12.0 {
continue;
}
out.push(RefinedCandidate {
keyframe_index: c.keyframe_index,
x: refined.x,
y: refined.y,
support: c.support,
unique: c.unique,
ambiguous: c.ambiguous,
error: refined.error,
analysis_error: c.analysis_error,
strong: c.support >= 10 && c.unique >= 6 && c.hyp_confidence >= 0.45,
});
}
out
}
pub struct SelectedCandidate {
pub keyframe_index: usize,
pub x: i32,
pub y: i32,
pub support: u32,
pub unique: u32,
pub ambiguous: bool,
pub error: f64,
pub analysis_error: f64,
pub confidence: f64,
}
fn candidate_confidence(unique: u32, error: f64) -> f64 {
(0.45 + 0.5 * (1.0 - libm::exp(-(unique as f64) / 7.0))).min(0.95) * libm::exp(-error / 20.0)
}
fn candidate_score(support: u32, unique: u32, confidence: f64) -> f64 {
(support as f64 * 0.25 + unique as f64) * confidence
}
fn candidate_position(
r: &RefinedCandidate,
keyframes: &[CandidateKeyframe<'_>],
) -> (u32, f64, f64) {
let k = &keyframes[r.keyframe_index];
(
k.canonical_idx,
k.x + r.x as f64 + k.dx,
k.y + r.y as f64 + k.dy,
)
}
pub fn select_candidate(
refined: &[RefinedCandidate],
keyframes: &[CandidateKeyframe<'_>],
) -> Option<SelectedCandidate> {
struct Scored {
index: usize,
score: f64,
confidence: f64,
}
let mut scored: Vec<Scored> = refined
.iter()
.enumerate()
.filter_map(|(index, r)| {
let confidence = candidate_confidence(r.unique, r.error);
let score = candidate_score(r.support, r.unique, confidence);
score.is_finite().then_some(Scored {
index,
score,
confidence,
})
})
.collect();
scored.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap());
let best_at = scored.iter().position(|s| refined[s.index].strong)?;
let best = &scored[best_at];
let (best_canvas, best_x, best_y) = candidate_position(&refined[best.index], keyframes);
let rival = scored.iter().enumerate().any(|(i, s)| {
i != best_at && {
let (canvas, x, y) = candidate_position(&refined[s.index], keyframes);
(canvas != best_canvas || js_hypot(x - best_x, y - best_y) > 6.0)
&& s.score > best.score * 0.8
}
});
let r = &refined[best.index];
Some(SelectedCandidate {
keyframe_index: r.keyframe_index,
x: r.x,
y: r.y,
support: r.support,
unique: r.unique,
ambiguous: r.ambiguous || rival,
error: r.error,
analysis_error: r.analysis_error,
confidence: best.confidence,
})
}
#[cfg(test)]
mod select_candidate_tests {
use super::*;
fn kf(x: f64, y: f64, canonical_idx: u32) -> CandidateKeyframe<'static> {
CandidateKeyframe {
features: &[],
gray: Gray {
width: 0,
height: 0,
data: &[],
},
patches: &[],
x,
y,
canonical_idx,
dx: 0.0,
dy: 0.0,
}
}
fn rc(
keyframe_index: usize,
support: u32,
unique: u32,
error: f64,
strong: bool,
) -> RefinedCandidate {
RefinedCandidate {
keyframe_index,
x: 0,
y: 0,
support,
unique,
ambiguous: false,
error,
analysis_error: 0.0,
strong,
}
}
#[test]
fn the_only_strong_candidate_is_chosen_and_its_confidence_is_in_range() {
let keyframes = [kf(0.0, 0.0, 0)];
let refined = [rc(0, 10, 6, 2.0, true)];
let chosen = select_candidate(&refined, &keyframes).unwrap();
assert_eq!(chosen.keyframe_index, 0);
assert!(!chosen.ambiguous);
assert!(chosen.confidence > 0.0 && chosen.confidence <= 0.95);
}
#[test]
fn a_higher_scoring_but_non_strong_candidate_is_skipped_for_the_first_strong_one() {
let keyframes = [kf(0.0, 0.0, 0), kf(0.0, 0.0, 0)];
let refined = [rc(0, 50, 50, 0.0, false), rc(1, 10, 6, 2.0, true)];
let chosen = select_candidate(&refined, &keyframes).unwrap();
assert_eq!(chosen.keyframe_index, 1);
}
#[test]
fn no_strong_candidate_returns_none() {
let keyframes = [kf(0.0, 0.0, 0)];
let refined = [rc(0, 3, 2, 1.0, false)];
assert!(select_candidate(&refined, &keyframes).is_none());
}
#[test]
fn a_rival_over_the_08_score_boundary_on_a_different_canonical_canvas_marks_ambiguous() {
let keyframes = [kf(0.0, 0.0, 0), kf(1000.0, 1000.0, 1)];
let refined = [rc(0, 10, 0, 0.0, true), rc(1, 9, 0, 0.0, false)];
let chosen = select_candidate(&refined, &keyframes).unwrap();
assert!(chosen.ambiguous, "9/10 = .9 is over the .8 rival boundary");
}
#[test]
fn a_rival_under_the_08_score_boundary_does_not_mark_ambiguous() {
let keyframes = [kf(0.0, 0.0, 0), kf(1000.0, 1000.0, 1)];
let refined = [rc(0, 10, 0, 0.0, true), rc(1, 7, 0, 0.0, false)];
let chosen = select_candidate(&refined, &keyframes).unwrap();
assert!(
!chosen.ambiguous,
"7/10 = .7 is under the .8 rival boundary"
);
}
#[test]
fn a_same_canvas_close_candidate_is_not_a_rival_even_with_a_high_score() {
let keyframes = [kf(0.0, 0.0, 0), kf(3.0, 0.0, 0)];
let refined = [rc(0, 10, 0, 0.0, true), rc(1, 9, 0, 0.0, false)];
let chosen = select_candidate(&refined, &keyframes).unwrap();
assert!(
!chosen.ambiguous,
"same canvas, 3px apart: agreement, not a rival"
);
}
#[test]
fn a_different_canonical_canvas_at_the_same_point_is_still_a_rival() {
let keyframes = [kf(0.0, 0.0, 0), kf(0.0, 0.0, 1)];
let refined = [rc(0, 10, 0, 0.0, true), rc(1, 9, 0, 0.0, false)];
let chosen = select_candidate(&refined, &keyframes).unwrap();
assert!(
chosen.ambiguous,
"same point, different canonical canvas: still a rival"
);
}
#[test]
fn non_finite_scores_are_excluded_without_panicking_in_a_large_candidate_set() {
let mut keyframes = Vec::new();
let mut refined = Vec::new();
for i in 0..24 {
keyframes.push(kf(0.0, 0.0, 0));
let error = if i % 2 == 0 {
f64::NAN
} else {
f64::NEG_INFINITY
};
refined.push(rc(i, 10, 6, error, true));
}
keyframes.push(kf(0.0, 0.0, 0));
refined.push(rc(24, 10, 6, 2.0, true));
let chosen = select_candidate(&refined, &keyframes).unwrap();
assert_eq!(chosen.keyframe_index, 24);
assert!(chosen.confidence.is_finite());
}
#[test]
fn an_exact_score_tie_keeps_the_first_candidate_in_input_order() {
let keyframes = [kf(0.0, 0.0, 0), kf(0.0, 0.0, 0)];
let refined = [rc(0, 10, 6, 2.0, true), rc(1, 10, 6, 2.0, true)];
let chosen = select_candidate(&refined, &keyframes).unwrap();
assert_eq!(
chosen.keyframe_index, 0,
"a tied score keeps the first candidate in input order"
);
}
}