Nous analysons les chefs-d'œuvre de la peinture à l'aide du ML classique

Bonjour à tous! Un de mes amis étudie en tant qu'artiste et parle régulièrement de tel ou tel chef-d'œuvre, de techniques de composition uniques, de la perception des couleurs, de l'évolution de la peinture et d'artistes brillants. Dans le contexte de cet impact constant, j'ai décidé de vérifier si mes connaissances et compétences en ingénierie sont adaptées à l'analyse du patrimoine culturel mondial.

Armé d'un analyseur fait maison sous le couvert de la nuit, j'ai fait irruption dans la galerie en ligne et j'en ai sorti près de 50 000 tableaux. Voyons ce qui est intéressant à faire avec cela, en utilisant uniquement des outils ML classiques (prudence, trafic).

Transformation naïve


Comme beaucoup d'entre nous se souviennent des leçons d'informatique, une image est représentée comme un tableau d'octets qui sont responsables de la couleur de chaque pixel individuel. En règle générale, un schéma RVB est utilisé, dans lequel la couleur est divisée en trois composants (rouge / vert / bleu), qui, additionnés sur un fond noir, donnent la couleur d'origine perçue par une personne.

Puisque maintenant pour nous tous les chefs-d'œuvre ne sont devenus temporairement que des tableaux de nombres sur le disque, nous allons essayer de caractériser ces tableaux en construisant des histogrammes de la distribution des fréquences d'intensité pour chaque canal.

Nous utiliserons numpy pour les calculs et visualiserons en utilisant matplotlib.

La source
#      
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()


Exemples d'œuvres:









Après avoir soigneusement regardé les histogrammes de différentes images, nous pouvons remarquer que leur forme est très spécifique et varie considérablement d'un travail à l'autre.

À cet égard, nous faisons l'hypothèse que l'histogramme est une sorte de distribution de l'image, ce qui nous permet de le caractériser dans une certaine mesure.

Premier modèle


Nous collectons tous les histogrammes dans un grand ensemble de données et essayons d'y rechercher des «anomalies». Rapide, pratique et généralement mon algorithme préféré à ces fins est un svm de classe. Nous utiliserons son implémentation à partir de la bibliothèque sklearn

La source
#       ,       
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)


Voyons ce que nous trouvons anormal dans les bacs de notre galerie.

Travail réalisé au crayon:


Travail dans des couleurs très sombres:

Dame en rouge:

Quelque chose de sommaire:

Portrait très sombre:


Rechercher des emplois similaires


Eh bien, notre modèle trouve quelque chose d'inhabituel, loin de tout le reste.

Mais pouvons-nous faire un outil qui aidera à trouver un travail de couleur similaire?

Maintenant, chaque image est caractérisée par un vecteur de 153 valeurs (car lors de la construction de l'histogramme, elle a atteint 5 unités d'intensité par bin, 255/5 = 51 fréquences au total pour chaque canal).

Nous pouvons déterminer le «degré de similitude» en calculant les distances entre les vecteurs qui nous intéressent. La distance euclidienne familière de l'école ici accordera une grande attention à la longueur des composantes vectorielles, et nous aimerions accorder plus d'attention aux ensembles de nuances qui composent l'image. Nous trouverons ici la mesure du cosinus de la distance largement utilisée, par exemple, dans les problèmes d'analyse de texte. Essayons de l'appliquer pour cette tâche. Nous prenons l'implémentation de la bibliothèque scipy.

La source
#  ,        
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]


Voyons à quoi ressemble la «neuvième vague» d'Aivazovsky.

Original:



Œuvres similaires:





Ce qui ressemble aux «Amandes fleuries» de Van Gogh.

Original:



Oeuvres similaires:





Et à quoi ressemble une dame anormale trouvée plus tôt en rouge?

Original:



Oeuvres similaires:






Espaces colorimétriques


Jusqu'à ce moment, nous travaillions dans l'espace colorimétrique RVB. C'est très pratique pour la compréhension, mais loin d'être idéal pour nos tâches.

Regardez, par exemple, au bord du cube de couleur RVB



À l'œil nu, vous pouvez voir qu'il y a de grandes zones sur les visages où nos yeux ne voient pas de changements, et des zones relativement petites où notre perception des couleurs change très fortement. Cette perception non linéaire empêche la machine d'évaluer les couleurs comme le ferait une personne.

Heureusement, il existe de nombreux espaces colorimétriques, dont certains conviendront probablement à nos tâches.

Nous choisirons notre espace colorimétrique préféré en comparant son utilité pour résoudre un problème humain. Calculons par exemple l'artiste par le contenu de la toile!

Prenez tous les espaces colorimétriques disponibles de la bibliothèque opencv, entraînez xgboost sur chacun et voyez les métriques sur la sélection différée.

La source
# ,        
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")


précisionrappelscore f1espace colorimétrique
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

Une augmentation tangible de la qualité a été donnée par l'utilisation de l'espace colorimétrique LUV.

Les créateurs de cette échelle ont essayé de rendre la perception des changements de couleur le long de l'axe de l'échelle aussi uniforme que possible. Grâce à cela, le changement de couleur perçu et son évaluation mathématique seront aussi proches que possible.

Voici à quoi ressemble une tranche d'un espace colorimétrique donné lors de la fixation de l'un des axes:



Regardons le modèle


Après l'étape précédente, nous avons toujours un modèle qui peut prédire quelque chose.
Voyons quel travail nous apprenons le plus précisément.
précisionrappelscore f1Artiste
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

Les mesures elles-mêmes sont loin d'être idéales, mais vous devez vous rappeler que le jeu de couleurs n'est qu'une petite fraction des informations sur le travail. L'artiste utilise de nombreux moyens expressifs. Le fait que l'on retrouve dans ces données une certaine «écriture» de l'artiste est déjà une victoire.

