Зашумление и нормализация изображений

Часто при обучении возникают ситуации, когда данных не хватает, поэтому для увеличения разнообразия при обучении используют методы аугментации. Для изображений методов довольно много и они разделены по типам:

https://albumentations.ai/docs/api_reference/augmentations/

Не вдаваясь в детали рассмотрим метод добавления на изображения шума. Для этого есть метод аугментации GaussNoise (см. по тому же адресу), но рассмотрим, как это сделать вручную, используя PyTorch.

import numpy as np

import torch

import matplotlib.pyplot as plt

import cv2

Исходное изображение:

im = cv2.imread("IMG_6391.jpg")

im = np.array(im, dtype='float32')

im /=255.

plt.imshow(im)

plt.show()

2024-08-29_15-58-57

Нормализация изображений используется довольно часто, особенно когда данных мало и не возможно покрыть все возможные результаты даже с аугментацией. В таких случаях перед зашумлением делается нормализация изображения. 

Простейшая функция нормализации может быть реализована так:

def norm(x:torch.Tensor,smooth=1e-5):

    mean=x.mean()

    std=x.std()

    x=(x-mean)/(std+smooth)   

    return x

Т.е. считаем среднее значение пикселя по всему изображению. Затем среднее квадратичное отклонение, и вычисляем из этого нормализованное изображение. Смотрим полученное изображение:

imt = torch.from_numpy(im)

imt = norm(imt)

im2 = imt.numpy()

plt.imshow(im2)

plt.show()

2024-08-29_16-09-50

Видно, что изображение сверху стало слишком ярким, а снизу - темным, так что деталей не различить. Однако у функции mean есть аргументы:

torch.mean(input, dim, keepdim=False, *, dtype=None, out=None)

Если параметр Keepdim имеет значение True, выходной тензор имеет тот же размер, что и входной.  Можно построить функцию, которая не будет считать среднее и отклонение по всему изображению:

def norm2(x:torch.Tensor,smooth=1e-5):

    dim=list(range(1,x.ndim))   

    mean=x.mean(dim=dim,keepdim=True)   

    std=x.std(dim=dim,keepdim=True)

    x=(x-mean)/(std+smooth)   

    return x

В этом случае нормализованное изображение будет выглядеть интереснее:

2024-08-29_16-16-09

Но вернемся к зашумлению. В соревновании на Kaggle:

https://www.kaggle.com/competitions/blood-vessel-segmentation

было очень хорошее baseline:

https://www.kaggle.com/code/yoyobar/2-5d-cutting-model-baseline-training/notebook

В котором использовалась функция:

def add_noise(x:torch.Tensor,max_randn_rate=0.1,randn_rate=None,x_already_normed=False):

    """input.shape=(batch,f1,f2,...) output's var will be normalizate  """

    ndim=x.ndim-1

    if x_already_normed:

        x_std=torch.ones([x.shape[0]]+[1]*ndim,device=x.device,dtype=x.dtype)

        x_mean=torch.zeros([x.shape[0]]+[1]*ndim,device=x.device,dtype=x.dtype)

    else:

        dim=list(range(1,x.ndim))

        x_std=x.std(dim=dim,keepdim=True)

        x_mean=x.mean(dim=dim,keepdim=True)

    if randn_rate is None:

        randn_rate=max_randn_rate*np.random.rand()*torch.rand(x_mean.shape,device=x.device,dtype=x.dtype)

    cache=(x_std**2+(x_std*randn_rate)**2)**0.5   

   

    return (x-x_mean+torch.randn(size=x.shape,device=x.device,dtype=x.dtype)*randn_rate*x_std)/(cache+1e-7)

Она использовалась именно для добавления шума. Варьируя значения max_randn_rate можно получить различные значения шума (1,5, 20):

imt2 = add_noise(imt,max_randn_rate=1,x_already_normed=True)

im3 = imt2.numpy()

plt.imshow(im3)

plt.show()

  • 2024-08-29_16-24-56
  • 2024-08-29_16-25-13
  • 2024-08-29_16-25-34

Как видно, такой подход к зашумлению работает достаточно эффективно и позволяет вносить значительные изменения в выборку при обучении. Правда это не всегда работает, но это уже другая история.