"""
Module implementing the parameter classes for the LogPolarSolver Model
Author: Fabian A. Preiss
"""
from enum import Enum
from typing import Dict, Hashable, Optional, Tuple, cast, Type, Any
import numpy as np
from skimage.registration import phase_cross_correlation
from skimage.transform import AffineTransform
from imgreg.models.logpolar.enums import LogPolParams
from imgreg.util.methods import ImageMethods
from imgreg.util.params import ImageParameter, Parameter
# Root Parameters
[docs]class RefImageParam(ImageParameter):
"""
Reference image.
Attributes
----------
value : numpy.ndarray
"""
def __init__(self, image: Optional[np.ndarray] = None):
super().__init__(LogPolParams.REF_IMG, np.ndarray)
if image is not None:
self.value = image
self.title = "Reference Image"
[docs]class ModImageParam(ImageParameter):
"""
Modified image.
Attributes
----------
value : numpy.ndarray
"""
def __init__(self, image: Optional[np.ndarray] = None):
super().__init__(LogPolParams.MOD_IMG, np.ndarray)
if image is not None:
self.value = image
self.title = "Modified Image"
[docs]class GaussDiffParam(Parameter):
"""
Lower and upper kernel size of the DoG filter.
.. _wgaussdiff:
https://en.wikipedia.org/wiki/Difference_of_Gaussians
Attributes
----------
value : tuple[float, float]
Notes
-----
Typical ratios for image enhancement are in the order of 1:4 and 1:5. [#f3]_
A ratio of 1.6 approximates the Laplacian of Gaussian filter.
References
----------
.. [#f3] `Wikipedia, "Difference of Gaussians" <wgaussdiff_>`_
"""
def __init__(self, gauss_diff: Tuple[float, float]):
# NOTE mypy throws: Argument 2 to "__init__" of "Parameter" has incompatible type "object";
# therefore weird casting of Tuple[float, float] to Type[Any]
super().__init__(LogPolParams.GAUSS_DIFF, cast(Type[Any], Tuple[float, float]))
self.value = gauss_diff
[docs]class WindowWeightParam(Parameter):
"""
Weighting factor scaling beween windowed image and image, range of [0,1].
Attributes
----------
value : str
"""
def __init__(self, window_weight: float):
super().__init__(LogPolParams.WINDOW_WEIGHT, float)
self.value = window_weight
[docs]class WindowTypeParam(Parameter):
"""
Window type for FFT filter, see
`scipy.signal.windows.get_window <https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.windows.get_window.html>`_
for possible choices.
Attributes
----------
value : str
"""
def __init__(self, window_type: str):
super().__init__(LogPolParams.WINDOW_TYPE, str)
self.value = window_type
[docs]class WindowRadiusExpParam(Parameter):
"""
Window radius exponent, larger wrexp => faster computation.
Notes
-----
If a value larger then 1 is used, this introduces a lowpass filter.
Attributes
----------
value : float >= 1
"""
def __init__(self, w_r_exp: float):
super().__init__(LogPolParams.WINDOW_RADIUS_EXP, float)
self.value = w_r_exp
[docs]class UpsamplingParam(Parameter):
"""
Upsampling factor. 1 => no upsampling, 20 => precision to 1/20 of a pixel.
Attributes
----------
value : int
"""
def __init__(self, upsampling: int):
super().__init__(LogPolParams.UPSAMPLING, int)
self.value = upsampling
# Derived Parameters
# NOTE: design currently leads to a full clearing of derived properties although
# bounds is only an optical property and not necessary for the algorithm
[docs]class BoundsParam(Parameter):
"""
Boundaries for image slicing.
Attributes
----------
value : tuple[int, int, int, int]
"""
def __init__(
self, parent_parameters, bounds: Optional[Tuple[int, int, int, int]] = None
):
super().__init__(
LogPolParams.BOUNDS,
BoundsParam._value_function_handle,
parent_parameters=parent_parameters,
)
if bounds is not None:
self.value = bounds
@staticmethod
def _value_function_handle(
params: Dict[Enum, Parameter]
) -> Tuple[int, int, int, int]:
image = params[LogPolParams.REF_IMG].value
wrexp = params[LogPolParams.WINDOW_RADIUS_EXP].value
warp_radius = ImageMethods.compute_warp_radius(min(image.shape), wrexp)
center = np.array(image.shape) // 2
r_lower, r_upper = (
center[0] - warp_radius,
center[0] + warp_radius,
)
c_lower, c_upper = (
center[1] - warp_radius,
center[1] + warp_radius,
)
return (r_lower, r_upper, c_lower, c_upper)
def _calculate_gaussdiff(image_type: Enum, params: Dict[Enum, Parameter]) -> np.ndarray:
return ImageMethods.compute_dgfw(
params[image_type].value,
params[LogPolParams.GAUSS_DIFF].value,
params[LogPolParams.WINDOW_WEIGHT].value,
params[LogPolParams.WINDOW_TYPE].value,
)
[docs]class GaussDiffWindowRefParam(ImageParameter):
"""
Gaussian difference filtered and windowed reference input image.
Attributes
----------
value : numpy.ndarray
"""
def __init__(self, parent_parameters, gauss_diff_ref_img=None):
super().__init__(
LogPolParams.GAUSS_DIFF_REF_IMG,
GaussDiffWindowRefParam._value_function_handle,
parent_parameters=parent_parameters,
)
self.title = "Filtered Reference Image"
if gauss_diff_ref_img is not None:
self.value = gauss_diff_ref_img
@staticmethod
def _value_function_handle(params: Dict[Enum, Parameter]) -> np.ndarray:
return _calculate_gaussdiff(LogPolParams.REF_IMG, params)
[docs]class GaussDiffWindowModParam(ImageParameter):
"""
Gaussian difference filtered and windowed modified input image.
Attributes
----------
value : numpy.ndarray
"""
def __init__(self, parent_parameters, gauss_diff_mod_img=None):
super().__init__(
LogPolParams.GAUSS_DIFF_MOD_IMG,
GaussDiffWindowModParam._value_function_handle,
parent_parameters=parent_parameters,
)
self.title = "Filtered Modified Image"
if gauss_diff_mod_img is not None:
self.value = gauss_diff_mod_img
@staticmethod
def _value_function_handle(params: Dict[Enum, Parameter]) -> np.ndarray:
return _calculate_gaussdiff(LogPolParams.MOD_IMG, params)
[docs]class FourierRefParam(ImageParameter):
"""
Fourier transformed and filtered reference image.
Attributes
----------
value : numpy.ndarray
"""
def __init__(self, parent_parameters, fourier_ref_img=None):
super().__init__(
LogPolParams.FOURIER_REF_IMG,
FourierRefParam._value_function_handle,
parent_parameters=parent_parameters,
)
self.title = "Reference FFT Image\n(magnitude; zoomed)"
self.cmap = "magma"
if fourier_ref_img is not None:
self.value = fourier_ref_img
self.set_bounds_lookup(LogPolParams.BOUNDS)
@staticmethod
def _value_function_handle(params: Dict[Enum, Parameter]) -> np.ndarray:
return ImageMethods.compute_afts(params[LogPolParams.GAUSS_DIFF_REF_IMG].value)
[docs]class FourierModParam(ImageParameter):
"""
Fourier transformed and filtered modified image.
Attributes
----------
value : numpy.ndarray
"""
def __init__(self, parent_parameters, fourier_mod_img=None):
super().__init__(
LogPolParams.FOURIER_MOD_IMG,
FourierModParam._value_function_handle,
parent_parameters=parent_parameters,
)
self.title = "Modified FFT Image\n(magnitude; zoomed)"
self.cmap = "magma"
if fourier_mod_img is not None:
self.value = fourier_mod_img
self.set_bounds_lookup(LogPolParams.BOUNDS)
@staticmethod
def _value_function_handle(params: Dict[Enum, Parameter]) -> np.ndarray:
return ImageMethods.compute_afts(params[LogPolParams.GAUSS_DIFF_MOD_IMG].value)
[docs]class WarpedFourierRefParam(ImageParameter):
"""
Log-polar transformed of the fourier transformed reference image.
Attributes
----------
value : numpy.ndarray
"""
def __init__(self, parent_parameters, warped_fourier_ref_img=None):
super().__init__(
LogPolParams.WARPED_FOURIER_REF_IMG,
WarpedFourierRefParam._value_function_handle,
parent_parameters=parent_parameters,
)
self.title = "Reference FFT Image\n(Log-Polar-Transformed)"
self.cmap = "magma"
if warped_fourier_ref_img is not None:
self.value = warped_fourier_ref_img
@staticmethod
def _value_function_handle(params: Dict[Enum, Parameter]) -> np.ndarray:
return ImageMethods.compute_log_polar_tf(
params[LogPolParams.FOURIER_REF_IMG].value,
params[LogPolParams.WINDOW_RADIUS_EXP].value,
)
[docs]class WarpedFourierModParam(ImageParameter):
"""
Log-polar transformed of the fourier transformed modified image.
Attributes
----------
value : numpy.ndarray
"""
def __init__(self, parent_parameters, warped_fourier_mod_img=None):
super().__init__(
LogPolParams.WARPED_FOURIER_MOD_IMG,
WarpedFourierModParam._value_function_handle,
parent_parameters=parent_parameters,
)
self.title = "Modified FFT Image\n(Log-Polar-Transformed)"
self.cmap = "magma"
if warped_fourier_mod_img is not None:
self.value = warped_fourier_mod_img
@staticmethod
def _value_function_handle(params: Dict[Enum, Parameter]) -> np.ndarray:
return ImageMethods.compute_log_polar_tf(
params[LogPolParams.FOURIER_MOD_IMG].value,
params[LogPolParams.WINDOW_RADIUS_EXP].value,
)
[docs]class RecoveredRotationScalePhaseParam(Parameter):
"""
Recovered rotation angle in degrees and scaling factor including errors.
Notes
-----
The errors are a lower estimate under ideal assumptions and can be much
larger depending on the data.
Attributes
----------
value : tuple[np.ndarray, np.ndarray, dict[str, Hashable]]
"""
def __init__(self, parent_parameters, recovered_rotation_scale_phase=None):
super().__init__(
LogPolParams.RECOVERED_ROTATION_SCALE_PHASE,
RecoveredRotationScalePhaseParam._value_function_handle,
parent_parameters=parent_parameters,
)
if recovered_rotation_scale_phase is not None:
self.value = recovered_rotation_scale_phase
@staticmethod
def _value_function_handle(
params: Dict[Enum, Parameter]
) -> Tuple[np.ndarray, np.ndarray, Dict[str, Hashable]]:
return ImageMethods.recover_rs(
params[LogPolParams.WARPED_FOURIER_REF_IMG].value,
params[LogPolParams.WARPED_FOURIER_MOD_IMG].value,
params[LogPolParams.REF_IMG].value.shape,
params[LogPolParams.UPSAMPLING].value,
params[LogPolParams.WINDOW_RADIUS_EXP].value,
)
[docs]class RecoveredRotationParam(Parameter):
"""
Recovered rotation angle and error in degrees.
Notes
-----
The errors are a lower estimate under ideal assumptions and can be much
larger depending on the data.
Attributes
----------
value : numpy.ndarray
"""
def __init__(self, parent_parameters, recovered_rotation=None):
super().__init__(
LogPolParams.RECOVERED_ROTATION,
RecoveredRotationParam._value_function_handle,
parent_parameters=parent_parameters,
)
if recovered_rotation is not None:
self.value = recovered_rotation
@staticmethod
def _value_function_handle(params: Dict[Enum, Parameter]) -> np.ndarray:
rotation, _, _ = params[LogPolParams.RECOVERED_ROTATION_SCALE_PHASE].value
return cast(np.ndarray, rotation)
[docs]class RecoveredScaleParam(Parameter):
"""
Recovered scaling factor and error in degrees.
Notes
-----
The errors are a lower estimate under ideal assumptions and can be much
larger depending on the data.
Attributes
----------
value : numpy.ndarray
"""
def __init__(self, parent_parameters, recovered_scale=None):
super().__init__(
LogPolParams.RECOVERED_SCALE,
RecoveredScaleParam._value_function_handle,
parent_parameters=parent_parameters,
)
if recovered_scale is not None:
self.value = recovered_scale
@staticmethod
def _value_function_handle(params: Dict[Enum, Parameter]) -> np.ndarray:
_, scale, _ = params[LogPolParams.RECOVERED_ROTATION_SCALE_PHASE].value
return cast(np.ndarray, scale)
[docs]class RecoveredRotScaleParam(ImageParameter):
"""
Rotation and scaling recovered image.
Attributes
----------
value : numpy.ndarray
"""
def __init__(self, parent_parameters, recovered_rot_scale_img=None):
super().__init__(
LogPolParams.RECOVERED_ROT_SCALE_IMG,
RecoveredRotScaleParam._value_function_handle,
parent_parameters=parent_parameters,
)
self.title = "Rotation Recovered Image"
if recovered_rot_scale_img is not None:
self.value = recovered_rot_scale_img
@staticmethod
def _value_function_handle(params: Dict[Enum, Parameter]) -> np.ndarray:
return ImageMethods.compute_rts(
params[LogPolParams.MOD_IMG].value,
angle=params[LogPolParams.RECOVERED_ROTATION].value[0],
scale=params[LogPolParams.RECOVERED_SCALE].value[0],
translation=(0, 0),
inverse=True,
preserve_range=True,
)
# TODO rename angle, rotation such that angle in deg, rot in rad
# TODO possibly define seperate function in ImageMethods
[docs]class RecoveredTranslationParam(Parameter):
"""
Recovered x,y translation vector and error.
Notes
-----
The errors are a lower estimate under ideal assumptions and can be much
larger depending on the data.
Attributes
----------
value : numpy.ndarray
"""
def __init__(self, parent_parameters, recovered_translation=None):
super().__init__(
LogPolParams.RECOVERED_TRANSLATION,
RecoveredTranslationParam._value_function_handle,
parent_parameters=parent_parameters,
)
if recovered_translation is not None:
self.value = recovered_translation
@staticmethod
def _value_function_handle(params: Dict[Enum, Parameter]) -> np.ndarray:
shifts, error, _ = phase_cross_correlation(
params[LogPolParams.REF_IMG].value,
params[LogPolParams.RECOVERED_ROT_SCALE_IMG].value,
upsample_factor=params[LogPolParams.UPSAMPLING].value,
)
tform = AffineTransform(
scale=params[LogPolParams.RECOVERED_SCALE].value[0],
rotation=np.deg2rad(-params[LogPolParams.RECOVERED_ROTATION].value[0]),
)
shifts = tform.params[:2, :2] @ shifts[::-1]
return np.array([*shifts, error])
[docs]class RecoveredRotScaleTr(ImageParameter):
"""
Rotation, scaling and translation recovered image.
Attributes
----------
value : numpy.ndarray
"""
def __init__(self, parent_parameters, recovered_rot_scale_tr_img=None):
super().__init__(
LogPolParams.RECOVERED_ROT_SCALE_TR_IMG,
RecoveredRotScaleTr._value_function_handle,
parent_parameters=parent_parameters,
)
self.title = "Recovered Image"
if recovered_rot_scale_tr_img is not None:
self.value = recovered_rot_scale_tr_img
@staticmethod
def _value_function_handle(params: Dict[Enum, Parameter]) -> np.ndarray:
return ImageMethods.compute_rts(
params[LogPolParams.MOD_IMG].value,
angle=params[LogPolParams.RECOVERED_ROTATION].value[0],
scale=params[LogPolParams.RECOVERED_SCALE].value[0],
translation=params[LogPolParams.RECOVERED_TRANSLATION].value[:-1],
inverse=True,
)