Nous choisirons l'un des artistes pour une analyse plus approfondie. Que ce soit Claude Oscar Monet (je vais rendre ma femme gentille, elle aime les impressionnistes).

Prenons son travail, demandons au modèle de nous dire l'auteur et calculons les fréquences
Auteur préditNombre de prédictions
Claude Oscar Monet186
Pierre Auguste Renoir171
Vincent Van Gogh25
Peter Paul Rubensdix-neuf
Gustave Dore17

Beaucoup de gens ont tendance à confondre Monet et Manet, et notre modèle préfère le confondre avec Renoir et Van Gogh. Voyons ce qui, selon le modèle, est similaire à Van Gogh.










Et maintenant, nous allons utiliser notre fonction de recherche pour des œuvres similaires et trouver des peintures de Van Gogh similaires aux œuvres mentionnées ci-dessus (cette fois, nous mesurerons les distances dans l'espace LUV).

Original:



Oeuvre similaire:



Original:



Oeuvres similaires:






Original:



Oeuvres similaires:





Satisfait de moi-même, j'ai montré les résultats à un ami et j'ai découvert que l'approche de l'histogramme est en fait assez grossière, car elle analyse la distribution non pas de la couleur elle-même, mais de ses composants séparément. De plus, ce n'est pas tant la fréquence des couleurs qui importe que leur composition. Il s'est avéré que les artistes contemporains ont fait leurs preuves dans le choix des schémas de couleurs. J'ai donc découvert Johannes Itten et sa roue chromatique.

Roue chromatique d'Itten




Johannes Itten est un artiste, théoricien de l'art et professeur, auteur de livres célèbres sur la forme et la couleur. La roue chromatique est l'un des outils les plus connus qui aide à combiner les couleurs pour plaire à l'œil.

Nous illustrons les méthodes de sélection des couleurs les plus populaires:



  1. Couleurs complémentaires - situées sur les parties opposées du cercle
  2. Couleurs adjacentes - adjacentes au cercle
  3. Triade classique - Des couleurs au sommet d'un triangle équilatéral
  4. Triade de contraste - Couleurs au sommet d'un triangle isocèle
  5. Règle de rectangle - Couleurs aux sommets du rectangle
  6. Règle du carré - couleurs sur les sommets du carré

Nous analysons en tant qu'artistes


Essayons de mettre en pratique les connaissances acquises. Pour commencer, nous obtenons un tableau de couleurs se trouvant sur la roue chromatique, les reconnaissant de l'image.



Maintenant, nous pouvons comparer par paires chaque pixel de nos peintures avec un tableau de couleurs de cercle Itten. Nous remplaçons le pixel d'origine par le plus proche dans la roue chromatique et calculons la fréquence des couleurs dans l'image résultante

La source
#          
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');












Avez-vous remarqué que le «jeune arlésien» a peu changé après notre transformation? Il vaut peut-être la peine de mesurer non seulement les fréquences des nouvelles couleurs, mais aussi les statistiques sur les erreurs de conversion - cela peut nous aider dans l'analyse.

Mais cela ne suffit pas pour la présente analyse. Cherchons des combinaisons harmonieuses dans un cercle?

Toutes les paires complémentaires:



Toutes les triades classiques:



Et tous les carrés:



Nous chercherons ces combinaisons dans nos tableaux, trouverons les plus significatives (en fréquence) et
verrons ce qui se passe.

Pour commencer la paire:











Puis les triades:













Et, enfin, les carrés:















Pas mal à l'œil nu, mais les nouvelles métriques aideront-elles à déterminer l'auteur de l'ouvrage?

Nous formerons le modèle en utilisant uniquement les fréquences de couleur d'Itten, les caractéristiques d'erreur et les combinaisons harmonieuses trouvées.

Cette fois, la liste des artistes les plus «prévisibles» a un peu changé, ce qui signifie qu'une approche différente de l'analyse nous a permis d'extraire un peu plus d'informations du contenu de l'image.
précisionrappelscore f1Artiste
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

Conclusion


Les oeuvres d'art sont uniques. Les artistes utilisent de nombreuses techniques de composition qui nous font admirer leur travail encore et encore.
La palette de couleurs est un élément important, mais loin d'être le seul dans l'analyse des œuvres des artistes.

Beaucoup de vrais critiques d'art se moqueront de la naïveté de l'analyse, mais j'étais toujours satisfait du travail accompli. Il y a plusieurs autres idées qui ne sont pas encore parvenues aux mains de:

  1. Appliquer des algorithmes de clustering à l'analyse des schémas de couleurs des artistes. Nous pourrions sûrement y mettre en évidence des groupes intéressants, distinguer les différentes tendances de la peinture
  2. Appliquez des algorithmes de clustering à des peintures individuelles. Recherchez des «tracés» qui peuvent être identifiés par un jeu de couleurs. Par exemple, dans différents groupes, obtenez des paysages, des portraits et des natures mortes
  3. Recherchez non seulement des paires, des triplets et des carrés, mais aussi d'autres combinaisons dans le cercle d'Itten
  4. Passez de l'analyse de fréquence à l'analyse des taches de couleur en regroupant les pixels par emplacement
  5. Trouvez des œuvres dont la paternité est mise en doute et voyez pour qui le modèle votera

PS


Cet article était à l'origine un projet de fin d'études pour un cours d'apprentissage automatique , mais plusieurs personnes ont recommandé de le transformer en matériel pour Habr.

J'espère que vous étiez intéressé.

Tout le code utilisé dans le travail est disponible sur github .

All Articles