Skip to main content

oximedia_codec/reconstruct/
cdef_apply.rs

1//! CDEF (Constrained Directional Enhancement Filter) application.
2//!
3//! CDEF is a directional filter applied after the loop filter to reduce
4//! ringing and mosquito noise artifacts. It uses directional filtering
5//! along edges to preserve sharpness while reducing noise.
6
7#![forbid(unsafe_code)]
8#![allow(clippy::unreadable_literal)]
9#![allow(clippy::items_after_statements)]
10#![allow(clippy::unnecessary_wraps)]
11#![allow(clippy::struct_excessive_bools)]
12#![allow(clippy::identity_op)]
13#![allow(clippy::range_plus_one)]
14#![allow(clippy::needless_range_loop)]
15#![allow(clippy::useless_conversion)]
16#![allow(clippy::redundant_closure_for_method_calls)]
17#![allow(clippy::single_match_else)]
18#![allow(dead_code)]
19#![allow(clippy::doc_markdown)]
20#![allow(clippy::unused_self)]
21#![allow(clippy::trivially_copy_pass_by_ref)]
22#![allow(clippy::cast_possible_truncation)]
23#![allow(clippy::cast_sign_loss)]
24#![allow(clippy::cast_possible_wrap)]
25#![allow(clippy::missing_errors_doc)]
26#![allow(clippy::too_many_arguments)]
27#![allow(clippy::similar_names)]
28#![allow(clippy::many_single_char_names)]
29#![allow(clippy::cast_precision_loss)]
30#![allow(clippy::cast_lossless)]
31
32use super::pipeline::FrameContext;
33use super::{FrameBuffer, PlaneBuffer, PlaneType, ReconstructResult};
34
35// =============================================================================
36// Constants
37// =============================================================================
38
39/// CDEF block size (8x8).
40pub const CDEF_BLOCK_SIZE: usize = 8;
41
42/// Number of CDEF directions.
43pub const CDEF_NUM_DIRECTIONS: usize = 8;
44
45/// Maximum primary strength.
46pub const CDEF_MAX_PRIMARY: u8 = 15;
47
48/// Maximum secondary strength.
49pub const CDEF_MAX_SECONDARY: u8 = 4;
50
51/// Minimum damping value.
52pub const CDEF_DAMPING_MIN: u8 = 3;
53
54/// Maximum damping value.
55pub const CDEF_DAMPING_MAX: u8 = 6;
56
57/// Secondary strength values.
58pub const CDEF_SEC_STRENGTHS: [u8; 4] = [0, 1, 2, 4];
59
60// =============================================================================
61// Direction Offsets
62// =============================================================================
63
64/// Direction tap offsets for primary filtering (2 taps per direction).
65const CDEF_PRIMARY_OFFSETS: [[(i8, i8); 2]; CDEF_NUM_DIRECTIONS] = [
66    [(-1, 0), (1, 0)],  // 0: Horizontal
67    [(-1, -1), (1, 1)], // 1: 22.5 degrees
68    [(0, -1), (0, 1)],  // 2: Vertical
69    [(1, -1), (-1, 1)], // 3: 67.5 degrees
70    [(0, -1), (0, 1)],  // 4: Vertical (same as 2)
71    [(-1, -1), (1, 1)], // 5: 112.5 degrees
72    [(-1, 0), (1, 0)],  // 6: Horizontal (same as 0)
73    [(1, -1), (-1, 1)], // 7: 157.5 degrees
74];
75
76/// Direction tap offsets for secondary filtering (4 taps per direction).
77const CDEF_SECONDARY_OFFSETS: [[(i8, i8); 4]; CDEF_NUM_DIRECTIONS] = [
78    [(-2, 0), (2, 0), (-1, -1), (1, 1)],
79    [(-2, -1), (2, 1), (-1, -2), (1, 2)],
80    [(-1, -1), (1, 1), (-1, -2), (1, 2)],
81    [(2, -1), (-2, 1), (1, -2), (-1, 2)],
82    [(0, -2), (0, 2), (-1, -1), (1, 1)],
83    [(-2, -1), (2, 1), (-1, -2), (1, 2)],
84    [(-1, -1), (1, 1), (-2, 0), (2, 0)],
85    [(2, -1), (-2, 1), (1, -2), (-1, 2)],
86];
87
88// =============================================================================
89// CDEF Block Configuration
90// =============================================================================
91
92/// Configuration for CDEF on a single block.
93#[derive(Clone, Copy, Debug, Default)]
94pub struct CdefBlockConfig {
95    /// Primary strength (0-15).
96    pub primary_strength: u8,
97    /// Secondary strength (0-3, maps to 0, 1, 2, 4).
98    pub secondary_strength: u8,
99    /// Damping value.
100    pub damping: u8,
101    /// Detected direction (0-7).
102    pub direction: u8,
103    /// Skip CDEF for this block.
104    pub skip: bool,
105}
106
107impl CdefBlockConfig {
108    /// Create a new block configuration.
109    #[must_use]
110    pub const fn new(primary: u8, secondary: u8, damping: u8) -> Self {
111        Self {
112            primary_strength: primary,
113            secondary_strength: secondary,
114            damping,
115            direction: 0,
116            skip: false,
117        }
118    }
119
120    /// Get the actual secondary strength value.
121    #[must_use]
122    pub fn secondary_value(&self) -> u8 {
123        CDEF_SEC_STRENGTHS
124            .get(self.secondary_strength as usize)
125            .copied()
126            .unwrap_or(0)
127    }
128
129    /// Check if filtering is enabled.
130    #[must_use]
131    pub const fn is_enabled(&self) -> bool {
132        !self.skip && (self.primary_strength > 0 || self.secondary_strength > 0)
133    }
134}
135
136// =============================================================================
137// CDEF Filter Result
138// =============================================================================
139
140/// Result of CDEF filtering on a block.
141#[derive(Clone, Debug, Default)]
142pub struct CdefFilterResult {
143    /// Direction detected for this block.
144    pub direction: u8,
145    /// Variance measure.
146    pub variance: u32,
147    /// Number of pixels modified.
148    pub pixels_modified: u32,
149}
150
151// =============================================================================
152// Direction Detection
153// =============================================================================
154
155/// Detect the dominant edge direction in an 8x8 block.
156fn detect_direction(block: &[i16], stride: usize) -> (u8, u32) {
157    let mut best_dir = 0u8;
158    let mut best_var = u32::MAX;
159
160    // Direction detection kernels (simplified)
161    const DIR_KERNELS: [[i8; 8]; 8] = [
162        [1, 1, 1, 1, -1, -1, -1, -1], // Horizontal
163        [1, 1, 1, 0, 0, -1, -1, -1],  // 22.5 deg
164        [1, 1, 0, -1, -1, 0, 1, 1],   // Vertical
165        [-1, -1, -1, 0, 0, 1, 1, 1],  // 67.5 deg
166        [-1, -1, -1, -1, 1, 1, 1, 1], // Horizontal flip
167        [-1, -1, 0, 1, 1, 0, -1, -1], // 112.5 deg
168        [-1, -1, -1, -1, 1, 1, 1, 1], // Vertical flip
169        [1, 1, 1, 0, 0, -1, -1, -1],  // 157.5 deg
170    ];
171
172    for (dir, kernel) in DIR_KERNELS.iter().enumerate() {
173        let mut sum: i32 = 0;
174        let mut sum_sq: i32 = 0;
175
176        for row in 0..8 {
177            for col in 0..8 {
178                let pixel = i32::from(block[row * stride + col]);
179                let weight = i32::from(kernel[(row + col) % 8]);
180                let val = pixel * weight;
181                sum += val;
182                sum_sq += val * val;
183            }
184        }
185
186        // Compute variance-like measure
187        let variance = (sum_sq - (sum * sum / 64)).unsigned_abs();
188
189        if variance < best_var {
190            best_var = variance;
191            best_dir = dir as u8;
192        }
193    }
194
195    (best_dir, best_var)
196}
197
198// =============================================================================
199// CDEF Filtering
200// =============================================================================
201
202/// Compute the constrained difference.
203fn constrain(diff: i16, threshold: i16, damping: u8) -> i16 {
204    if threshold == 0 {
205        return 0;
206    }
207
208    let sign = if diff < 0 { -1i16 } else { 1i16 };
209    let abs_diff = diff.abs();
210    let abs_thresh = threshold.abs();
211
212    let damping_shift = damping.saturating_sub(abs_thresh as u8);
213
214    if damping_shift >= 15 {
215        return 0;
216    }
217
218    let clamped = abs_diff.min(abs_thresh);
219    let adjusted = clamped - (clamped >> damping_shift);
220
221    sign * adjusted
222}
223
224/// Apply CDEF filter to a single 8x8 block.
225fn filter_block(
226    src: &[i16],
227    src_stride: usize,
228    dst: &mut [i16],
229    dst_stride: usize,
230    config: &CdefBlockConfig,
231    bd: u8,
232) {
233    let max_val = (1i16 << bd) - 1;
234    let primary = i16::from(config.primary_strength);
235    let secondary = i16::from(config.secondary_value());
236    let damping = config.damping;
237    let dir = config.direction as usize;
238
239    for row in 0..CDEF_BLOCK_SIZE {
240        for col in 0..CDEF_BLOCK_SIZE {
241            let src_idx = row * src_stride + col;
242            let dst_idx = row * dst_stride + col;
243
244            let center = src[src_idx];
245            let mut sum: i32 = 0;
246
247            // Primary filter (along edge)
248            if primary > 0 {
249                for &(dx, dy) in &CDEF_PRIMARY_OFFSETS[dir] {
250                    let nx = col as i32 + i32::from(dx);
251                    let ny = row as i32 + i32::from(dy);
252
253                    if nx >= 0
254                        && nx < CDEF_BLOCK_SIZE as i32
255                        && ny >= 0
256                        && ny < CDEF_BLOCK_SIZE as i32
257                    {
258                        let neighbor_idx = ny as usize * src_stride + nx as usize;
259                        let neighbor = src[neighbor_idx];
260                        let diff = neighbor - center;
261                        sum += i32::from(constrain(diff, primary, damping)) * 2;
262                    }
263                }
264            }
265
266            // Secondary filter (perpendicular)
267            if secondary > 0 {
268                for &(dx, dy) in &CDEF_SECONDARY_OFFSETS[dir] {
269                    let nx = col as i32 + i32::from(dx);
270                    let ny = row as i32 + i32::from(dy);
271
272                    if nx >= 0
273                        && nx < CDEF_BLOCK_SIZE as i32
274                        && ny >= 0
275                        && ny < CDEF_BLOCK_SIZE as i32
276                    {
277                        let neighbor_idx = ny as usize * src_stride + nx as usize;
278                        let neighbor = src[neighbor_idx];
279                        let diff = neighbor - center;
280                        sum += i32::from(constrain(diff, secondary, damping));
281                    }
282                }
283            }
284
285            // Apply filter result
286            let filtered = i32::from(center) + ((sum + 8) >> 4);
287            dst[dst_idx] = (filtered as i16).clamp(0, max_val);
288        }
289    }
290}
291
292// =============================================================================
293// CDEF Applicator
294// =============================================================================
295
296/// CDEF applicator for applying CDEF to frames.
297#[derive(Debug)]
298pub struct CdefApplicator {
299    /// Frame width.
300    width: u32,
301    /// Frame height.
302    height: u32,
303    /// Bit depth.
304    bit_depth: u8,
305    /// Y damping.
306    damping_y: u8,
307    /// UV damping.
308    damping_uv: u8,
309    /// Y strength presets.
310    y_presets: Vec<(u8, u8)>,
311    /// UV strength presets.
312    uv_presets: Vec<(u8, u8)>,
313    /// Temporary buffer for filtering.
314    temp_buffer: Vec<i16>,
315}
316
317impl CdefApplicator {
318    /// Create a new CDEF applicator.
319    #[must_use]
320    pub fn new(width: u32, height: u32, bit_depth: u8) -> Self {
321        let buffer_size = CDEF_BLOCK_SIZE * CDEF_BLOCK_SIZE;
322
323        Self {
324            width,
325            height,
326            bit_depth,
327            damping_y: CDEF_DAMPING_MIN,
328            damping_uv: CDEF_DAMPING_MIN,
329            y_presets: vec![(0, 0); 8],
330            uv_presets: vec![(0, 0); 8],
331            temp_buffer: vec![0i16; buffer_size],
332        }
333    }
334
335    /// Set damping values.
336    pub fn set_damping(&mut self, y_damping: u8, uv_damping: u8) {
337        self.damping_y = y_damping.clamp(CDEF_DAMPING_MIN, CDEF_DAMPING_MAX);
338        self.damping_uv = uv_damping.clamp(CDEF_DAMPING_MIN, CDEF_DAMPING_MAX);
339    }
340
341    /// Set Y strength preset.
342    pub fn set_y_preset(&mut self, index: usize, primary: u8, secondary: u8) {
343        if index < self.y_presets.len() {
344            self.y_presets[index] = (primary.min(CDEF_MAX_PRIMARY), secondary.min(3));
345        }
346    }
347
348    /// Set UV strength preset.
349    pub fn set_uv_preset(&mut self, index: usize, primary: u8, secondary: u8) {
350        if index < self.uv_presets.len() {
351            self.uv_presets[index] = (primary.min(CDEF_MAX_PRIMARY), secondary.min(3));
352        }
353    }
354
355    /// Get damping for a plane.
356    #[must_use]
357    pub fn damping(&self, plane: PlaneType) -> u8 {
358        match plane {
359            PlaneType::Y => self.damping_y,
360            PlaneType::U | PlaneType::V => self.damping_uv,
361        }
362    }
363
364    /// Get adjusted damping for bit depth.
365    #[must_use]
366    pub fn adjusted_damping(&self, plane: PlaneType) -> u8 {
367        let base = self.damping(plane);
368        base + self.bit_depth.saturating_sub(8).min(4)
369    }
370
371    /// Apply CDEF to a frame.
372    ///
373    /// # Errors
374    ///
375    /// Returns error if CDEF application fails.
376    pub fn apply(
377        &mut self,
378        frame: &mut FrameBuffer,
379        _context: &FrameContext,
380    ) -> ReconstructResult<()> {
381        let bd = frame.bit_depth();
382
383        // Apply to Y plane
384        self.apply_to_plane(frame.y_plane_mut(), PlaneType::Y, bd)?;
385
386        // Apply to chroma planes
387        if let Some(u) = frame.u_plane_mut() {
388            self.apply_to_plane(u, PlaneType::U, bd)?;
389        }
390        if let Some(v) = frame.v_plane_mut() {
391            self.apply_to_plane(v, PlaneType::V, bd)?;
392        }
393
394        Ok(())
395    }
396
397    /// Apply CDEF to a single plane.
398    fn apply_to_plane(
399        &mut self,
400        plane: &mut PlaneBuffer,
401        plane_type: PlaneType,
402        bd: u8,
403    ) -> ReconstructResult<()> {
404        let width = plane.width() as usize;
405        let height = plane.height() as usize;
406        let stride = plane.stride();
407
408        let damping = self.adjusted_damping(plane_type);
409
410        // Process each 8x8 block
411        let blocks_x = width / CDEF_BLOCK_SIZE;
412        let blocks_y = height / CDEF_BLOCK_SIZE;
413
414        for by in 0..blocks_y {
415            for bx in 0..blocks_x {
416                let x = bx * CDEF_BLOCK_SIZE;
417                let y = by * CDEF_BLOCK_SIZE;
418
419                // Get block data
420                let block_start = y * stride + x;
421                let block_data = &plane.data()[block_start..];
422
423                // Detect direction
424                let (direction, _variance) = detect_direction(block_data, stride);
425
426                // Get strength from preset (use preset 0 for simplicity)
427                let (primary, secondary) = match plane_type {
428                    PlaneType::Y => self.y_presets.first().copied().unwrap_or((0, 0)),
429                    _ => self.uv_presets.first().copied().unwrap_or((0, 0)),
430                };
431
432                // Skip if no filtering needed
433                if primary == 0 && secondary == 0 {
434                    continue;
435                }
436
437                // Create block config
438                let config = CdefBlockConfig {
439                    primary_strength: primary,
440                    secondary_strength: secondary,
441                    damping,
442                    direction,
443                    skip: false,
444                };
445
446                // Filter the block
447                filter_block(
448                    block_data,
449                    stride,
450                    &mut self.temp_buffer,
451                    CDEF_BLOCK_SIZE,
452                    &config,
453                    bd,
454                );
455
456                // Write back filtered data
457                let plane_data = plane.data_mut();
458                for row in 0..CDEF_BLOCK_SIZE {
459                    for col in 0..CDEF_BLOCK_SIZE {
460                        let src_idx = row * CDEF_BLOCK_SIZE + col;
461                        let dst_idx = (y + row) * stride + (x + col);
462                        plane_data[dst_idx] = self.temp_buffer[src_idx];
463                    }
464                }
465            }
466        }
467
468        Ok(())
469    }
470
471    /// Filter a single block with given parameters.
472    pub fn filter_single_block(
473        &mut self,
474        plane: &mut PlaneBuffer,
475        x: u32,
476        y: u32,
477        config: &CdefBlockConfig,
478    ) -> CdefFilterResult {
479        let bd = plane.bit_depth();
480        let stride = plane.stride();
481
482        // Get block data
483        let block_start = y as usize * stride + x as usize;
484        let block_data = &plane.data()[block_start..];
485
486        // Detect direction if not specified
487        let (direction, variance) = if config.direction == 0 {
488            detect_direction(block_data, stride)
489        } else {
490            (config.direction, 0)
491        };
492
493        let mut actual_config = *config;
494        actual_config.direction = direction;
495
496        if !actual_config.is_enabled() {
497            return CdefFilterResult {
498                direction,
499                variance,
500                pixels_modified: 0,
501            };
502        }
503
504        // Filter the block
505        filter_block(
506            block_data,
507            stride,
508            &mut self.temp_buffer,
509            CDEF_BLOCK_SIZE,
510            &actual_config,
511            bd,
512        );
513
514        // Count modified pixels and write back
515        let mut pixels_modified = 0u32;
516        let plane_data = plane.data_mut();
517
518        for row in 0..CDEF_BLOCK_SIZE {
519            for col in 0..CDEF_BLOCK_SIZE {
520                let src_idx = row * CDEF_BLOCK_SIZE + col;
521                let dst_idx = (y as usize + row) * stride + (x as usize + col);
522
523                if plane_data[dst_idx] != self.temp_buffer[src_idx] {
524                    pixels_modified += 1;
525                }
526                plane_data[dst_idx] = self.temp_buffer[src_idx];
527            }
528        }
529
530        CdefFilterResult {
531            direction,
532            variance,
533            pixels_modified,
534        }
535    }
536}
537
538// =============================================================================
539// Tests
540// =============================================================================
541
542#[cfg(test)]
543mod tests {
544    use super::*;
545    use crate::reconstruct::ChromaSubsampling;
546
547    #[test]
548    fn test_cdef_block_config() {
549        let config = CdefBlockConfig::new(8, 2, 4);
550        assert_eq!(config.primary_strength, 8);
551        assert_eq!(config.secondary_strength, 2);
552        assert_eq!(config.secondary_value(), 2);
553        assert!(config.is_enabled());
554
555        let config_disabled = CdefBlockConfig::new(0, 0, 4);
556        assert!(!config_disabled.is_enabled());
557    }
558
559    #[test]
560    fn test_cdef_secondary_values() {
561        let mut config = CdefBlockConfig::new(0, 0, 4);
562        assert_eq!(config.secondary_value(), 0);
563
564        config.secondary_strength = 1;
565        assert_eq!(config.secondary_value(), 1);
566
567        config.secondary_strength = 2;
568        assert_eq!(config.secondary_value(), 2);
569
570        config.secondary_strength = 3;
571        assert_eq!(config.secondary_value(), 4);
572    }
573
574    #[test]
575    fn test_constrain() {
576        // Zero threshold returns zero
577        assert_eq!(constrain(10, 0, 3), 0);
578
579        // Positive difference
580        let result = constrain(5, 10, 3);
581        assert!(result >= 0 && result <= 5);
582
583        // Negative difference
584        let result = constrain(-5, 10, 3);
585        assert!(result <= 0 && result >= -5);
586    }
587
588    #[test]
589    fn test_detect_direction() {
590        // Test direction detection returns valid direction
591        let block = vec![128i16; 64];
592        let (dir, var) = detect_direction(&block, 8);
593        assert!(dir < 8); // Direction should be 0-7
594        let _ = var; // Variance depends on block content
595    }
596
597    #[test]
598    fn test_cdef_applicator_creation() {
599        let applicator = CdefApplicator::new(1920, 1080, 8);
600        assert_eq!(applicator.width, 1920);
601        assert_eq!(applicator.height, 1080);
602        assert_eq!(applicator.bit_depth, 8);
603    }
604
605    #[test]
606    fn test_cdef_applicator_damping() {
607        let mut applicator = CdefApplicator::new(64, 64, 8);
608        applicator.set_damping(4, 5);
609
610        assert_eq!(applicator.damping(PlaneType::Y), 4);
611        assert_eq!(applicator.damping(PlaneType::U), 5);
612        assert_eq!(applicator.damping(PlaneType::V), 5);
613    }
614
615    #[test]
616    fn test_cdef_applicator_adjusted_damping() {
617        let applicator = CdefApplicator::new(64, 64, 10);
618        let adj_damping = applicator.adjusted_damping(PlaneType::Y);
619
620        // 10-bit adds 2 to base damping
621        assert_eq!(adj_damping, CDEF_DAMPING_MIN + 2);
622    }
623
624    #[test]
625    fn test_cdef_applicator_presets() {
626        let mut applicator = CdefApplicator::new(64, 64, 8);
627        applicator.set_y_preset(0, 8, 2);
628        applicator.set_uv_preset(0, 4, 1);
629
630        assert_eq!(applicator.y_presets[0], (8, 2));
631        assert_eq!(applicator.uv_presets[0], (4, 1));
632    }
633
634    #[test]
635    fn test_cdef_applicator_apply() {
636        let mut frame = FrameBuffer::new(64, 64, 8, ChromaSubsampling::Cs420);
637        let context = FrameContext::new(64, 64);
638
639        let mut applicator = CdefApplicator::new(64, 64, 8);
640        applicator.set_y_preset(0, 4, 1);
641
642        let result = applicator.apply(&mut frame, &context);
643        assert!(result.is_ok());
644    }
645
646    #[test]
647    fn test_filter_block() {
648        let src = vec![128i16; 64];
649        let mut dst = vec![0i16; 64];
650
651        let config = CdefBlockConfig::new(4, 1, 4);
652
653        filter_block(&src, 8, &mut dst, 8, &config, 8);
654
655        // Uniform input should produce similar output
656        for &val in &dst {
657            assert!((val - 128).abs() < 10);
658        }
659    }
660
661    #[test]
662    fn test_filter_single_block() {
663        let mut frame = FrameBuffer::new(64, 64, 8, ChromaSubsampling::Cs420);
664
665        // Set some test values
666        for y in 0..8 {
667            for x in 0..8 {
668                frame.y_plane_mut().set(x, y, 128);
669            }
670        }
671
672        let mut applicator = CdefApplicator::new(64, 64, 8);
673        let config = CdefBlockConfig::new(4, 1, 4);
674
675        let result = applicator.filter_single_block(frame.y_plane_mut(), 0, 0, &config);
676
677        assert!(result.direction < CDEF_NUM_DIRECTIONS as u8);
678    }
679
680    #[test]
681    fn test_constants() {
682        assert_eq!(CDEF_BLOCK_SIZE, 8);
683        assert_eq!(CDEF_NUM_DIRECTIONS, 8);
684        assert_eq!(CDEF_MAX_PRIMARY, 15);
685        assert_eq!(CDEF_MAX_SECONDARY, 4);
686        assert_eq!(CDEF_DAMPING_MIN, 3);
687        assert_eq!(CDEF_DAMPING_MAX, 6);
688    }
689}