simple_metrics.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. import numpy as np
  2. from scipy.stats import entropy
  3. from ..util._backends import dispatchable
  4. from ..util.dtype import dtype_range
  5. from .._shared.utils import _supported_float_type, check_shape_equality, warn
  6. __all__ = [
  7. 'mean_squared_error',
  8. 'normalized_root_mse',
  9. 'peak_signal_noise_ratio',
  10. 'normalized_mutual_information',
  11. ]
  12. def _as_floats(image0, image1):
  13. """
  14. Promote im1, im2 to nearest appropriate floating point precision.
  15. """
  16. float_type = _supported_float_type((image0.dtype, image1.dtype))
  17. image0 = np.asarray(image0, dtype=float_type)
  18. image1 = np.asarray(image1, dtype=float_type)
  19. return image0, image1
  20. @dispatchable
  21. def mean_squared_error(image0, image1):
  22. """
  23. Compute the mean-squared error between two images.
  24. Parameters
  25. ----------
  26. image0, image1 : ndarray
  27. Images. Any dimensionality, must have same shape.
  28. Returns
  29. -------
  30. mse : float
  31. The mean-squared error (MSE) metric.
  32. Notes
  33. -----
  34. .. versionchanged:: 0.16
  35. This function was renamed from ``skimage.measure.compare_mse`` to
  36. ``skimage.metrics.mean_squared_error``.
  37. """
  38. check_shape_equality(image0, image1)
  39. image0, image1 = _as_floats(image0, image1)
  40. return np.mean((image0 - image1) ** 2, dtype=np.float64)
  41. @dispatchable
  42. def normalized_root_mse(image_true, image_test, *, normalization='euclidean'):
  43. """
  44. Compute the normalized root mean-squared error (NRMSE) between two
  45. images.
  46. Parameters
  47. ----------
  48. image_true : ndarray
  49. Ground-truth image, same shape as im_test.
  50. image_test : ndarray
  51. Test image.
  52. normalization : {'euclidean', 'min-max', 'mean'}, optional
  53. Controls the normalization method to use in the denominator of the
  54. NRMSE. There is no standard method of normalization across the
  55. literature [1]_. The methods available here are as follows:
  56. - 'euclidean' : normalize by the averaged Euclidean norm of
  57. ``im_true``::
  58. NRMSE = RMSE * sqrt(N) / || im_true ||
  59. where || . || denotes the Frobenius norm and ``N = im_true.size``.
  60. This result is equivalent to::
  61. NRMSE = || im_true - im_test || / || im_true ||.
  62. - 'min-max' : normalize by the intensity range of ``im_true``.
  63. - 'mean' : normalize by the mean of ``im_true``
  64. Returns
  65. -------
  66. nrmse : float
  67. The NRMSE metric.
  68. Notes
  69. -----
  70. .. versionchanged:: 0.16
  71. This function was renamed from ``skimage.measure.compare_nrmse`` to
  72. ``skimage.metrics.normalized_root_mse``.
  73. References
  74. ----------
  75. .. [1] https://en.wikipedia.org/wiki/Root-mean-square_deviation
  76. """
  77. check_shape_equality(image_true, image_test)
  78. image_true, image_test = _as_floats(image_true, image_test)
  79. # Ensure that both 'Euclidean' and 'euclidean' match
  80. normalization = normalization.lower()
  81. if normalization == 'euclidean':
  82. denom = np.sqrt(np.mean((image_true * image_true), dtype=np.float64))
  83. elif normalization == 'min-max':
  84. denom = image_true.max() - image_true.min()
  85. elif normalization == 'mean':
  86. denom = image_true.mean()
  87. else:
  88. raise ValueError("Unsupported norm_type")
  89. return np.sqrt(mean_squared_error(image_true, image_test)) / denom
  90. def peak_signal_noise_ratio(image_true, image_test, *, data_range=None):
  91. """
  92. Compute the peak signal to noise ratio (PSNR) for an image.
  93. Parameters
  94. ----------
  95. image_true : ndarray
  96. Ground-truth image, same shape as im_test.
  97. image_test : ndarray
  98. Test image.
  99. data_range : int, optional
  100. The data range of the input image (distance between minimum and
  101. maximum possible values). By default, this is estimated from the image
  102. data-type.
  103. Returns
  104. -------
  105. psnr : float
  106. The PSNR metric.
  107. Notes
  108. -----
  109. .. versionchanged:: 0.16
  110. This function was renamed from ``skimage.measure.compare_psnr`` to
  111. ``skimage.metrics.peak_signal_noise_ratio``.
  112. References
  113. ----------
  114. .. [1] https://en.wikipedia.org/wiki/Peak_signal-to-noise_ratio
  115. """
  116. check_shape_equality(image_true, image_test)
  117. if data_range is None:
  118. if image_true.dtype != image_test.dtype:
  119. warn(
  120. "Inputs have mismatched dtype. Setting data_range based on "
  121. "image_true."
  122. )
  123. dmin, dmax = dtype_range[image_true.dtype.type]
  124. true_min, true_max = np.min(image_true), np.max(image_true)
  125. if true_max > dmax or true_min < dmin:
  126. raise ValueError(
  127. "image_true has intensity values outside the range expected "
  128. "for its data type. Please manually specify the data_range."
  129. )
  130. if true_min >= 0:
  131. # most common case (255 for uint8, 1 for float)
  132. data_range = dmax
  133. else:
  134. data_range = dmax - dmin
  135. image_true, image_test = _as_floats(image_true, image_test)
  136. err = mean_squared_error(image_true, image_test)
  137. data_range = float(data_range) # prevent overflow for small integer types
  138. return 10 * np.log10((data_range**2) / err)
  139. def _pad_to(arr, shape):
  140. """Pad an array with trailing zeros to a given target shape.
  141. Parameters
  142. ----------
  143. arr : ndarray
  144. The input array.
  145. shape : tuple
  146. The target shape.
  147. Returns
  148. -------
  149. padded : ndarray
  150. The padded array.
  151. Examples
  152. --------
  153. >>> _pad_to(np.ones((1, 1), dtype=int), (1, 3))
  154. array([[1, 0, 0]])
  155. """
  156. if not all(s >= i for s, i in zip(shape, arr.shape)):
  157. raise ValueError(
  158. f'Target shape {shape} cannot be smaller than input'
  159. f'shape {arr.shape} along any axis.'
  160. )
  161. padding = [(0, s - i) for s, i in zip(shape, arr.shape)]
  162. return np.pad(arr, pad_width=padding, mode='constant', constant_values=0)
  163. def normalized_mutual_information(image0, image1, *, bins=100):
  164. r"""Compute the normalized mutual information (NMI).
  165. The normalized mutual information of :math:`A` and :math:`B` is given by:
  166. .. math::
  167. Y(A, B) = \frac{H(A) + H(B)}{H(A, B)}
  168. where :math:`H(X) := - \sum_{x \in X}{p(x) \log p(x)}` is the entropy,
  169. :math:`X` is the set of image values, and :math:`p(x)` is the probability
  170. of occurrence of value :math:`x \in X`.
  171. It was proposed to be useful in registering images by Colin Studholme and
  172. colleagues [1]_. It ranges from 1 (perfectly uncorrelated image values)
  173. to 2 (perfectly correlated image values, whether positively or negatively).
  174. Parameters
  175. ----------
  176. image0, image1 : ndarray
  177. Images to be compared. The two input images must have the same number
  178. of dimensions.
  179. bins : int or sequence of int, optional
  180. The number of bins along each axis of the joint histogram.
  181. Returns
  182. -------
  183. nmi : float
  184. The normalized mutual information between the two arrays, computed at
  185. the granularity given by ``bins``. Higher NMI implies more similar
  186. input images.
  187. Raises
  188. ------
  189. ValueError
  190. If the images don't have the same number of dimensions.
  191. Notes
  192. -----
  193. If the two input images are not the same shape, the smaller image is padded
  194. with zeros.
  195. References
  196. ----------
  197. .. [1] C. Studholme, D.L.G. Hill, & D.J. Hawkes (1999). An overlap
  198. invariant entropy measure of 3D medical image alignment.
  199. Pattern Recognition 32(1):71-86
  200. :DOI:`10.1016/S0031-3203(98)00091-0`
  201. """
  202. if image0.ndim != image1.ndim:
  203. raise ValueError(
  204. f'NMI requires images of same number of dimensions. '
  205. f'Got {image0.ndim}D for `image0` and '
  206. f'{image1.ndim}D for `image1`.'
  207. )
  208. if image0.shape != image1.shape:
  209. max_shape = np.maximum(image0.shape, image1.shape)
  210. padded0 = _pad_to(image0, max_shape)
  211. padded1 = _pad_to(image1, max_shape)
  212. else:
  213. padded0, padded1 = image0, image1
  214. hist, bin_edges = np.histogramdd(
  215. [np.reshape(padded0, -1), np.reshape(padded1, -1)],
  216. bins=bins,
  217. density=True,
  218. )
  219. H0 = entropy(np.sum(hist, axis=0))
  220. H1 = entropy(np.sum(hist, axis=1))
  221. H01 = entropy(np.reshape(hist, -1))
  222. return (H0 + H1) / H01