From 5f7fc5283094dd74e8385a8df7757241ed8dd017 Mon Sep 17 00:00:00 2001 From: "Josh Warner (Mac)" Date: Sat, 29 Jun 2013 18:00:26 -0500 Subject: [PATCH] FEAT: generator to add various types of random noise to images --- skimage/util/__init__.py | 4 +- skimage/util/noise.py | 119 ++++++++++++++++++++++++ skimage/util/tests/test_random_noise.py | 87 +++++++++++++++++ 3 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 skimage/util/noise.py create mode 100644 skimage/util/tests/test_random_noise.py diff --git a/skimage/util/__init__.py b/skimage/util/__init__.py index 5433a14b..a4274484 100644 --- a/skimage/util/__init__.py +++ b/skimage/util/__init__.py @@ -1,6 +1,7 @@ from .dtype import (img_as_float, img_as_int, img_as_uint, img_as_ubyte, img_as_bool, dtype_limits) from .shape import view_as_blocks, view_as_windows +from .noise import random_noise import numpy ver = numpy.__version__.split('.') @@ -20,4 +21,5 @@ __all__ = ['img_as_float', 'dtype_limits', 'view_as_blocks', 'view_as_windows', - 'pad'] + 'pad', + 'random_noise'] diff --git a/skimage/util/noise.py b/skimage/util/noise.py new file mode 100644 index 00000000..bf1d412d --- /dev/null +++ b/skimage/util/noise.py @@ -0,0 +1,119 @@ +import numpy as np +from .dtype import img_as_float + + +__all__ = ['random_noise'] + + +def random_noise(image, mode='gaussian', seed=None, **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 + One of the following strings, selecting the type of noise to add: + + 'gaussian' Gaussian-distributed additive noise. + '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. + 'speckle' Multiplicative noise using out = image + n*image, where + n is uniform noise with specified mean & variance. + seed : int + If provided, this will set the random seed before generating noise. + m : float + Mean of random distribution. Used in 'gaussian' and 'speckle'. + v : float + Variance of random distribution. Used in 'gaussian' and 'speckle'. + Note: variance = (standard deviation) ** 2 + d : float + Proportion of image pixels to replace with noise on range [0, 1]. + Used in 'salt', 'pepper', and 'salt & pepper'. + p : float + Proportion of salt vs. pepper noise for 's&p' on range [0, 1]. + Higher values represent more salt. + + Returns + ------- + out : ndarray + Output floating-point image data on range [0, 1]. + + """ + mode = mode.lower() + image = img_as_float(image) + if seed is not None: + np.random.seed(seed=seed) + + allowedtypes = { + 'gaussian': 'gaussian_values', + 'poisson': '', + 'salt': 'sp_values', + 'pepper': 'sp_values', + 's&p': 's&p_values', + 'speckle': 'gaussian_values'} + + kwdefaults = { + 'm': 0., + 'v': 0.01, + 'd': 0.05, + 'p': 0.5} + + allowedkwargs = { + 'gaussian_values': ['m', 'v'], + 'sp_values': ['d'], + 's&p_values': ['d', 'p']} + + for key in kwargs: + if key not in allowedkwargs[allowedtypes[mode]]: + raise ValueError('%s keyword not in allowed keywords %s' % + (key, allowedkwargs[allowedtypes[mode]])) + + # Set kwarg defaults + for kw in allowedkwargs[allowedtypes[mode]]: + kwargs.setdefault(kw, kwdefaults[kw]) + + if mode == 'gaussian': + noise = np.random.normal(kwargs['m'], kwargs['v'] ** 0.5, image.shape) + out = np.clip(image + noise, 0., 1.) + + elif mode == 'poisson': + # Generating noise for each unique value in image. + out = np.zeros_like(image) + for val in np.unique(image): + # Generate mask for a unique value, replace w/values drawn from + # Poisson distribution about the unique value + mask = image == val + out[mask] = np.poisson(val, mask.sum()) + + elif mode == 'salt': + # Re-call function with mode='s&p' and p=1 (all salt noise) + out = random_noise(image, mode='s&p', seed=seed, d=kwargs['d'], p=1) + + elif mode == 'pepper': + # Re-call function with mode='s&p' and p=1 (all pepper noise) + out = random_noise(image, mode='s&p', seed=seed, d=kwargs['d'], p=0) + + elif mode == 's&p': + out = image.copy() + + # Salt mode + num_salt = np.ceil(kwargs['d'] * image.size * kwargs['p']) + coords = [np.random.randint(0, i - 1, num_salt) + for i in image.shape] + out[coords] = 1 + + # Pepper mode + num_pepper = np.ceil(kwargs['d'] * image.size * (1. - kwargs['p'])) + coords = [np.random.randint(0, i - 1, num_pepper) + for i in image.shape] + out[coords] = 0 + + elif mode == 'speckle': + noise = np.random.normal(kwargs['m'], kwargs['v'] ** 0.5, image.shape) + out = np.clip(image + image * noise, 0., 1.) + + return out diff --git a/skimage/util/tests/test_random_noise.py b/skimage/util/tests/test_random_noise.py new file mode 100644 index 00000000..98b78117 --- /dev/null +++ b/skimage/util/tests/test_random_noise.py @@ -0,0 +1,87 @@ +from numpy.testing import assert_array_equal, assert_allclose + +import numpy as np +from skimage.data import camera +from skimage.util import random_noise, img_as_float + + +def test_set_seed(): + seed = 42 + cam = camera() + test = random_noise(cam, seed=seed) + assert_array_equal(test, random_noise(cam, seed=seed)) + + +def test_salt(): + seed = 42 + cam = img_as_float(camera()) + cam_noisy = random_noise(cam, seed=seed, mode='salt', d=0.15) + saltmask = cam != cam_noisy + + # Ensure all changes are to 1.0 + assert_allclose(cam_noisy[saltmask], np.ones(saltmask.sum())) + + # Ensure approximately correct amount of noise was added + proportion = float(saltmask.sum()) / (cam.shape[0] * cam.shape[1]) + assert 0.11 < proportion <= 0.18 + + +def test_pepper(): + seed = 42 + cam = img_as_float(camera()) + cam_noisy = random_noise(cam, seed=seed, mode='pepper', d=0.15) + peppermask = cam != cam_noisy + + # Ensure all changes are to 1.0 + assert_allclose(cam_noisy[peppermask], np.zeros(peppermask.sum())) + + # Ensure approximately correct amount of noise was added + proportion = float(peppermask.sum()) / (cam.shape[0] * cam.shape[1]) + assert 0.11 < proportion <= 0.18 + + +def test_salt_and_pepper(): + seed = 42 + cam = img_as_float(camera()) + cam_noisy = random_noise(cam, seed=seed, mode='s&p', d=0.15, p=0.25) + saltmask = np.logical_and(cam != cam_noisy, cam_noisy == 1.) + peppermask = np.logical_and(cam != cam_noisy, cam_noisy == 0.) + + # Ensure all changes are to 0. or 1. + assert_allclose(cam_noisy[saltmask], np.ones(saltmask.sum())) + assert_allclose(cam_noisy[peppermask], np.zeros(peppermask.sum())) + + # Ensure approximately correct amount of noise was added + proportion = float( + saltmask.sum() + peppermask.sum()) / (cam.shape[0] * cam.shape[1]) + 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 + + +def test_gaussian(): + seed = 42 + data = np.zeros((128, 128)) + 0.5 + data_gaussian = random_noise(data, seed=seed, v=0.01) + assert 0.008 < data_gaussian.var() < 0.012 + + data_gaussian = random_noise(data, seed=seed, m=0.3, v=0.015) + assert 0.28 < data_gaussian.mean() - 0.5 < 0.32 + assert 0.012 < data_gaussian.var() < 0.018 + + +def test_speckle(): + seed = 42 + data = np.zeros((128, 128)) + 0.1 + np.random.seed(seed=42) + noise = np.random.normal(0.1, 0.02 ** 0.5, (128, 128)) + expected = np.clip(data + data * noise, 0, 1) + + data_speckle = random_noise(data, mode='speckle', seed=seed, m=0.1, + v=0.02) + assert_allclose(expected, data_speckle) + + +if __name__ == '__main__': + np.testing.run_module_suite()