Skip to main content

optical_flow_lk/
lk.rs

1use image::{GrayImage, ImageBuffer, Luma, Pixel};
2
3use crate::features::reserve_total;
4use crate::float_image::FloatImageView;
5use crate::pyramid::{
6    build_float_pyramid_into, build_pyramid_into, level_dimensions, pyramid_level_count,
7    reserve_pyramid,
8};
9use crate::utils::fast_gradients::compute_gradients_into;
10use crate::utils::float_gradients::scharr_f32;
11
12mod fixed;
13
14/// One level of an f32 image pyramid, as built by [`FloatTrackerContext`].
15pub type FloatPyramidLevel = ImageBuffer<Luma<f32>, Vec<f32>>;
16
17/// Default [`TrackParams::min_eigen_threshold`] of the 8-bit tracker.
18///
19/// The minimum eigenvalue of the per-window spatial gradient matrix is
20/// normalized by the window area before comparison, so this threshold is
21/// independent of `window_size`. Its unit is (grey levels / pixel)^2. A window
22/// flatter than this is reported as [`TrackStatus::LowTexture`]. The f32
23/// tracker has no default: its threshold is in the image's own unit.
24///
25/// Thresholds below about 1e-4 are not meaningful: at the strongest 8-bit
26/// edges, f32 rounding of the eigenvalue alone reaches about that size, so
27/// such edges could pass as trackable windows at random.
28pub const DEFAULT_MIN_EIGEN_THRESHOLD: f32 = 1e-3;
29
30/// Per-level convergence threshold, in pixels of the current pyramid level.
31///
32/// The iteration at a level stops once both components of the displacement
33/// update are below this value, or once two consecutive updates cancel each
34/// other (the estimate oscillates around the optimum, which is then taken as
35/// their midpoint). This matches the effective stopping precision of OpenCV's
36/// `calcOpticalFlowPyrLK`. A point that reaches `max_iterations` without
37/// meeting either condition is reported as [`TrackStatus::Diverged`].
38pub const CONVERGENCE_EPSILON: f32 = 0.01;
39
40/// Why a feature point ended up where it did after tracking.
41///
42/// See [`TrackResult`] for the coordinate convention.
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum TrackStatus {
45    /// The iteration converged inside the image; the position is trustworthy.
46    Tracked,
47    /// The search window left the image (in the previous or the next frame).
48    OutOfBounds,
49    /// The iteration hit `max_iterations` without converging (see
50    /// [`CONVERGENCE_EPSILON`]), or a step exploded (non-finite or larger than
51    /// the window). The reported position is the last estimate, not a lost
52    /// track: callers that prefer OpenCV's semantics may still use it.
53    Diverged,
54    /// The minimum eigenvalue of the spatial gradient matrix fell below the
55    /// configured threshold, i.e. the window is too flat to track reliably.
56    LowTexture,
57    /// The point was tracked forward, but re-tracking it back to the previous
58    /// frame did not return close enough to the original position. Only
59    /// produced by [`calc_optical_flow_fb`]. A strong occlusion/outlier signal.
60    FbInconsistent,
61}
62
63/// Default forward-backward round-trip threshold (pixels) for
64/// [`calc_optical_flow_fb`].
65pub const DEFAULT_FB_THRESHOLD: f32 = 0.7;
66
67/// Lucas-Kanade parameters of a tracking call.
68///
69/// The fields are named so that they cannot be swapped unnoticed. For 8-bit
70/// images, for example:
71///
72/// ```
73/// use optical_flow_lk::{DEFAULT_MIN_EIGEN_THRESHOLD, TrackParams};
74///
75/// let params = TrackParams {
76///     window_size: 21,
77///     max_iterations: 30,
78///     min_eigen_threshold: DEFAULT_MIN_EIGEN_THRESHOLD,
79/// };
80/// ```
81#[derive(Clone, Copy, Debug, PartialEq)]
82pub struct TrackParams {
83    /// Side of the square search window in pixels; must be odd.
84    pub window_size: usize,
85    /// Most iterations per pyramid level, see [`CONVERGENCE_EPSILON`].
86    pub max_iterations: usize,
87    /// Windows whose minimum gradient eigenvalue, normalized by the window
88    /// area, is below this value are reported as [`TrackStatus::LowTexture`].
89    /// In (grey levels / pixel)^2 for 8-bit images, see
90    /// [`DEFAULT_MIN_EIGEN_THRESHOLD`]; in (input unit / pixel)^2 for the f32
91    /// tracker, which has no default.
92    pub min_eigen_threshold: f32,
93}
94
95/// Arithmetic of the 8-bit tracker's window kernels, see
96/// [`TrackOptions::kernel`].
97///
98/// The fixed-point kernels follow the scheme of OpenCV's
99/// `calcOpticalFlowPyrLK`: window samples are bilinear interpolations with
100/// 14-bit integer weights, kept as 16-bit integers with 5 fractional bits for
101/// intensities and 6 for gradients, and every window sum is an exact integer
102/// accumulated from 16-bit products. The integers are the same on every
103/// instruction set, so results are bit for bit identical on x86 (SSE2,
104/// AVX2), wasm32 (`simd128`) and the scalar fallback. Against the f32
105/// reference, tracked positions typically differ by a few ten-thousandths of
106/// a pixel; see the changelog of 0.6.0 for measurements.
107///
108/// The f32 tracker ([`FloatTrackerContext`]) always uses its f32 kernels.
109#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
110#[non_exhaustive]
111pub enum TrackKernel {
112    /// Fixed point where the target has SIMD kernels for it: x86 and x86-64
113    /// with SSE2 (AVX2 when the CPU has it) and wasm32 built with `simd128`.
114    /// The f32 reference elsewhere, including aarch64, which has no
115    /// fixed-point SIMD kernel yet.
116    #[default]
117    Auto,
118    /// Fixed point on every target, with a scalar kernel where there is no
119    /// SIMD one. Gives the same results as `Auto` on SIMD targets.
120    FixedPoint,
121    /// The f32 reference kernel, the arithmetic of 0.5 and earlier releases.
122    /// Windows wider than 127 px always use it.
123    Reference,
124}
125
126/// Options of a tracking call beyond [`TrackParams`], for
127/// [`TrackerContext::track_with`] and [`TrackerContext::track_fb_with`] and
128/// their [`FloatTrackerContext`] counterparts. The default reproduces
129/// `track` and `track_fb`.
130///
131/// ```
132/// use optical_flow_lk::TrackOptions;
133///
134/// // Check the round trip on the two finest levels only.
135/// const OPTIONS: TrackOptions = TrackOptions::new().with_fb_levels(2);
136/// assert_eq!(OPTIONS.fb_levels, Some(2));
137/// ```
138#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
139#[non_exhaustive]
140pub struct TrackOptions {
141    /// Pyramid level the forward pass starts at, 0 being full resolution.
142    /// `None` starts at the coarsest level; levels above it are clamped to
143    /// it. The displacement from `predicted` (or zero) seeds the start level.
144    ///
145    /// Each level above full resolution doubles the motion the iteration can
146    /// recover from its seed, about half a window at level 0. Starting lower
147    /// saves the coarse levels' work when a prediction (a gyroscope, a
148    /// constant-velocity model) is already that close, and loses the capture
149    /// range they give otherwise.
150    pub start_level: Option<usize>,
151    /// Pyramid levels the backward pass of `track_fb_with` runs, the finest
152    /// ones: `Some(k)` runs levels `k - 1` down to 0, `None` all of them, as
153    /// `track_fb` does. Must be at least 1; ignored by `track_with`.
154    ///
155    /// The backward pass tracks the forward results back into the previous
156    /// frame, seeded at the original points, which a consistent round trip
157    /// returns to. A correct forward match therefore needs no coarse levels
158    /// to come back. Fewer levels make the check cheaper and slightly more
159    /// permissive: a wrong match that the coarse levels would have carried
160    /// further away can stay within the threshold. On the bundled example
161    /// pair, see the changelog of 0.6.0 for how many points change status.
162    pub fb_levels: Option<usize>,
163    /// Arithmetic of the 8-bit tracker. Ignored by [`FloatTrackerContext`].
164    pub kernel: TrackKernel,
165}
166
167impl TrackOptions {
168    /// The default options: all levels in both passes, [`TrackKernel::Auto`].
169    pub const fn new() -> Self {
170        TrackOptions {
171            start_level: None,
172            fb_levels: None,
173            kernel: TrackKernel::Auto,
174        }
175    }
176
177    /// Starts the forward pass at pyramid level `level`, see
178    /// [`start_level`](Self::start_level).
179    pub const fn with_start_level(mut self, level: usize) -> Self {
180        self.start_level = Some(level);
181        self
182    }
183
184    /// Runs the backward pass over the finest `levels` levels, see
185    /// [`fb_levels`](Self::fb_levels).
186    pub const fn with_fb_levels(mut self, levels: usize) -> Self {
187        self.fb_levels = Some(levels);
188        self
189    }
190
191    /// Selects the arithmetic of the 8-bit tracker.
192    pub const fn with_kernel(mut self, kernel: TrackKernel) -> Self {
193        self.kernel = kernel;
194        self
195    }
196}
197
198/// Per-point result of [`calc_optical_flow_ex`].
199///
200/// # Coordinate convention
201/// Positions are in level-0 (full resolution) pixel coordinates. The origin is
202/// the center of the top-left pixel, x grows to the right and y downwards; this
203/// matches [`crate::good_features_to_track`] and bilinear sampling throughout
204/// the crate.
205#[derive(Debug, Clone, Copy, PartialEq)]
206pub struct TrackResult {
207    /// Tracked position in the next frame.
208    pub pos: (f32, f32),
209    /// Why tracking ended the way it did.
210    pub status: TrackStatus,
211    /// Mean absolute photometric residual over the window at the final
212    /// position, in the input's intensity unit: grey levels for 8-bit images,
213    /// the image's own unit (e.g. °C) for the f32 tracker.
214    ///
215    /// Comparable across points regardless of `window_size`; intended for
216    /// downstream outlier gating. It is [`f32::INFINITY`] when no residual
217    /// could be measured (the window was out of bounds).
218    pub error: f32,
219}
220
221/// Compute optical flow using the pyramidal Lucas-Kanade method, returning a
222/// per-point [`TrackResult`] with status and photometric error.
223///
224/// # Arguments
225/// * `prev_pyramid` - Previous frame (pyramid of grayscale)
226/// * `curr_pyramid` - Next frame (pyramid of grayscale)
227/// * `prev_points` - Feature points to track (in prev frame, level-0 coordinates)
228/// * `predicted` - Optional predicted positions in the next frame, one per
229///   `prev_point` (level-0 coordinates). When `Some`, the iteration is seeded
230///   with the predicted displacement at the coarsest pyramid level instead of
231///   zero, which markedly improves convergence under large inter-frame motion
232///   (e.g. gyroscope-predicted feature motion). `None` reproduces the classic
233///   zero-initialized behavior. Per-level propagation is unchanged.
234/// * `params` - window size, iterations and texture threshold, see
235///   [`TrackParams`]
236///
237/// # Panics
238/// Panics if the pyramids differ in level count or level sizes, if
239/// `params.window_size` is even, or if `predicted` is `Some` but its length
240/// differs from `prev_points`.
241///
242/// # Returns
243/// One [`TrackResult`] per input point, in the same order.
244pub fn calc_optical_flow_ex(
245    prev_pyramid: &[GrayImage],
246    curr_pyramid: &[GrayImage],
247    prev_points: &[(f32, f32)],
248    predicted: Option<&[(f32, f32)]>,
249    params: TrackParams,
250) -> Vec<TrackResult> {
251    let mut scratch = Scratch::default();
252    let mut out = Vec::new();
253    track_into(
254        prev_pyramid,
255        &mut Gradients::default(),
256        curr_pyramid,
257        prev_points,
258        predicted,
259        params,
260        None,
261        GrayImage::select(TrackKernel::Auto, params.window_size),
262        &mut scratch,
263        &mut out,
264    );
265    out
266}
267
268/// Reusable per-call scratch buffers for the Lucas-Kanade loop. Owned by
269/// [`TrackerContext`] (or created transiently by the free functions) so the
270/// steady-state hot path performs no heap allocation.
271#[derive(Default)]
272struct Scratch {
273    /// Patches of the f32 kernels: intensities and both gradients.
274    float_patch: [Vec<f32>; 3],
275    /// Patches of the fixed-point kernels, rows padded to whole lane chunks.
276    fixed_patch: [Vec<i16>; 3],
277    displacements: Vec<(f32, f32)>,
278}
279
280impl Scratch {
281    fn reserve(&mut self, capacity: &TrackerCapacity) {
282        let size = capacity.window_size;
283        for buffer in &mut self.float_patch {
284            reserve_total(buffer, size * size);
285        }
286        for buffer in &mut self.fixed_patch {
287            reserve_total(buffer, size * fixed::stride(size));
288        }
289        reserve_total(&mut self.displacements, capacity.points);
290    }
291}
292
293/// Scharr gradients of the levels of one pyramid, each computed when first
294/// needed and kept until the pyramid changes. The contexts keep one per
295/// pyramid, so every frame's gradients are computed once, however many
296/// passes read them; the free functions use a transient one per call.
297#[derive(Default)]
298struct Gradients<G> {
299    levels: Vec<GradientLevel<G>>,
300}
301
302#[derive(Default)]
303struct GradientLevel<G> {
304    x: Vec<G>,
305    y: Vec<G>,
306    /// Whether `x` and `y` hold the gradients of the pyramid's current level.
307    ready: bool,
308}
309
310impl<G: Copy + Default> Gradients<G> {
311    /// Forgets every level, for a pyramid that was rebuilt.
312    fn invalidate(&mut self) {
313        for level in &mut self.levels {
314            level.ready = false;
315        }
316    }
317
318    /// Allocates the planes of a `width` x `height` pyramid with `levels`
319    /// levels.
320    fn reserve(&mut self, width: u32, height: u32, levels: usize) {
321        for (index, (w, h)) in level_dimensions(width, height, levels).enumerate() {
322            if index == self.levels.len() {
323                self.levels.push(GradientLevel::default());
324            }
325            let pixels = w as usize * h as usize;
326            reserve_total(&mut self.levels[index].x, pixels);
327            reserve_total(&mut self.levels[index].y, pixels);
328        }
329    }
330
331    /// The gradients of `pyramid[level]`, computed now unless they are kept
332    /// from an earlier call.
333    fn level<I: TrackImage<Gradient = G>>(&mut self, pyramid: &[I], level: usize) -> (&[G], &[G]) {
334        if self.levels.len() < pyramid.len() {
335            self.levels
336                .resize_with(pyramid.len(), GradientLevel::default);
337        }
338        let image = &pyramid[level];
339        let slot = &mut self.levels[level];
340        if !slot.ready {
341            let (w, h) = image.dimensions();
342            let pixels = w as usize * h as usize;
343            slot.x.resize(pixels, G::default());
344            slot.y.resize(pixels, G::default());
345            image.gradients_into(&mut slot.x, &mut slot.y);
346            slot.ready = true;
347        }
348        (&slot.x, &slot.y)
349    }
350}
351
352/// Core pyramidal Lucas-Kanade loop, writing one [`TrackResult`] per point into
353/// `out`. `prev_gradients` caches the gradients of `prev_pyramid`. The pass
354/// runs from `start_level` (`None`: the top of the pyramid) down to full
355/// resolution with `kernel`. All temporaries live in `scratch`; given
356/// sufficient capacity this is allocation-free.
357#[allow(clippy::too_many_arguments)]
358fn track_into<I: TrackImage>(
359    prev_pyramid: &[I],
360    prev_gradients: &mut Gradients<I::Gradient>,
361    curr_pyramid: &[I],
362    prev_points: &[(f32, f32)],
363    predicted: Option<&[(f32, f32)]>,
364    params: TrackParams,
365    start_level: Option<usize>,
366    kernel: I::Kernel,
367    scratch: &mut Scratch,
368    out: &mut Vec<TrackResult>,
369) {
370    assert_eq!(prev_pyramid.len(), curr_pyramid.len());
371    assert!(
372        !prev_pyramid.is_empty(),
373        "pyramid must have at least 1 level"
374    );
375    assert!(
376        prev_pyramid
377            .iter()
378            .zip(curr_pyramid)
379            .all(|(prev, curr)| prev.dimensions() == curr.dimensions()),
380        "both pyramids must have the same level sizes"
381    );
382    let TrackParams {
383        window_size,
384        max_iterations,
385        min_eigen_threshold,
386    } = params;
387    assert!(window_size % 2 == 1, "Window size must be odd");
388    if let Some(predicted) = predicted {
389        assert_eq!(
390            predicted.len(),
391            prev_points.len(),
392            "predicted must have one entry per prev_point"
393        );
394    }
395
396    let top = prev_pyramid.len() - 1;
397    let start = start_level.map_or(top, |level| level.min(top));
398    let radius = window_size / 2;
399
400    // Total displacement per point, accumulated coarse-to-fine in level-0 units.
401    // Seeding it from a prediction makes the start level begin at the
402    // predicted position; everything else is identical to the zero-init path.
403    let displacements = &mut scratch.displacements;
404    displacements.clear();
405    match predicted {
406        Some(predicted) => displacements.extend(
407            prev_points
408                .iter()
409                .zip(predicted.iter())
410                .map(|((px, py), (gx, gy))| (gx - px, gy - py)),
411        ),
412        None => displacements.resize(prev_points.len(), (0.0, 0.0)),
413    }
414
415    // Initialize results at the input positions; the loop refines them in place.
416    out.clear();
417    out.extend(prev_points.iter().map(|&(x, y)| TrackResult {
418        pos: (x, y),
419        status: TrackStatus::Tracked,
420        error: f32::INFINITY,
421    }));
422
423    // Process levels from the start level (coarse) to the bottom (fine).
424    for level in (0..=start).rev() {
425        let prev_img = &prev_pyramid[level];
426        let curr_img = &curr_pyramid[level];
427        let (lw, lh) = prev_img.dimensions();
428        let (grad_x, grad_y) = prev_gradients.level(prev_pyramid, level);
429
430        let level = Level::<I> {
431            prev: prev_img.pixels(),
432            curr: curr_img.pixels(),
433            grad_x,
434            grad_y,
435            width: lw as usize,
436            height: lh as usize,
437            scale: 2f32.powi(level as i32),
438            is_finest: level == 0,
439            window_size,
440            radius,
441            max_iterations,
442            min_eigen_threshold,
443        };
444
445        I::track_level(kernel, &level, prev_points, scratch, out);
446    }
447
448    // Fold accumulated displacements into the reported positions.
449    for (idx, (x, y)) in prev_points.iter().enumerate() {
450        let (dx, dy) = scratch.displacements[idx];
451        out[idx].pos = (x + dx, y + dy);
452    }
453}
454
455/// Everything the per-level loop needs about one pyramid level.
456struct Level<'a, I: TrackImage> {
457    prev: &'a [I::Pixel],
458    curr: &'a [I::Pixel],
459    grad_x: &'a [I::Gradient],
460    grad_y: &'a [I::Gradient],
461    width: usize,
462    height: usize,
463    scale: f32,
464    is_finest: bool,
465    window_size: usize,
466    radius: usize,
467    max_iterations: usize,
468    min_eigen_threshold: f32,
469}
470
471/// The f32 reference kernel, also the only kernel of the f32 tracker. The
472/// portable body is written so the compiler can vectorize it for whatever the
473/// target enables (SSE2 baseline, NEON, `simd128`); on x86 an AVX2 copy is
474/// selected at runtime.
475#[derive(Clone, Copy, Debug)]
476enum Kernel {
477    Portable,
478    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
479    Avx2,
480}
481
482impl Kernel {
483    fn detect() -> Self {
484        #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
485        if is_x86_feature_detected!("avx2") {
486            return Kernel::Avx2;
487        }
488        Kernel::Portable
489    }
490
491    /// Refines every point's displacement at one level.
492    fn track_level<I: TrackImage>(
493        self,
494        level: &Level<I>,
495        prev_points: &[(f32, f32)],
496        scratch: &mut Scratch,
497        out: &mut [TrackResult],
498    ) {
499        let n_pixels = level.window_size * level.window_size;
500        let Scratch {
501            float_patch: [prev, ix, iy],
502            displacements,
503            ..
504        } = scratch;
505        for patch in [&mut *prev, &mut *ix, &mut *iy] {
506            patch.resize(n_pixels, 0.0);
507        }
508        let windows = FloatWindows { prev, ix, iy };
509        match self {
510            Kernel::Portable => track_level_impl(level, prev_points, displacements, windows, out),
511            #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
512            // SAFETY: `detect` only returns `Avx2` after a positive runtime check.
513            Kernel::Avx2 => unsafe {
514                track_level_avx2(level, prev_points, displacements, windows, out)
515            },
516        }
517    }
518}
519
520#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
521#[target_feature(enable = "avx2")]
522unsafe fn track_level_avx2<I: TrackImage, W: Windows<I>>(
523    level: &Level<I>,
524    prev_points: &[(f32, f32)],
525    displacements: &mut [(f32, f32)],
526    windows: W,
527    out: &mut [TrackResult],
528) {
529    track_level_impl(level, prev_points, displacements, windows, out)
530}
531
532/// A fixed-point kernel of the 8-bit tracker, see the `fixed` module. All of
533/// them return the same results. aarch64 has no NEON kernel yet and runs the
534/// scalar one when `TrackKernel::FixedPoint` asks for fixed point.
535#[derive(Clone, Copy, Debug)]
536enum FixedKernel {
537    /// The kernel of targets without SIMD kernels; the tests run it on all.
538    #[cfg_attr(
539        any(
540            all(
541                any(target_arch = "x86", target_arch = "x86_64"),
542                target_feature = "sse2"
543            ),
544            all(target_arch = "wasm32", target_feature = "simd128")
545        ),
546        allow(dead_code)
547    )]
548    Scalar,
549    #[cfg(all(
550        any(target_arch = "x86", target_arch = "x86_64"),
551        target_feature = "sse2"
552    ))]
553    Sse2,
554    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
555    Avx2(fixed::Avx2),
556    #[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
557    Simd128,
558}
559
560impl FixedKernel {
561    /// The SIMD kernel every CPU of the compilation target has.
562    #[cfg(all(
563        any(target_arch = "x86", target_arch = "x86_64"),
564        target_feature = "sse2"
565    ))]
566    const BASELINE: Self = FixedKernel::Sse2;
567    #[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
568    const BASELINE: Self = FixedKernel::Simd128;
569    #[cfg(not(any(
570        all(
571            any(target_arch = "x86", target_arch = "x86_64"),
572            target_feature = "sse2"
573        ),
574        all(target_arch = "wasm32", target_feature = "simd128")
575    )))]
576    const BASELINE: Self = FixedKernel::Scalar;
577
578    /// The fastest fixed-point kernel of this CPU; `Scalar` without SIMD.
579    fn detect() -> Self {
580        #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
581        if let Some(avx2) = fixed::Avx2::detect() {
582            return FixedKernel::Avx2(avx2);
583        }
584        Self::BASELINE
585    }
586
587    /// Refines every point's displacement at one level.
588    fn track_level(
589        self,
590        level: &Level<GrayImage>,
591        prev_points: &[(f32, f32)],
592        scratch: &mut Scratch,
593        out: &mut [TrackResult],
594    ) {
595        let size = level.window_size;
596        let stride = fixed::stride(size);
597        let Scratch {
598            fixed_patch: [pixels, grad_x, grad_y],
599            displacements,
600            ..
601        } = scratch;
602        for patch in [&mut *pixels, &mut *grad_x, &mut *grad_y] {
603            patch.resize(size * stride, 0);
604        }
605        let patch = fixed::Patch {
606            size,
607            stride,
608            pixels,
609            grad_x,
610            grad_y,
611        };
612        fn run<K: fixed::Ops>(
613            level: &Level<GrayImage>,
614            prev_points: &[(f32, f32)],
615            displacements: &mut [(f32, f32)],
616            patch: fixed::Patch<'_>,
617            ops: K,
618            out: &mut [TrackResult],
619        ) {
620            let windows = FixedWindows { patch, ops };
621            track_level_impl(level, prev_points, displacements, windows, out)
622        }
623        match self {
624            FixedKernel::Scalar => {
625                run(level, prev_points, displacements, patch, fixed::Scalar, out)
626            }
627            #[cfg(all(
628                any(target_arch = "x86", target_arch = "x86_64"),
629                target_feature = "sse2"
630            ))]
631            FixedKernel::Sse2 => run(level, prev_points, displacements, patch, fixed::Sse2, out),
632            #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
633            FixedKernel::Avx2(avx2) => run(level, prev_points, displacements, patch, avx2, out),
634            #[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
635            FixedKernel::Simd128 => run(
636                level,
637                prev_points,
638                displacements,
639                patch,
640                fixed::Simd128,
641                out,
642            ),
643        }
644    }
645}
646
647/// The kernel of one pass of the 8-bit tracker.
648#[derive(Clone, Copy, Debug)]
649enum ByteKernel {
650    Reference(Kernel),
651    Fixed(FixedKernel),
652}
653
654/// Per-window arithmetic of the Lucas-Kanade loop: the patch of the previous
655/// frame, the residual sums against the next frame and the final error.
656trait Windows<I: TrackImage> {
657    /// Fills the patch of `win` and returns the spatial gradient matrix
658    /// `(gxx, gxy, gyy)`, summed over the window, in (unit / pixel)^2.
659    fn build(&mut self, lv: &Level<I>, win: &Window) -> (f32, f32, f32);
660
661    /// The gradient-weighted residual `(bx, by)` between the patch and the
662    /// next frame sampled at `win`.
663    fn step(&self, lv: &Level<I>, win: &Window) -> (f32, f32);
664
665    /// Mean absolute residual between the patch and the next frame at `win`.
666    fn error(&self, lv: &Level<I>, win: &Window) -> f32;
667}
668
669/// The f32 reference windows.
670struct FloatWindows<'a> {
671    prev: &'a mut [f32],
672    ix: &'a mut [f32],
673    iy: &'a mut [f32],
674}
675
676impl<I: TrackImage> Windows<I> for FloatWindows<'_> {
677    #[inline(always)]
678    fn build(&mut self, lv: &Level<I>, win: &Window) -> (f32, f32, f32) {
679        build_patch(lv, win, self.prev, self.ix, self.iy)
680    }
681
682    #[inline(always)]
683    fn step(&self, lv: &Level<I>, win: &Window) -> (f32, f32) {
684        lk_step(lv, win, self.prev, self.ix, self.iy)
685    }
686
687    #[inline(always)]
688    fn error(&self, lv: &Level<I>, win: &Window) -> f32 {
689        window_error(lv, win, self.prev)
690    }
691}
692
693/// The fixed-point windows of the 8-bit tracker, with kernels `ops`.
694struct FixedWindows<'a, K> {
695    patch: fixed::Patch<'a>,
696    ops: K,
697}
698
699impl<K: fixed::Ops> Windows<GrayImage> for FixedWindows<'_, K> {
700    #[inline(always)]
701    fn build(&mut self, lv: &Level<GrayImage>, win: &Window) -> (f32, f32, f32) {
702        let source = fixed::Source {
703            pixels: fixed::Plane {
704                data: lv.prev,
705                width: lv.width,
706            },
707            grad_x: fixed::Plane {
708                data: lv.grad_x,
709                width: lv.width,
710            },
711            grad_y: fixed::Plane {
712                data: lv.grad_y,
713                width: lv.width,
714            },
715        };
716        let weights = fixed::Weights::of(win);
717        let [xx, xy, yy] = self
718            .ops
719            .build(source, win.x0, win.y0, weights, &mut self.patch);
720        let scale = |sum: i64| sum as f32 * fixed::SPATIAL_SCALE;
721        (scale(xx), scale(xy), scale(yy))
722    }
723
724    #[inline(always)]
725    fn step(&self, lv: &Level<GrayImage>, win: &Window) -> (f32, f32) {
726        let next = fixed::Plane {
727            data: lv.curr,
728            width: lv.width,
729        };
730        let weights = fixed::Weights::of(win);
731        let [bx, by] = self.ops.step(next, win.x0, win.y0, weights, &self.patch);
732        (
733            bx as f32 * fixed::RESIDUAL_SCALE,
734            by as f32 * fixed::RESIDUAL_SCALE,
735        )
736    }
737
738    #[inline(always)]
739    fn error(&self, lv: &Level<GrayImage>, win: &Window) -> f32 {
740        let next = fixed::Plane {
741            data: lv.curr,
742            width: lv.width,
743        };
744        let weights = fixed::Weights::of(win);
745        let sum = self.ops.error(next, win.x0, win.y0, weights, &self.patch);
746        sum as f32 * fixed::ERROR_SCALE / (lv.window_size * lv.window_size) as f32
747    }
748}
749
750/// Refines every point's displacement at one pyramid level.
751#[inline(always)]
752fn track_level_impl<I: TrackImage, W: Windows<I>>(
753    lv: &Level<I>,
754    prev_points: &[(f32, f32)],
755    displacements: &mut [(f32, f32)],
756    mut windows: W,
757    out: &mut [TrackResult],
758) {
759    let n_pixels = lv.window_size * lv.window_size;
760    let window_limit = lv.window_size as f32;
761    // Mean residual at the final position, infinite when the window left the
762    // frame.
763    let final_error = |windows: &W, cx: f32, cy: f32| {
764        Window::new(cx, cy, lv.radius, lv.width, lv.height)
765            .map_or(f32::INFINITY, |win| windows.error(lv, &win))
766    };
767
768    for (idx, &(prev_x, prev_y)) in prev_points.iter().enumerate() {
769        // Scale the original point for the current level.
770        let x = prev_x / lv.scale;
771        let y = prev_y / lv.scale;
772
773        // Add the current displacement, scaled for this level.
774        let mut dx = displacements[idx].0 / lv.scale;
775        let mut dy = displacements[idx].1 / lv.scale;
776
777        // The window must stay inside the previous image to build the patch.
778        let Some(prev_win) = Window::new(x, y, lv.radius, lv.width, lv.height) else {
779            out[idx].status = TrackStatus::OutOfBounds;
780            continue;
781        };
782
783        // Spatial gradient matrix and cached previous/gradient patches.
784        let (gxx, gxy, gyy) = windows.build(lv, &prev_win);
785
786        // Reject low-texture windows up front (normalized by window area so
787        // the threshold does not depend on `window_size`).
788        let (inv_h00, inv_h01, inv_h11) =
789            match I::spatial_inverse(gxx, gxy, gyy, n_pixels, lv.min_eigen_threshold) {
790                Spatial::Invertible(inverse) => inverse,
791                Spatial::LowTexture { measure_error } => {
792                    out[idx].status = TrackStatus::LowTexture;
793                    if measure_error && lv.is_finest {
794                        out[idx].error = final_error(&windows, x + dx, y + dy);
795                    }
796                    continue;
797                }
798            };
799
800        // Refine the displacement at the current level.
801        let mut converged = false;
802        let mut out_of_bounds = false;
803        let mut diverged = false;
804        let mut prev_step = (0.0f32, 0.0f32);
805        for iteration in 0..lv.max_iterations {
806            let Some(win) = Window::new(x + dx, y + dy, lv.radius, lv.width, lv.height) else {
807                out_of_bounds = true;
808                break;
809            };
810
811            let (bx, by) = windows.step(lv, &win);
812
813            let ddx = inv_h00 * bx + inv_h01 * by;
814            let ddy = inv_h01 * bx + inv_h11 * by;
815            dx += ddx;
816            dy += ddy;
817
818            // Guard against runaway steps.
819            if !dx.is_finite()
820                || !dy.is_finite()
821                || ddx.abs() > window_limit
822                || ddy.abs() > window_limit
823            {
824                diverged = true;
825                break;
826            }
827
828            if ddx.abs() < CONVERGENCE_EPSILON && ddy.abs() < CONVERGENCE_EPSILON {
829                converged = true;
830                break;
831            }
832
833            // Two consecutive steps that cancel mean the estimate is bouncing
834            // across the optimum; settle on the midpoint.
835            if iteration > 0
836                && (ddx + prev_step.0).abs() < CONVERGENCE_EPSILON
837                && (ddy + prev_step.1).abs() < CONVERGENCE_EPSILON
838            {
839                dx -= ddx * 0.5;
840                dy -= ddy * 0.5;
841                converged = true;
842                break;
843            }
844            prev_step = (ddx, ddy);
845        }
846
847        out[idx].status = if out_of_bounds {
848            TrackStatus::OutOfBounds
849        } else if diverged || !converged {
850            TrackStatus::Diverged
851        } else {
852            TrackStatus::Tracked
853        };
854
855        // Update the total displacement with the current level scale.
856        displacements[idx] = (dx * lv.scale, dy * lv.scale);
857
858        if lv.is_finest {
859            out[idx].error = if out_of_bounds {
860                f32::INFINITY
861            } else {
862                final_error(&windows, x + dx, y + dy)
863            };
864        }
865    }
866}
867
868/// A sampling window anchored at an integer top-left corner. Because the
869/// window offsets are integers, every sample shares the same fractional
870/// position and therefore the same four bilinear weights; each window row is
871/// then a contiguous run of `window_size + 1` pixels from two image rows.
872struct Window {
873    x0: usize,
874    y0: usize,
875    w00: f32,
876    w10: f32,
877    w01: f32,
878    w11: f32,
879}
880
881impl Window {
882    /// Returns `None` when the full 2x2 bilinear footprint of the window around
883    /// `(cx, cy)` is not inside a `width` x `height` image.
884    #[inline(always)]
885    fn new(cx: f32, cy: f32, radius: usize, width: usize, height: usize) -> Option<Window> {
886        let r = radius as f32;
887        // Written so that NaN coordinates fail the test.
888        if !(cx >= r && cy >= r && cx < width as f32 - r - 1.0 && cy < height as f32 - r - 1.0) {
889            return None;
890        }
891        let fx0 = cx.floor();
892        let fy0 = cy.floor();
893        let fx = cx - fx0;
894        let fy = cy - fy0;
895        Some(Window {
896            x0: fx0 as usize - radius,
897            y0: fy0 as usize - radius,
898            w00: (1.0 - fx) * (1.0 - fy),
899            w10: fx * (1.0 - fy),
900            w01: (1.0 - fx) * fy,
901            w11: fx * fy,
902        })
903    }
904
905    /// The same window with every weight multiplied by `factor`.
906    #[inline(always)]
907    fn scaled(&self, factor: f32) -> Window {
908        Window {
909            x0: self.x0,
910            y0: self.y0,
911            w00: self.w00 * factor,
912            w10: self.w10 * factor,
913            w01: self.w01 * factor,
914            w11: self.w11 * factor,
915        }
916    }
917
918    /// The two image rows feeding window row `j`: `size + 1` pixels each.
919    #[inline(always)]
920    fn rows<'a, T>(
921        &self,
922        data: &'a [T],
923        width: usize,
924        j: usize,
925        size: usize,
926    ) -> (&'a [T], &'a [T]) {
927        let start = (self.y0 + j) * width + self.x0;
928        (
929            &data[start..start + size + 1],
930            &data[start + width..start + width + size + 1],
931        )
932    }
933}
934
935/// Bilinear samples of `L` consecutive window pixels from two source rows.
936#[inline(always)]
937fn sample_lanes<T: Copy + Into<f32>, const L: usize>(win: &Window, a: &[T], b: &[T]) -> [f32; L] {
938    let a0: &[T; L] = a[..L].try_into().unwrap();
939    let a1: &[T; L] = a[1..=L].try_into().unwrap();
940    let b0: &[T; L] = b[..L].try_into().unwrap();
941    let b1: &[T; L] = b[1..=L].try_into().unwrap();
942    let mut s = [0f32; L];
943    for k in 0..L {
944        s[k] = win.w00 * a0[k].into()
945            + win.w10 * a1[k].into()
946            + win.w01 * b0[k].into()
947            + win.w11 * b1[k].into();
948    }
949    s
950}
951
952#[inline(always)]
953fn total<const L: usize>(acc: &[f32; L]) -> f32 {
954    acc.iter().sum()
955}
956
957/// Fills the previous-image and gradient patches for `win` and returns the
958/// spatial gradient matrix `(gxx, gxy, gyy)`.
959#[inline(always)]
960fn build_patch<I: TrackImage>(
961    lv: &Level<I>,
962    win: &Window,
963    prev_patch: &mut [f32],
964    ix_patch: &mut [f32],
965    iy_patch: &mut [f32],
966) -> (f32, f32, f32) {
967    #[inline(always)]
968    #[allow(clippy::too_many_arguments)]
969    fn lanes<P: Copy + Into<f32>, G: Copy + Into<f32>, const L: usize>(
970        win: &Window,
971        gwin: &Window,
972        (pa, pb): (&[P], &[P]),
973        (xa, xb): (&[G], &[G]),
974        (ya, yb): (&[G], &[G]),
975        p: &mut [f32],
976        ox: &mut [f32],
977        oy: &mut [f32],
978        acc: &mut [[f32; L]; 3],
979    ) {
980        let s = sample_lanes::<P, L>(win, pa, pb);
981        let sx = sample_lanes::<G, L>(gwin, xa, xb);
982        let sy = sample_lanes::<G, L>(gwin, ya, yb);
983        let p: &mut [f32; L] = (&mut p[..L]).try_into().unwrap();
984        let ox: &mut [f32; L] = (&mut ox[..L]).try_into().unwrap();
985        let oy: &mut [f32; L] = (&mut oy[..L]).try_into().unwrap();
986        *p = s;
987        *ox = sx;
988        *oy = sy;
989        for k in 0..L {
990            acc[0][k] += sx[k] * sx[k];
991            acc[1][k] += sx[k] * sy[k];
992            acc[2][k] += sy[k] * sy[k];
993        }
994    }
995
996    let size = lv.window_size;
997    // Scharr outputs are 32x the intensity slope for both image types.
998    let gwin = win.scaled(1.0 / 32.0);
999    let mut acc8 = [[0f32; 8]; 3];
1000    let mut acc4 = [[0f32; 4]; 3];
1001    let mut acc1 = [[0f32; 1]; 3];
1002
1003    for j in 0..size {
1004        let (pa, pb) = win.rows(lv.prev, lv.width, j, size);
1005        let (xa, xb) = win.rows(lv.grad_x, lv.width, j, size);
1006        let (ya, yb) = win.rows(lv.grad_y, lv.width, j, size);
1007        let p = &mut prev_patch[j * size..][..size];
1008        let ox = &mut ix_patch[j * size..][..size];
1009        let oy = &mut iy_patch[j * size..][..size];
1010
1011        let mut i = 0;
1012        while i + 8 <= size {
1013            lanes::<_, _, 8>(
1014                win,
1015                &gwin,
1016                (&pa[i..], &pb[i..]),
1017                (&xa[i..], &xb[i..]),
1018                (&ya[i..], &yb[i..]),
1019                &mut p[i..],
1020                &mut ox[i..],
1021                &mut oy[i..],
1022                &mut acc8,
1023            );
1024            i += 8;
1025        }
1026        if i + 4 <= size {
1027            lanes::<_, _, 4>(
1028                win,
1029                &gwin,
1030                (&pa[i..], &pb[i..]),
1031                (&xa[i..], &xb[i..]),
1032                (&ya[i..], &yb[i..]),
1033                &mut p[i..],
1034                &mut ox[i..],
1035                &mut oy[i..],
1036                &mut acc4,
1037            );
1038            i += 4;
1039        }
1040        while i < size {
1041            lanes::<_, _, 1>(
1042                win,
1043                &gwin,
1044                (&pa[i..], &pb[i..]),
1045                (&xa[i..], &xb[i..]),
1046                (&ya[i..], &yb[i..]),
1047                &mut p[i..],
1048                &mut ox[i..],
1049                &mut oy[i..],
1050                &mut acc1,
1051            );
1052            i += 1;
1053        }
1054    }
1055
1056    (
1057        total(&acc8[0]) + total(&acc4[0]) + total(&acc1[0]),
1058        total(&acc8[1]) + total(&acc4[1]) + total(&acc1[1]),
1059        total(&acc8[2]) + total(&acc4[2]) + total(&acc1[2]),
1060    )
1061}
1062
1063/// One Lucas-Kanade update: the gradient-weighted residual `(bx, by)` between
1064/// the cached previous patch and the next image sampled at `win`.
1065#[inline(always)]
1066fn lk_step<I: TrackImage>(
1067    lv: &Level<I>,
1068    win: &Window,
1069    prev_patch: &[f32],
1070    ix_patch: &[f32],
1071    iy_patch: &[f32],
1072) -> (f32, f32) {
1073    #[inline(always)]
1074    fn lanes<P: Copy + Into<f32>, const L: usize>(
1075        win: &Window,
1076        (a, b): (&[P], &[P]),
1077        p: &[f32],
1078        gx: &[f32],
1079        gy: &[f32],
1080        acc: &mut [[f32; L]; 2],
1081    ) {
1082        let s = sample_lanes::<P, L>(win, a, b);
1083        let p: &[f32; L] = p[..L].try_into().unwrap();
1084        let gx: &[f32; L] = gx[..L].try_into().unwrap();
1085        let gy: &[f32; L] = gy[..L].try_into().unwrap();
1086        for k in 0..L {
1087            let e = p[k] - s[k];
1088            acc[0][k] += gx[k] * e;
1089            acc[1][k] += gy[k] * e;
1090        }
1091    }
1092
1093    let size = lv.window_size;
1094    let mut acc8 = [[0f32; 8]; 2];
1095    let mut acc4 = [[0f32; 4]; 2];
1096    let mut acc1 = [[0f32; 1]; 2];
1097
1098    for j in 0..size {
1099        let (a, b) = win.rows(lv.curr, lv.width, j, size);
1100        let p = &prev_patch[j * size..][..size];
1101        let gx = &ix_patch[j * size..][..size];
1102        let gy = &iy_patch[j * size..][..size];
1103
1104        let mut i = 0;
1105        while i + 8 <= size {
1106            lanes::<_, 8>(
1107                win,
1108                (&a[i..], &b[i..]),
1109                &p[i..],
1110                &gx[i..],
1111                &gy[i..],
1112                &mut acc8,
1113            );
1114            i += 8;
1115        }
1116        if i + 4 <= size {
1117            lanes::<_, 4>(
1118                win,
1119                (&a[i..], &b[i..]),
1120                &p[i..],
1121                &gx[i..],
1122                &gy[i..],
1123                &mut acc4,
1124            );
1125            i += 4;
1126        }
1127        while i < size {
1128            lanes::<_, 1>(
1129                win,
1130                (&a[i..], &b[i..]),
1131                &p[i..],
1132                &gx[i..],
1133                &gy[i..],
1134                &mut acc1,
1135            );
1136            i += 1;
1137        }
1138    }
1139
1140    (
1141        total(&acc8[0]) + total(&acc4[0]) + total(&acc1[0]),
1142        total(&acc8[1]) + total(&acc4[1]) + total(&acc1[1]),
1143    )
1144}
1145
1146/// Mean absolute photometric residual between the cached previous patch and the
1147/// next image sampled at `win`.
1148#[inline(always)]
1149fn window_error<I: TrackImage>(lv: &Level<I>, win: &Window, prev_patch: &[f32]) -> f32 {
1150    #[inline(always)]
1151    fn lanes<P: Copy + Into<f32>, const L: usize>(
1152        win: &Window,
1153        (a, b): (&[P], &[P]),
1154        p: &[f32],
1155        acc: &mut [f32; L],
1156    ) {
1157        let s = sample_lanes::<P, L>(win, a, b);
1158        let p: &[f32; L] = p[..L].try_into().unwrap();
1159        for k in 0..L {
1160            acc[k] += (p[k] - s[k]).abs();
1161        }
1162    }
1163
1164    let size = lv.window_size;
1165    let mut acc8 = [0f32; 8];
1166    let mut acc4 = [0f32; 4];
1167    let mut acc1 = [0f32; 1];
1168
1169    for j in 0..size {
1170        let (a, b) = win.rows(lv.curr, lv.width, j, size);
1171        let p = &prev_patch[j * size..][..size];
1172        let mut i = 0;
1173        while i + 8 <= size {
1174            lanes::<_, 8>(win, (&a[i..], &b[i..]), &p[i..], &mut acc8);
1175            i += 8;
1176        }
1177        if i + 4 <= size {
1178            lanes::<_, 4>(win, (&a[i..], &b[i..]), &p[i..], &mut acc4);
1179            i += 4;
1180        }
1181        while i < size {
1182            lanes::<_, 1>(win, (&a[i..], &b[i..]), &p[i..], &mut acc1);
1183            i += 1;
1184        }
1185    }
1186
1187    (total(&acc8) + total(&acc4) + total(&acc1)) / (size * size) as f32
1188}
1189
1190/// Track points prev->next, then re-track the results next->prev, and flag any
1191/// point whose round-trip lands further than `fb_threshold` pixels from where it
1192/// started as [`TrackStatus::FbInconsistent`].
1193///
1194/// This is the standard forward-backward consistency check and is the cheapest
1195/// reliable way to reject occlusions and ambiguous matches before they poison a
1196/// downstream pose solve. Points that already failed the forward pass keep their
1197/// forward status (the round-trip is only evaluated for forward-tracked points).
1198///
1199/// Both passes reuse the supplied pyramids. The free-function form still
1200/// allocates its result vectors internally; for a fully allocation-free
1201/// steady-state call, use the equivalent [`TrackerContext`] method.
1202///
1203/// # Arguments
1204/// * `prev_pyramid` / `next_pyramid` - frame pyramids (shared by both passes)
1205/// * `prev_points` - points to track (level-0 coordinates)
1206/// * `predicted` - optional initial guess for the forward pass, see
1207///   [`calc_optical_flow_ex`]
1208/// * `params` - as in [`calc_optical_flow_ex`]
1209/// * `fb_threshold` - maximum allowed round-trip distance in pixels; see
1210///   [`DEFAULT_FB_THRESHOLD`]
1211///
1212/// # Panics
1213/// As [`calc_optical_flow_ex`].
1214pub fn calc_optical_flow_fb(
1215    prev_pyramid: &[GrayImage],
1216    next_pyramid: &[GrayImage],
1217    prev_points: &[(f32, f32)],
1218    predicted: Option<&[(f32, f32)]>,
1219    params: TrackParams,
1220    fb_threshold: f32,
1221) -> Vec<TrackResult> {
1222    let mut scratch = Scratch::default();
1223    let mut forward = Vec::new();
1224    let kernel = GrayImage::select(TrackKernel::Auto, params.window_size);
1225    track_into(
1226        prev_pyramid,
1227        &mut Gradients::default(),
1228        next_pyramid,
1229        prev_points,
1230        predicted,
1231        params,
1232        None,
1233        kernel,
1234        &mut scratch,
1235        &mut forward,
1236    );
1237
1238    let forward_pos: Vec<(f32, f32)> = forward.iter().map(|r| r.pos).collect();
1239    let mut backward = Vec::new();
1240    // Seed the backward pass at the original points (the round-trip is expected
1241    // to return there). This keeps the check robust under large motion, as in
1242    // OpenCV's OPTFLOW_USE_INITIAL_FLOW reverse check, without weakening it: a
1243    // genuinely wrong forward match still fails to land back within threshold.
1244    track_into(
1245        next_pyramid,
1246        &mut Gradients::default(),
1247        prev_pyramid,
1248        &forward_pos,
1249        Some(prev_points),
1250        params,
1251        None,
1252        kernel,
1253        &mut scratch,
1254        &mut backward,
1255    );
1256
1257    mark_fb_inconsistent(&mut forward, &backward, prev_points, fb_threshold);
1258    forward
1259}
1260
1261/// Flag forward-tracked points whose backward round-trip exceeds the threshold.
1262/// Factored out so the [`TrackerContext`] path can reuse it without allocating.
1263fn mark_fb_inconsistent(
1264    forward: &mut [TrackResult],
1265    backward: &[TrackResult],
1266    prev_points: &[(f32, f32)],
1267    fb_threshold: f32,
1268) {
1269    let threshold_sq = fb_threshold * fb_threshold;
1270    for (idx, result) in forward.iter_mut().enumerate() {
1271        // Only forward-tracked points are eligible; others keep their status.
1272        if result.status != TrackStatus::Tracked {
1273            continue;
1274        }
1275
1276        let back = &backward[idx];
1277        let dx = back.pos.0 - prev_points[idx].0;
1278        let dy = back.pos.1 - prev_points[idx].1;
1279        if back.status != TrackStatus::Tracked || dx * dx + dy * dy > threshold_sq {
1280            result.status = TrackStatus::FbInconsistent;
1281        }
1282    }
1283}
1284
1285/// Minimum eigenvalue of the symmetric 2x2 matrix `[[a, b], [b, c]]`.
1286fn min_eigenvalue(a: f32, b: f32, c: f32) -> f32 {
1287    let trace = a + c;
1288    let det = a * c - b * b;
1289    let disc = (trace * trace - 4.0 * det).max(0.0).sqrt();
1290    (trace - disc) / 2.0
1291}
1292
1293fn invert_2x2(a00: f32, a01: f32, a11: f32, det_epsilon: f32) -> Option<(f32, f32, f32)> {
1294    let det = a00 * a11 - a01 * a01;
1295    if det.abs() <= det_epsilon {
1296        return None;
1297    }
1298
1299    let inv_det = 1.0 / det;
1300    Some((a11 * inv_det, -a01 * inv_det, a00 * inv_det))
1301}
1302
1303/// What the spatial gradient matrix of a window allows.
1304enum Spatial {
1305    /// The matrix inverse `(h00, h01, h11)`.
1306    Invertible((f32, f32, f32)),
1307    /// Too flat to track; `measure_error` tells whether the residual is
1308    /// still reported.
1309    LowTexture { measure_error: bool },
1310}
1311
1312/// A pyramid level the Lucas-Kanade loop can track on: 8-bit images with
1313/// i16 Scharr gradients, or f32 images with f32 gradients.
1314trait TrackImage: Sized {
1315    type Pixel: Copy + Into<f32>;
1316    type Gradient: Copy + Into<f32> + Default;
1317    /// The window kernels a pass can run with.
1318    type Kernel: Copy;
1319
1320    fn dimensions(&self) -> (u32, u32);
1321
1322    fn pixels(&self) -> &[Self::Pixel];
1323
1324    /// Scharr gradients, 32 times the slope per pixel, zero on the border.
1325    fn gradients_into(&self, grad_x: &mut [Self::Gradient], grad_y: &mut [Self::Gradient]);
1326
1327    /// Classifies the spatial gradient matrix `[[gxx, gxy], [gxy, gyy]]`,
1328    /// summed over `n_pixels` window samples of slopes per pixel.
1329    fn spatial_inverse(gxx: f32, gxy: f32, gyy: f32, n_pixels: usize, threshold: f32) -> Spatial;
1330
1331    /// The kernel for `choice` and windows of `window_size` on this CPU.
1332    fn select(choice: TrackKernel, window_size: usize) -> Self::Kernel;
1333
1334    /// Refines every point's displacement at one level with `kernel`.
1335    fn track_level(
1336        kernel: Self::Kernel,
1337        level: &Level<Self>,
1338        prev_points: &[(f32, f32)],
1339        scratch: &mut Scratch,
1340        out: &mut [TrackResult],
1341    );
1342}
1343
1344impl TrackImage for GrayImage {
1345    type Pixel = u8;
1346    type Gradient = i16;
1347    type Kernel = ByteKernel;
1348
1349    fn dimensions(&self) -> (u32, u32) {
1350        ImageBuffer::dimensions(self)
1351    }
1352
1353    fn select(choice: TrackKernel, window_size: usize) -> ByteKernel {
1354        let fixed = FixedKernel::detect();
1355        let fixed_point = window_size <= fixed::MAX_WINDOW
1356            && match choice {
1357                TrackKernel::Auto => !matches!(fixed, FixedKernel::Scalar),
1358                TrackKernel::FixedPoint => true,
1359                TrackKernel::Reference => false,
1360            };
1361        if fixed_point {
1362            ByteKernel::Fixed(fixed)
1363        } else {
1364            ByteKernel::Reference(Kernel::detect())
1365        }
1366    }
1367
1368    fn track_level(
1369        kernel: ByteKernel,
1370        level: &Level<Self>,
1371        prev_points: &[(f32, f32)],
1372        scratch: &mut Scratch,
1373        out: &mut [TrackResult],
1374    ) {
1375        match kernel {
1376            ByteKernel::Reference(kernel) => kernel.track_level(level, prev_points, scratch, out),
1377            ByteKernel::Fixed(kernel) => kernel.track_level(level, prev_points, scratch, out),
1378        }
1379    }
1380
1381    fn pixels(&self) -> &[u8] {
1382        self.as_raw()
1383    }
1384
1385    fn gradients_into(&self, grad_x: &mut [i16], grad_y: &mut [i16]) {
1386        compute_gradients_into(self, grad_x, grad_y);
1387    }
1388
1389    #[inline(always)]
1390    fn spatial_inverse(gxx: f32, gxy: f32, gyy: f32, n_pixels: usize, threshold: f32) -> Spatial {
1391        // Structured like OpenCV's calcOpticalFlowPyrLK: the smaller
1392        // eigenvalue per window sample, (trace - discriminant) / 2 in f32,
1393        // against the threshold, then a determinant test. It is kept so that
1394        // 8-bit results stay comparable with OpenCV and earlier releases.
1395        // As in OpenCV, the subtraction cancels for edge-like windows; see
1396        // DEFAULT_MIN_EIGEN_THRESHOLD for the resulting noise. Unlike OpenCV,
1397        // the discriminant is trace^2 - 4 det rather than (a - c)^2 + 4 b^2,
1398        // which costs precision only for nearly isotropic windows, below
1399        // 1e-3 relative, too little to move a decision. Sums and the
1400        // determinant bound are in (grey levels / pixel)^2, not OpenCV's
1401        // fixed-point scale. The f32 tracker uses a scale-free test instead.
1402        let min_eig = min_eigenvalue(gxx, gxy, gyy) / n_pixels as f32;
1403        if min_eig < threshold {
1404            return Spatial::LowTexture {
1405                measure_error: true,
1406            };
1407        }
1408        match invert_2x2(gxx, gxy, gyy, 1e-6) {
1409            Some(inverse) => Spatial::Invertible(inverse),
1410            None => Spatial::LowTexture {
1411                measure_error: false,
1412            },
1413        }
1414    }
1415}
1416
1417impl TrackImage for FloatPyramidLevel {
1418    type Pixel = f32;
1419    type Gradient = f32;
1420    type Kernel = Kernel;
1421
1422    fn dimensions(&self) -> (u32, u32) {
1423        ImageBuffer::dimensions(self)
1424    }
1425
1426    fn select(_: TrackKernel, _: usize) -> Kernel {
1427        Kernel::detect()
1428    }
1429
1430    fn track_level(
1431        kernel: Kernel,
1432        level: &Level<Self>,
1433        prev_points: &[(f32, f32)],
1434        scratch: &mut Scratch,
1435        out: &mut [TrackResult],
1436    ) {
1437        kernel.track_level(level, prev_points, scratch, out)
1438    }
1439
1440    fn pixels(&self) -> &[f32] {
1441        self.as_raw()
1442    }
1443
1444    fn gradients_into(&self, grad_x: &mut [f32], grad_y: &mut [f32]) {
1445        let (width, height) = ImageBuffer::dimensions(self);
1446        scharr_f32(
1447            self.as_raw(),
1448            width as usize,
1449            height as usize,
1450            grad_x,
1451            grad_y,
1452        );
1453    }
1454
1455    #[inline(always)]
1456    fn spatial_inverse(gxx: f32, gxy: f32, gyy: f32, n_pixels: usize, threshold: f32) -> Spatial {
1457        // Physical units can make every entry tiny, so no test here uses an
1458        // absolute epsilon. In f64, det / lambda_max has no cancellation, and
1459        // an eigenvalue ratio below f32 resolution means a singular matrix
1460        // at any scale.
1461        let (a, b, c) = (gxx as f64, gyy as f64, gxy as f64);
1462        let determinant = a * b - c * c;
1463        let largest = (a + b + ((a - b).powi(2) + 4.0 * c * c).sqrt()) * 0.5;
1464        let smallest = if determinant > 0.0 {
1465            determinant / largest
1466        } else {
1467            0.0
1468        };
1469        let resolvable = smallest > largest * f32::EPSILON as f64;
1470        if !resolvable || smallest / (n_pixels as f64) < threshold as f64 {
1471            return Spatial::LowTexture {
1472                measure_error: true,
1473            };
1474        }
1475        let inverse = 1.0 / determinant;
1476        Spatial::Invertible((
1477            (b * inverse) as f32,
1478            (-c * inverse) as f32,
1479            (a * inverse) as f32,
1480        ))
1481    }
1482}
1483
1484/// Upper bounds for the work a tracker context will see, used to allocate
1485/// every buffer up front with `with_capacity` or `reserve` of
1486/// [`TrackerContext`] and [`FloatTrackerContext`].
1487#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1488pub struct TrackerCapacity {
1489    /// Frame width in pixels.
1490    pub width: u32,
1491    /// Frame height in pixels.
1492    pub height: u32,
1493    /// Pyramid levels passed to `prepare` / `advance`.
1494    pub levels: usize,
1495    /// Largest `window_size` passed to `track` / `track_fb`.
1496    pub window_size: usize,
1497    /// Most points per `track` / `track_fb` call.
1498    pub points: usize,
1499}
1500
1501/// Pyramids, their gradients, scratch and result buffers of a tracker
1502/// context, generic over the pyramid level type.
1503#[derive(Default)]
1504struct Tracker<I: TrackImage> {
1505    prev_pyramid: Vec<I>,
1506    next_pyramid: Vec<I>,
1507    /// Gradients of `prev_pyramid`, which the forward pass tracks from.
1508    prev_gradients: Gradients<I::Gradient>,
1509    /// Gradients of `next_pyramid`, which the backward pass tracks from and
1510    /// which become `prev_gradients` when `advance` reuses the pyramid.
1511    next_gradients: Gradients<I::Gradient>,
1512    /// Level count of `next_pyramid` while it holds the pyramid of the last
1513    /// next frame, which `advance` can then reuse. `None` before the first
1514    /// frame and after `reserve` reshaped the base level.
1515    next_levels: Option<usize>,
1516    scratch: Scratch,
1517    results: Vec<TrackResult>,
1518    forward_pos: Vec<(f32, f32)>,
1519    backward: Vec<TrackResult>,
1520}
1521
1522impl<P> Tracker<ImageBuffer<P, Vec<P::Subpixel>>>
1523where
1524    P: Pixel,
1525    P::Subpixel: Default,
1526    ImageBuffer<P, Vec<P::Subpixel>>: TrackImage,
1527{
1528    /// Allocates both pyramids and every buffer for `capacity`.
1529    fn reserve(&mut self, capacity: &TrackerCapacity) {
1530        let TrackerCapacity {
1531            width,
1532            height,
1533            levels,
1534            points,
1535            ..
1536        } = *capacity;
1537        reserve_pyramid(&mut self.prev_pyramid, width, height, levels);
1538        let base = self.next_pyramid.first().map(|level| level.dimensions());
1539        reserve_pyramid(&mut self.next_pyramid, width, height, levels);
1540        if self.next_pyramid.first().map(|level| level.dimensions()) != base {
1541            // The base level was reshaped and no longer holds the last frame.
1542            self.next_levels = None;
1543        }
1544        // Levels may have been reshaped, so their gradients are recomputed
1545        // when next needed.
1546        for gradients in [&mut self.prev_gradients, &mut self.next_gradients] {
1547            gradients.reserve(width, height, levels);
1548            gradients.invalidate();
1549        }
1550        self.scratch.reserve(capacity);
1551        reserve_total(&mut self.results, points);
1552        reserve_total(&mut self.forward_pos, points);
1553        reserve_total(&mut self.backward, points);
1554    }
1555}
1556
1557impl<I: TrackImage> Tracker<I> {
1558    /// Records that both pyramids were just built from a frame pair.
1559    fn built_both(&mut self) {
1560        self.prev_gradients.invalidate();
1561        self.next_gradients.invalidate();
1562        self.built_next();
1563    }
1564
1565    /// Records that `next_pyramid` was just built from a frame.
1566    fn built_next(&mut self) {
1567        self.next_gradients.invalidate();
1568        self.next_levels = Some(self.next_pyramid.len());
1569    }
1570
1571    /// Shared body of `advance`. `build` builds a pyramid of the new frame of
1572    /// size `dimensions`, `rebuild` one of the given base level, both with the
1573    /// caller's `levels`.
1574    fn advance(
1575        &mut self,
1576        dimensions: (u32, u32),
1577        levels: usize,
1578        build: impl Fn(&mut Vec<I>),
1579        rebuild: impl FnOnce(&I, &mut Vec<I>),
1580    ) {
1581        // The base level of the next pyramid is the last frame itself.
1582        let last_frame = self
1583            .next_levels
1584            .filter(|_| self.next_pyramid.first().map(I::dimensions) == Some(dimensions));
1585        match last_frame {
1586            Some(built)
1587                if built == self.next_pyramid.len()
1588                    && built == pyramid_level_count(dimensions.0, dimensions.1, levels) =>
1589            {
1590                // The last frame's gradients move along with its pyramid.
1591                std::mem::swap(&mut self.prev_pyramid, &mut self.next_pyramid);
1592                std::mem::swap(&mut self.prev_gradients, &mut self.next_gradients);
1593            }
1594            Some(_) => {
1595                rebuild(&self.next_pyramid[0], &mut self.prev_pyramid);
1596                self.prev_gradients.invalidate();
1597            }
1598            None => {
1599                build(&mut self.prev_pyramid);
1600                self.prev_gradients.invalidate();
1601            }
1602        }
1603        build(&mut self.next_pyramid);
1604        self.built_next();
1605    }
1606
1607    fn track(
1608        &mut self,
1609        prev_points: &[(f32, f32)],
1610        predicted: Option<&[(f32, f32)]>,
1611        params: TrackParams,
1612        options: TrackOptions,
1613    ) -> &[TrackResult] {
1614        track_into(
1615            &self.prev_pyramid,
1616            &mut self.prev_gradients,
1617            &self.next_pyramid,
1618            prev_points,
1619            predicted,
1620            params,
1621            options.start_level,
1622            I::select(options.kernel, params.window_size),
1623            &mut self.scratch,
1624            &mut self.results,
1625        );
1626        &self.results
1627    }
1628
1629    fn track_fb(
1630        &mut self,
1631        prev_points: &[(f32, f32)],
1632        predicted: Option<&[(f32, f32)]>,
1633        params: TrackParams,
1634        fb_threshold: f32,
1635        options: TrackOptions,
1636    ) -> &[TrackResult] {
1637        let backward_start = options.fb_levels.map(|levels| {
1638            assert!(levels >= 1, "fb_levels must be at least 1");
1639            levels - 1
1640        });
1641        let kernel = I::select(options.kernel, params.window_size);
1642        track_into(
1643            &self.prev_pyramid,
1644            &mut self.prev_gradients,
1645            &self.next_pyramid,
1646            prev_points,
1647            predicted,
1648            params,
1649            options.start_level,
1650            kernel,
1651            &mut self.scratch,
1652            &mut self.results,
1653        );
1654
1655        self.forward_pos.clear();
1656        self.forward_pos.extend(self.results.iter().map(|r| r.pos));
1657
1658        // Seed the backward pass at the original points (see calc_optical_flow_fb).
1659        track_into(
1660            &self.next_pyramid,
1661            &mut self.next_gradients,
1662            &self.prev_pyramid,
1663            &self.forward_pos,
1664            Some(prev_points),
1665            params,
1666            backward_start,
1667            kernel,
1668            &mut self.scratch,
1669            &mut self.backward,
1670        );
1671
1672        mark_fb_inconsistent(&mut self.results, &self.backward, prev_points, fb_threshold);
1673        &self.results
1674    }
1675}
1676
1677/// Reusable owner of every buffer the tracking hot path touches: both frame
1678/// pyramids, the Lucas-Kanade scratch, the result vector and the
1679/// forward-backward intermediates.
1680///
1681/// Create one per tracking thread. Each frame, call [`advance`](Self::advance)
1682/// with the new frame, or [`prepare`](Self::prepare) with a frame pair, then
1683/// [`track`](Self::track) / [`track_fb`](Self::track_fb). `advance` reuses
1684/// the pyramid of the last frame, so it builds one pyramid per frame instead
1685/// of two. All buffers are resized in place and reused. To keep allocation
1686/// out of the per-frame path, create the context with
1687/// [`with_capacity`](Self::with_capacity): frames of the reserved size and
1688/// level count, with at most the reserved window size and point count, then
1689/// perform **no heap allocation**. Without a reservation the first frame
1690/// allocates, and so does any later call with more points than before. The
1691/// free functions ([`calc_optical_flow_ex`], [`calc_optical_flow_fb`]) are
1692/// thin convenience wrappers that allocate their own scratch.
1693#[derive(Default)]
1694pub struct TrackerContext {
1695    tracker: Tracker<GrayImage>,
1696}
1697
1698impl TrackerContext {
1699    /// Creates an empty context. Buffers grow to fit on the first
1700    /// [`prepare`](Self::prepare) / [`track`](Self::track) call and are reused
1701    /// thereafter.
1702    pub fn new() -> Self {
1703        Self::default()
1704    }
1705
1706    /// Creates a context with every buffer allocated for `capacity`.
1707    pub fn with_capacity(capacity: TrackerCapacity) -> Self {
1708        let mut context = Self::default();
1709        context.reserve(capacity);
1710        context
1711    }
1712
1713    /// Allocates every buffer for `capacity` now, so that later calls within
1714    /// it do not allocate. Existing capacity is kept.
1715    pub fn reserve(&mut self, capacity: TrackerCapacity) {
1716        self.tracker.reserve(&capacity);
1717    }
1718
1719    /// Builds the previous- and next-frame pyramids into the context's reusable
1720    /// buffers. Zero-alloc in steady state (same image size and `levels`).
1721    ///
1722    /// # Panics
1723    /// Panics if the two frames differ in size.
1724    pub fn prepare(&mut self, prev: &GrayImage, next: &GrayImage, levels: usize) {
1725        assert_eq!(
1726            prev.dimensions(),
1727            next.dimensions(),
1728            "frames must have the same size"
1729        );
1730        build_pyramid_into(prev, levels, &mut self.tracker.prev_pyramid);
1731        build_pyramid_into(next, levels, &mut self.tracker.next_pyramid);
1732        self.tracker.built_both();
1733    }
1734
1735    /// Moves to the next frame pair: the pyramid of the last next frame
1736    /// becomes the previous-frame pyramid, by swapping buffers, and the
1737    /// next-frame pyramid is built from `frame`.
1738    ///
1739    /// The pyramids, and therefore all tracking results, equal those of
1740    /// `prepare(last_next, frame, levels)` bit for bit, where `last_next` is
1741    /// the last frame passed to `advance` or as `next` to `prepare`. Only one
1742    /// pyramid is built, and the caller does not need to keep the last frame.
1743    /// This is the preferred per-frame call. In steady state (same frame size
1744    /// and `levels`) it performs no heap allocation.
1745    ///
1746    /// When the last frame cannot be reused as is, the call still leaves both
1747    /// pyramids ready for tracking:
1748    /// - With a different `levels` than before, the previous-frame pyramid is
1749    ///   rebuilt from the last frame, which still equals
1750    ///   `prepare(last_next, frame, levels)`.
1751    /// - Without a last frame (on the first call, or after
1752    ///   [`reserve`](Self::reserve) changed the frame size) or when `frame`
1753    ///   differs in size from it, both pyramids are built from `frame`, as
1754    ///   `prepare(frame, frame, levels)` does. Tracking on that pair measures
1755    ///   no motion.
1756    ///
1757    /// ```
1758    /// # use image::GrayImage;
1759    /// # use optical_flow_lk::{DEFAULT_FB_THRESHOLD, DEFAULT_MIN_EIGEN_THRESHOLD, TrackParams, TrackerContext};
1760    /// # let frames = vec![GrayImage::new(64, 48); 3];
1761    /// # let mut points = vec![(32.0, 24.0)];
1762    /// # let params = TrackParams {
1763    /// #     window_size: 21,
1764    /// #     max_iterations: 30,
1765    /// #     min_eigen_threshold: DEFAULT_MIN_EIGEN_THRESHOLD,
1766    /// # };
1767    /// let mut ctx = TrackerContext::new();
1768    /// ctx.advance(&frames[0], 4);
1769    /// for frame in &frames[1..] {
1770    ///     ctx.advance(frame, 4);
1771    ///     let results = ctx.track_fb(&points, None, params, DEFAULT_FB_THRESHOLD);
1772    ///     points = results.iter().map(|result| result.pos).collect();
1773    /// }
1774    /// ```
1775    pub fn advance(&mut self, frame: &GrayImage, levels: usize) {
1776        self.tracker.advance(
1777            frame.dimensions(),
1778            levels,
1779            |pyramid| build_pyramid_into(frame, levels, pyramid),
1780            |base, pyramid| build_pyramid_into(base, levels, pyramid),
1781        );
1782    }
1783
1784    /// The previous-frame pyramid built by the last [`prepare`](Self::prepare)
1785    /// or [`advance`](Self::advance).
1786    pub fn prev_pyramid(&self) -> &[GrayImage] {
1787        &self.tracker.prev_pyramid
1788    }
1789
1790    /// The next-frame pyramid built by the last [`prepare`](Self::prepare) or
1791    /// [`advance`](Self::advance).
1792    pub fn next_pyramid(&self) -> &[GrayImage] {
1793        &self.tracker.next_pyramid
1794    }
1795
1796    /// Tracks `prev_points` using the prepared pyramids, returning the results
1797    /// held inside the context. See [`calc_optical_flow_ex`] for the argument
1798    /// semantics. Allocation-free in steady state.
1799    ///
1800    /// The Scharr gradients of each pyramid level are computed once per frame
1801    /// and kept with the pyramid, so `track` after `track_fb` on the same
1802    /// frames, or the next frame's forward pass after `advance`, reuse them.
1803    pub fn track(
1804        &mut self,
1805        prev_points: &[(f32, f32)],
1806        predicted: Option<&[(f32, f32)]>,
1807        params: TrackParams,
1808    ) -> &[TrackResult] {
1809        self.track_with(prev_points, predicted, params, TrackOptions::new())
1810    }
1811
1812    /// [`track`](Self::track) with [`TrackOptions`]: the level the pass starts
1813    /// at and the kernel arithmetic.
1814    pub fn track_with(
1815        &mut self,
1816        prev_points: &[(f32, f32)],
1817        predicted: Option<&[(f32, f32)]>,
1818        params: TrackParams,
1819        options: TrackOptions,
1820    ) -> &[TrackResult] {
1821        self.tracker.track(prev_points, predicted, params, options)
1822    }
1823
1824    /// Forward-backward consistent tracking using the prepared pyramids. See
1825    /// [`calc_optical_flow_fb`] for semantics. Reuses the context's scratch and
1826    /// intermediate point buffers, so it is allocation-free in steady state.
1827    pub fn track_fb(
1828        &mut self,
1829        prev_points: &[(f32, f32)],
1830        predicted: Option<&[(f32, f32)]>,
1831        params: TrackParams,
1832        fb_threshold: f32,
1833    ) -> &[TrackResult] {
1834        self.track_fb_with(
1835            prev_points,
1836            predicted,
1837            params,
1838            fb_threshold,
1839            TrackOptions::new(),
1840        )
1841    }
1842
1843    /// [`track_fb`](Self::track_fb) with [`TrackOptions`]: the level the
1844    /// forward pass starts at, the levels of the backward pass and the kernel
1845    /// arithmetic.
1846    ///
1847    /// ```
1848    /// # use image::GrayImage;
1849    /// # use optical_flow_lk::{DEFAULT_FB_THRESHOLD, DEFAULT_MIN_EIGEN_THRESHOLD, TrackOptions, TrackParams, TrackerContext};
1850    /// # let (frame, points) = (GrayImage::new(64, 48), vec![(32.0, 24.0)]);
1851    /// # let params = TrackParams {
1852    /// #     window_size: 13,
1853    /// #     max_iterations: 30,
1854    /// #     min_eigen_threshold: DEFAULT_MIN_EIGEN_THRESHOLD,
1855    /// # };
1856    /// let mut ctx = TrackerContext::new();
1857    /// ctx.advance(&frame, 4);
1858    /// // Round trip over the finest level only.
1859    /// let options = TrackOptions::new().with_fb_levels(1);
1860    /// let results = ctx.track_fb_with(&points, None, params, DEFAULT_FB_THRESHOLD, options);
1861    /// ```
1862    ///
1863    /// # Panics
1864    /// As [`track`](Self::track), and if `options.fb_levels` is `Some(0)`.
1865    pub fn track_fb_with(
1866        &mut self,
1867        prev_points: &[(f32, f32)],
1868        predicted: Option<&[(f32, f32)]>,
1869        params: TrackParams,
1870        fb_threshold: f32,
1871        options: TrackOptions,
1872    ) -> &[TrackResult] {
1873        self.tracker
1874            .track_fb(prev_points, predicted, params, fb_threshold, options)
1875    }
1876}
1877
1878/// Pyramidal Lucas-Kanade tracker for f32 images in physical units, e.g.
1879/// thermal frames in °C. The counterpart of [`TrackerContext`], owning both
1880/// frame pyramids and every buffer the tracking hot path touches.
1881///
1882/// The algorithm and its parameters match the 8-bit tracker: pyramids of
1883/// 2x2 means, Scharr gradients, bilinear window sampling, the same
1884/// convergence rules, prediction and forward-backward check, and the same
1885/// [`TrackResult`] and [`TrackStatus`]. Pixels, pyramids and gradients stay
1886/// in f32 throughout; nothing is quantized. The pyramid averages exactly
1887/// instead of truncating like the 8-bit pyramid, so on the same frames the
1888/// two trackers agree closely but not bit for bit.
1889///
1890/// Units follow the image:
1891/// - [`TrackParams::min_eigen_threshold`] is in (input unit / pixel)^2, the
1892///   unit of the f32 detector's scores, and has no default. A window whose mean minimum
1893///   gradient eigenvalue is below it is [`TrackStatus::LowTexture`]. Windows
1894///   whose eigenvalue ratio is below f32 resolution are low texture as well,
1895///   at any contrast: the singularity test is relative, so small physical
1896///   gradients still track.
1897/// - [`TrackResult::error`] is the mean absolute residual in the input unit.
1898/// - Positions, `fb_threshold` and [`CONVERGENCE_EPSILON`] are in pixels.
1899///
1900/// Per frame, call [`advance`](Self::advance) with the new frame, or
1901/// [`prepare`](Self::prepare) with a frame pair, as for [`TrackerContext`].
1902/// Create the context with [`with_capacity`](Self::with_capacity) to keep
1903/// allocation out of the per-frame path.
1904#[derive(Default)]
1905pub struct FloatTrackerContext {
1906    tracker: Tracker<FloatPyramidLevel>,
1907}
1908
1909impl FloatTrackerContext {
1910    /// Creates an empty context. Buffers grow to fit on the first call and
1911    /// are reused thereafter.
1912    pub fn new() -> Self {
1913        Self::default()
1914    }
1915
1916    /// Creates a context with every buffer allocated for `capacity`.
1917    pub fn with_capacity(capacity: TrackerCapacity) -> Self {
1918        let mut context = Self::default();
1919        context.reserve(capacity);
1920        context
1921    }
1922
1923    /// Allocates every buffer for `capacity` now, so that later calls within
1924    /// it do not allocate. Existing capacity is kept.
1925    pub fn reserve(&mut self, capacity: TrackerCapacity) {
1926        self.tracker.reserve(&capacity);
1927    }
1928
1929    /// Builds the previous- and next-frame pyramids with up to `levels`
1930    /// levels; halving stops below 2 pixels.
1931    ///
1932    /// # Panics
1933    /// Panics if the two frames differ in size.
1934    pub fn prepare(&mut self, prev: FloatImageView<'_>, next: FloatImageView<'_>, levels: usize) {
1935        assert_eq!(
1936            prev.dimensions(),
1937            next.dimensions(),
1938            "frames must have the same size"
1939        );
1940        build_float_pyramid_into(prev, levels, &mut self.tracker.prev_pyramid);
1941        build_float_pyramid_into(next, levels, &mut self.tracker.next_pyramid);
1942        self.tracker.built_both();
1943    }
1944
1945    /// Moves to the next frame pair: the pyramid of the last next frame
1946    /// becomes the previous-frame pyramid, by swapping buffers, and the
1947    /// next-frame pyramid is built from `frame`. The results equal those of
1948    /// `prepare(last_next, frame, levels)` bit for bit with one pyramid build
1949    /// instead of two, and the steady state performs no heap allocation.
1950    /// Without a reusable last frame, the pyramids are built as described
1951    /// for [`TrackerContext::advance`].
1952    pub fn advance(&mut self, frame: FloatImageView<'_>, levels: usize) {
1953        self.tracker.advance(
1954            frame.dimensions(),
1955            levels,
1956            |pyramid| build_float_pyramid_into(frame, levels, pyramid),
1957            |base, pyramid| {
1958                let (width, height) = base.dimensions();
1959                let base = FloatImageView::new(base.as_raw(), width, height)
1960                    .expect("the base level is a copy of a validated frame");
1961                build_float_pyramid_into(base, levels, pyramid);
1962            },
1963        );
1964    }
1965
1966    /// The previous-frame pyramid built by the last [`prepare`](Self::prepare)
1967    /// or [`advance`](Self::advance).
1968    pub fn prev_pyramid(&self) -> &[FloatPyramidLevel] {
1969        &self.tracker.prev_pyramid
1970    }
1971
1972    /// The next-frame pyramid built by the last [`prepare`](Self::prepare) or
1973    /// [`advance`](Self::advance).
1974    pub fn next_pyramid(&self) -> &[FloatPyramidLevel] {
1975        &self.tracker.next_pyramid
1976    }
1977
1978    /// Tracks `prev_points` from the previous into the next frame, as
1979    /// [`calc_optical_flow_ex`] does. `params.min_eigen_threshold` is in
1980    /// (input unit / pixel)^2. Returns one result per point, in order, from
1981    /// the context's own buffer.
1982    ///
1983    /// # Panics
1984    /// Panics if `prepare` was not called, `params.window_size` is even, or
1985    /// `predicted` has a different length than `prev_points`.
1986    pub fn track(
1987        &mut self,
1988        prev_points: &[(f32, f32)],
1989        predicted: Option<&[(f32, f32)]>,
1990        params: TrackParams,
1991    ) -> &[TrackResult] {
1992        self.track_with(prev_points, predicted, params, TrackOptions::new())
1993    }
1994
1995    /// [`track`](Self::track) with [`TrackOptions`]. The f32 tracker always
1996    /// uses its f32 kernels, so only `start_level` applies.
1997    pub fn track_with(
1998        &mut self,
1999        prev_points: &[(f32, f32)],
2000        predicted: Option<&[(f32, f32)]>,
2001        params: TrackParams,
2002        options: TrackOptions,
2003    ) -> &[TrackResult] {
2004        self.tracker.track(prev_points, predicted, params, options)
2005    }
2006
2007    /// Forward-backward consistent tracking, as [`calc_optical_flow_fb`]
2008    /// does; `fb_threshold` is in pixels.
2009    ///
2010    /// # Panics
2011    /// As [`track`](Self::track).
2012    pub fn track_fb(
2013        &mut self,
2014        prev_points: &[(f32, f32)],
2015        predicted: Option<&[(f32, f32)]>,
2016        params: TrackParams,
2017        fb_threshold: f32,
2018    ) -> &[TrackResult] {
2019        self.track_fb_with(
2020            prev_points,
2021            predicted,
2022            params,
2023            fb_threshold,
2024            TrackOptions::new(),
2025        )
2026    }
2027
2028    /// [`track_fb`](Self::track_fb) with [`TrackOptions`]: the level the
2029    /// forward pass starts at and the levels of the backward pass.
2030    ///
2031    /// # Panics
2032    /// As [`track`](Self::track), and if `options.fb_levels` is `Some(0)`.
2033    pub fn track_fb_with(
2034        &mut self,
2035        prev_points: &[(f32, f32)],
2036        predicted: Option<&[(f32, f32)]>,
2037        params: TrackParams,
2038        fb_threshold: f32,
2039        options: TrackOptions,
2040    ) -> &[TrackResult] {
2041        self.tracker
2042            .track_fb(prev_points, predicted, params, fb_threshold, options)
2043    }
2044}
2045
2046#[cfg(test)]
2047mod tests {
2048    use super::*;
2049    use crate::pyramid::build_pyramid;
2050    use image::Luma;
2051
2052    #[test]
2053    fn invert_2x2_returns_inverse_components() {
2054        let (inv00, inv01, inv11) = invert_2x2(4.0, 1.0, 3.0, 1e-6).unwrap();
2055
2056        assert!((inv00 - 3.0 / 11.0).abs() < 1e-6);
2057        assert!((inv01 + 1.0 / 11.0).abs() < 1e-6);
2058        assert!((inv11 - 4.0 / 11.0).abs() < 1e-6);
2059    }
2060
2061    #[test]
2062    fn invert_2x2_rejects_singular_matrix() {
2063        assert!(invert_2x2(1.0, 2.0, 4.0, 1e-6).is_none());
2064    }
2065
2066    /// Reference bilinear sample with the crate's pixel-center convention.
2067    fn bilinear(img: &GrayImage, x: f32, y: f32) -> f32 {
2068        let (x0, y0) = (x.floor(), y.floor());
2069        let (fx, fy) = (x - x0, y - y0);
2070        let px = |dx: u32, dy: u32| img.get_pixel(x0 as u32 + dx, y0 as u32 + dy)[0] as f32;
2071        px(0, 0) * (1.0 - fx) * (1.0 - fy)
2072            + px(1, 0) * fx * (1.0 - fy)
2073            + px(0, 1) * (1.0 - fx) * fy
2074            + px(1, 1) * fx * fy
2075    }
2076
2077    #[test]
2078    fn window_sampling_matches_per_pixel_bilinear_reference() {
2079        let img = GrayImage::from_fn(40, 30, |x, y| Luma([((x * 37 + y * 11) ^ (x * y)) as u8]));
2080        let mut grad_x = vec![0i16; 40 * 30];
2081        let mut grad_y = vec![0i16; 40 * 30];
2082        compute_gradients_into(&img, &mut grad_x, &mut grad_y);
2083
2084        for &size in &[1usize, 5, 7, 11, 21] {
2085            let radius = size / 2;
2086            let lv = Level::<GrayImage> {
2087                prev: img.as_raw(),
2088                curr: img.as_raw(),
2089                grad_x: &grad_x,
2090                grad_y: &grad_y,
2091                width: 40,
2092                height: 30,
2093                scale: 1.0,
2094                is_finest: true,
2095                window_size: size,
2096                radius,
2097                max_iterations: 1,
2098                min_eigen_threshold: 0.0,
2099            };
2100            let (cx, cy) = (17.3f32, 14.6f32);
2101            let win = Window::new(cx, cy, radius, 40, 30).unwrap();
2102            let mut patch = vec![0f32; size * size];
2103            let mut ix = vec![0f32; size * size];
2104            let mut iy = vec![0f32; size * size];
2105            build_patch(&lv, &win, &mut patch, &mut ix, &mut iy);
2106
2107            for j in 0..size {
2108                for i in 0..size {
2109                    let (sx, sy) = (
2110                        cx + (i as f32 - radius as f32),
2111                        cy + (j as f32 - radius as f32),
2112                    );
2113                    let expected = bilinear(&img, sx, sy);
2114                    let got = patch[j * size + i];
2115                    assert!(
2116                        (got - expected).abs() < 1e-3,
2117                        "size {size} at ({i},{j}): {got} vs {expected}"
2118                    );
2119                }
2120            }
2121            // The next image is the previous image, so the residual at the same
2122            // position is zero and a step away from it is not.
2123            assert!(window_error(&lv, &win, &patch) < 1e-4);
2124            let (bx, by) = lk_step(&lv, &win, &patch, &ix, &iy);
2125            assert!(
2126                bx.abs() < 1e-2 && by.abs() < 1e-2,
2127                "size {size}: ({bx}, {by})"
2128            );
2129        }
2130    }
2131
2132    /// Every fixed-point kernel this CPU runs.
2133    fn fixed_kernels() -> Vec<FixedKernel> {
2134        #[allow(unused_mut)]
2135        let mut kernels = vec![FixedKernel::Scalar, FixedKernel::BASELINE];
2136        #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
2137        if let Some(avx2) = fixed::Avx2::detect() {
2138            kernels.push(FixedKernel::Avx2(avx2));
2139        }
2140        kernels
2141    }
2142
2143    /// Smoothed hash noise, `w` x `h`, shifted by `(sx, sy)` pixels.
2144    fn texture(w: u32, h: u32, (sx, sy): (f32, f32)) -> GrayImage {
2145        let noise = |x: i32, y: i32| {
2146            let mut s = (y.rem_euclid(1024) as u32 * 1024 + x.rem_euclid(1024) as u32)
2147                .wrapping_mul(2654435761);
2148            s ^= s >> 15;
2149            s = s.wrapping_mul(0x85ebca6b);
2150            (s >> 24) as f32
2151        };
2152        let smooth = |x: f32, y: f32| {
2153            let (x0, y0) = (x.floor(), y.floor());
2154            let (fx, fy) = (x - x0, y - y0);
2155            let mut total = 0.0;
2156            for dy in -2..=3 {
2157                for dx in -2..=3 {
2158                    let wx = if dx == -2 {
2159                        1.0 - fx
2160                    } else if dx == 3 {
2161                        fx
2162                    } else {
2163                        1.0
2164                    };
2165                    let wy = if dy == -2 {
2166                        1.0 - fy
2167                    } else if dy == 3 {
2168                        fy
2169                    } else {
2170                        1.0
2171                    };
2172                    total += wx * wy * noise(x0 as i32 + dx, y0 as i32 + dy);
2173                }
2174            }
2175            total / 25.0
2176        };
2177        GrayImage::from_fn(w, h, |x, y| {
2178            Luma([smooth(x as f32 - sx, y as f32 - sy) as u8])
2179        })
2180    }
2181
2182    #[test]
2183    fn fixed_point_kernels_track_identically() {
2184        for (w, h, shift) in [(97u32, 71u32, (1.7f32, -0.6f32)), (160, 120, (4.25, 3.5))] {
2185            let prev = build_pyramid(&texture(w, h, (0.0, 0.0)), 3);
2186            let next = build_pyramid(&texture(w, h, shift), 3);
2187            // A lattice reaching into the borders, where windows fall back
2188            // to the scalar kernel and leave the frame.
2189            let points: Vec<(f32, f32)> = (0..90)
2190                .map(|i| {
2191                    (
2192                        1.5 + (i % 10) as f32 * w as f32 / 9.6,
2193                        2.0 + (i / 10) as f32 * h as f32 / 8.7,
2194                    )
2195                })
2196                .collect();
2197            let predicted: Vec<(f32, f32)> = points
2198                .iter()
2199                .map(|&(x, y)| (x + shift.0, y + shift.1))
2200                .collect();
2201            for window_size in [1, 7, 13, 21, 31] {
2202                let params = TrackParams {
2203                    window_size,
2204                    max_iterations: 20,
2205                    min_eigen_threshold: DEFAULT_MIN_EIGEN_THRESHOLD,
2206                };
2207                for guess in [None, Some(predicted.as_slice())] {
2208                    let run = |kernel| {
2209                        let mut out = Vec::new();
2210                        track_into(
2211                            &prev,
2212                            &mut Gradients::default(),
2213                            &next,
2214                            &points,
2215                            guess,
2216                            params,
2217                            None,
2218                            ByteKernel::Fixed(kernel),
2219                            &mut Scratch::default(),
2220                            &mut out,
2221                        );
2222                        out.iter()
2223                            .map(|r| {
2224                                (
2225                                    r.pos.0.to_bits(),
2226                                    r.pos.1.to_bits(),
2227                                    r.status,
2228                                    r.error.to_bits(),
2229                                )
2230                            })
2231                            .collect::<Vec<_>>()
2232                    };
2233                    let kernels = fixed_kernels();
2234                    let scalar = run(kernels[0]);
2235                    let tracked = scalar
2236                        .iter()
2237                        .filter(|r| r.2 == TrackStatus::Tracked)
2238                        .count();
2239                    assert!(window_size == 1 || tracked > 20, "{tracked} tracked");
2240                    for &kernel in &kernels[1..] {
2241                        assert_eq!(
2242                            run(kernel),
2243                            scalar,
2244                            "{kernel:?}, {w}x{h}, window {window_size}"
2245                        );
2246                    }
2247                }
2248            }
2249        }
2250    }
2251
2252    #[test]
2253    fn window_rejects_footprints_touching_the_border() {
2254        assert!(Window::new(10.0, 10.0, 10, 40, 40).is_some());
2255        assert!(Window::new(9.9, 10.0, 10, 40, 40).is_none());
2256        assert!(Window::new(28.9, 10.0, 10, 40, 40).is_some());
2257        assert!(Window::new(29.0, 10.0, 10, 40, 40).is_none());
2258        assert!(Window::new(f32::NAN, 10.0, 10, 40, 40).is_none());
2259    }
2260}