Source code for pylorenzmie.lib.CircleTransform
'''Orientation alignment transform for detecting ring-like features.'''
import numpy as np
from numpy.typing import NDArray
from scipy.fft import fft2, ifft2, fftshift
from scipy.signal import savgol_filter
__all__ = ['CircleTransform', 'circletransform']
[docs]
class CircleTransform:
'''Transform an image to emphasize ring-like holographic features.
Implements the orientation alignment transform described in
Krishnatreya & Grier, *Opt. Express* **22**, 12773 (2014).
The kernel is cached so repeated calls on images of the same shape
pay no recomputation cost.
'''
def __init__(self) -> None:
self._kernel = np.ones((1, 1))
[docs]
def kernel(self, shape: tuple[int, int]) -> NDArray[complex]:
'''Fourier-space orientation alignment kernel.
Parameters
----------
shape : tuple of int
``(ny, nx)`` shape of the image to be transformed.
Returns
-------
kernel : ndarray, complex
``K(k) = e^{-2i\\theta} / k``, shifted for FFT pixel ordering.
'''
if shape == self._kernel.shape:
return self._kernel
ny, nx = shape
kx = fftshift(np.linspace(-1., 1, nx, endpoint=False))
ky = fftshift(np.linspace(-1., 1, ny, endpoint=False))
k = np.hypot.outer(ky, kx) + 0.001 # epsilon avoids DC singularity
kernel = np.subtract.outer(1.j * ky, kx) / k
kernel *= kernel / k
self._kernel = kernel
return kernel
[docs]
def transform(self, image: NDArray[float]) -> NDArray[float]:
'''Apply the orientation alignment transform.
Parameters
----------
image : ndarray
Grayscale image data.
Returns
-------
transformed : ndarray
Array with the same shape as *image*, with ring-like features
enhanced. Values are normalized to [0, 1].
'''
psi = np.empty_like(image, dtype=complex)
psi.real = savgol_filter(image, 13, 3, 1, axis=1)
psi.imag = savgol_filter(image, 13, 3, 1, axis=0)
psi *= psi
psi = fft2(psi, workers=-1, overwrite_x=True)
psi *= self.kernel(image.shape)
psi = ifft2(psi, workers=-1, overwrite_x=True)
c = np.square(psi.real) + np.square(psi.imag)
c /= np.max(c)
return c
_transform = CircleTransform()
[docs]
def circletransform(image: NDArray[float]) -> NDArray[float]:
'''Transform an image to emphasize ring-like features.
Convenience wrapper around :class:`CircleTransform` that reuses a
module-level instance so the Fourier kernel is cached across calls.
Parameters
----------
image : ndarray
Grayscale image data.
Returns
-------
transformed : ndarray
Transformed image with ring-like features enhanced.
'''
return _transform.transform(image)
def example() -> None: # pragma: no cover
import matplotlib.pyplot as plt
from pylorenzmie.utilities import example_hologram
a = example_hologram('image0400.png').data
b = circletransform(a)
fig, (axa, axb) = plt.subplots(nrows=2, sharex=True, sharey=True)
axa.imshow(a, cmap='gray')
axb.imshow(b, cmap='gray')
axa.axis('off')
axb.axis('off')
axa.set_xlim(400, 1050)
axa.set_ylim(500, 950)
plt.tight_layout()
plt.show()
if __name__ == '__main__': # pragma: no cover
example()