Salt & Pepper Noise and Median Filters, Part II – The Code

In my first post on salt & pepper noise (hereon s&p noise) and median filters I gave an overview what s&p noise is, why it occurs, and how we can tackle getting rid of it. As discussed, median filters are especially effective at removing s&p noise from images. At the end of the last post I promised to delve into the code behind generating an image with s&p noise and the filters to remove it. Read on for code extracts and explanations.

In order to manipulate images I used the OpenCV  library on top of the Python programming language. OpenCV is an open-source set of programming functions aimed at computer vision applications. It is supported on macOS, Linux, Windows, iOS, and Android and has interfaces to C, C++, Python, and Java. The installation of OpenCV and Python on macOS was quite involved but this tutorial from pyimagesearch was a great starting point. There are many sources of installation instructions for other operating systems just a Google search away. Once you’ve installed the library, the tutorials (python-specific here) in the official OpenCV documentation are great for getting familiar with how the library works.

Generating Noise

In order to remove s&p noise we’ll first have it to add it to an image. Below is a Python function written to do just that with 8-bit images:

def salt_n_pepper(img, pad = 101, show = 1):
	# Convert img1 to 0 to 1 float to avoid wrapping that occurs with uint8
	img = to_std_float(img)
	
	# Generate noise to be added to the image. We are interested in occurrences of high
	# and low bounds of pad. Increased pad size lowers occurence of high and low bounds.
	# These high and low bounds are converted to salt and pepper noise later in the 
	# function. randint is inclusive of low bound and exclusive of high bound.
	noise = np.random.randint(pad, size = (img.shape[0], img.shape[1], 1))
	
	# Convert high and low bounds of pad in noise to salt and pepper noise then add it to
	# our image. 1 is subtracted from pad to match bounds behaviour of np.random.randint.
	img = np.where(noise == 0, 0, img)
	img = np.where(noise == (pad-1), 1, img)
	
	# Properly handles the conversion from float16 back to uint8
	img = to_std_uint8(img)
	
	display_result(img, 'Image with Salt & Pepper Noise', show)
	
	return img

The function accepts an image (‘img’) to be modified, a ‘pad’ quantity that will be used in generating/distributing noise, and a ‘show’ variable which is used to indicate to the function whether it should display its output to the user. Let’s explore the function in sections:

	# Convert img1 to 0 to 1 float to avoid wrapping that occurs with uint8
	img = to_std_float(img) 

This above section calls the following function:

def to_std_float(img):
	#Converts img to 0 to 1 float to avoid wrapping that occurs with uint8
	img.astype(np.float16, copy = False)
	img = np.multiply(img, (1/255))
	
	return img

This function accepts an 8-bit image and converts its integer pixel values that range from 0-255 to floating point pixel values that range from 0-1. By doing this we avoid a property of the NumPy uint8 data type (NumPy is a scientific computing package for Python that OpenCV in Python relies on). If we do not convert to a floating point data type here, adding a value to a pixel which increases it to a number greater than 255 will cause the value to wrap around to zero before continuing the addition. For example, 255+2 will produce 1 instead of being clipped to 255 (as would happen when a pixel value is saturated in a camera).

The next section generates a set of random integers that will be used to generate s&p noise:

	# Generate noise to be added to the image. We are interested in occurrences of high
	# and low bounds of pad. Increased pad size lowers occurence of high and low bounds.
	# These high and low bounds are converted to salt and pepper noise later in the 
	# function. randint is inclusive of low bound and exclusive of high bound.
	noise = np.random.randint(pad, size = (img.shape[0], img.shape[1], 1))

We call the ‘randint’ function in NumPy to supply us with a set of random integers with values from 0 to (pad – 1) that is the same shape of the image we are adding noise to. The rationale here is that noise will be added to the image where 0 and (pad – 1) show up in the random integer set. Higher values of pad will decrease the likelihood of 0 and (pad – 1) occurring and result in less noise being added to the image (vice versa for smaller values of pad).

As indicated above, once we have our random integers we add noise to the image where 0 and (pad – 1) show up in the random integer set:

	# Convert high and low bounds of pad in noise to salt and pepper noise then add it to
	# our image. 1 is subtracted from pad to match bounds behaviour of np.random.randint.
	img = np.where(noise == 0, 0, img)
	img = np.where(noise == (pad-1), 1, img)

Above we use the ‘where’ function in Numpy to insert a 0 into our image when a 0 appears in our random set and to insert a 1 where (pad – 1) appears in our random set. This produces the characteristic black and white s&p speckles. ‘where’ allows for this to be accomplished without loops.

Now that we have added noise to our image all that is left to be done is to convert our image back to 8-bit pixel values. That is accomplished with the following line:

	# Properly handles the conversion from float16 back to uint8
	img = to_std_uint8(img)

Which in turn calls the following function:

def to_std_uint8(img):
	# Properly handles the conversion to uint8
	img = cv2.convertScaleAbs(img, alpha = (255/1))
	
	return img

‘convertScaleAbs’ is an OpenCV function that safely handles the conversion from our 0-1 floating point pixel values to a 0-255 integer pixel values. We now have an image with s&p noise that we can either display or return to the main calling function for use elsewhere:

	display_result(img, 'Image with Salt & Pepper Noise', show)
	
	return img

‘display_result’ is a function I wrote that wraps OpenCV’s display functions and allows me to specify with a 0 or 1 whether the output of a function should be displayed to the user:

def display_result(img, title = 'Image', show = 1):
	cv2.imshow(title, img)
	#Required to show and close the image window
	if show == 1:
		cv2.waitKey(0)
		cv2.destroyAllWindows()
Filtering Noise

Now that we have our image with s&p noise added to it we can apply filters to it to remove the noise. Below is my Python code for applying a Median filter to an image:

def median(img, ksize = 3, title = 'Median Filter Result', show = 1):
	# Median filter function provided by OpenCV. ksize is the kernel size. 
	img = cv2.medianBlur(img, ksize)
	
	display_result(img, title, show)
	
	return img

OpenCV allows us to not have to reinvent the wheel by providing a built-in ‘medianBlur’ function:

	# Median filter function provided by OpenCV. ksize is the kernel size. 
	img = cv2.medianBlur(img, ksize)

All we need to do is supply the image to be filtered (‘img’) and the aperture size (‘ksize’) which will be used to make a ‘ksize’ x ‘ksize’ filter. The aperture value must be odd and greater than 1. Larger aperture values will result in increased blurring of details due to a large region of pixels being used to generate the filtered pixel. Once the image is filtered we can display it and return it for use.

Leave a Reply

Your email address will not be published.