Analizamos las obras maestras de la pintura con la ayuda de ML clásico.

¡Hola a todos! Un amigo mío estudia como artista y habla regularmente sobre esta o aquella obra maestra, sobre técnicas de composición únicas, sobre la percepción del color, sobre la evolución de la pintura y artistas brillantes. En el contexto de este impacto constante, decidí verificar si mis conocimientos y habilidades de ingeniería son adecuados para analizar el patrimonio cultural mundial.

Armado con un analizador improvisado al amparo de la noche, irrumpí en la galería en línea y saqué casi 50 mil pinturas de allí. Veamos qué interesante se puede hacer con esto, usando solo herramientas clásicas de ML (precaución, tráfico).

Transformación ingenua


Como muchos de nosotros recordamos de las lecciones de informática, una imagen se representa como una matriz de bytes que son responsables del color de cada píxel individual. Como regla general, se utiliza un esquema RGB, en el que el color se divide en tres componentes (rojo / verde / azul), que, cuando se suman con un fondo negro, dan el color original que percibe una persona.

Como ahora para nosotros todas las obras maestras se han convertido temporalmente en solo conjuntos de números en el disco, trataremos de caracterizar estos conjuntos construyendo histogramas de la distribución de frecuencias de intensidad para cada canal.

Usaremos numpy para los cálculos y visualizaremos usando matplotlib.

Fuente
#      
def load_image_by_index(i):
    image_path = paintings_links.iloc[i].img_path
    img = cv2.imdecode(np.fromfile(str(Path.cwd()/image_path), np.uint8), cv2.IMREAD_UNCHANGED)
    return img    
