Repository: scikit-image/scikit-image

Issue:
API inconsistency: phase_cross_correlation does not return error & phasediff
## Description

If you use the `skimage.registration.phase_cross_correlation()` method without using one of the mask keyword arguments, the function returns a tuple of three items (shift, error, phasediff) is returned. But if you use the same function and pass it a mask keyword argument, then you do not get a tuple of three items returned but instead just a single numpy array of the shift.

I think the number of things returned should be consistent regardless of which keyword arguments  were passed to the function. This is also inconsistent with the [documentation](https://scikit-image.org/docs/stable/api/skimage.registration.html#skimage.registration.phase_cross_correlation) for this function.

## Way to reproduce
```python
from skimage import data
from skimage.registration import phase_cross_correlation

reference_image = data.camera()
EXPECTED_SHIFT = 20  # PIXELS
moving_image = np.roll(reference_image, EXPECTED_SHIFT, axis=1)

# Calculate a simple shift
shift, error, phasediff = phase_cross_correlation(reference_image, moving_image)
print(shift, error, phasediff)

# Do the same thing, this time adding a mask keyword argument
# Mask a single pixel
mask = np.ones(reference_image.shape).astype(bool)
mask[0][0] = False
shift, error, phasediff = phase_cross_correlation(reference_image, moving_image, reference_mask=mask)
print(shift, error, phasediff)
```

Which gives us the output:
```python-traceback
[  0. -20.] 3.332000937312528e-08 -1.64296573239739e-17

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-95-e4901b97d58b> in <module>
     13 mask[0][0] = False
     14 
---> 15 shift, error, phasediff = phase_cross_correlation(reference_image, moving_image, reference_mask=mask)
     16 print(shift, error, phasediff)

ValueError: not enough values to unpack (expected 3, got 2)

```

The result from the second case is a single numpy array, containing the shift. 

## Version information
```python
# Paste the output of the following python commands
from __future__ import print_function
import sys; print(sys.version)
import platform; print(platform.platform())
import skimage; print("scikit-image version: {}".format(skimage.__version__))
import numpy; print("numpy version: {}".format(numpy.__version__))
```

```python
3.6.8 |Anaconda, Inc.| (default, Feb 21 2019, 18:30:04) [MSC v.1916 64 bit (AMD64)]
Windows-10-10.0.17134-SP0
scikit-image version: 0.18.dev0
numpy version: 1.16.4

```



Discussion:
Thank you @GenevieveBuckley for reporting this. You are right the documentation is not precise enough... `phase_cross_correlation` uses `masked_register_translation` under the hood when any of `reference_mask` or `moving_mask` is provided, this function doesn't provide any of `error` or `phasediff`.

But if the `return_error` argument is set to `False`, `phase_cross_correlation` only returns `shifts`. One solution would be to raise an error if a mask is provided and `return_error` is True.
I would also suggest to set `return_error` to `False`  by default and deprecate it...
Yeah I see why it's happening, but I would prefer more consistency across related functions.

I don't have any particular preference about how to resolve that (but I also don't really use `error` or `phasediff`, so I might under-value the usefulness of having them there).
Is it something which has been introduced recently? I'm tagging this as 0.18 in case it's a new thing and we want to fix it before the release.
Just wanted to add a couple of points here. The results are different based on passing a mask or not. 

Here is a toy example with a (5,5) shift introduced. When running with an "all true" mask:

```
 phase_cross_correlation(chip_a, chip_b, upsample_factor=8,                             
        reference_mask=np.ones(chip_a.shape).astype('bool'),
        moving_mask=np.ones(chip_a.shape).astype('bool'))
```

I get a shift of `array([4., 6.])` whereas when you do this without passing a mask for the same chips:

 ```
  phase_cross_correlation(chip_a, chip_b, upsample_factor=8)
 ```
 
 the shift obtained is `(array([3.625, 5.5  ]), 0.6189459615493899, 3.03756883711757e-05)`
 
 Ignoring the differences in outputs (as reported above), I guess the documentation might be updated to indicate that in case of a masked operation, the user should expect integer shifts only. 
 
 I do find the RMS error useful. While registering large images, I fit a polynomial for the warping function, and the RMS can be useful to exclude "bad" registration fits. But not sure if it's generally useful. Just my 2 cents!
I'm leaning on the side of keeping `error` and `phasediff` available for users and return NaN when these values are not available. However, because `return_error` already defaults to `True` I don't see a sensible deprecation path towards that behavior. Maybe this would work:

- Now: Raise a warning that next release will start returning `error` and `phasediff` (as NaN if mask is supplied). The warning goes away with `return_error="always"`, which also changes to the new behavior of always returning `error` and `phasediff`.
- Next release: deprecate `return_error`

