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,204 @@
|
|
|
1
|
+
"""General (arbitrary-order) DTCWT scattering network.
|
|
2
|
+
|
|
3
|
+
A scattering network computes locally shift-invariant feature maps by
|
|
4
|
+
iterating wavelet bandpasses with the smooth modulus ``sqrt(|Wx|^2 + b^2) - b``
|
|
5
|
+
and smoothing the intermediate invariants with the wavelet lowpass. With the
|
|
6
|
+
DTCWT's near shift-invariance this gives a small, cheap, highly invariant
|
|
7
|
+
front-end for classification and segmentation.
|
|
8
|
+
|
|
9
|
+
Paths are sequences of increasing scales ``(j1 < j2 < ... < jm)`` with
|
|
10
|
+
``m <= max_order``. For each path the network returns one invariant:
|
|
11
|
+
``avg_pool2d(lowpass, 2)`` when the path can still be extended (the smoothed
|
|
12
|
+
modulus feeding the next order) and the raw ``|bandpass|`` at the terminal
|
|
13
|
+
paths. With ``(J=2, max_order=2)`` the output matches
|
|
14
|
+
:class:`~torchwavelets.scatter.lowlevel.ScatLayerj2` exactly.
|
|
15
|
+
|
|
16
|
+
Shift invariance: the raw level-1 modulus ``|W_1 x|`` is only *weakly* shift
|
|
17
|
+
invariant (the single-tree level-1 design), so individual raw bandpass
|
|
18
|
+
invariants drift with sub-scale shifts; the smoothed invariants (pooled
|
|
19
|
+
lowpasses) and the order-0 lowpass are exactly invariant at dyadic integer
|
|
20
|
+
shifts, and the invariance improves with scale.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import torch
|
|
26
|
+
import torch.nn as nn
|
|
27
|
+
import torch.nn.functional as F
|
|
28
|
+
|
|
29
|
+
from ..dtcwt._autocast import autocast_off
|
|
30
|
+
from ..dtcwt.coeffs import biort as _biort
|
|
31
|
+
from ..dtcwt.coeffs import qshift as _qshift
|
|
32
|
+
from ..dtcwt.transform2d import (
|
|
33
|
+
_compute_dtype,
|
|
34
|
+
_dtcwt2d_impl,
|
|
35
|
+
_prep_filt,
|
|
36
|
+
_validate_names,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
__all__ = ["DTCWTScattering", "dtcwt_scattering"]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _smooth_mag(highs: torch.Tensor, magbias: float) -> torch.Tensor:
|
|
43
|
+
"""Smooth modulus ``sqrt(|z|^2 + b^2) - b`` of complex bandpasses."""
|
|
44
|
+
return torch.sqrt(highs.abs().pow(2) + magbias**2) - magbias
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _resolve_filters(biort, qshift, dtype, device):
|
|
48
|
+
_validate_names(biort, qshift)
|
|
49
|
+
h0o, _, h1o, _ = _biort(biort)
|
|
50
|
+
h0a, h0b, _, _, h1a, h1b, _, _ = _qshift(qshift)
|
|
51
|
+
return {
|
|
52
|
+
"h0o": _prep_filt(h0o, dtype, device),
|
|
53
|
+
"h1o": _prep_filt(h1o, dtype, device),
|
|
54
|
+
"h0a": _prep_filt(h0a, dtype, device),
|
|
55
|
+
"h0b": _prep_filt(h0b, dtype, device),
|
|
56
|
+
"h1a": _prep_filt(h1a, dtype, device),
|
|
57
|
+
"h1b": _prep_filt(h1b, dtype, device),
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _dtcwt_levels(u: torch.Tensor, f, levels: int):
|
|
62
|
+
"""``levels``-level DTCWT of ``u``; returns ``(lowpass, bandpass)``."""
|
|
63
|
+
yl, yh = _dtcwt2d_impl(u, f, levels, [False] * levels, [False] * levels, 2)
|
|
64
|
+
return yl, yh[-1]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _scattering_impl(x, f, J, max_order, magbias):
|
|
68
|
+
"""BFS path expansion; returns ``(s0, [(path, invariant), ...])``.
|
|
69
|
+
|
|
70
|
+
Invariant semantics (matching ScatLayerj2): the invariant of an
|
|
71
|
+
extendable path is the smoothed (avg-pooled) lowpass of the level-1
|
|
72
|
+
transform of its modulus; terminal paths return the raw modulus.
|
|
73
|
+
"""
|
|
74
|
+
if J < 1:
|
|
75
|
+
raise ValueError("J must be >= 1 for a scattering network")
|
|
76
|
+
# order-0 invariant: the smoothed final lowpass
|
|
77
|
+
yl, _ = _dtcwt_levels(x, f, J)
|
|
78
|
+
s0 = F.avg_pool2d(yl, 2)
|
|
79
|
+
|
|
80
|
+
results = []
|
|
81
|
+
# frontier entries: (u, current_scale, path, pending_path)
|
|
82
|
+
# pending_path = the path whose smoothed invariant comes from the level-1
|
|
83
|
+
# transform of u (computed at this node).
|
|
84
|
+
frontier = [(x, 0, (), None)]
|
|
85
|
+
for _ in range(max_order):
|
|
86
|
+
next_frontier = []
|
|
87
|
+
for u, scale, path, pending in frontier:
|
|
88
|
+
# level-1 transform of u: needed for the pending invariant and as
|
|
89
|
+
# the first step of the j-loop below.
|
|
90
|
+
ll1, hh1 = _dtcwt_levels(u, f, 1)
|
|
91
|
+
u1 = _flatten_orientations(_smooth_mag(hh1, magbias))
|
|
92
|
+
if pending is not None:
|
|
93
|
+
results.append((pending, F.avg_pool2d(ll1, 2)))
|
|
94
|
+
for j in range(scale + 1, J + 1):
|
|
95
|
+
if j == scale + 1:
|
|
96
|
+
ll, hh, u_next = ll1, hh1, u1
|
|
97
|
+
else:
|
|
98
|
+
ll, hh = _dtcwt_levels(u, f, j - scale)
|
|
99
|
+
u_next = _flatten_orientations(_smooth_mag(hh, magbias))
|
|
100
|
+
new_path = path + (j,)
|
|
101
|
+
if j < J and len(new_path) < max_order:
|
|
102
|
+
next_frontier.append((u_next, j, new_path, new_path))
|
|
103
|
+
else:
|
|
104
|
+
results.append((new_path, _upstream_order(u_next, len(new_path))))
|
|
105
|
+
frontier = next_frontier
|
|
106
|
+
results.sort(key=lambda pv: (len(pv[0]), pv[0]))
|
|
107
|
+
return s0, results
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _flatten_orientations(u: torch.Tensor) -> torch.Tensor:
|
|
111
|
+
"""``(N, C, 6, H, W)`` modulus -> ``(N, 6*C, H, W)``."""
|
|
112
|
+
b, ch, _, h, w = u.shape
|
|
113
|
+
return u.reshape(b, ch * 6, h, w)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _upstream_order(u: torch.Tensor, m: int) -> torch.Tensor:
|
|
117
|
+
"""Rotate the orientation blocks to the reference channel order.
|
|
118
|
+
|
|
119
|
+
Internally the moduli propagate channel-major as
|
|
120
|
+
``(C, o1st, o2nd, ..., om)``; the reference (``ScatLayerj2``) stacks the
|
|
121
|
+
first orientation block last: ``(C, o2nd, ..., om, o1st)``. For a
|
|
122
|
+
first-order modulus (``m == 1``) this is the identity — the reference
|
|
123
|
+
keeps the moduli channel-major ``(C, o1st)``.
|
|
124
|
+
"""
|
|
125
|
+
b, c6, h, w = u.shape
|
|
126
|
+
c = c6 // 6**m
|
|
127
|
+
blocks = u.reshape(b, c, *([6] * m), h, w)
|
|
128
|
+
perm = (0, 1) + tuple(range(3, m + 2)) + (2, m + 2, m + 3)
|
|
129
|
+
return blocks.permute(perm).reshape(b, c6, h, w)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def dtcwt_scattering(
|
|
133
|
+
x: torch.Tensor,
|
|
134
|
+
J: int = 3,
|
|
135
|
+
max_order: int = 2,
|
|
136
|
+
biort="near_sym_a",
|
|
137
|
+
qshift="qshift_a",
|
|
138
|
+
magbias: float = 1e-2,
|
|
139
|
+
) -> tuple[torch.Tensor, list[tuple[tuple[int, ...], torch.Tensor]]]:
|
|
140
|
+
"""Forward DTCWT scattering network.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
x (torch.Tensor): ``(N, C, H, W)`` input.
|
|
144
|
+
J (int): number of wavelet scales.
|
|
145
|
+
max_order (int): maximum scattering order.
|
|
146
|
+
biort (str): filter bank name.
|
|
147
|
+
qshift (str): filter bank name.
|
|
148
|
+
magbias (float): smoothing bias of the modulus.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
tuple[torch.Tensor, list[tuple[tuple[int, ...], torch.Tensor]]]:
|
|
152
|
+
``(s0, invariants)``: the smoothed order-0 lowpass
|
|
153
|
+
``(N, C, H/2^J, W/2^J)`` and a list of ``(path, tensor)`` pairs, one
|
|
154
|
+
per path (orders 1..max_order, ordered by order then scale). The
|
|
155
|
+
invariant size depends only on the path's final scale ``jm``:
|
|
156
|
+
``H/2^jm``.
|
|
157
|
+
"""
|
|
158
|
+
if x.ndim != 4:
|
|
159
|
+
raise ValueError(f"dtcwt_scattering expects (N, C, H, W); got {tuple(x.shape)}")
|
|
160
|
+
if isinstance(J, bool) or not isinstance(J, int) or J < 1:
|
|
161
|
+
raise ValueError(f"J must be a positive int; got {J!r}")
|
|
162
|
+
if isinstance(max_order, bool) or not isinstance(max_order, int) or max_order < 1:
|
|
163
|
+
raise ValueError(f"max_order must be a positive int; got {max_order!r}")
|
|
164
|
+
dtype = _compute_dtype(x)
|
|
165
|
+
f = _resolve_filters(biort, qshift, dtype, x.device)
|
|
166
|
+
with autocast_off(x.device):
|
|
167
|
+
return _scattering_impl(x.to(dtype), f, J, max_order, float(magbias))
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class DTCWTScattering(nn.Module):
|
|
171
|
+
"""DTCWT scattering network module (see :func:`dtcwt_scattering`)."""
|
|
172
|
+
|
|
173
|
+
def __init__(self, J=3, max_order=2, biort="near_sym_a", qshift="qshift_a", magbias=1e-2):
|
|
174
|
+
super().__init__()
|
|
175
|
+
if isinstance(J, bool) or not isinstance(J, int) or J < 1:
|
|
176
|
+
raise ValueError(f"J must be a positive int; got {J!r}")
|
|
177
|
+
if isinstance(max_order, bool) or not isinstance(max_order, int) or max_order < 1:
|
|
178
|
+
raise ValueError(f"max_order must be a positive int; got {max_order!r}")
|
|
179
|
+
self.J = J
|
|
180
|
+
self.max_order = max_order
|
|
181
|
+
self.biort = biort
|
|
182
|
+
self.qshift = qshift
|
|
183
|
+
self.magbias = float(magbias)
|
|
184
|
+
filts = _resolve_filters(biort, qshift, torch.float64, torch.device("cpu"))
|
|
185
|
+
for name, t in filts.items():
|
|
186
|
+
self.register_buffer(name, t)
|
|
187
|
+
|
|
188
|
+
def forward(self, x: torch.Tensor):
|
|
189
|
+
if x.ndim != 4:
|
|
190
|
+
raise ValueError(f"DTCWTScattering expects (N, C, H, W); got {tuple(x.shape)}")
|
|
191
|
+
dtype = _compute_dtype(x)
|
|
192
|
+
f = {
|
|
193
|
+
n: getattr(self, n).to(dtype=dtype, device=x.device)
|
|
194
|
+
for n in ("h0o", "h1o", "h0a", "h0b", "h1a", "h1b")
|
|
195
|
+
}
|
|
196
|
+
with autocast_off(x.device):
|
|
197
|
+
return _scattering_impl(x.to(dtype), f, self.J, self.max_order, self.magbias)
|
|
198
|
+
|
|
199
|
+
def extra_repr(self) -> str:
|
|
200
|
+
return (
|
|
201
|
+
f"J={self.J}, max_order={self.max_order}, "
|
|
202
|
+
f"biort={self.biort!r}, qshift={self.qshift!r}, "
|
|
203
|
+
f"magbias={self.magbias}"
|
|
204
|
+
)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Stationary (undecimated) wavelet transforms."""
|
|
2
|
+
|
|
3
|
+
from .transform import (
|
|
4
|
+
ISWT1D,
|
|
5
|
+
ISWT2D,
|
|
6
|
+
SWT1D,
|
|
7
|
+
SWT2D,
|
|
8
|
+
iswt1d,
|
|
9
|
+
iswt2d,
|
|
10
|
+
swt1d,
|
|
11
|
+
swt2d,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = ["swt1d", "iswt1d", "swt2d", "iswt2d", "SWT1D", "ISWT1D", "SWT2D", "ISWT2D"]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Low-level stationary (undecimated / a trous) wavelet filter banks.
|
|
2
|
+
|
|
3
|
+
The SWT is the undecimated periodic wavelet transform: at level ``j`` the
|
|
4
|
+
analysis filters are dilated (zero-stuffed) by ``2**(j-1)`` and applied with
|
|
5
|
+
no decimation, so every subband has the same length as the input. The
|
|
6
|
+
phase conventions match PyWavelets exactly (verified bit-for-bit by the
|
|
7
|
+
parity tests).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import torch
|
|
13
|
+
import torch.nn.functional as F
|
|
14
|
+
|
|
15
|
+
__all__ = ["swt_axis", "dilate_filter"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def dilate_filter(h: torch.Tensor, fstep: int) -> torch.Tensor:
|
|
19
|
+
"""Insert ``fstep - 1`` zeros between filter taps (a trous dilation)."""
|
|
20
|
+
L = h.numel()
|
|
21
|
+
e = torch.zeros(1, 1, L * fstep, 1, dtype=h.dtype, device=h.device)
|
|
22
|
+
e[0, 0, ::fstep, 0] = h.reshape(-1)
|
|
23
|
+
return e
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def swt_axis(x: torch.Tensor, h: torch.Tensor, dim: int, fstep: int) -> torch.Tensor:
|
|
27
|
+
"""One stationary analysis filter along ``dim`` (no decimation).
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
x: ``(N, C, H, W)`` input.
|
|
31
|
+
h: reversed analysis filter, ``(1, 1, L, 1)`` (column orientation;
|
|
32
|
+
transposed internally for ``dim == 3``).
|
|
33
|
+
dim: spatial dimension (2 or 3).
|
|
34
|
+
fstep: dilation factor ``2**(level - 1)``.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
``(N, C, ...)`` with the same spatial size as ``x``.
|
|
38
|
+
"""
|
|
39
|
+
n = x.shape[dim]
|
|
40
|
+
L = h.numel()
|
|
41
|
+
Ld = L * fstep # dilated filter length
|
|
42
|
+
e = dilate_filter(h, fstep)
|
|
43
|
+
if dim == 3:
|
|
44
|
+
e = e.transpose(2, 3).contiguous()
|
|
45
|
+
# pywt: out[i] = sum_m dec[m] x[(i + (L/2 - m)*fstep) mod n]; with the
|
|
46
|
+
# reversed-filter convention this is out[i] = sum_k h_rev[k]
|
|
47
|
+
# x[(i + (k - L/2 + 1)*fstep) mod n].
|
|
48
|
+
shift = fstep * (L // 2 - 1)
|
|
49
|
+
idx = (torch.arange(n + Ld - 1, device=x.device) - shift) % n
|
|
50
|
+
xp = x.index_select(dim, idx)
|
|
51
|
+
stride = (1, 1)
|
|
52
|
+
C = x.shape[1]
|
|
53
|
+
return F.conv2d(xp, e.repeat(C, 1, 1, 1), stride=stride, groups=C)
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
"""Stationary (undecimated) wavelet transform: functional and module API.
|
|
2
|
+
|
|
3
|
+
The SWT (algorithm a trous) keeps every subband at the input resolution by
|
|
4
|
+
dilating the analysis filters by ``2**(level-1)`` and skipping decimation.
|
|
5
|
+
Coefficients follow the PyWavelets conventions exactly (coarsest first), and
|
|
6
|
+
``iswt`` / ``iswt2`` are exact inverses (verified bit-for-bit against pywt
|
|
7
|
+
by the parity tests).
|
|
8
|
+
|
|
9
|
+
Inputs must be divisible by ``2**level`` along every transformed axis, as
|
|
10
|
+
required by the stationary transform (PyWavelets raises the same error).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import cast
|
|
16
|
+
|
|
17
|
+
import torch
|
|
18
|
+
import torch.nn as nn
|
|
19
|
+
|
|
20
|
+
from ..core.filters import resolve_2d
|
|
21
|
+
from ..dwt import lowlevel as dwt_ll
|
|
22
|
+
from ..dwt._validation import check_spatial
|
|
23
|
+
from .lowlevel import swt_axis
|
|
24
|
+
|
|
25
|
+
__all__ = ["swt1d", "iswt1d", "swt2d", "iswt2d", "SWT1D", "ISWT1D", "SWT2D", "ISWT2D"]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _check_level(level: int) -> None:
|
|
29
|
+
if isinstance(level, bool) or not isinstance(level, int) or level < 1:
|
|
30
|
+
raise ValueError(f"level must be a positive int; got {level!r}")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _check_divisible(n: int, level: int, name: str) -> None:
|
|
34
|
+
if n % (2**level) != 0:
|
|
35
|
+
raise ValueError(
|
|
36
|
+
f"{name} length {n} is not divisible by 2**level = {2**level}; "
|
|
37
|
+
"the stationary wavelet transform requires it (pad the input)"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _iswt_idwt(lo: torch.Tensor, hi: torch.Tensor, g0r, g1r) -> torch.Tensor:
|
|
42
|
+
"""Single-level periodic inverse DWT on 3-D ``(N, C, L)`` coefficients."""
|
|
43
|
+
lo4 = lo.unsqueeze(2)
|
|
44
|
+
hi4 = hi.unsqueeze(2)
|
|
45
|
+
return dwt_ll.sfb1d(lo4, hi4, g0r, g1r, "periodization", 3).squeeze(2)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def swt1d(
|
|
49
|
+
x: torch.Tensor, wave, level: int, norm: bool = False
|
|
50
|
+
) -> list[tuple[torch.Tensor, torch.Tensor]]:
|
|
51
|
+
"""1-D stationary wavelet transform.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
x (torch.Tensor): ``(N, C, L)`` input.
|
|
55
|
+
wave (str or pywt.Wavelet or tuple): wavelet name / bank (see
|
|
56
|
+
:func:`torchwavelets.core.filters.filter_bank`).
|
|
57
|
+
level (int): number of scales.
|
|
58
|
+
norm (bool): normalize so the coefficient energy equals the input
|
|
59
|
+
energy (orthogonal wavelets only, like PyWavelets).
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
list[tuple[torch.Tensor, torch.Tensor]]: ``[(cA_n, cD_n), ...,
|
|
63
|
+
(cA_1, cD_1)]`` (coarsest first), every subband of shape ``(N, C, L)``.
|
|
64
|
+
"""
|
|
65
|
+
_check_level(level)
|
|
66
|
+
if x.ndim != 3:
|
|
67
|
+
raise ValueError(f"swt1d expects (N, C, L); got {tuple(x.shape)}")
|
|
68
|
+
check_spatial(x.shape[-1:], ("L",), "swt1d")
|
|
69
|
+
_check_divisible(x.shape[-1], level, "signal")
|
|
70
|
+
h0c, h1c, _, _, g0c, g1c, _, _ = resolve_2d(wave, x.dtype, x.device)
|
|
71
|
+
if norm:
|
|
72
|
+
h0c, h1c = h0c / 2**0.5, h1c / 2**0.5
|
|
73
|
+
x4 = x.unsqueeze(-1) # (N, C, L, 1)
|
|
74
|
+
ret = []
|
|
75
|
+
a = x4
|
|
76
|
+
for j in range(1, level + 1):
|
|
77
|
+
fstep = 2 ** (j - 1)
|
|
78
|
+
ca = swt_axis(a, h0c, 2, fstep)
|
|
79
|
+
cd = swt_axis(a, h1c, 2, fstep)
|
|
80
|
+
ret.append((ca.squeeze(-1), cd.squeeze(-1)))
|
|
81
|
+
a = ca
|
|
82
|
+
ret.reverse()
|
|
83
|
+
return ret
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def iswt1d(coeffs, wave, norm: bool = False) -> torch.Tensor:
|
|
87
|
+
"""Inverse 1-D stationary wavelet transform (exact inverse of :func:`swt1d`)."""
|
|
88
|
+
for cA, cD in coeffs:
|
|
89
|
+
if cA.shape != cD.shape:
|
|
90
|
+
raise ValueError(
|
|
91
|
+
"iswt1d requires cA and cD of the same shape; got "
|
|
92
|
+
f"{tuple(cA.shape)} vs {tuple(cD.shape)}"
|
|
93
|
+
)
|
|
94
|
+
_, _, _, _, _, _, g0r, g1r = resolve_2d(wave, coeffs[0][0].dtype, coeffs[0][0].device)
|
|
95
|
+
if norm:
|
|
96
|
+
g0r, g1r = g0r * 2**0.5, g1r * 2**0.5
|
|
97
|
+
n_levels = len(coeffs)
|
|
98
|
+
output = coeffs[0][0].clone()
|
|
99
|
+
N = output.shape[-1]
|
|
100
|
+
for j in range(n_levels, 0, -1):
|
|
101
|
+
step = 2 ** (j - 1)
|
|
102
|
+
cD = coeffs[-j][1]
|
|
103
|
+
for first in range(step):
|
|
104
|
+
idx = torch.arange(first, N, step, device=output.device)
|
|
105
|
+
even = idx[0::2]
|
|
106
|
+
odd = idx[1::2]
|
|
107
|
+
x1 = _iswt_idwt(
|
|
108
|
+
output.index_select(-1, even),
|
|
109
|
+
cD.index_select(-1, even),
|
|
110
|
+
g0r,
|
|
111
|
+
g1r,
|
|
112
|
+
)
|
|
113
|
+
x2 = _iswt_idwt(
|
|
114
|
+
output.index_select(-1, odd),
|
|
115
|
+
cD.index_select(-1, odd),
|
|
116
|
+
g0r,
|
|
117
|
+
g1r,
|
|
118
|
+
)
|
|
119
|
+
x2 = torch.roll(x2, 1, dims=-1)
|
|
120
|
+
output[..., idx] = (x1 + x2) / 2
|
|
121
|
+
return output
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def swt2d(
|
|
125
|
+
x: torch.Tensor, wave, level: int, norm: bool = False
|
|
126
|
+
) -> list[tuple[torch.Tensor, tuple[torch.Tensor, torch.Tensor, torch.Tensor]]]:
|
|
127
|
+
"""2-D stationary wavelet transform.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
x (torch.Tensor): ``(N, C, H, W)`` input.
|
|
131
|
+
wave (str or pywt.Wavelet or tuple): as in :func:`swt1d`.
|
|
132
|
+
level (int): as in :func:`swt1d`.
|
|
133
|
+
norm (bool): as in :func:`swt1d`.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
list[tuple[torch.Tensor, tuple[torch.Tensor, torch.Tensor, torch.Tensor]]]:
|
|
137
|
+
``[(cA_j, (cH_j, cV_j, cD_j)), ...]`` (coarsest first), all subbands
|
|
138
|
+
of shape ``(N, C, H, W)``.
|
|
139
|
+
"""
|
|
140
|
+
_check_level(level)
|
|
141
|
+
if x.ndim != 4:
|
|
142
|
+
raise ValueError(f"swt2d expects (N, C, H, W); got {tuple(x.shape)}")
|
|
143
|
+
check_spatial(x.shape[2:], ("H", "W"), "swt2d")
|
|
144
|
+
_check_divisible(x.shape[-2], level, "height")
|
|
145
|
+
_check_divisible(x.shape[-1], level, "width")
|
|
146
|
+
h0c, h1c, h0r, h1r, _, _, _, _ = resolve_2d(wave, x.dtype, x.device)
|
|
147
|
+
if norm:
|
|
148
|
+
h0c, h1c, h0r, h1r = h0c / 2**0.5, h1c / 2**0.5, h0r / 2**0.5, h1r / 2**0.5
|
|
149
|
+
ret = []
|
|
150
|
+
a = x
|
|
151
|
+
for j in range(1, level + 1):
|
|
152
|
+
fstep = 2 ** (j - 1)
|
|
153
|
+
# axis 0 (rows, dim 2), then axis 1 (cols, dim 3), like pywt.swt2
|
|
154
|
+
low = swt_axis(a, h0c, 2, fstep)
|
|
155
|
+
high = swt_axis(a, h1c, 2, fstep)
|
|
156
|
+
aa = swt_axis(low, h0r, 3, fstep)
|
|
157
|
+
ad = swt_axis(low, h1r, 3, fstep)
|
|
158
|
+
da = swt_axis(high, h0r, 3, fstep)
|
|
159
|
+
dd = swt_axis(high, h1r, 3, fstep)
|
|
160
|
+
ret.append((aa, (da, ad, dd)))
|
|
161
|
+
a = aa
|
|
162
|
+
ret.reverse()
|
|
163
|
+
return ret
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def iswt2d(coeffs, wave, norm: bool = False) -> torch.Tensor:
|
|
167
|
+
"""Inverse 2-D stationary wavelet transform (exact inverse of :func:`swt2d`)."""
|
|
168
|
+
for cA, cs in coeffs:
|
|
169
|
+
if any(c.shape != cA.shape for c in cs):
|
|
170
|
+
raise ValueError(
|
|
171
|
+
"iswt2d requires all subbands of a level to share the shape "
|
|
172
|
+
f"{tuple(cA.shape)}; got {[tuple(c.shape) for c in cs]}"
|
|
173
|
+
)
|
|
174
|
+
_, _, _, _, g0c, g1c, g0r, g1r = resolve_2d(wave, coeffs[0][0].dtype, coeffs[0][0].device)
|
|
175
|
+
if norm:
|
|
176
|
+
g0c, g1c, g0r, g1r = g0c * 2**0.5, g1c * 2**0.5, g0r * 2**0.5, g1r * 2**0.5
|
|
177
|
+
n_levels = len(coeffs)
|
|
178
|
+
output = coeffs[0][0].clone()
|
|
179
|
+
H, W = output.shape[-2:]
|
|
180
|
+
for j in range(n_levels):
|
|
181
|
+
step = 2 ** (n_levels - j - 1)
|
|
182
|
+
cH, cV, cD = coeffs[j][1]
|
|
183
|
+
for fh in range(step):
|
|
184
|
+
for fw in range(step):
|
|
185
|
+
approx = output.clone()
|
|
186
|
+
out_slice = output[..., fh::step, fw::step]
|
|
187
|
+
out_slice.zero_()
|
|
188
|
+
for oh in (0, 1):
|
|
189
|
+
for ow in (0, 1):
|
|
190
|
+
h_eo = slice(fh + oh * step, H, 2 * step)
|
|
191
|
+
w_eo = slice(fw + ow * step, W, 2 * step)
|
|
192
|
+
ll = approx[..., h_eo, w_eo]
|
|
193
|
+
lh = cH[..., h_eo, w_eo]
|
|
194
|
+
hl = cV[..., h_eo, w_eo]
|
|
195
|
+
hh = cD[..., h_eo, w_eo]
|
|
196
|
+
lo = dwt_ll.sfb1d(ll, lh, g0c, g1c, "periodization", 2)
|
|
197
|
+
hi = dwt_ll.sfb1d(hl, hh, g0c, g1c, "periodization", 2)
|
|
198
|
+
xr = dwt_ll.sfb1d(lo, hi, g0r, g1r, "periodization", 3)
|
|
199
|
+
if oh:
|
|
200
|
+
xr = torch.roll(xr, 1, dims=-2)
|
|
201
|
+
if ow:
|
|
202
|
+
xr = torch.roll(xr, 1, dims=-1)
|
|
203
|
+
out_slice += xr
|
|
204
|
+
out_slice /= 4
|
|
205
|
+
return output
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class SWT1D(nn.Module):
|
|
209
|
+
"""1-D stationary wavelet transform module (see :func:`swt1d`)."""
|
|
210
|
+
|
|
211
|
+
def __init__(self, wave, level: int, norm: bool = False):
|
|
212
|
+
super().__init__()
|
|
213
|
+
_check_level(level)
|
|
214
|
+
self.wave = wave
|
|
215
|
+
self.level = level
|
|
216
|
+
self.norm = norm
|
|
217
|
+
h0c, h1c, _, _, _, _, _, _ = resolve_2d(wave, torch.float64, torch.device("cpu"))
|
|
218
|
+
self.register_buffer("h0c", h0c)
|
|
219
|
+
self.register_buffer("h1c", h1c)
|
|
220
|
+
|
|
221
|
+
def forward(self, x: torch.Tensor):
|
|
222
|
+
h0c = cast(torch.Tensor, self.h0c).to(dtype=x.dtype, device=x.device)
|
|
223
|
+
h1c = cast(torch.Tensor, self.h1c).to(dtype=x.dtype, device=x.device)
|
|
224
|
+
if self.norm:
|
|
225
|
+
h0c, h1c = h0c / 2**0.5, h1c / 2**0.5
|
|
226
|
+
_check_level(self.level)
|
|
227
|
+
if x.ndim != 3:
|
|
228
|
+
raise ValueError(f"SWT1D expects (N, C, L); got {tuple(x.shape)}")
|
|
229
|
+
_check_divisible(x.shape[-1], self.level, "signal")
|
|
230
|
+
x4 = x.unsqueeze(-1)
|
|
231
|
+
ret = []
|
|
232
|
+
a = x4
|
|
233
|
+
for j in range(1, self.level + 1):
|
|
234
|
+
fstep = 2 ** (j - 1)
|
|
235
|
+
ca = swt_axis(a, h0c, 2, fstep)
|
|
236
|
+
cd = swt_axis(a, h1c, 2, fstep)
|
|
237
|
+
ret.append((ca.squeeze(-1), cd.squeeze(-1)))
|
|
238
|
+
a = ca
|
|
239
|
+
ret.reverse()
|
|
240
|
+
return ret
|
|
241
|
+
|
|
242
|
+
def extra_repr(self) -> str:
|
|
243
|
+
return f"wave={self.wave!r}, level={self.level}, norm={self.norm}"
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
class ISWT1D(nn.Module):
|
|
247
|
+
"""1-D inverse stationary wavelet transform module (see :func:`iswt1d`)."""
|
|
248
|
+
|
|
249
|
+
def __init__(self, wave, norm: bool = False):
|
|
250
|
+
super().__init__()
|
|
251
|
+
self.wave = wave
|
|
252
|
+
self.norm = norm
|
|
253
|
+
_, _, _, _, _, _, g0r, g1r = resolve_2d(wave, torch.float64, torch.device("cpu"))
|
|
254
|
+
self.register_buffer("g0r", g0r)
|
|
255
|
+
self.register_buffer("g1r", g1r)
|
|
256
|
+
|
|
257
|
+
def forward(self, coeffs):
|
|
258
|
+
g0r = self.g0r.to(dtype=coeffs[0][0].dtype, device=coeffs[0][0].device)
|
|
259
|
+
g1r = self.g1r.to(dtype=coeffs[0][0].dtype, device=coeffs[0][0].device)
|
|
260
|
+
if self.norm:
|
|
261
|
+
g0r, g1r = g0r * 2**0.5, g1r * 2**0.5
|
|
262
|
+
n_levels = len(coeffs)
|
|
263
|
+
output = coeffs[0][0].clone()
|
|
264
|
+
N = output.shape[-1]
|
|
265
|
+
for j in range(n_levels, 0, -1):
|
|
266
|
+
step = 2 ** (j - 1)
|
|
267
|
+
cD = coeffs[-j][1]
|
|
268
|
+
for first in range(step):
|
|
269
|
+
idx = torch.arange(first, N, step, device=output.device)
|
|
270
|
+
even = idx[0::2]
|
|
271
|
+
odd = idx[1::2]
|
|
272
|
+
x1 = _iswt_idwt(
|
|
273
|
+
output.index_select(-1, even),
|
|
274
|
+
cD.index_select(-1, even),
|
|
275
|
+
g0r,
|
|
276
|
+
g1r,
|
|
277
|
+
)
|
|
278
|
+
x2 = _iswt_idwt(
|
|
279
|
+
output.index_select(-1, odd),
|
|
280
|
+
cD.index_select(-1, odd),
|
|
281
|
+
g0r,
|
|
282
|
+
g1r,
|
|
283
|
+
)
|
|
284
|
+
x2 = torch.roll(x2, 1, dims=-1)
|
|
285
|
+
output[..., idx] = (x1 + x2) / 2
|
|
286
|
+
return output
|
|
287
|
+
|
|
288
|
+
def extra_repr(self) -> str:
|
|
289
|
+
return f"wave={self.wave!r}, norm={self.norm}"
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
class SWT2D(nn.Module):
|
|
293
|
+
"""2-D stationary wavelet transform module (see :func:`swt2d`)."""
|
|
294
|
+
|
|
295
|
+
def __init__(self, wave, level: int, norm: bool = False):
|
|
296
|
+
super().__init__()
|
|
297
|
+
_check_level(level)
|
|
298
|
+
self.wave = wave
|
|
299
|
+
self.level = level
|
|
300
|
+
self.norm = norm
|
|
301
|
+
h0c, h1c, h0r, h1r, _, _, _, _ = resolve_2d(wave, torch.float64, torch.device("cpu"))
|
|
302
|
+
for name, t in (("h0c", h0c), ("h1c", h1c), ("h0r", h0r), ("h1r", h1r)):
|
|
303
|
+
self.register_buffer(name, t)
|
|
304
|
+
|
|
305
|
+
def forward(self, x: torch.Tensor):
|
|
306
|
+
if x.ndim != 4:
|
|
307
|
+
raise ValueError(f"SWT2D expects (N, C, H, W); got {tuple(x.shape)}")
|
|
308
|
+
_check_divisible(x.shape[-2], self.level, "height")
|
|
309
|
+
_check_divisible(x.shape[-1], self.level, "width")
|
|
310
|
+
f = {
|
|
311
|
+
n: getattr(self, n).to(dtype=x.dtype, device=x.device)
|
|
312
|
+
for n in ("h0c", "h1c", "h0r", "h1r")
|
|
313
|
+
}
|
|
314
|
+
if self.norm:
|
|
315
|
+
for n in f:
|
|
316
|
+
f[n] = f[n] / 2**0.5
|
|
317
|
+
ret = []
|
|
318
|
+
a = x
|
|
319
|
+
for j in range(1, self.level + 1):
|
|
320
|
+
fstep = 2 ** (j - 1)
|
|
321
|
+
low = swt_axis(a, f["h0c"], 2, fstep)
|
|
322
|
+
high = swt_axis(a, f["h1c"], 2, fstep)
|
|
323
|
+
aa = swt_axis(low, f["h0r"], 3, fstep)
|
|
324
|
+
ad = swt_axis(low, f["h1r"], 3, fstep)
|
|
325
|
+
da = swt_axis(high, f["h0r"], 3, fstep)
|
|
326
|
+
dd = swt_axis(high, f["h1r"], 3, fstep)
|
|
327
|
+
ret.append((aa, (da, ad, dd)))
|
|
328
|
+
a = aa
|
|
329
|
+
ret.reverse()
|
|
330
|
+
return ret
|
|
331
|
+
|
|
332
|
+
def extra_repr(self) -> str:
|
|
333
|
+
return f"wave={self.wave!r}, level={self.level}, norm={self.norm}"
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
class ISWT2D(nn.Module):
|
|
337
|
+
"""2-D inverse stationary wavelet transform module (see :func:`iswt2d`)."""
|
|
338
|
+
|
|
339
|
+
def __init__(self, wave, norm: bool = False):
|
|
340
|
+
super().__init__()
|
|
341
|
+
self.wave = wave
|
|
342
|
+
self.norm = norm
|
|
343
|
+
_, _, _, _, g0c, g1c, g0r, g1r = resolve_2d(wave, torch.float64, torch.device("cpu"))
|
|
344
|
+
for name, t in (("g0c", g0c), ("g1c", g1c), ("g0r", g0r), ("g1r", g1r)):
|
|
345
|
+
self.register_buffer(name, t)
|
|
346
|
+
|
|
347
|
+
def forward(self, coeffs):
|
|
348
|
+
f = {
|
|
349
|
+
n: getattr(self, n).to(dtype=coeffs[0][0].dtype, device=coeffs[0][0].device)
|
|
350
|
+
for n in ("g0c", "g1c", "g0r", "g1r")
|
|
351
|
+
}
|
|
352
|
+
if self.norm:
|
|
353
|
+
for n in f:
|
|
354
|
+
f[n] = f[n] * 2**0.5
|
|
355
|
+
n_levels = len(coeffs)
|
|
356
|
+
output = coeffs[0][0].clone()
|
|
357
|
+
H, W = output.shape[-2:]
|
|
358
|
+
for j in range(n_levels):
|
|
359
|
+
step = 2 ** (n_levels - j - 1)
|
|
360
|
+
cH, cV, cD = coeffs[j][1]
|
|
361
|
+
for fh in range(step):
|
|
362
|
+
for fw in range(step):
|
|
363
|
+
approx = output.clone()
|
|
364
|
+
out_slice = output[..., fh::step, fw::step]
|
|
365
|
+
out_slice.zero_()
|
|
366
|
+
for oh in (0, 1):
|
|
367
|
+
for ow in (0, 1):
|
|
368
|
+
h_eo = slice(fh + oh * step, H, 2 * step)
|
|
369
|
+
w_eo = slice(fw + ow * step, W, 2 * step)
|
|
370
|
+
ll = approx[..., h_eo, w_eo]
|
|
371
|
+
lh = cH[..., h_eo, w_eo]
|
|
372
|
+
hl = cV[..., h_eo, w_eo]
|
|
373
|
+
hh = cD[..., h_eo, w_eo]
|
|
374
|
+
lo = dwt_ll.sfb1d(ll, lh, f["g0c"], f["g1c"], "periodization", 2)
|
|
375
|
+
hi = dwt_ll.sfb1d(hl, hh, f["g0c"], f["g1c"], "periodization", 2)
|
|
376
|
+
xr = dwt_ll.sfb1d(lo, hi, f["g0r"], f["g1r"], "periodization", 3)
|
|
377
|
+
if oh:
|
|
378
|
+
xr = torch.roll(xr, 1, dims=-2)
|
|
379
|
+
if ow:
|
|
380
|
+
xr = torch.roll(xr, 1, dims=-1)
|
|
381
|
+
out_slice += xr
|
|
382
|
+
out_slice /= 4
|
|
383
|
+
return output
|
|
384
|
+
|
|
385
|
+
def extra_repr(self) -> str:
|
|
386
|
+
return f"wave={self.wave!r}, norm={self.norm}"
|