pub mod adaptive;
pub mod classification;
pub mod edge;
pub mod histogram;
pub mod motion;
pub mod transition_detect;
use crate::error::{CvError, CvResult};
use oximedia_codec::VideoFrame;
use oximedia_core::Timestamp;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChangeType {
Cut,
Fade,
Dissolve,
GradualUnknown,
}
impl ChangeType {
#[must_use]
pub const fn is_gradual(&self) -> bool {
matches!(self, Self::Fade | Self::Dissolve | Self::GradualUnknown)
}
#[must_use]
pub const fn is_cut(&self) -> bool {
matches!(self, Self::Cut)
}
}
#[derive(Debug, Clone)]
pub struct SceneChange {
pub frame_number: usize,
pub timestamp: Timestamp,
pub confidence: f64,
pub change_type: ChangeType,
pub metadata: SceneMetadata,
}
impl SceneChange {
#[must_use]
pub fn new(
frame_number: usize,
timestamp: Timestamp,
confidence: f64,
change_type: ChangeType,
) -> Self {
Self {
frame_number,
timestamp,
confidence: confidence.clamp(0.0, 1.0),
change_type,
metadata: SceneMetadata::default(),
}
}
#[must_use]
pub fn meets_threshold(&self, threshold: f64) -> bool {
self.confidence >= threshold
}
}
#[derive(Debug, Clone, Default)]
pub struct SceneMetadata {
pub histogram_diff: Option<f64>,
pub edge_change_ratio: Option<f64>,
pub motion_score: Option<f64>,
pub color_diff: Option<f64>,
pub transition_duration: Option<usize>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DetectionMethod {
Histogram,
HistogramHsv,
Edge,
Motion,
Adaptive,
Hybrid,
}
#[derive(Debug, Clone)]
pub struct SceneConfig {
pub method: DetectionMethod,
pub threshold: f64,
pub min_scene_length: usize,
pub detect_gradual: bool,
pub gradual_window: usize,
pub gradual_threshold: f64,
pub use_temporal_coherence: bool,
pub adaptive_config: adaptive::AdaptiveConfig,
pub histogram_config: histogram::HistogramConfig,
pub edge_config: edge::EdgeConfig,
pub motion_config: motion::MotionConfig,
}
impl Default for SceneConfig {
fn default() -> Self {
Self {
method: DetectionMethod::Histogram,
threshold: 0.3,
min_scene_length: 15, detect_gradual: true,
gradual_window: 10,
gradual_threshold: 0.15,
use_temporal_coherence: true,
adaptive_config: adaptive::AdaptiveConfig::default(),
histogram_config: histogram::HistogramConfig::default(),
edge_config: edge::EdgeConfig::default(),
motion_config: motion::MotionConfig::default(),
}
}
}
impl SceneConfig {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub const fn with_method(mut self, method: DetectionMethod) -> Self {
self.method = method;
self
}
#[must_use]
pub const fn with_threshold(mut self, threshold: f64) -> Self {
self.threshold = threshold;
self
}
#[must_use]
pub const fn with_min_scene_length(mut self, min_scene_length: usize) -> Self {
self.min_scene_length = min_scene_length;
self
}
#[must_use]
pub const fn with_detect_gradual(mut self, detect_gradual: bool) -> Self {
self.detect_gradual = detect_gradual;
self
}
#[must_use]
pub const fn with_gradual_window(mut self, window: usize) -> Self {
self.gradual_window = window;
self
}
#[must_use]
pub const fn with_temporal_coherence(mut self, enabled: bool) -> Self {
self.use_temporal_coherence = enabled;
self
}
pub fn validate(&self) -> CvResult<()> {
if self.threshold < 0.0 || self.threshold > 1.0 {
return Err(CvError::invalid_parameter(
"threshold",
format!("{} (must be 0.0-1.0)", self.threshold),
));
}
if self.gradual_threshold < 0.0 || self.gradual_threshold > 1.0 {
return Err(CvError::invalid_parameter(
"gradual_threshold",
format!("{} (must be 0.0-1.0)", self.gradual_threshold),
));
}
if self.min_scene_length == 0 {
return Err(CvError::invalid_parameter(
"min_scene_length",
"must be greater than 0",
));
}
if self.gradual_window == 0 {
return Err(CvError::invalid_parameter(
"gradual_window",
"must be greater than 0",
));
}
Ok(())
}
}
pub struct SceneDetector {
config: SceneConfig,
}
impl SceneDetector {
#[must_use]
pub fn new(config: SceneConfig) -> Self {
Self { config }
}
#[must_use]
pub fn default_detector() -> Self {
Self::new(SceneConfig::default())
}
#[must_use]
pub const fn config(&self) -> &SceneConfig {
&self.config
}
pub fn detect_scenes(&self, frames: &[VideoFrame]) -> CvResult<Vec<SceneChange>> {
self.config.validate()?;
if frames.len() < 2 {
return Ok(Vec::new());
}
let mut changes = match self.config.method {
DetectionMethod::Histogram => self.detect_histogram(frames)?,
DetectionMethod::HistogramHsv => self.detect_histogram_hsv(frames)?,
DetectionMethod::Edge => self.detect_edge(frames)?,
DetectionMethod::Motion => self.detect_motion(frames)?,
DetectionMethod::Adaptive => self.detect_adaptive(frames)?,
DetectionMethod::Hybrid => self.detect_hybrid(frames)?,
};
changes = self.filter_min_scene_length(changes);
if self.config.use_temporal_coherence {
changes = self.apply_temporal_coherence(changes);
}
if self.config.detect_gradual {
let gradual = self.detect_gradual_transitions(frames)?;
changes.extend(gradual);
changes.sort_by_key(|c| c.frame_number);
}
Ok(changes)
}
fn detect_histogram(&self, frames: &[VideoFrame]) -> CvResult<Vec<SceneChange>> {
histogram::detect_histogram_changes(frames, &self.config)
}
fn detect_histogram_hsv(&self, frames: &[VideoFrame]) -> CvResult<Vec<SceneChange>> {
histogram::detect_histogram_hsv_changes(frames, &self.config)
}
fn detect_edge(&self, frames: &[VideoFrame]) -> CvResult<Vec<SceneChange>> {
edge::detect_edge_changes(frames, &self.config)
}
fn detect_motion(&self, frames: &[VideoFrame]) -> CvResult<Vec<SceneChange>> {
motion::detect_motion_changes(frames, &self.config)
}
fn detect_adaptive(&self, frames: &[VideoFrame]) -> CvResult<Vec<SceneChange>> {
adaptive::detect_adaptive_changes(frames, &self.config)
}
fn detect_hybrid(&self, frames: &[VideoFrame]) -> CvResult<Vec<SceneChange>> {
let hist_changes = self.detect_histogram(frames)?;
let edge_changes = self.detect_edge(frames)?;
let motion_changes = self.detect_motion(frames)?;
let mut combined = Vec::new();
let all_frames: std::collections::HashSet<usize> = hist_changes
.iter()
.chain(edge_changes.iter())
.chain(motion_changes.iter())
.map(|c| c.frame_number)
.collect();
for frame_num in all_frames {
let hist_score = hist_changes
.iter()
.find(|c| c.frame_number == frame_num)
.map_or(0.0, |c| c.confidence);
let edge_score = edge_changes
.iter()
.find(|c| c.frame_number == frame_num)
.map_or(0.0, |c| c.confidence);
let motion_score = motion_changes
.iter()
.find(|c| c.frame_number == frame_num)
.map_or(0.0, |c| c.confidence);
let vote_count =
(hist_score > 0.0) as u32 + (edge_score > 0.0) as u32 + (motion_score > 0.0) as u32;
if vote_count >= 2 {
let avg_confidence = (hist_score + edge_score + motion_score) / 3.0;
combined.push(SceneChange {
frame_number: frame_num,
timestamp: frames[frame_num].timestamp,
confidence: avg_confidence,
change_type: ChangeType::Cut,
metadata: SceneMetadata {
histogram_diff: Some(hist_score),
edge_change_ratio: Some(edge_score),
motion_score: Some(motion_score),
..Default::default()
},
});
}
}
combined.sort_by_key(|c| c.frame_number);
Ok(combined)
}
fn detect_gradual_transitions(&self, frames: &[VideoFrame]) -> CvResult<Vec<SceneChange>> {
if frames.len() < self.config.gradual_window {
return Ok(Vec::new());
}
let mut gradual_changes = Vec::new();
let window = self.config.gradual_window;
for i in 0..frames.len() - window {
let start_frame = &frames[i];
let end_frame = &frames[i + window];
let similarity = histogram::compute_frame_similarity(start_frame, end_frame)?;
let diff = 1.0 - similarity;
if diff > self.config.gradual_threshold && diff < self.config.threshold {
let change_type = self.classify_gradual_transition(frames, i, i + window)?;
gradual_changes.push(SceneChange {
frame_number: i + window / 2, timestamp: frames[i + window / 2].timestamp,
confidence: diff,
change_type,
metadata: SceneMetadata {
transition_duration: Some(window),
histogram_diff: Some(diff),
..Default::default()
},
});
}
}
Ok(gradual_changes)
}
fn classify_gradual_transition(
&self,
frames: &[VideoFrame],
start: usize,
end: usize,
) -> CvResult<ChangeType> {
let start_brightness = histogram::compute_average_brightness(&frames[start])?;
let end_brightness = histogram::compute_average_brightness(&frames[end])?;
let brightness_change = (end_brightness - start_brightness).abs();
let brightness_ratio = if start_brightness > 0.0 {
brightness_change / start_brightness
} else {
0.0
};
if brightness_ratio > 0.5 {
return Ok(ChangeType::Fade);
}
Ok(ChangeType::Dissolve)
}
fn filter_min_scene_length(&self, changes: Vec<SceneChange>) -> Vec<SceneChange> {
if changes.is_empty() {
return changes;
}
let mut filtered = Vec::new();
let mut last_frame = 0;
for change in changes {
if change.frame_number - last_frame >= self.config.min_scene_length {
filtered.push(change.clone());
last_frame = change.frame_number;
}
}
filtered
}
fn apply_temporal_coherence(&self, changes: Vec<SceneChange>) -> Vec<SceneChange> {
if changes.len() < 2 {
return changes;
}
let mut coherent = Vec::new();
let window = 3;
for (i, change) in changes.iter().enumerate() {
let nearby_count = changes
.iter()
.enumerate()
.filter(|(j, c)| {
*j != i && (c.frame_number as i64 - change.frame_number as i64).abs() <= window
})
.count();
if nearby_count > 0 || change.confidence > self.config.threshold * 1.5 {
coherent.push(change.clone());
}
}
coherent
}
pub fn analyze_frame_pair(
&self,
frame1: &VideoFrame,
frame2: &VideoFrame,
) -> CvResult<SceneChange> {
let similarity = match self.config.method {
DetectionMethod::Histogram => histogram::compute_frame_similarity(frame1, frame2)?,
DetectionMethod::HistogramHsv => {
histogram::compute_frame_similarity_hsv(frame1, frame2)?
}
DetectionMethod::Edge => {
edge::compute_edge_similarity(frame1, frame2, &self.config.edge_config)?
}
DetectionMethod::Motion => {
motion::compute_motion_score(frame1, frame2, &self.config.motion_config)?
}
_ => {
histogram::compute_frame_similarity(frame1, frame2)?
}
};
let diff = 1.0 - similarity;
let change_type = if diff > self.config.threshold {
ChangeType::Cut
} else {
ChangeType::GradualUnknown
};
Ok(SceneChange::new(
0, frame2.timestamp,
diff,
change_type,
))
}
}
impl Default for SceneDetector {
fn default() -> Self {
Self::default_detector()
}
}
pub fn compute_frame_difference(
frame1: &VideoFrame,
frame2: &VideoFrame,
method: DetectionMethod,
) -> CvResult<f64> {
let similarity = match method {
DetectionMethod::Histogram => histogram::compute_frame_similarity(frame1, frame2)?,
DetectionMethod::HistogramHsv => histogram::compute_frame_similarity_hsv(frame1, frame2)?,
DetectionMethod::Edge => {
let config = edge::EdgeConfig::default();
edge::compute_edge_similarity(frame1, frame2, &config)?
}
DetectionMethod::Motion => {
let config = motion::MotionConfig::default();
motion::compute_motion_score(frame1, frame2, &config)?
}
DetectionMethod::Adaptive | DetectionMethod::Hybrid => {
histogram::compute_frame_similarity(frame1, frame2)?
}
};
Ok(1.0 - similarity)
}