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,420 @@
|
|
|
1
|
+
"""3-D dual-tree complex wavelet transform (functional and module API).
|
|
2
|
+
|
|
3
|
+
The 3-D DTCWT (Kingsbury's design, as in the ``dtcwt`` numpy package) runs
|
|
4
|
+
four separable trees with the level-1 biorthogonal and level-2+
|
|
5
|
+
quarter-sample-shift filters on a doubled work grid and combines the octant
|
|
6
|
+
outputs into 28 complex orientation subbands per level (7 ``(H/L)`` face
|
|
7
|
+
combinations x 4 subbands each).
|
|
8
|
+
|
|
9
|
+
Precision follows the input (float32 -> complex64, float64 -> complex128);
|
|
10
|
+
half inputs are computed in float32. Boundary handling is Kingsbury's
|
|
11
|
+
symmetric extension (``ext_mode=4``: each input dimension must be even;
|
|
12
|
+
higher levels are edge-padded to a multiple of 4).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import torch
|
|
18
|
+
import torch.nn as nn
|
|
19
|
+
|
|
20
|
+
from ..dwt._validation import check_spatial
|
|
21
|
+
from ._autocast import autocast_off
|
|
22
|
+
from .coeffs import biort as _biort
|
|
23
|
+
from .coeffs import qshift as _qshift
|
|
24
|
+
from .lowlevel import coldfilt, colfilter, colifilt
|
|
25
|
+
from .transform2d import _compute_dtype, _prep_filt, _validate_names
|
|
26
|
+
|
|
27
|
+
__all__ = ["dtcwt3d", "idtcwt3d", "DTCWT3D", "IDTCWT3D"]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _axis_fn(x: torch.Tensor, fn, axis: int):
|
|
31
|
+
"""Apply ``fn`` (filters the dim-2 of a 4-D view) along ``axis`` of 5-D."""
|
|
32
|
+
order = [0, 1, axis] + [i for i in (2, 3, 4) if i != axis]
|
|
33
|
+
xp = x.permute(order)
|
|
34
|
+
shp = xp.shape
|
|
35
|
+
y = fn(xp.reshape(shp[0], shp[1], shp[2], shp[3] * shp[4]))
|
|
36
|
+
out_shp = list(shp)
|
|
37
|
+
out_shp[2] = y.shape[2] # the filtered dim may change size
|
|
38
|
+
y = y.reshape(out_shp)
|
|
39
|
+
inv = [0, 1] + [order.index(i) for i in (2, 3, 4)]
|
|
40
|
+
return y.permute(inv)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _colfilter_axis(x, h, axis):
|
|
44
|
+
return _axis_fn(x, lambda t: colfilter(t, h), axis)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _coldfilt_axis(x, ha, hb, highpass, axis):
|
|
48
|
+
return _axis_fn(x, lambda t: coldfilt(t, ha, hb, highpass), axis)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _colifilt_axis(x, ha, hb, highpass, axis):
|
|
52
|
+
return _axis_fn(x, lambda t: colifilt(t, ha, hb, highpass), axis)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _pad_to_multiple(x, mult: int) -> torch.Tensor:
|
|
56
|
+
for axis in (2, 3, 4):
|
|
57
|
+
n = x.shape[axis]
|
|
58
|
+
if n % mult != 0:
|
|
59
|
+
before = (
|
|
60
|
+
x[..., :1, :, :]
|
|
61
|
+
if axis == 2
|
|
62
|
+
else x[..., :, :1, :]
|
|
63
|
+
if axis == 3
|
|
64
|
+
else x[..., :, :, :1]
|
|
65
|
+
)
|
|
66
|
+
after = (
|
|
67
|
+
x[..., -1:, :, :]
|
|
68
|
+
if axis == 2
|
|
69
|
+
else x[..., :, -1:, :]
|
|
70
|
+
if axis == 3
|
|
71
|
+
else x[..., :, :, -1:]
|
|
72
|
+
)
|
|
73
|
+
x = torch.cat((before, x, after), dim=axis)
|
|
74
|
+
return x
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _cube2c(y: torch.Tensor) -> torch.Tensor:
|
|
78
|
+
"""Octant (quad-tree) -> 4 complex subbands (numpy dtcwt cube2c)."""
|
|
79
|
+
A = y[..., 0::2, 0::2, 0::2]
|
|
80
|
+
B = y[..., 0::2, 1::2, 0::2]
|
|
81
|
+
C = y[..., 1::2, 0::2, 0::2]
|
|
82
|
+
D = y[..., 1::2, 1::2, 0::2]
|
|
83
|
+
E = y[..., 0::2, 0::2, 1::2]
|
|
84
|
+
F = y[..., 0::2, 1::2, 1::2]
|
|
85
|
+
G = y[..., 1::2, 0::2, 1::2]
|
|
86
|
+
H = y[..., 1::2, 1::2, 1::2]
|
|
87
|
+
p = 0.5 * (A - G - D - F) + 0.5j * (B - H + C + E)
|
|
88
|
+
q = 0.5 * (A - G + D + F) + 0.5j * (-B + H + C + E)
|
|
89
|
+
r = 0.5 * (A + G + D - F) + 0.5j * (B + H - C + E)
|
|
90
|
+
s = 0.5 * (A + G - D + F) + 0.5j * (-B - H - C + E)
|
|
91
|
+
return torch.stack((p, q, r, s), dim=-1)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _c2cube(z: torch.Tensor) -> torch.Tensor:
|
|
95
|
+
"""Inverse of :func:`_cube2c` (numpy dtcwt c2cube)."""
|
|
96
|
+
p, q, r, s = torch.unbind(z, dim=-1)
|
|
97
|
+
pr, pi = p.real, p.imag
|
|
98
|
+
qr, qi = q.real, q.imag
|
|
99
|
+
rr, ri = r.real, r.imag
|
|
100
|
+
sr, si = s.real, s.imag
|
|
101
|
+
shp = z.shape[:-1]
|
|
102
|
+
y = torch.zeros(shp[:2] + tuple(2 * s for s in shp[2:]), dtype=pr.dtype, device=z.device)
|
|
103
|
+
y[..., 0::2, 0::2, 0::2] = pr + qr + rr + sr
|
|
104
|
+
y[..., 1::2, 0::2, 1::2] = -pr - qr + rr + sr
|
|
105
|
+
y[..., 1::2, 1::2, 0::2] = -pr + qr + rr - sr
|
|
106
|
+
y[..., 0::2, 1::2, 1::2] = -pr + qr - rr + sr
|
|
107
|
+
y[..., 0::2, 1::2, 0::2] = pi - qi + ri - si
|
|
108
|
+
y[..., 1::2, 1::2, 1::2] = -pi + qi + ri - si
|
|
109
|
+
y[..., 1::2, 0::2, 0::2] = pi + qi - ri - si
|
|
110
|
+
y[..., 0::2, 0::2, 1::2] = pi + qi + ri + si
|
|
111
|
+
return y * 0.5
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _resolve(biort, qshift, dtype, device):
|
|
115
|
+
_validate_names(biort, qshift)
|
|
116
|
+
h0o, g0o, h1o, g1o = _biort(biort)
|
|
117
|
+
h0a, h0b, g0a, g0b, h1a, h1b, g1a, g1b = _qshift(qshift)
|
|
118
|
+
return {
|
|
119
|
+
"h0o": _prep_filt(h0o, dtype, device),
|
|
120
|
+
"g0o": _prep_filt(g0o, dtype, device),
|
|
121
|
+
"h1o": _prep_filt(h1o, dtype, device),
|
|
122
|
+
"g1o": _prep_filt(g1o, dtype, device),
|
|
123
|
+
"h0a": _prep_filt(h0a, dtype, device),
|
|
124
|
+
"h0b": _prep_filt(h0b, dtype, device),
|
|
125
|
+
"h1a": _prep_filt(h1a, dtype, device),
|
|
126
|
+
"h1b": _prep_filt(h1b, dtype, device),
|
|
127
|
+
"g0a": _prep_filt(g0a, dtype, device),
|
|
128
|
+
"g0b": _prep_filt(g0b, dtype, device),
|
|
129
|
+
"g1a": _prep_filt(g1a, dtype, device),
|
|
130
|
+
"g1b": _prep_filt(g1b, dtype, device),
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _level1_xfm(x, h0o, h1o):
|
|
135
|
+
D, H, W = x.shape[2:]
|
|
136
|
+
L = h0o.numel()
|
|
137
|
+
d2, h2, w2 = 2 * D, 2 * H, 2 * W
|
|
138
|
+
if L % 2 == 0:
|
|
139
|
+
d2, h2, w2 = d2 + 2, h2 + 2, w2 + 2
|
|
140
|
+
work = x.new_zeros((x.shape[0], x.shape[1], d2, h2, w2))
|
|
141
|
+
if L % 2 == 0:
|
|
142
|
+
work[..., :D, :H, :W] = x
|
|
143
|
+
work[..., D, :H, :W] = x[..., -1:, :, :]
|
|
144
|
+
work[..., :D, H, :W] = x[..., :, -1:, :]
|
|
145
|
+
work[..., :D, :H, W] = x[..., :, :, -1:]
|
|
146
|
+
work[..., D, H, W] = x[..., -1:, -1:, -1:]
|
|
147
|
+
else:
|
|
148
|
+
work[..., :D, :H, :W] = x
|
|
149
|
+
|
|
150
|
+
# 1: filter along W (first-D x first-H block; fills both W halves)
|
|
151
|
+
blk = work[..., :D, :H, :W]
|
|
152
|
+
work[..., :D, :H, W:] = _colfilter_axis(blk, h1o, 4)
|
|
153
|
+
work[..., :D, :H, :W] = _colfilter_axis(blk, h0o, 4)
|
|
154
|
+
|
|
155
|
+
# 2: filter along H (into 2H), then along D
|
|
156
|
+
blk2 = work[..., :D, :H, :]
|
|
157
|
+
y2 = torch.cat((_colfilter_axis(blk2, h0o, 3), _colfilter_axis(blk2, h1o, 3)), dim=3)
|
|
158
|
+
work[..., :D, :, :] = _colfilter_axis(y2, h0o, 2)
|
|
159
|
+
work[..., D:, :, :] = _colfilter_axis(y2, h1o, 2)
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
work[..., :D, :H, :W],
|
|
163
|
+
torch.cat(
|
|
164
|
+
(
|
|
165
|
+
_cube2c(work[..., :D, H : 2 * H, :W]),
|
|
166
|
+
_cube2c(work[..., D : 2 * D, :H, :W]),
|
|
167
|
+
_cube2c(work[..., D : 2 * D, H : 2 * H, :W]),
|
|
168
|
+
_cube2c(work[..., :D, :H, W : 2 * W]),
|
|
169
|
+
_cube2c(work[..., :D, H : 2 * H, W : 2 * W]),
|
|
170
|
+
_cube2c(work[..., D : 2 * D, :H, W : 2 * W]),
|
|
171
|
+
_cube2c(work[..., D : 2 * D, H : 2 * H, W : 2 * W]),
|
|
172
|
+
),
|
|
173
|
+
dim=-1,
|
|
174
|
+
),
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _level2_xfm(x, h0a, h0b, h1a, h1b):
|
|
179
|
+
x = _pad_to_multiple(x, 4)
|
|
180
|
+
D, H, W = x.shape[2:]
|
|
181
|
+
|
|
182
|
+
# 1: filter along W (full D, H); both outputs from the original input
|
|
183
|
+
hi_w = _coldfilt_axis(x, h1b, h1a, True, 4)
|
|
184
|
+
lo_w = _coldfilt_axis(x, h0b, h0a, False, 4)
|
|
185
|
+
work = torch.cat((lo_w, hi_w), dim=4)
|
|
186
|
+
|
|
187
|
+
# 2: filter along H, concatenate, then along D
|
|
188
|
+
a = _coldfilt_axis(work, h0b, h0a, False, 3)
|
|
189
|
+
b = _coldfilt_axis(work, h1b, h1a, True, 3)
|
|
190
|
+
y2 = torch.cat((a, b), dim=3)
|
|
191
|
+
work = torch.cat(
|
|
192
|
+
(
|
|
193
|
+
_coldfilt_axis(y2, h0b, h0a, False, 2),
|
|
194
|
+
_coldfilt_axis(y2, h1b, h1a, True, 2),
|
|
195
|
+
),
|
|
196
|
+
dim=2,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
D2, H2, W2 = work.shape[2:]
|
|
200
|
+
s0a = slice(0, D2 >> 1)
|
|
201
|
+
s0b = slice(D2 >> 1, None)
|
|
202
|
+
s1a = slice(0, H2 >> 1)
|
|
203
|
+
s1b = slice(H2 >> 1, None)
|
|
204
|
+
s2a = slice(0, W2 >> 1)
|
|
205
|
+
s2b = slice(W2 >> 1, None)
|
|
206
|
+
return (
|
|
207
|
+
work[..., s0a, s1a, s2a],
|
|
208
|
+
torch.cat(
|
|
209
|
+
(
|
|
210
|
+
_cube2c(work[..., s0a, s1b, s2a]),
|
|
211
|
+
_cube2c(work[..., s0b, s1a, s2a]),
|
|
212
|
+
_cube2c(work[..., s0b, s1b, s2a]),
|
|
213
|
+
_cube2c(work[..., s0a, s1a, s2b]),
|
|
214
|
+
_cube2c(work[..., s0a, s1b, s2b]),
|
|
215
|
+
_cube2c(work[..., s0b, s1a, s2b]),
|
|
216
|
+
_cube2c(work[..., s0b, s1b, s2b]),
|
|
217
|
+
),
|
|
218
|
+
dim=-1,
|
|
219
|
+
),
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _level1_ifm(yl, yh, g0o, g1o):
|
|
224
|
+
D, H, W = yl.shape[2:]
|
|
225
|
+
L = g0o.numel()
|
|
226
|
+
d2, h2, w2 = 2 * D, 2 * H, 2 * W
|
|
227
|
+
if L % 2 == 0:
|
|
228
|
+
d2, h2, w2 = d2 + 2, h2 + 2, w2 + 2
|
|
229
|
+
work = yl.new_zeros((yl.shape[0], yl.shape[1], d2, h2, w2))
|
|
230
|
+
work[..., :D, :H, :W] = yl
|
|
231
|
+
work[..., :D, H : 2 * H, :W] = _c2cube(yh[..., 0:4])
|
|
232
|
+
work[..., D : 2 * D, :H, :W] = _c2cube(yh[..., 4:8])
|
|
233
|
+
work[..., D : 2 * D, H : 2 * H, :W] = _c2cube(yh[..., 8:12])
|
|
234
|
+
work[..., :D, :H, W : 2 * W] = _c2cube(yh[..., 12:16])
|
|
235
|
+
work[..., :D, H : 2 * H, W : 2 * W] = _c2cube(yh[..., 16:20])
|
|
236
|
+
work[..., D : 2 * D, :H, W : 2 * W] = _c2cube(yh[..., 20:24])
|
|
237
|
+
work[..., D : 2 * D, H : 2 * H, W : 2 * W] = _c2cube(yh[..., 24:28])
|
|
238
|
+
|
|
239
|
+
# rows (H): filter both H halves along H, then combine along D
|
|
240
|
+
y = _colfilter_axis(work[..., :, :H, :], g0o, 3) + _colfilter_axis(work[..., :, H:, :], g1o, 3)
|
|
241
|
+
work[..., :D, :H, :] = _colfilter_axis(y[..., :D, :, :], g0o, 2) + _colfilter_axis(
|
|
242
|
+
y[..., D:, :, :], g1o, 2
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
# depth (D): filter both D halves along W, combine into the first W half
|
|
246
|
+
z = _colfilter_axis(work[..., :D, :H, :W], g0o, 4) + _colfilter_axis(
|
|
247
|
+
work[..., :D, :H, W:], g1o, 4
|
|
248
|
+
)
|
|
249
|
+
work[..., :D, :H, :W] = z
|
|
250
|
+
|
|
251
|
+
if L % 2 == 0:
|
|
252
|
+
return work[..., 1 : d2 >> 1, 1 : h2 >> 1, 1 : w2 >> 1]
|
|
253
|
+
return work[..., :D, :H, :W]
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _level2_ifm(yl, yh, g0a, g0b, g1a, g1b, prev_level_size):
|
|
257
|
+
D, H, W = yl.shape[2:]
|
|
258
|
+
work = yl.new_zeros((yl.shape[0], yl.shape[1], 2 * D, 2 * H, 2 * W))
|
|
259
|
+
work[..., :D, :H, :W] = yl
|
|
260
|
+
work[..., :D, H:, :W] = _c2cube(yh[..., 0:4])
|
|
261
|
+
work[..., D:, :H, :W] = _c2cube(yh[..., 4:8])
|
|
262
|
+
work[..., D:, H:, :W] = _c2cube(yh[..., 8:12])
|
|
263
|
+
work[..., :D, :H, W:] = _c2cube(yh[..., 12:16])
|
|
264
|
+
work[..., :D, H:, W:] = _c2cube(yh[..., 16:20])
|
|
265
|
+
work[..., D:, :H, W:] = _c2cube(yh[..., 20:24])
|
|
266
|
+
work[..., D:, H:, W:] = _c2cube(yh[..., 24:28])
|
|
267
|
+
|
|
268
|
+
# rows (H): combine the H halves, then the D halves
|
|
269
|
+
y = _colifilt_axis(work[..., :, :H, :], g0b, g0a, False, 3) + _colifilt_axis(
|
|
270
|
+
work[..., :, H:, :], g1b, g1a, True, 3
|
|
271
|
+
)
|
|
272
|
+
work = _colifilt_axis(y[..., :D, :, :], g0b, g0a, False, 2) + _colifilt_axis(
|
|
273
|
+
y[..., D:, :, :], g1b, g1a, True, 2
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
# depth (D): combine the W halves
|
|
277
|
+
work = _colifilt_axis(work[..., :, :, :W], g0b, g0a, False, 4) + _colifilt_axis(
|
|
278
|
+
work[..., :, :, W:], g1b, g1a, True, 4
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
if 2 * yh.shape[2] != prev_level_size[0]:
|
|
282
|
+
work = work[..., 1:-1, :, :]
|
|
283
|
+
if 2 * yh.shape[3] != prev_level_size[1]:
|
|
284
|
+
work = work[..., :, 1:-1, :]
|
|
285
|
+
if 2 * yh.shape[4] != prev_level_size[2]:
|
|
286
|
+
work = work[..., :, :, 1:-1]
|
|
287
|
+
return work
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _dtcwt3d_impl(x, f, J):
|
|
291
|
+
yh = []
|
|
292
|
+
yl = x
|
|
293
|
+
for level in range(J):
|
|
294
|
+
if level == 0:
|
|
295
|
+
yl, h = _level1_xfm(yl, f["h0o"], f["h1o"])
|
|
296
|
+
else:
|
|
297
|
+
yl, h = _level2_xfm(yl, f["h0a"], f["h0b"], f["h1a"], f["h1b"])
|
|
298
|
+
yh.append(h)
|
|
299
|
+
return yl, yh
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _validate_even3d(dims):
|
|
303
|
+
"""The 3-D work grid requires even spatial dims at every level."""
|
|
304
|
+
check_spatial(dims, ("D", "H", "W"), "dtcwt3d")
|
|
305
|
+
bad = [d for d in dims if d % 2 != 0]
|
|
306
|
+
if bad:
|
|
307
|
+
raise ValueError(
|
|
308
|
+
"dtcwt3d requires even spatial dimensions (the doubled work grid "
|
|
309
|
+
f"needs even sizes); got {tuple(dims)}"
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _idtcwt3d_impl(yl, yh, f):
|
|
314
|
+
J = len(yh)
|
|
315
|
+
for level in range(J - 1, 0, -1):
|
|
316
|
+
prev = yh[level - 1].shape[2:5]
|
|
317
|
+
yl = _level2_ifm(yl, yh[level], f["g0a"], f["g0b"], f["g1a"], f["g1b"], prev)
|
|
318
|
+
yl = _level1_ifm(yl, yh[0], f["g0o"], f["g1o"])
|
|
319
|
+
return yl
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def dtcwt3d(
|
|
323
|
+
x: torch.Tensor, biort="near_sym_a", qshift="qshift_a", J=3
|
|
324
|
+
) -> tuple[torch.Tensor, list[torch.Tensor] | None]:
|
|
325
|
+
"""Forward 3-D DTCWT.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
x (torch.Tensor): ``(N, C, D, H, W)`` input with even spatial dims.
|
|
329
|
+
biort (str): filter bank name.
|
|
330
|
+
qshift (str): filter bank name.
|
|
331
|
+
J (int): number of levels.
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
tuple[torch.Tensor, list[torch.Tensor]]: ``(yl, yh)``: real lowpass
|
|
335
|
+
``(N, C, D', H', W')`` and a list of ``J`` complex bandpasses
|
|
336
|
+
``(N, C, D'', H'', W'', 28)`` (finest first).
|
|
337
|
+
"""
|
|
338
|
+
if x.ndim != 5:
|
|
339
|
+
raise ValueError(f"dtcwt3d expects (N, C, D, H, W); got {tuple(x.shape)}")
|
|
340
|
+
if isinstance(J, bool) or not isinstance(J, int) or J < 0:
|
|
341
|
+
raise ValueError(f"J must be a non-negative int; got {J!r}")
|
|
342
|
+
if J == 0:
|
|
343
|
+
return x, None
|
|
344
|
+
_validate_even3d(x.shape[2:])
|
|
345
|
+
dtype = _compute_dtype(x)
|
|
346
|
+
f = _resolve(biort, qshift, dtype, x.device)
|
|
347
|
+
with autocast_off(x.device):
|
|
348
|
+
return _dtcwt3d_impl(x.to(dtype), f, J)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def idtcwt3d(coeffs, biort="near_sym_a", qshift="qshift_a") -> torch.Tensor:
|
|
352
|
+
"""Inverse 3-D DTCWT (exact inverse of :func:`dtcwt3d`)."""
|
|
353
|
+
yl, yh = coeffs
|
|
354
|
+
if yl is None:
|
|
355
|
+
raise ValueError("The lowpass coefficient tensor yl cannot be None")
|
|
356
|
+
dtype = _compute_dtype(yl)
|
|
357
|
+
f = _resolve(biort, qshift, dtype, yl.device)
|
|
358
|
+
with autocast_off(yl.device):
|
|
359
|
+
return _idtcwt3d_impl(yl.to(dtype), yh, f)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
class DTCWT3D(nn.Module):
|
|
363
|
+
"""3-D DTCWT module (see :func:`dtcwt3d`)."""
|
|
364
|
+
|
|
365
|
+
def __init__(self, biort="near_sym_a", qshift="qshift_a", J=3):
|
|
366
|
+
super().__init__()
|
|
367
|
+
if isinstance(J, bool) or not isinstance(J, int) or J < 0:
|
|
368
|
+
raise ValueError(f"J must be a non-negative int; got {J!r}")
|
|
369
|
+
self.biort = biort
|
|
370
|
+
self.qshift = qshift
|
|
371
|
+
self.J = J
|
|
372
|
+
filts = _resolve(biort, qshift, torch.float64, torch.device("cpu"))
|
|
373
|
+
for name in ("h0o", "h1o", "h0a", "h1a", "h0b", "h1b"):
|
|
374
|
+
self.register_buffer(name, filts[name])
|
|
375
|
+
|
|
376
|
+
def forward(self, x: torch.Tensor):
|
|
377
|
+
if x.ndim != 5:
|
|
378
|
+
raise ValueError(f"DTCWT3D expects (N, C, D, H, W); got {tuple(x.shape)}")
|
|
379
|
+
if self.J == 0:
|
|
380
|
+
return x, None
|
|
381
|
+
_validate_even3d(x.shape[2:])
|
|
382
|
+
dtype = _compute_dtype(x)
|
|
383
|
+
f = {
|
|
384
|
+
n: getattr(self, n).to(dtype=dtype, device=x.device)
|
|
385
|
+
for n in ("h0o", "h1o", "h0a", "h1a", "h0b", "h1b")
|
|
386
|
+
}
|
|
387
|
+
with autocast_off(x.device):
|
|
388
|
+
return _dtcwt3d_impl(x.to(dtype), f, self.J)
|
|
389
|
+
|
|
390
|
+
def extra_repr(self) -> str:
|
|
391
|
+
return f"biort={self.biort!r}, qshift={self.qshift!r}, J={self.J}"
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
class IDTCWT3D(nn.Module):
|
|
395
|
+
"""3-D inverse DTCWT module (see :func:`idtcwt3d`)."""
|
|
396
|
+
|
|
397
|
+
def __init__(self, biort="near_sym_a", qshift="qshift_a"):
|
|
398
|
+
super().__init__()
|
|
399
|
+
self.biort = biort
|
|
400
|
+
self.qshift = qshift
|
|
401
|
+
filts = _resolve(biort, qshift, torch.float64, torch.device("cpu"))
|
|
402
|
+
for name in ("g0o", "g1o", "g0a", "g1a", "g0b", "g1b"):
|
|
403
|
+
self.register_buffer(name, filts[name])
|
|
404
|
+
|
|
405
|
+
def forward(self, coeffs):
|
|
406
|
+
yl, yh = coeffs
|
|
407
|
+
if yl is None:
|
|
408
|
+
raise ValueError("The lowpass coefficient tensor yl cannot be None")
|
|
409
|
+
if yl.ndim != 5:
|
|
410
|
+
raise ValueError(f"IDTCWT3D expects 5-D coefficients; got shape {tuple(yl.shape)}")
|
|
411
|
+
dtype = _compute_dtype(yl)
|
|
412
|
+
f = {
|
|
413
|
+
n: getattr(self, n).to(dtype=dtype, device=yl.device)
|
|
414
|
+
for n in ("g0o", "g1o", "g0a", "g1a", "g0b", "g1b")
|
|
415
|
+
}
|
|
416
|
+
with autocast_off(yl.device):
|
|
417
|
+
return _idtcwt3d_impl(yl.to(dtype), yh, f)
|
|
418
|
+
|
|
419
|
+
def extra_repr(self) -> str:
|
|
420
|
+
return f"biort={self.biort!r}, qshift={self.qshift!r}"
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Single-scale DTCWT stages (plain functions, native complex tensors)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import torch
|
|
6
|
+
|
|
7
|
+
from .lowlevel import (
|
|
8
|
+
coldfilt,
|
|
9
|
+
colfilter,
|
|
10
|
+
colifilt,
|
|
11
|
+
highs_to_orientations,
|
|
12
|
+
orientations_to_highs,
|
|
13
|
+
rowdfilt,
|
|
14
|
+
rowfilter,
|
|
15
|
+
rowifilt,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = ["get_dimensions5", "fwd_j1", "inv_j1", "fwd_j2plus", "inv_j2plus"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_dimensions5(o_dim: int) -> tuple[int, int, int]:
|
|
22
|
+
"""Map ``o_dim`` to ``(o_dim, h_dim, w_dim)`` for 5-D complex tensors
|
|
23
|
+
``(N, C, O, H, W)``."""
|
|
24
|
+
o_dim = o_dim % 5
|
|
25
|
+
if o_dim == 4:
|
|
26
|
+
h_dim, w_dim = 2, 3
|
|
27
|
+
elif o_dim == 3:
|
|
28
|
+
h_dim, w_dim = 2, 4
|
|
29
|
+
else:
|
|
30
|
+
h_dim, w_dim = 3, 4
|
|
31
|
+
return o_dim, h_dim, w_dim
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def fwd_j1(x: torch.Tensor, h0o, h1o, skip_hps: bool, o_dim: int):
|
|
35
|
+
"""Level-1 forward DTCWT (biorthogonal filters)."""
|
|
36
|
+
if not skip_hps:
|
|
37
|
+
lo = rowfilter(x, h0o)
|
|
38
|
+
hi = rowfilter(x, h1o)
|
|
39
|
+
ll = colfilter(lo, h0o)
|
|
40
|
+
lh = colfilter(lo, h1o)
|
|
41
|
+
hl = colfilter(hi, h0o)
|
|
42
|
+
hh = colfilter(hi, h1o)
|
|
43
|
+
highs = highs_to_orientations(lh, hl, hh, o_dim)
|
|
44
|
+
else:
|
|
45
|
+
ll = colfilter(rowfilter(x, h0o), h0o)
|
|
46
|
+
highs = None
|
|
47
|
+
return ll, highs
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def inv_j1(ll: torch.Tensor, highs, g0o, g1o, o_dim: int, h_dim: int, w_dim: int) -> torch.Tensor:
|
|
51
|
+
"""Level-1 inverse DTCWT."""
|
|
52
|
+
if highs is None:
|
|
53
|
+
return rowfilter(colfilter(ll, g0o), g0o)
|
|
54
|
+
lh, hl, hh = orientations_to_highs(highs, o_dim)
|
|
55
|
+
|
|
56
|
+
# Possibly crop the lowpass to match the bandpass sizes.
|
|
57
|
+
r, c = ll.shape[2:]
|
|
58
|
+
r1, c1 = highs.shape[h_dim], highs.shape[w_dim]
|
|
59
|
+
if r != r1 * 2:
|
|
60
|
+
ll = ll[:, :, 1:-1]
|
|
61
|
+
if c != c1 * 2:
|
|
62
|
+
ll = ll[:, :, :, 1:-1]
|
|
63
|
+
|
|
64
|
+
hi = colfilter(hh, g1o) + colfilter(hl, g0o)
|
|
65
|
+
lo = colfilter(lh, g1o) + colfilter(ll, g0o)
|
|
66
|
+
return rowfilter(hi, g1o) + rowfilter(lo, g0o)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def fwd_j2plus(x: torch.Tensor, h0a, h1a, h0b, h1b, skip_hps: bool, o_dim: int):
|
|
70
|
+
"""Level >= 2 forward DTCWT (quarter-sample-shift filters)."""
|
|
71
|
+
if not skip_hps:
|
|
72
|
+
lo = rowdfilt(x, h0b, h0a, False)
|
|
73
|
+
hi = rowdfilt(x, h1b, h1a, True)
|
|
74
|
+
ll = coldfilt(lo, h0b, h0a, False)
|
|
75
|
+
lh = coldfilt(lo, h1b, h1a, True)
|
|
76
|
+
hl = coldfilt(hi, h0b, h0a, False)
|
|
77
|
+
hh = coldfilt(hi, h1b, h1a, True)
|
|
78
|
+
highs = highs_to_orientations(lh, hl, hh, o_dim)
|
|
79
|
+
else:
|
|
80
|
+
ll = coldfilt(rowdfilt(x, h0b, h0a, False), h0b, h0a, False)
|
|
81
|
+
highs = None
|
|
82
|
+
return ll, highs
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def inv_j2plus(ll: torch.Tensor, highs, g0a, g1a, g0b, g1b, o_dim: int, h_dim: int, w_dim: int):
|
|
86
|
+
"""Level >= 2 inverse DTCWT."""
|
|
87
|
+
if highs is None:
|
|
88
|
+
return rowifilt(colifilt(ll, g0b, g0a, False), g0b, g0a, False)
|
|
89
|
+
lh, hl, hh = orientations_to_highs(highs, o_dim)
|
|
90
|
+
|
|
91
|
+
hi = colifilt(hh, g1b, g1a, True) + colifilt(hl, g0b, g0a, False)
|
|
92
|
+
lo = colifilt(lh, g1b, g1a, True) + colifilt(ll, g0b, g0a, False)
|
|
93
|
+
return rowifilt(hi, g1b, g1a, True) + rowifilt(lo, g0b, g0a, False)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Shared input validation for the transforms.
|
|
2
|
+
|
|
3
|
+
Everything here is pure Python / math on purpose: no PyWavelets call, so the
|
|
4
|
+
module API stays fully traceable under ``torch.compile``.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import math
|
|
10
|
+
import warnings
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def check_spatial(shape: tuple[int, ...], names: tuple[str, ...], where: str) -> None:
|
|
14
|
+
"""Raise a clear error for zero-size spatial dimensions."""
|
|
15
|
+
for name, size in zip(names, shape, strict=True):
|
|
16
|
+
if size == 0:
|
|
17
|
+
raise ValueError(
|
|
18
|
+
f"{where} requires non-empty spatial dimensions; got "
|
|
19
|
+
f"zero-size {name!r} in {tuple(shape)}"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def check_reflect_length(
|
|
24
|
+
shape: tuple[int, ...], names: tuple[str, ...], mode: str, where: str
|
|
25
|
+
) -> None:
|
|
26
|
+
"""Mirror PyWavelets: reflect needs a length > 1 along every transformed axis."""
|
|
27
|
+
if mode != "reflect":
|
|
28
|
+
return
|
|
29
|
+
for name, size in zip(names, shape, strict=True):
|
|
30
|
+
if size == 1:
|
|
31
|
+
raise ValueError(
|
|
32
|
+
f"Input data length must be greater than 1 for [anti]reflect "
|
|
33
|
+
f"mode ({where}: {name!r} has length 1)."
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def validate_level(level: int) -> None:
|
|
38
|
+
if isinstance(level, bool) or not isinstance(level, int) or level < 0:
|
|
39
|
+
raise ValueError(f"level must be a non-negative int; got {level!r}")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def dwt_max_level(data_len: int, filter_len: int) -> int:
|
|
43
|
+
"""Maximum useful decomposition level (matches ``pywt.dwt_max_level``)."""
|
|
44
|
+
if filter_len < 2:
|
|
45
|
+
raise ValueError(f"invalid filter length: {filter_len}")
|
|
46
|
+
if data_len < filter_len - 1:
|
|
47
|
+
return 0
|
|
48
|
+
return int(math.floor(math.log2(data_len / (filter_len - 1))))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def warn_if_too_high(n: int, length: int, level: int) -> None:
|
|
52
|
+
"""Mirror PyWavelets' warning when the level is too high."""
|
|
53
|
+
if level > 0 and level > dwt_max_level(n, length):
|
|
54
|
+
warnings.warn(
|
|
55
|
+
f"Level value of {level} is too high: all coefficients will "
|
|
56
|
+
"experience boundary effects.",
|
|
57
|
+
UserWarning,
|
|
58
|
+
stacklevel=3,
|
|
59
|
+
)
|