Contents
Salt and Pepper Noise
“Salt and Pepper” noise is a useful tool to augment images for training deep learning models. If we are dealing with RGB (or multi-channel) images, then we can add different grain noise to each channel - I’m going to call it RGB noise and it is not the same as Gaussian noise. The example below aims at 8 bit images. If we are handling 12 or 16 bit images (e.g. medical imaging), then we have to adapt upperValue
.
import numpy as np
import PIL
class SaltAndPepperNoise(object):
r""" Implements 'Salt-and-Pepper' noise
Adding grain (salt and pepper) noise
(https://en.wikipedia.org/wiki/Salt-and-pepper_noise)
assumption: high values = white, low values = black
Inputs:
- threshold (float):
- imgType (str): {"cv2","PIL"}
- lowerValue (int): value for "pepper"
- upperValue (int): value for "salt"
- noiseType (str): {"SnP", "RGB"}
Output:
- image ({np.ndarray, PIL.Image}): image with
noise added
"""
def __init__(self,
treshold:float = 0.005,
imgType:str = "cv2",
lowerValue:int = 5,
upperValue:int = 250,
noiseType:str = "SnP"):
self.treshold = treshold
self.imgType = imgType
self.lowerValue = lowerValue # 255 would be too high
self.upperValue = upperValue # 0 would be too low
if (noiseType != "RGB") and (noiseType != "SnP"):
raise Exception("'noiseType' not of value {'SnP', 'RGB'}")
else:
self.noiseType = noiseType
super(SaltAndPepperNoise).__init__()
def __call__(self, img):
if self.imgType == "PIL":
img = np.array(img)
if type(img) != np.ndarray:
raise TypeError("Image is not of type 'np.ndarray'!")
if self.noiseType == "SnP":
random_matrix = np.random.rand(img.shape[0],img.shape[1])
img[random_matrix>=(1-self.treshold)] = self.upperValue
img[random_matrix<=self.treshold] = self.lowerValue
elif self.noiseType == "RGB":
random_matrix = np.random.random(img.shape)
img[random_matrix>=(1-self.treshold)] = self.upperValue
img[random_matrix<=self.treshold] = self.lowerValue
if self.imgType == "cv2":
return img
elif self.imgType == "PIL":
# return as PIL image for torchvision transforms compliance
return PIL.Image.fromarray(img)
A little example:
import matplotlib.pyplot as plt
import numpy as np
example_image_rgb = np.ones((1200,1300,3), dtype=np.uint8)*128
example_image_gray = np.ones((1200,1300), dtype=np.uint8)*128
plt.figure(figsize=(5,5))
plt.imshow(example_image_rgb)
plt.title("Example image (all values = 128)")
plt.axis("off")
plt.show()

SnP_noise = SaltAndPepperNoise()
RGB_noise = SaltAndPepperNoise(noiseType="RGB")
plt.figure(figsize=(15,15))
plt.subplot(2,2,1)
plt.imshow(SnP_noise(example_image_rgb))
plt.title("Salt and Pepper noise on RGB image")
plt.axis("off")
plt.subplot(2,2,2)
plt.imshow(SnP_noise(example_image_gray), cmap="gray")
plt.title("Salt and Pepper noise on grayscale image")
plt.axis("off")
plt.subplot(2,2,3)
plt.imshow(RGB_noise(example_image_rgb))
plt.title("RGB noise on RGB image")
plt.axis("off")
plt.subplot(2,2,4)
plt.imshow(RGB_noise(example_image_gray), cmap="gray")
plt.title("RGB noise on grayscale image")
plt.axis("off")
plt.show()

Progressive Sprinkles
I came across progressive sprinkles. While it originates from cut out
, I would argue that it is quite similar to salt and pepper noise. If we extend salt and pepper noise to larger patches and not just single pixels, then we’ll end up with something very similar. It might also be interesting to check if could use something else then {0,255} (min/max per color channel) and add some patches that are more similar to the ones around the patch.
Let’s start with a blank image:
import numpy as np
import matplotlib.pyplot as plt
example_image_rgb = np.ones((1200,1300,3), dtype=np.uint8)*128
example_image_gray = np.ones((1200,1300), dtype=np.uint8)*128
plt.figure(figsize=(5,5))
plt.imshow(example_image_rgb)
plt.title("Example image (all values = 128)")
plt.axis("off")
plt.show()

