1use crate::kernels::features::{match_features, Feature};
8use crate::kernels::geometry::{js_hypot, Rect};
9use crate::kernels::motion::{
10 audit_translation, refine_patches, translation_hypotheses, Gray, MatchPoints, Patch, Point,
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 for m in models.iter().take(8) {
74 if m.support < 6 {
75 continue;
76 }
77 let audit = audit_translation(k.gray, gray, m.x, m.y, Some(roi), factor > 1.0);
79 if audit.overlap < 0.22
80 || !audit.error.is_finite()
81 || (audit.mismatch > 0.12 && audit.agreement < 0.5)
82 {
83 continue;
84 }
85 out.push(AuditedCandidate {
86 keyframe_index: ki,
87 guess_x: m.x * factor,
88 guess_y: m.y * factor,
89 support: m.support,
90 unique: m.unique,
91 hyp_confidence: m.confidence,
92 ambiguous: m.ambiguous,
93 analysis_error: audit.error,
94 });
95 }
96 }
97 out
98}
99
100#[derive(Clone, Copy)]
103pub struct RefinedCandidate {
104 pub keyframe_index: usize,
105 pub x: i32,
106 pub y: i32,
107 pub support: u32,
108 pub unique: u32,
109 pub ambiguous: bool,
110 pub error: f64,
111 pub analysis_error: f64,
112 pub strong: bool,
113}
114
115pub fn evaluate_candidates_refine(
119 audited: &[AuditedCandidate],
120 keyframes: &[CandidateKeyframe<'_>],
121 native: Gray<'_>,
122 region: Rect,
123 radius: i32,
124) -> Vec<RefinedCandidate> {
125 let mut out = Vec::new();
126 for c in audited {
127 let refined = refine_patches(
128 keyframes[c.keyframe_index].patches,
129 native,
130 region,
131 Point {
132 x: c.guess_x,
133 y: c.guess_y,
134 },
135 radius,
136 );
137 if !refined.error.is_finite() || refined.error > 12.0 {
138 continue;
139 }
140 out.push(RefinedCandidate {
141 keyframe_index: c.keyframe_index,
142 x: refined.x,
143 y: refined.y,
144 support: c.support,
145 unique: c.unique,
146 ambiguous: c.ambiguous,
147 error: refined.error,
148 analysis_error: c.analysis_error,
149 strong: c.support >= 10 && c.unique >= 6 && c.hyp_confidence >= 0.45,
150 });
151 }
152 out
153}
154
155pub struct SelectedCandidate {
159 pub keyframe_index: usize,
160 pub x: i32,
161 pub y: i32,
162 pub support: u32,
163 pub unique: u32,
164 pub ambiguous: bool,
165 pub error: f64,
166 pub analysis_error: f64,
167 pub confidence: f64,
171}
172
173fn candidate_confidence(unique: u32, error: f64) -> f64 {
174 (0.45 + 0.5 * (1.0 - libm::exp(-(unique as f64) / 7.0))).min(0.95) * libm::exp(-error / 20.0)
175}
176
177fn candidate_score(support: u32, unique: u32, confidence: f64) -> f64 {
178 (support as f64 * 0.25 + unique as f64) * confidence
179}
180
181fn candidate_position(
185 r: &RefinedCandidate,
186 keyframes: &[CandidateKeyframe<'_>],
187) -> (u32, f64, f64) {
188 let k = &keyframes[r.keyframe_index];
189 (
190 k.canonical_idx,
191 k.x + r.x as f64 + k.dx,
192 k.y + r.y as f64 + k.dy,
193 )
194}
195
196pub fn select_candidate(
197 refined: &[RefinedCandidate],
198 keyframes: &[CandidateKeyframe<'_>],
199) -> Option<SelectedCandidate> {
200 struct Scored {
201 index: usize,
202 score: f64,
203 confidence: f64,
204 }
205 let mut scored: Vec<Scored> = refined
216 .iter()
217 .enumerate()
218 .filter_map(|(index, r)| {
219 let confidence = candidate_confidence(r.unique, r.error);
220 let score = candidate_score(r.support, r.unique, confidence);
221 score.is_finite().then_some(Scored {
222 index,
223 score,
224 confidence,
225 })
226 })
227 .collect();
228 scored.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap());
231 let best_at = scored.iter().position(|s| refined[s.index].strong)?;
232 let best = &scored[best_at];
233 let (best_canvas, best_x, best_y) = candidate_position(&refined[best.index], keyframes);
234 let rival = scored.iter().enumerate().any(|(i, s)| {
239 i != best_at && {
240 let (canvas, x, y) = candidate_position(&refined[s.index], keyframes);
241 (canvas != best_canvas || js_hypot(x - best_x, y - best_y) > 6.0)
242 && s.score > best.score * 0.8
243 }
244 });
245 let r = &refined[best.index];
246 Some(SelectedCandidate {
247 keyframe_index: r.keyframe_index,
248 x: r.x,
249 y: r.y,
250 support: r.support,
251 unique: r.unique,
252 ambiguous: r.ambiguous || rival,
253 error: r.error,
254 analysis_error: r.analysis_error,
255 confidence: best.confidence,
256 })
257}
258
259#[cfg(test)]
260mod select_candidate_tests {
261 use super::*;
262
263 fn kf(x: f64, y: f64, canonical_idx: u32) -> CandidateKeyframe<'static> {
266 CandidateKeyframe {
267 features: &[],
268 gray: Gray {
269 width: 0,
270 height: 0,
271 data: &[],
272 },
273 patches: &[],
274 x,
275 y,
276 canonical_idx,
277 dx: 0.0,
278 dy: 0.0,
279 }
280 }
281 fn rc(
282 keyframe_index: usize,
283 support: u32,
284 unique: u32,
285 error: f64,
286 strong: bool,
287 ) -> RefinedCandidate {
288 RefinedCandidate {
289 keyframe_index,
290 x: 0,
291 y: 0,
292 support,
293 unique,
294 ambiguous: false,
295 error,
296 analysis_error: 0.0,
297 strong,
298 }
299 }
300
301 #[test]
302 fn the_only_strong_candidate_is_chosen_and_its_confidence_is_in_range() {
303 let keyframes = [kf(0.0, 0.0, 0)];
304 let refined = [rc(0, 10, 6, 2.0, true)];
305 let chosen = select_candidate(&refined, &keyframes).unwrap();
306 assert_eq!(chosen.keyframe_index, 0);
307 assert!(!chosen.ambiguous);
308 assert!(chosen.confidence > 0.0 && chosen.confidence <= 0.95);
309 }
310
311 #[test]
312 fn a_higher_scoring_but_non_strong_candidate_is_skipped_for_the_first_strong_one() {
313 let keyframes = [kf(0.0, 0.0, 0), kf(0.0, 0.0, 0)];
317 let refined = [rc(0, 50, 50, 0.0, false), rc(1, 10, 6, 2.0, true)];
318 let chosen = select_candidate(&refined, &keyframes).unwrap();
319 assert_eq!(chosen.keyframe_index, 1);
320 }
321
322 #[test]
323 fn no_strong_candidate_returns_none() {
324 let keyframes = [kf(0.0, 0.0, 0)];
325 let refined = [rc(0, 3, 2, 1.0, false)];
326 assert!(select_candidate(&refined, &keyframes).is_none());
327 }
328
329 #[test]
330 fn a_rival_over_the_08_score_boundary_on_a_different_canonical_canvas_marks_ambiguous() {
331 let keyframes = [kf(0.0, 0.0, 0), kf(1000.0, 1000.0, 1)];
334 let refined = [rc(0, 10, 0, 0.0, true), rc(1, 9, 0, 0.0, false)];
335 let chosen = select_candidate(&refined, &keyframes).unwrap();
336 assert!(chosen.ambiguous, "9/10 = .9 is over the .8 rival boundary");
337 }
338
339 #[test]
340 fn a_rival_under_the_08_score_boundary_does_not_mark_ambiguous() {
341 let keyframes = [kf(0.0, 0.0, 0), kf(1000.0, 1000.0, 1)];
344 let refined = [rc(0, 10, 0, 0.0, true), rc(1, 7, 0, 0.0, false)];
345 let chosen = select_candidate(&refined, &keyframes).unwrap();
346 assert!(
347 !chosen.ambiguous,
348 "7/10 = .7 is under the .8 rival boundary"
349 );
350 }
351
352 #[test]
353 fn a_same_canvas_close_candidate_is_not_a_rival_even_with_a_high_score() {
354 let keyframes = [kf(0.0, 0.0, 0), kf(3.0, 0.0, 0)];
357 let refined = [rc(0, 10, 0, 0.0, true), rc(1, 9, 0, 0.0, false)];
358 let chosen = select_candidate(&refined, &keyframes).unwrap();
359 assert!(
360 !chosen.ambiguous,
361 "same canvas, 3px apart: agreement, not a rival"
362 );
363 }
364
365 #[test]
366 fn a_different_canonical_canvas_at_the_same_point_is_still_a_rival() {
367 let keyframes = [kf(0.0, 0.0, 0), kf(0.0, 0.0, 1)];
370 let refined = [rc(0, 10, 0, 0.0, true), rc(1, 9, 0, 0.0, false)];
371 let chosen = select_candidate(&refined, &keyframes).unwrap();
372 assert!(
373 chosen.ambiguous,
374 "same point, different canonical canvas: still a rival"
375 );
376 }
377
378 #[test]
379 fn non_finite_scores_are_excluded_without_panicking_in_a_large_candidate_set() {
380 let mut keyframes = Vec::new();
387 let mut refined = Vec::new();
388 for i in 0..24 {
389 keyframes.push(kf(0.0, 0.0, 0));
390 let error = if i % 2 == 0 {
391 f64::NAN
392 } else {
393 f64::NEG_INFINITY
394 };
395 refined.push(rc(i, 10, 6, error, true));
396 }
397 keyframes.push(kf(0.0, 0.0, 0));
398 refined.push(rc(24, 10, 6, 2.0, true));
399 let chosen = select_candidate(&refined, &keyframes).unwrap();
400 assert_eq!(chosen.keyframe_index, 24);
401 assert!(chosen.confidence.is_finite());
402 }
403
404 #[test]
405 fn an_exact_score_tie_keeps_the_first_candidate_in_input_order() {
406 let keyframes = [kf(0.0, 0.0, 0), kf(0.0, 0.0, 0)];
409 let refined = [rc(0, 10, 6, 2.0, true), rc(1, 10, 6, 2.0, true)];
410 let chosen = select_candidate(&refined, &keyframes).unwrap();
411 assert_eq!(
412 chosen.keyframe_index, 0,
413 "a tied score keeps the first candidate in input order"
414 );
415 }
416}