Home > OS >  Add noise with varying grain size in Python
Add noise with varying grain size in Python

Time:04-11

I am trying to add noise into image to imitate real world noise created by having high ISO settings in camera.

from skimage.util import random_noise
import random

val = random.uniform(0.036, 0.107)
noisy_img = random_noise(im_arr, mode='gaussian', var=val ** 2)
noisy_img = (255 * noisy_img).astype(np.uint8)

That code works fine, but the size of the noise grain is always 1 pixel. I really want to have a varying size of the noise grain. How can I achieve that?

CodePudding user response:

It's very challenging to imitate the varying grain size noise of high ISO settings.
One of the reasons is that the source of the varying grain is not purely physical effect.
Some of the grain comes from digital noise reduction (image processing) artifacts that are different from camera to camera.

I thought about a relatively simple solution:

  • Add random noise at different resolutions.
  • Resize the different resolutions to the original image size.
  • Sum the resized images to from "noise image" (with zero mean).
  • Add the "noise image" to the original (clean) image.

A lot of tuning is required - selecting the resolutions, setting different noise to different resolutions, select the resizing interpolation method...

I don't think it's going to be exactly what you are looking for, but it applies "noise with varying grain size", and may give you a lead.


Code sample:

from skimage.util import random_noise
from skimage.io import imsave
from skimage.transform import resize
import random
import numpy as np

im_arr = np.full((256, 320), 0.5)  # Original image - use gray image for testing

rows, cols = im_arr.shape

val = 0.036 #random.uniform(0.036, 0.107) # Use constant variance (for testing).

# Full resolution
noise_im1 = np.zeros((rows, cols))
noise_im1 = random_noise(noise_im1, mode='gaussian', var=val**2, clip=False)

# Half resolution
noise_im2 = np.zeros((rows//2, cols//2))
noise_im2 = random_noise(noise_im2, mode='gaussian', var=(val*2)**2, clip=False)  # Use val*2 (needs tuning...)
noise_im2 = resize(noise_im2, (rows, cols))  # Upscale to original image size

# Quarter resolution
noise_im3 = np.zeros((rows//4, cols//4))
noise_im3 = random_noise(noise_im3, mode='gaussian', var=(val*4)**2, clip=False)  # Use val*4 (needs tuning...)
noise_im3 = resize(noise_im3, (rows, cols))  # What is the interpolation method?

noise_im = noise_im1   noise_im2   noise_im3  # Sum the noise in multiple resolutions (the mean of noise_im is around zero).

noisy_img = im_arr   noise_im  # Add noise_im to the input image.

noisy_img = np.round((255 * noisy_img)).clip(0, 255).astype(np.uint8)

imsave('noisy_img.png', noisy_img)

Result:
enter image description here

CodePudding user response:

Your question suggests that you want spatially correlated noise, whereby neighboring pixels share some information. If you don't really care about what that correlation structure looks like, you can use a simple smoothing kernel to generate noise with coarser granularity.

One way to achieve that would be:

from skimage.data import shepp_logan_phantom
from skimage.util import random_noise
from scipy.ndimage import correlate
import numpy as np

# Granularity = 1
im_arr = shepp_logan_phantom()
val = 0.05
noisy_img = random_noise(im_arr, mode='gaussian', var=val)

# Correlated noise to increase granularity
# Generate random noise like skimage's random_noise does
noise = np.random.normal(scale=np.sqrt(val), size=im_arr.shape)
# Create a smoothing kernel
weights = np.array([[0, 1, 0], [1, 1, 1], [0, 1, 0]]) / 5
# Apply it to the noise
noise_corr = correlate(noise, weights)
# Apply noise to image and clip
noisy_img_corr = np.clip(im_arr   noise_corr, 0, 1)


fig, (ax1, ax2) = plt.subplots(ncols=2)
ax1.imshow(noisy_img)
ax1.set_title("Uncorrelated noise")
ax1.axis("off")
ax2.imshow(noisy_img_corr)
ax2.set_title("Correlated noise")
ax2.axis("off")

corr

Or you could come up with better noise model from first principles if you know where the noise in your camera is coming from. There are some ideas here: https://graphics.stanford.edu/courses/cs178-10/lectures/noise-27apr10-150dpi-med.pdf .

CodePudding user response:

Rotem's answer is the best implementation.

I (the original poster) use the following code to expand on his implementation for colored images and using PIL as import, just in case anyone need it later:


from skimage.transform import resize
import numpy as np
from skimage.util import random_noise
from PIL import Image

def gen_noise_mask(rows, cols):
    val = 0.036  # random.uniform(0.036, 0.107) # Use constant variance (for testing).
    # Full resolution
    noise_im1 = np.zeros((rows, cols))
    noise_im1 = random_noise(noise_im1, mode='gaussian', var=val ** 2, clip=False)

    # Half resolution
    noise_im2 = np.zeros((rows // 2, cols // 2))
    noise_im2 = random_noise(noise_im2, mode='gaussian', var=(val * 2) ** 2, clip=False)  # Use val*2 (needs tuning...)
    noise_im2 = resize(noise_im2, (rows, cols))  # Upscale to original image size

    # Quarter resolution
    noise_im3 = np.zeros((rows // 4, cols // 4))
    noise_im3 = random_noise(noise_im3, mode='gaussian', var=(val * 4) ** 2, clip=False)  # Use val*4 (needs tuning...)
    noise_im3 = resize(noise_im3, (rows, cols))  # What is the interpolation method?

    noise_im = noise_im1   noise_im2   noise_im3  # Sum the noise in multiple resolutions (the mean of noise_im is around zero).
    return noise_im


def noiseGenerator(im):
    im_arr = np.asarray(im)

    rows, cols, depth = im_arr.shape

    rgba_array = np.zeros((rows, cols, depth), 'float64')
    for d in range(0, depth):
        rgba_array[..., d]  = gen_noise_mask(rows, cols)
    noisy_img = im_arr / 255   rgba_array  # Add noise_im to the input image.
    noisy_img = np.round((255 * noisy_img)).clip(0, 255).astype(np.uint8)
    return Image.fromarray(noisy_img)
  • Related