Como se livrar de fotos borradas usando Python

Quando tiramos uma grande série de fotos, algumas são imprecisas. Uma grande empresa automobilística enfrentou o mesmo problema. Algumas das fotos durante a inspeção do carro ficaram embaçadas, o que poderia afetar negativamente as vendas.

Imagens de baixa qualidade reduzem diretamente os lucros.


  • Como um aplicativo reconhece fotos difusas no nível do algoritmo?
  • Como medir a clareza de uma imagem RGB?



Formulação do problema


Eu trabalho como analista em uma grande empresa automobilística. Ao inspecionar um carro, ao inspecionar um carro, eles tiram muitas fotos através de um aplicativo especial, que é imediatamente enviado ao banco de dados. Algumas imagens estão tremidas, o que é ruim para as vendas.

A partir daqui, surge o problema: "como reconhecer imagens difusas no nível do algoritmo?"

Desenvolvi um algoritmo baseado em uma amostra de 1200 fotos de diferentes elementos de carros. Uma característica da amostra é que ela não está rotulada, porque é difícil identificar quais fotos são nítidas e quais não são.

Acontece que aprender o modelo de BC "com um professor" não é aplicável à solução.

No decorrer do trabalho, usei ferramentas:

  • Python . Bibliotecas: numpy, matplotlib, cv2;
  • Caderno Jupyter .

No artigo, descreverei a solução para o problema para o qual vim.

Descrição da abordagem para resolver o problema


Etapa 1. Definindo os limites


Que foto pode ser chamada clara?
Um em que os limites dos objetos são pronunciados. Em fotos confusas, as bordas dos objetos ficam embaçadas.

Como determinar os limites dos objetos na imagem?

Fronteiras onde vemos a maior diferença de cores.

Acontece que, para determinar a clareza da imagem, primeiro é necessário determinar os limites dos objetos das fotografias e, em seguida, avaliar seu tamanho, espessura, número etc.

A foto consiste em uma matriz tridimensional de números de 0 a 255: (largura, altura, 3 cores).
Eu defini os limites aplicando um filtro como na criação de uma rede neural profunda: multiplicando uma matriz tridimensional por matrizes (para cada cor):

    │ 1 -1 │
    │ 1 -1 │

Com uma diferença de cor, a matriz resultante produzirá um número de módulo alto.
Então, definimos os limites verticais e horizontais. A média aritmética mostra as bordas comuns da fotografia.

Etapa 2. Análise dos limites para maior clareza


Os limites são definidos.

Como distinguir a borda de uma imagem difusa da borda de uma imagem nítida?

Passando por diferentes opções, encontrei a seguinte abordagem:

  1. defina os limites da foto original (descrita na etapa 1);
  2. desfoque a imagem original;
  3. defina os limites da imagem tremida (descrita na etapa 1);
  4. consideramos a razão da média aritmética do parágrafo 1 e do parágrafo 2;
  5. o coeficiente resultante caracteriza a clareza da imagem.

A lógica é simples: em fotografias nítidas, a mudança nas bordas ocorrerá mais significativamente do que nas borradas, o que significa que o coeficiente será maior.

Implementação em Python do algoritmo


Para resolver o problema diretamente, usamos as seguintes bibliotecas:

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

Para os parâmetros para determinar os limites, definimos a função de definição de matriz:

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

Sob o parâmetro n, especificamos o número de pixels que incluímos na estimativa de limites. A orientação da matriz pode ser horizontal ou vertical.

Outras funções são semelhantes a uma camada de rede neural profunda:

# 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 - uma multiplicação das cores da imagem por matrizes que revelam a borda.
conv_forward - Uma definição completa das bordas na foto inteira.
pool_forward - reduz o tamanho da matriz resultante.

Separadamente, observo o valor das linhas na função 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), 
    :]

Para análise, usamos não toda a imagem, mas apenas sua parte central, porque a câmera foca com mais frequência no centro. Se a imagem estiver nítida, o centro ficará nítido.

A função a seguir determina os limites dos objetos na imagem usando as funções anteriores:

# 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)))

A função determina os limites verticais, depois os horizontais, e retorna a média aritmética de ambas as matrizes.

E a principal função para emitir o parâmetro de definição:

# 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

Primeiro, determinamos os limites da imagem original, depois borramos a imagem, depois determinamos os limites da foto borrada e, finalmente, consideramos a proporção dos limites médios aritméticos da imagem original e da desfocada.

A função retorna uma lista de fatores de definição, uma matriz de bordas da imagem original e uma matriz de bordas desfocadas.

Exemplo de Operação de Algoritmo


Para análise, tirei fotos do banco de fotos freepik.com.









Determinamos os limites da primeira imagem antes e depois da desfocagem:



Segunda:





Terceira:





Quarta:





Nas imagens, observa-se que as alterações de borda nas imagens nítidas (3ª e 4ª) são mais fortes que nas difusas (1ª e 2ª).

Após os cálculos, obtemos os coeficientes:

[5.92918651681958,
2.672756123184502,
10.695051017699232,
11.901115749698139]

Os coeficientes confirmam as conclusões: quanto maior o coeficiente, mais nítida a foto.
Além disso, a segunda imagem é menos clara que a primeira, refletida nos coeficientes.

Recursos de abordagem


  • quanto mais nítida a imagem, mais forte a borda muda, o que significa que quanto maior o parâmetro;
  • para necessidades diferentes, é necessária uma clareza diferente. Portanto, é necessário determinar os limites de clareza por conta própria: em algum lugar, o coeficiente de fotos claras suficientes estará acima de 7, em algum lugar apenas acima de 10;
  • o coeficiente depende do brilho da foto. As margens das fotos escuras mudarão mais fracas, o que significa que o coeficiente será menor. Acontece que os limites de clareza devem ser determinados levando em consideração a iluminação, isto é, para fotografias comuns;

Um algoritmo de trabalho pode ser encontrado na minha conta do github .

All Articles