Repository: scikit-image/scikit-image

Issue:
make compare_psnr, compare_ssim, etc less picky about dtypes and range
## Description

As mentioned previously in #2878, functions from `skimage.measure.compare_psnr` fail when the input dtypes do not match.  This is a nuisance in cases where for example one array may be float32 and the other float64.  A specific example of this is that `denoise_nl_means` from releases <=0.13 return a float32 denoised image for float64 input.  The user must then convert to matching dtypes before PSNR, SSIM, etc can be computed.

It would be nice to remove this restriction.  ~~I think some of these also restrict floats to lie in the range [0, 1] or [-1, 1] when this should also not be necessary.~~ 

This is something I will try to make a PR to resolve, but please provide feedback if there is a reason this should not be done.

**edit:**  data ranges appear to be handled properly.


Discussion:

Resolving patch:
diff --git a/skimage/measure/_structural_similarity.py b/skimage/measure/_structural_similarity.py
--- a/skimage/measure/_structural_similarity.py
+++ b/skimage/measure/_structural_similarity.py
@@ -5,7 +5,7 @@
 
 from ..util.dtype import dtype_range
 from ..util.arraycrop import crop
-
+from .._shared.utils import warn
 
 __all__ = ['compare_ssim']
 
@@ -81,9 +81,6 @@ def compare_ssim(X, Y, win_size=None, gradient=False,
        DOI:10.1007/s10043-009-0119-z
 
     """
-    if not X.dtype == Y.dtype:
-        raise ValueError('Input images must have the same dtype.')
-
     if not X.shape == Y.shape:
         raise ValueError('Input images must have the same dimensions.')
 
@@ -148,6 +145,9 @@ def compare_ssim(X, Y, win_size=None, gradient=False,
         raise ValueError('Window size must be odd.')
 
     if data_range is None:
+        if X.dtype != Y.dtype:
+            warn("Inputs have mismatched dtype.  Setting data_range based on "
+                 "X.dtype.")
         dmin, dmax = dtype_range[X.dtype.type]
         data_range = dmax - dmin
 
diff --git a/skimage/measure/simple_metrics.py b/skimage/measure/simple_metrics.py
--- a/skimage/measure/simple_metrics.py
+++ b/skimage/measure/simple_metrics.py
@@ -12,8 +12,6 @@
 
 def _assert_compatible(im1, im2):
     """Raise an error if the shape and dtype do not match."""
-    if not im1.dtype == im2.dtype:
-        raise ValueError('Input images must have the same dtype.')
     if not im1.shape == im2.shape:
         raise ValueError('Input images must have the same dimensions.')
     return
@@ -22,10 +20,8 @@ def _assert_compatible(im1, im2):
 def _as_floats(im1, im2):
     """Promote im1, im2 to nearest appropriate floating point precision."""
     float_type = np.result_type(im1.dtype, im2.dtype, np.float32)
-    if im1.dtype != float_type:
-        im1 = im1.astype(float_type)
-    if im2.dtype != float_type:
-        im2 = im2.astype(float_type)
+    im1 = np.asarray(im1, dtype=float_type)
+    im2 = np.asarray(im2, dtype=float_type)
     return im1, im2
 
 
@@ -133,6 +129,9 @@ def compare_psnr(im_true, im_test, data_range=None, dynamic_range=None):
         data_range = dynamic_range
 
     if data_range is None:
+        if im_true.dtype != im_test.dtype:
+            warn("Inputs have mismatched dtype.  Setting data_range based on "
+                 "im_true.")
         dmin, dmax = dtype_range[im_true.dtype.type]
         true_min, true_max = np.min(im_true), np.max(im_true)
         if true_max > dmax or true_min < dmin:


Test patch:
diff --git a/skimage/measure/tests/test_simple_metrics.py b/skimage/measure/tests/test_simple_metrics.py
--- a/skimage/measure/tests/test_simple_metrics.py
+++ b/skimage/measure/tests/test_simple_metrics.py
@@ -25,9 +25,20 @@ def test_PSNR_vs_IPOL():
 
 def test_PSNR_float():
     p_uint8 = compare_psnr(cam, cam_noisy)
-    p_float64 = compare_psnr(cam/255., cam_noisy/255., data_range=1)
+    p_float64 = compare_psnr(cam / 255., cam_noisy / 255.,
+                             data_range=1)
     assert_almost_equal(p_uint8, p_float64, decimal=5)
 
+    # mixed precision inputs
+    p_mixed = compare_psnr(cam / 255., np.float32(cam_noisy / 255.),
+                           data_range=1)
+    assert_almost_equal(p_mixed, p_float64, decimal=5)
+
+    # mismatched dtype results in a warning if data_range is unspecified
+    with expected_warnings(['Inputs have mismatched dtype']):
+        p_mixed = compare_psnr(cam / 255., np.float32(cam_noisy / 255.))
+    assert_almost_equal(p_mixed, p_float64, decimal=5)
+
 
 def test_PSNR_dynamic_range_and_data_range():
     # Tests deprecation of "dynamic_range" in favor of "data_range"
@@ -41,8 +52,7 @@ def test_PSNR_dynamic_range_and_data_range():
 
 
 def test_PSNR_errors():
-    with testing.raises(ValueError):
-        compare_psnr(cam, cam.astype(np.float32))
+    # shape mismatch
     with testing.raises(ValueError):
         compare_psnr(cam, cam[:-1, :])
 
@@ -54,6 +64,10 @@ def test_NRMSE():
     assert_equal(compare_nrmse(y, x, 'Euclidean'), 1/np.sqrt(3))
     assert_equal(compare_nrmse(y, x, 'min-max'), 1/(y.max()-y.min()))
 
+    # mixed precision inputs are allowed
+    assert_almost_equal(compare_nrmse(y, np.float32(x), 'min-max'),
+                        1 / (y.max() - y.min()))
+
 
 def test_NRMSE_no_int_overflow():
     camf = cam.astype(np.float32)
@@ -66,8 +80,7 @@ def test_NRMSE_no_int_overflow():
 
 def test_NRMSE_errors():
     x = np.ones(4)
-    with testing.raises(ValueError):
-        compare_nrmse(x.astype(np.uint8), x.astype(np.float32))
+    # shape mismatch
     with testing.raises(ValueError):
         compare_nrmse(x[:-1], x)
     # invalid normalization name
diff --git a/skimage/measure/tests/test_structural_similarity.py b/skimage/measure/tests/test_structural_similarity.py
--- a/skimage/measure/tests/test_structural_similarity.py
+++ b/skimage/measure/tests/test_structural_similarity.py
@@ -5,6 +5,7 @@
 from skimage.measure import compare_ssim as ssim
 
 from skimage._shared import testing
+from skimage._shared._warnings import expected_warnings
 from skimage._shared.testing import (assert_equal, assert_almost_equal,
                                      assert_array_almost_equal)
 
@@ -192,23 +193,26 @@ def test_mssim_vs_legacy():
     assert_almost_equal(mssim, mssim_skimage_0pt11)
 
 
-def test_invalid_input():
-    X = np.zeros((3, 3), dtype=np.double)
-    Y = np.zeros((3, 3), dtype=np.int)
-    with testing.raises(ValueError):
-        ssim(X, Y)
+def test_mssim_mixed_dtype():
+    mssim = ssim(cam, cam_noisy)
+    with expected_warnings(['Inputs have mismatched dtype']):
+        mssim_mixed = ssim(cam, cam_noisy.astype(np.float32))
+    assert_almost_equal(mssim, mssim_mixed)
 
-    Y = np.zeros((4, 4), dtype=np.double)
-    with testing.raises(ValueError):
-        ssim(X, Y)
+    # no warning when user supplies data_range
+    mssim_mixed = ssim(cam, cam_noisy.astype(np.float32), data_range=255)
+    assert_almost_equal(mssim, mssim_mixed)
 
-    with testing.raises(ValueError):
-        ssim(X, X, win_size=8)
 
-    # do not allow both image content weighting and gradient calculation
+def test_invalid_input():
+    # size mismatch
+    X = np.zeros((9, 9), dtype=np.double)
+    Y = np.zeros((8, 8), dtype=np.double)
+    with testing.raises(ValueError):
+        ssim(X, Y)
+    # win_size exceeds image extent
     with testing.raises(ValueError):
-        ssim(X, X, image_content_weighting=True,
-             gradient=True)
+        ssim(X, X, win_size=X.shape[0] + 1)
     # some kwarg inputs must be non-negative
     with testing.raises(ValueError):
         ssim(X, X, K1=-0.1)