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, level_dimensions, pyramid_level_count,
reserve_pyramid,
};
use crate::utils::fast_gradients::compute_gradients_into;
use crate::utils::float_gradients::scharr_f32;
mod fixed;
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(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum TrackKernel {
#[default]
Auto,
FixedPoint,
Reference,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct TrackOptions {
pub start_level: Option<usize>,
pub fb_levels: Option<usize>,
pub kernel: TrackKernel,
}
impl TrackOptions {
pub const fn new() -> Self {
TrackOptions {
start_level: None,
fb_levels: None,
kernel: TrackKernel::Auto,
}
}
pub const fn with_start_level(mut self, level: usize) -> Self {
self.start_level = Some(level);
self
}
pub const fn with_fb_levels(mut self, levels: usize) -> Self {
self.fb_levels = Some(levels);
self
}
pub const fn with_kernel(mut self, kernel: TrackKernel) -> Self {
self.kernel = kernel;
self
}
}
#[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,
&mut Gradients::default(),
curr_pyramid,
prev_points,
predicted,
params,
None,
GrayImage::select(TrackKernel::Auto, params.window_size),
&mut scratch,
&mut out,
);
out
}
#[derive(Default)]
struct Scratch {
float_patch: [Vec<f32>; 3],
fixed_patch: [Vec<i16>; 3],
displacements: Vec<(f32, f32)>,
}
impl Scratch {
fn reserve(&mut self, capacity: &TrackerCapacity) {
let size = capacity.window_size;
for buffer in &mut self.float_patch {
reserve_total(buffer, size * size);
}
for buffer in &mut self.fixed_patch {
reserve_total(buffer, size * fixed::stride(size));
}
reserve_total(&mut self.displacements, capacity.points);
}
}
#[derive(Default)]
struct Gradients<G> {
levels: Vec<GradientLevel<G>>,
}
#[derive(Default)]
struct GradientLevel<G> {
x: Vec<G>,
y: Vec<G>,
ready: bool,
}
impl<G: Copy + Default> Gradients<G> {
fn invalidate(&mut self) {
for level in &mut self.levels {
level.ready = false;
}
}
fn reserve(&mut self, width: u32, height: u32, levels: usize) {
for (index, (w, h)) in level_dimensions(width, height, levels).enumerate() {
if index == self.levels.len() {
self.levels.push(GradientLevel::default());
}
let pixels = w as usize * h as usize;
reserve_total(&mut self.levels[index].x, pixels);
reserve_total(&mut self.levels[index].y, pixels);
}
}
fn level<I: TrackImage<Gradient = G>>(&mut self, pyramid: &[I], level: usize) -> (&[G], &[G]) {
if self.levels.len() < pyramid.len() {
self.levels
.resize_with(pyramid.len(), GradientLevel::default);
}
let image = &pyramid[level];
let slot = &mut self.levels[level];
if !slot.ready {
let (w, h) = image.dimensions();
let pixels = w as usize * h as usize;
slot.x.resize(pixels, G::default());
slot.y.resize(pixels, G::default());
image.gradients_into(&mut slot.x, &mut slot.y);
slot.ready = true;
}
(&slot.x, &slot.y)
}
}
#[allow(clippy::too_many_arguments)]
fn track_into<I: TrackImage>(
prev_pyramid: &[I],
prev_gradients: &mut Gradients<I::Gradient>,
curr_pyramid: &[I],
prev_points: &[(f32, f32)],
predicted: Option<&[(f32, f32)]>,
params: TrackParams,
start_level: Option<usize>,
kernel: I::Kernel,
scratch: &mut Scratch,
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 top = prev_pyramid.len() - 1;
let start = start_level.map_or(top, |level| level.min(top));
let radius = window_size / 2;
let displacements = &mut scratch.displacements;
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)),
}
out.clear();
out.extend(prev_points.iter().map(|&(x, y)| TrackResult {
pos: (x, y),
status: TrackStatus::Tracked,
error: f32::INFINITY,
}));
for level in (0..=start).rev() {
let prev_img = &prev_pyramid[level];
let curr_img = &curr_pyramid[level];
let (lw, lh) = prev_img.dimensions();
let (grad_x, grad_y) = prev_gradients.level(prev_pyramid, level);
let level = Level::<I> {
prev: prev_img.pixels(),
curr: curr_img.pixels(),
grad_x,
grad_y,
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,
};
I::track_level(kernel, &level, prev_points, scratch, out);
}
for (idx, (x, y)) in prev_points.iter().enumerate() {
let (dx, dy) = scratch.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, Debug)]
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
}
fn track_level<I: TrackImage>(
self,
level: &Level<I>,
prev_points: &[(f32, f32)],
scratch: &mut Scratch,
out: &mut [TrackResult],
) {
let n_pixels = level.window_size * level.window_size;
let Scratch {
float_patch: [prev, ix, iy],
displacements,
..
} = scratch;
for patch in [&mut *prev, &mut *ix, &mut *iy] {
patch.resize(n_pixels, 0.0);
}
let windows = FloatWindows { prev, ix, iy };
match self {
Kernel::Portable => track_level_impl(level, prev_points, displacements, windows, out),
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
Kernel::Avx2 => unsafe {
track_level_avx2(level, prev_points, displacements, windows, out)
},
}
}
}
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
#[target_feature(enable = "avx2")]
unsafe fn track_level_avx2<I: TrackImage, W: Windows<I>>(
level: &Level<I>,
prev_points: &[(f32, f32)],
displacements: &mut [(f32, f32)],
windows: W,
out: &mut [TrackResult],
) {
track_level_impl(level, prev_points, displacements, windows, out)
}
#[derive(Clone, Copy, Debug)]
enum FixedKernel {
#[cfg_attr(
any(
all(
any(target_arch = "x86", target_arch = "x86_64"),
target_feature = "sse2"
),
all(target_arch = "wasm32", target_feature = "simd128")
),
allow(dead_code)
)]
Scalar,
#[cfg(all(
any(target_arch = "x86", target_arch = "x86_64"),
target_feature = "sse2"
))]
Sse2,
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
Avx2(fixed::Avx2),
#[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
Simd128,
}
impl FixedKernel {
#[cfg(all(
any(target_arch = "x86", target_arch = "x86_64"),
target_feature = "sse2"
))]
const BASELINE: Self = FixedKernel::Sse2;
#[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
const BASELINE: Self = FixedKernel::Simd128;
#[cfg(not(any(
all(
any(target_arch = "x86", target_arch = "x86_64"),
target_feature = "sse2"
),
all(target_arch = "wasm32", target_feature = "simd128")
)))]
const BASELINE: Self = FixedKernel::Scalar;
fn detect() -> Self {
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
if let Some(avx2) = fixed::Avx2::detect() {
return FixedKernel::Avx2(avx2);
}
Self::BASELINE
}
fn track_level(
self,
level: &Level<GrayImage>,
prev_points: &[(f32, f32)],
scratch: &mut Scratch,
out: &mut [TrackResult],
) {
let size = level.window_size;
let stride = fixed::stride(size);
let Scratch {
fixed_patch: [pixels, grad_x, grad_y],
displacements,
..
} = scratch;
for patch in [&mut *pixels, &mut *grad_x, &mut *grad_y] {
patch.resize(size * stride, 0);
}
let patch = fixed::Patch {
size,
stride,
pixels,
grad_x,
grad_y,
};
fn run<K: fixed::Ops>(
level: &Level<GrayImage>,
prev_points: &[(f32, f32)],
displacements: &mut [(f32, f32)],
patch: fixed::Patch<'_>,
ops: K,
out: &mut [TrackResult],
) {
let windows = FixedWindows { patch, ops };
track_level_impl(level, prev_points, displacements, windows, out)
}
match self {
FixedKernel::Scalar => {
run(level, prev_points, displacements, patch, fixed::Scalar, out)
}
#[cfg(all(
any(target_arch = "x86", target_arch = "x86_64"),
target_feature = "sse2"
))]
FixedKernel::Sse2 => run(level, prev_points, displacements, patch, fixed::Sse2, out),
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
FixedKernel::Avx2(avx2) => run(level, prev_points, displacements, patch, avx2, out),
#[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
FixedKernel::Simd128 => run(
level,
prev_points,
displacements,
patch,
fixed::Simd128,
out,
),
}
}
}
#[derive(Clone, Copy, Debug)]
enum ByteKernel {
Reference(Kernel),
Fixed(FixedKernel),
}
trait Windows<I: TrackImage> {
fn build(&mut self, lv: &Level<I>, win: &Window) -> (f32, f32, f32);
fn step(&self, lv: &Level<I>, win: &Window) -> (f32, f32);
fn error(&self, lv: &Level<I>, win: &Window) -> f32;
}
struct FloatWindows<'a> {
prev: &'a mut [f32],
ix: &'a mut [f32],
iy: &'a mut [f32],
}
impl<I: TrackImage> Windows<I> for FloatWindows<'_> {
#[inline(always)]
fn build(&mut self, lv: &Level<I>, win: &Window) -> (f32, f32, f32) {
build_patch(lv, win, self.prev, self.ix, self.iy)
}
#[inline(always)]
fn step(&self, lv: &Level<I>, win: &Window) -> (f32, f32) {
lk_step(lv, win, self.prev, self.ix, self.iy)
}
#[inline(always)]
fn error(&self, lv: &Level<I>, win: &Window) -> f32 {
window_error(lv, win, self.prev)
}
}
struct FixedWindows<'a, K> {
patch: fixed::Patch<'a>,
ops: K,
}
impl<K: fixed::Ops> Windows<GrayImage> for FixedWindows<'_, K> {
#[inline(always)]
fn build(&mut self, lv: &Level<GrayImage>, win: &Window) -> (f32, f32, f32) {
let source = fixed::Source {
pixels: fixed::Plane {
data: lv.prev,
width: lv.width,
},
grad_x: fixed::Plane {
data: lv.grad_x,
width: lv.width,
},
grad_y: fixed::Plane {
data: lv.grad_y,
width: lv.width,
},
};
let weights = fixed::Weights::of(win);
let [xx, xy, yy] = self
.ops
.build(source, win.x0, win.y0, weights, &mut self.patch);
let scale = |sum: i64| sum as f32 * fixed::SPATIAL_SCALE;
(scale(xx), scale(xy), scale(yy))
}
#[inline(always)]
fn step(&self, lv: &Level<GrayImage>, win: &Window) -> (f32, f32) {
let next = fixed::Plane {
data: lv.curr,
width: lv.width,
};
let weights = fixed::Weights::of(win);
let [bx, by] = self.ops.step(next, win.x0, win.y0, weights, &self.patch);
(
bx as f32 * fixed::RESIDUAL_SCALE,
by as f32 * fixed::RESIDUAL_SCALE,
)
}
#[inline(always)]
fn error(&self, lv: &Level<GrayImage>, win: &Window) -> f32 {
let next = fixed::Plane {
data: lv.curr,
width: lv.width,
};
let weights = fixed::Weights::of(win);
let sum = self.ops.error(next, win.x0, win.y0, weights, &self.patch);
sum as f32 * fixed::ERROR_SCALE / (lv.window_size * lv.window_size) as f32
}
}
#[inline(always)]
fn track_level_impl<I: TrackImage, W: Windows<I>>(
lv: &Level<I>,
prev_points: &[(f32, f32)],
displacements: &mut [(f32, f32)],
mut windows: W,
out: &mut [TrackResult],
) {
let n_pixels = lv.window_size * lv.window_size;
let window_limit = lv.window_size as f32;
let final_error = |windows: &W, cx: f32, cy: f32| {
Window::new(cx, cy, lv.radius, lv.width, lv.height)
.map_or(f32::INFINITY, |win| windows.error(lv, &win))
};
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) = windows.build(lv, &prev_win);
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 = final_error(&windows, x + dx, y + dy);
}
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) = windows.step(lv, &win);
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 {
final_error(&windows, x + dx, y + dy)
};
}
}
}
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>, win: &Window, 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 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();
let kernel = GrayImage::select(TrackKernel::Auto, params.window_size);
track_into(
prev_pyramid,
&mut Gradients::default(),
next_pyramid,
prev_points,
predicted,
params,
None,
kernel,
&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,
&mut Gradients::default(),
prev_pyramid,
&forward_pos,
Some(prev_points),
params,
None,
kernel,
&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: Sized {
type Pixel: Copy + Into<f32>;
type Gradient: Copy + Into<f32> + Default;
type Kernel: Copy;
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;
fn select(choice: TrackKernel, window_size: usize) -> Self::Kernel;
fn track_level(
kernel: Self::Kernel,
level: &Level<Self>,
prev_points: &[(f32, f32)],
scratch: &mut Scratch,
out: &mut [TrackResult],
);
}
impl TrackImage for GrayImage {
type Pixel = u8;
type Gradient = i16;
type Kernel = ByteKernel;
fn dimensions(&self) -> (u32, u32) {
ImageBuffer::dimensions(self)
}
fn select(choice: TrackKernel, window_size: usize) -> ByteKernel {
let fixed = FixedKernel::detect();
let fixed_point = window_size <= fixed::MAX_WINDOW
&& match choice {
TrackKernel::Auto => !matches!(fixed, FixedKernel::Scalar),
TrackKernel::FixedPoint => true,
TrackKernel::Reference => false,
};
if fixed_point {
ByteKernel::Fixed(fixed)
} else {
ByteKernel::Reference(Kernel::detect())
}
}
fn track_level(
kernel: ByteKernel,
level: &Level<Self>,
prev_points: &[(f32, f32)],
scratch: &mut Scratch,
out: &mut [TrackResult],
) {
match kernel {
ByteKernel::Reference(kernel) => kernel.track_level(level, prev_points, scratch, out),
ByteKernel::Fixed(kernel) => kernel.track_level(level, prev_points, scratch, out),
}
}
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;
type Kernel = Kernel;
fn dimensions(&self) -> (u32, u32) {
ImageBuffer::dimensions(self)
}
fn select(_: TrackKernel, _: usize) -> Kernel {
Kernel::detect()
}
fn track_level(
kernel: Kernel,
level: &Level<Self>,
prev_points: &[(f32, f32)],
scratch: &mut Scratch,
out: &mut [TrackResult],
) {
kernel.track_level(level, prev_points, scratch, out)
}
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>,
prev_gradients: Gradients<I::Gradient>,
next_gradients: Gradients<I::Gradient>,
next_levels: Option<usize>,
scratch: Scratch,
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);
let base = self.next_pyramid.first().map(|level| level.dimensions());
reserve_pyramid(&mut self.next_pyramid, width, height, levels);
if self.next_pyramid.first().map(|level| level.dimensions()) != base {
self.next_levels = None;
}
for gradients in [&mut self.prev_gradients, &mut self.next_gradients] {
gradients.reserve(width, height, levels);
gradients.invalidate();
}
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 built_both(&mut self) {
self.prev_gradients.invalidate();
self.next_gradients.invalidate();
self.built_next();
}
fn built_next(&mut self) {
self.next_gradients.invalidate();
self.next_levels = Some(self.next_pyramid.len());
}
fn advance(
&mut self,
dimensions: (u32, u32),
levels: usize,
build: impl Fn(&mut Vec<I>),
rebuild: impl FnOnce(&I, &mut Vec<I>),
) {
let last_frame = self
.next_levels
.filter(|_| self.next_pyramid.first().map(I::dimensions) == Some(dimensions));
match last_frame {
Some(built)
if built == self.next_pyramid.len()
&& built == pyramid_level_count(dimensions.0, dimensions.1, levels) =>
{
std::mem::swap(&mut self.prev_pyramid, &mut self.next_pyramid);
std::mem::swap(&mut self.prev_gradients, &mut self.next_gradients);
}
Some(_) => {
rebuild(&self.next_pyramid[0], &mut self.prev_pyramid);
self.prev_gradients.invalidate();
}
None => {
build(&mut self.prev_pyramid);
self.prev_gradients.invalidate();
}
}
build(&mut self.next_pyramid);
self.built_next();
}
fn track(
&mut self,
prev_points: &[(f32, f32)],
predicted: Option<&[(f32, f32)]>,
params: TrackParams,
options: TrackOptions,
) -> &[TrackResult] {
track_into(
&self.prev_pyramid,
&mut self.prev_gradients,
&self.next_pyramid,
prev_points,
predicted,
params,
options.start_level,
I::select(options.kernel, params.window_size),
&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,
options: TrackOptions,
) -> &[TrackResult] {
let backward_start = options.fb_levels.map(|levels| {
assert!(levels >= 1, "fb_levels must be at least 1");
levels - 1
});
let kernel = I::select(options.kernel, params.window_size);
track_into(
&self.prev_pyramid,
&mut self.prev_gradients,
&self.next_pyramid,
prev_points,
predicted,
params,
options.start_level,
kernel,
&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,
&mut self.next_gradients,
&self.prev_pyramid,
&self.forward_pos,
Some(prev_points),
params,
backward_start,
kernel,
&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);
self.tracker.built_both();
}
pub fn advance(&mut self, frame: &GrayImage, levels: usize) {
self.tracker.advance(
frame.dimensions(),
levels,
|pyramid| build_pyramid_into(frame, levels, pyramid),
|base, pyramid| build_pyramid_into(base, levels, 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.track_with(prev_points, predicted, params, TrackOptions::new())
}
pub fn track_with(
&mut self,
prev_points: &[(f32, f32)],
predicted: Option<&[(f32, f32)]>,
params: TrackParams,
options: TrackOptions,
) -> &[TrackResult] {
self.tracker.track(prev_points, predicted, params, options)
}
pub fn track_fb(
&mut self,
prev_points: &[(f32, f32)],
predicted: Option<&[(f32, f32)]>,
params: TrackParams,
fb_threshold: f32,
) -> &[TrackResult] {
self.track_fb_with(
prev_points,
predicted,
params,
fb_threshold,
TrackOptions::new(),
)
}
pub fn track_fb_with(
&mut self,
prev_points: &[(f32, f32)],
predicted: Option<&[(f32, f32)]>,
params: TrackParams,
fb_threshold: f32,
options: TrackOptions,
) -> &[TrackResult] {
self.tracker
.track_fb(prev_points, predicted, params, fb_threshold, options)
}
}
#[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);
self.tracker.built_both();
}
pub fn advance(&mut self, frame: FloatImageView<'_>, levels: usize) {
self.tracker.advance(
frame.dimensions(),
levels,
|pyramid| build_float_pyramid_into(frame, levels, pyramid),
|base, pyramid| {
let (width, height) = base.dimensions();
let base = FloatImageView::new(base.as_raw(), width, height)
.expect("the base level is a copy of a validated frame");
build_float_pyramid_into(base, levels, 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.track_with(prev_points, predicted, params, TrackOptions::new())
}
pub fn track_with(
&mut self,
prev_points: &[(f32, f32)],
predicted: Option<&[(f32, f32)]>,
params: TrackParams,
options: TrackOptions,
) -> &[TrackResult] {
self.tracker.track(prev_points, predicted, params, options)
}
pub fn track_fb(
&mut self,
prev_points: &[(f32, f32)],
predicted: Option<&[(f32, f32)]>,
params: TrackParams,
fb_threshold: f32,
) -> &[TrackResult] {
self.track_fb_with(
prev_points,
predicted,
params,
fb_threshold,
TrackOptions::new(),
)
}
pub fn track_fb_with(
&mut self,
prev_points: &[(f32, f32)],
predicted: Option<&[(f32, f32)]>,
params: TrackParams,
fb_threshold: f32,
options: TrackOptions,
) -> &[TrackResult] {
self.tracker
.track_fb(prev_points, predicted, params, fb_threshold, options)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pyramid::build_pyramid;
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, &win, &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})"
);
}
}
fn fixed_kernels() -> Vec<FixedKernel> {
#[allow(unused_mut)]
let mut kernels = vec![FixedKernel::Scalar, FixedKernel::BASELINE];
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
if let Some(avx2) = fixed::Avx2::detect() {
kernels.push(FixedKernel::Avx2(avx2));
}
kernels
}
fn texture(w: u32, h: u32, (sx, sy): (f32, f32)) -> GrayImage {
let noise = |x: i32, y: i32| {
let mut s = (y.rem_euclid(1024) as u32 * 1024 + x.rem_euclid(1024) as u32)
.wrapping_mul(2654435761);
s ^= s >> 15;
s = s.wrapping_mul(0x85ebca6b);
(s >> 24) as f32
};
let smooth = |x: f32, y: f32| {
let (x0, y0) = (x.floor(), y.floor());
let (fx, fy) = (x - x0, y - y0);
let mut total = 0.0;
for dy in -2..=3 {
for dx in -2..=3 {
let wx = if dx == -2 {
1.0 - fx
} else if dx == 3 {
fx
} else {
1.0
};
let wy = if dy == -2 {
1.0 - fy
} else if dy == 3 {
fy
} else {
1.0
};
total += wx * wy * noise(x0 as i32 + dx, y0 as i32 + dy);
}
}
total / 25.0
};
GrayImage::from_fn(w, h, |x, y| {
Luma([smooth(x as f32 - sx, y as f32 - sy) as u8])
})
}
#[test]
fn fixed_point_kernels_track_identically() {
for (w, h, shift) in [(97u32, 71u32, (1.7f32, -0.6f32)), (160, 120, (4.25, 3.5))] {
let prev = build_pyramid(&texture(w, h, (0.0, 0.0)), 3);
let next = build_pyramid(&texture(w, h, shift), 3);
let points: Vec<(f32, f32)> = (0..90)
.map(|i| {
(
1.5 + (i % 10) as f32 * w as f32 / 9.6,
2.0 + (i / 10) as f32 * h as f32 / 8.7,
)
})
.collect();
let predicted: Vec<(f32, f32)> = points
.iter()
.map(|&(x, y)| (x + shift.0, y + shift.1))
.collect();
for window_size in [1, 7, 13, 21, 31] {
let params = TrackParams {
window_size,
max_iterations: 20,
min_eigen_threshold: DEFAULT_MIN_EIGEN_THRESHOLD,
};
for guess in [None, Some(predicted.as_slice())] {
let run = |kernel| {
let mut out = Vec::new();
track_into(
&prev,
&mut Gradients::default(),
&next,
&points,
guess,
params,
None,
ByteKernel::Fixed(kernel),
&mut Scratch::default(),
&mut out,
);
out.iter()
.map(|r| {
(
r.pos.0.to_bits(),
r.pos.1.to_bits(),
r.status,
r.error.to_bits(),
)
})
.collect::<Vec<_>>()
};
let kernels = fixed_kernels();
let scalar = run(kernels[0]);
let tracked = scalar
.iter()
.filter(|r| r.2 == TrackStatus::Tracked)
.count();
assert!(window_size == 1 || tracked > 20, "{tracked} tracked");
for &kernel in &kernels[1..] {
assert_eq!(
run(kernel),
scalar,
"{kernel:?}, {w}x{h}, window {window_size}"
);
}
}
}
}
}
#[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());
}
}