phasecongruency 0.2.3__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.
- phasecongruency/__init__.py +107 -0
- phasecongruency/frequencyfilt.py +508 -0
- phasecongruency/phasecongruency.py +1025 -0
- phasecongruency/syntheticimages.py +298 -0
- phasecongruency/utilities.py +218 -0
- phasecongruency-0.2.3.dist-info/METADATA +132 -0
- phasecongruency-0.2.3.dist-info/RECORD +10 -0
- phasecongruency-0.2.3.dist-info/WHEEL +5 -0
- phasecongruency-0.2.3.dist-info/licenses/LICENSE +21 -0
- phasecongruency-0.2.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Image Phase Congruency
|
|
3
|
+
|
|
4
|
+
Phase based feature detection and image enhancement.
|
|
5
|
+
|
|
6
|
+
Peter Kovesi
|
|
7
|
+
peterkovesi.com
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
# Phase congruency and feature detection
|
|
11
|
+
from .phasecongruency import (
|
|
12
|
+
phasecongmono,
|
|
13
|
+
phasesymmono,
|
|
14
|
+
ppdrc,
|
|
15
|
+
highpassmonogenic,
|
|
16
|
+
bandpassmonogenic,
|
|
17
|
+
gaborconvolve,
|
|
18
|
+
monofilt,
|
|
19
|
+
phasecong3,
|
|
20
|
+
phasesym,
|
|
21
|
+
ppdenoise,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# Frequency domain filters
|
|
25
|
+
from .frequencyfilt import (
|
|
26
|
+
filtergrid,
|
|
27
|
+
filtergrids,
|
|
28
|
+
gridangles,
|
|
29
|
+
cosineangularfilter,
|
|
30
|
+
gaussianangularfilter,
|
|
31
|
+
lowpassfilter,
|
|
32
|
+
highpassfilter,
|
|
33
|
+
bandpassfilter,
|
|
34
|
+
highboostfilter,
|
|
35
|
+
loggabor,
|
|
36
|
+
monogenicfilters,
|
|
37
|
+
packedmonogenicfilters,
|
|
38
|
+
perfft2,
|
|
39
|
+
geoseries,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# Synthetic test images
|
|
43
|
+
from .syntheticimages import (
|
|
44
|
+
step2line,
|
|
45
|
+
circsine,
|
|
46
|
+
starsine,
|
|
47
|
+
noiseonf,
|
|
48
|
+
nophase,
|
|
49
|
+
quantizephase,
|
|
50
|
+
swapphase,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Utilities
|
|
54
|
+
from .utilities import (
|
|
55
|
+
replacenan,
|
|
56
|
+
fillnan,
|
|
57
|
+
hysthresh,
|
|
58
|
+
imgnormalise,
|
|
59
|
+
imgnormalize,
|
|
60
|
+
histtruncate,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
__version__ = "0.2.3"
|
|
64
|
+
|
|
65
|
+
__all__ = [
|
|
66
|
+
# Phase congruency
|
|
67
|
+
"phasecongmono",
|
|
68
|
+
"phasesymmono",
|
|
69
|
+
"ppdrc",
|
|
70
|
+
"highpassmonogenic",
|
|
71
|
+
"bandpassmonogenic",
|
|
72
|
+
"gaborconvolve",
|
|
73
|
+
"monofilt",
|
|
74
|
+
"phasecong3",
|
|
75
|
+
"phasesym",
|
|
76
|
+
"ppdenoise",
|
|
77
|
+
# Frequency filters
|
|
78
|
+
"filtergrid",
|
|
79
|
+
"filtergrids",
|
|
80
|
+
"gridangles",
|
|
81
|
+
"cosineangularfilter",
|
|
82
|
+
"gaussianangularfilter",
|
|
83
|
+
"lowpassfilter",
|
|
84
|
+
"highpassfilter",
|
|
85
|
+
"bandpassfilter",
|
|
86
|
+
"highboostfilter",
|
|
87
|
+
"loggabor",
|
|
88
|
+
"monogenicfilters",
|
|
89
|
+
"packedmonogenicfilters",
|
|
90
|
+
"perfft2",
|
|
91
|
+
"geoseries",
|
|
92
|
+
# Synthetic images
|
|
93
|
+
"step2line",
|
|
94
|
+
"circsine",
|
|
95
|
+
"starsine",
|
|
96
|
+
"noiseonf",
|
|
97
|
+
"nophase",
|
|
98
|
+
"quantizephase",
|
|
99
|
+
"swapphase",
|
|
100
|
+
# Utilities
|
|
101
|
+
"replacenan",
|
|
102
|
+
"fillnan",
|
|
103
|
+
"hysthresh",
|
|
104
|
+
"imgnormalise",
|
|
105
|
+
"imgnormalize",
|
|
106
|
+
"histtruncate",
|
|
107
|
+
]
|
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Functions for constructing image filters in the frequency domain.
|
|
3
|
+
|
|
4
|
+
Copyright (c) Peter Kovesi
|
|
5
|
+
peterkovesi.com
|
|
6
|
+
|
|
7
|
+
MIT License.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
from numpy.fft import fft2, ifft2, ifftshift
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def filtergrids(rows, cols=None):
|
|
15
|
+
"""Generate grids for constructing frequency domain filters.
|
|
16
|
+
|
|
17
|
+
Parameters
|
|
18
|
+
----------
|
|
19
|
+
rows : int or tuple
|
|
20
|
+
Number of rows, or a tuple (rows, cols).
|
|
21
|
+
cols : int, optional
|
|
22
|
+
Number of columns. Not needed if rows is a tuple.
|
|
23
|
+
|
|
24
|
+
Returns
|
|
25
|
+
-------
|
|
26
|
+
f : ndarray
|
|
27
|
+
Grid of size (rows, cols) containing frequency values from 0 to 0.5.
|
|
28
|
+
The grid is quadrant shifted so that 0 frequency is at f[0,0].
|
|
29
|
+
fx : ndarray
|
|
30
|
+
Grid containing normalised frequency values ranging from -0.5 to 0.5
|
|
31
|
+
in x direction. Quadrant shifted.
|
|
32
|
+
fy : ndarray
|
|
33
|
+
Grid containing normalised frequency values ranging from -0.5 to 0.5
|
|
34
|
+
in y direction. Quadrant shifted.
|
|
35
|
+
"""
|
|
36
|
+
if cols is None:
|
|
37
|
+
if isinstance(rows, (tuple, list)):
|
|
38
|
+
rows, cols = rows
|
|
39
|
+
else:
|
|
40
|
+
raise ValueError("Must provide both rows and cols, or a tuple (rows, cols)")
|
|
41
|
+
|
|
42
|
+
if cols % 2 == 1:
|
|
43
|
+
fxrange = np.arange(-(cols - 1) / 2, (cols - 1) / 2 + 1) / cols
|
|
44
|
+
else:
|
|
45
|
+
fxrange = np.arange(-cols / 2, cols / 2) / cols
|
|
46
|
+
|
|
47
|
+
if rows % 2 == 1:
|
|
48
|
+
fyrange = np.arange(-(rows - 1) / 2, (rows - 1) / 2 + 1) / rows
|
|
49
|
+
else:
|
|
50
|
+
fyrange = np.arange(-rows / 2, rows / 2) / rows
|
|
51
|
+
|
|
52
|
+
fx, fy = np.meshgrid(fxrange, fyrange)
|
|
53
|
+
|
|
54
|
+
# Quadrant shift so that filters are constructed with 0 frequency at corners
|
|
55
|
+
fx = ifftshift(fx)
|
|
56
|
+
fy = ifftshift(fy)
|
|
57
|
+
|
|
58
|
+
f = np.sqrt(fx**2 + fy**2)
|
|
59
|
+
return f, fx, fy
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def filtergrid(rows, cols=None):
|
|
63
|
+
"""Generate grid for constructing frequency domain filters.
|
|
64
|
+
|
|
65
|
+
Parameters
|
|
66
|
+
----------
|
|
67
|
+
rows : int or tuple
|
|
68
|
+
Number of rows, or a tuple (rows, cols).
|
|
69
|
+
cols : int, optional
|
|
70
|
+
Number of columns.
|
|
71
|
+
|
|
72
|
+
Returns
|
|
73
|
+
-------
|
|
74
|
+
f : ndarray
|
|
75
|
+
Grid of size (rows, cols) containing normalised frequency values
|
|
76
|
+
from 0 to 0.5. Grid is quadrant shifted so that 0 frequency is at
|
|
77
|
+
f[0,0].
|
|
78
|
+
"""
|
|
79
|
+
if cols is None:
|
|
80
|
+
if isinstance(rows, (tuple, list)):
|
|
81
|
+
rows, cols = rows
|
|
82
|
+
else:
|
|
83
|
+
raise ValueError("Must provide both rows and cols, or a tuple (rows, cols)")
|
|
84
|
+
|
|
85
|
+
if cols % 2 == 1:
|
|
86
|
+
fxrange = np.arange(-(cols - 1) / 2, (cols - 1) / 2 + 1) / cols
|
|
87
|
+
else:
|
|
88
|
+
fxrange = np.arange(-cols / 2, cols / 2) / cols
|
|
89
|
+
|
|
90
|
+
if rows % 2 == 1:
|
|
91
|
+
fyrange = np.arange(-(rows - 1) / 2, (rows - 1) / 2 + 1) / rows
|
|
92
|
+
else:
|
|
93
|
+
fyrange = np.arange(-rows / 2, rows / 2) / rows
|
|
94
|
+
|
|
95
|
+
fx, fy = np.meshgrid(fxrange, fyrange)
|
|
96
|
+
f = np.sqrt(fx**2 + fy**2)
|
|
97
|
+
return ifftshift(f)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def monogenicfilters(rows, cols=None):
|
|
101
|
+
"""Generate monogenic filter grids.
|
|
102
|
+
|
|
103
|
+
Parameters
|
|
104
|
+
----------
|
|
105
|
+
rows : int or tuple
|
|
106
|
+
Number of rows, or a tuple (rows, cols).
|
|
107
|
+
cols : int, optional
|
|
108
|
+
Number of columns.
|
|
109
|
+
|
|
110
|
+
Returns
|
|
111
|
+
-------
|
|
112
|
+
H1 : ndarray (complex)
|
|
113
|
+
First monogenic filter: 1j * fx / f
|
|
114
|
+
H2 : ndarray (complex)
|
|
115
|
+
Second monogenic filter: 1j * fy / f
|
|
116
|
+
f : ndarray
|
|
117
|
+
Frequency grid corresponding to the filters.
|
|
118
|
+
|
|
119
|
+
Notes
|
|
120
|
+
-----
|
|
121
|
+
H1, H2, and f are quadrant shifted so that the 0 frequency value is at
|
|
122
|
+
coordinate [0,0].
|
|
123
|
+
"""
|
|
124
|
+
if cols is None:
|
|
125
|
+
if isinstance(rows, (tuple, list)):
|
|
126
|
+
rows, cols = rows
|
|
127
|
+
else:
|
|
128
|
+
raise ValueError("Must provide both rows and cols, or a tuple (rows, cols)")
|
|
129
|
+
|
|
130
|
+
f, fx, fy = filtergrids(rows, cols)
|
|
131
|
+
f[0, 0] = 1 # Avoid divide by zero
|
|
132
|
+
|
|
133
|
+
H1 = 1j * fx / f
|
|
134
|
+
H2 = 1j * fy / f
|
|
135
|
+
|
|
136
|
+
H1[0, 0] = 0 # Restore 0 DC value
|
|
137
|
+
H2[0, 0] = 0
|
|
138
|
+
f[0, 0] = 0
|
|
139
|
+
return H1, H2, f
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def packedmonogenicfilters(rows, cols=None):
|
|
143
|
+
"""Monogenic filter where both filters are packed in one Complex grid.
|
|
144
|
+
|
|
145
|
+
Parameters
|
|
146
|
+
----------
|
|
147
|
+
rows : int or tuple
|
|
148
|
+
Number of rows, or a tuple (rows, cols).
|
|
149
|
+
cols : int, optional
|
|
150
|
+
Number of columns.
|
|
151
|
+
|
|
152
|
+
Returns
|
|
153
|
+
-------
|
|
154
|
+
H : ndarray (complex)
|
|
155
|
+
The two monogenic filters packed into one complex grid.
|
|
156
|
+
f : ndarray
|
|
157
|
+
Frequency grid corresponding to the filter.
|
|
158
|
+
|
|
159
|
+
Notes
|
|
160
|
+
-----
|
|
161
|
+
The two monogenic filters H1 = 1j*fx/f and H2 = 1j*fy/f are packed
|
|
162
|
+
together as a complex valued matrix. When the convolution is performed
|
|
163
|
+
via the FFT the real part of the result will correspond to the convolution
|
|
164
|
+
with H1 and the imaginary part with H2.
|
|
165
|
+
"""
|
|
166
|
+
if cols is None:
|
|
167
|
+
if isinstance(rows, (tuple, list)):
|
|
168
|
+
rows, cols = rows
|
|
169
|
+
else:
|
|
170
|
+
raise ValueError("Must provide both rows and cols, or a tuple (rows, cols)")
|
|
171
|
+
|
|
172
|
+
f, fx, fy = filtergrids(rows, cols)
|
|
173
|
+
f[0, 0] = 1 # Avoid divide by zero
|
|
174
|
+
|
|
175
|
+
# Pack: H = (1j*fx - fy) / f (note subtraction because i*i = -1)
|
|
176
|
+
H = (1j * fx - fy) / f
|
|
177
|
+
|
|
178
|
+
H[0, 0] = 0 # Restore 0 DC value
|
|
179
|
+
f[0, 0] = 0
|
|
180
|
+
return H, f
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def lowpassfilter(sze_or_f, cutoff, n):
|
|
184
|
+
"""Construct a low-pass Butterworth filter.
|
|
185
|
+
|
|
186
|
+
Parameters
|
|
187
|
+
----------
|
|
188
|
+
sze_or_f : tuple or float
|
|
189
|
+
Either a tuple (rows, cols) specifying the size of filter to construct,
|
|
190
|
+
or a scalar frequency value.
|
|
191
|
+
cutoff : float
|
|
192
|
+
Cutoff frequency of the filter (0-0.5).
|
|
193
|
+
n : int
|
|
194
|
+
Order of the filter (n is doubled so that it is always even).
|
|
195
|
+
|
|
196
|
+
Returns
|
|
197
|
+
-------
|
|
198
|
+
f : ndarray or float
|
|
199
|
+
The filter. If sze_or_f was a tuple, returns a 2D array with frequency
|
|
200
|
+
origin at the corners.
|
|
201
|
+
"""
|
|
202
|
+
if isinstance(sze_or_f, (tuple, list)):
|
|
203
|
+
if cutoff < 0 or cutoff > 0.5:
|
|
204
|
+
raise ValueError("cutoff frequency must be between 0 and 0.5")
|
|
205
|
+
f = filtergrid(sze_or_f)
|
|
206
|
+
return 1.0 / (1.0 + (f / cutoff) ** (2 * n))
|
|
207
|
+
else:
|
|
208
|
+
return 1.0 / (1.0 + (sze_or_f / cutoff) ** (2 * n))
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def highpassfilter(sze, cutoff, n):
|
|
212
|
+
"""Construct a high-pass Butterworth filter.
|
|
213
|
+
|
|
214
|
+
Parameters
|
|
215
|
+
----------
|
|
216
|
+
sze : tuple
|
|
217
|
+
A tuple (rows, cols) specifying the size of filter to construct.
|
|
218
|
+
cutoff : float
|
|
219
|
+
Cutoff frequency of the filter (0-0.5).
|
|
220
|
+
n : int
|
|
221
|
+
Order of the filter.
|
|
222
|
+
|
|
223
|
+
Returns
|
|
224
|
+
-------
|
|
225
|
+
f : ndarray
|
|
226
|
+
Frequency domain filter with frequency origin at the corners.
|
|
227
|
+
"""
|
|
228
|
+
if cutoff < 0 or cutoff > 0.5:
|
|
229
|
+
raise ValueError("cutoff frequency must be between 0 and 0.5")
|
|
230
|
+
return 1.0 - lowpassfilter(sze, cutoff, n)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def bandpassfilter(sze, cutin, cutoff, n):
|
|
234
|
+
"""Construct a band-pass Butterworth filter.
|
|
235
|
+
|
|
236
|
+
Parameters
|
|
237
|
+
----------
|
|
238
|
+
sze : tuple
|
|
239
|
+
A tuple (rows, cols) specifying the size of filter to construct.
|
|
240
|
+
cutin : float
|
|
241
|
+
Lower cutoff frequency (0-0.5).
|
|
242
|
+
cutoff : float
|
|
243
|
+
Upper cutoff frequency (0-0.5).
|
|
244
|
+
n : int
|
|
245
|
+
Order of the filter.
|
|
246
|
+
|
|
247
|
+
Returns
|
|
248
|
+
-------
|
|
249
|
+
f : ndarray
|
|
250
|
+
Frequency domain filter with frequency origin at the corners.
|
|
251
|
+
"""
|
|
252
|
+
if cutin < 0 or cutin > 0.5 or cutoff < 0 or cutoff > 0.5:
|
|
253
|
+
raise ValueError("Frequencies must be between 0 and 0.5")
|
|
254
|
+
if n < 1:
|
|
255
|
+
raise ValueError("Order of filter must be greater than 1")
|
|
256
|
+
return lowpassfilter(sze, cutoff, n) - lowpassfilter(sze, cutin, n)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def highboostfilter(sze, cutoff, n, boost):
|
|
260
|
+
"""Construct a high-boost Butterworth filter.
|
|
261
|
+
|
|
262
|
+
Parameters
|
|
263
|
+
----------
|
|
264
|
+
sze : tuple
|
|
265
|
+
A tuple (rows, cols) specifying the size of filter to construct.
|
|
266
|
+
cutoff : float
|
|
267
|
+
Cutoff frequency of the filter (0-0.5).
|
|
268
|
+
n : int
|
|
269
|
+
Order of the filter.
|
|
270
|
+
boost : float
|
|
271
|
+
Ratio that high frequency values are boosted relative to low
|
|
272
|
+
frequency values.
|
|
273
|
+
|
|
274
|
+
Returns
|
|
275
|
+
-------
|
|
276
|
+
f : ndarray
|
|
277
|
+
Frequency domain filter with frequency origin at the corners.
|
|
278
|
+
"""
|
|
279
|
+
if cutoff < 0 or cutoff > 0.5:
|
|
280
|
+
raise ValueError("cutoff frequency must be between 0 and 0.5")
|
|
281
|
+
|
|
282
|
+
if boost >= 1: # high-boost filter
|
|
283
|
+
f = (1.0 - 1.0 / boost) * highpassfilter(sze, cutoff, n) + 1.0 / boost
|
|
284
|
+
else: # low-boost filter
|
|
285
|
+
f = (1.0 - boost) * lowpassfilter(sze, cutoff, n) + boost
|
|
286
|
+
|
|
287
|
+
return f
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def loggabor(f, fo, sigmaOnf):
|
|
291
|
+
"""The logarithmic Gabor function in the frequency domain.
|
|
292
|
+
|
|
293
|
+
Parameters
|
|
294
|
+
----------
|
|
295
|
+
f : float
|
|
296
|
+
Frequency to evaluate the function at.
|
|
297
|
+
fo : float
|
|
298
|
+
Centre frequency of filter.
|
|
299
|
+
sigmaOnf : float
|
|
300
|
+
Ratio of the standard deviation of the Gaussian describing the log
|
|
301
|
+
Gabor filter's transfer function in the frequency domain to the filter
|
|
302
|
+
center frequency.
|
|
303
|
+
|
|
304
|
+
Returns
|
|
305
|
+
-------
|
|
306
|
+
float
|
|
307
|
+
Log Gabor filter value.
|
|
308
|
+
|
|
309
|
+
Notes
|
|
310
|
+
-----
|
|
311
|
+
sigmaOnf = 0.75 gives a filter bandwidth of about 1 octave.
|
|
312
|
+
sigmaOnf = 0.55 gives a filter bandwidth of about 2 octaves.
|
|
313
|
+
"""
|
|
314
|
+
if f < np.finfo(float).eps:
|
|
315
|
+
return 0.0
|
|
316
|
+
else:
|
|
317
|
+
return np.exp((-(np.log(f / fo)) ** 2) / (2 * np.log(sigmaOnf) ** 2))
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def gridangles(freq, fx, fy):
|
|
321
|
+
"""Generate arrays of filter grid angles.
|
|
322
|
+
|
|
323
|
+
Parameters
|
|
324
|
+
----------
|
|
325
|
+
freq : ndarray
|
|
326
|
+
Frequency grid (output of filtergrids).
|
|
327
|
+
fx : ndarray
|
|
328
|
+
x-frequency grid (output of filtergrids).
|
|
329
|
+
fy : ndarray
|
|
330
|
+
y-frequency grid (output of filtergrids).
|
|
331
|
+
|
|
332
|
+
Returns
|
|
333
|
+
-------
|
|
334
|
+
sintheta : ndarray
|
|
335
|
+
Sine of the angles in the filter grid.
|
|
336
|
+
costheta : ndarray
|
|
337
|
+
Cosine of the angles in the filter grid.
|
|
338
|
+
"""
|
|
339
|
+
freq[0, 0] = 1 # Avoid divide by 0
|
|
340
|
+
sintheta = fx / freq
|
|
341
|
+
costheta = fy / freq
|
|
342
|
+
freq[0, 0] = 0 # Restore 0 DC
|
|
343
|
+
|
|
344
|
+
return sintheta, costheta
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def cosineangularfilter(angl, wavelen, sintheta, costheta):
|
|
348
|
+
"""Orientation selective frequency domain filter with cosine windowing.
|
|
349
|
+
|
|
350
|
+
Parameters
|
|
351
|
+
----------
|
|
352
|
+
angl : float
|
|
353
|
+
Orientation of the filter (radians).
|
|
354
|
+
wavelen : float
|
|
355
|
+
Wavelength of the angular cosine window function.
|
|
356
|
+
sintheta : ndarray
|
|
357
|
+
Grid as returned by gridangles().
|
|
358
|
+
costheta : ndarray
|
|
359
|
+
Grid as returned by gridangles().
|
|
360
|
+
|
|
361
|
+
Returns
|
|
362
|
+
-------
|
|
363
|
+
fltr : ndarray
|
|
364
|
+
The angular filter.
|
|
365
|
+
"""
|
|
366
|
+
sinangl = np.sin(angl)
|
|
367
|
+
cosangl = np.cos(angl)
|
|
368
|
+
fltr = np.zeros(sintheta.shape)
|
|
369
|
+
|
|
370
|
+
for idx in np.ndindex(sintheta.shape):
|
|
371
|
+
ds = sintheta[idx] * cosangl - costheta[idx] * sinangl
|
|
372
|
+
dc = costheta[idx] * cosangl + sintheta[idx] * sinangl
|
|
373
|
+
dtheta = abs(np.arctan2(ds, dc))
|
|
374
|
+
dtheta = min(dtheta * 2 * np.pi / wavelen, np.pi)
|
|
375
|
+
fltr[idx] = (np.cos(dtheta) + 1) / 2
|
|
376
|
+
|
|
377
|
+
return fltr
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def gaussianangularfilter(angl, thetaSigma, sintheta, costheta):
|
|
381
|
+
"""Orientation selective frequency domain filter with Gaussian windowing.
|
|
382
|
+
|
|
383
|
+
Parameters
|
|
384
|
+
----------
|
|
385
|
+
angl : float
|
|
386
|
+
Orientation of the filter (radians).
|
|
387
|
+
thetaSigma : float
|
|
388
|
+
Standard deviation of angular Gaussian window function.
|
|
389
|
+
sintheta : ndarray
|
|
390
|
+
Grid as returned by gridangles().
|
|
391
|
+
costheta : ndarray
|
|
392
|
+
Grid as returned by gridangles().
|
|
393
|
+
|
|
394
|
+
Returns
|
|
395
|
+
-------
|
|
396
|
+
fltr : ndarray
|
|
397
|
+
The angular filter.
|
|
398
|
+
"""
|
|
399
|
+
sinangl = np.sin(angl)
|
|
400
|
+
cosangl = np.cos(angl)
|
|
401
|
+
fltr = np.zeros(sintheta.shape)
|
|
402
|
+
|
|
403
|
+
for idx in np.ndindex(sintheta.shape):
|
|
404
|
+
ds = sintheta[idx] * cosangl - costheta[idx] * sinangl
|
|
405
|
+
dc = costheta[idx] * cosangl + sintheta[idx] * sinangl
|
|
406
|
+
dtheta = np.arctan2(ds, dc)
|
|
407
|
+
fltr[idx] = np.exp((-dtheta ** 2) / (2 * thetaSigma ** 2))
|
|
408
|
+
|
|
409
|
+
return fltr
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def perfft2(img):
|
|
413
|
+
"""2D Fourier transform of Moisan's periodic image component.
|
|
414
|
+
|
|
415
|
+
Parameters
|
|
416
|
+
----------
|
|
417
|
+
img : ndarray (2D, real)
|
|
418
|
+
Image to be transformed.
|
|
419
|
+
|
|
420
|
+
Returns
|
|
421
|
+
-------
|
|
422
|
+
P : ndarray (complex)
|
|
423
|
+
2D FFT of periodic image component.
|
|
424
|
+
S : ndarray (complex)
|
|
425
|
+
2D FFT of smooth component.
|
|
426
|
+
p : ndarray (real)
|
|
427
|
+
Periodic component (spatial domain).
|
|
428
|
+
s : ndarray (real)
|
|
429
|
+
Smooth component (spatial domain).
|
|
430
|
+
|
|
431
|
+
Notes
|
|
432
|
+
-----
|
|
433
|
+
Moisan's "Periodic plus Smooth Image Decomposition" decomposes an image
|
|
434
|
+
into two components: img = p + s, where s is the 'smooth' component with
|
|
435
|
+
mean 0 and p is the 'periodic' component which has no sharp discontinuities
|
|
436
|
+
when one moves cyclically across the image boundaries.
|
|
437
|
+
|
|
438
|
+
Reference:
|
|
439
|
+
L. Moisan, "Periodic plus Smooth Image Decomposition", Journal of
|
|
440
|
+
Mathematical Imaging and Vision, vol 39:2, pp. 161-179, 2011.
|
|
441
|
+
"""
|
|
442
|
+
rows, cols = img.shape
|
|
443
|
+
|
|
444
|
+
# Compute the boundary image
|
|
445
|
+
s = np.zeros((rows, cols))
|
|
446
|
+
s[0, :] = img[0, :] - img[-1, :]
|
|
447
|
+
s[-1, :] = -s[0, :]
|
|
448
|
+
s[:, 0] = s[:, 0] + img[:, 0] - img[:, -1]
|
|
449
|
+
s[:, -1] = s[:, -1] - img[:, 0] + img[:, -1]
|
|
450
|
+
|
|
451
|
+
# Generate grid for the filter in frequency domain
|
|
452
|
+
cxrange = 2 * np.pi * np.arange(cols) / cols
|
|
453
|
+
cyrange = 2 * np.pi * np.arange(rows) / rows
|
|
454
|
+
cx, cy = np.meshgrid(cxrange, cyrange)
|
|
455
|
+
denom = 2 * (2 - np.cos(cx) - np.cos(cy))
|
|
456
|
+
|
|
457
|
+
# Generate FFT of smooth component
|
|
458
|
+
S = fft2(s) / denom
|
|
459
|
+
S[0, 0] = 0.0 # Enforce 0 mean
|
|
460
|
+
|
|
461
|
+
P = fft2(img) - S # FFT of periodic component
|
|
462
|
+
|
|
463
|
+
s = np.real(ifft2(S))
|
|
464
|
+
p = img - s
|
|
465
|
+
|
|
466
|
+
return P, S, p, s
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def geoseries(s1, mult_or_n=None, n=None):
|
|
470
|
+
"""Generate geometric series.
|
|
471
|
+
|
|
472
|
+
Useful for generating geometrically scaled wavelengths for specifying
|
|
473
|
+
filter banks.
|
|
474
|
+
|
|
475
|
+
Can be called as:
|
|
476
|
+
geoseries(s1, mult, n) - Generate n values starting at s1 with
|
|
477
|
+
multiplier mult.
|
|
478
|
+
geoseries((s1, sn), n) - Generate n values from s1 to sn.
|
|
479
|
+
|
|
480
|
+
Parameters
|
|
481
|
+
----------
|
|
482
|
+
s1 : float or tuple
|
|
483
|
+
Starting value, or tuple (s1, sn) of start and end values.
|
|
484
|
+
mult_or_n : float or int
|
|
485
|
+
If s1 is a scalar, this is the multiplier between successive values.
|
|
486
|
+
If s1 is a tuple, this is n (number of elements).
|
|
487
|
+
n : int, optional
|
|
488
|
+
Number of elements in the series. Only used when s1 is a scalar.
|
|
489
|
+
|
|
490
|
+
Returns
|
|
491
|
+
-------
|
|
492
|
+
s : ndarray
|
|
493
|
+
The geometric series.
|
|
494
|
+
"""
|
|
495
|
+
if isinstance(s1, (tuple, list)):
|
|
496
|
+
s1_val, sn_val = s1
|
|
497
|
+
n_val = int(mult_or_n)
|
|
498
|
+
assert s1_val > 0, "Starting value must be > 0"
|
|
499
|
+
assert n_val > 0, "Number of elements must be a +ve integer"
|
|
500
|
+
if n_val == 1:
|
|
501
|
+
return np.array([s1_val], dtype=float)
|
|
502
|
+
mult = np.exp(np.log(sn_val / s1_val) / (n_val - 1))
|
|
503
|
+
return s1_val * mult ** np.arange(n_val)
|
|
504
|
+
else:
|
|
505
|
+
assert n is not None, "Must provide n when s1 is a scalar"
|
|
506
|
+
assert n > 0, "Number of elements must be a +ve integer"
|
|
507
|
+
assert s1 > 0, "Starting value must be > 0"
|
|
508
|
+
return s1 * mult_or_n ** np.arange(n)
|