use crate::encoder::frame_obu::quantize_global_param;
use crate::inter_pred::setup_shear;
use crate::uncompressed_header_tail::{WarpModelType, WARPEDMODEL_PREC_BITS};
const PREPASS_BLOCK: usize = 16;
const PREPASS_RANGE: i32 = 24;
const ELECT_ENERGY_RATIO: f64 = 0.75;
const MIN_MOTION_ENERGY: f64 = 0.25;
const CLASS_UPGRADE_RATIO: f64 = 0.8;
const CLASS_UPGRADE_MIN_GAIN: f64 = 0.02;
#[derive(Debug, Clone, Copy)]
pub(crate) struct MotionSample {
x: f64,
y: f64,
u: f64,
v: f64,
}
pub(crate) fn collect_motion_samples(
input_y: &[u16],
ref_y: &[u16],
width: usize,
height: usize,
) -> Vec<MotionSample> {
let mut samples = Vec::new();
if width < PREPASS_BLOCK || height < PREPASS_BLOCK {
return samples;
}
let sad_int = |row0: usize, col0: usize, dy: i32, dx: i32| -> u64 {
let mut sad = 0u64;
for i in 0..PREPASS_BLOCK {
let sy = ((row0 + i) as i32 + dy).clamp(0, height as i32 - 1) as usize;
for j in 0..PREPASS_BLOCK {
let sx = ((col0 + j) as i32 + dx).clamp(0, width as i32 - 1) as usize;
let d = i32::from(input_y[(row0 + i) * width + col0 + j])
- i32::from(ref_y[sy * width + sx]);
sad += d.unsigned_abs() as u64;
}
}
sad
};
let sad_subpel = |row0: usize, col0: usize, my8: i32, mx8: i32| -> u64 {
let fy = (my8.rem_euclid(8)) as u32;
let fx = (mx8.rem_euclid(8)) as u32;
let iy = (my8 - fy as i32) / 8;
let ix = (mx8 - fx as i32) / 8;
let at = |r: i32, c: i32| -> u32 {
let r = r.clamp(0, height as i32 - 1) as usize;
let c = c.clamp(0, width as i32 - 1) as usize;
u32::from(ref_y[r * width + c])
};
let mut sad = 0u64;
for i in 0..PREPASS_BLOCK {
let r = (row0 + i) as i32 + iy;
for j in 0..PREPASS_BLOCK {
let c = (col0 + j) as i32 + ix;
let p00 = at(r, c);
let p01 = at(r, c + 1);
let p10 = at(r + 1, c);
let p11 = at(r + 1, c + 1);
let top = p00 * (8 - fx) + p01 * fx;
let bot = p10 * (8 - fx) + p11 * fx;
let pred = (top * (8 - fy) + bot * fy + 32) >> 6;
let d = i32::from(input_y[(row0 + i) * width + col0 + j]) - pred as i32;
sad += d.unsigned_abs() as u64;
}
}
sad
};
let (hw, hh) = (width / 2, height / 2);
let downsample = |p: &[u16]| -> Vec<u16> {
let mut d = vec![0u16; hw * hh];
for r in 0..hh {
for c in 0..hw {
let s = u32::from(p[(2 * r) * width + 2 * c])
+ u32::from(p[(2 * r) * width + 2 * c + 1])
+ u32::from(p[(2 * r + 1) * width + 2 * c])
+ u32::from(p[(2 * r + 1) * width + 2 * c + 1]);
d[r * hw + c] = ((s + 2) >> 2) as u16;
}
}
d
};
let input_half = downsample(input_y);
let ref_half = downsample(ref_y);
let half_block = PREPASS_BLOCK / 2;
let sad_half = |row0: usize, col0: usize, dy: i32, dx: i32| -> u64 {
let (r0, c0) = (row0 / 2, col0 / 2);
let mut sad = 0u64;
for i in 0..half_block {
let sy = ((r0 + i) as i32 + dy).clamp(0, hh as i32 - 1) as usize;
for j in 0..half_block {
let sx = ((c0 + j) as i32 + dx).clamp(0, hw as i32 - 1) as usize;
let d = i32::from(input_half[(r0 + i) * hw + c0 + j])
- i32::from(ref_half[sy * hw + sx]);
sad += d.unsigned_abs() as u64;
}
}
sad
};
let half_range = PREPASS_RANGE / 2;
let mut row0 = 0usize;
while row0 + PREPASS_BLOCK <= height {
let mut col0 = 0usize;
while col0 + PREPASS_BLOCK <= width {
let mut half_best = (sad_half(row0, col0, 0, 0), 0i32, 0i32);
for dy in -half_range..=half_range {
for dx in -half_range..=half_range {
if dy == 0 && dx == 0 {
continue;
}
let c = sad_half(row0, col0, dy, dx)
+ (dy.unsigned_abs() + dx.unsigned_abs()) as u64;
if c < half_best.0 {
half_best = (c, dy, dx);
}
}
}
let (cy, cx) = (half_best.1 * 2, half_best.2 * 2);
let mut best = (sad_int(row0, col0, cy, cx), cy, cx);
for dy in cy - 3..=cy + 3 {
for dx in cx - 3..=cx + 3 {
if dy == cy && dx == cx {
continue;
}
let c = sad_int(row0, col0, dy, dx)
+ (dy.unsigned_abs() + dx.unsigned_abs()) as u64;
if c < best.0 {
best = (c, dy, dx);
}
}
}
let mut best8 = (
sad_subpel(row0, col0, best.1 * 8, best.2 * 8),
best.1 * 8,
best.2 * 8,
);
for step in [4i32, 2, 1] {
let (by, bx) = (best8.1, best8.2);
for dy in [-step, 0, step] {
for dx in [-step, 0, step] {
if dy == 0 && dx == 0 {
continue;
}
let c = sad_subpel(row0, col0, by + dy, bx + dx);
if c < best8.0 {
best8 = (c, by + dy, bx + dx);
}
}
}
}
samples.push(MotionSample {
x: col0 as f64 + (PREPASS_BLOCK as f64) / 2.0 - 0.5,
y: row0 as f64 + (PREPASS_BLOCK as f64) / 2.0 - 0.5,
u: f64::from(best8.2) / 8.0,
v: f64::from(best8.1) / 8.0,
});
col0 += PREPASS_BLOCK;
}
row0 += PREPASS_BLOCK;
}
samples
}
fn solve_linear<const N: usize>(mut a: [[f64; N]; N], mut b: [f64; N]) -> Option<[f64; N]> {
for col in 0..N {
let mut piv = col;
for row in col + 1..N {
if a[row][col].abs() > a[piv][col].abs() {
piv = row;
}
}
if a[piv][col].abs() < 1e-9 {
return None;
}
a.swap(col, piv);
b.swap(col, piv);
let pivot_row = a[col];
let pivot_b = b[col];
for row in col + 1..N {
let f = a[row][col] / pivot_row[col];
for (dst, src) in a[row][col..].iter_mut().zip(&pivot_row[col..]) {
*dst -= f * *src;
}
b[row] -= f * pivot_b;
}
}
let mut x = [0.0f64; N];
for col in (0..N).rev() {
let mut s = b[col];
for k in col + 1..N {
s -= a[col][k] * x[k];
}
x[col] = s / a[col][col];
}
Some(x)
}
#[derive(Debug, Clone, Copy)]
struct ContinuousModel {
m: [f64; 6],
}
impl ContinuousModel {
fn identity() -> Self {
Self {
m: [0.0, 0.0, 1.0, 0.0, 0.0, 1.0],
}
}
fn motion_at(&self, x: f64, y: f64) -> (f64, f64) {
let u = (self.m[2] - 1.0) * x + self.m[3] * y + self.m[0];
let v = self.m[4] * x + (self.m[5] - 1.0) * y + self.m[1];
(u, v)
}
}
fn mean_sq_residual(model: &ContinuousModel, samples: &[MotionSample]) -> f64 {
if samples.is_empty() {
return 0.0;
}
let mut acc = 0.0;
for s in samples {
let (mu, mv) = model.motion_at(s.x, s.y);
acc += (s.u - mu).powi(2) + (s.v - mv).powi(2);
}
acc / samples.len() as f64
}
fn fit_translation(samples: &[MotionSample]) -> ContinuousModel {
let n = samples.len() as f64;
let (mut su, mut sv) = (0.0, 0.0);
for s in samples {
su += s.u;
sv += s.v;
}
ContinuousModel {
m: [su / n, sv / n, 1.0, 0.0, 0.0, 1.0],
}
}
fn fit_rotzoom(samples: &[MotionSample]) -> Option<ContinuousModel> {
let n = samples.len() as f64;
let (mut sx, mut sy, mut q) = (0.0, 0.0, 0.0);
let (mut p1, mut p2, mut su, mut sv) = (0.0, 0.0, 0.0, 0.0);
for s in samples {
sx += s.x;
sy += s.y;
q += s.x * s.x + s.y * s.y;
p1 += s.x * s.u + s.y * s.v;
p2 += s.y * s.u - s.x * s.v;
su += s.u;
sv += s.v;
}
let a = [
[q, 0.0, sx, sy],
[0.0, q, sy, -sx],
[sx, sy, n, 0.0],
[sy, -sx, 0.0, n],
];
let x = solve_linear(a, [p1, p2, su, sv])?;
Some(ContinuousModel {
m: [x[2], x[3], 1.0 + x[0], x[1], -x[1], 1.0 + x[0]],
})
}
fn fit_affine(samples: &[MotionSample]) -> Option<ContinuousModel> {
let n = samples.len() as f64;
let (mut sx, mut sy, mut sxx, mut sxy, mut syy) = (0.0, 0.0, 0.0, 0.0, 0.0);
let (mut sxu, mut syu, mut su) = (0.0, 0.0, 0.0);
let (mut sxv, mut syv, mut sv) = (0.0, 0.0, 0.0);
for s in samples {
sx += s.x;
sy += s.y;
sxx += s.x * s.x;
sxy += s.x * s.y;
syy += s.y * s.y;
sxu += s.x * s.u;
syu += s.y * s.u;
su += s.u;
sxv += s.x * s.v;
syv += s.y * s.v;
sv += s.v;
}
let a = [[sxx, sxy, sx], [sxy, syy, sy], [sx, sy, n]];
let xu = solve_linear(a, [sxu, syu, su])?;
let xv = solve_linear(a, [sxv, syv, sv])?;
Some(ContinuousModel {
m: [xu[2], xv[2], 1.0 + xu[0], xu[1], xv[0], 1.0 + xv[1]],
})
}
fn quantize_model(
model: &ContinuousModel,
gm_type: WarpModelType,
allow_high_precision_mv: bool,
samples: &[MotionSample],
) -> [i32; 6] {
let one = 1i32 << WARPEDMODEL_PREC_BITS;
let scale = f64::from(one);
let mut p = [0i32, 0, one, 0, 0, one];
if gm_type as u8 >= WarpModelType::RotZoom as u8 {
for (idx, slot) in p.iter_mut().enumerate().take(6).skip(2) {
let raw = (model.m[idx] * scale).round() as i64;
let raw = raw.clamp(i64::from(i32::MIN), i64::from(i32::MAX)) as i32;
*slot = quantize_global_param(gm_type, allow_high_precision_mv, idx, raw);
}
if gm_type == WarpModelType::RotZoom {
p[4] = -p[3];
p[5] = p[2];
}
}
let (mut tu, mut tv) = (0.0f64, 0.0f64);
if !samples.is_empty() {
let m2 = f64::from(p[2]) / scale;
let m3 = f64::from(p[3]) / scale;
let m4 = f64::from(p[4]) / scale;
let m5 = f64::from(p[5]) / scale;
for s in samples {
tu += s.u - ((m2 - 1.0) * s.x + m3 * s.y);
tv += s.v - (m4 * s.x + (m5 - 1.0) * s.y);
}
tu /= samples.len() as f64;
tv /= samples.len() as f64;
}
for (idx, t) in [(0usize, tu), (1usize, tv)] {
let raw = (t * scale).round() as i64;
let raw = raw.clamp(i64::from(i32::MIN), i64::from(i32::MAX)) as i32;
p[idx] = quantize_global_param(gm_type, allow_high_precision_mv, idx, raw);
}
p
}
fn continuous_of_params(p: &[i32; 6]) -> ContinuousModel {
let scale = f64::from(1i32 << WARPEDMODEL_PREC_BITS);
ContinuousModel {
m: [
f64::from(p[0]) / scale,
f64::from(p[1]) / scale,
f64::from(p[2]) / scale,
f64::from(p[3]) / scale,
f64::from(p[4]) / scale,
f64::from(p[5]) / scale,
],
}
}
struct Candidate {
gm_type: WarpModelType,
params: [i32; 6],
err: f64,
}
fn candidate_for(
gm_type: WarpModelType,
samples: &[MotionSample],
allow_high_precision_mv: bool,
) -> Option<Candidate> {
let min_samples = match gm_type {
WarpModelType::Translation => 4,
WarpModelType::RotZoom => 6,
_ => 8,
};
if samples.len() < min_samples {
return None;
}
let fit = |subset: &[MotionSample]| -> Option<ContinuousModel> {
match gm_type {
WarpModelType::Translation => Some(fit_translation(subset)),
WarpModelType::RotZoom => fit_rotzoom(subset),
WarpModelType::Affine => fit_affine(subset),
WarpModelType::Identity => None,
}
};
let first = fit(samples)?;
let mut residuals: Vec<f64> = samples
.iter()
.map(|s| {
let (mu, mv) = first.motion_at(s.x, s.y);
((s.u - mu).powi(2) + (s.v - mv).powi(2)).sqrt()
})
.collect();
let mut sorted = residuals.clone();
sorted.sort_by(|a, b| a.partial_cmp(b).expect("residuals are finite"));
let median = sorted[sorted.len() / 2];
let cutoff = (3.0 * median).max(1.0);
let trimmed: Vec<MotionSample> = samples
.iter()
.zip(residuals.drain(..))
.filter_map(|(s, r)| (r <= cutoff).then_some(*s))
.collect();
let (model, scored_samples) = if trimmed.len() >= min_samples {
(fit(&trimmed)?, trimmed)
} else {
(first, samples.to_vec())
};
let params = quantize_model(&model, gm_type, allow_high_precision_mv, &scored_samples);
if params
== [
0,
0,
1 << WARPEDMODEL_PREC_BITS,
0,
0,
1 << WARPEDMODEL_PREC_BITS,
]
{
return None;
}
if gm_type as u8 >= WarpModelType::RotZoom as u8 {
match setup_shear(params) {
Some(s) if s.warp_valid => {}
_ => return None,
}
}
let err = mean_sq_residual(&continuous_of_params(¶ms), samples);
let params = if gm_type == WarpModelType::Translation {
[
params[1], params[0], params[2], params[3], params[4], params[5],
]
} else {
params
};
Some(Candidate {
gm_type,
params,
err,
})
}
pub(crate) fn estimate_global_motion(
samples: &[MotionSample],
allow_high_precision_mv: bool,
) -> (WarpModelType, [i32; 6]) {
let one = 1i32 << WARPEDMODEL_PREC_BITS;
let identity = (WarpModelType::Identity, [0, 0, one, 0, 0, one]);
if samples.is_empty() {
return identity;
}
let base_err = mean_sq_residual(&ContinuousModel::identity(), samples);
if base_err < MIN_MOTION_ENERGY {
return identity;
}
let gate = (1.0 - ELECT_ENERGY_RATIO) * base_err;
let mut elected: Option<Candidate> = None;
for gm_type in [
WarpModelType::Translation,
WarpModelType::RotZoom,
WarpModelType::Affine,
] {
let Some(cand) = candidate_for(gm_type, samples, allow_high_precision_mv) else {
continue;
};
if cand.err > gate {
continue;
}
elected = match elected {
None => Some(cand),
Some(prev)
if cand.err <= CLASS_UPGRADE_RATIO * prev.err
&& prev.err - cand.err > CLASS_UPGRADE_MIN_GAIN =>
{
Some(cand)
}
Some(prev) => Some(prev),
};
}
match elected {
Some(c) => (c.gm_type, c.params),
None => identity,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn field(model: &ContinuousModel, noise: f64) -> Vec<MotionSample> {
let mut out = Vec::new();
let mut rng: u32 = 0x1234_5678;
let mut next = || {
rng = rng.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
(f64::from(rng >> 8) / f64::from(1u32 << 24) - 0.5) * 2.0 * noise
};
for by in 0..6 {
for bx in 0..8 {
let x = bx as f64 * 16.0 + 7.5;
let y = by as f64 * 16.0 + 7.5;
let (u, v) = model.motion_at(x, y);
out.push(MotionSample {
x,
y,
u: u + next(),
v: v + next(),
});
}
}
out
}
#[test]
fn translation_field_elects_translation() {
let model = ContinuousModel {
m: [3.25, -1.5, 1.0, 0.0, 0.0, 1.0],
};
let (t, p) = estimate_global_motion(&field(&model, 0.05), false);
assert_eq!(t, WarpModelType::Translation);
assert_eq!(p[0], (-1.5f64 * 65536.0) as i32);
assert_eq!(p[1], (3.25f64 * 65536.0) as i32);
}
#[test]
fn zoom_field_elects_rotzoom() {
let model = ContinuousModel {
m: [1.0, 0.5, 1.02, 0.0, 0.0, 1.02],
};
let (t, p) = estimate_global_motion(&field(&model, 0.05), false);
assert_eq!(t, WarpModelType::RotZoom);
assert_eq!(p[4], -p[3]);
assert_eq!(p[5], p[2]);
assert!((f64::from(p[2]) / 65536.0 - 1.02).abs() < 0.001, "{}", p[2]);
assert!(matches!(setup_shear(p), Some(s) if s.warp_valid));
}
#[test]
fn rotation_field_elects_rotzoom() {
let (c, s) = (0.9998f64, 0.0175f64);
let model = ContinuousModel {
m: [0.0, 0.0, c, s, -s, c],
};
let (t, p) = estimate_global_motion(&field(&model, 0.05), false);
assert_eq!(t, WarpModelType::RotZoom);
assert!((f64::from(p[3]) / 65536.0 - s).abs() < 0.001);
}
#[test]
fn shear_field_elects_affine() {
let model = ContinuousModel {
m: [0.0, 0.0, 1.0, 0.02, 0.0, 1.0],
};
let (t, p) = estimate_global_motion(&field(&model, 0.02), false);
assert_eq!(t, WarpModelType::Affine);
assert!((f64::from(p[3]) / 65536.0 - 0.02).abs() < 0.001);
assert!(f64::from(p[4].abs()) / 65536.0 < 0.001);
}
#[test]
fn static_field_stays_identity() {
let (t, _) = estimate_global_motion(&field(&ContinuousModel::identity(), 0.1), false);
assert_eq!(t, WarpModelType::Identity);
}
#[test]
fn incoherent_field_stays_identity() {
let (t, _) = estimate_global_motion(&field(&ContinuousModel::identity(), 4.0), false);
assert_eq!(t, WarpModelType::Identity);
}
#[test]
fn prepass_recovers_a_pure_translation() {
let (w, h) = (64usize, 64usize);
let hash = |r: i64, c: i64| -> u32 {
let mut x = (r.wrapping_mul(0x9E37_79B1) ^ c.wrapping_mul(0x85EB_CA77)) as u64;
x ^= x >> 15;
x = x.wrapping_mul(0x2545_F491_4F6C_DD1D);
((x >> 32) & 0xFF) as u32
};
let tex = |r: i64, c: i64| -> u16 {
let mut acc = 0u32;
for dr in -2..=2i64 {
for dc in -2..=2i64 {
acc += hash(r + dr, c + dc);
}
}
(acc / 25) as u16
};
let mut refp = vec![0u16; w * h];
let mut cur = vec![0u16; w * h];
for r in 0..h as i64 {
for c in 0..w as i64 {
refp[(r * w as i64 + c) as usize] = tex(r, c);
cur[(r * w as i64 + c) as usize] = tex(r - 5, c + 3);
}
}
let samples = collect_motion_samples(&cur, &refp, w, h);
assert_eq!(samples.len(), 16);
let interior: Vec<&MotionSample> = samples
.iter()
.filter(|s| s.x > 16.0 && s.x < 48.0 && s.y > 16.0 && s.y < 48.0)
.collect();
assert!(!interior.is_empty());
for s in interior {
assert!(
(s.u - 3.0).abs() < 0.51 && (s.v + 5.0).abs() < 0.51,
"block at ({}, {}) measured ({}, {})",
s.x,
s.y,
s.u,
s.v
);
}
}
}