Repository: scikit-image/scikit-image

Issue:
Salt and Pepper noise yields possibly wrong result
## Description

I investigated salt and pepper noise (from `skimage.util.random_noise`) with a few small input sizes. The results differ from what I expected for black-and-white images.
## Way to reproduce

``` python
import numpy as np
from skimage.util import random_noise
print(random_noise(np.zeros((2, 3)), mode='salt', amount=1.0))
```

Yields:

```
[[ 1.  1.  0.]
 [ 0.  0.  0.]]
```

But I expected salt everywhere (ones everywhere).

``` python
print(random_noise(np.ones((2, 3)), mode='pepper', amount=1.0))
```

Yields:

```
[[ 0.  0.  1.]
 [ 1.  1.  1.]]
```

But I expected pepper everywhere.

Same holds for larger images. I.e.

``` python
print(np.sum(random_noise(np.zeros((1000, 1000)), mode='salt', amount=1.0)))
```

Yields:

```
632022.0
```

whereas I expected 1M.

Reshaping an arbitrary image yields a `ValueError`, although the intermediate result is two-dimensional.

``` python
print(random_noise(np.zeros((2, 3, 4)).reshape(1, -1), mode='salt', amount=1.0))
```

Yields:

```
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-175-57dba4fbf87a> in <module>()
----> 1 print(random_noise(np.zeros((2, 3, 4)).reshape(1, -1), mode='salt', amount=1.0))

/net/ssd/software/anaconda/envs/py35/lib/python3.5/site-packages/skimage/util/noise.py in random_noise(image, mode, seed, clip, **kwargs)
    159         # Re-call function with mode='s&p' and p=1 (all salt noise)
    160         out = random_noise(image, mode='s&p', seed=seed,
--> 161                            amount=kwargs['amount'], salt_vs_pepper=1.)
    162 
    163     elif mode == 'pepper':

/net/ssd/software/anaconda/envs/py35/lib/python3.5/site-packages/skimage/util/noise.py in random_noise(image, mode, seed, clip, **kwargs)
    175             kwargs['amount'] * image.size * kwargs['salt_vs_pepper'])
    176         coords = [np.random.randint(0, i - 1, int(num_salt))
--> 177                   for i in image.shape]
    178         out[coords] = 1
    179 

/net/ssd/software/anaconda/envs/py35/lib/python3.5/site-packages/skimage/util/noise.py in <listcomp>(.0)
    175             kwargs['amount'] * image.size * kwargs['salt_vs_pepper'])
    176         coords = [np.random.randint(0, i - 1, int(num_salt))
--> 177                   for i in image.shape]
    178         out[coords] = 1
    179 

mtrand.pyx in mtrand.RandomState.randint (numpy/random/mtrand/mtrand.c:14492)()

ValueError: low >= high
```

My `skimage.__version__` is 0.11.3.b



Discussion:
Ah, that's an off-by-one error on randint. I can confirm that this happens on master, too.

In addition to the off-by-one error, the way that we are generating S&P noise is incorrect. With a probability of 1.0, we say, "generate `image.size` coordinates at random, then replace those with 1s". Of course the problem is that generating that many random coordinates results in repeated coordinates, and we end up replacing ~2/3 of the coordinates at random, on average. For small probabilities this doesn't matter, which is probably why it's gone unnoticed for so long.

@LukasDrude thanks for the report! Fix upcoming! =)


Resolving patch:
diff --git a/skimage/util/noise.py b/skimage/util/noise.py
--- a/skimage/util/noise.py
+++ b/skimage/util/noise.py
@@ -21,8 +21,11 @@ def random_noise(image, mode='gaussian', seed=None, clip=True, **kwargs):
                       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.
-        - 's&p'       Replaces random pixels with 0 or 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 uniform noise with specified mean & variance.
     seed : int
@@ -166,23 +169,16 @@ def random_noise(image, mode='gaussian', seed=None, clip=True, **kwargs):
                            amount=kwargs['amount'], salt_vs_pepper=0.)
 
     elif mode == 's&p':
-        # This mode makes no effort to avoid repeat sampling. Thus, the
-        # exact number of replaced pixels is only approximate.
         out = image.copy()
-
-        # Salt mode
-        num_salt = np.ceil(
-            kwargs['amount'] * image.size * kwargs['salt_vs_pepper'])
-        coords = [np.random.randint(0, i - 1, int(num_salt))
-                  for i in image.shape]
-        out[coords] = 1
-
-        # Pepper mode
-        num_pepper = np.ceil(
-            kwargs['amount'] * image.size * (1. - kwargs['salt_vs_pepper']))
-        coords = [np.random.randint(0, i - 1, int(num_pepper))
-                  for i in image.shape]
-        out[coords] = low_clip
+        p = kwargs['amount']
+        q = kwargs['salt_vs_pepper']
+        flipped = np.random.choice([True, False], size=image.shape,
+                                   p=[p, 1 - p])
+        salted = np.random.choice([True, False], size=image.shape,
+                                  p=[q, 1 - q])
+        peppered = ~salted
+        out[flipped & salted] = 1
+        out[flipped & peppered] = low_clip
 
     elif mode == 'speckle':
         noise = np.random.normal(kwargs['mean'], kwargs['var'] ** 0.5,


Test patch:
diff --git a/skimage/util/tests/test_random_noise.py b/skimage/util/tests/test_random_noise.py
--- a/skimage/util/tests/test_random_noise.py
+++ b/skimage/util/tests/test_random_noise.py
@@ -26,6 +26,19 @@ def test_salt():
     assert 0.11 < proportion <= 0.15
 
 
+def test_salt_p1():
+    image = np.random.rand(2, 3)
+    noisy = random_noise(image, mode='salt', amount=1)
+    assert_array_equal(noisy, [[1, 1, 1], [1, 1, 1]])
+
+
+def test_singleton_dim():
+    """Ensure images where size of a given dimension is 1 work correctly."""
+    image = np.random.rand(1, 20)
+    noisy = random_noise(image, mode='salt', amount=0.1, seed=42)
+    assert np.sum(noisy == 1) == 2
+
+
 def test_pepper():
     seed = 42
     cam = img_as_float(camera())
@@ -69,7 +82,7 @@ def test_salt_and_pepper():
     assert 0.11 < proportion <= 0.18
 
     # Verify the relative amount of salt vs. pepper is close to expected
-    assert 0.18 < saltmask.sum() / float(peppermask.sum()) < 0.32
+    assert 0.18 < saltmask.sum() / float(peppermask.sum()) < 0.33
 
 
 def test_gaussian():