Wir analysieren die Meisterwerke der Malerei mit Hilfe der klassischen ML

Hallo alle zusammen! Ein Freund von mir studiert als Künstler und spricht regelmäßig über dieses oder jenes Meisterwerk, über einzigartige Kompositionstechniken, über Farbwahrnehmung, über die Entwicklung der Malerei und über brillante Künstler. Vor dem Hintergrund dieser ständigen Auswirkungen habe ich mich entschlossen zu prüfen, ob meine technischen Kenntnisse und Fähigkeiten für die Analyse des Weltkulturerbes geeignet sind.

Mit einem provisorischen Parser im Schutz der Nacht bewaffnet, stürmte ich in die Online-Galerie und brachte von dort fast 50.000 Gemälde heraus. Mal sehen, was damit interessant ist, wenn nur klassische ML-Tools verwendet werden (Vorsicht, Verkehr).

Naive Transformation


Wie sich viele von uns aus dem Informatikunterricht erinnern, wird ein Bild als eine Reihe von Bytes dargestellt, die für die Farbe jedes einzelnen Pixels verantwortlich sind. In der Regel wird ein RGB-Schema verwendet, bei dem die Farbe in drei Komponenten (rot / grün / blau) unterteilt ist, die zusammen mit einem schwarzen Hintergrund die ursprüngliche Farbe ergeben, die von einer Person wahrgenommen wird.

Da für uns alle Meisterwerke vorübergehend nur zu Arrays von Zahlen auf der Platte geworden sind, werden wir versuchen, diese Arrays zu charakterisieren, indem wir Histogramme der Verteilung der Intensitätsfrequenzen für jeden Kanal erstellen.

Wir werden numpy für Berechnungen verwenden und mit matplotlib visualisieren.

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


Beispiele für Arbeiten:









Wenn wir uns die Histogramme verschiedener Bilder genau ansehen, können wir feststellen, dass ihre Form sehr spezifisch ist und von Arbeit zu Arbeit sehr unterschiedlich ist.

In diesem Zusammenhang gehen wir davon aus, dass das Histogramm eine Art Abguss des Bildes ist, der eine gewisse Charakterisierung ermöglicht.

Erstes Modell


Wir sammeln alle Histogramme in einem großen Datensatz und versuchen, darin nach „Anomalien“ zu suchen. Schnell, bequem und im Allgemeinen ist mein Lieblingsalgorithmus für solche Zwecke eine Klasse svm. Wir werden seine Implementierung aus der sklearn-Bibliothek verwenden

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


Mal sehen, was für Anomalien wir in den Behältern unserer Galerie finden.

Arbeit mit Bleistift erledigt:


Arbeit in sehr dunklen Farben:

Dame in Rot:

Etwas skizzenhaftes:

Sehr dunkles Porträt:


Suche nach ähnlichen Jobs


Nun, unser Modell findet etwas Ungewöhnliches, weit entfernt von allem anderen.

Aber können wir ein Werkzeug entwickeln, das hilft, farbähnliche Arbeiten zu finden?

Jetzt ist jedes Bild durch einen Vektor von 153 Werten gekennzeichnet (da beim Erstellen des Histogramms 5 Intensitätseinheiten pro Bin erreicht wurden, insgesamt 255/5 = 51 Frequenzen für jeden Kanal).

Wir können den „Ähnlichkeitsgrad“ bestimmen, indem wir die Abstände zwischen den für uns interessanten Vektoren berechnen. Die hier bekannte euklidische Entfernung von der Schule wird der Länge der Vektorkomponenten viel Aufmerksamkeit schenken, und wir möchten den Farbsätzen, aus denen das Bild besteht, mehr Aufmerksamkeit schenken. Hier finden wir das Kosinusmaß für die Entfernung, das beispielsweise bei Textanalyseproblemen weit verbreitet ist. Versuchen wir, es für diese Aufgabe anzuwenden. Wir übernehmen die Implementierung aus der Scipy-Bibliothek.

Quelle
#  ,        
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]


Mal sehen, wie Aivazovskys "Neunte Welle" aussieht.

Original:



Ähnliche Werke:





Wie Van Goghs „Blühende Mandeln“ aussehen.

Original:



Ähnliche Werke:





Und wie sieht eine anomale Dame aus, die früher in Rot gefunden wurde?

Original:



Ähnliche Werke:






