Repository: scikit-image/scikit-image

Issue:
Phase cross correlation : uncorrect shift in some conditions
Hello,

Here is a simple case that illustrates problems that I meet in a more complicated one.
In some "conditions" (?), the calculated shift is not correct.
In the pseudo 1D example below, I play with 2 parameters:
**length** : which is the length of my trench of my 'profile'
**shift** : which is the shift applied between the 2 'profiles'

with length=20, the calculated shifts are correct in the both cases
with length=10, the calculated shift is not correct in the case of shift=20.

**length** = 10 / **shift** = 15 : **OK**
![image](https://user-images.githubusercontent.com/16154687/124728606-04ec5500-df10-11eb-82a8-428053fa7b4a.png)

**length** = 10 / **shift** = 20 : **NOK**
![image](https://user-images.githubusercontent.com/16154687/124728665-103f8080-df10-11eb-9cce-af6f4d082172.png)

What the reason of this ?
Thanks

Patrick

```
import numpy as np
import matplotlib.pyplot as plt

from skimage.registration import phase_cross_correlation as cross_correl
from skimage.transform import EuclideanTransform, warp


def test(length, shift):
    img = np.ones((2, 100, 10))
    img[0, 0:40, :] = 0.5
    img[0, 50:50 + length, :] = 0.5
    img[1, 0:40 + shift, :] = 0.5
    img[1, 50 + shift:50 + length + shift, :] = 0.5

    shift_calc = cross_correl(img[0], img[1],
                              upsample_factor=1,
                              overlap_ratio=0.3)[0]

    tform = EuclideanTransform(translation=[-shift_calc[1], -shift_calc[0]])
    img_reg = warp(img[1], tform, mode='edge')

    plt.figure(figsize=(8, 6))
    plt.title(f"target : {shift} / calcul : {-shift_calc[0]}")
    plt.plot(img[0, :, 0], 'b-', label='ref')
    plt.plot(img[1, :, 0], 'k--', label='mov', zorder=10)
    plt.plot(img_reg[:, 0], 'r-', label='reg')
    plt.legend()

# TESTS OK
test(length=20, shift=15)
test(length=20, shift=20)
plt.show()

# TESTS NOT OK
test(length=10, shift=15)
test(length=10, shift=20)    # UNCORRECT CALCULATED SHIFT
plt.show()
```


Discussion:

Resolving patch:
diff --git a/skimage/registration/_phase_cross_correlation.py b/skimage/registration/_phase_cross_correlation.py
--- a/skimage/registration/_phase_cross_correlation.py
+++ b/skimage/registration/_phase_cross_correlation.py
@@ -118,14 +118,15 @@ def _compute_error(cross_correlation_max, src_amp, target_amp):
 def phase_cross_correlation(reference_image, moving_image, *,
                             upsample_factor=1, space="real",
                             return_error=True, reference_mask=None,
-                            moving_mask=None, overlap_ratio=0.3):
+                            moving_mask=None, overlap_ratio=0.3,
+                            normalization="phase"):
     """Efficient subpixel image translation registration by cross-correlation.
 
     This code gives the same precision as the FFT upsampled cross-correlation
     in a fraction of the computation time and with reduced memory requirements.
     It obtains an initial estimate of the cross-correlation peak by an FFT and
     then refines the shift estimation by upsampling the DFT only in a small
-    neighborhood of that estimate by means of a matrix-multiply DFT.
+    neighborhood of that estimate by means of a matrix-multiply DFT [1]_.
 
     Parameters
     ----------
@@ -166,6 +167,10 @@ def phase_cross_correlation(reference_image, moving_image, *,
         robustness against spurious matches due to small overlap between
         masked images. Used only if one of ``reference_mask`` or
         ``moving_mask`` is None.
+    normalization : {"phase", None}
+        The type of normalization to apply to the cross-correlation. This
+        parameter is unused when masks (`reference_mask` and `moving_mask`) are
+        supplied.
 
     Returns
     -------
@@ -180,20 +185,41 @@ def phase_cross_correlation(reference_image, moving_image, *,
         Global phase difference between the two images (should be
         zero if images are non-negative).
 
+    Notes
+    -----
+    The use of cross-correlation to estimate image translation has a long
+    history dating back to at least [2]_. The "phase correlation"
+    method (selected by ``normalization="phase"``) was first proposed in [3]_.
+    Publications [1]_ and [2]_ use an unnormalized cross-correlation
+    (``normalization=None``). Which form of normalization is better is
+    application-dependent. For example, the phase correlation method works
+    well in registering images under different illumination, but is not very
+    robust to noise. In a high noise scenario, the unnormalized method may be
+    preferable.
+
+    When masks are provided, a masked normalized cross-correlation algorithm is
+    used [5]_, [6]_.
+
     References
     ----------
     .. [1] Manuel Guizar-Sicairos, Samuel T. Thurman, and James R. Fienup,
            "Efficient subpixel image registration algorithms,"
            Optics Letters 33, 156-158 (2008). :DOI:`10.1364/OL.33.000156`
-    .. [2] James R. Fienup, "Invariant error metrics for image reconstruction"
+    .. [2] P. Anuta, Spatial registration of multispectral and multitemporal
+           digital imagery using fast Fourier transform techniques, IEEE Trans.
+           Geosci. Electron., vol. 8, no. 4, pp. 353–368, Oct. 1970.
+           :DOI:`10.1109/TGE.1970.271435`.
+    .. [3] C. D. Kuglin D. C. Hines. The phase correlation image alignment
+           method, Proceeding of IEEE International Conference on Cybernetics
+           and Society, pp. 163-165, New York, NY, USA, 1975, pp. 163–165.
+    .. [4] James R. Fienup, "Invariant error metrics for image reconstruction"
            Optics Letters 36, 8352-8357 (1997). :DOI:`10.1364/AO.36.008352`
-    .. [3] Dirk Padfield. Masked Object Registration in the Fourier Domain.
+    .. [5] Dirk Padfield. Masked Object Registration in the Fourier Domain.
            IEEE Transactions on Image Processing, vol. 21(5),
            pp. 2706-2718 (2012). :DOI:`10.1109/TIP.2011.2181402`
-    .. [4] D. Padfield. "Masked FFT registration". In Proc. Computer Vision and
+    .. [6] D. Padfield. "Masked FFT registration". In Proc. Computer Vision and
            Pattern Recognition, pp. 2918-2925 (2010).
            :DOI:`10.1109/CVPR.2010.5540032`
-
     """
     if (reference_mask is not None) or (moving_mask is not None):
         return _masked_phase_cross_correlation(reference_image, moving_image,
@@ -218,6 +244,11 @@ def phase_cross_correlation(reference_image, moving_image, *,
     # Whole-pixel shift - Compute cross-correlation by an IFFT
     shape = src_freq.shape
     image_product = src_freq * target_freq.conj()
+    if normalization == "phase":
+        eps = np.finfo(image_product.real.dtype).eps
+        image_product /= np.maximum(np.abs(image_product), 100 * eps)
+    elif normalization is not None:
+        raise ValueError("normalization must be either phase or None")
     cross_correlation = ifftn(image_product)
 
     # Locate maximum


Test patch:
diff --git a/skimage/registration/tests/test_phase_cross_correlation.py b/skimage/registration/tests/test_phase_cross_correlation.py
--- a/skimage/registration/tests/test_phase_cross_correlation.py
+++ b/skimage/registration/tests/test_phase_cross_correlation.py
@@ -12,28 +12,46 @@
 )
 
 
-def test_correlation():
+@pytest.mark.parametrize('normalization', [None, 'phase'])
+def test_correlation(normalization):
     reference_image = fft.fftn(camera())
     shift = (-7, 12)
     shifted_image = fourier_shift(reference_image, shift)
 
     # pixel precision
-    result, error, diffphase = phase_cross_correlation(reference_image,
-                                                       shifted_image,
-                                                       space="fourier")
+    result, _, _ = phase_cross_correlation(reference_image,
+                                           shifted_image,
+                                           space="fourier",
+                                           normalization=normalization)
     assert_allclose(result[:2], -np.array(shift))
 
 
-def test_subpixel_precision():
+@pytest.mark.parametrize('normalization', ['nonexisting'])
+def test_correlation_invalid_normalization(normalization):
+    reference_image = fft.fftn(camera())
+    shift = (-7, 12)
+    shifted_image = fourier_shift(reference_image, shift)
+
+    # pixel precision
+    with pytest.raises(ValueError):
+        phase_cross_correlation(reference_image,
+                                shifted_image,
+                                space="fourier",
+                                normalization=normalization)
+
+
+@pytest.mark.parametrize('normalization', [None, 'phase'])
+def test_subpixel_precision(normalization):
     reference_image = fft.fftn(camera())
     subpixel_shift = (-2.4, 1.32)
     shifted_image = fourier_shift(reference_image, subpixel_shift)
 
     # subpixel precision
-    result, error, diffphase = phase_cross_correlation(reference_image,
-                                                       shifted_image,
-                                                       upsample_factor=100,
-                                                       space="fourier")
+    result, _, _ = phase_cross_correlation(reference_image,
+                                           shifted_image,
+                                           upsample_factor=100,
+                                           space="fourier",
+                                           normalization=normalization)
     assert_allclose(result[:2], -np.array(subpixel_shift), atol=0.05)