Как избавиться от размытых фотографий с помощью Python

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

Некачественные снимки напрямую снижают прибыль.

  • Как приложению распознавать нечеткие фотографии на уровне алгоритма?
  • Как измерить четкость RGB-изображения?

Постановка задачи

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

Отсюда возникает задача: «как на уровне алгоритма распознавать нечеткие снимки?»

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

Получается, обучение ML-модели «с учителем» к решению не применимо.

В ходе работы использовал инструменты:

  • Python. Библиотеки: numpy, matplotlib, cv2;
  • Jupyter notebook.

В статье опишу решение задачи, к которому пришел.

Описание подхода к решению задачи

Этап 1. Определение границ

Какая фотография может называться четкой?
Та, у которой границы объектов ярко выражены. У нечетких снимков границы объектов размыты.

Как определить границы объектов на снимке?

Границы там, где видим наибольший перепад цвета.

Получается, для определения четкости снимка вначале нужно определить границы объектов фотографий, потом оценить их величину, толщину, количество и т.д.

Фотография состоит из трехмерного массива чисел от 0 до 255: (ширина, высота, 3 цвета).
Границы определил наложением фильтра как в создании глубокой нейросети: перемножением трехмерного массива на матрицы (по каждому цвету):

    │ 1 -1 │
    │ 1 -1 │

При перепаде цвета результирующий массив будет выдавать высокое по модулю число.
Так определим вертикальные и горизонтальные границы. Среднеарифметическое значений показывает общие границы фотографии.

Этап 2. Анализ границ на предмет четкости

Границы определили.

Как отличить границу нечеткого изображения от границы четкого?

Перебирая разные варианты, нашел следующий подход:

  1. определяем границы оригинальной фотографии (описано в этапе 1);
  2. размываем оригинальное изображение;
  3. определяем границы размытого снимка (описано в этапе 1);
  4. считаем отношение средних арифметических пункта 1 и пункта 2;
  5. полученный коэффициент характеризует четкость снимка.

Логика проста: у четких фотографий изменение границ будет происходить существеннее, чем у нечетких, а значит, коэффициент будет выше.

Реализация алгоритма в Python

Непосредственно для решения задачи используем следующие библиотеки:

import numpy as np
import matplotlib.pyplot as plt
import cv2

Для параметров определения границ зададим функцию определения матрицы:

def edges(n, orient):
    edges = np.ones((2*n, 2*n, 3))
    
    if orient == 'vert':
        for i in range(0, 2*n):
            edges[i][n: 2*n] *= -1
    elif orient == 'horiz':
        edges[n: 2*n] *= -1
    
    return edges

Под параметром n задаем количество пикселей, которые включаем в оценку границ. Ориентация матрицы может быть горизонтальной или вертикальной.

Дальнейшие функции аналогичны слою глубокой нейросети:

# Apply one filter defined by parameters W and single slice
def conv_single_step(a_slice_prev, W):
    s = W * a_slice_prev
    Z = np.sum(s)
    Z = np.abs(Z)
    
    return Z
   
# Full edge filter
def conv_forward(A_prev, W, hparameters):
    m = len(A_prev)
    (f, f, n_C) = W.shape
    stride = hparameters['stride']
    pad = hparameters['pad']
    
    Z = list()
    flag = 0
    z_max = hparameters['z_max']
    
    if len(z_max) == 0:
        z_max = list()
        flag = 1
    
    for i in range(m):
        
        (x0, x1, x2) = A_prev[i].shape
        A_prev_pad = A_prev[i][ 
                            int(x0 / 4) : int(x0 * 3 / 4), 
                            int(x1 / 4) : int(x1 * 3 / 4), 
                            :]
        
        (n_H_prev, n_W_prev, n_C_prev) = A_prev_pad.shape
        n_H = int((n_H_prev - f + 2*pad) / stride) + 1
        n_W = int((n_W_prev - f + 2*pad) / stride) + 1
        z = np.zeros((n_H, n_W))
        
        a_prev_pad = A_prev_pad
        
        for h in range(n_H):
            vert_start = h * stride
            vert_end = h * stride + f
            
            for w in range(n_W):
                horiz_start = w * stride
                horiz_end = w * stride + f
                
               
                a_slice_prev = a_prev_pad[vert_start: vert_end, horiz_start: horiz_end, :]

                weights = W[:, :, :]
                z[h, w] = conv_single_step(a_slice_prev, weights)
        
        if flag == 1:
            z_max.append(np.max(z))
        Z.append(z / z_max[i])
        
    cache = (A_prev, W, hparameters)
    
    return Z, z_max, cache

