Skip to main content

cram_codec/
wavelet.rs

1//! Separable multi-level 3D wavelet transforms by lifting.
2//!
3//! - **CDF 9/7** (f64 lifting, the JPEG 2000 irreversible filter): the lossy
4//!   path. Float, but plain IEEE `+`/`*` in a fixed order with no fused ops,
5//!   so the CPU decoder is deterministic.
6//! - **LeGall 5/3** (integer lifting, the JPEG 2000 reversible filter): the
7//!   lossless path, exact on `i32`.
8//!
9//! Both use whole-sample symmetric extension and work on any length ≥ 1 (odd
10//! lengths keep the extra sample in the low band). The pyramid is dyadic on the
11//! LL region; at each level every axis whose current length is ≥ [`MIN_LEN`]
12//! is split, so a 64×128×128 block gets 4 levels on its short axis and 5 on
13//! the others (anisotropic bands where axes stop early). Arrays are always
14//! handled as 3D, with leading unit axes for 1D/2D input.
15
16/// An axis is split at a level only while its LL length is at least this.
17pub const MIN_LEN: usize = 8;
18/// Level cap for 2D/3D arrays.
19pub const MAX_LEVELS: usize = 6;
20/// Level cap for 1D signals (and the size of the basis-norm table).
21pub const MAX_LEVELS_1D: usize = 13;
22
23const ALPHA: f64 = -1.586_134_342_059_924;
24const BETA: f64 = -0.052_980_118_572_961;
25const GAMMA: f64 = 0.882_911_075_530_934;
26const DELTA: f64 = 0.443_506_852_043_971;
27const KAPPA: f64 = 1.149_604_398_860_241;
28
29/// A subband: level `level` (0 = finest), orientation `mask` (bit `a` set =
30/// highpass along axis `a`; `mask == 0` is the final LL), half-open region.
31#[derive(Clone, Debug, PartialEq, Eq)]
32pub struct Band {
33    /// Decomposition level (0 = finest detail); the LL band has `level = nlev`.
34    pub level: usize,
35    /// Highpass axes bitmask.
36    pub mask: u8,
37    /// Region start per axis.
38    pub start: [usize; 3],
39    /// Region end per axis (exclusive).
40    pub end: [usize; 3],
41}
42
43impl Band {
44    /// Number of coefficients.
45    pub fn len(&self) -> usize {
46        (0..3).map(|a| self.end[a] - self.start[a]).product()
47    }
48    /// Whether the band is empty.
49    pub fn is_empty(&self) -> bool {
50        self.len() == 0
51    }
52}
53
54/// The dyadic pyramid for a shape.
55#[derive(Clone, Debug)]
56pub struct Pyramid {
57    /// Shape (3D, row-major, axis 2 fastest).
58    pub dims: [usize; 3],
59    /// `lens[l]` = LL extent entering level `l` (`lens[0] = dims`).
60    pub lens: Vec<[usize; 3]>,
61    /// `split[l][a]`: axis `a` is transformed at level `l`.
62    pub split: Vec<[bool; 3]>,
63    /// Bands in coding order: LL first, then detail bands coarse → fine.
64    pub bands: Vec<Band>,
65}
66
67/// Pad an n-D shape (n ≤ 3) to 3D with leading unit axes.
68pub fn as3(dims: &[usize]) -> [usize; 3] {
69    assert!(!dims.is_empty() && dims.len() <= 3, "1D–3D arrays only");
70    let mut d = [1; 3];
71    d[3 - dims.len()..].copy_from_slice(dims);
72    d
73}
74
75impl Pyramid {
76    /// Build the pyramid for `dims` with at most `max_levels` levels.
77    pub fn new(dims: [usize; 3], max_levels: usize) -> Self {
78        let mut lens = vec![dims];
79        let mut split = Vec::new();
80        while split.len() < max_levels {
81            let cur = *lens.last().unwrap();
82            let s = [0, 1, 2].map(|a| cur[a] >= MIN_LEN);
83            if !s.iter().any(|&b| b) {
84                break;
85            }
86            split.push(s);
87            lens.push([0, 1, 2].map(|a| if s[a] { cur[a].div_ceil(2) } else { cur[a] }));
88        }
89        let nlev = split.len();
90        let mut bands = vec![Band {
91            level: nlev,
92            mask: 0,
93            start: [0; 3],
94            end: lens[nlev],
95        }];
96        for l in (0..nlev).rev() {
97            for mask in 1u8..8 {
98                if (0..3).any(|a| mask >> a & 1 == 1 && !split[l][a]) {
99                    continue;
100                }
101                let mut start = [0; 3];
102                let mut end = [0; 3];
103                for a in 0..3 {
104                    if mask >> a & 1 == 1 {
105                        start[a] = lens[l + 1][a];
106                        end[a] = lens[l][a];
107                    } else {
108                        end[a] = lens[l + 1][a];
109                    }
110                }
111                let b = Band {
112                    level: l,
113                    mask,
114                    start,
115                    end,
116                };
117                if !b.is_empty() {
118                    bands.push(b);
119                }
120            }
121        }
122        Self {
123            dims,
124            lens,
125            split,
126            bands,
127        }
128    }
129
130    /// Number of levels.
131    pub fn nlev(&self) -> usize {
132        self.split.len()
133    }
134
135    /// Total cells.
136    pub fn len(&self) -> usize {
137        self.dims.iter().product()
138    }
139
140    /// Whether the shape is empty.
141    pub fn is_empty(&self) -> bool {
142        self.len() == 0
143    }
144
145    /// How many times axis `a` is split in levels `0..l`.
146    fn splits_before(&self, a: usize, l: usize) -> usize {
147        self.split[..l].iter().filter(|s| s[a]).count()
148    }
149
150    /// L2 norm of the CDF 9/7 synthesis basis function of a coefficient in
151    /// `band` (separable product of 1D norms). Scaling coefficients by it makes
152    /// one uniform step equal-slope (MSE-matched) across bands.
153    pub fn band_weight(&self, band: &Band) -> f64 {
154        let (lo, hi) = norms_97();
155        (0..3)
156            .map(|a| {
157                let l = band.level;
158                if l < self.nlev() && self.split[l][a] {
159                    let k = self.splits_before(a, l) + 1;
160                    if band.mask >> a & 1 == 1 {
161                        hi[k]
162                    } else {
163                        lo[k]
164                    }
165                } else {
166                    lo[self.splits_before(a, l.min(self.nlev()))]
167                }
168            })
169            .product()
170    }
171
172    /// The coarser-level coefficient ("parent") of position `p` in detail band
173    /// `bi`: same orientation one level up, or `None` if that band does not
174    /// exist (coarsest detail level, or the axis set stops splitting).
175    pub fn parent_band(&self, bi: usize) -> Option<usize> {
176        let b = &self.bands[bi];
177        if b.mask == 0 || b.level + 1 >= self.nlev() {
178            return None;
179        }
180        self.bands
181            .iter()
182            .position(|c| c.level == b.level + 1 && c.mask == b.mask)
183    }
184
185    /// Map a position in band `b` to its parent position in band `pb`.
186    #[inline]
187    pub fn parent_pos(&self, b: &Band, pb: &Band, p: [usize; 3]) -> [usize; 3] {
188        let mut q = [0; 3];
189        for a in 0..3 {
190            let rel = p[a] - b.start[a];
191            let r = if self.split[pb.level][a] {
192                rel / 2
193            } else {
194                rel
195            };
196            q[a] = (pb.start[a] + r).min(pb.end[a] - 1);
197        }
198        q
199    }
200}
201
202fn lines(dims: [usize; 3], ext: [usize; 3], axis: usize, mut f: impl FnMut(usize, usize)) {
203    // f(offset of first element, stride) for every line along `axis` inside `ext`
204    let stride = [dims[1] * dims[2], dims[2], 1];
205    let others: Vec<usize> = (0..3).filter(|&a| a != axis).collect();
206    for i in 0..ext[others[0]] {
207        for j in 0..ext[others[1]] {
208            f(i * stride[others[0]] + j * stride[others[1]], stride[axis]);
209        }
210    }
211}
212
213// ---------------------------------------------------------------- CDF 9/7
214
215#[inline]
216fn lift_d(s: &[f64], d: &mut [f64], c: f64) {
217    // d_i += c (s_i + s_{i+1}), s_{ns} mirrors to s_{ns-1}
218    let ns = s.len();
219    for i in 0..d.len() {
220        let r = if i + 1 < ns { s[i + 1] } else { s[ns - 1] };
221        d[i] += c * (s[i] + r);
222    }
223}
224
225#[inline]
226fn lift_s(s: &mut [f64], d: &[f64], c: f64) {
227    // s_i += c (d_{i-1} + d_i), d_{-1} = d_0, d_{nd} = d_{nd-1}
228    let nd = d.len();
229    for i in 0..s.len() {
230        let l = if i == 0 { d[0] } else { d[i - 1] };
231        let r = if i < nd { d[i] } else { d[nd - 1] };
232        s[i] += c * (l + r);
233    }
234}
235
236/// One level of forward CDF 9/7 on `x` (in place: low half first).
237pub fn fwd97(x: &mut [f64], tmp: &mut Vec<f64>) {
238    let n = x.len();
239    if n < 2 {
240        return;
241    }
242    let ns = n.div_ceil(2);
243    tmp.clear();
244    tmp.extend(x.iter().step_by(2));
245    tmp.extend(x.iter().skip(1).step_by(2));
246    let (s, d) = tmp.split_at_mut(ns);
247    lift_d(s, d, ALPHA);
248    lift_s(s, d, BETA);
249    lift_d(s, d, GAMMA);
250    lift_s(s, d, DELTA);
251    for v in s.iter_mut() {
252        *v *= KAPPA;
253    }
254    for v in d.iter_mut() {
255        *v /= KAPPA;
256    }
257    x.copy_from_slice(tmp);
258}
259
260/// Inverse of [`fwd97`].
261pub fn inv97(x: &mut [f64], tmp: &mut Vec<f64>) {
262    let n = x.len();
263    if n < 2 {
264        return;
265    }
266    let ns = n.div_ceil(2);
267    tmp.clear();
268    tmp.extend_from_slice(x);
269    let (s, d) = tmp.split_at_mut(ns);
270    for v in s.iter_mut() {
271        *v /= KAPPA;
272    }
273    for v in d.iter_mut() {
274        *v *= KAPPA;
275    }
276    lift_s(s, d, -DELTA);
277    lift_d(s, d, -GAMMA);
278    lift_s(s, d, -BETA);
279    lift_d(s, d, -ALPHA);
280    for i in 0..ns {
281        x[2 * i] = s[i];
282    }
283    for i in 0..n / 2 {
284        x[2 * i + 1] = d[i];
285    }
286}
287
288/// 1D synthesis-basis norms `(lo[k], hi[k])` after `k` splits (`lo[0] = 1`).
289pub fn norms_97() -> (&'static [f64], &'static [f64]) {
290    use std::sync::OnceLock;
291    static N: OnceLock<(Vec<f64>, Vec<f64>)> = OnceLock::new();
292    let n = N.get_or_init(|| {
293        let len = 1 << 16;
294        let mut lo = vec![1.0];
295        let mut hi = vec![0.0];
296        let mut tmp = Vec::new();
297        for k in 1..=MAX_LEVELS_1D {
298            // lengths of the LL region per split
299            let mut ls: Vec<usize> = vec![len];
300            for _ in 0..k {
301                ls.push(ls.last().unwrap().div_ceil(2usize));
302            }
303            let norm = |pos: usize, tmp: &mut Vec<f64>| {
304                let mut x = vec![0.0; len];
305                x[pos] = 1.0;
306                for j in (0..k).rev() {
307                    inv97(&mut x[..ls[j]], tmp);
308                }
309                x.iter().map(|v| v * v).sum::<f64>().sqrt()
310            };
311            lo.push(norm(ls[k] / 2, &mut tmp));
312            hi.push(norm(ls[k] + (ls[k - 1] - ls[k]) / 2, &mut tmp));
313        }
314        (lo, hi)
315    });
316    (&n.0, &n.1)
317}
318
319fn apply3<T: Copy + Default>(
320    p: &Pyramid,
321    x: &mut [T],
322    inverse: bool,
323    mut f: impl FnMut(&mut [T], &mut Vec<T>, bool),
324) {
325    let mut line = Vec::new();
326    let mut tmp = Vec::new();
327    let levels: Vec<usize> = if inverse {
328        (0..p.nlev()).rev().collect()
329    } else {
330        (0..p.nlev()).collect()
331    };
332    for l in levels {
333        let axes: Vec<usize> = if inverse {
334            vec![2, 1, 0]
335        } else {
336            vec![0, 1, 2]
337        };
338        for a in axes {
339            if !p.split[l][a] {
340                continue;
341            }
342            let ext = p.lens[l];
343            let n = ext[a];
344            lines(p.dims, ext, a, |off, st| {
345                line.clear();
346                line.extend((0..n).map(|i| x[off + i * st]));
347                f(&mut line, &mut tmp, inverse);
348                for i in 0..n {
349                    x[off + i * st] = line[i];
350                }
351            });
352        }
353    }
354}
355
356/// Forward multi-level CDF 9/7 in place.
357pub fn forward_97(p: &Pyramid, x: &mut [f64]) {
358    apply3(p, x, false, |l, t, _| fwd97(l, t));
359}
360
361/// Inverse multi-level CDF 9/7 in place.
362pub fn inverse_97(p: &Pyramid, x: &mut [f64]) {
363    apply3(p, x, true, |l, t, _| inv97(l, t));
364}
365
366// ---------------------------------------------------------------- LeGall 5/3 (integer)
367
368/// One level of forward integer 5/3 (reversible) on `x`.
369pub fn fwd53(x: &mut [i64], tmp: &mut Vec<i64>) {
370    let n = x.len();
371    if n < 2 {
372        return;
373    }
374    let ns = n.div_ceil(2);
375    tmp.clear();
376    tmp.extend(x.iter().step_by(2));
377    tmp.extend(x.iter().skip(1).step_by(2));
378    let (s, d) = tmp.split_at_mut(ns);
379    for i in 0..d.len() {
380        let r = if i + 1 < ns { s[i + 1] } else { s[ns - 1] };
381        d[i] -= (s[i] + r).div_euclid(2);
382    }
383    let nd = d.len();
384    for i in 0..ns {
385        let l = if i == 0 { d[0] } else { d[i - 1] };
386        let r = if i < nd { d[i] } else { d[nd - 1] };
387        s[i] += (l + r + 2).div_euclid(4);
388    }
389    x.copy_from_slice(tmp);
390}
391
392/// Inverse of [`fwd53`].
393pub fn inv53(x: &mut [i64], tmp: &mut Vec<i64>) {
394    let n = x.len();
395    if n < 2 {
396        return;
397    }
398    let ns = n.div_ceil(2);
399    tmp.clear();
400    tmp.extend_from_slice(x);
401    let (s, d) = tmp.split_at_mut(ns);
402    let nd = d.len();
403    for i in 0..ns {
404        let l = if i == 0 { d[0] } else { d[i - 1] };
405        let r = if i < nd { d[i] } else { d[nd - 1] };
406        s[i] -= (l + r + 2).div_euclid(4);
407    }
408    for i in 0..nd {
409        let r = if i + 1 < ns { s[i + 1] } else { s[ns - 1] };
410        d[i] += (s[i] + r).div_euclid(2);
411    }
412    for i in 0..ns {
413        x[2 * i] = s[i];
414    }
415    for i in 0..nd {
416        x[2 * i + 1] = d[i];
417    }
418}
419
420/// Forward multi-level integer 5/3 in place.
421pub fn forward_53(p: &Pyramid, x: &mut [i64]) {
422    apply3(p, x, false, |l, t, _| fwd53(l, t));
423}
424
425/// Inverse multi-level integer 5/3 in place.
426pub fn inverse_53(p: &Pyramid, x: &mut [i64]) {
427    apply3(p, x, true, |l, t, _| inv53(l, t));
428}
429
430#[cfg(test)]
431mod tests {
432    use super::*;
433
434    fn field(d: [usize; 3]) -> Vec<f64> {
435        let mut v = Vec::new();
436        for i in 0..d[0] {
437            for j in 0..d[1] {
438                for k in 0..d[2] {
439                    let (x, y, z) = (i as f64, j as f64, k as f64);
440                    v.push(
441                        (0.3 * x).sin() + (0.11 * y * z).cos() + 0.01 * x * y
442                            - (k % 7) as f64 * 0.1,
443                    );
444                }
445            }
446        }
447        v
448    }
449
450    #[test]
451    fn perfect_reconstruction_97_any_size() {
452        for d in [
453            [1, 1, 1],
454            [1, 1, 2],
455            [1, 1, 37],
456            [3, 17, 9],
457            [13, 8, 31],
458            [16, 16, 16],
459            [1, 50, 23],
460        ] {
461            let p = Pyramid::new(d, MAX_LEVELS);
462            let x = field(d);
463            let mut y = x.clone();
464            forward_97(&p, &mut y);
465            inverse_97(&p, &mut y);
466            let e = x
467                .iter()
468                .zip(&y)
469                .map(|(a, b)| (a - b).abs())
470                .fold(0.0, f64::max);
471            assert!(e < 1e-11, "{d:?}: {e}");
472            let cells: usize = p.bands.iter().map(|b| b.len()).sum();
473            assert_eq!(cells, p.len(), "bands tile the array {d:?}");
474        }
475    }
476
477    #[test]
478    fn lossless_53_any_size() {
479        for d in [[1, 1, 5], [7, 9, 11], [16, 32, 8], [2, 1, 64]] {
480            let p = Pyramid::new(d, MAX_LEVELS);
481            let x: Vec<i64> = field(d)
482                .iter()
483                .map(|v| (v * 1000.0) as i64 ^ 0x5a)
484                .collect();
485            let mut y = x.clone();
486            forward_53(&p, &mut y);
487            if p.nlev() > 0 {
488                assert_ne!(x, y);
489            }
490            inverse_53(&p, &mut y);
491            assert_eq!(x, y);
492        }
493    }
494
495    #[test]
496    fn weights_near_orthonormal() {
497        let (lo, hi) = norms_97();
498        // CDF 9/7 with this scaling is close to orthonormal
499        for k in 1..=MAX_LEVELS {
500            assert!(
501                (lo[k] - 1.0).abs() < 0.1 && (hi[k] - 1.0).abs() < 0.15,
502                "{k}: {} {}",
503                lo[k],
504                hi[k]
505            );
506        }
507    }
508
509    #[test]
510    fn parent_positions_in_band() {
511        let p = Pyramid::new([64, 128, 128], MAX_LEVELS);
512        assert_eq!(p.nlev(), 5);
513        for (bi, b) in p.bands.iter().enumerate() {
514            if let Some(pi) = p.parent_band(bi) {
515                let pb = &p.bands[pi];
516                let q = p.parent_pos(b, pb, [b.end[0] - 1, b.end[1] - 1, b.end[2] - 1]);
517                for a in 0..3 {
518                    assert!(q[a] >= pb.start[a] && q[a] < pb.end[a]);
519                }
520            }
521        }
522    }
523}