pub mod comb;
pub mod field;
pub mod metrics;
pub mod pattern;
pub mod telecine;
use crate::error::{CvError, CvResult};
use oximedia_codec::VideoFrame;
pub use comb::{CombDetector, CombDetectorConfig, CombPattern};
pub use field::{Field, FieldAnalyzer, FieldParity};
pub use metrics::{
ConfidenceLevel, ContentType, DetectionScore, FieldOrder, InterlaceMetrics, TelecineMetrics,
};
pub use pattern::{
CadenceMapEntry, CadencePattern, FrameDifference, FramePrediction, PatternMatcher,
PatternValidation, PatternValidator, PulldownPattern,
};
pub use telecine::{
IvtcMethod, IvtcRecommendation, TelecineDetector, TelecineDetectorConfig, TelecineInfo,
TelecineStatistics,
};
#[derive(Debug, Clone)]
pub struct InterlaceDetectorConfig {
pub comb_config: CombDetectorConfig,
pub confidence_threshold: f64,
pub detect_field_order: bool,
pub min_frames: usize,
}
impl InterlaceDetectorConfig {
#[must_use]
pub fn new() -> Self {
Self {
comb_config: CombDetectorConfig::default(),
confidence_threshold: 0.5,
detect_field_order: true,
min_frames: 5,
}
}
#[must_use]
pub fn sensitive() -> Self {
Self {
comb_config: CombDetectorConfig::sensitive(),
confidence_threshold: 0.4,
detect_field_order: true,
min_frames: 3,
}
}
#[must_use]
pub fn specific() -> Self {
Self {
comb_config: CombDetectorConfig::specific(),
confidence_threshold: 0.6,
detect_field_order: true,
min_frames: 7,
}
}
}
impl Default for InterlaceDetectorConfig {
fn default() -> Self {
Self::new()
}
}
pub struct InterlaceDetector {
config: InterlaceDetectorConfig,
comb_detector: CombDetector,
field_analyzer: FieldAnalyzer,
}
impl InterlaceDetector {
#[must_use]
pub fn new(config: InterlaceDetectorConfig) -> Self {
let comb_detector = CombDetector::new(config.comb_config.clone());
let field_analyzer = FieldAnalyzer::new();
Self {
config,
comb_detector,
field_analyzer,
}
}
#[must_use]
pub fn with_defaults() -> Self {
Self::new(InterlaceDetectorConfig::default())
}
pub fn detect_interlacing(&self, frames: &[VideoFrame]) -> CvResult<InterlaceInfo> {
if frames.is_empty() {
return Err(CvError::insufficient_data(1, 0));
}
let metrics = if frames.len() >= 2 {
self.comb_detector.detect_temporal(frames)?
} else {
self.comb_detector.detect(&frames[0])?
};
let field_order =
if self.config.detect_field_order && frames.len() >= 2 && metrics.confidence() > 0.5 {
self.field_analyzer.detect_field_order(frames)?
} else {
FieldOrder::Unknown
};
let detection_score = self.calculate_detection_score(&metrics)?;
let content_type = detection_score.dominant_type();
let confidence = detection_score.confidence();
let is_interlaced = content_type == ContentType::Interlaced
&& confidence >= self.config.confidence_threshold;
Ok(InterlaceInfo {
is_interlaced,
content_type,
confidence,
field_order,
metrics,
detection_score,
})
}
fn calculate_detection_score(&self, metrics: &InterlaceMetrics) -> CvResult<DetectionScore> {
let interlaced_score = metrics.confidence();
let progressive_score = 1.0 - interlaced_score;
let telecine_score = 0.0;
let metric_variance = self.calculate_metric_variance(metrics);
let mixed_score = if metric_variance > 0.3 {
metric_variance
} else {
0.0
};
Ok(DetectionScore::from_components(
progressive_score,
interlaced_score,
telecine_score,
mixed_score,
))
}
fn calculate_metric_variance(&self, metrics: &InterlaceMetrics) -> f64 {
let values = [
metrics.comb_score,
metrics.field_diff,
metrics.spatial_comb,
metrics.temporal_comb,
metrics.edge_comb,
];
let mean = values.iter().sum::<f64>() / values.len() as f64;
let variance: f64 = values
.iter()
.map(|&v| {
let diff = v - mean;
diff * diff
})
.sum::<f64>()
/ values.len() as f64;
variance.sqrt()
}
pub fn detect_detailed(&self, frames: &[VideoFrame]) -> CvResult<DetailedInterlaceInfo> {
let basic_info = self.detect_interlacing(frames)?;
let comb_map = if frames.is_empty() {
None
} else {
Some(
self.comb_detector
.generate_comb_map(&frames[frames.len() - 1])?,
)
};
let comb_patterns = if frames.is_empty() {
Vec::new()
} else {
self.comb_detector
.detect_comb_patterns(&frames[frames.len() - 1])?
};
let field_parities = if frames.len() >= 2 {
self.field_analyzer.analyze_field_parity(frames)?
} else {
Vec::new()
};
Ok(DetailedInterlaceInfo {
basic_info,
comb_map,
comb_patterns,
field_parities,
})
}
pub fn recommend_deinterlace(
&self,
frames: &[VideoFrame],
) -> CvResult<DeinterlaceRecommendation> {
let info = self.detect_interlacing(frames)?;
if !info.is_interlaced {
return Ok(DeinterlaceRecommendation {
should_deinterlace: false,
method: DeinterlaceMethod::None,
confidence: info.confidence,
field_order: info.field_order,
});
}
let method = if info.metrics.temporal_comb > 0.7 {
DeinterlaceMethod::MotionAdaptive
} else if info.metrics.spatial_comb > 0.6 {
DeinterlaceMethod::EdgeDirected
} else if info.metrics.field_diff > 0.5 {
DeinterlaceMethod::FieldBlend
} else {
DeinterlaceMethod::LineDouble
};
Ok(DeinterlaceRecommendation {
should_deinterlace: true,
method,
confidence: info.confidence,
field_order: info.field_order,
})
}
#[must_use]
pub const fn comb_detector(&self) -> &CombDetector {
&self.comb_detector
}
#[must_use]
pub const fn field_analyzer(&self) -> &FieldAnalyzer {
&self.field_analyzer
}
}
impl Default for InterlaceDetector {
fn default() -> Self {
Self::with_defaults()
}
}
#[derive(Debug, Clone)]
pub struct InterlaceInfo {
pub is_interlaced: bool,
pub content_type: ContentType,
pub confidence: f64,
pub field_order: FieldOrder,
pub metrics: InterlaceMetrics,
pub detection_score: DetectionScore,
}
impl InterlaceInfo {
#[must_use]
pub fn confidence_level(&self) -> ConfidenceLevel {
ConfidenceLevel::from_value(self.confidence)
}
#[must_use]
pub fn is_confident(&self) -> bool {
self.confidence >= 0.7
}
}
#[derive(Debug, Clone)]
pub struct DetailedInterlaceInfo {
pub basic_info: InterlaceInfo,
pub comb_map: Option<Vec<u8>>,
pub comb_patterns: Vec<CombPattern>,
pub field_parities: Vec<FieldParity>,
}
impl DetailedInterlaceInfo {
#[must_use]
pub fn comb_pattern_count(&self) -> usize {
self.comb_patterns.len()
}
#[must_use]
pub fn average_comb_length(&self) -> f64 {
if self.comb_patterns.is_empty() {
return 0.0;
}
let total_length: usize = self.comb_patterns.iter().map(|p| p.length).sum();
total_length as f64 / self.comb_patterns.len() as f64
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct DeinterlaceRecommendation {
pub should_deinterlace: bool,
pub method: DeinterlaceMethod,
pub confidence: f64,
pub field_order: FieldOrder,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DeinterlaceMethod {
None,
LineDouble,
FieldBlend,
EdgeDirected,
MotionAdaptive,
MotionCompensated,
}
impl DeinterlaceMethod {
#[must_use]
pub const fn complexity(&self) -> Complexity {
match self {
Self::None => Complexity::None,
Self::LineDouble | Self::FieldBlend => Complexity::Low,
Self::EdgeDirected => Complexity::Medium,
Self::MotionAdaptive => Complexity::High,
Self::MotionCompensated => Complexity::VeryHigh,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Complexity {
None,
Low,
Medium,
High,
VeryHigh,
}
pub struct ContentAnalyzer {
interlace_detector: InterlaceDetector,
telecine_detector: TelecineDetector,
}
impl ContentAnalyzer {
#[must_use]
pub fn new() -> Self {
Self {
interlace_detector: InterlaceDetector::default(),
telecine_detector: TelecineDetector::default(),
}
}
#[must_use]
pub fn with_config(
interlace_config: InterlaceDetectorConfig,
telecine_config: TelecineDetectorConfig,
) -> Self {
Self {
interlace_detector: InterlaceDetector::new(interlace_config),
telecine_detector: TelecineDetector::new(telecine_config),
}
}
pub fn analyze(&mut self, frames: &[VideoFrame]) -> CvResult<ContentAnalysis> {
let interlace_info = self.interlace_detector.detect_interlacing(frames)?;
let telecine_info = self.telecine_detector.detect(frames)?;
let overall_type =
if telecine_info.is_telecine && telecine_info.confidence > interlace_info.confidence {
ContentType::Telecine
} else if interlace_info.is_interlaced {
ContentType::Interlaced
} else {
ContentType::Progressive
};
let recommendation = self.generate_recommendation(&interlace_info, &telecine_info)?;
Ok(ContentAnalysis {
overall_type,
interlace_info,
telecine_info,
recommendation,
})
}
fn generate_recommendation(
&self,
interlace_info: &InterlaceInfo,
telecine_info: &TelecineInfo,
) -> CvResult<ProcessingRecommendation> {
if telecine_info.is_telecine && telecine_info.confidence > 0.6 {
Ok(ProcessingRecommendation::Ivtc {
pattern: telecine_info.pattern,
confidence: telecine_info.confidence,
})
} else if interlace_info.is_interlaced && interlace_info.confidence > 0.5 {
let deinterlace_rec = self.interlace_detector.recommend_deinterlace(&[])?;
Ok(ProcessingRecommendation::Deinterlace {
method: deinterlace_rec.method,
confidence: interlace_info.confidence,
})
} else {
Ok(ProcessingRecommendation::None)
}
}
pub fn reset(&mut self) {
self.telecine_detector.reset();
}
}
impl Default for ContentAnalyzer {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct ContentAnalysis {
pub overall_type: ContentType,
pub interlace_info: InterlaceInfo,
pub telecine_info: TelecineInfo,
pub recommendation: ProcessingRecommendation,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ProcessingRecommendation {
None,
Deinterlace {
method: DeinterlaceMethod,
confidence: f64,
},
Ivtc {
pattern: PulldownPattern,
confidence: f64,
},
}
impl ProcessingRecommendation {
#[must_use]
pub const fn requires_processing(&self) -> bool {
!matches!(self, Self::None)
}
#[must_use]
pub const fn confidence(&self) -> f64 {
match self {
Self::None => 0.0,
Self::Deinterlace { confidence, .. } | Self::Ivtc { confidence, .. } => *confidence,
}
}
}