Repository: scikit-image/scikit-image

Issue:
phase_cross_correlation with disambiguate=True can fail with TypeError: 'NoneType' object is not iterable
### Description:

`phase_cross_correlation` with `disambiguate=True` can fails with `TypeError: 'NoneType' object is not iterable`.

For example this happens if one of the images is empty.


### Way to reproduce:

```python
import numpy as np
import skimage as ski

im = np.random.randint(0, 100, ((10,10)))

ski.registration.phase_cross_correlation(im, im * 0, disambiguate=True)
```

```
Traceback (most recent call last):
  File "<pyshell#7>", line 1, in <module>
    ski.registration.phase_cross_correlation(im, im*0, disambiguate=True)
  File "C:\Python311\Lib\site-packages\skimage\registration\_phase_cross_correlation.py", line 401, in phase_cross_correlation
    shift = _disambiguate_shift(reference_image, moving_image, shift)
  File "C:\Python311\Lib\site-packages\skimage\registration\_phase_cross_correlation.py", line 171, in _disambiguate_shift
    for sl, pos_shift, neg_shift in zip(
TypeError: 'NoneType' object is not iterable
```

### Version information:

```Shell
3.11.0 (main, Oct 24 2022, 18:26:48) [MSC v.1933 64 bit (AMD64)]
Windows-10-10.0.19045-SP0
scikit-image version: 0.21.0
numpy version: 1.24.3
```
```



Discussion:
Stepping through the code in the debugger confirms: [Here](https://github.com/scikit-image/scikit-image/blob/v0.21.0/skimage/registration/_phase_cross_correlation.py#L160) `max_slice` is never changed from `None` and `max_corr` remains `-1`, because `corr` is always `-1` or `nan`.

Probably a line like `if max_slice is None: return shift` should be added between the two loops. Possibly a warning should be logged.
Thanks for reporting and already suggesting a solution, @petsuter! I can reproduce and your suggestion seems like an appropriate fix at a first glance. Do you have time and desire to do a pull request? :D

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
@@ -178,6 +178,13 @@ def _disambiguate_shift(reference_image, moving_image, shift):
         if corr > max_corr:
             max_corr = corr
             max_slice = test_slice
+    if max_slice is None:
+        warnings.warn(
+            f"Could not determine real-space shift for periodic shift {shift!r} "
+            f"as requested by `disambiguate=True` (disambiguation is degenerate).",
+            stacklevel=4,
+        )
+        return shift
     real_shift_acc = []
     for sl, pos_shift, neg_shift in zip(max_slice, positive_shift, negative_shift):
         real_shift_acc.append(pos_shift if sl.stop is None else neg_shift)


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,4 +1,5 @@
 import itertools
+import inspect
 import warnings
 
 import numpy as np
@@ -231,6 +232,23 @@ def test_disambiguate_zero_shift(disambiguate):
     np.testing.assert_array_equal(computed_shift, np.array((0.0, 0.0)))
 
 
+def test_disambiguate_empty_image():
+    """When the image is empty, disambiguation becomes degenerate."""
+    image = camera()
+    regex = "Could not determine real-space shift"
+    with pytest.warns(UserWarning, match=regex) as record:
+        shift, error, phasediff = phase_cross_correlation(
+            image, image * 0, disambiguate=True
+        )
+        expected_lineno = inspect.currentframe().f_lineno - 3
+    np.testing.assert_array_equal(shift, np.array([0.0, 0.0]))
+    assert np.isnan(error)
+    assert phasediff == 0.0
+    # Assert correct stacklevel
+    assert record[0].filename == __file__
+    assert record[0].lineno == expected_lineno
+
+
 @pytest.mark.parametrize("return_error", [False, True, "always"])
 def test_deprecated_return_error(return_error):
     img = np.ones((10, 10))