| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243 |
- __all__ = ['random_noise']
- import numpy as np
- from .dtype import img_as_float
- def _bernoulli(p, shape, *, rng):
- """
- Bernoulli trials at a given probability of a given size.
- This function is meant as a lower-memory alternative to calls such as
- `np.random.choice([True, False], size=image.shape, p=[p, 1-p])`.
- While `np.random.choice` can handle many classes, for the 2-class case
- (Bernoulli trials), this function is much more efficient.
- Parameters
- ----------
- p : float
- The probability that any given trial returns `True`.
- shape : int or tuple of ints
- The shape of the ndarray to return.
- rng : `numpy.random.Generator`
- ``Generator`` instance, typically obtained via `np.random.default_rng()`.
- Returns
- -------
- out : ndarray[bool]
- The results of Bernoulli trials in the given `size` where success
- occurs with probability `p`.
- """
- if p == 0:
- return np.zeros(shape, dtype=bool)
- if p == 1:
- return np.ones(shape, dtype=bool)
- return rng.random(shape) <= p
- def random_noise(image, mode='gaussian', rng=None, clip=True, **kwargs):
- """
- Function to add random noise of various types to a floating-point image.
- Parameters
- ----------
- image : ndarray
- Input image data. Will be converted to float.
- mode : str, optional
- One of the following strings, selecting the type of noise to add:
- 'gaussian' (default)
- Gaussian-distributed additive noise.
- 'localvar'
- Gaussian-distributed additive noise, with specified local variance
- at each point of `image`.
- 'poisson'
- Poisson-distributed noise generated from the data.
- 'salt'
- Replaces random pixels with 1.
- 'pepper'
- Replaces random pixels with 0 (for unsigned images) or -1 (for
- signed images).
- 's&p'
- Replaces random pixels with either 1 or `low_val`, where `low_val`
- is 0 for unsigned images or -1 for signed images.
- 'speckle'
- Multiplicative noise using ``out = image + n * image``, where ``n``
- is Gaussian noise with specified mean & variance.
- rng : {`numpy.random.Generator`, int}, optional
- Pseudo-random number generator.
- By default, a PCG64 generator is used (see :func:`numpy.random.default_rng`).
- If `rng` is an int, it is used to seed the generator.
- clip : bool, optional
- If True (default), the output will be clipped after noise is applied.
- This may be needed to maintain the proper image data range.
- If False, clipping is not applied, and the output may extend beyond
- the range [-1, 1].
- mean : float, optional
- Mean of random distribution. Used in 'gaussian' and 'speckle'.
- Default : 0.
- var : float, optional
- Variance of random distribution. Used in 'gaussian' and 'speckle'.
- Note: variance = (standard deviation) ** 2. Default : 0.01
- local_vars : ndarray, optional
- Array of positive floats, same shape as `image`, defining the local
- variance at every image point. Used in 'localvar'.
- amount : float, optional
- Proportion of image pixels to replace with noise on range [0, 1].
- Used in 'salt', 'pepper', and 'salt & pepper'. Default : 0.05
- salt_vs_pepper : float, optional
- Proportion of salt vs. pepper noise for 's&p' on range [0, 1].
- Higher values represent more salt. Default : 0.5 (equal amounts)
- Returns
- -------
- out : ndarray
- Output floating-point image data on range [0, 1] or [-1, 1] if the
- input `image` was unsigned or signed, respectively.
- Notes
- -----
- Speckle, Poisson, Localvar, and Gaussian noise may generate noise outside
- the valid image range. The default is to clip (not alias) these values,
- but they may be preserved by setting `clip=False`. Note that in this case
- the output may contain values outside the ranges [0, 1] or [-1, 1].
- Use this option with care.
- Because of the prevalence of exclusively positive floating-point images in
- intermediate calculations, it is not possible to intuit if an input is
- signed based on dtype alone. Instead, negative values are explicitly
- searched for. Only if found does this function assume signed input.
- Unexpected results only occur in rare, poorly exposes cases (e.g. if all
- values are above 50 percent gray in a signed `image`). In this event,
- manually scaling the input to the positive domain will solve the problem.
- The Poisson distribution is only defined for positive integers. To apply
- this noise type, the number of unique values in the image is found and
- the next round power of two is used to scale up the floating-point result,
- after which it is scaled back down to the floating-point image range.
- To generate Poisson noise against a signed image, the signed image is
- temporarily converted to an unsigned image in the floating point domain,
- Poisson noise is generated, then it is returned to the original range.
- """
- mode = mode.lower()
- # Detect if a signed image was input
- if image.min() < 0:
- low_clip = -1.0
- else:
- low_clip = 0.0
- image = img_as_float(image)
- rng = np.random.default_rng(rng)
- allowedtypes = {
- 'gaussian': 'gaussian_values',
- 'localvar': 'localvar_values',
- 'poisson': 'poisson_values',
- 'salt': 'sp_values',
- 'pepper': 'sp_values',
- 's&p': 's&p_values',
- 'speckle': 'gaussian_values',
- }
- kwdefaults = {
- 'mean': 0.0,
- 'var': 0.01,
- 'amount': 0.05,
- 'salt_vs_pepper': 0.5,
- 'local_vars': np.zeros_like(image) + 0.01,
- }
- allowedkwargs = {
- 'gaussian_values': ['mean', 'var'],
- 'localvar_values': ['local_vars'],
- 'sp_values': ['amount'],
- 's&p_values': ['amount', 'salt_vs_pepper'],
- 'poisson_values': [],
- }
- for key in kwargs:
- if key not in allowedkwargs[allowedtypes[mode]]:
- raise ValueError(
- f"{key} keyword not in allowed keywords "
- f"{allowedkwargs[allowedtypes[mode]]}"
- )
- # Set kwarg defaults
- for kw in allowedkwargs[allowedtypes[mode]]:
- kwargs.setdefault(kw, kwdefaults[kw])
- if mode == 'gaussian':
- noise = rng.normal(kwargs['mean'], kwargs['var'] ** 0.5, image.shape)
- out = image + noise
- elif mode == 'localvar':
- # Ensure local variance input is correct
- if (kwargs['local_vars'] <= 0).any():
- raise ValueError('All values of `local_vars` must be > 0.')
- # Safe shortcut usage broadcasts kwargs['local_vars'] as a ufunc
- out = image + rng.normal(0, kwargs['local_vars'] ** 0.5)
- elif mode == 'poisson':
- # Determine unique values in image & calculate the next power of two
- vals = len(np.unique(image))
- vals = 2 ** np.ceil(np.log2(vals))
- # Ensure image is exclusively positive
- if low_clip == -1.0:
- old_max = image.max()
- image = (image + 1.0) / (old_max + 1.0)
- # Generating noise for each unique value in image.
- out = rng.poisson(image * vals) / float(vals)
- # Return image to original range if input was signed
- if low_clip == -1.0:
- out = out * (old_max + 1.0) - 1.0
- elif mode == 'salt':
- # Re-call function with mode='s&p' and p=1 (all salt noise)
- out = random_noise(
- image,
- mode='s&p',
- rng=rng,
- amount=kwargs['amount'],
- salt_vs_pepper=1.0,
- clip=False,
- )
- elif mode == 'pepper':
- # Re-call function with mode='s&p' and p=1 (all pepper noise)
- out = random_noise(
- image,
- mode='s&p',
- rng=rng,
- amount=kwargs['amount'],
- salt_vs_pepper=0.0,
- clip=False,
- )
- elif mode == 's&p':
- out = image.copy()
- p = kwargs['amount']
- q = kwargs['salt_vs_pepper']
- flipped = _bernoulli(p, image.shape, rng=rng)
- salted = _bernoulli(q, image.shape, rng=rng)
- peppered = ~salted
- out[flipped & salted] = 1
- out[flipped & peppered] = low_clip
- elif mode == 'speckle':
- noise = rng.normal(kwargs['mean'], kwargs['var'] ** 0.5, image.shape)
- out = image + image * noise
- # Clip back to original range, if necessary
- if clip:
- out = np.clip(out, low_clip, 1.0)
- return out
|