Skip to main content

signinum_transcode/
dct53_2d.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Constrained 2D DCT to 5/3 wavelet experiments.
4//!
5//! The direct float path projects an 8x8 DCT block into one separable
6//! single-level 5/3 result without first storing the 8x8 spatial samples. The
7//! reference path materializes samples to keep the oracle easy to audit.
8
9use crate::dct_grid::{high_len, idct8_basis, low_len, validate_dct_block_grid};
10pub use crate::DctGridError as Dct53GridError;
11
12/// One separable single-level 2D 5/3 transform result.
13#[derive(Debug, Clone, PartialEq)]
14pub struct Dwt53TwoDimensional<T> {
15    /// Low-horizontal, low-vertical band.
16    pub ll: Vec<T>,
17    /// High-horizontal, low-vertical band.
18    pub hl: Vec<T>,
19    /// Low-horizontal, high-vertical band.
20    pub lh: Vec<T>,
21    /// High-horizontal, high-vertical band.
22    pub hh: Vec<T>,
23    /// Width of horizontally low-pass bands.
24    pub low_width: usize,
25    /// Height of vertically low-pass bands.
26    pub low_height: usize,
27    /// Width of horizontally high-pass bands.
28    pub high_width: usize,
29    /// Height of vertically high-pass bands.
30    pub high_height: usize,
31}
32
33impl Dwt53TwoDimensional<f64> {
34    /// Maximum absolute coefficient difference across matching bands.
35    #[must_use]
36    pub fn max_abs_diff(&self, other: &Self) -> f64 {
37        assert_eq!(self.low_width, other.low_width);
38        assert_eq!(self.low_height, other.low_height);
39        assert_eq!(self.high_width, other.high_width);
40        assert_eq!(self.high_height, other.high_height);
41
42        self.ll
43            .iter()
44            .zip(other.ll.iter())
45            .chain(self.hl.iter().zip(other.hl.iter()))
46            .chain(self.lh.iter().zip(other.lh.iter()))
47            .chain(self.hh.iter().zip(other.hh.iter()))
48            .map(|(actual, expected)| (actual - expected).abs())
49            .fold(0.0, f64::max)
50    }
51}
52
53/// Scratch storage for repeated DCT-grid to 5/3 projection calls.
54///
55/// Reuse one value per worker when transforming many components or tiles with
56/// matching geometry. The scratch caches linearized 5/3 weight rows; it does
57/// not store spatial samples.
58#[derive(Debug, Default)]
59pub struct Dct53GridScratch {
60    x_weights: Dwt53WeightRows,
61    y_weights: Dwt53WeightRows,
62}
63
64impl Dct53GridScratch {
65    /// Aggregate capacity of cached weight rows.
66    ///
67    /// This is intended for experimental tests and benchmark instrumentation.
68    #[must_use]
69    pub fn weight_row_capacity(&self) -> usize {
70        self.x_weights.weight_capacity() + self.y_weights.weight_capacity()
71    }
72}
73
74/// Map one 8x8 DCT block directly into a linearized one-level 2D 5/3 result.
75#[must_use]
76pub fn dct8x8_to_dwt53_float_linear(block: [[f64; 8]; 8]) -> Dwt53TwoDimensional<f64> {
77    let width = 8;
78    let height = 8;
79    let low_width = low_len(width);
80    let low_height = low_len(height);
81    let high_width = high_len(width);
82    let high_height = high_len(height);
83
84    let mut ll = Vec::with_capacity(low_width * low_height);
85    let mut hl = Vec::with_capacity(high_width * low_height);
86    let mut lh = Vec::with_capacity(low_width * high_height);
87    let mut hh = Vec::with_capacity(high_width * high_height);
88
89    for y in 0..low_height {
90        for x in 0..low_width {
91            ll.push(project_dct_block(&block, true, y, true, x));
92        }
93        for x in 0..high_width {
94            hl.push(project_dct_block(&block, true, y, false, x));
95        }
96    }
97
98    for y in 0..high_height {
99        for x in 0..low_width {
100            lh.push(project_dct_block(&block, false, y, true, x));
101        }
102        for x in 0..high_width {
103            hh.push(project_dct_block(&block, false, y, false, x));
104        }
105    }
106
107    Dwt53TwoDimensional {
108        ll,
109        hl,
110        lh,
111        hh,
112        low_width,
113        low_height,
114        high_width,
115        high_height,
116    }
117}
118
119/// Map an adjacent 8x8 DCT block grid directly into a linearized one-level 2D
120/// 5/3 result for the logical component dimensions.
121///
122/// Padded JPEG edge samples outside `width x height` are ignored.
123pub fn dct8x8_blocks_to_dwt53_float_linear(
124    blocks: &[[[f64; 8]; 8]],
125    block_cols: usize,
126    block_rows: usize,
127    width: usize,
128    height: usize,
129) -> Result<Dwt53TwoDimensional<f64>, Dct53GridError> {
130    let mut scratch = Dct53GridScratch::default();
131    dct8x8_blocks_to_dwt53_float_linear_with_scratch(
132        blocks,
133        block_cols,
134        block_rows,
135        width,
136        height,
137        &mut scratch,
138    )
139}
140
141/// Map an adjacent 8x8 DCT block grid directly into a linearized one-level 2D
142/// 5/3 result using caller-owned scratch for reusable weight rows.
143pub fn dct8x8_blocks_to_dwt53_float_linear_with_scratch(
144    blocks: &[[[f64; 8]; 8]],
145    block_cols: usize,
146    block_rows: usize,
147    width: usize,
148    height: usize,
149    scratch: &mut Dct53GridScratch,
150) -> Result<Dwt53TwoDimensional<f64>, Dct53GridError> {
151    validate_grid(blocks.len(), block_cols, block_rows, width, height)?;
152
153    let low_width = low_len(width);
154    let low_height = low_len(height);
155    let high_width = high_len(width);
156    let high_height = high_len(height);
157    scratch.x_weights.ensure_sample_len(width);
158    scratch.y_weights.ensure_sample_len(height);
159    let x_weights = &scratch.x_weights;
160    let y_weights = &scratch.y_weights;
161
162    let mut ll = Vec::with_capacity(low_width * low_height);
163    let mut hl = Vec::with_capacity(high_width * low_height);
164    let mut lh = Vec::with_capacity(low_width * high_height);
165    let mut hh = Vec::with_capacity(high_width * high_height);
166
167    for y in 0..low_height {
168        for x in 0..low_width {
169            ll.push(project_dct_grid(
170                blocks,
171                block_cols,
172                &y_weights.low[y].taps,
173                &x_weights.low[x].taps,
174            ));
175        }
176        for x in 0..high_width {
177            hl.push(project_dct_grid(
178                blocks,
179                block_cols,
180                &y_weights.low[y].taps,
181                &x_weights.high[x].taps,
182            ));
183        }
184    }
185
186    for y in 0..high_height {
187        for x in 0..low_width {
188            lh.push(project_dct_grid(
189                blocks,
190                block_cols,
191                &y_weights.high[y].taps,
192                &x_weights.low[x].taps,
193            ));
194        }
195        for x in 0..high_width {
196            hh.push(project_dct_grid(
197                blocks,
198                block_cols,
199                &y_weights.high[y].taps,
200                &x_weights.high[x].taps,
201            ));
202        }
203    }
204
205    Ok(Dwt53TwoDimensional {
206        ll,
207        hl,
208        lh,
209        hh,
210        low_width,
211        low_height,
212        high_width,
213        high_height,
214    })
215}
216
217/// Reference path for the 2D experiment:
218/// DCT coefficients -> float IDCT samples -> separable linearized 5/3.
219#[must_use]
220pub fn idct8x8_then_dwt53_float(block: [[f64; 8]; 8]) -> Dwt53TwoDimensional<f64> {
221    let mut samples = Vec::with_capacity(64);
222    for y in 0..8 {
223        for x in 0..8 {
224            samples.push(idct8x8_sample(&block, x, y));
225        }
226    }
227
228    linearized_53_2d_from_plane(&samples, 8, 8)
229}
230
231/// Reference path for a DCT block grid:
232/// DCT coefficients -> float IDCT samples -> separable linearized 5/3.
233pub fn dct8x8_blocks_then_dwt53_float(
234    blocks: &[[[f64; 8]; 8]],
235    block_cols: usize,
236    block_rows: usize,
237    width: usize,
238    height: usize,
239) -> Result<Dwt53TwoDimensional<f64>, Dct53GridError> {
240    validate_grid(blocks.len(), block_cols, block_rows, width, height)?;
241
242    let mut samples = Vec::with_capacity(width * height);
243    for y in 0..height {
244        let block_y = y / 8;
245        let local_y = y % 8;
246        for x in 0..width {
247            let block_x = x / 8;
248            let local_x = x % 8;
249            let block = &blocks[block_y * block_cols + block_x];
250            samples.push(idct8x8_sample(block, local_x, local_y));
251        }
252    }
253
254    Ok(linearized_53_2d_from_plane(&samples, width, height))
255}
256
257fn project_dct_block(
258    block: &[[f64; 8]; 8],
259    vertical_low: bool,
260    output_y: usize,
261    horizontal_low: bool,
262    output_x: usize,
263) -> f64 {
264    let mut output = 0.0;
265
266    for sample_y in 0..8 {
267        let y_weight = linearized_53_sample_weight(8, vertical_low, output_y, sample_y);
268        if y_weight == 0.0 {
269            continue;
270        }
271
272        for sample_x in 0..8 {
273            let x_weight = linearized_53_sample_weight(8, horizontal_low, output_x, sample_x);
274            if x_weight == 0.0 {
275                continue;
276            }
277
278            let sample_weight = y_weight * x_weight;
279            for (freq_y, coefficient_row) in block.iter().enumerate() {
280                let y_basis = idct8_basis(sample_y, freq_y);
281                for (freq_x, coefficient) in coefficient_row.iter().copied().enumerate() {
282                    output += sample_weight * y_basis * idct8_basis(sample_x, freq_x) * coefficient;
283                }
284            }
285        }
286    }
287
288    output
289}
290
291fn project_dct_grid(
292    blocks: &[[[f64; 8]; 8]],
293    block_cols: usize,
294    y_weights: &[SparseWeightTap],
295    x_weights: &[SparseWeightTap],
296) -> f64 {
297    let mut output = 0.0;
298
299    for &SparseWeightTap {
300        sample_idx: sample_y,
301        weight: y_weight,
302    } in y_weights
303    {
304        let block_y = sample_y / 8;
305        let local_y = sample_y % 8;
306
307        for &SparseWeightTap {
308            sample_idx: sample_x,
309            weight: x_weight,
310        } in x_weights
311        {
312            let block_x = sample_x / 8;
313            let local_x = sample_x % 8;
314            let block = &blocks[block_y * block_cols + block_x];
315            let sample_weight = y_weight * x_weight;
316
317            for (freq_y, coefficient_row) in block.iter().enumerate() {
318                let y_basis = idct8_basis(local_y, freq_y);
319                for (freq_x, coefficient) in coefficient_row.iter().copied().enumerate() {
320                    output += sample_weight * y_basis * idct8_basis(local_x, freq_x) * coefficient;
321                }
322            }
323        }
324    }
325
326    output
327}
328
329fn idct8x8_sample(block: &[[f64; 8]; 8], x: usize, y: usize) -> f64 {
330    let mut sample = 0.0;
331    for (freq_y, row) in block.iter().enumerate() {
332        let y_basis = idct8_basis(y, freq_y);
333        for (freq_x, coefficient) in row.iter().copied().enumerate() {
334            sample += coefficient * y_basis * idct8_basis(x, freq_x);
335        }
336    }
337    sample
338}
339
340pub(crate) fn linearized_53_2d_from_plane(
341    samples: &[f64],
342    width: usize,
343    height: usize,
344) -> Dwt53TwoDimensional<f64> {
345    debug_assert_eq!(samples.len(), width * height);
346
347    let low_width = low_len(width);
348    let low_height = low_len(height);
349    let high_width = high_len(width);
350    let high_height = high_len(height);
351
352    let mut row_low = Vec::with_capacity(height * low_width);
353    let mut row_high = Vec::with_capacity(height * high_width);
354    for y in 0..height {
355        let start = y * width;
356        let row = &samples[start..start + width];
357        let transformed = linearized_53_from_sample_slice(row);
358        row_low.extend(transformed.low);
359        row_high.extend(transformed.high);
360    }
361
362    let mut ll = Vec::with_capacity(low_width * low_height);
363    let mut lh = Vec::with_capacity(low_width * high_height);
364    for x in 0..low_width {
365        let column = column_from_rows(&row_low, low_width, x, height);
366        let transformed = linearized_53_from_sample_slice(&column);
367        ll.extend(transformed.low);
368        lh.extend(transformed.high);
369    }
370
371    let mut hl = Vec::with_capacity(high_width * low_height);
372    let mut hh = Vec::with_capacity(high_width * high_height);
373    for x in 0..high_width {
374        let column = column_from_rows(&row_high, high_width, x, height);
375        let transformed = linearized_53_from_sample_slice(&column);
376        hl.extend(transformed.low);
377        hh.extend(transformed.high);
378    }
379
380    Dwt53TwoDimensional {
381        ll: transpose_band(&ll, low_height, low_width),
382        hl: transpose_band(&hl, low_height, high_width),
383        lh: transpose_band(&lh, high_height, low_width),
384        hh: transpose_band(&hh, high_height, high_width),
385        low_width,
386        low_height,
387        high_width,
388        high_height,
389    }
390}
391
392fn column_from_rows(rows: &[f64], stride: usize, x: usize, height: usize) -> Vec<f64> {
393    (0..height).map(|y| rows[y * stride + x]).collect()
394}
395
396fn transpose_band(column_major: &[f64], height: usize, width: usize) -> Vec<f64> {
397    let mut row_major = Vec::with_capacity(width * height);
398    for y in 0..height {
399        for x in 0..width {
400            row_major.push(column_major[x * height + y]);
401        }
402    }
403    row_major
404}
405
406fn linearized_53_sample_weight(
407    sample_len: usize,
408    is_low: bool,
409    output_idx: usize,
410    sample_idx: usize,
411) -> f64 {
412    let mut basis = vec![0.0; sample_len];
413    basis[sample_idx] = 1.0;
414    let row = linearized_53_from_sample_slice(&basis);
415    if is_low {
416        row.low[output_idx]
417    } else {
418        row.high[output_idx]
419    }
420}
421
422fn linearized_53_from_sample_slice(samples: &[f64]) -> Dwt53OneDimensional {
423    let mut high = Vec::with_capacity(high_len(samples.len()));
424    for odd_idx in (1..samples.len()).step_by(2) {
425        let left = samples[odd_idx - 1];
426        let right = samples.get(odd_idx + 1).copied().unwrap_or(left);
427        high.push(samples[odd_idx] - ((left + right) * 0.5));
428    }
429
430    let mut low = Vec::with_capacity(low_len(samples.len()));
431    for even_idx in (0..samples.len()).step_by(2) {
432        let current = samples[even_idx];
433        let even_output_idx = even_idx / 2;
434        let left_high = even_output_idx.checked_sub(1).and_then(|idx| high.get(idx));
435        let right_high = high.get(even_output_idx);
436        let update = match (left_high, right_high) {
437            (Some(left), Some(right)) => (*left + *right) * 0.25,
438            (None, Some(right)) => *right * 0.5,
439            (Some(left), None) => *left * 0.5,
440            (None, None) => 0.0,
441        };
442        low.push(current + update);
443    }
444
445    Dwt53OneDimensional { low, high }
446}
447
448fn validate_grid(
449    block_count: usize,
450    block_cols: usize,
451    block_rows: usize,
452    width: usize,
453    height: usize,
454) -> Result<(), Dct53GridError> {
455    validate_dct_block_grid(block_count, block_cols, block_rows, width, height)
456}
457
458#[derive(Debug, Default)]
459struct Dwt53WeightRows {
460    sample_len: Option<usize>,
461    low: Vec<SparseWeightRow>,
462    high: Vec<SparseWeightRow>,
463}
464
465impl Dwt53WeightRows {
466    fn ensure_sample_len(&mut self, sample_len: usize) {
467        if self.sample_len == Some(sample_len) {
468            return;
469        }
470
471        resize_weight_rows(&mut self.low, low_len(sample_len), 5);
472        resize_weight_rows(&mut self.high, high_len(sample_len), 3);
473
474        for sample_idx in 0..sample_len {
475            let mut basis = vec![0.0; sample_len];
476            basis[sample_idx] = 1.0;
477            let transformed = linearized_53_from_sample_slice(&basis);
478            for (row, &weight) in self.low.iter_mut().zip(transformed.low.iter()) {
479                if weight != 0.0 {
480                    row.taps.push(SparseWeightTap { sample_idx, weight });
481                }
482            }
483            for (row, &weight) in self.high.iter_mut().zip(transformed.high.iter()) {
484                if weight != 0.0 {
485                    row.taps.push(SparseWeightTap { sample_idx, weight });
486                }
487            }
488        }
489
490        self.sample_len = Some(sample_len);
491    }
492
493    fn weight_capacity(&self) -> usize {
494        self.low
495            .iter()
496            .map(|row| row.taps.capacity())
497            .sum::<usize>()
498            + self
499                .high
500                .iter()
501                .map(|row| row.taps.capacity())
502                .sum::<usize>()
503    }
504}
505
506fn resize_weight_rows(rows: &mut Vec<SparseWeightRow>, row_count: usize, max_taps: usize) {
507    if rows.len() < row_count {
508        rows.resize_with(row_count, SparseWeightRow::default);
509    }
510    for row in rows.iter_mut().take(row_count) {
511        row.taps.clear();
512        if row.taps.capacity() < max_taps {
513            row.taps.reserve_exact(max_taps - row.taps.capacity());
514        }
515    }
516    rows.truncate(row_count);
517}
518
519#[derive(Debug, Default)]
520struct SparseWeightRow {
521    taps: Vec<SparseWeightTap>,
522}
523
524#[derive(Debug, Clone, Copy)]
525struct SparseWeightTap {
526    sample_idx: usize,
527    weight: f64,
528}
529
530struct Dwt53OneDimensional {
531    low: Vec<f64>,
532    high: Vec<f64>,
533}