Using `return_error="always"` feels a little messy but I think that's the sanest option if we want to keep it. @rfezzani would you be onboard with this approach too?
Sorry @lagru for my late answer :pray: 
:+1: I like your proposition :wink:.

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
@@ -3,6 +3,8 @@
 http://www.mathworks.com/matlabcentral/fileexchange/18401-efficient-subpixel-image-registration-by-cross-correlation
 """
 
+import warnings
+
 import numpy as np
 from scipy.fft import fftn, ifftn, fftfreq
 
@@ -141,10 +143,10 @@ def phase_cross_correlation(reference_image, moving_image, *,
         data will bypass FFT of input data. Case insensitive. Not
         used if any of ``reference_mask`` or ``moving_mask`` is not
         None.
-    return_error : bool, optional
-        Returns error and phase difference if on, otherwise only
-        shifts are returned. Has noeffect if any of ``reference_mask`` or
-        ``moving_mask`` is not None. In this case only shifts is returned.
+    return_error : bool, {"always"}, optional
+        Returns error and phase difference if "always" is given. If False, or
+        either ``reference_mask`` or ``moving_mask`` are given, only shifts are
+        returned.
     reference_mask : ndarray
         Boolean mask for ``reference_image``. The mask should evaluate
         to ``True`` (or 1) on valid pixels. ``reference_mask`` should
@@ -174,10 +176,14 @@ def phase_cross_correlation(reference_image, moving_image, *,
         numpy (e.g. Z, Y, X)
     error : float
         Translation invariant normalized RMS error between
-        ``reference_image`` and ``moving_image``.
+        ``reference_image`` and ``moving_image``. For masked cross-correlation
+        this error is not available and NaN is returned if ``return_error``
+        is "always".
     phasediff : float
         Global phase difference between the two images (should be
-        zero if images are non-negative).
+        zero if images are non-negative). For masked cross-correlation
+        this phase difference is not available and NaN is returned if
+        ``return_error`` is "always".
 
     Notes
     -----
@@ -215,10 +221,27 @@ def phase_cross_correlation(reference_image, moving_image, *,
            Pattern Recognition, pp. 2918-2925 (2010).
            :DOI:`10.1109/CVPR.2010.5540032`
     """
+    def warn_return_error():
+        warnings.warn(
+            "In scikit-image 0.21, phase_cross_correlation will start "
+            "returning a tuple or 3 items (shift, error, phasediff) always. "
+            "To enable the new return behavior and silence this warning, use "
+            "return_error='always'.",
+            category=FutureWarning,
+            stacklevel=3,
+        )
+
     if (reference_mask is not None) or (moving_mask is not None):
-        return _masked_phase_cross_correlation(reference_image, moving_image,
-                                               reference_mask, moving_mask,
-                                               overlap_ratio)
+        shifts = _masked_phase_cross_correlation(
+            reference_image, moving_image,
+            reference_mask, moving_mask,
+            overlap_ratio
+        )
+        if return_error == "always":
+            return shifts, np.nan, np.nan
+        else:
+            warn_return_error()
+            return shifts
 
     # images must be the same shape
     if reference_image.shape != moving_image.shape:
@@ -310,4 +333,5 @@ def phase_cross_correlation(reference_image, moving_image, *,
         return shifts, _compute_error(CCmax, src_amp, target_amp),\
             _compute_phasediff(CCmax)
     else:
+        warn_return_error()
         return shifts


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
@@ -1,3 +1,6 @@
+import warnings
+import re
+
 import numpy as np
 import pytest
 from numpy.testing import assert_allclose
@@ -167,6 +170,52 @@ def test_4d_input_subpixel():
     assert_allclose(result, -np.array(subpixel_shift), atol=0.05)
 
 
+@pytest.mark.parametrize("return_error", [True, False, "always"])
+@pytest.mark.parametrize("reference_mask", [None, True])
+def test_phase_cross_correlation_deprecation(return_error, reference_mask):
+    # For now, assert that phase_cross_correlation raises a warning that
+    # returning only shifts is deprecated. In skimage 0.21, this test should be
+    # updated for the deprecation of the return_error parameter.
+    should_warn = (
+        return_error is False
+        or (return_error != "always" and reference_mask is True)
+    )
+
+    reference_image = np.ones((10, 10))
+    moving_image = np.ones_like(reference_image)
+    if reference_mask is True:
+        # moving_mask defaults to reference_mask, passing moving_mask only is
+        # not supported, so we don't need to test it
+        reference_mask = np.ones_like(reference_image)
+
+    if should_warn:
+        msg = (
+            "In scikit-image 0.21, phase_cross_correlation will start "
+            "returning a tuple or 3 items (shift, error, phasediff) always. "
+            "To enable the new return behavior and silence this warning, use "
+            "return_error='always'."
+        )
+        with pytest.warns(FutureWarning, match=re.escape(msg)):
+            out = phase_cross_correlation(
+                reference_image=reference_image,
+                moving_image=moving_image,
+                return_error=return_error,
+                reference_mask=reference_mask,
+            )
+        assert not isinstance(out, tuple)
+    else:
+        with warnings.catch_warnings():
+            warnings.simplefilter("error")
+            out = phase_cross_correlation(
+                reference_image=reference_image,
+                moving_image=moving_image,
+                return_error=return_error,
+                reference_mask=reference_mask,
+            )
+        assert isinstance(out, tuple)
+        assert len(out) == 3
+
+
 def test_mismatch_upsampled_region_size():
     with pytest.raises(ValueError):
         _upsampled_dft(