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
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"""Low-level dual-tree complex wavelet filter banks.
|
|
2
|
+
|
|
3
|
+
These are the exact Kingsbury DTCWT operations (ported from the original
|
|
4
|
+
MATLAB toolbox via fbcotter/pytorch_wavelets): column/row filtering with
|
|
5
|
+
biorthogonal filters at level 1 and quarter-sample-shift filters at level 2+,
|
|
6
|
+
plus the quad-to-complex conversions. Everything is built from torch ops, so
|
|
7
|
+
gradients flow through all dtypes and ``torch.compile`` works.
|
|
8
|
+
|
|
9
|
+
Boundary handling follows Kingsbury's ``symmetric`` (half-sample) extension.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
import torch
|
|
16
|
+
import torch.nn.functional as F
|
|
17
|
+
|
|
18
|
+
from ..core.modes import pad_indices
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"colfilter",
|
|
22
|
+
"rowfilter",
|
|
23
|
+
"coldfilt",
|
|
24
|
+
"rowdfilt",
|
|
25
|
+
"colifilt",
|
|
26
|
+
"rowifilt",
|
|
27
|
+
"q2c",
|
|
28
|
+
"c2q",
|
|
29
|
+
"highs_to_orientations",
|
|
30
|
+
"orientations_to_highs",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def colfilter(X: torch.Tensor, h: torch.Tensor) -> torch.Tensor:
|
|
35
|
+
"""Column filter (filter along the rows) with decimation by 2."""
|
|
36
|
+
b, ch, row, col = X.shape
|
|
37
|
+
m = h.shape[2] // 2
|
|
38
|
+
xe = pad_indices(row, m, m, "symmetric", X.device)
|
|
39
|
+
return F.conv2d(X[:, :, xe], h.repeat(ch, 1, 1, 1), groups=ch)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def rowfilter(X: torch.Tensor, h: torch.Tensor) -> torch.Tensor:
|
|
43
|
+
"""Row filter (filter along the columns) with decimation by 2.
|
|
44
|
+
|
|
45
|
+
``h`` is a column filter ``(1, 1, L, 1)``; it is transposed internally.
|
|
46
|
+
"""
|
|
47
|
+
b, ch, row, col = X.shape
|
|
48
|
+
m = h.shape[2] // 2
|
|
49
|
+
h = h.transpose(2, 3).contiguous()
|
|
50
|
+
xe = pad_indices(col, m, m, "symmetric", X.device)
|
|
51
|
+
return F.conv2d(X[:, :, :, xe], h.repeat(ch, 1, 1, 1), groups=ch)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def coldfilt(X: torch.Tensor, ha: torch.Tensor, hb: torch.Tensor, highpass: bool) -> torch.Tensor:
|
|
55
|
+
"""Filter two-phase column filter bank with decimation by 2.
|
|
56
|
+
|
|
57
|
+
``X`` must have a row count divisible by 4. Combines the a/b tree filters
|
|
58
|
+
and interleaves their outputs.
|
|
59
|
+
"""
|
|
60
|
+
batch, ch, r, c = X.shape
|
|
61
|
+
r2 = r // 2
|
|
62
|
+
if r % 4 != 0:
|
|
63
|
+
raise ValueError(f"No. of rows in X must be a multiple of 4\nX was {X.shape}")
|
|
64
|
+
|
|
65
|
+
m = ha.shape[2]
|
|
66
|
+
xe = pad_indices(r, m, m, "symmetric", X.device)
|
|
67
|
+
X = torch.cat((X[:, :, xe[2::2]], X[:, :, xe[3::2]]), dim=1)
|
|
68
|
+
h = torch.cat((ha.repeat(ch, 1, 1, 1), hb.repeat(ch, 1, 1, 1)), dim=0)
|
|
69
|
+
X = F.conv2d(X, h, stride=(2, 1), groups=ch * 2)
|
|
70
|
+
|
|
71
|
+
if highpass:
|
|
72
|
+
X = torch.stack((X[:, ch:], X[:, :ch]), dim=-2).view(batch, ch, r2, c)
|
|
73
|
+
else:
|
|
74
|
+
X = torch.stack((X[:, :ch], X[:, ch:]), dim=-2).view(batch, ch, r2, c)
|
|
75
|
+
return X
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def rowdfilt(X: torch.Tensor, ha: torch.Tensor, hb: torch.Tensor, highpass: bool) -> torch.Tensor:
|
|
79
|
+
"""Filter two-phase row filter bank with decimation by 2.
|
|
80
|
+
|
|
81
|
+
``ha``/``hb`` are column filters ``(1, 1, L, 1)``, reshaped internally.
|
|
82
|
+
"""
|
|
83
|
+
batch, ch, r, c = X.shape
|
|
84
|
+
c2 = c // 2
|
|
85
|
+
if c % 4 != 0:
|
|
86
|
+
raise ValueError(f"No. of cols in X must be a multiple of 4\nX was {X.shape}")
|
|
87
|
+
|
|
88
|
+
m = ha.shape[2]
|
|
89
|
+
xe = pad_indices(c, m, m, "symmetric", X.device)
|
|
90
|
+
X = torch.cat((X[:, :, :, xe[2::2]], X[:, :, :, xe[3::2]]), dim=1)
|
|
91
|
+
h = torch.cat(
|
|
92
|
+
(ha.reshape(1, 1, 1, m).repeat(ch, 1, 1, 1), hb.reshape(1, 1, 1, m).repeat(ch, 1, 1, 1)),
|
|
93
|
+
dim=0,
|
|
94
|
+
)
|
|
95
|
+
X = F.conv2d(X, h, stride=(1, 2), groups=ch * 2)
|
|
96
|
+
|
|
97
|
+
if highpass:
|
|
98
|
+
Y = torch.stack((X[:, ch:], X[:, :ch]), dim=-1).view(batch, ch, r, c2)
|
|
99
|
+
else:
|
|
100
|
+
Y = torch.stack((X[:, :ch], X[:, ch:]), dim=-1).view(batch, ch, r, c2)
|
|
101
|
+
return Y
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def colifilt(X: torch.Tensor, ha: torch.Tensor, hb: torch.Tensor, highpass: bool) -> torch.Tensor:
|
|
105
|
+
"""Inverse of :func:`coldfilt` (with upsampling)."""
|
|
106
|
+
m = ha.shape[2]
|
|
107
|
+
m2 = m // 2
|
|
108
|
+
hao, hae = ha[:, :, 1::2], ha[:, :, ::2]
|
|
109
|
+
hbo, hbe = hb[:, :, 1::2], hb[:, :, ::2]
|
|
110
|
+
batch, ch, r, c = X.shape
|
|
111
|
+
if r % 2 != 0:
|
|
112
|
+
raise ValueError(f"No. of rows in X must be a multiple of 2.\nX was {X.shape}")
|
|
113
|
+
xe = pad_indices(r, m2, m2, "symmetric", X.device)
|
|
114
|
+
|
|
115
|
+
if m2 % 2 == 0:
|
|
116
|
+
h1, h2, h3, h4 = hae, hbe, hao, hbo
|
|
117
|
+
if highpass:
|
|
118
|
+
X = torch.cat(
|
|
119
|
+
(X[:, :, xe[1:-2:2]], X[:, :, xe[:-2:2]], X[:, :, xe[3::2]], X[:, :, xe[2::2]]),
|
|
120
|
+
dim=1,
|
|
121
|
+
)
|
|
122
|
+
else:
|
|
123
|
+
X = torch.cat(
|
|
124
|
+
(X[:, :, xe[:-2:2]], X[:, :, xe[1:-2:2]], X[:, :, xe[2::2]], X[:, :, xe[3::2]]),
|
|
125
|
+
dim=1,
|
|
126
|
+
)
|
|
127
|
+
else:
|
|
128
|
+
h1, h2, h3, h4 = hao, hbo, hae, hbe
|
|
129
|
+
if highpass:
|
|
130
|
+
X = torch.cat(
|
|
131
|
+
(
|
|
132
|
+
X[:, :, xe[2:-1:2]],
|
|
133
|
+
X[:, :, xe[1:-1:2]],
|
|
134
|
+
X[:, :, xe[2:-1:2]],
|
|
135
|
+
X[:, :, xe[1:-1:2]],
|
|
136
|
+
),
|
|
137
|
+
dim=1,
|
|
138
|
+
)
|
|
139
|
+
else:
|
|
140
|
+
X = torch.cat(
|
|
141
|
+
(
|
|
142
|
+
X[:, :, xe[1:-1:2]],
|
|
143
|
+
X[:, :, xe[2:-1:2]],
|
|
144
|
+
X[:, :, xe[1:-1:2]],
|
|
145
|
+
X[:, :, xe[2:-1:2]],
|
|
146
|
+
),
|
|
147
|
+
dim=1,
|
|
148
|
+
)
|
|
149
|
+
h = torch.cat(
|
|
150
|
+
(
|
|
151
|
+
h1.repeat(ch, 1, 1, 1),
|
|
152
|
+
h2.repeat(ch, 1, 1, 1),
|
|
153
|
+
h3.repeat(ch, 1, 1, 1),
|
|
154
|
+
h4.repeat(ch, 1, 1, 1),
|
|
155
|
+
),
|
|
156
|
+
dim=0,
|
|
157
|
+
)
|
|
158
|
+
X = F.conv2d(X, h, groups=4 * ch)
|
|
159
|
+
# Stack 4 tensors of shape [batch, ch, r2, c] into [batch, ch, r*2, c]
|
|
160
|
+
X = torch.stack(
|
|
161
|
+
[X[:, :ch], X[:, ch : 2 * ch], X[:, 2 * ch : 3 * ch], X[:, 3 * ch :]], dim=3
|
|
162
|
+
).view(batch, ch, r * 2, c)
|
|
163
|
+
return X
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def rowifilt(X: torch.Tensor, ha: torch.Tensor, hb: torch.Tensor, highpass: bool) -> torch.Tensor:
|
|
167
|
+
"""Inverse of :func:`rowdfilt` (with upsampling)."""
|
|
168
|
+
m = ha.shape[2]
|
|
169
|
+
m2 = m // 2
|
|
170
|
+
hao, hae = ha[:, :, 1::2], ha[:, :, ::2]
|
|
171
|
+
hbo, hbe = hb[:, :, 1::2], hb[:, :, ::2]
|
|
172
|
+
batch, ch, r, c = X.shape
|
|
173
|
+
if c % 2 != 0:
|
|
174
|
+
raise ValueError(f"No. of cols in X must be a multiple of 2.\nX was {X.shape}")
|
|
175
|
+
xe = pad_indices(c, m2, m2, "symmetric", X.device)
|
|
176
|
+
|
|
177
|
+
if m2 % 2 == 0:
|
|
178
|
+
h1, h2, h3, h4 = hae, hbe, hao, hbo
|
|
179
|
+
if highpass:
|
|
180
|
+
X = torch.cat(
|
|
181
|
+
(
|
|
182
|
+
X[:, :, :, xe[1:-2:2]],
|
|
183
|
+
X[:, :, :, xe[:-2:2]],
|
|
184
|
+
X[:, :, :, xe[3::2]],
|
|
185
|
+
X[:, :, :, xe[2::2]],
|
|
186
|
+
),
|
|
187
|
+
dim=1,
|
|
188
|
+
)
|
|
189
|
+
else:
|
|
190
|
+
X = torch.cat(
|
|
191
|
+
(
|
|
192
|
+
X[:, :, :, xe[:-2:2]],
|
|
193
|
+
X[:, :, :, xe[1:-2:2]],
|
|
194
|
+
X[:, :, :, xe[2::2]],
|
|
195
|
+
X[:, :, :, xe[3::2]],
|
|
196
|
+
),
|
|
197
|
+
dim=1,
|
|
198
|
+
)
|
|
199
|
+
else:
|
|
200
|
+
h1, h2, h3, h4 = hao, hbo, hae, hbe
|
|
201
|
+
if highpass:
|
|
202
|
+
X = torch.cat(
|
|
203
|
+
(
|
|
204
|
+
X[:, :, :, xe[2:-1:2]],
|
|
205
|
+
X[:, :, :, xe[1:-1:2]],
|
|
206
|
+
X[:, :, :, xe[2:-1:2]],
|
|
207
|
+
X[:, :, :, xe[1:-1:2]],
|
|
208
|
+
),
|
|
209
|
+
dim=1,
|
|
210
|
+
)
|
|
211
|
+
else:
|
|
212
|
+
X = torch.cat(
|
|
213
|
+
(
|
|
214
|
+
X[:, :, :, xe[1:-1:2]],
|
|
215
|
+
X[:, :, :, xe[2:-1:2]],
|
|
216
|
+
X[:, :, :, xe[1:-1:2]],
|
|
217
|
+
X[:, :, :, xe[2:-1:2]],
|
|
218
|
+
),
|
|
219
|
+
dim=1,
|
|
220
|
+
)
|
|
221
|
+
h = torch.cat(
|
|
222
|
+
(
|
|
223
|
+
h1.repeat(ch, 1, 1, 1),
|
|
224
|
+
h2.repeat(ch, 1, 1, 1),
|
|
225
|
+
h3.repeat(ch, 1, 1, 1),
|
|
226
|
+
h4.repeat(ch, 1, 1, 1),
|
|
227
|
+
),
|
|
228
|
+
dim=0,
|
|
229
|
+
).reshape(4 * ch, 1, 1, m2)
|
|
230
|
+
X = F.conv2d(X, h, groups=4 * ch)
|
|
231
|
+
X = torch.stack(
|
|
232
|
+
[X[:, :ch], X[:, ch : 2 * ch], X[:, 2 * ch : 3 * ch], X[:, 3 * ch :]], dim=4
|
|
233
|
+
).view(batch, ch, r, c * 2)
|
|
234
|
+
return X
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def q2c(y: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
|
|
238
|
+
"""Convert quad-number subimages to two complex subimages.
|
|
239
|
+
|
|
240
|
+
Returns ``(z1, z2)`` complex tensors (each of half the spatial size).
|
|
241
|
+
"""
|
|
242
|
+
y = y / np.sqrt(2)
|
|
243
|
+
a, b = y[:, :, 0::2, 0::2], y[:, :, 0::2, 1::2]
|
|
244
|
+
c, d = y[:, :, 1::2, 0::2], y[:, :, 1::2, 1::2]
|
|
245
|
+
z1 = torch.complex(a - d, b + c)
|
|
246
|
+
z2 = torch.complex(a + d, b - c)
|
|
247
|
+
return z1, z2
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def c2q(w1: torch.Tensor, w2: torch.Tensor) -> torch.Tensor:
|
|
251
|
+
"""Convert two complex subimages to a quad-number image (inverse of q2c)."""
|
|
252
|
+
w1r, w1i = w1.real, w1.imag
|
|
253
|
+
w2r, w2i = w2.real, w2.imag
|
|
254
|
+
|
|
255
|
+
x1 = w1r + w2r
|
|
256
|
+
x2 = w1i + w2i
|
|
257
|
+
x3 = w1i - w2i
|
|
258
|
+
x4 = -w1r + w2r
|
|
259
|
+
|
|
260
|
+
b, ch, r, c = w1r.shape
|
|
261
|
+
y = w1r.new_zeros((b, ch, r * 2, c * 2))
|
|
262
|
+
y[:, :, ::2, ::2] = x1
|
|
263
|
+
y[:, :, ::2, 1::2] = x2
|
|
264
|
+
y[:, :, 1::2, ::2] = x3
|
|
265
|
+
y[:, :, 1::2, 1::2] = x4
|
|
266
|
+
y /= np.sqrt(2)
|
|
267
|
+
return y
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def highs_to_orientations(lh, hl, hh, o_dim: int = 2) -> torch.Tensor:
|
|
271
|
+
"""Combine the three quad bandpasses into 6 complex orientations.
|
|
272
|
+
|
|
273
|
+
Returns a complex tensor with the 6 orientations stacked along ``o_dim``,
|
|
274
|
+
in the order 15, 45, 75, 105, 135, 165 degrees.
|
|
275
|
+
"""
|
|
276
|
+
(z15, z165), (z45, z135), (z75, z105) = q2c(lh), q2c(hh), q2c(hl)
|
|
277
|
+
return torch.stack([z15, z45, z75, z105, z135, z165], dim=o_dim)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def orientations_to_highs(highs: torch.Tensor, o_dim: int = 2):
|
|
281
|
+
"""Inverse of :func:`highs_to_orientations`."""
|
|
282
|
+
dev = highs.device
|
|
283
|
+
z15, z165 = torch.unbind(
|
|
284
|
+
torch.index_select(highs, o_dim, torch.tensor([0, 5], device=dev)), dim=o_dim
|
|
285
|
+
)
|
|
286
|
+
z45, z135 = torch.unbind(
|
|
287
|
+
torch.index_select(highs, o_dim, torch.tensor([1, 4], device=dev)), dim=o_dim
|
|
288
|
+
)
|
|
289
|
+
z75, z105 = torch.unbind(
|
|
290
|
+
torch.index_select(highs, o_dim, torch.tensor([2, 3], device=dev)), dim=o_dim
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
lh = c2q(z15, z165)
|
|
294
|
+
hl = c2q(z75, z105)
|
|
295
|
+
hh = c2q(z45, z135)
|
|
296
|
+
return lh, hl, hh
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""1-D dual-tree complex wavelet transform (functional and module API).
|
|
2
|
+
|
|
3
|
+
The 1-D DTCWT produces complex bandpass coefficients from the two
|
|
4
|
+
decimation phases of the highpass filtering (real = even phase, imaginary =
|
|
5
|
+
odd phase), so a single 1-D DTCWT level yields a complex band that is
|
|
6
|
+
approximately shift-invariant and analytic. Levels 2+ use the
|
|
7
|
+
quarter-sample-shift filters, exactly as in the 2-D transform.
|
|
8
|
+
|
|
9
|
+
Precision follows the input (float32 -> complex64, float64 -> complex128);
|
|
10
|
+
half inputs are computed in float32 (no usable complex16 kernels).
|
|
11
|
+
Boundary handling is Kingsbury's symmetric extension.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import torch
|
|
17
|
+
import torch.nn as nn
|
|
18
|
+
|
|
19
|
+
from ..dwt._validation import check_spatial
|
|
20
|
+
from ._autocast import autocast_off
|
|
21
|
+
from .coeffs import biort as _biort
|
|
22
|
+
from .coeffs import qshift as _qshift
|
|
23
|
+
from .lowlevel import coldfilt, colfilter, colifilt
|
|
24
|
+
from .transform2d import _compute_dtype, _prep_filt, _validate_names
|
|
25
|
+
|
|
26
|
+
__all__ = ["dtcwt1d", "idtcwt1d", "DTCWT1D", "IDTCWT1D"]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _to_col(x: torch.Tensor) -> torch.Tensor:
|
|
30
|
+
"""``(N, C, L)`` -> ``(N, C, L, 1)`` column layout (like numpy dtcwt)."""
|
|
31
|
+
return x.unsqueeze(-1)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _from_col(x: torch.Tensor) -> torch.Tensor:
|
|
35
|
+
return x.squeeze(-1)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _complex_from_phases(hi: torch.Tensor) -> torch.Tensor:
|
|
39
|
+
"""Highpass phases -> complex bandpass: ``hi[..., ::2] + 1j hi[..., 1::2]``."""
|
|
40
|
+
return torch.complex(hi[..., ::2, :], hi[..., 1::2, :])
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _phases_from_complex(z: torch.Tensor) -> torch.Tensor:
|
|
44
|
+
"""Complex bandpass -> interleaved real/imag phases (inverse of above)."""
|
|
45
|
+
b, ch, r, c = z.shape
|
|
46
|
+
out = torch.empty((b, ch, 2 * r, c), dtype=z.real.dtype, device=z.device)
|
|
47
|
+
out[..., ::2, :] = z.real
|
|
48
|
+
out[..., 1::2, :] = z.imag
|
|
49
|
+
return out
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _resolve(biort, qshift, dtype, device):
|
|
53
|
+
_validate_names(biort, qshift)
|
|
54
|
+
h0o, g0o, h1o, g1o = _biort(biort)
|
|
55
|
+
h0a, h0b, g0a, g0b, h1a, h1b, g1a, g1b = _qshift(qshift)
|
|
56
|
+
return {
|
|
57
|
+
"h0o": _prep_filt(h0o, dtype, device),
|
|
58
|
+
"g0o": _prep_filt(g0o, dtype, device),
|
|
59
|
+
"h1o": _prep_filt(h1o, dtype, device),
|
|
60
|
+
"g1o": _prep_filt(g1o, dtype, device),
|
|
61
|
+
"h0a": _prep_filt(h0a, dtype, device),
|
|
62
|
+
"h0b": _prep_filt(h0b, dtype, device),
|
|
63
|
+
"h1a": _prep_filt(h1a, dtype, device),
|
|
64
|
+
"h1b": _prep_filt(h1b, dtype, device),
|
|
65
|
+
"g0a": _prep_filt(g0a, dtype, device),
|
|
66
|
+
"g0b": _prep_filt(g0b, dtype, device),
|
|
67
|
+
"g1a": _prep_filt(g1a, dtype, device),
|
|
68
|
+
"g1b": _prep_filt(g1b, dtype, device),
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _dtcwt1d_impl(x, f, J, include_scale):
|
|
73
|
+
"""Forward on the column layout ``(N, C, L, 1)``."""
|
|
74
|
+
scales = [x.new_zeros([])] * J
|
|
75
|
+
hi = colfilter(x, f["h1o"])
|
|
76
|
+
lo = colfilter(x, f["h0o"])
|
|
77
|
+
yh = [_complex_from_phases(hi)]
|
|
78
|
+
if include_scale:
|
|
79
|
+
scales[0] = lo
|
|
80
|
+
for j in range(1, J):
|
|
81
|
+
if lo.shape[2] % 4 != 0:
|
|
82
|
+
lo = torch.cat((lo[:, :, 0:1], lo, lo[:, :, -1:]), dim=2)
|
|
83
|
+
hi = coldfilt(lo, f["h1b"], f["h1a"], True)
|
|
84
|
+
lo = coldfilt(lo, f["h0b"], f["h0a"], False)
|
|
85
|
+
yh.append(_complex_from_phases(hi))
|
|
86
|
+
if include_scale:
|
|
87
|
+
scales[j] = lo
|
|
88
|
+
if include_scale:
|
|
89
|
+
return scales, yh
|
|
90
|
+
return lo, yh
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _idtcwt1d_impl(lo, yh, f):
|
|
94
|
+
"""Inverse on the column layout."""
|
|
95
|
+
J = len(yh)
|
|
96
|
+
if J == 0:
|
|
97
|
+
return lo
|
|
98
|
+
for j in range(J - 1, 0, -1):
|
|
99
|
+
hj = yh[j]
|
|
100
|
+
if hj is None: # skipped bandpass: treat as complex zeros
|
|
101
|
+
cdtype = torch.complex128 if lo.dtype == torch.float64 else torch.complex64
|
|
102
|
+
hj = torch.zeros(
|
|
103
|
+
(lo.shape[0], lo.shape[1], lo.shape[2] // 2, lo.shape[3]),
|
|
104
|
+
dtype=cdtype,
|
|
105
|
+
device=lo.device,
|
|
106
|
+
)
|
|
107
|
+
hi = _phases_from_complex(hj)
|
|
108
|
+
lo = colifilt(lo, f["g0b"], f["g0a"], False) + colifilt(hi, f["g1b"], f["g1a"], True)
|
|
109
|
+
nxt = yh[j - 1]
|
|
110
|
+
if nxt is not None and lo.shape[2] != 2 * nxt.shape[2]:
|
|
111
|
+
lo = lo[:, :, 1:-1]
|
|
112
|
+
hj = yh[0]
|
|
113
|
+
if hj is None:
|
|
114
|
+
cdtype = torch.complex128 if lo.dtype == torch.float64 else torch.complex64
|
|
115
|
+
hj = torch.zeros(
|
|
116
|
+
(lo.shape[0], lo.shape[1], lo.shape[2] // 2, lo.shape[3]),
|
|
117
|
+
dtype=cdtype,
|
|
118
|
+
device=lo.device,
|
|
119
|
+
)
|
|
120
|
+
hi = _phases_from_complex(hj)
|
|
121
|
+
z = colfilter(lo, f["g0o"]) + colfilter(hi, f["g1o"])
|
|
122
|
+
return z
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def dtcwt1d(
|
|
126
|
+
x: torch.Tensor, biort="near_sym_a", qshift="qshift_a", J=3, include_scale=False
|
|
127
|
+
) -> tuple[torch.Tensor | list[torch.Tensor], list[torch.Tensor] | None]:
|
|
128
|
+
"""Forward 1-D dual-tree complex wavelet transform.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
x (torch.Tensor): ``(N, C, L)`` input.
|
|
132
|
+
biort (str): filter bank name.
|
|
133
|
+
qshift (str): filter bank name.
|
|
134
|
+
J (int): number of levels (``0`` returns ``(x, None)``).
|
|
135
|
+
include_scale (bool): also return the intermediate lowpasses.
|
|
136
|
+
|
|
137
|
+
Odd-length inputs are padded by one sample (the last sample is repeated)
|
|
138
|
+
and the inverse returns the padded length ``L + 1``, matching the 2-D
|
|
139
|
+
transform's convention.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
tuple[torch.Tensor, list[torch.Tensor]]: ``(yl, yh)``: lowpass
|
|
143
|
+
``(N, C, L')`` and a list of ``J`` complex bandpasses ``(N, C, L'')``
|
|
144
|
+
(finest first).
|
|
145
|
+
"""
|
|
146
|
+
if x.ndim != 3:
|
|
147
|
+
raise ValueError(f"dtcwt1d expects (N, C, L); got {tuple(x.shape)}")
|
|
148
|
+
check_spatial(x.shape[-1:], ("L",), "dtcwt1d")
|
|
149
|
+
if isinstance(J, bool) or not isinstance(J, int) or J < 0:
|
|
150
|
+
raise ValueError(f"J must be a non-negative int; got {J!r}")
|
|
151
|
+
dtype = _compute_dtype(x)
|
|
152
|
+
f = _resolve(biort, qshift, dtype, x.device)
|
|
153
|
+
x = x.to(dtype)
|
|
154
|
+
if x.shape[-1] % 2 != 0:
|
|
155
|
+
x = torch.cat((x, x[..., -1:]), dim=-1)
|
|
156
|
+
x4 = _to_col(x)
|
|
157
|
+
if J == 0:
|
|
158
|
+
return x, None
|
|
159
|
+
with autocast_off(x.device):
|
|
160
|
+
out = _dtcwt1d_impl(x4, f, J, bool(include_scale))
|
|
161
|
+
if include_scale:
|
|
162
|
+
scales, yh = out
|
|
163
|
+
return [s.squeeze(-1) for s in scales], [h.squeeze(-1) for h in yh]
|
|
164
|
+
lo, yh = out
|
|
165
|
+
return lo.squeeze(-1), [h.squeeze(-1) for h in yh]
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def idtcwt1d(coeffs, biort="near_sym_a", qshift="qshift_a") -> torch.Tensor:
|
|
169
|
+
"""Inverse 1-D DTCWT (exact inverse of :func:`dtcwt1d`)."""
|
|
170
|
+
yl, yh = coeffs
|
|
171
|
+
if yl is None:
|
|
172
|
+
raise ValueError("The lowpass coefficient tensor yl cannot be None")
|
|
173
|
+
dtype = _compute_dtype(yl)
|
|
174
|
+
f = _resolve(biort, qshift, dtype, yl.device)
|
|
175
|
+
lo = _to_col(yl.to(dtype))
|
|
176
|
+
if yh is None: # J == 0 forward
|
|
177
|
+
return yl
|
|
178
|
+
highs = [None if h is None else _to_col(h) for h in yh]
|
|
179
|
+
with autocast_off(yl.device):
|
|
180
|
+
z = _idtcwt1d_impl(lo, highs, f)
|
|
181
|
+
return _from_col(z)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class DTCWT1D(nn.Module):
|
|
185
|
+
"""1-D DTCWT module (see :func:`dtcwt1d`)."""
|
|
186
|
+
|
|
187
|
+
def __init__(self, biort="near_sym_a", qshift="qshift_a", J=3, include_scale=False):
|
|
188
|
+
super().__init__()
|
|
189
|
+
if isinstance(J, bool) or not isinstance(J, int) or J < 0:
|
|
190
|
+
raise ValueError(f"J must be a non-negative int; got {J!r}")
|
|
191
|
+
self.biort = biort
|
|
192
|
+
self.qshift = qshift
|
|
193
|
+
self.J = J
|
|
194
|
+
self.include_scale = bool(include_scale)
|
|
195
|
+
filts = _resolve(biort, qshift, torch.float64, torch.device("cpu"))
|
|
196
|
+
for name in ("h0o", "h1o", "h0a", "h1a", "h0b", "h1b"):
|
|
197
|
+
self.register_buffer(name, filts[name])
|
|
198
|
+
|
|
199
|
+
def forward(self, x: torch.Tensor):
|
|
200
|
+
if x.ndim != 3:
|
|
201
|
+
raise ValueError(f"DTCWT1D expects (N, C, L); got {tuple(x.shape)}")
|
|
202
|
+
check_spatial(x.shape[-1:], ("L",), "DTCWT1D")
|
|
203
|
+
if self.J == 0:
|
|
204
|
+
return x, None
|
|
205
|
+
dtype = _compute_dtype(x)
|
|
206
|
+
x = x.to(dtype)
|
|
207
|
+
if x.shape[-1] % 2 != 0:
|
|
208
|
+
x = torch.cat((x, x[..., -1:]), dim=-1)
|
|
209
|
+
f = {
|
|
210
|
+
n: getattr(self, n).to(dtype=dtype, device=x.device)
|
|
211
|
+
for n in ("h0o", "h1o", "h0a", "h1a", "h0b", "h1b")
|
|
212
|
+
}
|
|
213
|
+
with autocast_off(x.device):
|
|
214
|
+
out = _dtcwt1d_impl(_to_col(x), f, self.J, self.include_scale)
|
|
215
|
+
if self.include_scale:
|
|
216
|
+
scales, yh = out
|
|
217
|
+
return [s.squeeze(-1) for s in scales], [h.squeeze(-1) for h in yh]
|
|
218
|
+
lo, yh = out
|
|
219
|
+
return lo.squeeze(-1), [h.squeeze(-1) for h in yh]
|
|
220
|
+
|
|
221
|
+
def extra_repr(self) -> str:
|
|
222
|
+
return f"biort={self.biort!r}, qshift={self.qshift!r}, J={self.J}"
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
class IDTCWT1D(nn.Module):
|
|
226
|
+
"""1-D inverse DTCWT module (see :func:`idtcwt1d`)."""
|
|
227
|
+
|
|
228
|
+
def __init__(self, biort="near_sym_a", qshift="qshift_a"):
|
|
229
|
+
super().__init__()
|
|
230
|
+
self.biort = biort
|
|
231
|
+
self.qshift = qshift
|
|
232
|
+
filts = _resolve(biort, qshift, torch.float64, torch.device("cpu"))
|
|
233
|
+
for name in ("g0o", "g1o", "g0a", "g1a", "g0b", "g1b"):
|
|
234
|
+
self.register_buffer(name, filts[name])
|
|
235
|
+
|
|
236
|
+
def forward(self, coeffs):
|
|
237
|
+
yl, yh = coeffs
|
|
238
|
+
if yl is None:
|
|
239
|
+
raise ValueError("The lowpass coefficient tensor yl cannot be None")
|
|
240
|
+
dtype = _compute_dtype(yl)
|
|
241
|
+
f = {
|
|
242
|
+
n: getattr(self, n).to(dtype=dtype, device=yl.device)
|
|
243
|
+
for n in ("g0o", "g1o", "g0a", "g1a", "g0b", "g1b")
|
|
244
|
+
}
|
|
245
|
+
lo = _to_col(yl.to(dtype))
|
|
246
|
+
if yh is None: # J == 0 forward
|
|
247
|
+
return yl
|
|
248
|
+
highs = [None if h is None else _to_col(h) for h in yh]
|
|
249
|
+
with autocast_off(yl.device):
|
|
250
|
+
return _from_col(_idtcwt1d_impl(lo, highs, f))
|
|
251
|
+
|
|
252
|
+
def extra_repr(self) -> str:
|
|
253
|
+
return f"biort={self.biort!r}, qshift={self.qshift!r}"
|