# pooling
def pool_forward(A_prev, hparameters, mode = 'max'):
    m = len(A_prev)
    f = hparameters['f']
    stride = hparameters['stride']
    
    A = list()
    
    for i in range(m):
        (n_H_prev, n_W_prev) = A_prev[i].shape
        
        n_H = int(1 + (n_H_prev - f) / stride)
        n_W = int(1 + (n_W_prev - f) / stride)
        
        a = np.zeros((n_H, n_W))
        
        for h in range(n_H):
            vert_start = h * stride
            vert_end = h * stride + f
            
            for w in range(n_W):
                horiz_start = w * stride
                horiz_end = w * stride + f
                
                a_prev_slice = A_prev[i][vert_start: vert_end, horiz_start: horiz_end]

                if mode == 'max':
                    a[h, w] = np.max(a_prev_slice)
                elif mode == 'avg':
                    a[h, w] = np.mean(a_prev_slice)
                        
        A.append(a)

    cache = (A_prev, hparameters)
    
    return A, cache

conv_single_step — одно перемножение цветов картинки на матрицы, выявляющую границу.
conv_forward — полное определение границ на всей фотографии.
pool_forward — уменьшаем размер полученного массива.

Отдельно отмечу значение строчек в функции conv_forward:

(x0, x1, x2) = A_prev[i].shape
A_prev_pad = A_prev[i][ 
    int(x0 / 4) : int(x0 * 3 / 4), 
    int(x1 / 4) : int(x1 * 3 / 4), 
    :]

Для анализа используем не всё изображение, а только его центральную часть, т.к. фокус фотоаппарата чаще наводится на центр. Если снимок четкий, то и центр будет четкий.

Следующая функция определяет границы объектов на снимке, используя предыдущие функции:

# main layer
def borders(images, filter_size = 1, stride = 1, pool_stride = 2, pool_size = 2, z_max = []):
    Wv = edges(filter_size, 'vert')
    hparameters = {'pad': pad, 'stride': stride, 'pool_stride': pool_stride, 'f': pool_size, 'z_max': z_max}
    Z, z_max_v, _ = conv_forward(images, Wv, hparameters)
    
    print('edge filter applied')
    
    hparameters_pool = {'stride': pool_stride, 'f': pool_size}
    Av, _ = pool_forward(Z, hparameters_pool, mode = 'max')
    
    print('vertical filter applied')
    
    Wh = edges(filter_size, 'horiz')
    hparameters = {'pad': pad, 'stride': stride, 'pool_stride': pool_stride, 'f': pool_size, 'z_max': z_max}
    Z, z_max_h, _ = conv_forward(images, Wh, hparameters)
    
    print('edge filter applied')
    
    hparameters_pool = {'stride': pool_stride, 'f': pool_size}
    Ah, _ = pool_forward(Z, hparameters_pool, mode = 'max')
    
    print('horizontal filter applied')   
    
    return [(Av[i] + Ah[i]) / 2 for i in range(len(Av))], list(map(np.max, zip(z_max_v, z_max_h)))

Функция определяет вертикальные границы, затем горизонтальные, и возвращает среднее арифметическое обоих массивов.

И основная функция для выдачи параметра четкости:

# calculate borders of original and blurred images
def orig_blur(images, filter_size = 1, stride = 3, pool_stride = 2, pool_size = 2, blur = 57):
    z_max = []

    img, z_max = borders(images, 
                         filter_size = filter_size, 
                         stride = stride, 
                         pool_stride = pool_stride, 
                         pool_size = pool_size
                        )
    print('original image borders is calculated')
    
    blurred_img = [cv2.GaussianBlur(x, (blur, blur), 0) for x in images]
    print('images blurred')
    
    blurred, z_max = borders(blurred_img, 
                             filter_size = filter_size, 
                             stride = stride, 
                             pool_stride = pool_stride, 
                             pool_size = pool_size, 
                             z_max = z_max
                            )
    print('blurred image borders is calculated')

    return [np.mean(orig) / np.mean(blurred) for (orig, blurred) in zip(img, blurred)], img, blurred

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

Функция возвращает список коэффициентов четкости, массив границ оригинального снимка и массив границ размытого.

Пример работы алгоритма

Для анализа взял снимки с фотостока freepik.com.

Определяем границы первого снимка до и после размытия:

Второго:

Третьего:

Четвертого:

На изображениях видно, что изменение границ у четких снимков (3й и 4й) происходит сильнее, чем у нечетких (1й и 2й).

После расчетов получаем коэффициенты:

[5.92918651681958,
2.672756123184502,
10.695051017699232,
11.901115749698139]

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

Особенности подхода

  • чем снимок четче, тем сильнее изменяется граница, а значит, тем выше будет параметр;
  • для разных нужд необходима разная четкость. Поэтому необходимо определять границы четкости самостоятельно: где-то коэффициент достаточной четких фотографий будет выше 7, где-то только выше 10;
  • коэффициент зависит от яркости фотографии. Границы темных фотографий будут изменяться слабее, а значит, и коэффициент будет меньше. Получается, границы четкости нужно определять с учетом освещения, то есть для типовых фотографий;

Работающий алгоритм можно найти на моем аккаунте в github.

Специально для сайта ITWORLD.UZ. Новость взята с сайта Хабр