1use crate::kernels::features::{match_features, Feature};
8use crate::kernels::geometry::{js_hypot, Rect};
9use crate::kernels::motion::{
10 refine_patches, translation_hypotheses, Gray, MatchPoints, Patch, Point, TranslationAudit,
11};
12
13pub struct CandidateKeyframe<'a> {
20 pub features: &'a [Feature],
21 pub gray: Gray<'a>,
22 pub patches: &'a [Patch<'a>],
23 pub x: f64,
24 pub y: f64,
25 pub canonical_idx: u32,
26 pub dx: f64,
27 pub dy: f64,
28}
29
30#[derive(Clone, Copy)]
33pub struct AuditedCandidate {
34 pub keyframe_index: usize,
35 pub guess_x: f64,
36 pub guess_y: f64,
37 pub support: u32,
38 pub unique: u32,
39 pub hyp_confidence: f64,
40 pub ambiguous: bool,
41 pub analysis_error: f64,
42}
43
44pub fn evaluate_candidates_audit(
49 keyframes: &[CandidateKeyframe<'_>],
50 features: &[Feature],
51 gray: Gray<'_>,
52 roi: Rect,
53 factor: f64,
54) -> Vec<AuditedCandidate> {
55 let mut out = Vec::new();
56 for (ki, k) in keyframes.iter().enumerate() {
57 let raw_matches = match_features(k.features, features, true);
58 let match_points: Vec<MatchPoints> = raw_matches
59 .iter()
60 .map(|m| {
61 let a = &k.features[m.a as usize];
62 let b = &features[m.b as usize];
63 MatchPoints {
64 ax: a.x as f64,
65 ay: a.y as f64,
66 bx: b.x as f64,
67 by: b.y as f64,
68 unique: m.unique,
69 }
70 })
71 .collect();
72 let models = translation_hypotheses(&match_points, 16);
73 let mut prepared = None;
76 for m in models.iter().take(8) {
77 if m.support < 6 {
78 continue;
79 }
80 let pair =
82 prepared.get_or_insert_with(|| TranslationAudit::new(k.gray, gray, factor > 1.0));
83 let audit = pair.measure(m.x, m.y, Some(roi));
84 if audit.overlap < 0.22
85 || !audit.error.is_finite()
86 || (audit.mismatch > 0.12 && audit.agreement < 0.5)
87 {
88 continue;
89 }
90 out.push(AuditedCandidate {
91 keyframe_index: ki,
92 guess_x: m.x * factor,
93 guess_y: m.y * factor,
94 support: m.support,
95 unique: m.unique,
96 hyp_confidence: m.confidence,
97 ambiguous: m.ambiguous,
98 analysis_error: audit.error,
99 });
100 }
101 }
102 out
103}
104
105#[derive(Clone, Copy)]
108pub struct RefinedCandidate {
109 pub keyframe_index: usize,
110 pub x: i32,
111 pub y: i32,
112 pub support: u32,
113 pub unique: u32,
114 pub ambiguous: bool,
115 pub error: f64,
116 pub analysis_error: f64,
117 pub strong: bool,
118}
119
120pub fn evaluate_candidates_refine(
124 audited: &[AuditedCandidate],
125 keyframes: &[CandidateKeyframe<'_>],
126 native: Gray<'_>,
127 region: Rect,
128 radius: i32,
129) -> Vec<RefinedCandidate> {
130 let mut out = Vec::new();
131 for c in audited {
132 let refined = refine_patches(
133 keyframes[c.keyframe_index].patches,
134 native,
135 region,
136 Point {
137 x: c.guess_x,
138 y: c.guess_y,
139 },
140 radius,
141 );
142 if !refined.error.is_finite() || refined.error > 12.0 {
143 continue;
144 }
145 out.push(RefinedCandidate {
146 keyframe_index: c.keyframe_index,
147 x: refined.x,
148 y: refined.y,
149 support: c.support,
150 unique: c.unique,
151 ambiguous: c.ambiguous,
152 error: refined.error,
153 analysis_error: c.analysis_error,
154 strong: c.support >= 10 && c.unique >= 6 && c.hyp_confidence >= 0.45,
155 });
156 }
157 out
158}
159
160pub struct SelectedCandidate {
164 pub keyframe_index: usize,
165 pub x: i32,
166 pub y: i32,
167 pub support: u32,
168 pub unique: u32,
169 pub ambiguous: bool,
170 pub error: f64,
171 pub analysis_error: f64,
172 pub confidence: f64,
176}
177
178fn candidate_confidence(unique: u32, error: f64) -> f64 {
179 (0.45 + 0.5 * (1.0 - libm::exp(-(unique as f64) / 7.0))).min(0.95) * libm::exp(-error / 20.0)
180}
181
182fn candidate_score(support: u32, unique: u32, confidence: f64) -> f64 {
183 (support as f64 * 0.25 + unique as f64) * confidence
184}
185
186fn candidate_position(
190 r: &RefinedCandidate,
191 keyframes: &[CandidateKeyframe<'_>],
192) -> (u32, f64, f64) {
193 let k = &keyframes[r.keyframe_index];
194 (
195 k.canonical_idx,
196 k.x + r.x as f64 + k.dx,
197 k.y + r.y as f64 + k.dy,
198 )
199}
200
201pub fn select_candidate(
202 refined: &[RefinedCandidate],
203 keyframes: &[CandidateKeyframe<'_>],
204) -> Option<SelectedCandidate> {
205 struct Scored {
206 index: usize,
207 score: f64,
208 confidence: f64,
209 }
210 let mut scored: Vec<Scored> = refined
221 .iter()
222 .enumerate()
223 .filter_map(|(index, r)| {
224 let confidence = candidate_confidence(r.unique, r.error);
225 let score = candidate_score(r.support, r.unique, confidence);
226 score.is_finite().then_some(Scored {
227 index,
228 score,
229 confidence,
230 })
231 })
232 .collect();
233 scored.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap());
236 let best_at = scored.iter().position(|s| refined[s.index].strong)?;
237 let best = &scored[best_at];
238 let (best_canvas, best_x, best_y) = candidate_position(&refined[best.index], keyframes);
239 let rival = scored.iter().enumerate().any(|(i, s)| {
244 i != best_at && {
245 let (canvas, x, y) = candidate_position(&refined[s.index], keyframes);
246 (canvas != best_canvas || js_hypot(x - best_x, y - best_y) > 6.0)
247 && s.score > best.score * 0.8
248 }
249 });
250 let r = &refined[best.index];
251 Some(SelectedCandidate {
252 keyframe_index: r.keyframe_index,
253 x: r.x,
254 y: r.y,
255 support: r.support,
256 unique: r.unique,
257 ambiguous: r.ambiguous || rival,
258 error: r.error,
259 analysis_error: r.analysis_error,
260 confidence: best.confidence,
261 })
262}
263
264#[cfg(test)]
265mod select_candidate_tests {
266 use super::*;
267
268 fn kf(x: f64, y: f64, canonical_idx: u32) -> CandidateKeyframe<'static> {
271 CandidateKeyframe {
272 features: &[],
273 gray: Gray {
274 width: 0,
275 height: 0,
276 data: &[],
277 },
278 patches: &[],
279 x,
280 y,
281 canonical_idx,
282 dx: 0.0,
283 dy: 0.0,
284 }
285 }
286 fn rc(
287 keyframe_index: usize,
288 support: u32,
289 unique: u32,
290 error: f64,
291 strong: bool,
292 ) -> RefinedCandidate {
293 RefinedCandidate {
294 keyframe_index,
295 x: 0,
296 y: 0,
297 support,
298 unique,
299 ambiguous: false,
300 error,
301 analysis_error: 0.0,
302 strong,
303 }
304 }
305
306 #[test]
307 fn the_only_strong_candidate_is_chosen_and_its_confidence_is_in_range() {
308 let keyframes = [kf(0.0, 0.0, 0)];
309 let refined = [rc(0, 10, 6, 2.0, true)];
310 let chosen = select_candidate(&refined, &keyframes).unwrap();
311 assert_eq!(chosen.keyframe_index, 0);
312 assert!(!chosen.ambiguous);
313 assert!(chosen.confidence > 0.0 && chosen.confidence <= 0.95);
314 }
315
316 #[test]
317 fn a_higher_scoring_but_non_strong_candidate_is_skipped_for_the_first_strong_one() {
318 let keyframes = [kf(0.0, 0.0, 0), kf(0.0, 0.0, 0)];
322 let refined = [rc(0, 50, 50, 0.0, false), rc(1, 10, 6, 2.0, true)];
323 let chosen = select_candidate(&refined, &keyframes).unwrap();
324 assert_eq!(chosen.keyframe_index, 1);
325 }
326
327 #[test]
328 fn no_strong_candidate_returns_none() {
329 let keyframes = [kf(0.0, 0.0, 0)];
330 let refined = [rc(0, 3, 2, 1.0, false)];
331 assert!(select_candidate(&refined, &keyframes).is_none());
332 }
333
334 #[test]
335 fn a_rival_over_the_08_score_boundary_on_a_different_canonical_canvas_marks_ambiguous() {
336 let keyframes = [kf(0.0, 0.0, 0), kf(1000.0, 1000.0, 1)];
339 let refined = [rc(0, 10, 0, 0.0, true), rc(1, 9, 0, 0.0, false)];
340 let chosen = select_candidate(&refined, &keyframes).unwrap();
341 assert!(chosen.ambiguous, "9/10 = .9 is over the .8 rival boundary");
342 }
343
344 #[test]
345 fn a_rival_under_the_08_score_boundary_does_not_mark_ambiguous() {
346 let keyframes = [kf(0.0, 0.0, 0), kf(1000.0, 1000.0, 1)];
349 let refined = [rc(0, 10, 0, 0.0, true), rc(1, 7, 0, 0.0, false)];
350 let chosen = select_candidate(&refined, &keyframes).unwrap();
351 assert!(
352 !chosen.ambiguous,
353 "7/10 = .7 is under the .8 rival boundary"
354 );
355 }
356
357 #[test]
358 fn a_same_canvas_close_candidate_is_not_a_rival_even_with_a_high_score() {
359 let keyframes = [kf(0.0, 0.0, 0), kf(3.0, 0.0, 0)];
362 let refined = [rc(0, 10, 0, 0.0, true), rc(1, 9, 0, 0.0, false)];
363 let chosen = select_candidate(&refined, &keyframes).unwrap();
364 assert!(
365 !chosen.ambiguous,
366 "same canvas, 3px apart: agreement, not a rival"
367 );
368 }
369
370 #[test]
371 fn a_different_canonical_canvas_at_the_same_point_is_still_a_rival() {
372 let keyframes = [kf(0.0, 0.0, 0), kf(0.0, 0.0, 1)];
375 let refined = [rc(0, 10, 0, 0.0, true), rc(1, 9, 0, 0.0, false)];
376 let chosen = select_candidate(&refined, &keyframes).unwrap();
377 assert!(
378 chosen.ambiguous,
379 "same point, different canonical canvas: still a rival"
380 );
381 }
382
383 #[test]
384 fn non_finite_scores_are_excluded_without_panicking_in_a_large_candidate_set() {
385 let mut keyframes = Vec::new();
392 let mut refined = Vec::new();
393 for i in 0..24 {
394 keyframes.push(kf(0.0, 0.0, 0));
395 let error = if i % 2 == 0 {
396 f64::NAN
397 } else {
398 f64::NEG_INFINITY
399 };
400 refined.push(rc(i, 10, 6, error, true));
401 }
402 keyframes.push(kf(0.0, 0.0, 0));
403 refined.push(rc(24, 10, 6, 2.0, true));
404 let chosen = select_candidate(&refined, &keyframes).unwrap();
405 assert_eq!(chosen.keyframe_index, 24);
406 assert!(chosen.confidence.is_finite());
407 }
408
409 #[test]
410 fn an_exact_score_tie_keeps_the_first_candidate_in_input_order() {
411 let keyframes = [kf(0.0, 0.0, 0), kf(0.0, 0.0, 0)];
414 let refined = [rc(0, 10, 6, 2.0, true), rc(1, 10, 6, 2.0, true)];
415 let chosen = select_candidate(&refined, &keyframes).unwrap();
416 assert_eq!(
417 chosen.keyframe_index, 0,
418 "a tied score keeps the first candidate in input order"
419 );
420 }
421}