pub mod generate;
pub mod output;
pub mod timestamps;
pub mod utils;
pub use generate::generate_sprite_sheet;
#[allow(unused_imports)]
pub use utils::{adjust_grid_for_count, calculate_optimal_grid, parse_timestamp};
pub use utils::{parse_duration, validate_and_adjust_config};
pub use crate::progress::TranscodeProgress;
pub use anyhow::{anyhow, Context, Result};
pub use colored::Colorize;
pub use serde::{Deserialize, Serialize};
pub use std::fmt;
pub use std::path::{Path, PathBuf};
pub use tracing::{debug, info};
const MAX_SPRITE_DIMENSION: u32 = 16384;
const DEFAULT_THUMB_WIDTH: u32 = 160;
const DEFAULT_THUMB_HEIGHT: u32 = 90;
const DEFAULT_GRID_COLS: usize = 5;
const DEFAULT_GRID_ROWS: usize = 5;
const DEFAULT_SPACING: u32 = 2;
const DEFAULT_MARGIN: u32 = 0;
#[derive(Debug, Clone)]
pub struct SpriteSheetOptions {
pub input: PathBuf,
pub output: PathBuf,
pub config: SpriteSheetConfig,
pub generate_vtt: bool,
pub vtt_output: Option<PathBuf>,
pub generate_manifest: bool,
pub manifest_output: Option<PathBuf>,
pub show_timestamps: bool,
pub json_output: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SpriteSheetConfig {
pub interval: Option<f64>,
pub count: Option<usize>,
pub thumbnail_width: u32,
pub thumbnail_height: u32,
pub columns: usize,
pub rows: usize,
pub format: ImageFormat,
pub quality: u8,
pub strategy: SamplingStrategy,
pub layout: LayoutMode,
pub spacing: u32,
pub margin: u32,
pub maintain_aspect_ratio: bool,
pub compression: u8,
}
impl Default for SpriteSheetConfig {
fn default() -> Self {
Self {
interval: None,
count: None,
thumbnail_width: DEFAULT_THUMB_WIDTH,
thumbnail_height: DEFAULT_THUMB_HEIGHT,
columns: DEFAULT_GRID_COLS,
rows: DEFAULT_GRID_ROWS,
format: ImageFormat::Png,
quality: 90,
strategy: SamplingStrategy::Uniform,
layout: LayoutMode::Grid,
spacing: DEFAULT_SPACING,
margin: DEFAULT_MARGIN,
maintain_aspect_ratio: true,
compression: 6,
}
}
}
impl SpriteSheetConfig {
pub fn total_thumbnails(&self) -> usize {
if let Some(count) = self.count {
count
} else {
self.columns * self.rows
}
}
pub fn sprite_dimensions(&self) -> (u32, u32) {
let cols = self.columns as u32;
let rows = self.rows as u32;
let width =
self.margin * 2 + self.thumbnail_width * cols + self.spacing * (cols.saturating_sub(1));
let height = self.margin * 2
+ self.thumbnail_height * rows
+ self.spacing * (rows.saturating_sub(1));
(width, height)
}
pub fn validate(&self) -> Result<()> {
if self.thumbnail_width == 0 || self.thumbnail_height == 0 {
return Err(anyhow!("Thumbnail dimensions must be greater than zero"));
}
if self.columns == 0 || self.rows == 0 {
return Err(anyhow!("Grid columns and rows must be greater than zero"));
}
let (width, height) = self.sprite_dimensions();
if width > MAX_SPRITE_DIMENSION || height > MAX_SPRITE_DIMENSION {
return Err(anyhow!(
"Sprite sheet dimensions ({}x{}) exceed maximum ({}x{})",
width,
height,
MAX_SPRITE_DIMENSION,
MAX_SPRITE_DIMENSION
));
}
if self.quality > 100 {
return Err(anyhow!("Quality must be between 0 and 100"));
}
if self.compression > 9 {
return Err(anyhow!("Compression level must be between 0 and 9"));
}
if self.count.is_none() && self.interval.is_none() {
return Err(anyhow!("Either count or interval must be specified"));
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ImageFormat {
Png,
Jpeg,
Webp,
}
impl ImageFormat {
pub fn from_str(s: &str) -> Result<Self> {
match s.to_lowercase().as_str() {
"png" => Ok(Self::Png),
"jpg" | "jpeg" => Ok(Self::Jpeg),
"webp" => Ok(Self::Webp),
_ => Err(anyhow!("Unsupported image format: {}", s)),
}
}
pub fn name(&self) -> &'static str {
match self {
Self::Png => "PNG",
Self::Jpeg => "JPEG",
Self::Webp => "WebP",
}
}
#[allow(dead_code)]
pub fn extension(&self) -> &'static str {
match self {
Self::Png => "png",
Self::Jpeg => "jpg",
Self::Webp => "webp",
}
}
pub fn is_lossless(&self) -> bool {
matches!(self, Self::Png)
}
}
impl fmt::Display for ImageFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.name())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum SamplingStrategy {
Uniform,
SceneBased,
KeyframeOnly,
Smart,
}
impl SamplingStrategy {
pub fn from_str(s: &str) -> Result<Self> {
match s.to_lowercase().as_str() {
"uniform" => Ok(Self::Uniform),
"scene" | "scene-based" => Ok(Self::SceneBased),
"keyframe" | "keyframe-only" => Ok(Self::KeyframeOnly),
"smart" => Ok(Self::Smart),
_ => Err(anyhow!("Unsupported sampling strategy: {}", s)),
}
}
pub fn name(&self) -> &'static str {
match self {
Self::Uniform => "Uniform",
Self::SceneBased => "Scene-based",
Self::KeyframeOnly => "Keyframe-only",
Self::Smart => "Smart",
}
}
#[allow(dead_code)]
pub fn description(&self) -> &'static str {
match self {
Self::Uniform => "Extract frames at regular intervals",
Self::SceneBased => "Extract representative frames from each scene",
Self::KeyframeOnly => "Extract only keyframes (I-frames)",
Self::Smart => "Intelligent frame selection based on content",
}
}
}
impl fmt::Display for SamplingStrategy {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.name())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum LayoutMode {
Grid,
Vertical,
Horizontal,
Auto,
}
impl LayoutMode {
pub fn from_str(s: &str) -> Result<Self> {
match s.to_lowercase().as_str() {
"grid" => Ok(Self::Grid),
"vertical" | "vert" | "column" => Ok(Self::Vertical),
"horizontal" | "horiz" | "row" | "filmstrip" => Ok(Self::Horizontal),
"auto" => Ok(Self::Auto),
_ => Err(anyhow!("Unsupported layout mode: {}", s)),
}
}
pub fn name(&self) -> &'static str {
match self {
Self::Grid => "Grid",
Self::Vertical => "Vertical",
Self::Horizontal => "Horizontal",
Self::Auto => "Auto",
}
}
}
impl fmt::Display for LayoutMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.name())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ThumbnailMetadata {
pub index: usize,
pub timestamp: f64,
pub x: u32,
pub y: u32,
pub width: u32,
pub height: u32,
}
#[derive(Debug, Serialize)]
pub struct SpriteSheetResult {
pub success: bool,
pub sprite_path: String,
pub sprite_width: u32,
pub sprite_height: u32,
pub thumbnail_count: usize,
pub thumbnail_width: u32,
pub thumbnail_height: u32,
pub columns: usize,
pub rows: usize,
pub format: String,
pub vtt_path: Option<String>,
pub manifest_path: Option<String>,
pub processing_time: f64,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SpriteSheetManifest {
pub sprite_file: String,
pub video_file: String,
pub sprite_width: u32,
pub sprite_height: u32,
pub thumbnails: Vec<ThumbnailMetadata>,
pub config: SpriteSheetConfig,
pub video_duration: f64,
pub video_fps: f64,
pub generated_at: String,
}
#[allow(dead_code)]
pub mod presets {
use super::*;
pub fn youtube_preview() -> SpriteSheetConfig {
SpriteSheetConfig {
interval: None,
count: Some(100),
thumbnail_width: 160,
thumbnail_height: 90,
columns: 10,
rows: 10,
format: ImageFormat::Jpeg,
quality: 85,
strategy: SamplingStrategy::Uniform,
layout: LayoutMode::Grid,
spacing: 0,
margin: 0,
maintain_aspect_ratio: true,
compression: 6,
}
}
pub fn high_quality() -> SpriteSheetConfig {
SpriteSheetConfig {
interval: None,
count: Some(100),
thumbnail_width: 320,
thumbnail_height: 180,
columns: 10,
rows: 10,
format: ImageFormat::Png,
quality: 95,
strategy: SamplingStrategy::Smart,
layout: LayoutMode::Grid,
spacing: 4,
margin: 2,
maintain_aspect_ratio: true,
compression: 6,
}
}
pub fn web_optimized() -> SpriteSheetConfig {
SpriteSheetConfig {
interval: Some(5.0),
count: None,
thumbnail_width: 128,
thumbnail_height: 72,
columns: 8,
rows: 8,
format: ImageFormat::Webp,
quality: 80,
strategy: SamplingStrategy::Uniform,
layout: LayoutMode::Grid,
spacing: 0,
margin: 0,
maintain_aspect_ratio: true,
compression: 9,
}
}
pub fn filmstrip() -> SpriteSheetConfig {
SpriteSheetConfig {
interval: Some(10.0),
count: None,
thumbnail_width: 200,
thumbnail_height: 112,
columns: 10,
rows: 1,
format: ImageFormat::Png,
quality: 90,
strategy: SamplingStrategy::KeyframeOnly,
layout: LayoutMode::Horizontal,
spacing: 4,
margin: 0,
maintain_aspect_ratio: true,
compression: 6,
}
}
pub fn scene_preview() -> SpriteSheetConfig {
SpriteSheetConfig {
interval: None,
count: Some(50),
thumbnail_width: 240,
thumbnail_height: 135,
columns: 10,
rows: 5,
format: ImageFormat::Jpeg,
quality: 90,
strategy: SamplingStrategy::SceneBased,
layout: LayoutMode::Grid,
spacing: 2,
margin: 0,
maintain_aspect_ratio: true,
compression: 6,
}
}
pub fn mobile() -> SpriteSheetConfig {
SpriteSheetConfig {
interval: Some(15.0),
count: None,
thumbnail_width: 96,
thumbnail_height: 54,
columns: 6,
rows: 6,
format: ImageFormat::Webp,
quality: 75,
strategy: SamplingStrategy::Uniform,
layout: LayoutMode::Grid,
spacing: 1,
margin: 0,
maintain_aspect_ratio: true,
compression: 9,
}
}
pub fn get_preset(name: &str) -> Option<SpriteSheetConfig> {
match name.to_lowercase().as_str() {
"youtube" | "youtube-preview" => Some(youtube_preview()),
"high-quality" | "hq" => Some(high_quality()),
"web" | "web-optimized" => Some(web_optimized()),
"filmstrip" | "strip" => Some(filmstrip()),
"scene" | "scene-preview" => Some(scene_preview()),
"mobile" => Some(mobile()),
_ => None,
}
}
pub fn list_presets() -> Vec<&'static str> {
vec![
"youtube-preview",
"high-quality",
"web-optimized",
"filmstrip",
"scene-preview",
"mobile",
]
}
}
#[allow(dead_code)]
pub mod color {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Color {
pub r: u8,
pub g: u8,
pub b: u8,
pub a: u8,
}
impl Color {
pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
Self { r, g, b, a }
}
pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
Self::new(r, g, b, 255)
}
pub fn from_hex(hex: &str) -> Option<Self> {
let hex = hex.trim_start_matches('#');
if hex.len() != 6 && hex.len() != 8 {
return None;
}
let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
let a = if hex.len() == 8 {
u8::from_str_radix(&hex[6..8], 16).ok()?
} else {
255
};
Some(Self::new(r, g, b, a))
}
pub fn to_hex(&self) -> String {
if self.a == 255 {
format!("#{:02X}{:02X}{:02X}", self.r, self.g, self.b)
} else {
format!("#{:02X}{:02X}{:02X}{:02X}", self.r, self.g, self.b, self.a)
}
}
pub fn blend(&self, other: &Self) -> Self {
let alpha = other.a as f32 / 255.0;
let inv_alpha = 1.0 - alpha;
let r = (self.r as f32 * inv_alpha + other.r as f32 * alpha) as u8;
let g = (self.g as f32 * inv_alpha + other.g as f32 * alpha) as u8;
let b = (self.b as f32 * inv_alpha + other.b as f32 * alpha) as u8;
let a = 255;
Self::new(r, g, b, a)
}
pub fn with_alpha(&self, alpha: u8) -> Self {
Self::new(self.r, self.g, self.b, alpha)
}
pub fn to_grayscale(&self) -> Self {
let gray =
(0.299 * self.r as f32 + 0.587 * self.g as f32 + 0.114 * self.b as f32) as u8;
Self::new(gray, gray, gray, self.a)
}
}
impl Color {
pub const BLACK: Self = Self::rgb(0, 0, 0);
pub const WHITE: Self = Self::rgb(255, 255, 255);
pub const RED: Self = Self::rgb(255, 0, 0);
pub const GREEN: Self = Self::rgb(0, 255, 0);
pub const BLUE: Self = Self::rgb(0, 0, 255);
pub const YELLOW: Self = Self::rgb(255, 255, 0);
pub const CYAN: Self = Self::rgb(0, 255, 255);
pub const MAGENTA: Self = Self::rgb(255, 0, 255);
pub const TRANSPARENT: Self = Self::new(0, 0, 0, 0);
}
}
#[allow(dead_code)]
pub mod overlay {
use super::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum OverlayStyle {
Simple,
Box,
Shadow,
Outline,
Pill,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum OverlayPosition {
TopLeft,
TopCenter,
TopRight,
MiddleLeft,
MiddleCenter,
MiddleRight,
BottomLeft,
BottomCenter,
BottomRight,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OverlayConfig {
pub style: OverlayStyle,
pub position: OverlayPosition,
pub text_color: String,
pub background_color: String,
pub font_size: u32,
pub padding: u32,
pub shadow_offset: (i32, i32),
pub outline_width: u32,
}
impl Default for OverlayConfig {
fn default() -> Self {
Self {
style: OverlayStyle::Box,
position: OverlayPosition::BottomRight,
text_color: "#FFFFFF".to_string(),
background_color: "#000000CC".to_string(),
font_size: 12,
padding: 4,
shadow_offset: (1, 1),
outline_width: 1,
}
}
}
pub fn calculate_overlay_size(timestamp: &str, config: &OverlayConfig) -> (u32, u32) {
let char_width = (config.font_size as f32 * 0.6) as u32;
let text_width = timestamp.len() as u32 * char_width;
let text_height = config.font_size;
let width = text_width + 2 * config.padding;
let height = text_height + 2 * config.padding;
(width, height)
}
pub fn calculate_overlay_position(
thumb_width: u32,
thumb_height: u32,
overlay_width: u32,
overlay_height: u32,
position: OverlayPosition,
) -> (u32, u32) {
let margin = 4u32;
match position {
OverlayPosition::TopLeft => (margin, margin),
OverlayPosition::TopCenter => ((thumb_width.saturating_sub(overlay_width)) / 2, margin),
OverlayPosition::TopRight => {
(thumb_width.saturating_sub(overlay_width + margin), margin)
}
OverlayPosition::MiddleLeft => {
(margin, (thumb_height.saturating_sub(overlay_height)) / 2)
}
OverlayPosition::MiddleCenter => (
(thumb_width.saturating_sub(overlay_width)) / 2,
(thumb_height.saturating_sub(overlay_height)) / 2,
),
OverlayPosition::MiddleRight => (
thumb_width.saturating_sub(overlay_width + margin),
(thumb_height.saturating_sub(overlay_height)) / 2,
),
OverlayPosition::BottomLeft => {
(margin, thumb_height.saturating_sub(overlay_height + margin))
}
OverlayPosition::BottomCenter => (
(thumb_width.saturating_sub(overlay_width)) / 2,
thumb_height.saturating_sub(overlay_height + margin),
),
OverlayPosition::BottomRight => (
thumb_width.saturating_sub(overlay_width + margin),
thumb_height.saturating_sub(overlay_height + margin),
),
}
}
}
#[allow(dead_code)]
pub mod image_utils {
pub struct ImageBuffer {
pub width: u32,
pub height: u32,
pub data: Vec<u8>,
pub channels: u8,
}
impl ImageBuffer {
pub fn new(width: u32, height: u32, channels: u8) -> Self {
let size = (width * height * channels as u32) as usize;
Self {
width,
height,
data: vec![0; size],
channels,
}
}
pub fn from_raw(width: u32, height: u32, channels: u8, data: Vec<u8>) -> Self {
Self {
width,
height,
data,
channels,
}
}
pub fn get_pixel(&self, x: u32, y: u32) -> Option<[u8; 4]> {
if x >= self.width || y >= self.height {
return None;
}
let offset = ((y * self.width + x) * self.channels as u32) as usize;
match self.channels {
1 => {
let gray = self.data[offset];
Some([gray, gray, gray, 255])
}
3 => Some([
self.data[offset],
self.data[offset + 1],
self.data[offset + 2],
255,
]),
4 => Some([
self.data[offset],
self.data[offset + 1],
self.data[offset + 2],
self.data[offset + 3],
]),
_ => None,
}
}
pub fn set_pixel(&mut self, x: u32, y: u32, pixel: [u8; 4]) -> bool {
if x >= self.width || y >= self.height {
return false;
}
let offset = ((y * self.width + x) * self.channels as u32) as usize;
match self.channels {
1 => {
let gray = (0.299 * pixel[0] as f32
+ 0.587 * pixel[1] as f32
+ 0.114 * pixel[2] as f32) as u8;
self.data[offset] = gray;
}
3 => {
self.data[offset] = pixel[0];
self.data[offset + 1] = pixel[1];
self.data[offset + 2] = pixel[2];
}
4 => {
self.data[offset] = pixel[0];
self.data[offset + 1] = pixel[1];
self.data[offset + 2] = pixel[2];
self.data[offset + 3] = pixel[3];
}
_ => return false,
}
true
}
pub fn resize_nearest(&self, new_width: u32, new_height: u32) -> Self {
let mut result = Self::new(new_width, new_height, self.channels);
let x_ratio = self.width as f32 / new_width as f32;
let y_ratio = self.height as f32 / new_height as f32;
for y in 0..new_height {
for x in 0..new_width {
let src_x = (x as f32 * x_ratio) as u32;
let src_y = (y as f32 * y_ratio) as u32;
if let Some(pixel) = self.get_pixel(src_x, src_y) {
result.set_pixel(x, y, pixel);
}
}
}
result
}
pub fn resize_bilinear(&self, new_width: u32, new_height: u32) -> Self {
let mut result = Self::new(new_width, new_height, self.channels);
let x_ratio = (self.width - 1) as f32 / new_width as f32;
let y_ratio = (self.height - 1) as f32 / new_height as f32;
for y in 0..new_height {
for x in 0..new_width {
let gx = x as f32 * x_ratio;
let gy = y as f32 * y_ratio;
let gxi = gx as u32;
let gyi = gy as u32;
let gxi_next = (gxi + 1).min(self.width - 1);
let gyi_next = (gyi + 1).min(self.height - 1);
let c00 = self.get_pixel(gxi, gyi).unwrap_or([0; 4]);
let c10 = self.get_pixel(gxi_next, gyi).unwrap_or([0; 4]);
let c01 = self.get_pixel(gxi, gyi_next).unwrap_or([0; 4]);
let c11 = self.get_pixel(gxi_next, gyi_next).unwrap_or([0; 4]);
let x_weight = gx - gxi as f32;
let y_weight = gy - gyi as f32;
let mut pixel = [0u8; 4];
for i in 0..4 {
let top = c00[i] as f32 * (1.0 - x_weight) + c10[i] as f32 * x_weight;
let bottom = c01[i] as f32 * (1.0 - x_weight) + c11[i] as f32 * x_weight;
pixel[i] = (top * (1.0 - y_weight) + bottom * y_weight) as u8;
}
result.set_pixel(x, y, pixel);
}
}
result
}
pub fn fill_rect(&mut self, x: u32, y: u32, width: u32, height: u32, color: [u8; 4]) {
for dy in 0..height {
for dx in 0..width {
self.set_pixel(x + dx, y + dy, color);
}
}
}
pub fn composite(&mut self, other: &ImageBuffer, x: u32, y: u32, alpha_blend: bool) {
for dy in 0..other.height {
for dx in 0..other.width {
if let Some(src_pixel) = other.get_pixel(dx, dy) {
let dest_x = x + dx;
let dest_y = y + dy;
if dest_x < self.width && dest_y < self.height {
if alpha_blend && src_pixel[3] < 255 {
if let Some(dest_pixel) = self.get_pixel(dest_x, dest_y) {
let alpha = src_pixel[3] as f32 / 255.0;
let inv_alpha = 1.0 - alpha;
let blended = [
(dest_pixel[0] as f32 * inv_alpha
+ src_pixel[0] as f32 * alpha)
as u8,
(dest_pixel[1] as f32 * inv_alpha
+ src_pixel[1] as f32 * alpha)
as u8,
(dest_pixel[2] as f32 * inv_alpha
+ src_pixel[2] as f32 * alpha)
as u8,
255,
];
self.set_pixel(dest_x, dest_y, blended);
}
} else {
self.set_pixel(dest_x, dest_y, src_pixel);
}
}
}
}
}
}
pub fn perceptual_hash(&self) -> u64 {
let small = self.resize_nearest(8, 8);
let mut gray_values = Vec::new();
for y in 0..8 {
for x in 0..8 {
if let Some(pixel) = small.get_pixel(x, y) {
let gray = (0.299 * pixel[0] as f32
+ 0.587 * pixel[1] as f32
+ 0.114 * pixel[2] as f32) as u8;
gray_values.push(gray);
}
}
}
let avg: u32 = gray_values.iter().map(|&v| v as u32).sum::<u32>() / 64;
let mut hash = 0u64;
for (i, &value) in gray_values.iter().enumerate() {
if value as u32 > avg {
hash |= 1 << i;
}
}
hash
}
pub fn hash_distance(hash1: u64, hash2: u64) -> u32 {
(hash1 ^ hash2).count_ones()
}
}
}
#[allow(dead_code)]
pub mod analysis {
pub struct FrameQuality {
pub sharpness: f64,
pub contrast: f64,
pub brightness: f64,
pub motion_blur: f64,
pub overall: f64,
}
impl FrameQuality {
pub fn calculate_overall(
sharpness: f64,
contrast: f64,
brightness: f64,
motion_blur: f64,
) -> f64 {
let sharpness_weight = 0.4;
let contrast_weight = 0.2;
let brightness_weight = 0.1;
let motion_blur_weight = 0.3;
sharpness * sharpness_weight
+ contrast * contrast_weight
+ brightness * brightness_weight
+ (1.0 - motion_blur) * motion_blur_weight
}
pub fn new(sharpness: f64, contrast: f64, brightness: f64, motion_blur: f64) -> Self {
let overall = Self::calculate_overall(sharpness, contrast, brightness, motion_blur);
Self {
sharpness,
contrast,
brightness,
motion_blur,
overall,
}
}
pub fn is_acceptable(&self) -> bool {
self.overall >= 0.5 && self.motion_blur < 0.7
}
}
pub struct SceneChange {
pub frame_index: usize,
pub timestamp: f64,
pub confidence: f64,
}
pub fn histogram_difference(hist1: &[u32; 256], hist2: &[u32; 256]) -> f64 {
let mut diff = 0.0;
for i in 0..256 {
let h1 = hist1[i] as f64;
let h2 = hist2[i] as f64;
diff += (h1 - h2).abs();
}
diff / (256.0 * 1000.0) }
pub fn is_scene_change(difference: f64, threshold: f64) -> bool {
difference > threshold
}
}