Skip to main content

oximedia_graph/filters/video/
deinterlace.rs

1//! Deinterlacing filter.
2//!
3//! This filter converts interlaced video to progressive video using various
4//! deinterlacing algorithms.
5
6#![forbid(unsafe_code)]
7#![allow(clippy::cast_lossless)]
8#![allow(clippy::cast_precision_loss)]
9#![allow(clippy::cast_possible_truncation)]
10#![allow(clippy::cast_sign_loss)]
11#![allow(clippy::cast_possible_wrap)]
12#![allow(clippy::similar_names)]
13#![allow(clippy::many_single_char_names)]
14#![allow(clippy::missing_errors_doc)]
15#![allow(clippy::match_same_arms)]
16#![allow(clippy::doc_markdown)]
17#![allow(clippy::unused_self)]
18#![allow(clippy::unnecessary_cast)]
19#![allow(clippy::bool_to_int_with_if)]
20#![allow(clippy::needless_range_loop)]
21#![allow(clippy::too_many_lines)]
22#![allow(clippy::unnecessary_wraps)]
23#![allow(clippy::map_unwrap_or)]
24#![allow(clippy::no_effect_underscore_binding)]
25#![allow(clippy::unreadable_literal)]
26#![allow(dead_code)]
27
28use std::collections::VecDeque;
29
30use crate::error::{GraphError, GraphResult};
31use crate::frame::FilterFrame;
32use crate::node::{Node, NodeId, NodeState, NodeType};
33use crate::port::{InputPort, OutputPort, PortFormat, PortId, PortType, VideoPortFormat};
34use oximedia_codec::{Plane, VideoFrame};
35use oximedia_core::Timestamp;
36
37/// Deinterlacing mode/algorithm.
38#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
39pub enum DeinterlaceMode {
40    /// Bob deinterlacing - simple field interpolation (doubles frame rate).
41    Bob,
42    /// Weave - combine fields (may cause combing artifacts).
43    Weave,
44    /// Blend - average adjacent fields (reduces motion blur).
45    #[default]
46    Blend,
47    /// Yadif - Yet Another DeInterlacing Filter (high quality).
48    Yadif,
49    /// Yadif with spatial interpolation only (no temporal).
50    YadifSpatial,
51}
52
53impl DeinterlaceMode {
54    /// Check if this mode doubles the frame rate.
55    #[must_use]
56    pub fn doubles_framerate(&self) -> bool {
57        matches!(self, Self::Bob | Self::Yadif)
58    }
59
60    /// Check if this mode requires temporal information.
61    #[must_use]
62    pub fn requires_temporal(&self) -> bool {
63        matches!(self, Self::Yadif)
64    }
65}
66
67/// Field order in interlaced video.
68#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
69pub enum FieldOrder {
70    /// Top field first (TFF).
71    #[default]
72    TopFieldFirst,
73    /// Bottom field first (BFF).
74    BottomFieldFirst,
75    /// Auto-detect from video metadata.
76    Auto,
77}
78
79impl FieldOrder {
80    /// Get the starting line for the first field.
81    #[must_use]
82    pub fn first_field_start(&self) -> usize {
83        match self {
84            Self::TopFieldFirst | Self::Auto => 0,
85            Self::BottomFieldFirst => 1,
86        }
87    }
88
89    /// Get the starting line for the second field.
90    #[must_use]
91    pub fn second_field_start(&self) -> usize {
92        match self {
93            Self::TopFieldFirst | Self::Auto => 1,
94            Self::BottomFieldFirst => 0,
95        }
96    }
97}
98
99/// Configuration for the deinterlace filter.
100#[derive(Clone, Debug)]
101pub struct DeinterlaceConfig {
102    /// Deinterlacing algorithm.
103    pub mode: DeinterlaceMode,
104    /// Field order.
105    pub field_order: FieldOrder,
106    /// Only deinterlace detected interlaced frames.
107    pub auto_detect: bool,
108    /// Threshold for interlace detection (0.0-1.0).
109    pub detection_threshold: f64,
110}
111
112impl Default for DeinterlaceConfig {
113    fn default() -> Self {
114        Self {
115            mode: DeinterlaceMode::default(),
116            field_order: FieldOrder::default(),
117            auto_detect: false,
118            detection_threshold: 0.5,
119        }
120    }
121}
122
123impl DeinterlaceConfig {
124    /// Create a new deinterlace configuration.
125    #[must_use]
126    pub fn new(mode: DeinterlaceMode) -> Self {
127        Self {
128            mode,
129            ..Default::default()
130        }
131    }
132
133    /// Set the field order.
134    #[must_use]
135    pub fn with_field_order(mut self, order: FieldOrder) -> Self {
136        self.field_order = order;
137        self
138    }
139
140    /// Enable auto-detection of interlaced content.
141    #[must_use]
142    pub fn with_auto_detect(mut self, enabled: bool) -> Self {
143        self.auto_detect = enabled;
144        self
145    }
146
147    /// Set the detection threshold.
148    #[must_use]
149    pub fn with_detection_threshold(mut self, threshold: f64) -> Self {
150        self.detection_threshold = threshold.clamp(0.0, 1.0);
151        self
152    }
153}
154
155/// Deinterlacing filter.
156///
157/// Converts interlaced video to progressive video using various algorithms.
158///
159/// # Example
160///
161/// ```ignore
162/// use oximedia_graph::filters::video::{DeinterlaceFilter, DeinterlaceConfig, DeinterlaceMode};
163/// use oximedia_graph::node::NodeId;
164///
165/// let config = DeinterlaceConfig::new(DeinterlaceMode::Yadif)
166///     .with_field_order(FieldOrder::TopFieldFirst);
167///
168/// let filter = DeinterlaceFilter::new(NodeId(0), "deinterlace", config);
169/// ```
170pub struct DeinterlaceFilter {
171    id: NodeId,
172    name: String,
173    state: NodeState,
174    inputs: Vec<InputPort>,
175    outputs: Vec<OutputPort>,
176    config: DeinterlaceConfig,
177    /// Frame buffer for temporal algorithms.
178    frame_buffer: VecDeque<VideoFrame>,
179    /// Current output frame index.
180    output_frame_idx: u64,
181    /// Pending output frames (for bob/yadif that produce 2 frames per input).
182    pending_output: Vec<VideoFrame>,
183}
184
185impl DeinterlaceFilter {
186    /// Create a new deinterlace filter.
187    #[must_use]
188    pub fn new(id: NodeId, name: impl Into<String>, config: DeinterlaceConfig) -> Self {
189        Self {
190            id,
191            name: name.into(),
192            state: NodeState::Idle,
193            inputs: vec![InputPort::new(PortId(0), "input", PortType::Video)
194                .with_format(PortFormat::Video(VideoPortFormat::any()))],
195            outputs: vec![OutputPort::new(PortId(0), "output", PortType::Video)
196                .with_format(PortFormat::Video(VideoPortFormat::any()))],
197            config,
198            frame_buffer: VecDeque::with_capacity(3),
199            output_frame_idx: 0,
200            pending_output: Vec::new(),
201        }
202    }
203
204    /// Get the current configuration.
205    #[must_use]
206    pub fn config(&self) -> &DeinterlaceConfig {
207        &self.config
208    }
209
210    /// Detect if a frame is interlaced by analyzing combing artifacts.
211    fn detect_interlaced(&self, frame: &VideoFrame) -> bool {
212        if frame.planes.is_empty() {
213            return false;
214        }
215
216        let plane = &frame.planes[0];
217        let height = frame.height as usize;
218        let width = frame.width as usize;
219
220        // Simple comb detection: look for large differences between adjacent lines
221        let mut comb_score = 0u64;
222        let mut total_samples = 0u64;
223
224        for y in 1..height - 1 {
225            let row_prev = plane.row(y - 1);
226            let row_curr = plane.row(y);
227            let row_next = plane.row(y + 1);
228
229            for x in 0..width {
230                let prev = row_prev.get(x).copied().unwrap_or(0) as i32;
231                let curr = row_curr.get(x).copied().unwrap_or(0) as i32;
232                let next = row_next.get(x).copied().unwrap_or(0) as i32;
233
234                // Combing metric: current line differs significantly from interpolation
235                let interpolated = (prev + next) / 2;
236                let diff = (curr - interpolated).unsigned_abs() as u64;
237
238                if diff > 20 {
239                    comb_score += diff;
240                }
241                total_samples += 1;
242            }
243        }
244
245        if total_samples == 0 {
246            return false;
247        }
248
249        let score = comb_score as f64 / total_samples as f64;
250        score > self.config.detection_threshold * 10.0
251    }
252
253    /// Deinterlace using bob algorithm (line doubling with interpolation).
254    fn bob_deinterlace(&self, frame: &VideoFrame, field: usize) -> VideoFrame {
255        let mut output = VideoFrame::new(frame.format, frame.width, frame.height);
256        output.frame_type = frame.frame_type;
257        output.color_info = frame.color_info;
258
259        // Adjust timestamp for bob (two frames per input)
260        let half_duration =
261            frame.timestamp.timebase.den as i64 / (frame.timestamp.timebase.num as i64 * 2);
262        let pts_offset = if field == 0 { 0 } else { half_duration };
263        output.timestamp =
264            Timestamp::new(frame.timestamp.pts + pts_offset, frame.timestamp.timebase);
265
266        for (plane_idx, src_plane) in frame.planes.iter().enumerate() {
267            let (width, height) = frame.plane_dimensions(plane_idx);
268            let mut dst_data = vec![0u8; (width * height) as usize];
269
270            let field_start = if field == 0 {
271                self.config.field_order.first_field_start()
272            } else {
273                self.config.field_order.second_field_start()
274            };
275
276            for y in 0..height as usize {
277                let is_field_line = (y + field_start) % 2 == 0;
278
279                if is_field_line {
280                    // Copy original field line
281                    let src_row = src_plane.row(y);
282                    for x in 0..width as usize {
283                        dst_data[y * width as usize + x] = src_row.get(x).copied().unwrap_or(0);
284                    }
285                } else {
286                    // Interpolate from adjacent lines
287                    let y_prev = y.saturating_sub(1);
288                    let y_next = (y + 1).min(height as usize - 1);
289
290                    let prev_row = src_plane.row(y_prev);
291                    let next_row = src_plane.row(y_next);
292
293                    for x in 0..width as usize {
294                        let prev = prev_row.get(x).copied().unwrap_or(0) as u16;
295                        let next = next_row.get(x).copied().unwrap_or(0) as u16;
296                        dst_data[y * width as usize + x] = ((prev + next) / 2) as u8;
297                    }
298                }
299            }
300
301            output.planes.push(Plane::new(dst_data, width as usize));
302        }
303
304        output
305    }
306
307    /// Deinterlace using weave algorithm (combine fields).
308    fn weave_deinterlace(&self, frame: &VideoFrame) -> VideoFrame {
309        // Weave simply passes through the frame, combining both fields
310        // This is essentially a no-op but marks the frame as progressive
311        frame.clone()
312    }
313
314    /// Deinterlace using blend algorithm (average adjacent lines).
315    fn blend_deinterlace(&self, frame: &VideoFrame) -> VideoFrame {
316        let mut output = VideoFrame::new(frame.format, frame.width, frame.height);
317        output.timestamp = frame.timestamp;
318        output.frame_type = frame.frame_type;
319        output.color_info = frame.color_info;
320
321        for (plane_idx, src_plane) in frame.planes.iter().enumerate() {
322            let (width, height) = frame.plane_dimensions(plane_idx);
323            let mut dst_data = vec![0u8; (width * height) as usize];
324
325            for y in 0..height as usize {
326                let curr_row = src_plane.row(y);
327
328                if y == 0 || y == height as usize - 1 {
329                    // Copy edge lines directly
330                    for x in 0..width as usize {
331                        dst_data[y * width as usize + x] = curr_row.get(x).copied().unwrap_or(0);
332                    }
333                } else {
334                    // Blend with adjacent lines
335                    let prev_row = src_plane.row(y - 1);
336                    let next_row = src_plane.row(y + 1);
337
338                    for x in 0..width as usize {
339                        let prev = prev_row.get(x).copied().unwrap_or(0) as u32;
340                        let curr = curr_row.get(x).copied().unwrap_or(0) as u32;
341                        let next = next_row.get(x).copied().unwrap_or(0) as u32;
342
343                        // 50% current, 25% each adjacent
344                        let blended = (prev + curr * 2 + next) / 4;
345                        dst_data[y * width as usize + x] = blended as u8;
346                    }
347                }
348            }
349
350            output.planes.push(Plane::new(dst_data, width as usize));
351        }
352
353        output
354    }
355
356    /// Deinterlace using YADIF algorithm.
357    fn yadif_deinterlace(&self, field: usize) -> Option<VideoFrame> {
358        // YADIF requires 3 frames: previous, current, next
359        if self.frame_buffer.len() < 2 {
360            return None;
361        }
362
363        let curr_idx = if self.frame_buffer.len() >= 3 { 1 } else { 0 };
364        let frame = &self.frame_buffer[curr_idx];
365
366        let prev_frame = self.frame_buffer.front();
367        let next_frame = if self.frame_buffer.len() >= 3 {
368            self.frame_buffer.get(2)
369        } else {
370            self.frame_buffer.get(1)
371        };
372
373        let mut output = VideoFrame::new(frame.format, frame.width, frame.height);
374        output.frame_type = frame.frame_type;
375        output.color_info = frame.color_info;
376
377        // Adjust timestamp
378        let half_duration =
379            frame.timestamp.timebase.den as i64 / (frame.timestamp.timebase.num as i64 * 2);
380        let pts_offset = if field == 0 { 0 } else { half_duration };
381        output.timestamp =
382            Timestamp::new(frame.timestamp.pts + pts_offset, frame.timestamp.timebase);
383
384        for (plane_idx, src_plane) in frame.planes.iter().enumerate() {
385            let (width, height) = frame.plane_dimensions(plane_idx);
386            let mut dst_data = vec![0u8; (width * height) as usize];
387
388            let field_start = if field == 0 {
389                self.config.field_order.first_field_start()
390            } else {
391                self.config.field_order.second_field_start()
392            };
393
394            let prev_plane = prev_frame.and_then(|f| f.planes.get(plane_idx));
395            let next_plane = next_frame.and_then(|f| f.planes.get(plane_idx));
396
397            for y in 0..height as usize {
398                let is_field_line = (y + field_start) % 2 == 0;
399
400                if is_field_line {
401                    // Copy original field line
402                    let src_row = src_plane.row(y);
403                    for x in 0..width as usize {
404                        dst_data[y * width as usize + x] = src_row.get(x).copied().unwrap_or(0);
405                    }
406                } else {
407                    // YADIF interpolation
408                    for x in 0..width as usize {
409                        let pixel = self.yadif_pixel(
410                            src_plane,
411                            prev_plane,
412                            next_plane,
413                            x,
414                            y,
415                            width as usize,
416                            height as usize,
417                        );
418                        dst_data[y * width as usize + x] = pixel;
419                    }
420                }
421            }
422
423            output.planes.push(Plane::new(dst_data, width as usize));
424        }
425
426        Some(output)
427    }
428
429    /// Calculate a single YADIF pixel.
430    #[allow(clippy::too_many_arguments)]
431    fn yadif_pixel(
432        &self,
433        curr: &Plane,
434        prev: Option<&Plane>,
435        next: Option<&Plane>,
436        x: usize,
437        y: usize,
438        width: usize,
439        height: usize,
440    ) -> u8 {
441        let y_prev = y.saturating_sub(1);
442        let y_next = (y + 1).min(height - 1);
443
444        // Spatial prediction (from current frame)
445        let c_prev = curr.row(y_prev).get(x).copied().unwrap_or(0) as i32;
446        let c_next = curr.row(y_next).get(x).copied().unwrap_or(0) as i32;
447        let spatial = (c_prev + c_next) / 2;
448
449        // Temporal prediction (from previous and next frames)
450        let temporal = if let (Some(p), Some(n)) = (prev, next) {
451            let p_curr = p.row(y).get(x).copied().unwrap_or(0) as i32;
452            let n_curr = n.row(y).get(x).copied().unwrap_or(0) as i32;
453            (p_curr + n_curr) / 2
454        } else {
455            spatial
456        };
457
458        // Edge detection for choosing between spatial and temporal
459        let edge_prev = curr
460            .row(y_prev)
461            .get(x.saturating_sub(1))
462            .copied()
463            .unwrap_or(0) as i32;
464        let edge_next = curr
465            .row(y_next)
466            .get((x + 1).min(width - 1))
467            .copied()
468            .unwrap_or(0) as i32;
469
470        let spatial_diff = (c_prev - c_next).abs();
471        let edge_diff = (edge_prev - edge_next).abs();
472
473        // Use spatial prediction if there's significant spatial variation
474        let result = if spatial_diff > edge_diff * 2 {
475            temporal
476        } else {
477            spatial
478        };
479
480        result.clamp(0, 255) as u8
481    }
482
483    /// Process a frame and produce deinterlaced output.
484    fn process_frame(&mut self, frame: VideoFrame) -> Vec<VideoFrame> {
485        // Check for auto-detection
486        if self.config.auto_detect && !self.detect_interlaced(&frame) {
487            return vec![frame];
488        }
489
490        self.frame_buffer.push_back(frame);
491
492        // Keep buffer size limited
493        while self.frame_buffer.len() > 3 {
494            self.frame_buffer.pop_front();
495        }
496
497        let mut output = Vec::new();
498
499        match self.config.mode {
500            DeinterlaceMode::Bob => {
501                if let Some(frame) = self.frame_buffer.back() {
502                    output.push(self.bob_deinterlace(frame, 0));
503                    output.push(self.bob_deinterlace(frame, 1));
504                }
505            }
506            DeinterlaceMode::Weave => {
507                if let Some(frame) = self.frame_buffer.back() {
508                    output.push(self.weave_deinterlace(frame));
509                }
510            }
511            DeinterlaceMode::Blend => {
512                if let Some(frame) = self.frame_buffer.back() {
513                    output.push(self.blend_deinterlace(frame));
514                }
515            }
516            DeinterlaceMode::Yadif => {
517                if let Some(frame) = self.yadif_deinterlace(0) {
518                    output.push(frame);
519                }
520                if let Some(frame) = self.yadif_deinterlace(1) {
521                    output.push(frame);
522                }
523            }
524            DeinterlaceMode::YadifSpatial => {
525                // Spatial-only YADIF (similar to blend but with edge-aware interpolation)
526                if let Some(frame) = self.frame_buffer.back() {
527                    output.push(self.blend_deinterlace(frame));
528                }
529            }
530        }
531
532        self.output_frame_idx += output.len() as u64;
533        output
534    }
535}
536
537impl Node for DeinterlaceFilter {
538    fn id(&self) -> NodeId {
539        self.id
540    }
541
542    fn name(&self) -> &str {
543        &self.name
544    }
545
546    fn node_type(&self) -> NodeType {
547        NodeType::Filter
548    }
549
550    fn state(&self) -> NodeState {
551        self.state
552    }
553
554    fn set_state(&mut self, state: NodeState) -> GraphResult<()> {
555        if !self.state.can_transition_to(state) {
556            return Err(GraphError::InvalidStateTransition {
557                node: self.id,
558                from: self.state.to_string(),
559                to: state.to_string(),
560            });
561        }
562        self.state = state;
563        Ok(())
564    }
565
566    fn inputs(&self) -> &[InputPort] {
567        &self.inputs
568    }
569
570    fn outputs(&self) -> &[OutputPort] {
571        &self.outputs
572    }
573
574    fn process(&mut self, input: Option<FilterFrame>) -> GraphResult<Option<FilterFrame>> {
575        // First, check for pending output from previous processing
576        if !self.pending_output.is_empty() {
577            return Ok(Some(FilterFrame::Video(self.pending_output.remove(0))));
578        }
579
580        match input {
581            Some(FilterFrame::Video(frame)) => {
582                let mut output_frames = self.process_frame(frame);
583
584                if output_frames.is_empty() {
585                    Ok(None)
586                } else {
587                    let first = output_frames.remove(0);
588                    self.pending_output = output_frames;
589                    Ok(Some(FilterFrame::Video(first)))
590                }
591            }
592            Some(_) => Err(GraphError::PortTypeMismatch {
593                expected: "Video".to_string(),
594                actual: "Audio".to_string(),
595            }),
596            None => Ok(None),
597        }
598    }
599
600    fn flush(&mut self) -> GraphResult<Vec<FilterFrame>> {
601        let mut output: Vec<FilterFrame> = self
602            .pending_output
603            .drain(..)
604            .map(FilterFrame::Video)
605            .collect();
606
607        // Process any remaining buffered frames
608        while let Some(frame) = self.frame_buffer.pop_front() {
609            output.push(FilterFrame::Video(frame));
610        }
611
612        Ok(output)
613    }
614
615    fn reset(&mut self) -> GraphResult<()> {
616        self.frame_buffer.clear();
617        self.pending_output.clear();
618        self.output_frame_idx = 0;
619        self.set_state(NodeState::Idle)
620    }
621}
622
623/// Detect interlacing in a video frame.
624#[derive(Debug)]
625pub struct InterlaceDetector {
626    /// Detection threshold.
627    threshold: f64,
628    /// Number of frames analyzed.
629    frames_analyzed: u64,
630    /// Number of interlaced frames detected.
631    interlaced_count: u64,
632    /// Detected field order.
633    detected_field_order: Option<FieldOrder>,
634}
635
636impl Default for InterlaceDetector {
637    fn default() -> Self {
638        Self {
639            threshold: 0.5,
640            frames_analyzed: 0,
641            interlaced_count: 0,
642            detected_field_order: None,
643        }
644    }
645}
646
647impl InterlaceDetector {
648    /// Create a new interlace detector.
649    #[must_use]
650    pub fn new(threshold: f64) -> Self {
651        Self {
652            threshold: threshold.clamp(0.0, 1.0),
653            ..Default::default()
654        }
655    }
656
657    /// Analyze a frame for interlacing.
658    pub fn analyze(&mut self, frame: &VideoFrame) -> bool {
659        self.frames_analyzed += 1;
660
661        if frame.planes.is_empty() {
662            return false;
663        }
664
665        let is_interlaced = self.detect_combing(frame);
666        if is_interlaced {
667            self.interlaced_count += 1;
668        }
669
670        is_interlaced
671    }
672
673    /// Detect combing artifacts in a frame.
674    fn detect_combing(&self, frame: &VideoFrame) -> bool {
675        let plane = &frame.planes[0];
676        let height = frame.height as usize;
677        let width = frame.width as usize;
678
679        let mut comb_score = 0u64;
680        let mut samples = 0u64;
681
682        // Sample every 4th line and pixel for efficiency
683        for y in (2..height - 2).step_by(4) {
684            for x in (0..width).step_by(4) {
685                let row_m2 = plane.row(y - 2);
686                let row_m1 = plane.row(y - 1);
687                let row_0 = plane.row(y);
688                let row_p1 = plane.row(y + 1);
689                let row_p2 = plane.row(y + 2);
690
691                let m2 = row_m2.get(x).copied().unwrap_or(0) as i32;
692                let m1 = row_m1.get(x).copied().unwrap_or(0) as i32;
693                let c = row_0.get(x).copied().unwrap_or(0) as i32;
694                let p1 = row_p1.get(x).copied().unwrap_or(0) as i32;
695                let p2 = row_p2.get(x).copied().unwrap_or(0) as i32;
696
697                // Combing metric
698                let diff = ((m1 - c).abs() + (p1 - c).abs()) - ((m2 - c).abs() + (p2 - c).abs());
699                if diff > 20 {
700                    comb_score += diff.unsigned_abs() as u64;
701                }
702                samples += 1;
703            }
704        }
705
706        if samples == 0 {
707            return false;
708        }
709
710        let score = comb_score as f64 / samples as f64;
711        score > self.threshold * 10.0
712    }
713
714    /// Get the percentage of interlaced frames detected.
715    #[must_use]
716    pub fn interlaced_percentage(&self) -> f64 {
717        if self.frames_analyzed == 0 {
718            0.0
719        } else {
720            self.interlaced_count as f64 / self.frames_analyzed as f64
721        }
722    }
723
724    /// Check if content is likely interlaced.
725    #[must_use]
726    pub fn is_interlaced(&self) -> bool {
727        self.interlaced_percentage() > 0.5
728    }
729
730    /// Get the detected field order.
731    #[must_use]
732    pub fn field_order(&self) -> Option<FieldOrder> {
733        self.detected_field_order
734    }
735}
736
737#[cfg(test)]
738mod tests {
739    use super::*;
740    use oximedia_core::PixelFormat;
741
742    fn create_test_frame(width: u32, height: u32) -> VideoFrame {
743        let mut frame = VideoFrame::new(PixelFormat::Yuv420p, width, height);
744        frame.allocate();
745
746        // Create a pattern that simulates interlacing
747        if let Some(plane) = frame.planes.get_mut(0) {
748            let mut data = vec![0u8; (width * height) as usize];
749            for y in 0..height as usize {
750                for x in 0..width as usize {
751                    // Alternating line pattern
752                    data[y * width as usize + x] = if y % 2 == 0 { 200 } else { 50 };
753                }
754            }
755            *plane = Plane::new(data, width as usize);
756        }
757
758        frame
759    }
760
761    fn create_progressive_frame(width: u32, height: u32) -> VideoFrame {
762        let mut frame = VideoFrame::new(PixelFormat::Yuv420p, width, height);
763        frame.allocate();
764
765        // Create a smooth gradient (no interlacing)
766        if let Some(plane) = frame.planes.get_mut(0) {
767            let mut data = vec![0u8; (width * height) as usize];
768            for y in 0..height as usize {
769                for x in 0..width as usize {
770                    data[y * width as usize + x] = ((y * 255) / height as usize) as u8;
771                }
772            }
773            *plane = Plane::new(data, width as usize);
774        }
775
776        frame
777    }
778
779    #[test]
780    fn test_deinterlace_mode_properties() {
781        assert!(DeinterlaceMode::Bob.doubles_framerate());
782        assert!(DeinterlaceMode::Yadif.doubles_framerate());
783        assert!(!DeinterlaceMode::Blend.doubles_framerate());
784        assert!(!DeinterlaceMode::Weave.doubles_framerate());
785
786        assert!(DeinterlaceMode::Yadif.requires_temporal());
787        assert!(!DeinterlaceMode::Bob.requires_temporal());
788    }
789
790    #[test]
791    fn test_field_order() {
792        assert_eq!(FieldOrder::TopFieldFirst.first_field_start(), 0);
793        assert_eq!(FieldOrder::TopFieldFirst.second_field_start(), 1);
794        assert_eq!(FieldOrder::BottomFieldFirst.first_field_start(), 1);
795        assert_eq!(FieldOrder::BottomFieldFirst.second_field_start(), 0);
796    }
797
798    #[test]
799    fn test_deinterlace_config() {
800        let config = DeinterlaceConfig::new(DeinterlaceMode::Bob)
801            .with_field_order(FieldOrder::BottomFieldFirst)
802            .with_auto_detect(true)
803            .with_detection_threshold(0.7);
804
805        assert_eq!(config.mode, DeinterlaceMode::Bob);
806        assert_eq!(config.field_order, FieldOrder::BottomFieldFirst);
807        assert!(config.auto_detect);
808        assert!((config.detection_threshold - 0.7).abs() < 0.001);
809    }
810
811    #[test]
812    fn test_deinterlace_filter_creation() {
813        let config = DeinterlaceConfig::new(DeinterlaceMode::Blend);
814        let filter = DeinterlaceFilter::new(NodeId(0), "deinterlace", config);
815
816        assert_eq!(filter.id(), NodeId(0));
817        assert_eq!(filter.name(), "deinterlace");
818        assert_eq!(filter.node_type(), NodeType::Filter);
819    }
820
821    #[test]
822    fn test_bob_deinterlace() {
823        let config = DeinterlaceConfig::new(DeinterlaceMode::Bob);
824        let mut filter = DeinterlaceFilter::new(NodeId(0), "deinterlace", config);
825
826        let input = create_test_frame(64, 48);
827        let output_frames = filter.process_frame(input);
828
829        // Bob should produce 2 frames
830        assert_eq!(output_frames.len(), 2);
831    }
832
833    #[test]
834    fn test_blend_deinterlace() {
835        let config = DeinterlaceConfig::new(DeinterlaceMode::Blend);
836        let mut filter = DeinterlaceFilter::new(NodeId(0), "deinterlace", config);
837
838        let input = create_test_frame(64, 48);
839        let output_frames = filter.process_frame(input);
840
841        // Blend should produce 1 frame
842        assert_eq!(output_frames.len(), 1);
843    }
844
845    #[test]
846    fn test_weave_deinterlace() {
847        let config = DeinterlaceConfig::new(DeinterlaceMode::Weave);
848        let mut filter = DeinterlaceFilter::new(NodeId(0), "deinterlace", config);
849
850        let input = create_test_frame(64, 48);
851        let output_frames = filter.process_frame(input);
852
853        assert_eq!(output_frames.len(), 1);
854    }
855
856    #[test]
857    fn test_auto_detect_progressive() {
858        let config = DeinterlaceConfig::new(DeinterlaceMode::Blend).with_auto_detect(true);
859        let mut filter = DeinterlaceFilter::new(NodeId(0), "deinterlace", config);
860
861        let input = create_progressive_frame(64, 48);
862        let output_frames = filter.process_frame(input);
863
864        // Progressive frame should pass through unchanged
865        assert_eq!(output_frames.len(), 1);
866    }
867
868    #[test]
869    fn test_interlace_detector() {
870        let mut detector = InterlaceDetector::new(0.5);
871
872        let interlaced = create_test_frame(64, 48);
873        let _is_interlaced = detector.analyze(&interlaced);
874
875        assert!(detector.frames_analyzed > 0);
876        // Note: detection may vary based on pattern
877    }
878
879    #[test]
880    fn test_interlace_detector_percentage() {
881        let detector = InterlaceDetector {
882            threshold: 0.5,
883            frames_analyzed: 10,
884            interlaced_count: 7,
885            detected_field_order: None,
886        };
887
888        assert!((detector.interlaced_percentage() - 0.7).abs() < 0.001);
889        assert!(detector.is_interlaced());
890    }
891
892    #[test]
893    fn test_node_process() {
894        let config = DeinterlaceConfig::new(DeinterlaceMode::Blend);
895        let mut filter = DeinterlaceFilter::new(NodeId(0), "deinterlace", config);
896
897        let input = create_test_frame(64, 48);
898        let result = filter
899            .process(Some(FilterFrame::Video(input)))
900            .expect("operation should succeed")
901            .expect("operation should succeed");
902
903        assert!(matches!(result, FilterFrame::Video(_)));
904    }
905
906    #[test]
907    fn test_node_state_transitions() {
908        let config = DeinterlaceConfig::new(DeinterlaceMode::Blend);
909        let mut filter = DeinterlaceFilter::new(NodeId(0), "deinterlace", config);
910
911        assert_eq!(filter.state(), NodeState::Idle);
912        filter
913            .set_state(NodeState::Processing)
914            .expect("set_state should succeed");
915        assert_eq!(filter.state(), NodeState::Processing);
916    }
917
918    #[test]
919    fn test_process_none_input() {
920        let config = DeinterlaceConfig::new(DeinterlaceMode::Blend);
921        let mut filter = DeinterlaceFilter::new(NodeId(0), "deinterlace", config);
922
923        let result = filter.process(None).expect("process should succeed");
924        assert!(result.is_none());
925    }
926
927    #[test]
928    fn test_reset() {
929        let config = DeinterlaceConfig::new(DeinterlaceMode::Blend);
930        let mut filter = DeinterlaceFilter::new(NodeId(0), "deinterlace", config);
931
932        let input = create_test_frame(64, 48);
933        let _ = filter.process(Some(FilterFrame::Video(input)));
934
935        filter.reset().expect("reset should succeed");
936
937        assert!(filter.frame_buffer.is_empty());
938        assert_eq!(filter.output_frame_idx, 0);
939    }
940}