use image::{GrayImage, ImageBuffer, Luma, Pixel};
use crate::features::reserve_total;
use crate::float_image::FloatImageView;
use crate::pyramid::{build_float_pyramid_into, build_pyramid_into, reserve_pyramid};
use crate::utils::fast_gradients::compute_gradients_into;
use crate::utils::float_gradients::scharr_f32;
pub type FloatPyramidLevel = ImageBuffer<Luma<f32>, Vec<f32>>;
pub const DEFAULT_MIN_EIGEN_THRESHOLD: f32 = 1e-3;
pub const CONVERGENCE_EPSILON: f32 = 0.01;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TrackStatus {
Tracked,
OutOfBounds,
Diverged,
LowTexture,
FbInconsistent,
}
pub const DEFAULT_FB_THRESHOLD: f32 = 0.7;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct TrackParams {
pub window_size: usize,
pub max_iterations: usize,
pub min_eigen_threshold: f32,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TrackResult {
pub pos: (f32, f32),
pub status: TrackStatus,
pub error: f32,
}
pub fn calc_optical_flow_ex(
prev_pyramid: &[GrayImage],
curr_pyramid: &[GrayImage],
prev_points: &[(f32, f32)],
predicted: Option<&[(f32, f32)]>,
params: TrackParams,
) -> Vec<TrackResult> {
let mut scratch = Scratch::default();
let mut out = Vec::new();
track_into(
prev_pyramid,
curr_pyramid,
prev_points,
predicted,
params,
&mut scratch,
&mut out,
);
out
}
#[derive(Default)]
struct Scratch<G> {
prev_patch: Vec<f32>,
ix_patch: Vec<f32>,
iy_patch: Vec<f32>,
displacements: Vec<(f32, f32)>,
grad_x: Vec<G>,
grad_y: Vec<G>,
}
impl<G> Scratch<G> {
fn reserve(&mut self, capacity: &TrackerCapacity) {
let patch = capacity.window_size * capacity.window_size;
for buffer in [&mut self.prev_patch, &mut self.ix_patch, &mut self.iy_patch] {
reserve_total(buffer, patch);
}
reserve_total(&mut self.displacements, capacity.points);
let pixels = capacity.width as usize * capacity.height as usize;
reserve_total(&mut self.grad_x, pixels);
reserve_total(&mut self.grad_y, pixels);
}
}
fn track_into<I: TrackImage>(
prev_pyramid: &[I],
curr_pyramid: &[I],
prev_points: &[(f32, f32)],
predicted: Option<&[(f32, f32)]>,
params: TrackParams,
scratch: &mut Scratch<I::Gradient>,
out: &mut Vec<TrackResult>,
) {
assert_eq!(prev_pyramid.len(), curr_pyramid.len());
assert!(
!prev_pyramid.is_empty(),
"pyramid must have at least 1 level"
);
assert!(
prev_pyramid
.iter()
.zip(curr_pyramid)
.all(|(prev, curr)| prev.dimensions() == curr.dimensions()),
"both pyramids must have the same level sizes"
);
let TrackParams {
window_size,
max_iterations,
min_eigen_threshold,
} = params;
assert!(window_size % 2 == 1, "Window size must be odd");
if let Some(predicted) = predicted {
assert_eq!(
predicted.len(),
prev_points.len(),
"predicted must have one entry per prev_point"
);
}
let n_levels = prev_pyramid.len();
let radius = window_size / 2;
let n_pixels = window_size * window_size;
let Scratch {
prev_patch,
ix_patch,
iy_patch,
displacements,
grad_x: grad_x_buf,
grad_y: grad_y_buf,
} = scratch;
prev_patch.resize(n_pixels, 0.0);
ix_patch.resize(n_pixels, 0.0);
iy_patch.resize(n_pixels, 0.0);
displacements.clear();
match predicted {
Some(predicted) => displacements.extend(
prev_points
.iter()
.zip(predicted.iter())
.map(|((px, py), (gx, gy))| (gx - px, gy - py)),
),
None => displacements.resize(prev_points.len(), (0.0, 0.0)),
}
let (w0, h0) = prev_pyramid[0].dimensions();
grad_x_buf.resize((w0 * h0) as usize, Default::default());
grad_y_buf.resize((w0 * h0) as usize, Default::default());
out.clear();
out.extend(prev_points.iter().map(|&(x, y)| TrackResult {
pos: (x, y),
status: TrackStatus::Tracked,
error: f32::INFINITY,
}));
let kernel = Kernel::detect();
for level in (0..n_levels).rev() {
let prev_img = &prev_pyramid[level];
let curr_img = &curr_pyramid[level];
let (lw, lh) = prev_img.dimensions();
let level_pixels = (lw * lh) as usize;
prev_img.gradients_into(
&mut grad_x_buf[..level_pixels],
&mut grad_y_buf[..level_pixels],
);
let level = Level::<I> {
prev: prev_img.pixels(),
curr: curr_img.pixels(),
grad_x: &grad_x_buf[..level_pixels],
grad_y: &grad_y_buf[..level_pixels],
width: lw as usize,
height: lh as usize,
scale: 2f32.powi(level as i32),
is_finest: level == 0,
window_size,
radius,
max_iterations,
min_eigen_threshold,
};
kernel.track_level(
&level,
prev_points,
displacements,
prev_patch,
ix_patch,
iy_patch,
out,
);
}
for (idx, (x, y)) in prev_points.iter().enumerate() {
let (dx, dy) = displacements[idx];
out[idx].pos = (x + dx, y + dy);
}
}
struct Level<'a, I: TrackImage> {
prev: &'a [I::Pixel],
curr: &'a [I::Pixel],
grad_x: &'a [I::Gradient],
grad_y: &'a [I::Gradient],
width: usize,
height: usize,
scale: f32,
is_finest: bool,
window_size: usize,
radius: usize,
max_iterations: usize,
min_eigen_threshold: f32,
}
#[derive(Clone, Copy)]
enum Kernel {
Portable,
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
Avx2,
}
impl Kernel {
fn detect() -> Self {
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
if is_x86_feature_detected!("avx2") {
return Kernel::Avx2;
}
Kernel::Portable
}
#[allow(clippy::too_many_arguments)]
fn track_level<I: TrackImage>(
self,
level: &Level<I>,
prev_points: &[(f32, f32)],
displacements: &mut [(f32, f32)],
prev_patch: &mut [f32],
ix_patch: &mut [f32],
iy_patch: &mut [f32],
out: &mut [TrackResult],
) {
match self {
Kernel::Portable => track_level_impl(
level,
prev_points,
displacements,
prev_patch,
ix_patch,
iy_patch,
out,
),
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
Kernel::Avx2 => unsafe {
track_level_avx2(
level,
prev_points,
displacements,
prev_patch,
ix_patch,
iy_patch,
out,
)
},
}
}
}
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
#[target_feature(enable = "avx2")]
#[allow(clippy::too_many_arguments)]
unsafe fn track_level_avx2<I: TrackImage>(
level: &Level<I>,
prev_points: &[(f32, f32)],
displacements: &mut [(f32, f32)],
prev_patch: &mut [f32],
ix_patch: &mut [f32],
iy_patch: &mut [f32],
out: &mut [TrackResult],
) {
track_level_impl(
level,
prev_points,
displacements,
prev_patch,
ix_patch,
iy_patch,
out,
)
}
#[inline(always)]
#[allow(clippy::too_many_arguments)]
fn track_level_impl<I: TrackImage>(
lv: &Level<I>,
prev_points: &[(f32, f32)],
displacements: &mut [(f32, f32)],
prev_patch: &mut [f32],
ix_patch: &mut [f32],
iy_patch: &mut [f32],
out: &mut [TrackResult],
) {
let n_pixels = lv.window_size * lv.window_size;
let window_limit = lv.window_size as f32;
for (idx, &(prev_x, prev_y)) in prev_points.iter().enumerate() {
let x = prev_x / lv.scale;
let y = prev_y / lv.scale;
let mut dx = displacements[idx].0 / lv.scale;
let mut dy = displacements[idx].1 / lv.scale;
let Some(prev_win) = Window::new(x, y, lv.radius, lv.width, lv.height) else {
out[idx].status = TrackStatus::OutOfBounds;
continue;
};
let (gxx, gxy, gyy) = build_patch(lv, &prev_win, prev_patch, ix_patch, iy_patch);
let (inv_h00, inv_h01, inv_h11) =
match I::spatial_inverse(gxx, gxy, gyy, n_pixels, lv.min_eigen_threshold) {
Spatial::Invertible(inverse) => inverse,
Spatial::LowTexture { measure_error } => {
out[idx].status = TrackStatus::LowTexture;
if measure_error && lv.is_finest {
out[idx].error = window_error(lv, x + dx, y + dy, prev_patch);
}
continue;
}
};
let mut converged = false;
let mut out_of_bounds = false;
let mut diverged = false;
let mut prev_step = (0.0f32, 0.0f32);
for iteration in 0..lv.max_iterations {
let Some(win) = Window::new(x + dx, y + dy, lv.radius, lv.width, lv.height) else {
out_of_bounds = true;
break;
};
let (bx, by) = lk_step(lv, &win, prev_patch, ix_patch, iy_patch);
let ddx = inv_h00 * bx + inv_h01 * by;
let ddy = inv_h01 * bx + inv_h11 * by;
dx += ddx;
dy += ddy;
if !dx.is_finite()
|| !dy.is_finite()
|| ddx.abs() > window_limit
|| ddy.abs() > window_limit
{
diverged = true;
break;
}
if ddx.abs() < CONVERGENCE_EPSILON && ddy.abs() < CONVERGENCE_EPSILON {
converged = true;
break;
}
if iteration > 0
&& (ddx + prev_step.0).abs() < CONVERGENCE_EPSILON
&& (ddy + prev_step.1).abs() < CONVERGENCE_EPSILON
{
dx -= ddx * 0.5;
dy -= ddy * 0.5;
converged = true;
break;
}
prev_step = (ddx, ddy);
}
out[idx].status = if out_of_bounds {
TrackStatus::OutOfBounds
} else if diverged || !converged {
TrackStatus::Diverged
} else {
TrackStatus::Tracked
};
displacements[idx] = (dx * lv.scale, dy * lv.scale);
if lv.is_finest {
out[idx].error = if out_of_bounds {
f32::INFINITY
} else {
window_error(lv, x + dx, y + dy, prev_patch)
};
}
}
}
struct Window {
x0: usize,
y0: usize,
w00: f32,
w10: f32,
w01: f32,
w11: f32,
}
impl Window {
#[inline(always)]
fn new(cx: f32, cy: f32, radius: usize, width: usize, height: usize) -> Option<Window> {
let r = radius as f32;
if !(cx >= r && cy >= r && cx < width as f32 - r - 1.0 && cy < height as f32 - r - 1.0) {
return None;
}
let fx0 = cx.floor();
let fy0 = cy.floor();
let fx = cx - fx0;
let fy = cy - fy0;
Some(Window {
x0: fx0 as usize - radius,
y0: fy0 as usize - radius,
w00: (1.0 - fx) * (1.0 - fy),
w10: fx * (1.0 - fy),
w01: (1.0 - fx) * fy,
w11: fx * fy,
})
}
#[inline(always)]
fn scaled(&self, factor: f32) -> Window {
Window {
x0: self.x0,
y0: self.y0,
w00: self.w00 * factor,
w10: self.w10 * factor,
w01: self.w01 * factor,
w11: self.w11 * factor,
}
}
#[inline(always)]
fn rows<'a, T>(
&self,
data: &'a [T],
width: usize,
j: usize,
size: usize,
) -> (&'a [T], &'a [T]) {
let start = (self.y0 + j) * width + self.x0;
(
&data[start..start + size + 1],
&data[start + width..start + width + size + 1],
)
}
}
#[inline(always)]
fn sample_lanes<T: Copy + Into<f32>, const L: usize>(win: &Window, a: &[T], b: &[T]) -> [f32; L] {
let a0: &[T; L] = a[..L].try_into().unwrap();
let a1: &[T; L] = a[1..=L].try_into().unwrap();
let b0: &[T; L] = b[..L].try_into().unwrap();
let b1: &[T; L] = b[1..=L].try_into().unwrap();
let mut s = [0f32; L];
for k in 0..L {
s[k] = win.w00 * a0[k].into()
+ win.w10 * a1[k].into()
+ win.w01 * b0[k].into()
+ win.w11 * b1[k].into();
}
s
}
#[inline(always)]
fn total<const L: usize>(acc: &[f32; L]) -> f32 {
acc.iter().sum()
}
#[inline(always)]
fn build_patch<I: TrackImage>(
lv: &Level<I>,
win: &Window,
prev_patch: &mut [f32],
ix_patch: &mut [f32],
iy_patch: &mut [f32],
) -> (f32, f32, f32) {
#[inline(always)]
#[allow(clippy::too_many_arguments)]
fn lanes<P: Copy + Into<f32>, G: Copy + Into<f32>, const L: usize>(
win: &Window,
gwin: &Window,
(pa, pb): (&[P], &[P]),
(xa, xb): (&[G], &[G]),
(ya, yb): (&[G], &[G]),
p: &mut [f32],
ox: &mut [f32],
oy: &mut [f32],
acc: &mut [[f32; L]; 3],
) {
let s = sample_lanes::<P, L>(win, pa, pb);
let sx = sample_lanes::<G, L>(gwin, xa, xb);
let sy = sample_lanes::<G, L>(gwin, ya, yb);
let p: &mut [f32; L] = (&mut p[..L]).try_into().unwrap();
let ox: &mut [f32; L] = (&mut ox[..L]).try_into().unwrap();
let oy: &mut [f32; L] = (&mut oy[..L]).try_into().unwrap();
*p = s;
*ox = sx;
*oy = sy;
for k in 0..L {
acc[0][k] += sx[k] * sx[k];
acc[1][k] += sx[k] * sy[k];
acc[2][k] += sy[k] * sy[k];
}
}
let size = lv.window_size;
let gwin = win.scaled(1.0 / 32.0);
let mut acc8 = [[0f32; 8]; 3];
let mut acc4 = [[0f32; 4]; 3];
let mut acc1 = [[0f32; 1]; 3];
for j in 0..size {
let (pa, pb) = win.rows(lv.prev, lv.width, j, size);
let (xa, xb) = win.rows(lv.grad_x, lv.width, j, size);
let (ya, yb) = win.rows(lv.grad_y, lv.width, j, size);
let p = &mut prev_patch[j * size..][..size];
let ox = &mut ix_patch[j * size..][..size];
let oy = &mut iy_patch[j * size..][..size];
let mut i = 0;
while i + 8 <= size {
lanes::<_, _, 8>(
win,
&gwin,
(&pa[i..], &pb[i..]),
(&xa[i..], &xb[i..]),
(&ya[i..], &yb[i..]),
&mut p[i..],
&mut ox[i..],
&mut oy[i..],
&mut acc8,
);
i += 8;
}
if i + 4 <= size {
lanes::<_, _, 4>(
win,
&gwin,
(&pa[i..], &pb[i..]),
(&xa[i..], &xb[i..]),
(&ya[i..], &yb[i..]),
&mut p[i..],
&mut ox[i..],
&mut oy[i..],
&mut acc4,
);
i += 4;
}
while i < size {
lanes::<_, _, 1>(
win,
&gwin,
(&pa[i..], &pb[i..]),
(&xa[i..], &xb[i..]),
(&ya[i..], &yb[i..]),
&mut p[i..],
&mut ox[i..],
&mut oy[i..],
&mut acc1,
);
i += 1;
}
}
(
total(&acc8[0]) + total(&acc4[0]) + total(&acc1[0]),
total(&acc8[1]) + total(&acc4[1]) + total(&acc1[1]),
total(&acc8[2]) + total(&acc4[2]) + total(&acc1[2]),
)
}
#[inline(always)]
fn lk_step<I: TrackImage>(
lv: &Level<I>,
win: &Window,
prev_patch: &[f32],
ix_patch: &[f32],
iy_patch: &[f32],
) -> (f32, f32) {
#[inline(always)]
fn lanes<P: Copy + Into<f32>, const L: usize>(
win: &Window,
(a, b): (&[P], &[P]),
p: &[f32],
gx: &[f32],
gy: &[f32],
acc: &mut [[f32; L]; 2],
) {
let s = sample_lanes::<P, L>(win, a, b);
let p: &[f32; L] = p[..L].try_into().unwrap();
let gx: &[f32; L] = gx[..L].try_into().unwrap();
let gy: &[f32; L] = gy[..L].try_into().unwrap();
for k in 0..L {
let e = p[k] - s[k];
acc[0][k] += gx[k] * e;
acc[1][k] += gy[k] * e;
}
}
let size = lv.window_size;
let mut acc8 = [[0f32; 8]; 2];
let mut acc4 = [[0f32; 4]; 2];
let mut acc1 = [[0f32; 1]; 2];
for j in 0..size {
let (a, b) = win.rows(lv.curr, lv.width, j, size);
let p = &prev_patch[j * size..][..size];
let gx = &ix_patch[j * size..][..size];
let gy = &iy_patch[j * size..][..size];
let mut i = 0;
while i + 8 <= size {
lanes::<_, 8>(
win,
(&a[i..], &b[i..]),
&p[i..],
&gx[i..],
&gy[i..],
&mut acc8,
);
i += 8;
}
if i + 4 <= size {
lanes::<_, 4>(
win,
(&a[i..], &b[i..]),
&p[i..],
&gx[i..],
&gy[i..],
&mut acc4,
);
i += 4;
}
while i < size {
lanes::<_, 1>(
win,
(&a[i..], &b[i..]),
&p[i..],
&gx[i..],
&gy[i..],
&mut acc1,
);
i += 1;
}
}
(
total(&acc8[0]) + total(&acc4[0]) + total(&acc1[0]),
total(&acc8[1]) + total(&acc4[1]) + total(&acc1[1]),
)
}
#[inline(always)]
fn window_error<I: TrackImage>(lv: &Level<I>, cx: f32, cy: f32, prev_patch: &[f32]) -> f32 {
#[inline(always)]
fn lanes<P: Copy + Into<f32>, const L: usize>(
win: &Window,
(a, b): (&[P], &[P]),
p: &[f32],
acc: &mut [f32; L],
) {
let s = sample_lanes::<P, L>(win, a, b);
let p: &[f32; L] = p[..L].try_into().unwrap();
for k in 0..L {
acc[k] += (p[k] - s[k]).abs();
}
}
let Some(win) = Window::new(cx, cy, lv.radius, lv.width, lv.height) else {
return f32::INFINITY;
};
let size = lv.window_size;
let mut acc8 = [0f32; 8];
let mut acc4 = [0f32; 4];
let mut acc1 = [0f32; 1];
for j in 0..size {
let (a, b) = win.rows(lv.curr, lv.width, j, size);
let p = &prev_patch[j * size..][..size];
let mut i = 0;
while i + 8 <= size {
lanes::<_, 8>(&win, (&a[i..], &b[i..]), &p[i..], &mut acc8);
i += 8;
}
if i + 4 <= size {
lanes::<_, 4>(&win, (&a[i..], &b[i..]), &p[i..], &mut acc4);
i += 4;
}
while i < size {
lanes::<_, 1>(&win, (&a[i..], &b[i..]), &p[i..], &mut acc1);
i += 1;
}
}
(total(&acc8) + total(&acc4) + total(&acc1)) / (size * size) as f32
}
pub fn calc_optical_flow_fb(
prev_pyramid: &[GrayImage],
next_pyramid: &[GrayImage],
prev_points: &[(f32, f32)],
predicted: Option<&[(f32, f32)]>,
params: TrackParams,
fb_threshold: f32,
) -> Vec<TrackResult> {
let mut scratch = Scratch::default();
let mut forward = Vec::new();
track_into(
prev_pyramid,
next_pyramid,
prev_points,
predicted,
params,
&mut scratch,
&mut forward,
);
let forward_pos: Vec<(f32, f32)> = forward.iter().map(|r| r.pos).collect();
let mut backward = Vec::new();
track_into(
next_pyramid,
prev_pyramid,
&forward_pos,
Some(prev_points),
params,
&mut scratch,
&mut backward,
);
mark_fb_inconsistent(&mut forward, &backward, prev_points, fb_threshold);
forward
}
fn mark_fb_inconsistent(
forward: &mut [TrackResult],
backward: &[TrackResult],
prev_points: &[(f32, f32)],
fb_threshold: f32,
) {
let threshold_sq = fb_threshold * fb_threshold;
for (idx, result) in forward.iter_mut().enumerate() {
if result.status != TrackStatus::Tracked {
continue;
}
let back = &backward[idx];
let dx = back.pos.0 - prev_points[idx].0;
let dy = back.pos.1 - prev_points[idx].1;
if back.status != TrackStatus::Tracked || dx * dx + dy * dy > threshold_sq {
result.status = TrackStatus::FbInconsistent;
}
}
}
fn min_eigenvalue(a: f32, b: f32, c: f32) -> f32 {
let trace = a + c;
let det = a * c - b * b;
let disc = (trace * trace - 4.0 * det).max(0.0).sqrt();
(trace - disc) / 2.0
}
fn invert_2x2(a00: f32, a01: f32, a11: f32, det_epsilon: f32) -> Option<(f32, f32, f32)> {
let det = a00 * a11 - a01 * a01;
if det.abs() <= det_epsilon {
return None;
}
let inv_det = 1.0 / det;
Some((a11 * inv_det, -a01 * inv_det, a00 * inv_det))
}
enum Spatial {
Invertible((f32, f32, f32)),
LowTexture { measure_error: bool },
}
trait TrackImage {
type Pixel: Copy + Into<f32>;
type Gradient: Copy + Into<f32> + Default;
fn dimensions(&self) -> (u32, u32);
fn pixels(&self) -> &[Self::Pixel];
fn gradients_into(&self, grad_x: &mut [Self::Gradient], grad_y: &mut [Self::Gradient]);
fn spatial_inverse(gxx: f32, gxy: f32, gyy: f32, n_pixels: usize, threshold: f32) -> Spatial;
}
impl TrackImage for GrayImage {
type Pixel = u8;
type Gradient = i16;
fn dimensions(&self) -> (u32, u32) {
ImageBuffer::dimensions(self)
}
fn pixels(&self) -> &[u8] {
self.as_raw()
}
fn gradients_into(&self, grad_x: &mut [i16], grad_y: &mut [i16]) {
compute_gradients_into(self, grad_x, grad_y);
}
#[inline(always)]
fn spatial_inverse(gxx: f32, gxy: f32, gyy: f32, n_pixels: usize, threshold: f32) -> Spatial {
let min_eig = min_eigenvalue(gxx, gxy, gyy) / n_pixels as f32;
if min_eig < threshold {
return Spatial::LowTexture {
measure_error: true,
};
}
match invert_2x2(gxx, gxy, gyy, 1e-6) {
Some(inverse) => Spatial::Invertible(inverse),
None => Spatial::LowTexture {
measure_error: false,
},
}
}
}
impl TrackImage for FloatPyramidLevel {
type Pixel = f32;
type Gradient = f32;
fn dimensions(&self) -> (u32, u32) {
ImageBuffer::dimensions(self)
}
fn pixels(&self) -> &[f32] {
self.as_raw()
}
fn gradients_into(&self, grad_x: &mut [f32], grad_y: &mut [f32]) {
let (width, height) = ImageBuffer::dimensions(self);
scharr_f32(
self.as_raw(),
width as usize,
height as usize,
grad_x,
grad_y,
);
}
#[inline(always)]
fn spatial_inverse(gxx: f32, gxy: f32, gyy: f32, n_pixels: usize, threshold: f32) -> Spatial {
let (a, b, c) = (gxx as f64, gyy as f64, gxy as f64);
let determinant = a * b - c * c;
let largest = (a + b + ((a - b).powi(2) + 4.0 * c * c).sqrt()) * 0.5;
let smallest = if determinant > 0.0 {
determinant / largest
} else {
0.0
};
let resolvable = smallest > largest * f32::EPSILON as f64;
if !resolvable || smallest / (n_pixels as f64) < threshold as f64 {
return Spatial::LowTexture {
measure_error: true,
};
}
let inverse = 1.0 / determinant;
Spatial::Invertible((
(b * inverse) as f32,
(-c * inverse) as f32,
(a * inverse) as f32,
))
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct TrackerCapacity {
pub width: u32,
pub height: u32,
pub levels: usize,
pub window_size: usize,
pub points: usize,
}
#[derive(Default)]
struct Tracker<I: TrackImage> {
prev_pyramid: Vec<I>,
next_pyramid: Vec<I>,
scratch: Scratch<I::Gradient>,
results: Vec<TrackResult>,
forward_pos: Vec<(f32, f32)>,
backward: Vec<TrackResult>,
}
impl<P> Tracker<ImageBuffer<P, Vec<P::Subpixel>>>
where
P: Pixel,
P::Subpixel: Default,
ImageBuffer<P, Vec<P::Subpixel>>: TrackImage,
{
fn reserve(&mut self, capacity: &TrackerCapacity) {
let TrackerCapacity {
width,
height,
levels,
points,
..
} = *capacity;
reserve_pyramid(&mut self.prev_pyramid, width, height, levels);
reserve_pyramid(&mut self.next_pyramid, width, height, levels);
self.scratch.reserve(capacity);
reserve_total(&mut self.results, points);
reserve_total(&mut self.forward_pos, points);
reserve_total(&mut self.backward, points);
}
}
impl<I: TrackImage> Tracker<I> {
fn track(
&mut self,
prev_points: &[(f32, f32)],
predicted: Option<&[(f32, f32)]>,
params: TrackParams,
) -> &[TrackResult] {
track_into(
&self.prev_pyramid,
&self.next_pyramid,
prev_points,
predicted,
params,
&mut self.scratch,
&mut self.results,
);
&self.results
}
fn track_fb(
&mut self,
prev_points: &[(f32, f32)],
predicted: Option<&[(f32, f32)]>,
params: TrackParams,
fb_threshold: f32,
) -> &[TrackResult] {
track_into(
&self.prev_pyramid,
&self.next_pyramid,
prev_points,
predicted,
params,
&mut self.scratch,
&mut self.results,
);
self.forward_pos.clear();
self.forward_pos.extend(self.results.iter().map(|r| r.pos));
track_into(
&self.next_pyramid,
&self.prev_pyramid,
&self.forward_pos,
Some(prev_points),
params,
&mut self.scratch,
&mut self.backward,
);
mark_fb_inconsistent(&mut self.results, &self.backward, prev_points, fb_threshold);
&self.results
}
}
#[derive(Default)]
pub struct TrackerContext {
tracker: Tracker<GrayImage>,
}
impl TrackerContext {
pub fn new() -> Self {
Self::default()
}
pub fn with_capacity(capacity: TrackerCapacity) -> Self {
let mut context = Self::default();
context.reserve(capacity);
context
}
pub fn reserve(&mut self, capacity: TrackerCapacity) {
self.tracker.reserve(&capacity);
}
pub fn prepare(&mut self, prev: &GrayImage, next: &GrayImage, levels: usize) {
assert_eq!(
prev.dimensions(),
next.dimensions(),
"frames must have the same size"
);
build_pyramid_into(prev, levels, &mut self.tracker.prev_pyramid);
build_pyramid_into(next, levels, &mut self.tracker.next_pyramid);
}
pub fn prev_pyramid(&self) -> &[GrayImage] {
&self.tracker.prev_pyramid
}
pub fn next_pyramid(&self) -> &[GrayImage] {
&self.tracker.next_pyramid
}
pub fn track(
&mut self,
prev_points: &[(f32, f32)],
predicted: Option<&[(f32, f32)]>,
params: TrackParams,
) -> &[TrackResult] {
self.tracker.track(prev_points, predicted, params)
}
pub fn track_fb(
&mut self,
prev_points: &[(f32, f32)],
predicted: Option<&[(f32, f32)]>,
params: TrackParams,
fb_threshold: f32,
) -> &[TrackResult] {
self.tracker
.track_fb(prev_points, predicted, params, fb_threshold)
}
}
#[derive(Default)]
pub struct FloatTrackerContext {
tracker: Tracker<FloatPyramidLevel>,
}
impl FloatTrackerContext {
pub fn new() -> Self {
Self::default()
}
pub fn with_capacity(capacity: TrackerCapacity) -> Self {
let mut context = Self::default();
context.reserve(capacity);
context
}
pub fn reserve(&mut self, capacity: TrackerCapacity) {
self.tracker.reserve(&capacity);
}
pub fn prepare(&mut self, prev: FloatImageView<'_>, next: FloatImageView<'_>, levels: usize) {
assert_eq!(
prev.dimensions(),
next.dimensions(),
"frames must have the same size"
);
build_float_pyramid_into(prev, levels, &mut self.tracker.prev_pyramid);
build_float_pyramid_into(next, levels, &mut self.tracker.next_pyramid);
}
pub fn prev_pyramid(&self) -> &[FloatPyramidLevel] {
&self.tracker.prev_pyramid
}
pub fn next_pyramid(&self) -> &[FloatPyramidLevel] {
&self.tracker.next_pyramid
}
pub fn track(
&mut self,
prev_points: &[(f32, f32)],
predicted: Option<&[(f32, f32)]>,
params: TrackParams,
) -> &[TrackResult] {
self.tracker.track(prev_points, predicted, params)
}
pub fn track_fb(
&mut self,
prev_points: &[(f32, f32)],
predicted: Option<&[(f32, f32)]>,
params: TrackParams,
fb_threshold: f32,
) -> &[TrackResult] {
self.tracker
.track_fb(prev_points, predicted, params, fb_threshold)
}
}
#[cfg(test)]
mod tests {
use super::*;
use image::Luma;
#[test]
fn invert_2x2_returns_inverse_components() {
let (inv00, inv01, inv11) = invert_2x2(4.0, 1.0, 3.0, 1e-6).unwrap();
assert!((inv00 - 3.0 / 11.0).abs() < 1e-6);
assert!((inv01 + 1.0 / 11.0).abs() < 1e-6);
assert!((inv11 - 4.0 / 11.0).abs() < 1e-6);
}
#[test]
fn invert_2x2_rejects_singular_matrix() {
assert!(invert_2x2(1.0, 2.0, 4.0, 1e-6).is_none());
}
fn bilinear(img: &GrayImage, x: f32, y: f32) -> f32 {
let (x0, y0) = (x.floor(), y.floor());
let (fx, fy) = (x - x0, y - y0);
let px = |dx: u32, dy: u32| img.get_pixel(x0 as u32 + dx, y0 as u32 + dy)[0] as f32;
px(0, 0) * (1.0 - fx) * (1.0 - fy)
+ px(1, 0) * fx * (1.0 - fy)
+ px(0, 1) * (1.0 - fx) * fy
+ px(1, 1) * fx * fy
}
#[test]
fn window_sampling_matches_per_pixel_bilinear_reference() {
let img = GrayImage::from_fn(40, 30, |x, y| Luma([((x * 37 + y * 11) ^ (x * y)) as u8]));
let mut grad_x = vec![0i16; 40 * 30];
let mut grad_y = vec![0i16; 40 * 30];
compute_gradients_into(&img, &mut grad_x, &mut grad_y);
for &size in &[1usize, 5, 7, 11, 21] {
let radius = size / 2;
let lv = Level::<GrayImage> {
prev: img.as_raw(),
curr: img.as_raw(),
grad_x: &grad_x,
grad_y: &grad_y,
width: 40,
height: 30,
scale: 1.0,
is_finest: true,
window_size: size,
radius,
max_iterations: 1,
min_eigen_threshold: 0.0,
};
let (cx, cy) = (17.3f32, 14.6f32);
let win = Window::new(cx, cy, radius, 40, 30).unwrap();
let mut patch = vec![0f32; size * size];
let mut ix = vec![0f32; size * size];
let mut iy = vec![0f32; size * size];
build_patch(&lv, &win, &mut patch, &mut ix, &mut iy);
for j in 0..size {
for i in 0..size {
let (sx, sy) = (
cx + (i as f32 - radius as f32),
cy + (j as f32 - radius as f32),
);
let expected = bilinear(&img, sx, sy);
let got = patch[j * size + i];
assert!(
(got - expected).abs() < 1e-3,
"size {size} at ({i},{j}): {got} vs {expected}"
);
}
}
assert!(window_error(&lv, cx, cy, &patch) < 1e-4);
let (bx, by) = lk_step(&lv, &win, &patch, &ix, &iy);
assert!(
bx.abs() < 1e-2 && by.abs() < 1e-2,
"size {size}: ({bx}, {by})"
);
}
}
#[test]
fn window_rejects_footprints_touching_the_border() {
assert!(Window::new(10.0, 10.0, 10, 40, 40).is_some());
assert!(Window::new(9.9, 10.0, 10, 40, 40).is_none());
assert!(Window::new(28.9, 10.0, 10, 40, 40).is_some());
assert!(Window::new(29.0, 10.0, 10, 40, 40).is_none());
assert!(Window::new(f32::NAN, 10.0, 10, 40, 40).is_none());
}
}