use core::{f32::consts::PI, time::Duration};
use derive_more::IsVariant;
use thiserror::Error;
use crate::frame::{LumaFrame, Timebase, Timestamp};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use std::{vec, vec::Vec};
use super::{ceil_32, cos_32, floor_32, sqrt_32};
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Options {
threshold: f64,
size: u32,
lowpass: u32,
#[cfg_attr(feature = "serde", serde(with = "humantime_serde"))]
min_duration: Duration,
initial_cut: bool,
}
impl Default for Options {
#[cfg_attr(not(tarpaulin), inline(always))]
fn default() -> Self {
Self::new()
}
}
impl Options {
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn new() -> Self {
Self {
threshold: 0.395,
size: 16,
lowpass: 2,
min_duration: Duration::from_secs(1),
initial_cut: true,
}
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn threshold(&self) -> f64 {
self.threshold
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn with_threshold(mut self, threshold: f64) -> Self {
self.set_threshold(threshold);
self
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn set_threshold(&mut self, threshold: f64) -> &mut Self {
self.threshold = threshold;
self
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn size(&self) -> u32 {
self.size
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn with_size(mut self, size: u32) -> Self {
self.set_size(size);
self
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn set_size(&mut self, size: u32) -> &mut Self {
self.size = size;
self
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn lowpass(&self) -> u32 {
self.lowpass
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn with_lowpass(mut self, lowpass: u32) -> Self {
self.set_lowpass(lowpass);
self
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn set_lowpass(&mut self, lowpass: u32) -> &mut Self {
self.lowpass = lowpass;
self
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn min_duration(&self) -> Duration {
self.min_duration
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn with_min_duration(mut self, min_duration: Duration) -> Self {
self.set_min_duration(min_duration);
self
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn set_min_duration(&mut self, min_duration: Duration) -> &mut Self {
self.min_duration = min_duration;
self
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn with_min_frames(mut self, frames: u32, fps: Timebase) -> Self {
self.set_min_frames(frames, fps);
self
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn set_min_frames(&mut self, frames: u32, fps: Timebase) -> &mut Self {
self.min_duration = fps.frames_to_duration(frames);
self
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn initial_cut(&self) -> bool {
self.initial_cut
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn with_initial_cut(mut self, val: bool) -> Self {
self.initial_cut = val;
self
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn set_initial_cut(&mut self, val: bool) -> &mut Self {
self.initial_cut = val;
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, IsVariant, Error)]
#[non_exhaustive]
pub enum Error {
#[error("phash size ({size}) must be >= 2")]
SizeTooSmall {
size: u32,
},
#[error("phash lowpass ({lowpass}) must be >= 1")]
LowpassTooSmall {
lowpass: u32,
},
#[error("phash dimensions overflow usize: size ({size}) * lowpass ({lowpass}) squared")]
DimensionsOverflow {
size: u32,
lowpass: u32,
},
}
#[derive(Debug, Clone)]
pub struct Detector {
options: Options,
imsize: usize,
size: usize,
threshold: f64,
dct_cos: Vec<f32>,
resize_table: ResizeTable,
resized: Vec<f32>,
dct_tmp: Vec<f32>,
dct_result: Vec<f32>,
low_freq: Vec<f32>,
sort_scratch: Vec<f32>,
current_hash: Vec<u64>,
previous_hash: Vec<u64>,
has_previous: bool,
last_cut_ts: Option<Timestamp>,
last_distance: Option<f64>,
}
impl Detector {
pub fn new(options: Options) -> Self {
Self::try_new(options).expect("invalid phash Options")
}
pub fn try_new(options: Options) -> Result<Self, Error> {
if options.size < 2 {
return Err(Error::SizeTooSmall { size: options.size });
}
if options.lowpass < 1 {
return Err(Error::LowpassTooSmall {
lowpass: options.lowpass,
});
}
let size = options.size as usize;
let lowpass = options.lowpass as usize;
let imsize = match size.checked_mul(lowpass) {
Some(v) => v,
None => {
return Err(Error::DimensionsOverflow {
size: options.size,
lowpass: options.lowpass,
});
}
};
let total = match imsize.checked_mul(imsize) {
Some(v) => v,
None => {
return Err(Error::DimensionsOverflow {
size: options.size,
lowpass: options.lowpass,
});
}
};
let threshold = options.threshold;
let bits = size * size;
let hash_words = bits.div_ceil(64);
let dct_cos = build_dct_cos(imsize);
Ok(Self {
options,
imsize,
size,
threshold,
dct_cos,
resize_table: ResizeTable::new(),
resized: vec![0.0f32; total],
dct_tmp: vec![0.0f32; total],
dct_result: vec![0.0f32; total],
low_freq: vec![0.0f32; bits],
sort_scratch: vec![0.0f32; bits],
current_hash: vec![0u64; hash_words],
previous_hash: vec![0u64; hash_words],
has_previous: false,
last_cut_ts: None,
last_distance: None,
})
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn options(&self) -> &Options {
&self.options
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn last_distance(&self) -> Option<f64> {
self.last_distance
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub fn clear(&mut self) {
self.has_previous = false;
self.last_cut_ts = None;
self.last_distance = None;
}
pub fn process(&mut self, frame: LumaFrame<'_>) -> Option<Timestamp> {
let ts = frame.timestamp();
if self.last_cut_ts.is_none() {
self.last_cut_ts = Some(if self.options.initial_cut {
ts.saturating_sub_duration(self.options.min_duration)
} else {
ts
});
}
self.compute_hash(&frame);
let mut cut: Option<Timestamp> = None;
if self.has_previous {
let dist = hamming_distance(&self.previous_hash, &self.current_hash);
let bits = self.size * self.size;
let norm = dist as f64 / bits as f64;
self.last_distance = Some(norm);
let min_elapsed = self
.last_cut_ts
.as_ref()
.and_then(|last| ts.duration_since(last))
.is_some_and(|d| d >= self.options.min_duration);
if norm >= self.threshold && min_elapsed {
cut = Some(ts);
self.last_cut_ts = Some(ts);
}
}
core::mem::swap(&mut self.current_hash, &mut self.previous_hash);
self.has_previous = true;
cut
}
fn compute_hash(&mut self, frame: &LumaFrame<'_>) {
self
.resize_table
.ensure(frame.width(), frame.height(), self.imsize);
let max = self.resize_table.apply(
&mut self.resized,
frame.data(),
frame.stride() as usize,
self.imsize,
);
let scale = if max == 0.0 { 1.0 } else { 1.0 / max };
for v in self.resized.iter_mut() {
*v *= scale;
}
dct2(
&self.dct_cos,
&self.resized,
&mut self.dct_tmp,
&mut self.dct_result,
self.imsize,
);
for y in 0..self.size {
let src_row = &self.dct_result[y * self.imsize..y * self.imsize + self.size];
let dst_row = &mut self.low_freq[y * self.size..(y + 1) * self.size];
dst_row.copy_from_slice(src_row);
}
self.sort_scratch.clone_from(&self.low_freq);
let median = median_f32(&mut self.sort_scratch);
self.current_hash.fill(0);
for (i, &v) in self.low_freq.iter().enumerate() {
if v > median {
self.current_hash[i / 64] |= 1u64 << (i % 64);
}
}
}
}
fn build_dct_cos(n: usize) -> Vec<f32> {
let mut c = vec![0.0f32; n * n];
let alpha0 = sqrt_32(1.0 / n as f32);
let alpha_k = sqrt_32(2.0 / n as f32);
for k in 0..n {
let a = if k == 0 { alpha0 } else { alpha_k };
for m in 0..n {
let angle = PI * (2.0 * m as f32 + 1.0) * k as f32 / (2.0 * n as f32);
c[k * n + m] = a * cos_32(angle);
}
}
c
}
fn dct2(c: &[f32], input: &[f32], tmp: &mut [f32], result: &mut [f32], n: usize) {
debug_assert_eq!(c.len(), n * n);
debug_assert_eq!(input.len(), n * n);
debug_assert_eq!(tmp.len(), n * n);
debug_assert_eq!(result.len(), n * n);
for m in 0..n {
for j in 0..n {
let mut s = 0.0f32;
for k in 0..n {
s += input[m * n + k] * c[j * n + k];
}
tmp[m * n + j] = s;
}
}
for k in 0..n {
for j in 0..n {
let mut s = 0.0f32;
for m in 0..n {
s += c[k * n + m] * tmp[m * n + j];
}
result[k * n + j] = s;
}
}
}
#[derive(Debug, Clone)]
struct ResizeTable {
src_w: u32,
src_h: u32,
inv_area: f32,
x_offsets: Vec<u32>,
x_weights: Vec<f32>,
x_range_starts: Vec<u32>,
y_offsets: Vec<u32>,
y_weights: Vec<f32>,
y_range_starts: Vec<u32>,
}
impl ResizeTable {
fn new() -> Self {
Self {
src_w: 0,
src_h: 0,
inv_area: 0.0,
x_offsets: Vec::new(),
x_weights: Vec::new(),
x_range_starts: Vec::new(),
y_offsets: Vec::new(),
y_weights: Vec::new(),
y_range_starts: Vec::new(),
}
}
fn ensure(&mut self, src_w: u32, src_h: u32, dst_size: usize) {
if self.src_w == src_w && self.src_h == src_h {
return;
}
self.rebuild(src_w, src_h, dst_size);
}
fn rebuild(&mut self, src_w: u32, src_h: u32, dst_size: usize) {
debug_assert!(src_w > 0 && src_h > 0, "source dimensions must be non-zero");
debug_assert!(dst_size > 0);
self.x_offsets.clear();
self.x_weights.clear();
self.x_range_starts.clear();
self.y_offsets.clear();
self.y_weights.clear();
self.y_range_starts.clear();
let scale_x = src_w as f32 / dst_size as f32;
let scale_y = src_h as f32 / dst_size as f32;
build_axis(
&mut self.x_offsets,
&mut self.x_weights,
&mut self.x_range_starts,
src_w,
dst_size,
scale_x,
);
build_axis(
&mut self.y_offsets,
&mut self.y_weights,
&mut self.y_range_starts,
src_h,
dst_size,
scale_y,
);
self.inv_area = 1.0 / (scale_x * scale_y);
self.src_w = src_w;
self.src_h = src_h;
}
fn apply(&self, dst: &mut [f32], src: &[u8], src_stride: usize, dst_size: usize) -> f32 {
debug_assert_eq!(dst.len(), dst_size * dst_size);
debug_assert_eq!(self.x_range_starts.len(), dst_size + 1);
debug_assert_eq!(self.y_range_starts.len(), dst_size + 1);
let mut max = 0.0f32;
for dst_y in 0..dst_size {
let y_start = self.y_range_starts[dst_y] as usize;
let y_end = self.y_range_starts[dst_y + 1] as usize;
for dst_x in 0..dst_size {
let x_start = self.x_range_starts[dst_x] as usize;
let x_end = self.x_range_starts[dst_x + 1] as usize;
let mut sum = 0.0f32;
for yi in y_start..y_end {
let sy = self.y_offsets[yi] as usize;
let wy = self.y_weights[yi];
let row_off = sy * src_stride;
let mut row_sum = 0.0f32;
for xi in x_start..x_end {
let sx = self.x_offsets[xi] as usize;
row_sum += (src[row_off + sx] as f32) * self.x_weights[xi];
}
sum += row_sum * wy;
}
let v = sum * self.inv_area;
dst[dst_y * dst_size + dst_x] = v;
if v > max {
max = v;
}
}
}
max
}
}
fn build_axis(
offsets: &mut Vec<u32>,
weights: &mut Vec<f32>,
range_starts: &mut Vec<u32>,
src_size: u32,
dst_size: usize,
scale: f32,
) {
for dst in 0..dst_size {
range_starts.push(offsets.len() as u32);
let a = dst as f32 * scale;
let b = (dst + 1) as f32 * scale;
let s_start = floor_32(a) as u32;
let s_end = (ceil_32(b) as u32).min(src_size);
for s in s_start..s_end {
let w = ((s + 1) as f32).min(b) - (s as f32).max(a);
if w > 0.0 {
offsets.push(s);
weights.push(w);
}
}
}
range_starts.push(offsets.len() as u32);
}
fn median_f32(buf: &mut [f32]) -> f32 {
let n = buf.len();
debug_assert!(n > 0);
if n == 1 {
return buf[0];
}
let mid = n / 2;
let (left, pivot, _right) = buf.select_nth_unstable_by(mid, |a, b| a.total_cmp(b));
let m2 = *pivot;
if n % 2 == 1 {
m2
} else {
let m1 = left.iter().copied().fold(f32::NEG_INFINITY, f32::max);
(m1 + m2) / 2.0
}
}
#[cfg_attr(not(tarpaulin), inline(always))]
fn hamming_distance(a: &[u64], b: &[u64]) -> u32 {
debug_assert_eq!(a.len(), b.len());
a.iter()
.zip(b.iter())
.map(|(x, y)| (x ^ y).count_ones())
.sum()
}
#[cfg(all(test, feature = "std"))]
mod tests {
use super::*;
use crate::frame::Timebase;
use core::num::NonZeroU32;
use std::{vec, vec::Vec};
const fn nz32(n: u32) -> NonZeroU32 {
match NonZeroU32::new(n) {
Some(v) => v,
None => panic!("zero"),
}
}
fn make_frame<'a>(data: &'a [u8], w: u32, h: u32, pts: i64) -> LumaFrame<'a> {
let tb = Timebase::new(1, nz32(1000));
LumaFrame::new(data, w, h, w, Timestamp::new(pts, tb))
}
#[test]
fn with_min_frames_matches_python_default() {
let fps = Timebase::new(30, nz32(1));
let opts = Options::default().with_min_frames(15, fps);
assert_eq!(opts.min_duration(), Duration::from_millis(500));
}
#[test]
fn with_min_frames_ntsc() {
let fps = Timebase::new(30_000, nz32(1001));
let opts = Options::default().with_min_frames(15, fps);
assert_eq!(opts.min_duration(), Duration::from_nanos(500_500_000));
}
#[test]
fn try_new_success() {
let det = Detector::try_new(Options::default()).expect("defaults are valid");
assert_eq!(det.options().size(), 16);
assert_eq!(det.options().lowpass(), 2);
}
#[test]
fn try_new_rejects_size_too_small() {
let opts = Options::default().with_size(1);
let err = Detector::try_new(opts).expect_err("should fail");
assert_eq!(err, Error::SizeTooSmall { size: 1 });
let opts = Options::default().with_size(0);
let err = Detector::try_new(opts).expect_err("should fail");
assert_eq!(err, Error::SizeTooSmall { size: 0 });
}
#[test]
fn try_new_rejects_lowpass_zero() {
let opts = Options::default().with_lowpass(0);
let err = Detector::try_new(opts).expect_err("should fail");
assert_eq!(err, Error::LowpassTooSmall { lowpass: 0 });
}
#[test]
#[should_panic(expected = "invalid phash Options")]
fn new_panics_on_invalid() {
let _ = Detector::new(Options::default().with_size(1));
}
#[test]
fn error_display() {
let e = Error::SizeTooSmall { size: 1 };
assert_eq!(format!("{e}"), "phash size (1) must be >= 2");
let e = Error::LowpassTooSmall { lowpass: 0 };
assert_eq!(format!("{e}"), "phash lowpass (0) must be >= 1");
}
#[test]
fn hamming_distance_basic() {
assert_eq!(hamming_distance(&[0, 0], &[0, 0]), 0);
assert_eq!(hamming_distance(&[0xFF, 0], &[0, 0]), 8);
assert_eq!(hamming_distance(&[!0u64, !0u64], &[0, 0]), 128);
assert_eq!(hamming_distance(&[0b1010_1010], &[0b0101_0101]), 8);
}
#[test]
fn build_dct_cos_is_orthonormal() {
let n = 8;
let c = build_dct_cos(n);
for i in 0..n {
for j in 0..n {
let mut s = 0.0f32;
for k in 0..n {
s += c[i * n + k] * c[j * n + k];
}
let expected = if i == j { 1.0 } else { 0.0 };
assert!(
(s - expected).abs() < 1e-5,
"C·Cᵀ at ({i},{j}) = {s}, want {expected}",
);
}
}
}
#[test]
fn dct_dc_of_constant_input() {
let n = 8;
let c = build_dct_cos(n);
let input = vec![1.0f32; n * n];
let mut tmp = vec![0.0f32; n * n];
let mut result = vec![0.0f32; n * n];
dct2(&c, &input, &mut tmp, &mut result, n);
assert!((result[0] - n as f32).abs() < 1e-4, "DC = {}", result[0]);
(1..n * n).for_each(|k| {
assert!(result[k].abs() < 1e-4, "AC [{k}] = {}", result[k]);
});
}
#[test]
fn resize_area_identity() {
let src = [
10u8, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160,
];
let mut dst = vec![0.0f32; 16];
let mut table = ResizeTable::new();
table.ensure(4, 4, 4);
let max = table.apply(&mut dst, &src, 4, 4);
for i in 0..16 {
assert!((dst[i] - src[i] as f32).abs() < 1e-5);
}
assert!((max - 160.0).abs() < 1e-5);
}
#[test]
fn resize_area_halve() {
let src = [
10u8, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160,
];
let mut dst = vec![0.0f32; 4];
let mut table = ResizeTable::new();
table.ensure(4, 4, 2);
let max = table.apply(&mut dst, &src, 4, 2);
assert!((dst[0] - (10.0 + 20.0 + 50.0 + 60.0) / 4.0).abs() < 1e-4);
assert!((dst[1] - (30.0 + 40.0 + 70.0 + 80.0) / 4.0).abs() < 1e-4);
assert!((dst[2] - (90.0 + 100.0 + 130.0 + 140.0) / 4.0).abs() < 1e-4);
assert!((dst[3] - (110.0 + 120.0 + 150.0 + 160.0) / 4.0).abs() < 1e-4);
assert!((max - 135.0).abs() < 1e-4);
}
#[test]
fn resize_table_rebuild_on_dim_change() {
let mut table = ResizeTable::new();
table.ensure(1920, 1080, 32);
let counts_first = (table.x_offsets.len(), table.y_offsets.len());
table.ensure(1920, 1080, 32);
assert_eq!(table.x_offsets.len(), counts_first.0);
table.ensure(1280, 720, 32);
assert_ne!(table.x_offsets.len(), counts_first.0);
assert_eq!(table.src_w, 1280);
assert_eq!(table.src_h, 720);
}
#[test]
fn median_odd_and_even() {
let mut v = [5.0f32, 1.0, 3.0, 2.0, 4.0];
assert_eq!(median_f32(&mut v), 3.0);
let mut v = [5.0f32, 1.0, 3.0, 2.0, 4.0, 6.0];
assert_eq!(median_f32(&mut v), (3.0 + 4.0) / 2.0);
}
#[test]
fn identical_frames_produce_no_cut() {
let mut det = Detector::new(Options::default());
let mut buf = vec![0u8; 128 * 96];
for (i, b) in buf.iter_mut().enumerate() {
*b = ((i * 7) % 256) as u8;
}
assert!(det.process(make_frame(&buf, 128, 96, 0)).is_none());
assert!(det.process(make_frame(&buf, 128, 96, 2000)).is_none());
assert!(det.process(make_frame(&buf, 128, 96, 4000)).is_none());
assert_eq!(det.last_distance(), Some(0.0));
}
fn ortho_halves_frames() -> (Vec<u8>, Vec<u8>) {
let mut top_bottom = vec![0u8; 128 * 96];
for y in 0..96 {
for x in 0..128 {
top_bottom[y * 128 + x] = if y < 48 { 220 } else { 30 };
}
}
let mut left_right = vec![0u8; 128 * 96];
for y in 0..96 {
for x in 0..128 {
left_right[y * 128 + x] = if x < 64 { 220 } else { 30 };
}
}
(top_bottom, left_right)
}
#[test]
fn very_different_frames_produce_cut() {
let opts = Options::default().with_min_duration(Duration::from_millis(0));
let mut det = Detector::new(opts);
let (a, b) = ortho_halves_frames();
assert!(det.process(make_frame(&a, 128, 96, 0)).is_none());
let cut = det.process(make_frame(&b, 128, 96, 33));
assert!(
cut.is_some(),
"expected cut between top/bottom and left/right halves"
);
assert!(
det.last_distance().unwrap() >= Options::default().threshold(),
"distance {} should meet default threshold 0.395",
det.last_distance().unwrap(),
);
}
#[test]
fn min_duration_suppresses_rapid_cuts() {
let opts = Options::default()
.with_min_duration(Duration::from_secs(1))
.with_initial_cut(false);
let mut det = Detector::new(opts);
let (a, b) = ortho_halves_frames();
let mut cuts = 0u32;
for i in 0..30i64 {
let frame_data = if i % 2 == 0 { &a } else { &b };
let ts = i * 33;
if det.process(make_frame(frame_data, 128, 96, ts)).is_some() {
cuts += 1;
}
}
assert_eq!(cuts, 0, "min_duration should suppress all cuts within 1s");
}
#[test]
#[cfg_attr(miri, ignore)] fn clear_resets_stream_state() {
let opts = Options::default().with_min_duration(Duration::from_millis(0));
let mut det = Detector::new(opts);
let (a, b) = ortho_halves_frames();
assert!(det.process(make_frame(&a, 128, 96, 0)).is_none());
let cut1 = det.process(make_frame(&b, 128, 96, 33));
assert!(cut1.is_some());
assert!(det.last_distance().is_some());
det.clear();
assert!(det.process(make_frame(&a, 128, 96, 1_000_000)).is_none());
assert!(
det.last_distance().is_none(),
"last_distance should be cleared"
);
let cut2 = det.process(make_frame(&b, 128, 96, 1_000_033));
assert!(cut2.is_some());
}
#[test]
fn clear_preserves_resize_table_when_dims_match() {
let opts = Options::default().with_min_duration(Duration::from_millis(0));
let mut det = Detector::new(opts);
let (a, _) = ortho_halves_frames();
det.process(make_frame(&a, 128, 96, 0));
assert_eq!(det.resize_table.src_w, 128);
assert_eq!(det.resize_table.src_h, 96);
let x_offsets_len = det.resize_table.x_offsets.len();
det.clear();
assert_eq!(det.resize_table.src_w, 128);
assert_eq!(det.resize_table.src_h, 96);
assert_eq!(det.resize_table.x_offsets.len(), x_offsets_len);
}
#[test]
fn hash_bit_packing_matches_layout() {
let mut det = Detector::new(Options::default());
let size = det.size;
for i in 0..(size * size) {
det.low_freq[i] = if i % 2 == 0 { -1.0 } else { 1.0 };
}
det.sort_scratch.clone_from(&det.low_freq);
det.sort_scratch.sort_unstable_by(|a, b| a.total_cmp(b));
let n = det.sort_scratch.len();
let median = (det.sort_scratch[n / 2 - 1] + det.sort_scratch[n / 2]) / 2.0;
det.current_hash.fill(0);
for (i, &v) in det.low_freq.iter().enumerate() {
if v > median {
det.current_hash[i / 64] |= 1u64 << (i % 64);
}
}
let set: u32 = det.current_hash.iter().map(|w| w.count_ones()).sum();
assert_eq!(set as usize, size * size / 2);
}
#[test]
fn options_accessors_builders_setters_roundtrip() {
let fps30 = Timebase::new(30, nz32(1));
let opts = Options::default()
.with_threshold(0.5)
.with_size(32)
.with_lowpass(4)
.with_min_duration(core::time::Duration::from_millis(333))
.with_initial_cut(false);
assert_eq!(opts.threshold(), 0.5);
assert_eq!(opts.size(), 32);
assert_eq!(opts.lowpass(), 4);
assert_eq!(opts.min_duration(), core::time::Duration::from_millis(333));
assert!(!opts.initial_cut());
let opts_frames = Options::default().with_min_frames(15, fps30);
assert_eq!(
opts_frames.min_duration(),
core::time::Duration::from_millis(500)
);
let mut opts = Options::default();
opts
.set_threshold(0.1)
.set_size(8)
.set_lowpass(2)
.set_min_duration(core::time::Duration::from_secs(1))
.set_initial_cut(true);
assert_eq!(opts.threshold(), 0.1);
assert_eq!(opts.size(), 8);
assert_eq!(opts.lowpass(), 2);
assert!(opts.initial_cut());
opts.set_min_frames(30, fps30);
assert_eq!(opts.min_duration(), core::time::Duration::from_secs(1));
}
#[test]
fn try_new_rejects_imsize_squared_overflow() {
let opts = Options::default().with_size(100_000).with_lowpass(100_000);
let err = Detector::try_new(opts).expect_err("imsize*imsize should overflow");
assert_eq!(
err,
Error::DimensionsOverflow {
size: 100_000,
lowpass: 100_000,
},
);
}
#[test]
fn median_f32_singleton() {
let mut buf = [42.0f32];
assert_eq!(super::median_f32(&mut buf), 42.0);
}
}