noise.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. __all__ = ['random_noise']
  2. import numpy as np
  3. from .dtype import img_as_float
  4. def _bernoulli(p, shape, *, rng):
  5. """
  6. Bernoulli trials at a given probability of a given size.
  7. This function is meant as a lower-memory alternative to calls such as
  8. `np.random.choice([True, False], size=image.shape, p=[p, 1-p])`.
  9. While `np.random.choice` can handle many classes, for the 2-class case
  10. (Bernoulli trials), this function is much more efficient.
  11. Parameters
  12. ----------
  13. p : float
  14. The probability that any given trial returns `True`.
  15. shape : int or tuple of ints
  16. The shape of the ndarray to return.
  17. rng : `numpy.random.Generator`
  18. ``Generator`` instance, typically obtained via `np.random.default_rng()`.
  19. Returns
  20. -------
  21. out : ndarray[bool]
  22. The results of Bernoulli trials in the given `size` where success
  23. occurs with probability `p`.
  24. """
  25. if p == 0:
  26. return np.zeros(shape, dtype=bool)
  27. if p == 1:
  28. return np.ones(shape, dtype=bool)
  29. return rng.random(shape) <= p
  30. def random_noise(image, mode='gaussian', rng=None, clip=True, **kwargs):
  31. """
  32. Function to add random noise of various types to a floating-point image.
  33. Parameters
  34. ----------
  35. image : ndarray
  36. Input image data. Will be converted to float.
  37. mode : str, optional
  38. One of the following strings, selecting the type of noise to add:
  39. 'gaussian' (default)
  40. Gaussian-distributed additive noise.
  41. 'localvar'
  42. Gaussian-distributed additive noise, with specified local variance
  43. at each point of `image`.
  44. 'poisson'
  45. Poisson-distributed noise generated from the data.
  46. 'salt'
  47. Replaces random pixels with 1.
  48. 'pepper'
  49. Replaces random pixels with 0 (for unsigned images) or -1 (for
  50. signed images).
  51. 's&p'
  52. Replaces random pixels with either 1 or `low_val`, where `low_val`
  53. is 0 for unsigned images or -1 for signed images.
  54. 'speckle'
  55. Multiplicative noise using ``out = image + n * image``, where ``n``
  56. is Gaussian noise with specified mean & variance.
  57. rng : {`numpy.random.Generator`, int}, optional
  58. Pseudo-random number generator.
  59. By default, a PCG64 generator is used (see :func:`numpy.random.default_rng`).
  60. If `rng` is an int, it is used to seed the generator.
  61. clip : bool, optional
  62. If True (default), the output will be clipped after noise is applied.
  63. This may be needed to maintain the proper image data range.
  64. If False, clipping is not applied, and the output may extend beyond
  65. the range [-1, 1].
  66. mean : float, optional
  67. Mean of random distribution. Used in 'gaussian' and 'speckle'.
  68. Default : 0.
  69. var : float, optional
  70. Variance of random distribution. Used in 'gaussian' and 'speckle'.
  71. Note: variance = (standard deviation) ** 2. Default : 0.01
  72. local_vars : ndarray, optional
  73. Array of positive floats, same shape as `image`, defining the local
  74. variance at every image point. Used in 'localvar'.
  75. amount : float, optional
  76. Proportion of image pixels to replace with noise on range [0, 1].
  77. Used in 'salt', 'pepper', and 'salt & pepper'. Default : 0.05
  78. salt_vs_pepper : float, optional
  79. Proportion of salt vs. pepper noise for 's&p' on range [0, 1].
  80. Higher values represent more salt. Default : 0.5 (equal amounts)
  81. Returns
  82. -------
  83. out : ndarray
  84. Output floating-point image data on range [0, 1] or [-1, 1] if the
  85. input `image` was unsigned or signed, respectively.
  86. Notes
  87. -----
  88. Speckle, Poisson, Localvar, and Gaussian noise may generate noise outside
  89. the valid image range. The default is to clip (not alias) these values,
  90. but they may be preserved by setting `clip=False`. Note that in this case
  91. the output may contain values outside the ranges [0, 1] or [-1, 1].
  92. Use this option with care.
  93. Because of the prevalence of exclusively positive floating-point images in
  94. intermediate calculations, it is not possible to intuit if an input is
  95. signed based on dtype alone. Instead, negative values are explicitly
  96. searched for. Only if found does this function assume signed input.
  97. Unexpected results only occur in rare, poorly exposes cases (e.g. if all
  98. values are above 50 percent gray in a signed `image`). In this event,
  99. manually scaling the input to the positive domain will solve the problem.
  100. The Poisson distribution is only defined for positive integers. To apply
  101. this noise type, the number of unique values in the image is found and
  102. the next round power of two is used to scale up the floating-point result,
  103. after which it is scaled back down to the floating-point image range.
  104. To generate Poisson noise against a signed image, the signed image is
  105. temporarily converted to an unsigned image in the floating point domain,
  106. Poisson noise is generated, then it is returned to the original range.
  107. """
  108. mode = mode.lower()
  109. # Detect if a signed image was input
  110. if image.min() < 0:
  111. low_clip = -1.0
  112. else:
  113. low_clip = 0.0
  114. image = img_as_float(image)
  115. rng = np.random.default_rng(rng)
  116. allowedtypes = {
  117. 'gaussian': 'gaussian_values',
  118. 'localvar': 'localvar_values',
  119. 'poisson': 'poisson_values',
  120. 'salt': 'sp_values',
  121. 'pepper': 'sp_values',
  122. 's&p': 's&p_values',
  123. 'speckle': 'gaussian_values',
  124. }
  125. kwdefaults = {
  126. 'mean': 0.0,
  127. 'var': 0.01,
  128. 'amount': 0.05,
  129. 'salt_vs_pepper': 0.5,
  130. 'local_vars': np.zeros_like(image) + 0.01,
  131. }
  132. allowedkwargs = {
  133. 'gaussian_values': ['mean', 'var'],
  134. 'localvar_values': ['local_vars'],
  135. 'sp_values': ['amount'],
  136. 's&p_values': ['amount', 'salt_vs_pepper'],
  137. 'poisson_values': [],
  138. }
  139. for key in kwargs:
  140. if key not in allowedkwargs[allowedtypes[mode]]:
  141. raise ValueError(
  142. f"{key} keyword not in allowed keywords "
  143. f"{allowedkwargs[allowedtypes[mode]]}"
  144. )
  145. # Set kwarg defaults
  146. for kw in allowedkwargs[allowedtypes[mode]]:
  147. kwargs.setdefault(kw, kwdefaults[kw])
  148. if mode == 'gaussian':
  149. noise = rng.normal(kwargs['mean'], kwargs['var'] ** 0.5, image.shape)
  150. out = image + noise
  151. elif mode == 'localvar':
  152. # Ensure local variance input is correct
  153. if (kwargs['local_vars'] <= 0).any():
  154. raise ValueError('All values of `local_vars` must be > 0.')
  155. # Safe shortcut usage broadcasts kwargs['local_vars'] as a ufunc
  156. out = image + rng.normal(0, kwargs['local_vars'] ** 0.5)
  157. elif mode == 'poisson':
  158. # Determine unique values in image & calculate the next power of two
  159. vals = len(np.unique(image))
  160. vals = 2 ** np.ceil(np.log2(vals))
  161. # Ensure image is exclusively positive
  162. if low_clip == -1.0:
  163. old_max = image.max()
  164. image = (image + 1.0) / (old_max + 1.0)
  165. # Generating noise for each unique value in image.
  166. out = rng.poisson(image * vals) / float(vals)
  167. # Return image to original range if input was signed
  168. if low_clip == -1.0:
  169. out = out * (old_max + 1.0) - 1.0
  170. elif mode == 'salt':
  171. # Re-call function with mode='s&p' and p=1 (all salt noise)
  172. out = random_noise(
  173. image,
  174. mode='s&p',
  175. rng=rng,
  176. amount=kwargs['amount'],
  177. salt_vs_pepper=1.0,
  178. clip=False,
  179. )
  180. elif mode == 'pepper':
  181. # Re-call function with mode='s&p' and p=1 (all pepper noise)
  182. out = random_noise(
  183. image,
  184. mode='s&p',
  185. rng=rng,
  186. amount=kwargs['amount'],
  187. salt_vs_pepper=0.0,
  188. clip=False,
  189. )
  190. elif mode == 's&p':
  191. out = image.copy()
  192. p = kwargs['amount']
  193. q = kwargs['salt_vs_pepper']
  194. flipped = _bernoulli(p, image.shape, rng=rng)
  195. salted = _bernoulli(q, image.shape, rng=rng)
  196. peppered = ~salted
  197. out[flipped & salted] = 1
  198. out[flipped & peppered] = low_clip
  199. elif mode == 'speckle':
  200. noise = rng.normal(kwargs['mean'], kwargs['var'] ** 0.5, image.shape)
  201. out = image + image * noise
  202. # Clip back to original range, if necessary
  203. if clip:
  204. out = np.clip(out, low_clip, 1.0)
  205. return out