Farbräume


Bis zu diesem Moment haben wir im RGB-Farbraum gearbeitet. Es ist sehr praktisch für das Verständnis, aber alles andere als ideal für unsere Aufgaben.

Schauen Sie zum Beispiel an den Rand des RGB-Farbwürfels.



Mit bloßem Auge können Sie sehen, dass es große Bereiche auf den Gesichtern gibt, in denen unsere Augen keine Veränderungen sehen, und relativ kleine Bereiche, in denen sich unsere Farbwahrnehmung sehr stark ändert. Diese nichtlineare Wahrnehmung verhindert, dass die Maschine Farben so bewertet, wie es eine Person tun würde.

Glücklicherweise gibt es viele Farbräume, wahrscheinlich passen einige zu unseren Aufgaben.

Wir werden unseren bevorzugten Farbraum auswählen, indem wir seine Nützlichkeit bei der Lösung eines menschlichen Problems vergleichen. Berechnen wir zum Beispiel den Künstler anhand des Inhalts der Leinwand!

Nehmen Sie alle verfügbaren Farbräume aus der opencv-Bibliothek, trainieren Sie jeweils xgboost und sehen Sie sich die Metriken für die verzögerte Auswahl an.

Quelle
# ,        
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äzisionerinnernF1-ScoreFarbraum
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

Eine spürbare Qualitätssteigerung wurde durch die Verwendung des LUV-Farbraums erzielt.

Die Macher dieser Skala haben versucht, die Wahrnehmung von Farbveränderungen entlang der Skala so einheitlich wie möglich zu gestalten. Dank dessen werden die wahrgenommene Farbänderung und ihre mathematische Bewertung so nah wie möglich sein.

So sieht ein Schnitt eines bestimmten Farbraums aus, wenn eine der Achsen fixiert wird:



Schauen wir uns das Modell an


Nach dem vorherigen Schritt haben wir noch ein Modell, das etwas vorhersagen kann.
Mal sehen, wessen Arbeit wir am genauesten lernen.
PräzisionerinnernF1-ScoreKünstler
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

Die Metriken selbst sind alles andere als ideal, aber Sie müssen bedenken, dass das Farbschema nur einen kleinen Teil der Informationen über die Arbeit ausmacht. Der Künstler verwendet viele Ausdrucksmittel. Die Tatsache, dass wir in diesen Daten eine bestimmte „Handschrift“ des Künstlers gefunden haben, ist bereits ein Sieg.

Wir werden einen der Künstler für eine tiefere Analyse auswählen. Lass es Claude Oscar Monet sein (ich werde meine Frau nett machen, sie mag die Impressionisten).

Nehmen wir seine Arbeit, bitten Sie das Modell, uns den Autor mitzuteilen und die Frequenzen zu berechnen
Voraussichtlicher AutorAnzahl der Vorhersagen
Claude Oscar Monet186
Pierre Auguste Renoir171
Vincent van Gogh25
Peter Paul Rubensneunzehn
Gustave Dore17

Viele Menschen neigen dazu, Monet und Manet zu verwechseln, und unser Modell verwechselt ihn lieber mit Renoir und Van Gogh. Mal sehen, was laut Modell Van Gogh ähnlich ist.










Und jetzt verwenden wir unsere Suchfunktion für ähnliche Werke und finden Van Goghs Gemälde ähnlich den oben genannten Werken (diesmal messen wir Entfernungen im LUV-Raum).

Original:



Ähnliche Arbeit:



Original:



Ähnliche Werke:






Original:



Ähnliche Werke:





Zufrieden mit mir selbst zeigte ich die Ergebnisse einem Freund und fand heraus, dass der Histogrammansatz tatsächlich ziemlich unhöflich ist, da er nicht die Verteilung der Farbe selbst, sondern ihrer Komponenten separat analysiert. Darüber hinaus ist weniger die Häufigkeit der Farben als vielmehr ihre Zusammensetzung wichtig. Es stellte sich heraus, dass zeitgenössische Künstler Ansätze zur Auswahl von Farbschemata bewiesen haben. Also habe ich von Johannes Itten und seinem Farbkreis erfahren.

Ittens Farbkreis




Johannes Itten ist Künstler, Kunsttheoretiker und Lehrer und Autor berühmter Bücher über Form und Farbe. Das Farbrad ist eines der bekanntesten Werkzeuge, mit denen Farben kombiniert werden können, um das Auge zu erfreuen.

