torchwavelets 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- torchwavelets/__init__.py +62 -0
- torchwavelets/_version.py +1 -0
- torchwavelets/compat.py +136 -0
- torchwavelets/core/__init__.py +13 -0
- torchwavelets/core/filters.py +211 -0
- torchwavelets/core/modes.py +109 -0
- torchwavelets/dtcwt/__init__.py +24 -0
- torchwavelets/dtcwt/_autocast.py +24 -0
- torchwavelets/dtcwt/coeffs.py +76 -0
- torchwavelets/dtcwt/data/__init__.py +5 -0
- torchwavelets/dtcwt/data/antonini.npz +0 -0
- torchwavelets/dtcwt/data/farras.npz +0 -0
- torchwavelets/dtcwt/data/legall.npz +0 -0
- torchwavelets/dtcwt/data/near_sym_a.npz +0 -0
- torchwavelets/dtcwt/data/near_sym_a2.npz +0 -0
- torchwavelets/dtcwt/data/near_sym_b.npz +0 -0
- torchwavelets/dtcwt/data/near_sym_b_bp.npz +0 -0
- torchwavelets/dtcwt/data/qshift_06.npz +0 -0
- torchwavelets/dtcwt/data/qshift_32.npz +0 -0
- torchwavelets/dtcwt/data/qshift_a.npz +0 -0
- torchwavelets/dtcwt/data/qshift_b.npz +0 -0
- torchwavelets/dtcwt/data/qshift_b_bp.npz +0 -0
- torchwavelets/dtcwt/data/qshift_c.npz +0 -0
- torchwavelets/dtcwt/data/qshift_d.npz +0 -0
- torchwavelets/dtcwt/lowlevel.py +296 -0
- torchwavelets/dtcwt/transform1d.py +253 -0
- torchwavelets/dtcwt/transform2d.py +298 -0
- torchwavelets/dtcwt/transform3d.py +420 -0
- torchwavelets/dtcwt/transform_funcs.py +93 -0
- torchwavelets/dwt/__init__.py +6 -0
- torchwavelets/dwt/_validation.py +59 -0
- torchwavelets/dwt/lowlevel.py +153 -0
- torchwavelets/dwt/transform1d.py +175 -0
- torchwavelets/dwt/transform2d.py +195 -0
- torchwavelets/packets.py +439 -0
- torchwavelets/py.typed +0 -0
- torchwavelets/scatter/__init__.py +6 -0
- torchwavelets/scatter/lowlevel.py +221 -0
- torchwavelets/scatter/network.py +204 -0
- torchwavelets/swt/__init__.py +14 -0
- torchwavelets/swt/lowlevel.py +53 -0
- torchwavelets/swt/transform.py +386 -0
- torchwavelets-0.1.0.dist-info/METADATA +303 -0
- torchwavelets-0.1.0.dist-info/RECORD +47 -0
- torchwavelets-0.1.0.dist-info/WHEEL +5 -0
- torchwavelets-0.1.0.dist-info/licenses/LICENSE +29 -0
- torchwavelets-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Low-level 1-D analysis/synthesis filter banks operating on 4-D tensors.
|
|
2
|
+
|
|
3
|
+
All functions operate on ``(N, C, H, W)`` tensors and filter along one spatial
|
|
4
|
+
axis (``dim=2`` for columns, ``dim=3`` for rows). Filters must be 4-D tensors
|
|
5
|
+
of shape ``(1, 1, L, 1)`` (column) or ``(1, 1, 1, L)`` (row); analysis filters
|
|
6
|
+
must already be reversed (see :mod:`torchwavelets.core.filters`).
|
|
7
|
+
|
|
8
|
+
Everything is built from plain torch ops (``conv2d`` / ``conv_transpose2d`` /
|
|
9
|
+
index-based padding), so gradients flow through all dtypes, autocast works
|
|
10
|
+
natively, and no custom ``autograd.Function`` is required.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import torch
|
|
16
|
+
import torch.nn.functional as F
|
|
17
|
+
|
|
18
|
+
from ..core.modes import pad_indices, padding_sizes
|
|
19
|
+
|
|
20
|
+
__all__ = ["afb1d", "sfb1d"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _dim_slice(dim: int, sl) -> tuple[slice, ...]:
|
|
24
|
+
s = [slice(None)] * 4
|
|
25
|
+
s[dim] = sl
|
|
26
|
+
return tuple(s)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _cat_last(x: torch.Tensor, dim: int) -> torch.Tensor:
|
|
30
|
+
"""Append a copy of the last sample along ``dim`` (used for odd lengths)."""
|
|
31
|
+
idx = torch.tensor([x.shape[dim] - 1], device=x.device)
|
|
32
|
+
return torch.cat((x, x.index_select(dim, idx)), dim=dim)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def afb1d(
|
|
36
|
+
x: torch.Tensor,
|
|
37
|
+
h0: torch.Tensor,
|
|
38
|
+
h1: torch.Tensor,
|
|
39
|
+
mode: str,
|
|
40
|
+
dim: int,
|
|
41
|
+
) -> torch.Tensor:
|
|
42
|
+
"""1-D analysis filter bank along ``dim`` with decimation by 2.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
x: 4-D input ``(N, C, H, W)``.
|
|
46
|
+
h0, h1: 4-D analysis filters (reversed), shape ``(1, 1, L, 1)`` if
|
|
47
|
+
``dim == 2`` else ``(1, 1, 1, L)``.
|
|
48
|
+
mode: boundary mode (see :mod:`torchwavelets.core.modes`).
|
|
49
|
+
dim: spatial dimension to filter along (2 or 3).
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
``(N, 2C, ...)`` tensor with the lowpass/highpass outputs interleaved
|
|
53
|
+
along the channel dimension as ``[lo_0, hi_0, lo_1, hi_1, ...]``.
|
|
54
|
+
"""
|
|
55
|
+
C = x.shape[1]
|
|
56
|
+
L = h0.numel()
|
|
57
|
+
stride = (2, 1) if dim == 2 else (1, 2)
|
|
58
|
+
h = torch.cat([h0, h1] * C, dim=0)
|
|
59
|
+
|
|
60
|
+
if mode == "periodization":
|
|
61
|
+
# Circular transform with the *reversed* analysis filter (conv2d is a
|
|
62
|
+
# correlation): c[k] = sum_m h[m] * x[(2k + L/2 - m) mod n], which is
|
|
63
|
+
# PyWavelets' periodization convention (bit-exact vs pywt.dwt).
|
|
64
|
+
n = x.shape[dim]
|
|
65
|
+
if n % 2 == 1:
|
|
66
|
+
x = _cat_last(x, dim)
|
|
67
|
+
n += 1
|
|
68
|
+
shift = L // 2 - 1
|
|
69
|
+
idx = (torch.arange(n + L - 2, device=x.device) - shift) % n
|
|
70
|
+
xp = x.index_select(dim, idx)
|
|
71
|
+
return F.conv2d(xp, h, stride=stride, groups=C)
|
|
72
|
+
|
|
73
|
+
n = x.shape[dim]
|
|
74
|
+
before, after = padding_sizes(n, L, mode)
|
|
75
|
+
if mode == "zero":
|
|
76
|
+
shape = list(x.shape)
|
|
77
|
+
shape[dim] = n + before + after
|
|
78
|
+
padded = torch.zeros(shape, dtype=x.dtype, device=x.device)
|
|
79
|
+
padded[_dim_slice(dim, slice(before, before + n))] = x
|
|
80
|
+
elif mode == "smooth":
|
|
81
|
+
# PyWavelets 'smooth': linear extrapolation from the edges,
|
|
82
|
+
# left pad (k = before..1): x0 + k*(x0 - x1), right pad (k = 1..after):
|
|
83
|
+
# x[-1] + k*(x[-1] - x[-2]).
|
|
84
|
+
x0 = x[_dim_slice(dim, slice(0, 1))]
|
|
85
|
+
x1 = x[_dim_slice(dim, slice(1, 2))] if n >= 2 else torch.zeros_like(x0)
|
|
86
|
+
xm1 = x[_dim_slice(dim, slice(-1, None))]
|
|
87
|
+
xm2 = x[_dim_slice(dim, slice(-2, -1))] if n >= 2 else torch.zeros_like(xm1)
|
|
88
|
+
kshape = [1, 1, 1, 1]
|
|
89
|
+
kshape[dim] = -1
|
|
90
|
+
k_left = torch.arange(before, 0, -1, dtype=x.dtype, device=x.device).view(kshape)
|
|
91
|
+
k_right = torch.arange(1, after + 1, dtype=x.dtype, device=x.device).view(kshape)
|
|
92
|
+
left = x0 + k_left * (x0 - x1)
|
|
93
|
+
right = xm1 + k_right * (xm1 - xm2)
|
|
94
|
+
padded = torch.cat([left, x, right], dim=dim)
|
|
95
|
+
else:
|
|
96
|
+
idx = pad_indices(n, before, after, mode, x.device)
|
|
97
|
+
padded = x.index_select(dim, idx)
|
|
98
|
+
return F.conv2d(padded, h, stride=stride, groups=C)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def sfb1d(
|
|
102
|
+
lo: torch.Tensor,
|
|
103
|
+
hi: torch.Tensor,
|
|
104
|
+
g0: torch.Tensor,
|
|
105
|
+
g1: torch.Tensor,
|
|
106
|
+
mode: str,
|
|
107
|
+
dim: int,
|
|
108
|
+
) -> torch.Tensor:
|
|
109
|
+
"""1-D synthesis filter bank along ``dim`` with upsampling by 2.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
lo, hi: 4-D lowpass/highpass coefficients ``(N, C, ...)``.
|
|
113
|
+
g0, g1: 4-D synthesis filters (natural order), shape ``(1, 1, L, 1)``
|
|
114
|
+
if ``dim == 2`` else ``(1, 1, 1, L)``.
|
|
115
|
+
mode: boundary mode.
|
|
116
|
+
dim: spatial dimension (2 or 3).
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Reconstructed ``(N, C, ...)`` tensor.
|
|
120
|
+
"""
|
|
121
|
+
C = lo.shape[1]
|
|
122
|
+
L = g0.numel()
|
|
123
|
+
stride = (2, 1) if dim == 2 else (1, 2)
|
|
124
|
+
g0 = torch.cat([g0] * C, dim=0)
|
|
125
|
+
g1 = torch.cat([g1] * C, dim=0)
|
|
126
|
+
|
|
127
|
+
if mode == "periodization":
|
|
128
|
+
# Circular synthesis: y[m] = sum_j g[j] * u[(m + j - L/2) mod 2n] with
|
|
129
|
+
# u the upsampled (zero-inserted) coefficient signal. g is flipped
|
|
130
|
+
# because conv2d is a correlation (PyWavelets' periodization, exact).
|
|
131
|
+
n_in = lo.shape[dim]
|
|
132
|
+
n_out = 2 * n_in
|
|
133
|
+
u_lo = torch.zeros(
|
|
134
|
+
*lo.shape[:dim], n_out, *lo.shape[dim + 1 :], dtype=lo.dtype, device=lo.device
|
|
135
|
+
)
|
|
136
|
+
u_hi = torch.zeros_like(u_lo)
|
|
137
|
+
u_lo[_dim_slice(dim, slice(None, None, 2))] = lo
|
|
138
|
+
u_hi[_dim_slice(dim, slice(None, None, 2))] = hi
|
|
139
|
+
idx = (torch.arange(n_out + L - 1, device=lo.device) - L // 2) % n_out
|
|
140
|
+
# Flip along the filter-tap dimension: conv2d is a correlation.
|
|
141
|
+
# g0/g1 are already tiled to C channels above.
|
|
142
|
+
flip = -2 if dim == 2 else -1
|
|
143
|
+
g0 = g0.flip(flip)
|
|
144
|
+
g1 = g1.flip(flip)
|
|
145
|
+
y = F.conv2d(u_lo.index_select(dim, idx), g0, groups=C) + F.conv2d(
|
|
146
|
+
u_hi.index_select(dim, idx), g1, groups=C
|
|
147
|
+
)
|
|
148
|
+
return y
|
|
149
|
+
|
|
150
|
+
pad = (L - 2, 0) if dim == 2 else (0, L - 2)
|
|
151
|
+
y = F.conv_transpose2d(lo, g0, stride=stride, padding=pad, groups=C)
|
|
152
|
+
y = y + F.conv_transpose2d(hi, g1, stride=stride, padding=pad, groups=C)
|
|
153
|
+
return y
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""1-D discrete wavelet transform (functional and module API).
|
|
2
|
+
|
|
3
|
+
The module API resolves its filters into registered buffers at construction
|
|
4
|
+
time (eager) and only performs torch ops (``.to(dtype, device)`` + convs) in
|
|
5
|
+
``forward``, so it is fully traceable by ``torch.compile``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import torch
|
|
11
|
+
|
|
12
|
+
from ..core.filters import filter_bank, resolve_1d, wave_name
|
|
13
|
+
from ..core.modes import validate_mode
|
|
14
|
+
from . import lowlevel
|
|
15
|
+
from ._validation import check_reflect_length, check_spatial, validate_level, warn_if_too_high
|
|
16
|
+
|
|
17
|
+
__all__ = ["dwt1d", "idwt1d", "DWT1D", "IDWT1D"]
|
|
18
|
+
|
|
19
|
+
_IS_COMPILING = getattr(torch.compiler, "is_compiling", lambda: False)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _dwt1d_impl(x: torch.Tensor, h0, h1, mode: str, level: int):
|
|
23
|
+
yl = x[:, :, None, :]
|
|
24
|
+
highs = []
|
|
25
|
+
for _ in range(level):
|
|
26
|
+
lohi = lowlevel.afb1d(yl, h0, h1, mode, 3)
|
|
27
|
+
yl = lohi[:, ::2]
|
|
28
|
+
highs.append(lohi[:, 1::2][:, :, 0])
|
|
29
|
+
return yl[:, :, 0], highs
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _idwt1d_impl(yl: torch.Tensor, highs, g0, g1, mode: str) -> torch.Tensor:
|
|
33
|
+
y = yl[:, :, None, :]
|
|
34
|
+
for h in reversed(highs):
|
|
35
|
+
if h is None:
|
|
36
|
+
h = torch.zeros_like(y[:, :, 0])
|
|
37
|
+
elif y.shape[3] == h.shape[2] + 1:
|
|
38
|
+
# PyWavelets convention: drop the extra sample introduced by an
|
|
39
|
+
# odd-length previous level before synthesizing.
|
|
40
|
+
y = y[..., :-1]
|
|
41
|
+
if y.shape[3] != h.shape[2]:
|
|
42
|
+
raise ValueError(
|
|
43
|
+
"coefficient shape mismatch: approximation "
|
|
44
|
+
f"{tuple(y.shape)} vs detail {tuple(h.shape)} at the same level"
|
|
45
|
+
)
|
|
46
|
+
y = lowlevel.sfb1d(y, h[:, :, None, :], g0, g1, mode, 3)
|
|
47
|
+
return y[:, :, 0]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def dwt1d(
|
|
51
|
+
x: torch.Tensor, wave="db1", mode: str = "symmetric", level: int = 1
|
|
52
|
+
) -> tuple[torch.Tensor, list[torch.Tensor]]:
|
|
53
|
+
"""Forward 1-D DWT.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
x (torch.Tensor): ``(N, C, L)`` input.
|
|
57
|
+
wave (str or pywt.Wavelet or tuple): wavelet name, ``pywt.Wavelet`` or
|
|
58
|
+
custom filter bank.
|
|
59
|
+
mode (str): boundary mode.
|
|
60
|
+
level (int): number of decomposition levels (``0`` returns ``(x, [])``).
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
tuple[torch.Tensor, list[torch.Tensor]]: ``(yl, yh)`` where ``yl`` is
|
|
64
|
+
the final lowpass of shape ``(N, C, L')`` and ``yh`` is a list of
|
|
65
|
+
``level`` highpass tensors, finest first. The output dtype matches the
|
|
66
|
+
input dtype.
|
|
67
|
+
"""
|
|
68
|
+
if x.ndim != 3:
|
|
69
|
+
raise ValueError(f"dwt1d expects a 3-D input (N, C, L); got shape {tuple(x.shape)}")
|
|
70
|
+
check_spatial(x.shape[-1:], ("L",), "dwt1d")
|
|
71
|
+
validate_level(level)
|
|
72
|
+
mode = validate_mode(mode)
|
|
73
|
+
check_reflect_length(x.shape[-1:], ("L",), mode, "dwt1d")
|
|
74
|
+
|
|
75
|
+
warn_if_too_high(x.shape[-1], filter_bank(wave).length, level)
|
|
76
|
+
h0, h1, _, _ = resolve_1d(wave, x.dtype, x.device)
|
|
77
|
+
return _dwt1d_impl(x, h0, h1, mode, level)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def idwt1d(coeffs, wave="db1", mode: str = "symmetric") -> torch.Tensor:
|
|
81
|
+
"""Inverse 1-D DWT.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
coeffs (tuple): ``(yl, yh)`` as returned by :func:`dwt1d` (any
|
|
85
|
+
highpass entry may be ``None``, in which case it is treated as
|
|
86
|
+
zeros).
|
|
87
|
+
wave (str or pywt.Wavelet or tuple): as in :func:`dwt1d`.
|
|
88
|
+
mode (str): as in :func:`dwt1d`.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
torch.Tensor: reconstructed ``(N, C, L')`` tensor (dtype matches the
|
|
92
|
+
coefficients).
|
|
93
|
+
|
|
94
|
+
This is a ``pywt.waverec``-style inverse: the coefficient pyramid from
|
|
95
|
+
:func:`dwt1d` is inverted level by level, and (like ``pywt.waverec``) an
|
|
96
|
+
odd-length previous level's extra sample is dropped inside the
|
|
97
|
+
reconstruction. Unlike ``pywt.idwt`` (single-level), the lowpass/detail
|
|
98
|
+
pair of a level does not need to match in length.
|
|
99
|
+
"""
|
|
100
|
+
yl, highs = coeffs
|
|
101
|
+
if yl is None:
|
|
102
|
+
raise ValueError("The lowpass coefficient tensor yl cannot be None")
|
|
103
|
+
if yl.ndim != 3:
|
|
104
|
+
raise ValueError(f"idwt1d expects 3-D coefficients; got shape {tuple(yl.shape)}")
|
|
105
|
+
check_spatial(yl.shape[-1:], ("L",), "idwt1d")
|
|
106
|
+
mode = validate_mode(mode)
|
|
107
|
+
|
|
108
|
+
_, _, g0, g1 = resolve_1d(wave, yl.dtype, yl.device)
|
|
109
|
+
return _idwt1d_impl(yl, highs, g0, g1, mode)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class DWT1D(torch.nn.Module):
|
|
113
|
+
"""1-D DWT module.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
J (int): number of decomposition levels.
|
|
117
|
+
wave (str or pywt.Wavelet or tuple): as in :func:`dwt1d`.
|
|
118
|
+
mode (str): as in :func:`dwt1d`.
|
|
119
|
+
|
|
120
|
+
The precision follows the input tensor: feed a float16 tensor and the
|
|
121
|
+
output is float16, regardless of the dtype used at construction time.
|
|
122
|
+
The module is ``torch.compile``-compatible (filters are resolved eagerly
|
|
123
|
+
at construction; only torch ops run in ``forward``).
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
def __init__(self, J: int = 1, wave="db1", mode: str = "symmetric"):
|
|
127
|
+
super().__init__()
|
|
128
|
+
validate_level(J)
|
|
129
|
+
self.J = J
|
|
130
|
+
self.wave = wave
|
|
131
|
+
self.mode = validate_mode(mode)
|
|
132
|
+
self._bank_length = filter_bank(wave).length
|
|
133
|
+
h0, h1, _, _ = resolve_1d(wave, torch.float64, torch.device("cpu"))
|
|
134
|
+
self.register_buffer("h0", h0)
|
|
135
|
+
self.register_buffer("h1", h1)
|
|
136
|
+
|
|
137
|
+
def forward(self, x: torch.Tensor):
|
|
138
|
+
if x.ndim != 3:
|
|
139
|
+
raise ValueError(f"DWT1D expects a 3-D input (N, C, L); got shape {tuple(x.shape)}")
|
|
140
|
+
check_spatial(x.shape[-1:], ("L",), "DWT1D")
|
|
141
|
+
check_reflect_length(x.shape[-1:], ("L",), self.mode, "DWT1D")
|
|
142
|
+
if not _IS_COMPILING():
|
|
143
|
+
warn_if_too_high(x.shape[-1], self._bank_length, self.J)
|
|
144
|
+
h0 = self.h0.to(dtype=x.dtype, device=x.device)
|
|
145
|
+
h1 = self.h1.to(dtype=x.dtype, device=x.device)
|
|
146
|
+
return _dwt1d_impl(x, h0, h1, self.mode, self.J)
|
|
147
|
+
|
|
148
|
+
def extra_repr(self) -> str:
|
|
149
|
+
return f"J={self.J}, wave={wave_name(self.wave)!r}, mode={self.mode!r}"
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class IDWT1D(torch.nn.Module):
|
|
153
|
+
"""1-D inverse DWT module (see :func:`idwt1d`)."""
|
|
154
|
+
|
|
155
|
+
def __init__(self, wave="db1", mode: str = "symmetric"):
|
|
156
|
+
super().__init__()
|
|
157
|
+
self.wave = wave
|
|
158
|
+
self.mode = validate_mode(mode)
|
|
159
|
+
_, _, g0, g1 = resolve_1d(wave, torch.float64, torch.device("cpu"))
|
|
160
|
+
self.register_buffer("g0", g0)
|
|
161
|
+
self.register_buffer("g1", g1)
|
|
162
|
+
|
|
163
|
+
def forward(self, coeffs):
|
|
164
|
+
yl, _ = coeffs
|
|
165
|
+
if yl is None:
|
|
166
|
+
raise ValueError("The lowpass coefficient tensor yl cannot be None")
|
|
167
|
+
if yl.ndim != 3:
|
|
168
|
+
raise ValueError(f"IDWT1D expects 3-D coefficients; got shape {tuple(yl.shape)}")
|
|
169
|
+
check_spatial(yl.shape[-1:], ("L",), "IDWT1D")
|
|
170
|
+
g0 = self.g0.to(dtype=yl.dtype, device=yl.device)
|
|
171
|
+
g1 = self.g1.to(dtype=yl.dtype, device=yl.device)
|
|
172
|
+
return _idwt1d_impl(yl, coeffs[1], g0, g1, self.mode)
|
|
173
|
+
|
|
174
|
+
def extra_repr(self) -> str:
|
|
175
|
+
return f"wave={wave_name(self.wave)!r}, mode={self.mode!r}"
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""2-D discrete wavelet transform (functional and module API).
|
|
2
|
+
|
|
3
|
+
The module API resolves its filters into registered buffers at construction
|
|
4
|
+
time (eager) and only performs torch ops (``.to(dtype, device)`` + convs) in
|
|
5
|
+
``forward``, so it is fully traceable by ``torch.compile``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import torch
|
|
11
|
+
|
|
12
|
+
from ..core.filters import filter_bank, resolve_2d, wave_name
|
|
13
|
+
from ..core.modes import validate_mode
|
|
14
|
+
from . import lowlevel
|
|
15
|
+
from ._validation import check_reflect_length, check_spatial, validate_level, warn_if_too_high
|
|
16
|
+
|
|
17
|
+
__all__ = ["dwt2d", "idwt2d", "DWT2D", "IDWT2D"]
|
|
18
|
+
|
|
19
|
+
_IS_COMPILING = getattr(torch.compiler, "is_compiling", lambda: False)
|
|
20
|
+
|
|
21
|
+
_DWT2D_NAMES = ("h0_col", "h1_col", "h0_row", "h1_row")
|
|
22
|
+
_IDWT2D_NAMES = ("g0_col", "g1_col", "g0_row", "g1_row")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _dwt2d_impl(x: torch.Tensor, h0c, h1c, h0r, h1r, mode: str, level: int):
|
|
26
|
+
yl = x
|
|
27
|
+
highs = []
|
|
28
|
+
for _ in range(level):
|
|
29
|
+
lohi = lowlevel.afb1d(yl, h0r, h1r, mode, 3) # rows
|
|
30
|
+
y = lowlevel.afb1d(lohi, h0c, h1c, mode, 2) # columns
|
|
31
|
+
s = y.shape
|
|
32
|
+
y = y.reshape(s[0], s[1] // 4, 4, s[-2], s[-1])
|
|
33
|
+
yl = y[:, :, 0].contiguous()
|
|
34
|
+
highs.append(y[:, :, 1:].contiguous())
|
|
35
|
+
return yl, highs
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _idwt2d_impl(yl: torch.Tensor, highs, g0c, g1c, g0r, g1r, mode: str) -> torch.Tensor:
|
|
39
|
+
y = yl
|
|
40
|
+
for h in reversed(highs):
|
|
41
|
+
if h is None:
|
|
42
|
+
h = torch.zeros(
|
|
43
|
+
y.shape[0],
|
|
44
|
+
y.shape[1],
|
|
45
|
+
3,
|
|
46
|
+
y.shape[-2],
|
|
47
|
+
y.shape[-1],
|
|
48
|
+
dtype=y.dtype,
|
|
49
|
+
device=y.device,
|
|
50
|
+
)
|
|
51
|
+
else:
|
|
52
|
+
# PyWavelets convention: drop extra samples introduced by
|
|
53
|
+
# odd-length previous levels before synthesizing.
|
|
54
|
+
if y.shape[-2] == h.shape[-2] + 1:
|
|
55
|
+
y = y[..., :-1, :]
|
|
56
|
+
if y.shape[-1] == h.shape[-1] + 1:
|
|
57
|
+
y = y[..., :-1]
|
|
58
|
+
if y.shape[-2] != h.shape[-2] or y.shape[-1] != h.shape[-1]:
|
|
59
|
+
raise ValueError(
|
|
60
|
+
"coefficient shape mismatch: approximation "
|
|
61
|
+
f"{tuple(y.shape)} vs detail {tuple(h.shape)} at the same level"
|
|
62
|
+
)
|
|
63
|
+
lh, hl, hh = torch.unbind(h, dim=2)
|
|
64
|
+
lo = lowlevel.sfb1d(y, lh, g0c, g1c, mode, 2)
|
|
65
|
+
hi = lowlevel.sfb1d(hl, hh, g0c, g1c, mode, 2)
|
|
66
|
+
y = lowlevel.sfb1d(lo, hi, g0r, g1r, mode, 3)
|
|
67
|
+
return y
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def dwt2d(
|
|
71
|
+
x: torch.Tensor, wave="db1", mode: str = "symmetric", level: int = 1
|
|
72
|
+
) -> tuple[torch.Tensor, list[torch.Tensor]]:
|
|
73
|
+
"""Forward 2-D DWT (separable, row then column filtering).
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
x (torch.Tensor): ``(N, C, H, W)`` input.
|
|
77
|
+
wave (str or pywt.Wavelet or tuple): wavelet name, ``pywt.Wavelet`` or
|
|
78
|
+
custom filter bank.
|
|
79
|
+
mode (str): boundary mode.
|
|
80
|
+
level (int): number of decomposition levels (``0`` returns ``(x, [])``).
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
``(yl, yh)`` where ``yl`` is the final lowpass ``(N, C, H', W')`` and
|
|
84
|
+
``yh`` is a list of ``level`` tensors of shape
|
|
85
|
+
``(N, C, 3, H'', W'')`` holding the ``(LH, HL, HH)`` subbands
|
|
86
|
+
(finest first). ``LH``/``HL``/``HH`` match PyWavelets'
|
|
87
|
+
``cH``/``cV``/``cD``. The output dtype matches the input dtype.
|
|
88
|
+
"""
|
|
89
|
+
if x.ndim != 4:
|
|
90
|
+
raise ValueError(f"dwt2d expects a 4-D input (N, C, H, W); got shape {tuple(x.shape)}")
|
|
91
|
+
check_spatial(x.shape[2:], ("H", "W"), "dwt2d")
|
|
92
|
+
mode = validate_mode(mode)
|
|
93
|
+
check_reflect_length(x.shape[2:], ("H", "W"), mode, "dwt2d")
|
|
94
|
+
validate_level(level)
|
|
95
|
+
mode = validate_mode(mode)
|
|
96
|
+
|
|
97
|
+
warn_if_too_high(min(x.shape[-2:]), filter_bank(wave).length, level)
|
|
98
|
+
h0c, h1c, h0r, h1r, _, _, _, _ = resolve_2d(wave, x.dtype, x.device)
|
|
99
|
+
return _dwt2d_impl(x, h0c, h1c, h0r, h1r, mode, level)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def idwt2d(coeffs, wave="db1", mode: str = "symmetric") -> torch.Tensor:
|
|
103
|
+
"""Inverse 2-D DWT.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
coeffs (tuple): ``(yl, yh)`` as returned by :func:`dwt2d` (any
|
|
107
|
+
highpass entry may be ``None``, in which case it is treated as
|
|
108
|
+
zeros).
|
|
109
|
+
wave (str or pywt.Wavelet or tuple): as in :func:`dwt2d`.
|
|
110
|
+
mode (str): as in :func:`dwt2d`.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
torch.Tensor: reconstructed ``(N, C, H', W')`` tensor (dtype matches
|
|
114
|
+
the coefficients).
|
|
115
|
+
"""
|
|
116
|
+
yl, highs = coeffs
|
|
117
|
+
if yl is None:
|
|
118
|
+
raise ValueError("The lowpass coefficient tensor yl cannot be None")
|
|
119
|
+
if yl.ndim != 4:
|
|
120
|
+
raise ValueError(f"idwt2d expects 4-D coefficients; got shape {tuple(yl.shape)}")
|
|
121
|
+
check_spatial(yl.shape[2:], ("H", "W"), "idwt2d")
|
|
122
|
+
mode = validate_mode(mode)
|
|
123
|
+
|
|
124
|
+
_, _, _, _, g0c, g1c, g0r, g1r = resolve_2d(wave, yl.dtype, yl.device)
|
|
125
|
+
return _idwt2d_impl(yl, highs, g0c, g1c, g0r, g1r, validate_mode(mode))
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class DWT2D(torch.nn.Module):
|
|
129
|
+
"""2-D DWT module.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
J (int): number of decomposition levels.
|
|
133
|
+
wave (str or pywt.Wavelet or tuple): as in :func:`dwt2d`.
|
|
134
|
+
mode (str): as in :func:`dwt2d`.
|
|
135
|
+
|
|
136
|
+
The precision follows the input tensor: feed a float16 tensor and the
|
|
137
|
+
output is float16, regardless of the dtype used at construction time.
|
|
138
|
+
The module is ``torch.compile``-compatible (filters are resolved eagerly
|
|
139
|
+
at construction; only torch ops run in ``forward``).
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
def __init__(self, J: int = 1, wave="db1", mode: str = "symmetric"):
|
|
143
|
+
super().__init__()
|
|
144
|
+
validate_level(J)
|
|
145
|
+
self.J = J
|
|
146
|
+
self.wave = wave
|
|
147
|
+
self.mode = validate_mode(mode)
|
|
148
|
+
self._bank_length = filter_bank(wave).length
|
|
149
|
+
filts = resolve_2d(wave, torch.float64, torch.device("cpu"))
|
|
150
|
+
for name, t in zip(_DWT2D_NAMES, filts[:4], strict=True):
|
|
151
|
+
self.register_buffer(name, t)
|
|
152
|
+
|
|
153
|
+
def forward(self, x: torch.Tensor):
|
|
154
|
+
if x.ndim != 4:
|
|
155
|
+
raise ValueError(f"DWT2D expects a 4-D input (N, C, H, W); got shape {tuple(x.shape)}")
|
|
156
|
+
check_spatial(x.shape[2:], ("H", "W"), "DWT2D")
|
|
157
|
+
check_reflect_length(x.shape[2:], ("H", "W"), self.mode, "DWT2D")
|
|
158
|
+
if not _IS_COMPILING():
|
|
159
|
+
warn_if_too_high(min(x.shape[-2:]), self._bank_length, self.J)
|
|
160
|
+
h0c = self.h0_col.to(dtype=x.dtype, device=x.device)
|
|
161
|
+
h1c = self.h1_col.to(dtype=x.dtype, device=x.device)
|
|
162
|
+
h0r = self.h0_row.to(dtype=x.dtype, device=x.device)
|
|
163
|
+
h1r = self.h1_row.to(dtype=x.dtype, device=x.device)
|
|
164
|
+
return _dwt2d_impl(x, h0c, h1c, h0r, h1r, self.mode, self.J)
|
|
165
|
+
|
|
166
|
+
def extra_repr(self) -> str:
|
|
167
|
+
return f"J={self.J}, wave={wave_name(self.wave)!r}, mode={self.mode!r}"
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class IDWT2D(torch.nn.Module):
|
|
171
|
+
"""2-D inverse DWT module (see :func:`idwt2d`)."""
|
|
172
|
+
|
|
173
|
+
def __init__(self, wave="db1", mode: str = "symmetric"):
|
|
174
|
+
super().__init__()
|
|
175
|
+
self.wave = wave
|
|
176
|
+
self.mode = validate_mode(mode)
|
|
177
|
+
filts = resolve_2d(wave, torch.float64, torch.device("cpu"))
|
|
178
|
+
for name, t in zip(_IDWT2D_NAMES, filts[4:], strict=True):
|
|
179
|
+
self.register_buffer(name, t)
|
|
180
|
+
|
|
181
|
+
def forward(self, coeffs):
|
|
182
|
+
yl, _ = coeffs
|
|
183
|
+
if yl is None:
|
|
184
|
+
raise ValueError("The lowpass coefficient tensor yl cannot be None")
|
|
185
|
+
if yl.ndim != 4:
|
|
186
|
+
raise ValueError(f"IDWT2D expects 4-D coefficients; got shape {tuple(yl.shape)}")
|
|
187
|
+
check_spatial(yl.shape[2:], ("H", "W"), "IDWT2D")
|
|
188
|
+
g0c = self.g0_col.to(dtype=yl.dtype, device=yl.device)
|
|
189
|
+
g1c = self.g1_col.to(dtype=yl.dtype, device=yl.device)
|
|
190
|
+
g0r = self.g0_row.to(dtype=yl.dtype, device=yl.device)
|
|
191
|
+
g1r = self.g1_row.to(dtype=yl.dtype, device=yl.device)
|
|
192
|
+
return _idwt2d_impl(yl, coeffs[1], g0c, g1c, g0r, g1r, self.mode)
|
|
193
|
+
|
|
194
|
+
def extra_repr(self) -> str:
|
|
195
|
+
return f"wave={wave_name(self.wave)!r}, mode={self.mode!r}"
|