#![allow(dead_code)]
#[derive(Debug, Clone, PartialEq)]
pub struct FrameLuminance {
pub frame_index: u64,
pub average_luma: f64,
pub min_luma: f64,
pub max_luma: f64,
}
impl FrameLuminance {
#[must_use]
pub fn new(frame_index: u64, average_luma: f64, min_luma: f64, max_luma: f64) -> Self {
Self {
frame_index,
average_luma,
min_luma,
max_luma,
}
}
#[must_use]
pub fn variation_from(&self, other: &Self) -> f64 {
(self.average_luma - other.average_luma).abs()
}
#[must_use]
pub fn dynamic_range(&self) -> f64 {
self.max_luma - self.min_luma
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum FlickerSeverity {
None,
Minor,
Moderate,
Severe,
}
impl FlickerSeverity {
#[must_use]
pub fn from_variation(variation: f64) -> Self {
if variation < 0.005 {
Self::None
} else if variation < 0.02 {
Self::Minor
} else if variation < 0.05 {
Self::Moderate
} else {
Self::Severe
}
}
}
#[derive(Debug, Clone)]
pub struct TemporalFlicker {
pub max_variation: f64,
pub avg_variation: f64,
pub flicker_frame_count: usize,
pub total_frames: usize,
pub threshold: f64,
}
impl TemporalFlicker {
#[must_use]
pub fn severity(&self) -> FlickerSeverity {
FlickerSeverity::from_variation(self.max_variation)
}
#[must_use]
pub fn has_flicker(&self) -> bool {
self.flicker_frame_count > 0
}
#[allow(clippy::cast_precision_loss)]
#[must_use]
pub fn flicker_pct(&self) -> f64 {
if self.total_frames == 0 {
return 0.0;
}
(self.flicker_frame_count as f64 / self.total_frames as f64) * 100.0
}
}
#[derive(Debug, Clone)]
pub struct TemporalUniformityAnalyzer {
frames: Vec<FrameLuminance>,
pub flicker_threshold: f64,
}
impl TemporalUniformityAnalyzer {
#[must_use]
pub fn new() -> Self {
Self {
frames: Vec::new(),
flicker_threshold: 0.02,
}
}
#[must_use]
pub fn with_threshold(threshold: f64) -> Self {
Self {
frames: Vec::new(),
flicker_threshold: threshold,
}
}
pub fn add_frame(&mut self, frame: FrameLuminance) {
self.frames.push(frame);
}
#[must_use]
pub fn frame_count(&self) -> usize {
self.frames.len()
}
#[must_use]
pub fn frames(&self) -> &[FrameLuminance] {
&self.frames
}
#[allow(clippy::cast_precision_loss)]
#[must_use]
pub fn detect_flicker(&self) -> Option<TemporalFlicker> {
if self.frames.len() < 2 {
return None;
}
let mut max_variation = 0.0_f64;
let mut total_variation = 0.0_f64;
let mut flicker_frame_count = 0_usize;
for window in self.frames.windows(2) {
let var = window[0].variation_from(&window[1]);
total_variation += var;
if var > max_variation {
max_variation = var;
}
if var > self.flicker_threshold {
flicker_frame_count += 1;
}
}
let pair_count = (self.frames.len() - 1) as f64;
let avg_variation = total_variation / pair_count;
Some(TemporalFlicker {
max_variation,
avg_variation,
flicker_frame_count,
total_frames: self.frames.len(),
threshold: self.flicker_threshold,
})
}
#[allow(clippy::cast_precision_loss)]
#[must_use]
pub fn mean_luminance(&self) -> f64 {
if self.frames.is_empty() {
return 0.0;
}
let sum: f64 = self.frames.iter().map(|f| f.average_luma).sum();
sum / self.frames.len() as f64
}
}
impl Default for TemporalUniformityAnalyzer {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn variation_from_same_frame() {
let f = FrameLuminance::new(0, 0.5, 0.1, 0.9);
assert!((f.variation_from(&f) - 0.0).abs() < 1e-12);
}
#[test]
fn variation_from_different_frames() {
let a = FrameLuminance::new(0, 0.5, 0.1, 0.9);
let b = FrameLuminance::new(1, 0.6, 0.1, 0.9);
assert!((a.variation_from(&b) - 0.1).abs() < 1e-12);
}
#[test]
fn dynamic_range_correct() {
let f = FrameLuminance::new(0, 0.5, 0.1, 0.9);
assert!((f.dynamic_range() - 0.8).abs() < 1e-12);
}
#[test]
fn severity_none_below_half_percent() {
assert_eq!(
FlickerSeverity::from_variation(0.004),
FlickerSeverity::None
);
}
#[test]
fn severity_minor_range() {
assert_eq!(
FlickerSeverity::from_variation(0.01),
FlickerSeverity::Minor
);
}
#[test]
fn severity_moderate_range() {
assert_eq!(
FlickerSeverity::from_variation(0.03),
FlickerSeverity::Moderate
);
}
#[test]
fn severity_severe_above_five_percent() {
assert_eq!(
FlickerSeverity::from_variation(0.06),
FlickerSeverity::Severe
);
}
#[test]
fn detect_flicker_returns_none_with_one_frame() {
let mut analyzer = TemporalUniformityAnalyzer::new();
analyzer.add_frame(FrameLuminance::new(0, 0.5, 0.1, 0.9));
assert!(analyzer.detect_flicker().is_none());
}
#[test]
fn detect_flicker_no_variation() {
let mut analyzer = TemporalUniformityAnalyzer::new();
for i in 0..5 {
analyzer.add_frame(FrameLuminance::new(i, 0.5, 0.1, 0.9));
}
let flicker = analyzer
.detect_flicker()
.expect("flicker detection should succeed");
assert_eq!(flicker.severity(), FlickerSeverity::None);
assert!(!flicker.has_flicker());
}
#[test]
fn detect_flicker_large_variation() {
let mut analyzer = TemporalUniformityAnalyzer::new();
analyzer.add_frame(FrameLuminance::new(0, 0.4, 0.0, 1.0));
analyzer.add_frame(FrameLuminance::new(1, 0.9, 0.0, 1.0)); let flicker = analyzer
.detect_flicker()
.expect("flicker detection should succeed");
assert_eq!(flicker.severity(), FlickerSeverity::Severe);
assert!(flicker.has_flicker());
}
#[test]
fn flicker_pct_half_frames() {
let mut analyzer = TemporalUniformityAnalyzer::with_threshold(0.05);
analyzer.add_frame(FrameLuminance::new(0, 0.0, 0.0, 1.0));
analyzer.add_frame(FrameLuminance::new(1, 0.5, 0.0, 1.0));
analyzer.add_frame(FrameLuminance::new(2, 0.5, 0.0, 1.0));
let flicker = analyzer
.detect_flicker()
.expect("flicker detection should succeed");
assert!(flicker.flicker_pct() > 0.0);
}
#[test]
fn mean_luminance_correct() {
let mut analyzer = TemporalUniformityAnalyzer::new();
analyzer.add_frame(FrameLuminance::new(0, 0.2, 0.0, 1.0));
analyzer.add_frame(FrameLuminance::new(1, 0.8, 0.0, 1.0));
assert!((analyzer.mean_luminance() - 0.5).abs() < 1e-12);
}
#[test]
fn mean_luminance_empty_is_zero() {
let analyzer = TemporalUniformityAnalyzer::new();
assert_eq!(analyzer.mean_luminance(), 0.0);
}
#[test]
fn frame_count_tracks_additions() {
let mut analyzer = TemporalUniformityAnalyzer::new();
assert_eq!(analyzer.frame_count(), 0);
analyzer.add_frame(FrameLuminance::new(0, 0.5, 0.0, 1.0));
assert_eq!(analyzer.frame_count(), 1);
}
}