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.
@@ -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)