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()
example image
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()
example image

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()
example image

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()
example image

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