#    
def get_hist_data_by_index(img_index):
    bin_div = 5 
    img = load_image_by_index(img_index)
    b, bins=  np.histogram(img[:,:,0], bins=255//bin_div, range=(0,255), density=True)
    g = np.histogram(img[:,:,1], bins=255//bin_div, range=(0,255), density=True)[0]
    r = np.histogram(img[:,:,2], bins=255//bin_div, range=(0,255), density=True)[0]
    return bins, r, g, b
#       
def plot_image_with_hist_by_index(img_index, height=6):
    bins, r, g, b = get_hist_data_by_index(img_index)
    img = load_image_by_index(img_index)
    fig = plt.figure(constrained_layout=True)

    if img.shape[0] < img.shape[1]:
        width_ratios = [3,1]
    else:
        width_ratios = [1,1]
        
    gs = GridSpec(3, 2, figure=fig, 
                  width_ratios = [3,1]
                 )
    ax_img = fig.add_subplot(gs[:,0])

    ax_r = fig.add_subplot(gs[0, 1])
    ax_g = fig.add_subplot(gs[1, 1], sharey=ax_r)
    ax_b = fig.add_subplot(gs[2, 1], sharey=ax_r)

    ax_img.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB),aspect = 'equal')
    ax_img.axis('off')
    
    ax_r.bar(bins[:-1], r, width = 5, color='red',alpha=0.7)
    ax_g.bar(bins[:-1], g, width = 5, color='green',alpha=0.7)
    ax_b.bar(bins[:-1], b, width = 5, color='blue',alpha=0.7)

    ax_r.axes.get_xaxis().set_ticks([])
    ax_r.axes.get_yaxis().set_ticks([])
    ax_g.axes.get_xaxis().set_ticks([])
    ax_g.axes.get_yaxis().set_ticks([])
    ax_b.axes.get_xaxis().set_ticks([])
    ax_b.axes.get_yaxis().set_ticks([])
    fig.suptitle("{} - {}".format(paintings_links.iloc[img_index].artist_name, 
                                 paintings_links.iloc[img_index].picture_name),ha= "left")
    
    fig.set_figheight(height)
    plt.axis('tight')
    if img.shape[0] < img.shape[1]:
        fig.set_figwidth(img.shape[1] *height / img.shape[0] *1.25)
    else:
        fig.set_figwidth(img.shape[1] *height / img.shape[0] *1.5)
    plt.show()


Ejemplos de trabajos:









Después de mirar cuidadosamente los histogramas de diferentes imágenes, podemos notar que su forma es muy específica y varía mucho de un trabajo a otro.

En este sentido, suponemos que el histograma es una especie de reparto de la imagen, lo que nos permite caracterizarlo en cierta medida.

Primer modelo


Recopilamos todos los histogramas en un gran conjunto de datos e intentamos buscar algunas "anomalías" en él. Rápido, conveniente y generalmente mi algoritmo favorito para tales propósitos es una clase svm. Usaremos su implementación desde la biblioteca sklearn

Fuente
#       ,       
res = []
error = []
for img_index in tqdm(range(paintings_links.shape[0])):
    try:
        bins, r, g, b = get_hist_data_by_index(img_index)
        res.append(np.hstack([r,g,b]))
    except:
        res.append(np.zeros(153,))
        error.append(img_index)
        
np_res = np.vstack(res)
#    
pd.DataFrame(np_res).to_pickle("histograms.pkl")
histograms = pd.read_pickle("histograms.pkl")
#  .         .   10    
one_class_svm = OneClassSVM(nu=10 / histograms.shape[0], gamma='auto')
one_class_svm.fit(histograms[~histograms.index.isin(bad_images)])
#  
svm_outliers = one_class_svm.predict(histograms)
svm_outliers = np.array([1 if label == -1 else 0 for label in svm_outliers])
#   
uncommon_images = paintings_links[(svm_outliers ==1) & (~histograms.index.isin(bad_images))].index.values
for i in uncommon_images:
    plot_image_with_hist_by_index(i,4)


Veamos qué anómalos encontramos en los contenedores de nuestra galería.

Trabajo realizado a lápiz:


trabajo en colores muy oscuros:

dama en rojo:

algo incompleto:

retrato muy oscuro:


Buscar trabajos similares


Bueno, nuestro modelo encuentra algo inusual, lejos de todo lo demás.

¿Pero podemos hacer una herramienta que ayude a encontrar trabajos similares en color?

Ahora cada imagen se caracteriza por un vector de 153 valores (porque al construir el histograma, alcanza 5 unidades de intensidad por bin, total 255/5 = 51 frecuencias para cada canal).

Podemos determinar el "grado de similitud" calculando las distancias entre los vectores que nos interesan. La distancia euclidiana familiar de la escuela aquí prestará mucha atención a la longitud de los componentes del vector, y nos gustaría prestar más atención a los conjuntos de sombras que componen la imagen. Aquí encontraremos la medida del coseno de la distancia ampliamente utilizada, por ejemplo, en problemas de análisis de texto. Intentemos aplicarlo para esta tarea. Tomamos la implementación de la biblioteca scipy.

Fuente
#  ,        
from scipy import spatial
def find_closest(target_id,n=5):
    distance_vector = np.apply_along_axis(spatial.distance.cosine,
                        arr=histograms,
                       axis=1,
                       v=histograms.values[target_id])
    return np.argsort(distance_vector)[:n]


Veamos qué se parece a la "Novena ola" de Aivazovsky.

Original:



obras similares:





lo que parece ser "Almendras florecientes" de Van Gogh.

Original:



trabajos similares:





¿Y qué parece una mujer anómala encontrada anteriormente en rojo?

Original:



Obras similares:






Espacios de color


Hasta ese momento, trabajamos en el espacio de color RGB. Es muy conveniente para la comprensión, pero lejos de ser ideal para nuestras tareas.

Mire, por ejemplo, al borde del cubo de color RGB



A simple vista, puede ver que hay grandes áreas en las caras donde nuestros ojos no ven cambios, y áreas relativamente pequeñas donde nuestra percepción del color cambia muy bruscamente. Esta percepción no lineal impide que la máquina evalúe los colores como lo haría una persona.

Afortunadamente, hay muchos espacios de color, probablemente algunos se adaptarán a nuestras tareas.

Elegiremos nuestro espacio de color favorito comparando su utilidad para resolver algún problema humano. ¡Por ejemplo, calculemos al artista por el contenido del lienzo!

Tome todos los espacios de color disponibles de la biblioteca opencv, entrene xgboost en cada uno y vea las métricas en la selección diferida.

Fuente
# ,        
def get_hist_data_by_index_and_colorspace(bgr_img, colorspace):
    bin_div = 5
    img_cvt = cv2.cvtColor(bgr_img, getattr(cv2, colorspace))
    c1, bins =  np.histogram(img_cvt[:,:,0], bins=255//bin_div, range=(0,255), density=True)
    c2 = np.histogram(img_cvt[:,:,1], bins=255//bin_div, range=(0,255), density=True)[0]
    c3 = np.histogram(img_cvt[:,:,2], bins=255//bin_div, range=(0,255), density=True)[0]
    return bins, c1, c2, c3
#        
all_res = {}
all_errors = {}
for colorspace in list_of_color_spaces:
    all_res[colorspace] =[]
    all_errors[colorspace] =[]
for img_index in tqdm(range(paintings_links.shape[0]) ):
    
    for colorspace in list_of_color_spaces:
        try:
            bgr_img = load_image_by_index(img_index)
            bins, c1, c2, c3 = get_hist_data_by_index_and_colorspace(bgr_img, colorspace)
            all_res[colorspace].append(np.hstack([c1, c2, c3]))
        except:
            all_res[colorspace].append(np.zeros(153,))
            all_errors[colorspace].append(img_index)
all_res_np = {}
for colorspace in list_of_color_spaces:  
    all_res_np[colorspace] = np.vstack(all_res[colorspace])
res = []
#        
for colorspace in tqdm(list_of_color_spaces):
    temp_df = pd.DataFrame(all_res_np.get(colorspace))
    temp_x_train =   temp_df[temp_df.index.isin(X_train.index.values)]
    temp_x_test =   temp_df[temp_df.index.isin(X_test.index.values)]

    xgb=XGBClassifier()
    xgb.fit(temp_x_train, y_train)
    current_res = classification_report(y_test, xgb.predict(temp_x_test), labels=None, target_names=None, output_dict=True).get("macro avg")
    current_res["colorspace"] = colorspace
    res.append(current_res)
pd.DataFrame(res).sort_values(by="f1-score")


precisiónrecordarpuntaje f1espacio de color
0.0013290.0036630.001059COLOR_BGR2YUV
0.0032290.0046890.001849COLOR_BGR2RGB
0.0030260.0041310.001868COLOR_BGR2HSV
0.0029090.0045780.001934COLOR_BGR2XYZ
0.0035450.0044340.001941COLOR_BGR2HLS
0.0039220.0047840.002098COLOR_BGR2LAB
0.0051180.0048360.002434COLOR_BGR2LUV

El uso del espacio de color LUV dio un aumento tangible en la calidad.

Los creadores de esta escala intentaron hacer que la percepción de los cambios de color a lo largo del eje de la escala sea lo más uniforme posible. Gracias a esto, el cambio de color percibido y su evaluación matemática serán lo más cercanos posible.

Así es como se ve una porción de un espacio de color dado cuando se fija uno de los ejes:



Veamos el modelo.


Después del paso anterior, todavía tenemos un modelo que puede predecir algo.
Veamos de quién es el trabajo que aprendemos con mayor precisión.
precisiónrecordarpuntaje f1Artista
0,0425530,0194170,026667Ilya Efimovich Repin
0,0555560.0200000,029412William Merrit Chase
0,0714290,0222220,033898Bonnard pierre
0,0354610,0352110,035336Jill Elvgren
0.1000000.0217390,035714Jean Auguste Dominic Ingres
0,0228140.2240660,041411Pierre Auguste Renoir
0.1000000,0285710,044444Albert Bierstadt
0.2500000.0322580,057143Hans Zatska
0,0303960.5187970,057428Claude Oscar Monet
0.2500000,0370370,064516Girotto walter

Las métricas en sí están lejos de ser ideales, pero debe recordar que el esquema de color es una pequeña fracción de la información sobre el trabajo. El artista utiliza muchos medios expresivos. El hecho de que encontremos en estos datos una cierta "escritura a mano" del artista ya es una victoria.

Elegiremos uno de los artistas para un análisis más profundo. Que haya Claude Oscar Monet (haré agradable a mi esposa, a ella le gustan los impresionistas).

Tomemos su trabajo, pídale al modelo que nos diga el autor y calcule las frecuencias.
Autor predichoNumero de predicciones
Claude Oscar Monet186
Pierre Auguste Renoir171
Vincent Van Gogh25
Peter Paul Rubensdiecinueve
Gustave Doré17

Muchas personas tienden a confundir a Monet y Manet, y nuestro modelo prefiere confundirlo con Renoir y Van Gogh. Veamos qué, según el modelo, es similar a Van Gogh.










Y ahora usaremos nuestra función de búsqueda para trabajos similares y encontraremos las pinturas de Van Gogh similares a las obras mencionadas anteriormente (esta vez mediremos distancias en el espacio LUV).

Original:



trabajo similar:



original:



trabajos similares:






original:



trabajos similares:





Satisfecho conmigo mismo, mostré los resultados a un amigo y descubrí que el enfoque del histograma es bastante grosero, ya que analiza la distribución no del color en sí, sino de sus componentes por separado. Además, no es tanto la frecuencia de los colores lo importante como su composición. Resultó que los artistas contemporáneos tienen enfoques probados para la elección de esquemas de color. Entonces me enteré de Johannes Itten y su rueda de colores.

Rueda de colores de Itten




Johannes Itten es artista, teórico del arte y profesor, autor de famosos libros sobre forma y color. La rueda de colores es una de las herramientas más conocidas que ayuda a combinar colores para agradar la vista.

Ilustramos los métodos de selección de color más populares:



  1. Colores complementarios: ubicados en partes opuestas del círculo.
  2. Colores adyacentes: adyacentes al círculo.
  3. Tríada clásica: colores en la parte superior de un triángulo equilátero
  4. Tríada de contraste: colores en la parte superior de un triángulo isósceles
  5. Regla de rectángulo: colores en los vértices del rectángulo
  6. Regla del cuadrado: colores en la parte superior del cuadrado

Analizamos como artistas


Tratemos de poner en práctica los conocimientos adquiridos. Para comenzar, obtenemos una variedad de colores en la rueda de colores, reconociéndolos en la imagen.



Ahora podemos comparar en pares cada píxel de nuestras pinturas con una variedad de colores de círculo de Itten. Reemplazamos el píxel original con el más cercano en la rueda de colores y calculamos la frecuencia de los colores en la imagen resultante

Fuente
#          
def plot_composition_analysis(image_index):
    img = load_image_by_index(image_index)
    luv_img = cv2.cvtColor(load_image_by_index(image_index), cv2.COLOR_BGR2LUV)
    closest_colors = np.argmin(euclidean_distances(luv_img.reshape(-1,3),wheel_colors_luv),axis=1)
    wheel_colors2[closest_colors].reshape(luv_img.shape)
    color_areas_img = wheel_colors2[closest_colors].reshape(img.shape)

    v, c = get_image_colors(image_index)
    #         
    c_int = (c*img.shape[1]).astype(int)
    c_int_delta = img.shape[1] - sum(c_int)
    c_int[np.argmax(c_int)] = c_int[np.argmax(c_int)] + c_int_delta

    _ = []

    for i, vi in enumerate(v):
        bar_width = c_int[i]
        _.append(np.tile(wheel_colors2[vi], (150,bar_width,1)))
    color_bar_img = np.hstack(_)
    final_image = np.hstack([
                                np.vstack([img,
                                           np.tile(np.array([254,254,254]),(160,img.shape[1],1))]),

                                np.tile(np.array([254,254,254]),(img.shape[0]+160,10,1)),
                                np.vstack([color_areas_img,
                                           np.tile(np.array([254,254,254]),(10,img.shape[1],1)),
                                           color_bar_img])
                            ])
    h = 12
    w = h / final_image.shape[1] * final_image.shape[0]
    fig = plt.figure(figsize=(h,w))
    plt.imshow(cv2.cvtColor(final_image.astype(np.uint8), cv2.COLOR_BGR2RGB),interpolation='nearest', aspect='auto')
    plt.title("{} - {}".format(paintings_links.iloc[image_index].artist_name, 
                             paintings_links.iloc[image_index].picture_name),ha= "center")
    plt.axis('off');












¿Te has dado cuenta de que "Young Arlesian" ha cambiado poco después de nuestra transformación? Quizás valga la pena medir no solo las frecuencias de los nuevos colores, sino también las estadísticas sobre los errores de conversión; esto puede ayudarnos en el análisis.

Pero esto no es suficiente para el presente análisis. ¿Vamos a buscar combinaciones armoniosas en un círculo?

Todos los pares complementarios:



Todas las tríadas clásicas:



Y todos los cuadrados:



Buscaremos estas combinaciones en nuestras pinturas, encontraremos las más significativas (en frecuencia) y
veremos qué sucede.

Para comenzar el par:











Luego las tríadas:













Y, finalmente, los cuadrados:















No está mal a simple vista, pero ¿las nuevas métricas ayudarán a determinar el autor del trabajo?

Entrenaremos el modelo utilizando solo las frecuencias de color de Itten, las características de error y las combinaciones armoniosas encontradas.

Esta vez, la lista de los artistas más "predecibles" ha cambiado un poco, lo que significa que un enfoque diferente al análisis nos permitió extraer algo más de información del contenido de la imagen.
precisiónrecordarpuntaje f1Artista
0,0434780,0121950,019048Martin, Henri-Jean-Guillaume
0,0326800.0290700,030769Camille Pissarro
0.1666670,0196080,035088Jean-Leon Jerome
0,0769230,0277780,040816Turner, Joseph Mallord William
0.1333330,0243900,041237Poortvliet, Rien
0.1000000,0263160,041667Max Klinger
0,0267250.2282160,047847Pierre Auguste Renoir
0.2000000,0285710.050000Brasilier, Andre
0,0287450.6390980.055016Claude Oscar Monet

Conclusión


Las obras de arte son únicas. Los artistas usan muchas técnicas de composición que nos hacen admirar su trabajo una y otra vez.
El esquema de color es un componente importante, pero lejos de ser el único en el análisis de las obras de los artistas.

Muchos críticos de arte reales se reirán de la ingenuidad del análisis, pero aún así estoy satisfecho con el trabajo realizado. Hay varias ideas más que aún no han llegado a las manos de:

  1. Aplique algoritmos de agrupamiento al análisis de los esquemas de color de los artistas. Seguramente podríamos resaltar grupos interesantes allí, distinguir entre varias tendencias en la pintura.
  2. Aplique algoritmos de agrupamiento a pinturas individuales. Busque "parcelas" que se puedan identificar por combinación de colores. Por ejemplo, en diferentes grupos se obtienen paisajes, retratos y bodegones.
  3. Busque no solo pares, triples y cuadrados, sino también otras combinaciones del círculo de Itten
  4. Pase del análisis de frecuencia al análisis de manchas de color agrupando píxeles por ubicación
  5. Encuentre trabajos en los que la autoría esté en duda y vea por quién votará el modelo

PD


Este artículo fue originalmente un proyecto de graduación para un curso de aprendizaje automático , pero varias personas recomendaron convertirlo en material para Habr.

Espero que te haya interesado.

Todo el código utilizado en el trabajo está disponible en github .

All Articles