Search code examples
pythonimageopencvnoise

Add noise with varying grain size in Python


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?


Solution

  • 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