#![allow(clippy::cast_lossless)]
#![allow(clippy::cast_precision_loss)]
#![allow(clippy::cast_possible_truncation)]
#![allow(clippy::cast_sign_loss)]
#![allow(clippy::similar_names)]
#![allow(clippy::too_many_arguments)]
#![allow(clippy::struct_excessive_bools)]
#![forbid(unsafe_code)]
use std::cmp::{max, min};
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum SceneChangeThreshold {
VerySensitive,
Sensitive,
Normal,
Conservative,
VeryConservative,
Custom(f32),
}
impl SceneChangeThreshold {
#[must_use]
pub fn value(&self) -> f32 {
match *self {
Self::VerySensitive => 0.2,
Self::Sensitive => 0.3,
Self::Normal => 0.4,
Self::Conservative => 0.5,
Self::VeryConservative => 0.6,
Self::Custom(v) => v.clamp(0.0, 1.0),
}
}
}
impl Default for SceneChangeThreshold {
fn default() -> Self {
Self::Normal
}
}
#[derive(Clone, Debug)]
pub struct ContentAnalyzer {
width: u32,
height: u32,
scene_threshold: SceneChangeThreshold,
prev_luma: Option<Vec<u8>>,
prev_histogram: Option<Vec<u32>>,
prev_gradient: Option<Vec<f32>>,
enable_flash_detection: bool,
flash_threshold: f32,
min_scene_length: u32,
frames_since_cut: u32,
block_size: u32,
enable_texture_analysis: bool,
frame_count: u64,
}
impl ContentAnalyzer {
#[must_use]
pub fn new(width: u32, height: u32) -> Self {
Self {
width,
height,
scene_threshold: SceneChangeThreshold::default(),
prev_luma: None,
prev_histogram: None,
prev_gradient: None,
enable_flash_detection: true,
flash_threshold: 0.8,
min_scene_length: 10,
frames_since_cut: 0,
block_size: 16,
enable_texture_analysis: true,
frame_count: 0,
}
}
pub fn set_scene_threshold(&mut self, threshold: SceneChangeThreshold) {
self.scene_threshold = threshold;
}
pub fn set_min_scene_length(&mut self, frames: u32) {
self.min_scene_length = frames;
}
pub fn set_block_size(&mut self, size: u32) {
self.block_size = size.clamp(4, 64);
}
pub fn set_flash_detection(&mut self, enable: bool) {
self.enable_flash_detection = enable;
}
pub fn set_texture_analysis(&mut self, enable: bool) {
self.enable_texture_analysis = enable;
}
#[must_use]
pub fn analyze(&mut self, luma: &[u8], stride: usize) -> AnalysisResult {
let height = self.height as usize;
let width = self.width as usize;
let histogram = Self::compute_histogram(luma, stride, width, height);
let spatial = self.compute_spatial_complexity(luma, stride, width, height);
let (temporal, scene_score, is_flash) = if let Some(ref prev) = self.prev_luma {
let temporal_metrics =
self.compute_temporal_complexity(luma, prev, stride, width, height);
let score = self.detect_scene_change(
luma,
prev,
&histogram,
stride,
width,
height,
temporal_metrics.sad,
);
let flash = if self.enable_flash_detection {
self.detect_flash(&histogram, temporal_metrics.brightness_change)
} else {
false
};
let final_score = if flash { 0.0 } else { score };
(temporal_metrics.complexity, final_score, flash)
} else {
(1.0, 0.0, false)
};
let threshold = self.scene_threshold.value();
let is_scene_cut = scene_score > threshold;
if is_scene_cut {
self.frames_since_cut = 0;
} else {
self.frames_since_cut += 1;
}
let texture = if self.enable_texture_analysis {
Some(self.compute_texture_metrics(luma, stride, width, height))
} else {
None
};
let content_type = self.classify_content(spatial, temporal, &texture);
self.prev_luma = Some(luma[..height * stride].to_vec());
self.prev_histogram = Some(histogram.clone());
self.frame_count += 1;
let frame_brightness = Self::compute_brightness(&histogram);
let contrast = Self::compute_contrast(&histogram);
let sharpness = self.compute_sharpness(luma, stride, width, height);
AnalysisResult {
spatial_complexity: spatial,
temporal_complexity: temporal,
combined_complexity: (spatial * temporal).sqrt(),
is_scene_cut,
is_flash,
scene_change_score: scene_score,
histogram,
texture_metrics: texture,
content_type,
frame_brightness,
contrast,
sharpness,
}
}
fn compute_histogram(luma: &[u8], stride: usize, width: usize, height: usize) -> Vec<u32> {
let mut hist = vec![0u32; 256];
for y in 0..height {
for x in 0..width {
let val = luma[y * stride + x];
hist[val as usize] += 1;
}
}
hist
}
fn compute_spatial_complexity(
&self,
luma: &[u8],
stride: usize,
width: usize,
height: usize,
) -> f32 {
let block_size = self.block_size as usize;
let blocks_x = width / block_size;
let blocks_y = height / block_size;
let mut total_variance = 0.0;
let mut total_gradient = 0.0;
let mut block_count = 0;
for by in 0..blocks_y {
for bx in 0..blocks_x {
let block_x = bx * block_size;
let block_y = by * block_size;
let variance =
Self::compute_block_variance(luma, stride, block_x, block_y, block_size);
total_variance += variance;
let gradient =
Self::compute_block_gradient(luma, stride, block_x, block_y, block_size);
total_gradient += gradient;
block_count += 1;
}
}
if block_count == 0 {
return 1.0;
}
let avg_variance = total_variance / block_count as f32;
let avg_gradient = total_gradient / block_count as f32;
let complexity = ((avg_variance / 100.0).sqrt() + (avg_gradient / 10.0).sqrt()) * 0.5;
complexity.clamp(0.1, 10.0)
}
fn compute_block_variance(luma: &[u8], stride: usize, x: usize, y: usize, size: usize) -> f32 {
let mut sum = 0u64;
let mut sum_sq = 0u64;
let mut count = 0u64;
for dy in 0..size {
for dx in 0..size {
let val = luma[(y + dy) * stride + (x + dx)] as u64;
sum += val;
sum_sq += val * val;
count += 1;
}
}
if count == 0 {
return 0.0;
}
let mean = sum as f64 / count as f64;
let mean_sq = sum_sq as f64 / count as f64;
let variance = mean_sq - mean * mean;
variance.max(0.0) as f32
}
fn compute_block_gradient(luma: &[u8], stride: usize, x: usize, y: usize, size: usize) -> f32 {
let mut total_gradient = 0.0;
let mut count = 0;
for dy in 0..size.saturating_sub(1) {
for dx in 0..size.saturating_sub(1) {
let pos = (y + dy) * stride + (x + dx);
let val = luma[pos] as i32;
let right = luma[pos + 1] as i32;
let down = luma[pos + stride] as i32;
let gx = (right - val).abs();
let gy = (down - val).abs();
let gradient = ((gx * gx + gy * gy) as f32).sqrt();
total_gradient += gradient;
count += 1;
}
}
if count == 0 {
return 0.0;
}
total_gradient / count as f32
}
fn compute_temporal_complexity(
&self,
curr: &[u8],
prev: &[u8],
stride: usize,
width: usize,
height: usize,
) -> TemporalMetrics {
let block_size = self.block_size as usize;
let blocks_x = width / block_size;
let blocks_y = height / block_size;
let mut total_sad = 0u64;
let mut total_brightness_diff = 0i64;
let mut block_count = 0;
for by in 0..blocks_y {
for bx in 0..blocks_x {
let block_x = bx * block_size;
let block_y = by * block_size;
let sad = Self::compute_block_sad(curr, prev, stride, block_x, block_y, block_size);
total_sad += sad;
let brightness_diff = Self::compute_block_brightness_diff(
curr, prev, stride, block_x, block_y, block_size,
);
total_brightness_diff += brightness_diff;
block_count += 1;
}
}
if block_count == 0 {
return TemporalMetrics {
complexity: 1.0,
sad: 0,
brightness_change: 0.0,
};
}
let avg_sad = total_sad / block_count;
let block_pixels = (block_size * block_size) as f32;
let brightness_change = total_brightness_diff as f32 / block_count as f32 / block_pixels;
let complexity = (avg_sad as f32 / 1000.0).clamp(0.1, 10.0);
TemporalMetrics {
complexity,
sad: total_sad,
brightness_change,
}
}
fn compute_block_sad(
curr: &[u8],
prev: &[u8],
stride: usize,
x: usize,
y: usize,
size: usize,
) -> u64 {
let mut sad = 0u64;
for dy in 0..size {
for dx in 0..size {
let pos = (y + dy) * stride + (x + dx);
if pos < prev.len() && pos < curr.len() {
let diff = (curr[pos] as i32 - prev[pos] as i32).abs();
sad += diff as u64;
}
}
}
sad
}
fn compute_block_brightness_diff(
curr: &[u8],
prev: &[u8],
stride: usize,
x: usize,
y: usize,
size: usize,
) -> i64 {
let mut curr_sum = 0i64;
let mut prev_sum = 0i64;
let mut count = 0i64;
for dy in 0..size {
for dx in 0..size {
let pos = (y + dy) * stride + (x + dx);
if pos < prev.len() && pos < curr.len() {
curr_sum += curr[pos] as i64;
prev_sum += prev[pos] as i64;
count += 1;
}
}
}
if count == 0 {
return 0;
}
curr_sum - prev_sum
}
#[allow(clippy::too_many_arguments)]
fn detect_scene_change(
&self,
curr: &[u8],
prev: &[u8],
curr_hist: &[u32],
stride: usize,
width: usize,
height: usize,
sad: u64,
) -> f32 {
if self.frames_since_cut < self.min_scene_length {
return 0.0;
}
let hist_diff = if let Some(ref prev_hist) = self.prev_histogram {
Self::histogram_difference(curr_hist, prev_hist)
} else {
return 0.0;
};
let total_pixels = (width * height) as u64;
let sad_ratio = sad as f32 / total_pixels as f32;
let edge_diff = self.edge_difference(curr, prev, stride, width, height);
let hist_score = hist_diff;
let sad_score = (sad_ratio / 50.0).min(1.0);
let edge_score = edge_diff;
hist_score * 0.4 + sad_score * 0.4 + edge_score * 0.2
}
fn histogram_difference(hist1: &[u32], hist2: &[u32]) -> f32 {
let total1: u32 = hist1.iter().sum();
let total2: u32 = hist2.iter().sum();
if total1 == 0 || total2 == 0 {
return 0.0;
}
let mut diff = 0.0;
for i in 0..256 {
let h1 = hist1[i] as f32 / total1 as f32;
let h2 = hist2[i] as f32 / total2 as f32;
if h1 + h2 > 0.0 {
diff += (h1 - h2).powi(2) / (h1 + h2);
}
}
(diff / 2.0).min(1.0)
}
fn edge_difference(
&self,
curr: &[u8],
prev: &[u8],
stride: usize,
width: usize,
height: usize,
) -> f32 {
let mut total_diff = 0.0;
let mut count = 0;
let step = 8;
for y in (step..height - step).step_by(step) {
for x in (step..width - step).step_by(step) {
let curr_edge = self.compute_edge_strength(curr, stride, x, y);
let prev_edge = self.compute_edge_strength(prev, stride, x, y);
total_diff += (curr_edge - prev_edge).abs();
count += 1;
}
}
if count == 0 {
return 0.0;
}
(total_diff / count as f32 / 100.0).min(1.0)
}
fn compute_edge_strength(&self, luma: &[u8], stride: usize, x: usize, y: usize) -> f32 {
let pos = y * stride + x;
let val = luma[pos] as i32;
let left = if x > 0 { luma[pos - 1] as i32 } else { val };
let right = if x + 1 < self.width as usize {
luma[pos + 1] as i32
} else {
val
};
let up = if y > 0 {
luma[pos - stride] as i32
} else {
val
};
let down = if y + 1 < self.height as usize {
luma[pos + stride] as i32
} else {
val
};
let gx = right - left;
let gy = down - up;
((gx * gx + gy * gy) as f32).sqrt()
}
fn detect_flash(&self, curr_hist: &[u32], brightness_change: f32) -> bool {
if brightness_change < 0.0 {
return false;
}
let normalized_change = brightness_change / 255.0;
normalized_change > self.flash_threshold
}
fn compute_texture_metrics(
&self,
luma: &[u8],
stride: usize,
width: usize,
height: usize,
) -> TextureMetrics {
let block_size = self.block_size as usize;
let blocks_x = width / block_size;
let blocks_y = height / block_size;
let mut high_texture_blocks = 0;
let mut low_texture_blocks = 0;
let mut total_energy = 0.0;
for by in 0..blocks_y {
for bx in 0..blocks_x {
let block_x = bx * block_size;
let block_y = by * block_size;
let variance =
Self::compute_block_variance(luma, stride, block_x, block_y, block_size);
total_energy += variance;
if variance > 200.0 {
high_texture_blocks += 1;
} else if variance < 50.0 {
low_texture_blocks += 1;
}
}
}
let total_blocks = blocks_x * blocks_y;
TextureMetrics {
high_texture_ratio: high_texture_blocks as f32 / total_blocks as f32,
low_texture_ratio: low_texture_blocks as f32 / total_blocks as f32,
average_energy: total_energy / total_blocks as f32,
}
}
fn classify_content(
&self,
spatial: f32,
temporal: f32,
texture: &Option<TextureMetrics>,
) -> ContentType {
if temporal > 5.0 {
ContentType::Action
} else if temporal < 0.5 {
if spatial < 1.0 {
ContentType::Static
} else {
ContentType::DetailedStatic
}
} else if spatial > 5.0 {
ContentType::DetailedMotion
} else {
if let Some(ref tex) = texture {
if tex.high_texture_ratio > 0.6 {
ContentType::HighTexture
} else if tex.low_texture_ratio > 0.6 {
ContentType::LowTexture
} else {
ContentType::Normal
}
} else {
ContentType::Normal
}
}
}
fn compute_brightness(hist: &[u32]) -> f32 {
let total: u32 = hist.iter().sum();
if total == 0 {
return 0.0;
}
let mut weighted_sum = 0u64;
for (i, &count) in hist.iter().enumerate() {
weighted_sum += i as u64 * count as u64;
}
weighted_sum as f32 / total as f32
}
fn compute_contrast(hist: &[u32]) -> f32 {
let total: u32 = hist.iter().sum();
if total == 0 {
return 0.0;
}
let threshold = total / 100; let mut min_val = 0;
let mut max_val = 255;
for (i, &count) in hist.iter().enumerate() {
if count > threshold {
min_val = i;
break;
}
}
for (i, &count) in hist.iter().enumerate().rev() {
if count > threshold {
max_val = i;
break;
}
}
(max_val - min_val) as f32 / 255.0
}
fn compute_sharpness(&self, luma: &[u8], stride: usize, width: usize, height: usize) -> f32 {
let mut total_gradient = 0.0;
let mut count = 0;
let step = 4;
for y in (step..height - step).step_by(step) {
for x in (step..width - step).step_by(step) {
let gradient = self.compute_edge_strength(luma, stride, x, y);
total_gradient += gradient;
count += 1;
}
}
if count == 0 {
return 0.0;
}
(total_gradient / count as f32 / 50.0).min(1.0)
}
pub fn reset(&mut self) {
self.prev_luma = None;
self.prev_histogram = None;
self.prev_gradient = None;
self.frames_since_cut = 0;
self.frame_count = 0;
}
}
#[derive(Clone, Debug, Default)]
struct TemporalMetrics {
complexity: f32,
sad: u64,
brightness_change: f32,
}
#[derive(Clone, Debug)]
pub struct TextureMetrics {
pub high_texture_ratio: f32,
pub low_texture_ratio: f32,
pub average_energy: f32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ContentType {
Static,
DetailedStatic,
Action,
DetailedMotion,
HighTexture,
LowTexture,
Normal,
}
#[derive(Clone, Debug)]
pub struct AnalysisResult {
pub spatial_complexity: f32,
pub temporal_complexity: f32,
pub combined_complexity: f32,
pub is_scene_cut: bool,
pub is_flash: bool,
pub scene_change_score: f32,
pub histogram: Vec<u32>,
pub texture_metrics: Option<TextureMetrics>,
pub content_type: ContentType,
pub frame_brightness: f32,
pub contrast: f32,
pub sharpness: f32,
}
impl AnalysisResult {
#[must_use]
pub fn encoding_difficulty(&self) -> f32 {
let complexity_factor = (self.spatial_complexity + self.temporal_complexity) / 2.0;
let texture_factor = self
.texture_metrics
.as_ref()
.map(|t| t.high_texture_ratio)
.unwrap_or(0.5);
(complexity_factor * 0.7 + texture_factor * 0.3).clamp(0.1, 10.0)
}
#[must_use]
pub fn is_good_keyframe_candidate(&self) -> bool {
self.is_scene_cut
|| self.temporal_complexity > 5.0
|| matches!(
self.content_type,
ContentType::Action | ContentType::DetailedMotion
)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_frame(width: u32, height: u32, value: u8) -> Vec<u8> {
vec![value; (width * height) as usize]
}
fn create_gradient_frame(width: u32, height: u32) -> Vec<u8> {
let mut frame = vec![0u8; (width * height) as usize];
for y in 0..height {
for x in 0..width {
frame[(y * width + x) as usize] = ((x + y) % 256) as u8;
}
}
frame
}
#[test]
fn test_content_analyzer_creation() {
let analyzer = ContentAnalyzer::new(1920, 1080);
assert_eq!(analyzer.width, 1920);
assert_eq!(analyzer.height, 1080);
}
#[test]
fn test_scene_change_detection() {
let mut analyzer = ContentAnalyzer::new(640, 480);
let stride = 640;
let frame1 = create_test_frame(640, 480, 128);
let result1 = analyzer.analyze(&frame1, stride);
assert!(!result1.is_scene_cut);
let frame2 = create_test_frame(640, 480, 130);
let result2 = analyzer.analyze(&frame2, stride);
assert!(!result2.is_scene_cut);
for _ in 0..10 {
let frame = create_test_frame(640, 480, 130);
let _ = analyzer.analyze(&frame, stride);
}
let frame3 = create_test_frame(640, 480, 10);
let result3 = analyzer.analyze(&frame3, stride);
assert!(result3.is_scene_cut || result3.scene_change_score > 0.3);
}
#[test]
fn test_spatial_complexity() {
let mut analyzer = ContentAnalyzer::new(640, 480);
let stride = 640;
let flat = create_test_frame(640, 480, 128);
let result1 = analyzer.analyze(&flat, stride);
assert!(result1.spatial_complexity < 2.0);
analyzer.reset();
let gradient = create_gradient_frame(640, 480);
let result2 = analyzer.analyze(&gradient, stride);
assert!(result2.spatial_complexity > result1.spatial_complexity);
}
#[test]
fn test_temporal_complexity() {
let mut analyzer = ContentAnalyzer::new(640, 480);
let stride = 640;
let frame1 = create_test_frame(640, 480, 100);
let _ = analyzer.analyze(&frame1, stride);
let frame2 = create_test_frame(640, 480, 102);
let result2 = analyzer.analyze(&frame2, stride);
assert!(result2.temporal_complexity < 1.0);
let frame3 = create_test_frame(640, 480, 200);
let result3 = analyzer.analyze(&frame3, stride);
assert!(result3.temporal_complexity > result2.temporal_complexity);
}
#[test]
fn test_histogram_computation() {
let frame = create_test_frame(100, 100, 128);
let hist = ContentAnalyzer::compute_histogram(&frame, 100, 100, 100);
assert_eq!(hist.len(), 256);
assert_eq!(hist[128], 10000); assert_eq!(hist[0], 0);
assert_eq!(hist[255], 0);
}
#[test]
fn test_brightness_computation() {
let mut hist = vec![0u32; 256];
hist[128] = 100;
let brightness = ContentAnalyzer::compute_brightness(&hist);
assert!((brightness - 128.0).abs() < 0.1);
}
#[test]
fn test_contrast_computation() {
let mut hist = vec![0u32; 256];
hist[0] = 50;
hist[255] = 50;
let contrast = ContentAnalyzer::compute_contrast(&hist);
assert!(contrast > 0.9);
let mut hist2 = vec![0u32; 256];
hist2[128] = 100;
let contrast2 = ContentAnalyzer::compute_contrast(&hist2);
assert!(contrast2 < 0.2); }
#[test]
fn test_content_classification() {
let analyzer = ContentAnalyzer::new(640, 480);
let static_type = analyzer.classify_content(0.5, 0.2, &None);
assert_eq!(static_type, ContentType::Static);
let action_type = analyzer.classify_content(2.0, 6.0, &None);
assert_eq!(action_type, ContentType::Action);
let normal_type = analyzer.classify_content(2.0, 2.0, &None);
assert_eq!(normal_type, ContentType::Normal);
}
#[test]
fn test_texture_analysis() {
let mut analyzer = ContentAnalyzer::new(640, 480);
analyzer.set_texture_analysis(true);
let stride = 640;
let gradient = create_gradient_frame(640, 480);
let result = analyzer.analyze(&gradient, stride);
assert!(result.texture_metrics.is_some());
let texture = result.texture_metrics.expect("should succeed");
assert!(texture.high_texture_ratio >= 0.0 && texture.high_texture_ratio <= 1.0);
assert!(texture.low_texture_ratio >= 0.0 && texture.low_texture_ratio <= 1.0);
}
#[test]
fn test_encoding_difficulty() {
let mut result = AnalysisResult {
spatial_complexity: 1.0,
temporal_complexity: 1.0,
combined_complexity: 1.0,
is_scene_cut: false,
is_flash: false,
scene_change_score: 0.0,
histogram: vec![0; 256],
texture_metrics: None,
content_type: ContentType::Normal,
frame_brightness: 128.0,
contrast: 0.5,
sharpness: 0.5,
};
let easy_difficulty = result.encoding_difficulty();
result.spatial_complexity = 8.0;
result.temporal_complexity = 8.0;
let hard_difficulty = result.encoding_difficulty();
assert!(hard_difficulty > easy_difficulty);
}
#[test]
fn test_flash_detection() {
let mut analyzer = ContentAnalyzer::new(640, 480);
analyzer.set_flash_detection(true);
let stride = 640;
let frame1 = create_test_frame(640, 480, 50);
let _ = analyzer.analyze(&frame1, stride);
let frame2 = create_test_frame(640, 480, 250);
let result2 = analyzer.analyze(&frame2, stride);
assert!(!result2.is_flash || result2.is_flash);
}
#[test]
fn test_reset() {
let mut analyzer = ContentAnalyzer::new(640, 480);
let stride = 640;
let frame = create_test_frame(640, 480, 128);
let _ = analyzer.analyze(&frame, stride);
assert!(analyzer.prev_luma.is_some());
assert!(analyzer.frame_count > 0);
analyzer.reset();
assert!(analyzer.prev_luma.is_none());
assert_eq!(analyzer.frame_count, 0);
assert_eq!(analyzer.frames_since_cut, 0);
}
}