From ee38b920d9b48159632b8190780944b156275a64 Mon Sep 17 00:00:00 2001 From: "Gregory R. Lee" Date: Sat, 23 Jan 2016 14:25:11 -0500 Subject: [PATCH 01/14] ENH: Add some simple image comparison metrics: PSNR and NRMSE --- skimage/measure/_simple_metrics.py | 117 +++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 skimage/measure/_simple_metrics.py diff --git a/skimage/measure/_simple_metrics.py b/skimage/measure/_simple_metrics.py new file mode 100644 index 00000000..39d32680 --- /dev/null +++ b/skimage/measure/_simple_metrics.py @@ -0,0 +1,117 @@ +from __future__ import division + +import numpy as np +from ..util.dtype import dtype_range + +__all__ = ['mse', 'nrmse', 'psnr'] + + +def mse(X, Y): + """ compute mean-squared error between two images. + + Parameters + ---------- + X, Y : ndarray + Image. Any dimensionality. + + Returns + ------- + mse : float + The MSE metric. + + """ + if not X.shape == Y.shape: + raise ValueError('Input images must have the same dimensions.') + if not X.dtype == Y.dtype: + raise ValueError('Input images must have the same dtype.') + return np.square(X - Y).mean() + + +def nrmse(im_true, im, norm_type='Euclidean'): + """ compute the normalized root mean-squared error between two images. + + Parameters + ---------- + im_true : ndarray + Ground-truth image. + im : ndarray + Test image. + norm_type : {'Euclidean', 'min-max', 'mean'} + Controls the normalization method to use in the denominator of the + NRMSE. + + Returns + ------- + nrmse : float + The NRMSE metric. + + Notes + ----- + There is no standard method of normalization across the literature [1]_. + The methods available here are as follows: + + - 'Euclidean' : normalize by the Euclidean norm of ``im_true``. + - 'min-max' : normalize by the intensity range of ``im_true``. + - 'mean' : normalize by the mean of ``im_true``. + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Root-mean-square_deviation + + """ + if not im_true.dtype == im.dtype: + raise ValueError('Input images must have the same dtype.') + + if not im_true.shape == im.shape: + raise ValueError('Input images must have the same dimensions.') + + norm_type = norm_type.lower() + if norm_type == 'euclidean': + denom = np.sqrt((im_true*im_true).mean()) + elif norm_type == 'min-max': + denom = im_true.max() - im_true.min() + elif norm_type == 'mean': + denom = im_true.mean() + return np.sqrt(mse(im_true, im)) / denom + + +def psnr(im_true, im, dynamic_range=None): + """ Compute the peak signal to noise ratio (PSNR) for an image. + + Parameters + ---------- + im_true : ndarray + Ground-truth image. + im : ndarray + Test image. + dynamic_range : int + The dynamic range of the input image (distance between minimum and + maximum possible values). By default, this is estimated from the image + data-type. + + Returns + ------- + psnr : float + The PSNR metric. + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Peak_signal-to-noise_ratio + + """ + if not im_true.dtype == im.dtype: + raise ValueError('Input images must have the same dtype.') + + if not im_true.shape == im.shape: + raise ValueError('Input images must have the same dimensions.') + + if dynamic_range is None: + dmin, dmax = dtype_range[im_true.dtype.type] + dynamic_range = dmax - dmin + + im_true = im_true.astype(np.float64) + im = im.astype(np.float64) + + err = mse(im_true, im) + psnr = 10 * np.log10((dynamic_range ** 2) / err) + return psnr From ef3896d013365f07a256a20672169767bf852a42 Mon Sep 17 00:00:00 2001 From: "Gregory R. Lee" Date: Sat, 23 Jan 2016 20:48:12 -0500 Subject: [PATCH 02/14] add new metrics to API --- skimage/measure/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/skimage/measure/__init__.py b/skimage/measure/__init__.py index 9e8df953..86464d7c 100755 --- a/skimage/measure/__init__.py +++ b/skimage/measure/__init__.py @@ -2,6 +2,7 @@ from ._find_contours import find_contours from ._marching_cubes import (marching_cubes, mesh_surface_area, correct_mesh_orientation) from ._regionprops import regionprops, perimeter +from ._simple_metrics import mse, nrmse, psnr from ._structural_similarity import structural_similarity from ._polygon import approximate_polygon, subdivide_polygon from ._pnpoly import points_in_poly, grid_points_in_poly @@ -34,4 +35,7 @@ __all__ = ['find_contours', 'profile_line', 'label', 'points_in_poly', - 'grid_points_in_poly'] + 'grid_points_in_poly', + 'mse', + 'nrmse', + 'psnr'] From 69e73ea52b5b0d6ef1edfd1e1203f40d3d7bedf5 Mon Sep 17 00:00:00 2001 From: "Gregory R. Lee" Date: Sat, 23 Jan 2016 20:48:38 -0500 Subject: [PATCH 03/14] TST: add tests for nrmse, psnr --- skimage/measure/_simple_metrics.py | 3 +- skimage/measure/tests/test_simple_metrics.py | 43 ++++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 skimage/measure/tests/test_simple_metrics.py diff --git a/skimage/measure/_simple_metrics.py b/skimage/measure/_simple_metrics.py index 39d32680..2848b42c 100644 --- a/skimage/measure/_simple_metrics.py +++ b/skimage/measure/_simple_metrics.py @@ -113,5 +113,4 @@ def psnr(im_true, im, dynamic_range=None): im = im.astype(np.float64) err = mse(im_true, im) - psnr = 10 * np.log10((dynamic_range ** 2) / err) - return psnr + return 10 * np.log10((dynamic_range ** 2) / err) diff --git a/skimage/measure/tests/test_simple_metrics.py b/skimage/measure/tests/test_simple_metrics.py new file mode 100644 index 00000000..0e7a79bf --- /dev/null +++ b/skimage/measure/tests/test_simple_metrics.py @@ -0,0 +1,43 @@ +import numpy as np +from numpy.testing import assert_equal, assert_raises, assert_almost_equal + +from skimage.measure import psnr, nrmse +import skimage.data + +np.random.seed(5) +cam = skimage.data.camera() +sigma = 20.0 +cam_noisy = np.clip(cam + sigma * np.random.randn(*cam.shape), 0, 255) +cam_noisy = cam_noisy.astype(cam.dtype) + + +def test_PSNR_vs_IPOL(): + # Tests vs. imdiff result from the following IPOL article and code: + # http://www.ipol.im/pub/art/2011/g_lmii/ + p_IPOL = 22.4497 + p = psnr(cam, cam_noisy) + assert_almost_equal(p, p_IPOL, decimal=4) + + +def test_PSNR_float(): + p_uint8 = psnr(cam, cam_noisy) + p_float64 = psnr(cam/255., cam_noisy/255., dynamic_range=1) + assert_almost_equal(p_uint8, p_float64) + + +def test_PSNR_errors(): + assert_raises(ValueError, psnr, cam, cam.astype(np.float32)) + assert_raises(ValueError, psnr, cam, cam[:-1, :]) + + +def test_NRMSE(): + x = np.ones(4) + y = np.asarray([0., 2., 2., 2.]) + assert_equal(nrmse(y, x, 'mean'), 1/np.mean(y)) + assert_equal(nrmse(y, x, 'Euclidean'), 0.5) + assert_equal(nrmse(y, x, 'min-max'), 0.5) + + assert_equal(nrmse(x, x), 0) + + assert_raises(ValueError, nrmse, x.astype(np.uint8), y) + assert_raises(ValueError, nrmse, x[:-1], y) From cfb9c760db3eb51d7e6215a93ff3eaae5d3eed12 Mon Sep 17 00:00:00 2001 From: "Gregory R. Lee" Date: Sat, 23 Jan 2016 21:35:36 -0500 Subject: [PATCH 04/14] TST: fix broken test --- skimage/measure/tests/test_simple_metrics.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/measure/tests/test_simple_metrics.py b/skimage/measure/tests/test_simple_metrics.py index 0e7a79bf..cab25ab7 100644 --- a/skimage/measure/tests/test_simple_metrics.py +++ b/skimage/measure/tests/test_simple_metrics.py @@ -34,8 +34,8 @@ def test_NRMSE(): x = np.ones(4) y = np.asarray([0., 2., 2., 2.]) assert_equal(nrmse(y, x, 'mean'), 1/np.mean(y)) - assert_equal(nrmse(y, x, 'Euclidean'), 0.5) - assert_equal(nrmse(y, x, 'min-max'), 0.5) + assert_equal(nrmse(y, x, 'Euclidean'), 1/np.sqrt(3)) + assert_equal(nrmse(y, x, 'min-max'), 1/(y.max()-y.min())) assert_equal(nrmse(x, x), 0) From 72607ca99e8ee01b92077d60a6271b7c2f82e33f Mon Sep 17 00:00:00 2001 From: "Gregory R. Lee" Date: Sun, 24 Jan 2016 12:38:38 -0500 Subject: [PATCH 05/14] update NRMSE docstrings and include run_module_suite in the corresponding test file --- .../{_simple_metrics.py => simple_metrics.py} | 44 +++++++++---------- skimage/measure/tests/test_simple_metrics.py | 7 ++- 2 files changed, 27 insertions(+), 24 deletions(-) rename skimage/measure/{_simple_metrics.py => simple_metrics.py} (69%) diff --git a/skimage/measure/_simple_metrics.py b/skimage/measure/simple_metrics.py similarity index 69% rename from skimage/measure/_simple_metrics.py rename to skimage/measure/simple_metrics.py index 2848b42c..883b4dbd 100644 --- a/skimage/measure/_simple_metrics.py +++ b/skimage/measure/simple_metrics.py @@ -7,7 +7,7 @@ __all__ = ['mse', 'nrmse', 'psnr'] def mse(X, Y): - """ compute mean-squared error between two images. + """Compute the mean-squared error between two images. Parameters ---------- @@ -27,42 +27,38 @@ def mse(X, Y): return np.square(X - Y).mean() -def nrmse(im_true, im, norm_type='Euclidean'): - """ compute the normalized root mean-squared error between two images. +def nrmse(im_true, im_test, norm_type='Euclidean'): + """Compute the normalized root mean-squared error between two images. Parameters ---------- im_true : ndarray Ground-truth image. - im : ndarray + im_test : ndarray Test image. norm_type : {'Euclidean', 'min-max', 'mean'} Controls the normalization method to use in the denominator of the - NRMSE. + NRMSE. There is no standard method of normalization across the + literature [1]_. The methods available here are as follows: + + - 'Euclidean' : normalize by the Euclidean norm of ``im_true``. + - 'min-max' : normalize by the intensity range of ``im_true``. + - 'mean' : normalize by the mean of ``im_true``. Returns ------- nrmse : float The NRMSE metric. - Notes - ----- - There is no standard method of normalization across the literature [1]_. - The methods available here are as follows: - - - 'Euclidean' : normalize by the Euclidean norm of ``im_true``. - - 'min-max' : normalize by the intensity range of ``im_true``. - - 'mean' : normalize by the mean of ``im_true``. - References ---------- .. [1] https://en.wikipedia.org/wiki/Root-mean-square_deviation """ - if not im_true.dtype == im.dtype: + if not im_true.dtype == im_test.dtype: raise ValueError('Input images must have the same dtype.') - if not im_true.shape == im.shape: + if not im_true.shape == im_test.shape: raise ValueError('Input images must have the same dimensions.') norm_type = norm_type.lower() @@ -72,17 +68,19 @@ def nrmse(im_true, im, norm_type='Euclidean'): denom = im_true.max() - im_true.min() elif norm_type == 'mean': denom = im_true.mean() - return np.sqrt(mse(im_true, im)) / denom + else: + raise ValueError("Unsupported norm_type") + return np.sqrt(mse(im_true, im_test)) / denom -def psnr(im_true, im, dynamic_range=None): +def psnr(im_true, im_test, dynamic_range=None): """ Compute the peak signal to noise ratio (PSNR) for an image. Parameters ---------- im_true : ndarray Ground-truth image. - im : ndarray + im_test : ndarray Test image. dynamic_range : int The dynamic range of the input image (distance between minimum and @@ -99,10 +97,10 @@ def psnr(im_true, im, dynamic_range=None): .. [1] https://en.wikipedia.org/wiki/Peak_signal-to-noise_ratio """ - if not im_true.dtype == im.dtype: + if not im_true.dtype == im_test.dtype: raise ValueError('Input images must have the same dtype.') - if not im_true.shape == im.shape: + if not im_true.shape == im_test.shape: raise ValueError('Input images must have the same dimensions.') if dynamic_range is None: @@ -110,7 +108,7 @@ def psnr(im_true, im, dynamic_range=None): dynamic_range = dmax - dmin im_true = im_true.astype(np.float64) - im = im.astype(np.float64) + im_test = im_test.astype(np.float64) - err = mse(im_true, im) + err = mse(im_true, im_test) return 10 * np.log10((dynamic_range ** 2) / err) diff --git a/skimage/measure/tests/test_simple_metrics.py b/skimage/measure/tests/test_simple_metrics.py index cab25ab7..43122e01 100644 --- a/skimage/measure/tests/test_simple_metrics.py +++ b/skimage/measure/tests/test_simple_metrics.py @@ -1,5 +1,6 @@ import numpy as np -from numpy.testing import assert_equal, assert_raises, assert_almost_equal +from numpy.testing import (run_module_suite, assert_equal, assert_raises, + assert_almost_equal) from skimage.measure import psnr, nrmse import skimage.data @@ -41,3 +42,7 @@ def test_NRMSE(): assert_raises(ValueError, nrmse, x.astype(np.uint8), y) assert_raises(ValueError, nrmse, x[:-1], y) + + +if __name__ == "__main__": + run_module_suite() From a681ccd3ee8c8b352e088f52858f17c009fc3cee Mon Sep 17 00:00:00 2001 From: "Gregory R. Lee" Date: Tue, 26 Jan 2016 17:56:22 -0500 Subject: [PATCH 06/14] TST: add test for invalid normalization type in NRMSE --- skimage/measure/tests/test_simple_metrics.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/skimage/measure/tests/test_simple_metrics.py b/skimage/measure/tests/test_simple_metrics.py index 43122e01..b209b81d 100644 --- a/skimage/measure/tests/test_simple_metrics.py +++ b/skimage/measure/tests/test_simple_metrics.py @@ -40,8 +40,13 @@ def test_NRMSE(): assert_equal(nrmse(x, x), 0) - assert_raises(ValueError, nrmse, x.astype(np.uint8), y) - assert_raises(ValueError, nrmse, x[:-1], y) + +def test_NRMSE_errors(): + x = np.ones(4) + assert_raises(ValueError, nrmse, x.astype(np.uint8), x.astype(np.float32)) + assert_raises(ValueError, nrmse, x[:-1], x) + # invalid normalization name + assert_raises(ValueError, nrmse, x, x, 'foo') if __name__ == "__main__": From f4773e303de43f8c5f1193fd23bb841394c77134 Mon Sep 17 00:00:00 2001 From: "Gregory R. Lee" Date: Tue, 26 Jan 2016 18:17:02 -0500 Subject: [PATCH 07/14] NRMSE, MSE, PSNR: make sure all inputs are float prior to the calculations. --- skimage/measure/simple_metrics.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/skimage/measure/simple_metrics.py b/skimage/measure/simple_metrics.py index 883b4dbd..726122b1 100644 --- a/skimage/measure/simple_metrics.py +++ b/skimage/measure/simple_metrics.py @@ -6,6 +6,16 @@ from ..util.dtype import dtype_range __all__ = ['mse', 'nrmse', 'psnr'] +def _as_floats(X, Y): + """Promote X, Y to nearest appropriate floating point precision.""" + float_type = np.result_type(X.dtype, Y.dtype, np.float32) + if X.dtype != float_type: + X = X.astype(float_type) + if Y.dtype != float_type: + Y = Y.astype(float_type) + return X, Y + + def mse(X, Y): """Compute the mean-squared error between two images. @@ -20,6 +30,8 @@ def mse(X, Y): The MSE metric. """ + X, Y = _as_floats(X, Y) + if not X.shape == Y.shape: raise ValueError('Input images must have the same dimensions.') if not X.dtype == Y.dtype: @@ -61,6 +73,8 @@ def nrmse(im_true, im_test, norm_type='Euclidean'): if not im_true.shape == im_test.shape: raise ValueError('Input images must have the same dimensions.') + im_true, im_test = _as_floats(im_true, im_test) + norm_type = norm_type.lower() if norm_type == 'euclidean': denom = np.sqrt((im_true*im_true).mean()) @@ -107,8 +121,7 @@ def psnr(im_true, im_test, dynamic_range=None): dmin, dmax = dtype_range[im_true.dtype.type] dynamic_range = dmax - dmin - im_true = im_true.astype(np.float64) - im_test = im_test.astype(np.float64) + im_true, im_test = _as_floats(im_true, im_test) err = mse(im_true, im_test) return 10 * np.log10((dynamic_range ** 2) / err) From a8124166c1b0ee875a055b976529fe002ecaf4cc Mon Sep 17 00:00:00 2001 From: "Gregory R. Lee" Date: Tue, 26 Jan 2016 18:17:56 -0500 Subject: [PATCH 08/14] Fix the default dynamic_range in PSNR and warn if inputs don't meet the assumptions made --- skimage/measure/simple_metrics.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/skimage/measure/simple_metrics.py b/skimage/measure/simple_metrics.py index 726122b1..a8cb4078 100644 --- a/skimage/measure/simple_metrics.py +++ b/skimage/measure/simple_metrics.py @@ -119,7 +119,15 @@ def psnr(im_true, im_test, dynamic_range=None): if dynamic_range is None: dmin, dmax = dtype_range[im_true.dtype.type] - dynamic_range = dmax - dmin + dynamic_range = dmax # assume non-negative inputs + if im_true.min() < 0: + raise ValueError( + "im_true contains negative values. Please manually specify " + "the dynamic range.") + if im_true.max() > dmax: + raise ValueError( + "im_true has a larger maximum intensity than expected for its " + "data type. Please manually specify the dynamic_range") im_true, im_test = _as_floats(im_true, im_test) From acef02667620bb4b1137132c55120c5a03b575c8 Mon Sep 17 00:00:00 2001 From: "Gregory R. Lee" Date: Tue, 26 Jan 2016 18:21:12 -0500 Subject: [PATCH 09/14] fixed outdated import name _simple_metrics in __init__.py --- skimage/measure/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/measure/__init__.py b/skimage/measure/__init__.py index 86464d7c..de865f9e 100755 --- a/skimage/measure/__init__.py +++ b/skimage/measure/__init__.py @@ -2,7 +2,7 @@ from ._find_contours import find_contours from ._marching_cubes import (marching_cubes, mesh_surface_area, correct_mesh_orientation) from ._regionprops import regionprops, perimeter -from ._simple_metrics import mse, nrmse, psnr +from .simple_metrics import mse, nrmse, psnr from ._structural_similarity import structural_similarity from ._polygon import approximate_polygon, subdivide_polygon from ._pnpoly import points_in_poly, grid_points_in_poly From ea3ad97b3e656a7466757e1197110ae135c4a180 Mon Sep 17 00:00:00 2001 From: "Gregory R. Lee" Date: Wed, 27 Jan 2016 11:24:56 -0500 Subject: [PATCH 10/14] TST: add MSE, NRMSE test that would fail in case of int overflow --- skimage/measure/tests/test_simple_metrics.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/skimage/measure/tests/test_simple_metrics.py b/skimage/measure/tests/test_simple_metrics.py index b209b81d..f1ab274a 100644 --- a/skimage/measure/tests/test_simple_metrics.py +++ b/skimage/measure/tests/test_simple_metrics.py @@ -2,7 +2,7 @@ import numpy as np from numpy.testing import (run_module_suite, assert_equal, assert_raises, assert_almost_equal) -from skimage.measure import psnr, nrmse +from skimage.measure import psnr, nrmse, mse import skimage.data np.random.seed(5) @@ -23,7 +23,7 @@ def test_PSNR_vs_IPOL(): def test_PSNR_float(): p_uint8 = psnr(cam, cam_noisy) p_float64 = psnr(cam/255., cam_noisy/255., dynamic_range=1) - assert_almost_equal(p_uint8, p_float64) + assert_almost_equal(p_uint8, p_float64, decimal=5) def test_PSNR_errors(): @@ -38,7 +38,14 @@ def test_NRMSE(): assert_equal(nrmse(y, x, 'Euclidean'), 1/np.sqrt(3)) assert_equal(nrmse(y, x, 'min-max'), 1/(y.max()-y.min())) - assert_equal(nrmse(x, x), 0) + +def test_NRMSE_no_int_overflow(): + camf = cam.astype(np.float32) + cam_noisyf = cam_noisy.astype(np.float32) + assert_almost_equal(mse(cam, cam_noisy), + mse(camf, cam_noisyf)) + assert_almost_equal(nrmse(cam, cam_noisy), + nrmse(camf, cam_noisyf)) def test_NRMSE_errors(): From 64f62032c3984287d167ebe615d664e33757a340 Mon Sep 17 00:00:00 2001 From: "Gregory R. Lee" Date: Wed, 27 Jan 2016 11:45:32 -0500 Subject: [PATCH 11/14] psnr: update dynamic range calculation also moved the identical dtype and shape checks into a separate utility function --- skimage/measure/simple_metrics.py | 46 ++++++++++++++----------------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/skimage/measure/simple_metrics.py b/skimage/measure/simple_metrics.py index a8cb4078..01bd0f29 100644 --- a/skimage/measure/simple_metrics.py +++ b/skimage/measure/simple_metrics.py @@ -6,6 +6,15 @@ from ..util.dtype import dtype_range __all__ = ['mse', 'nrmse', 'psnr'] +def _assert_compatible(X, Y): + """Raise an error if the shape and dtype do not match.""" + 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.') + return + + def _as_floats(X, Y): """Promote X, Y to nearest appropriate floating point precision.""" float_type = np.result_type(X.dtype, Y.dtype, np.float32) @@ -30,12 +39,8 @@ def mse(X, Y): The MSE metric. """ + _assert_compatible(X, Y) X, Y = _as_floats(X, Y) - - if not X.shape == Y.shape: - raise ValueError('Input images must have the same dimensions.') - if not X.dtype == Y.dtype: - raise ValueError('Input images must have the same dtype.') return np.square(X - Y).mean() @@ -67,12 +72,7 @@ def nrmse(im_true, im_test, norm_type='Euclidean'): .. [1] https://en.wikipedia.org/wiki/Root-mean-square_deviation """ - if not im_true.dtype == im_test.dtype: - raise ValueError('Input images must have the same dtype.') - - if not im_true.shape == im_test.shape: - raise ValueError('Input images must have the same dimensions.') - + _assert_compatible(im_true, im_test) im_true, im_test = _as_floats(im_true, im_test) norm_type = norm_type.lower() @@ -111,23 +111,19 @@ def psnr(im_true, im_test, dynamic_range=None): .. [1] https://en.wikipedia.org/wiki/Peak_signal-to-noise_ratio """ - if not im_true.dtype == im_test.dtype: - raise ValueError('Input images must have the same dtype.') - - if not im_true.shape == im_test.shape: - raise ValueError('Input images must have the same dimensions.') - + _assert_compatible(im_true, im_test) if dynamic_range is None: dmin, dmax = dtype_range[im_true.dtype.type] - dynamic_range = dmax # assume non-negative inputs - if im_true.min() < 0: + true_min, true_max = im_true.min(), im_true.max() + if true_max > dmax or true_min < dmin: raise ValueError( - "im_true contains negative values. Please manually specify " - "the dynamic range.") - if im_true.max() > dmax: - raise ValueError( - "im_true has a larger maximum intensity than expected for its " - "data type. Please manually specify the dynamic_range") + "im_true has intensity values outside the range expected for " + "its data type. Please manually specify the dynamic_range") + if true_min >= 0: + # most common case (255 for uint8, 1 for float) + dynamic_range = dmax + else: + dynamic_range = dmax - dmin im_true, im_test = _as_floats(im_true, im_test) From 0f71278a1081f89238252f896cec73a23f39cfa7 Mon Sep 17 00:00:00 2001 From: "Gregory R. Lee" Date: Wed, 27 Jan 2016 12:36:22 -0500 Subject: [PATCH 12/14] revert simple_metrics to np.float64 computations for all input types --- skimage/measure/simple_metrics.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/skimage/measure/simple_metrics.py b/skimage/measure/simple_metrics.py index 01bd0f29..399bcf99 100644 --- a/skimage/measure/simple_metrics.py +++ b/skimage/measure/simple_metrics.py @@ -16,12 +16,11 @@ def _assert_compatible(X, Y): def _as_floats(X, Y): - """Promote X, Y to nearest appropriate floating point precision.""" - float_type = np.result_type(X.dtype, Y.dtype, np.float32) - if X.dtype != float_type: - X = X.astype(float_type) - if Y.dtype != float_type: - Y = Y.astype(float_type) + """Promote X, Y to floating point precision.""" + if X.dtype != np.float64: + X = X.astype(np.float64) + if Y.dtype != np.float64: + Y = Y.astype(np.float64) return X, Y From 8483a62e76efe15e635d51b5d4135f3266acf6f7 Mon Sep 17 00:00:00 2001 From: "Gregory R. Lee" Date: Thu, 28 Jan 2016 22:03:45 -0500 Subject: [PATCH 13/14] make suggested naming changes in simple_metrics.py --- skimage/measure/__init__.py | 6 +-- skimage/measure/simple_metrics.py | 47 ++++++++++---------- skimage/measure/tests/test_simple_metrics.py | 23 +++++----- 3 files changed, 39 insertions(+), 37 deletions(-) diff --git a/skimage/measure/__init__.py b/skimage/measure/__init__.py index de865f9e..be5f2bd0 100755 --- a/skimage/measure/__init__.py +++ b/skimage/measure/__init__.py @@ -2,7 +2,7 @@ from ._find_contours import find_contours from ._marching_cubes import (marching_cubes, mesh_surface_area, correct_mesh_orientation) from ._regionprops import regionprops, perimeter -from .simple_metrics import mse, nrmse, psnr +from .simple_metrics import mean_squared_error, normalized_root_mse, psnr from ._structural_similarity import structural_similarity from ._polygon import approximate_polygon, subdivide_polygon from ._pnpoly import points_in_poly, grid_points_in_poly @@ -36,6 +36,6 @@ __all__ = ['find_contours', 'label', 'points_in_poly', 'grid_points_in_poly', - 'mse', - 'nrmse', + 'mean_squared_error', + 'normalized_root_mse', 'psnr'] diff --git a/skimage/measure/simple_metrics.py b/skimage/measure/simple_metrics.py index 399bcf99..07038bb4 100644 --- a/skimage/measure/simple_metrics.py +++ b/skimage/measure/simple_metrics.py @@ -3,48 +3,49 @@ from __future__ import division import numpy as np from ..util.dtype import dtype_range -__all__ = ['mse', 'nrmse', 'psnr'] +__all__ = ['mean_squared_error', 'normalized_root_mse', 'psnr'] -def _assert_compatible(X, Y): +def _assert_compatible(im1, im2): """Raise an error if the shape and dtype do not match.""" - if not X.dtype == Y.dtype: + if not im1.dtype == im2.dtype: raise ValueError('Input images must have the same dtype.') - if not X.shape == Y.shape: + if not im1.shape == im2.shape: raise ValueError('Input images must have the same dimensions.') return -def _as_floats(X, Y): - """Promote X, Y to floating point precision.""" - if X.dtype != np.float64: - X = X.astype(np.float64) - if Y.dtype != np.float64: - Y = Y.astype(np.float64) - return X, Y +def _as_floats(im1, im2): + """Promote im1, im2 to floating point precision.""" + if im1.dtype != np.float64: + im1 = im1.astype(np.float64) + if im2.dtype != np.float64: + im2 = im2.astype(np.float64) + return im1, im2 -def mse(X, Y): +def mean_squared_error(im1, im2): """Compute the mean-squared error between two images. Parameters ---------- - X, Y : ndarray + im1, im2 : ndarray Image. Any dimensionality. Returns ------- mse : float - The MSE metric. + The mean-squared error (MSE) metric. """ - _assert_compatible(X, Y) - X, Y = _as_floats(X, Y) - return np.square(X - Y).mean() + _assert_compatible(im1, im2) + im1, im2 = _as_floats(im1, im2) + return np.mean(np.square(im1 - im2)) -def nrmse(im_true, im_test, norm_type='Euclidean'): - """Compute the normalized root mean-squared error between two images. +def normalized_root_mse(im_true, im_test, norm_type='Euclidean'): + """Compute the normalized root mean-squared error (NRMSE) between two + images. Parameters ---------- @@ -76,14 +77,14 @@ def nrmse(im_true, im_test, norm_type='Euclidean'): norm_type = norm_type.lower() if norm_type == 'euclidean': - denom = np.sqrt((im_true*im_true).mean()) + denom = np.sqrt(np.mean((im_true*im_true))) elif norm_type == 'min-max': denom = im_true.max() - im_true.min() elif norm_type == 'mean': denom = im_true.mean() else: raise ValueError("Unsupported norm_type") - return np.sqrt(mse(im_true, im_test)) / denom + return np.sqrt(mean_squared_error(im_true, im_test)) / denom def psnr(im_true, im_test, dynamic_range=None): @@ -113,7 +114,7 @@ def psnr(im_true, im_test, dynamic_range=None): _assert_compatible(im_true, im_test) if dynamic_range is None: dmin, dmax = dtype_range[im_true.dtype.type] - true_min, true_max = im_true.min(), im_true.max() + true_min, true_max = np.min(im_true), np.max(im_true) if true_max > dmax or true_min < dmin: raise ValueError( "im_true has intensity values outside the range expected for " @@ -126,5 +127,5 @@ def psnr(im_true, im_test, dynamic_range=None): im_true, im_test = _as_floats(im_true, im_test) - err = mse(im_true, im_test) + err = mean_squared_error(im_true, im_test) return 10 * np.log10((dynamic_range ** 2) / err) diff --git a/skimage/measure/tests/test_simple_metrics.py b/skimage/measure/tests/test_simple_metrics.py index f1ab274a..b59a11a8 100644 --- a/skimage/measure/tests/test_simple_metrics.py +++ b/skimage/measure/tests/test_simple_metrics.py @@ -2,7 +2,7 @@ import numpy as np from numpy.testing import (run_module_suite, assert_equal, assert_raises, assert_almost_equal) -from skimage.measure import psnr, nrmse, mse +from skimage.measure import psnr, normalized_root_mse, mean_squared_error import skimage.data np.random.seed(5) @@ -34,26 +34,27 @@ def test_PSNR_errors(): def test_NRMSE(): x = np.ones(4) y = np.asarray([0., 2., 2., 2.]) - assert_equal(nrmse(y, x, 'mean'), 1/np.mean(y)) - assert_equal(nrmse(y, x, 'Euclidean'), 1/np.sqrt(3)) - assert_equal(nrmse(y, x, 'min-max'), 1/(y.max()-y.min())) + assert_equal(normalized_root_mse(y, x, 'mean'), 1/np.mean(y)) + assert_equal(normalized_root_mse(y, x, 'Euclidean'), 1/np.sqrt(3)) + assert_equal(normalized_root_mse(y, x, 'min-max'), 1/(y.max()-y.min())) def test_NRMSE_no_int_overflow(): camf = cam.astype(np.float32) cam_noisyf = cam_noisy.astype(np.float32) - assert_almost_equal(mse(cam, cam_noisy), - mse(camf, cam_noisyf)) - assert_almost_equal(nrmse(cam, cam_noisy), - nrmse(camf, cam_noisyf)) + assert_almost_equal(mean_squared_error(cam, cam_noisy), + mean_squared_error(camf, cam_noisyf)) + assert_almost_equal(normalized_root_mse(cam, cam_noisy), + normalized_root_mse(camf, cam_noisyf)) def test_NRMSE_errors(): x = np.ones(4) - assert_raises(ValueError, nrmse, x.astype(np.uint8), x.astype(np.float32)) - assert_raises(ValueError, nrmse, x[:-1], x) + assert_raises(ValueError, normalized_root_mse, + x.astype(np.uint8), x.astype(np.float32)) + assert_raises(ValueError, normalized_root_mse, x[:-1], x) # invalid normalization name - assert_raises(ValueError, nrmse, x, x, 'foo') + assert_raises(ValueError, normalized_root_mse, x, x, 'foo') if __name__ == "__main__": From b8e77401591ada5982d652ac6456dca364aa8220 Mon Sep 17 00:00:00 2001 From: "Gregory R. Lee" Date: Thu, 28 Jan 2016 22:08:58 -0500 Subject: [PATCH 14/14] use dtype=np.float64 in calls to np.mean --- skimage/measure/simple_metrics.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/skimage/measure/simple_metrics.py b/skimage/measure/simple_metrics.py index 07038bb4..6083010b 100644 --- a/skimage/measure/simple_metrics.py +++ b/skimage/measure/simple_metrics.py @@ -16,11 +16,12 @@ def _assert_compatible(im1, im2): def _as_floats(im1, im2): - """Promote im1, im2 to floating point precision.""" - if im1.dtype != np.float64: - im1 = im1.astype(np.float64) - if im2.dtype != np.float64: - im2 = im2.astype(np.float64) + """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) return im1, im2 @@ -40,7 +41,7 @@ def mean_squared_error(im1, im2): """ _assert_compatible(im1, im2) im1, im2 = _as_floats(im1, im2) - return np.mean(np.square(im1 - im2)) + return np.mean(np.square(im1 - im2), dtype=np.float64) def normalized_root_mse(im_true, im_test, norm_type='Euclidean'): @@ -77,7 +78,7 @@ def normalized_root_mse(im_true, im_test, norm_type='Euclidean'): norm_type = norm_type.lower() if norm_type == 'euclidean': - denom = np.sqrt(np.mean((im_true*im_true))) + denom = np.sqrt(np.mean((im_true*im_true), dtype=np.float64)) elif norm_type == 'min-max': denom = im_true.max() - im_true.min() elif norm_type == 'mean':