Wir veranschaulichen die beliebtesten Farbauswahlmethoden:



  1. Komplementärfarben - befinden sich an gegenüberliegenden Teilen des Kreises
  2. Benachbarte Farben - neben dem Kreis
  3. Klassische Triade - Farben an der Spitze eines gleichseitigen Dreiecks
  4. Kontrast-Triade - Farben an der Spitze eines gleichschenkligen Dreiecks
  5. Rechteckregel - Farben an den Eckpunkten des Rechtecks
  6. Regel des Quadrats - Farben auf den Oberseiten des Quadrats

Wir analysieren als Künstler


Versuchen wir, das erworbene Wissen in die Praxis umzusetzen. Zu Beginn erhalten wir eine Reihe von Farben, die auf dem Farbkreis liegen und diese anhand des Bildes erkennen.



Jetzt können wir jedes Pixel unserer Bilder paarweise mit einer Reihe von Itten-Kreisfarben vergleichen. Wir ersetzen das ursprüngliche Pixel durch das nächste im Farbkreis und berechnen die Häufigkeit der Farben im resultierenden Bild

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












Haben Sie bemerkt, wie sich „Young Arlesian“ nach unserer Transformation kaum verändert hat? Vielleicht lohnt es sich, nicht nur die Häufigkeit neuer Farben zu messen, sondern auch Statistiken zu Konvertierungsfehlern - dies kann uns bei der Analyse helfen.

Dies reicht jedoch für die vorliegende Analyse nicht aus. Suchen wir nach harmonischen Kombinationen in einem Kreis?

Alle komplementären Paare:



Alle klassischen Triaden:



Und alle Quadrate:



Wir werden diese Kombinationen in unseren Gemälden suchen, die signifikantesten (in der Häufigkeit) finden und
sehen, was passiert.

Um das Paar zu starten:











Dann die Triaden:













Und schließlich die Quadrate:















Nicht schlecht für das Auge, aber helfen die neuen Metriken dabei, den Autor der Arbeit zu bestimmen?

Wir werden das Modell nur mit den Farbfrequenzen, Fehlereigenschaften und harmonischen Kombinationen von Itten trainieren.

Diesmal hat sich die Liste der „vorhersehbarsten“ Künstler etwas geändert, was bedeutet, dass wir aufgrund eines anderen Analyseansatzes weitere Informationen aus dem Bildinhalt extrahieren konnten.
PräzisionerinnernF1-ScoreKünstler
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

Fazit


Kunstwerke sind einzigartig. Künstler verwenden viele Kompositionstechniken, die uns dazu bringen, ihre Arbeit immer wieder zu bewundern.
Das Farbschema ist eine wichtige, aber bei weitem nicht die einzige Komponente bei der Analyse von Künstlerwerken.

Viele echte Kunstkritiker werden über die Naivität der Analyse lachen, aber ich war trotzdem zufrieden mit der geleisteten Arbeit. Es gibt mehrere weitere Ideen, die noch nicht in die Hände von:

  1. Wenden Sie Clustering-Algorithmen auf die Analyse der Farbschemata von Künstlern an. Sicherlich könnten wir dort interessante Gruppen hervorheben und zwischen verschiedenen Trends in der Malerei unterscheiden
  2. Wenden Sie Clustering-Algorithmen auf einzelne Bilder an. Suchen Sie nach „Plots“, die anhand des Farbschemas identifiziert werden können. Zum Beispiel erhalten in verschiedenen Clustern Landschaften, Porträts und Stillleben
  3. Suchen Sie nicht nur nach Paaren, Tripeln und Quadraten, sondern auch nach anderen Kombinationen aus Ittens Kreis
  4. Wechseln Sie von der Frequenzanalyse zur Farbfleckanalyse, indem Sie die Pixel nach Position gruppieren
  5. Finden Sie Werke, bei denen die Urheberschaft zweifelhaft ist, und sehen Sie, für wen das Modell stimmen wird

PS


Dieser Artikel war ursprünglich ein Abschlussprojekt für einen Kurs zum maschinellen Lernen , aber mehrere Leute empfahlen, ihn in Material für Habr umzuwandeln.

Ich hoffe du warst interessiert.

Der gesamte in der Arbeit verwendete Code ist auf github verfügbar .

All Articles