There are many way to create the patches of different color. I decided to add patchCount
to and use imgFraction
to scale each patch according to the size of an image. a little bit of noise is added to introduce some randomness to patch sizes.
class ProgressiveSprinkles(object):
r""" Implements progressive sprinkles
Wright, L. (2019): Progressive sprinkles - a new data augmentation
for CNNs (and helps to achieve new 98+% NIH Malaria dataset accuracy).
(https://medium.com/@lessw/progressive-sprinkles-a-new-data-augmentation-for-cnns-and-helps-achieve-new-98-nih-malaria-6056965f671a)
Additionally, it allows to use patches of Gaussian noise (https://www.simonwenkel.com/2019/11/15/progressive-sprinkles.html).
Inputs:
- threshold (float):
- imgType (str): {"cv2","PIL"}
- lowerValue (int): value for "pepper"
- upperValue (int): value for "salt"
- noiseType (str): {"SnP", "RGB"}
Output:
- image ({np.ndarray, PIL.Image}): image with
noise added
"""
def __init__(self,
imgType:str = "cv2",
lowerValue:int = 0,
upperValue:int = 255,
noiseType:str = "BW",
imgFraction:float = 0.1,
patchCount:int = 11):
self.imgType = imgType
self.lowerValue = lowerValue
self.upperValue = upperValue
if (noiseType != "BW") and (noiseType != "Gauss"):
raise Exception("'noiseType' not of value {'BW', 'Gauss'}")
else:
self.noiseType = noiseType
self.patchCount = patchCount
self.imgFraction = imgFraction
super(ProgressiveSprinkles, self).__init__()
def __call__(self, img):
if self.imgType == "PIL":
img = np.array(img)
if type(img) != np.ndarray:
raise TypeError("Image is not of type 'np.ndarray'!")
# get valid dimensions for patches
x_min = 0
x_max = img.shape[1] * (1-self.imgFraction)
x_range = np.arange(x_min, x_max, 1, dtype=np.int)
y_min = 0
y_max = img.shape[0] * (1-self.imgFraction)
y_range = np.arange(y_min, y_max, 1, dtype=np.int)
if self.noiseType == "BW":
colors = np.array([self.lowerValue, self.upperValue], dtype=np.uint8)
for i in range(self.patchCount):
patch = np.zeros_like(img, dtype=np.int)
patch_y_min = np.random.choice(y_range)
patch_y_max = int(patch_y_min + np.random.rand() * (img.shape[0] * self.imgFraction - 2))
patch_x_min = np.random.choice(x_range)
patch_x_max = int(patch_x_min + np.random.rand() * (img.shape[1] * self.imgFraction - 2))
patch[patch_y_min:patch_y_max, patch_x_min:patch_x_max] = 1
img[patch == 1] = np.random.choice(colors)
elif self.noiseType == "Gauss":
colors = np.array([self.lowerValue, self.upperValue], dtype=np.uint8)
for i in range(self.patchCount):
patch = np.zeros_like(img, dtype=np.int)
patch_y_min = np.random.choice(y_range)
patch_y_max = int(patch_y_min + np.random.rand() * (img.shape[0] * self.imgFraction - 2))
patch_x_min = np.random.choice(x_range)
patch_x_max = int(patch_x_min + np.random.rand() * (img.shape[1] * self.imgFraction - 2))
patch[patch_y_min:patch_y_max, patch_x_min:patch_x_max] = 1
img[patch == 1] = np.array(self.upperValue*np.random.random(img[patch == 1].shape), dtype=np.int)
if self.imgType == "cv2":
return img
elif self.imgType == "PIL":
# return as PIL image for torchvision transforms compliance
return PIL.Image.fromarray(img)
Let’s apply it to our example images:
BW_noise = ProgressiveSprinkles()
Gauss_noise = ProgressiveSprinkles(noiseType="Gauss")
plt.figure(figsize=(15,15))
plt.subplot(2,2,1)
plt.imshow(BW_noise(example_image_rgb))
plt.title("Progressive sprinkles (BW) on RGB image")
plt.axis("off")
plt.subplot(2,2,2)
plt.imshow(BW_noise(example_image_gray), cmap="gray")
plt.title("Progressive sprinkles (BW)on grayscale image")
plt.axis("off")
example_image_rgb = np.ones((1200,1300,3), dtype=np.uint8)*128
example_image_gray = np.ones((1200,1300), dtype=np.uint8)*128
plt.subplot(2,2,3)
plt.imshow(Gauss_noise(example_image_rgb))
plt.title("Progressive sprinkles (Gauss) on RGB image")
plt.axis("off")
plt.subplot(2,2,4)
plt.imshow(Gauss_noise(example_image_gray), cmap="gray")
plt.title("Progressive sprinkles (Gauss) on grayscale image")
plt.axis("off")
plt.show()

NB!: If we would make patches small enough (1x1 pixel), and increase the number of patches, then we would end up with something similar to Salt and Pepper Noise