Cómo deshacerse de fotos borrosas usando Python

Cuando tomamos una gran serie de fotos, algunas de ellas son borrosas. Una gran empresa automotriz enfrentaba el mismo problema. Algunas de las fotografías durante la inspección del automóvil resultaron borrosas, lo que podría afectar negativamente las ventas.

Las imágenes de baja calidad reducen directamente los beneficios.


  • ¿Cómo reconoce una aplicación fotos borrosas a nivel de algoritmo?
  • ¿Cómo medir la claridad de una imagen RGB?



Formulación del problema


Trabajo como analista en una gran empresa automotriz. Al inspeccionar un automóvil, al inspeccionar un automóvil, toman muchas fotos a través de una aplicación especial, que se envían inmediatamente a la base de datos. Algunas imágenes son borrosas, lo que es malo para las ventas.

A partir de aquí surge el problema: "¿cómo reconocer imágenes borrosas a nivel de algoritmo?"

Desarrollé un algoritmo basado en una muestra de 1200 fotos de diferentes elementos de automóviles. Una característica de la muestra es que no está etiquetada, porque es difícil determinar qué imágenes son claras y cuáles no.

Resulta que aprender el modelo ML "con un maestro" no es aplicable a la solución.

En el curso del trabajo utilicé herramientas:

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

En el artículo describiré la solución al problema al que vine.

Descripción del enfoque para resolver el problema.


Etapa 1. Definiendo los límites


¿Qué foto se puede llamar clara?
Uno en el que se pronuncian los límites de los objetos. En tomas borrosas, los bordes de los objetos están borrosos.

¿Cómo determinar los límites de los objetos en la imagen?

Fronteras donde vemos la mayor diferencia de color.

Resulta que para determinar la claridad de la imagen, primero debe determinar los límites de los objetos de las fotografías, luego evaluar su tamaño, grosor, número, etc.

La foto consiste en una matriz tridimensional de números del 0 al 255: (ancho, alto, 3 colores).
Definí los límites aplicando un filtro como al crear una red neuronal profunda: multiplicando una matriz tridimensional por matrices (para cada color):

    │ 1 -1 │
    │ 1 -1 │

Con una diferencia de color, la matriz resultante producirá un número de módulo alto.
Entonces definimos los límites verticales y horizontales. La media aritmética muestra los bordes comunes de la fotografía.

Etapa 2. Análisis de los límites para mayor claridad.


Los límites están definidos.

¿Cómo distinguir el borde de una imagen borrosa del borde de una imagen clara?

Al pasar por diferentes opciones, encontré el siguiente enfoque:

  1. definir los límites de la foto original (descrita en el paso 1);
  2. desenfoque la imagen original;
  3. definir los límites de la imagen borrosa (descrita en el paso 1);
  4. consideramos la razón de la media aritmética del párrafo 1 y el párrafo 2;
  5. El coeficiente resultante caracteriza la claridad de la imagen.

La lógica es simple: en fotografías claras, el cambio en los bordes ocurrirá más significativamente que en los borrosos, lo que significa que el coeficiente será mayor.

Implementación de Python del algoritmo


Para resolver el problema directamente, utilizamos las siguientes bibliotecas:

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

Para los parámetros para determinar los límites, definimos la función de definición 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

Bajo el parámetro n, especificamos el número de píxeles que incluimos en la estimación de límites. La orientación de la matriz puede ser horizontal o vertical.

Otras funciones son similares a una capa de red neuronal 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: una multiplicación de los colores de la imagen por matrices que revelan el borde.
conv_forward: una definición completa de los bordes en toda la foto.
pool_forward: reduce el tamaño de la matriz resultante.

Por separado, observo el valor de las líneas en la función 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 el análisis, no utilizamos toda la imagen, sino solo su parte central, porque la cámara enfoca más a menudo en el centro. Si la imagen es clara, entonces el centro estará claro.

La siguiente función determina los límites de los objetos en la imagen utilizando las funciones 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)))

La función determina los límites verticales, luego los horizontales, y devuelve la media aritmética de ambas matrices.

Y la función principal para emitir el parámetro de definición:

# 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

Primero, determinamos los límites de la imagen original, luego desenfocamos la imagen, luego determinamos los límites de la foto borrosa y, finalmente, consideramos la relación de los límites medios aritméticos de la imagen original y la imagen borrosa.

La función devuelve una lista de factores de definición, una matriz de bordes de la imagen original y una matriz de bordes borrosos.

Ejemplo de operación de algoritmo


Para el análisis, tomé fotos del stock de fotos de freepik.com.









Determinamos los límites de la primera imagen antes y después del desenfoque:



Segundo:





Tercero:





Cuarto:





en las imágenes se ve que los cambios en el borde para imágenes claras (3ra y 4ta) son más fuertes que para las borrosas (1ra y 2da).

Después de los cálculos, obtenemos los coeficientes:

[5.92918651681958,
2.672756123184502,
10.695051017699232,
11.901115749698139]

Los coeficientes confirman las conclusiones: cuanto mayor es el coeficiente, más nítida es la foto.
Además, la segunda imagen es menos clara que la primera, lo que se refleja en los coeficientes.

Características de aproximación


  • cuanto más nítida es la imagen, más fuerte cambia el borde, lo que significa que mayor será el parámetro;
  • para diferentes necesidades, se necesita una claridad diferente. Por lo tanto, es necesario determinar los límites de claridad por su cuenta: en algún lugar, el coeficiente de suficientes fotos claras será superior a 7, en algún lugar solo superior a 10;
  • El coeficiente depende del brillo de la foto. Los bordes de las fotos oscuras cambiarán más débil, lo que significa que el coeficiente será menor. Resulta que los límites de claridad deben determinarse teniendo en cuenta la iluminación, es decir, para fotografías estándar;

Se puede encontrar un algoritmo de trabajo en mi cuenta de github .

All Articles