Analisamos as obras-primas da pintura com a ajuda do clássico ML

Olá a todos! Um amigo meu estuda como artista e fala regularmente sobre esta ou aquela obra-prima, sobre técnicas composicionais únicas, sobre percepção de cores, sobre a evolução da pintura e artistas brilhantes. No contexto desse impacto constante, decidi verificar se meus conhecimentos e habilidades de engenharia são adequados para analisar o patrimônio cultural mundial.

Armado com um analisador improvisado sob a cobertura da noite, entrei na galeria on-line e trouxe quase 50 mil pinturas de lá. Vamos ver o que é interessante fazer com isso, usando apenas ferramentas clássicas de ML (cuidado, tráfego).

Transformação ingênua


Como muitos de nós lembramos das lições de ciência da computação, uma imagem é representada como uma matriz de bytes responsáveis ​​pela cor de cada pixel individual. Como regra, é usado um esquema RGB, no qual a cor é dividida em três componentes (vermelho / verde / azul), que, somados com um fundo preto, dão a cor original que é percebida por uma pessoa.

Como agora para nós todas as obras-primas temporariamente se tornaram apenas matrizes de números no disco, tentaremos caracterizá-las construindo histogramas da distribuição de frequências de intensidade para cada canal.

Usaremos numpy para cálculos e visualizaremos usando matplotlib.

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


Exemplos de trabalhos:









Tendo analisado cuidadosamente os histogramas de diferentes figuras, podemos notar que sua forma é muito específica e varia muito de trabalho para trabalho.

Nesse sentido, assumimos que o histograma é uma espécie de elenco da imagem, o que permite que seja caracterizado até certo ponto.

Primeiro modelo


Coletamos todos os histogramas em um grande conjunto de dados e tentamos procurar algumas "anomalias" nele. Rápido, conveniente e geralmente o meu algoritmo favorito para esses fins é uma classe svm. Usaremos sua implementação na biblioteca sklearn

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


Vamos ver o que encontramos de anômalos nas caixas da nossa galeria.

Trabalho realizado a lápis:


Trabalho em cores muito escuras:

Senhora de vermelho:

Algo incompleto:

Retrato muito escuro:


Procurar empregos semelhantes


Bem, nosso modelo encontra algo incomum, longe de tudo o resto.

Mas podemos criar uma ferramenta que ajude a encontrar trabalhos com cores semelhantes?

Agora, cada imagem é caracterizada por um vetor de 153 valores (porque, ao construir o histograma, atingiu 5 unidades de intensidade por compartimento, total 255/5 = 51 frequências para cada canal).

Podemos determinar o "grau de similaridade" calculando as distâncias entre os vetores de interesse para nós. A distância euclidiana familiar da escola aqui prestará muita atenção ao comprimento dos componentes vetoriais, e gostaríamos de prestar mais atenção aos conjuntos de tons que compõem a imagem. Aqui vamos encontrar a medida de distância do cosseno amplamente utilizada, por exemplo, em problemas de análise de texto. Vamos tentar aplicá-lo a esta tarefa. Tomamos a implementação da biblioteca scipy.

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


Vamos ver o que parece ser a "Nona Onda" de Aivazovsky.

Original:



Trabalhos similares:





O que parece as “Amêndoas Floridas” de Van Gogh.

Original:



Trabalhos similares:





E o que parece uma senhora anômala encontrada anteriormente em vermelho?

Original:



Trabalhos similares:






Espaços de cor


Até aquele momento, trabalhamos no espaço de cores RGB. É muito conveniente para a compreensão, mas longe do ideal para nossas tarefas.

Olhe, por exemplo, à beira do cubo de cores RGB A



olho nu, você pode ver que existem grandes áreas nas faces onde nossos olhos não vêem mudanças e áreas relativamente pequenas em que nossa percepção de cores muda muito acentuadamente. Essa percepção não linear impede que a máquina avalie as cores da maneira que uma pessoa faria.

Felizmente, existem muitos espaços de cores, provavelmente alguns se encaixam em nossas tarefas.

Escolheremos nosso espaço de cores favorito comparando sua utilidade na solução de um problema humano. Vamos, por exemplo, calcular o artista pelo conteúdo da tela!

Pegue todos os espaços de cores disponíveis na biblioteca opencv, treine xgboost em cada um e veja as métricas na seleção adiada.

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


precisãorecordarpontuação f1espaço colorido
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

Um aumento tangível na qualidade foi dado pelo uso do espaço de cores LUV.

Os criadores dessa escala tentaram tornar a percepção das mudanças de cor ao longo do eixo da escala o mais uniforme possível. Graças a isso, a mudança de cor percebida e sua avaliação matemática serão o mais próximo possível.

É assim que uma fatia de um determinado espaço de cor se parece ao fixar um dos eixos:



Vamos olhar para o modelo


Após a etapa anterior, ainda temos um modelo que pode prever algo.
Vamos ver cujo trabalho aprendemos com mais precisão.
precisãorecordarpontuação 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

As métricas estão longe de serem ideais, mas é preciso lembrar que o esquema de cores é uma pequena fração das informações sobre o trabalho. O artista usa muitos meios expressivos. O fato de termos encontrado nesses dados uma certa “caligrafia” do artista já é uma vitória.

Vamos escolher um dos artistas para uma análise mais profunda. Que seja Claude Oscar Monet (farei minha esposa legal, ela gosta dos impressionistas).

Vamos pegar o trabalho dele, pedir ao modelo para nos dizer o autor e calcular as frequências
Autor previstoNúmero de previsões
Claude Oscar Monet186
Pierre Auguste Renoir171
Vincent Van Gogh25
Peter Paul Rubensdezenove
Gustave Dore17

Muitas pessoas tendem a confundir Monet e Manet, e nosso modelo prefere confundi-lo com Renoir e Van Gogh. Vamos ver o que, de acordo com o modelo, é semelhante a Van Gogh.










E agora usaremos nossa função de pesquisa para trabalhos semelhantes e encontraremos as pinturas de Van Gogh semelhantes às obras mencionadas acima (desta vez, mediremos distâncias no espaço LUV).

Original:



Trabalho semelhante:



Original:



Trabalhos semelhantes:






Original:



Trabalhos semelhantes:





Satisfeito comigo, mostrei os resultados a um amigo e descobri que a abordagem do histograma é realmente bastante rude, pois não analisa a distribuição da cor em si, mas de seus componentes separadamente. Além disso, não é tanto a frequência das cores que é importante como a sua composição. Os artistas contemporâneos revelaram abordagens comprovadas para a escolha de esquemas de cores. Então eu descobri sobre Johannes Itten e sua roda de cores.

Roda de cores de Itten




Johannes Itten é um artista, teórico da arte e professor, autor de livros famosos sobre formas e cores. A roda de cores é uma das ferramentas mais conhecidas que ajuda a combinar cores para agradar aos olhos.

Ilustramos os métodos de seleção de cores mais populares:



  1. Cores complementares - localizadas em partes opostas do círculo
  2. Cores adjacentes - adjacentes ao círculo
  3. Tríade Clássica - Cores no Topo de um Triângulo Equilateral
  4. Tríade de contraste - cores no topo de um triângulo isósceles
  5. Regra do retângulo - cores nos vértices do retângulo
  6. Regra do quadrado - cores na parte superior do quadrado

Analisamos como artistas


Vamos tentar colocar em prática o conhecimento adquirido. Para começar, temos uma variedade de cores na roda de cores, reconhecendo-as na imagem.



Agora podemos comparar em pares cada pixel de nossas pinturas com uma variedade de cores do círculo Itten. Substituímos o pixel original pelo pixel mais próximo da roda de cores e calculamos a frequência das cores na imagem resultante

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












Você já reparou como o "jovem arlesiano" mudou pouco depois da nossa transformação? Talvez valha a pena medir não apenas as frequências de novas cores, mas também estatísticas sobre erros de conversão - isso pode nos ajudar na análise.

Mas isso não é suficiente para a presente análise. Vamos procurar combinações harmoniosas em um círculo?

Todos os pares complementares:



Todas as tríades clássicas:



E todos os quadrados:



procuraremos essas combinações em nossas pinturas, encontraremos as mais significativas (em frequência) e
veremos o que acontece.

Para iniciar o par:











Depois as tríades:













E, finalmente, os quadrados:















Nada mal a olho, mas as novas métricas ajudarão a determinar o autor do trabalho?

Treinaremos o modelo usando apenas as frequências de cores de Itten, características de erro e combinações harmoniosas encontradas.

Desta vez, a lista dos artistas mais “previsíveis” mudou um pouco, o que significa que uma abordagem diferente da análise nos permitiu extrair mais informações do conteúdo da imagem.
precisãorecordarpontuação f1Artista
0,0434780,0121950,019048Martin, Henri-Jean-Guillaume
0,0326800,0290700,030769Camille Pissarro
0,1666670,0196080,035088Jean-Leon Jerome
0,0769230,0277780,040816Joseph Turner Mallord William
0,1333330,0243900,041237Poortvliet, Rien
0.1000000,0263160,041667Max Klinger
0,0267250,22282160,047847Pierre Auguste Renoir
0.2000000,0285710,050000Andre Brasilier
0,0287450,6390980,055016Claude Oscar Monet

Conclusão


Obras de arte são únicas. Os artistas usam muitas técnicas de composição que nos fazem admirar seu trabalho repetidamente.
O esquema de cores é importante, mas longe de ser o único componente na análise das obras dos artistas.

Muitos críticos de arte de verdade rirão da ingenuidade da análise, mas ainda assim fiquei satisfeito com o trabalho realizado. Existem várias outras idéias que ainda não chegaram às mãos de:

  1. Aplique algoritmos de agrupamento à análise dos esquemas de cores dos artistas. Certamente poderíamos destacar grupos interessantes por lá, distinguir entre várias tendências da pintura
  2. Aplique algoritmos de agrupamento a pinturas individuais. Procure por "plotagens" que possam ser identificadas pelo esquema de cores. Por exemplo, em diferentes grupos, obtém paisagens, retratos e naturezas-mortas
  3. Pesquise não apenas pares, triplos e quadrados, mas também outras combinações do círculo de Itten
  4. Passe da análise de frequência para a análise de pontos coloridos agrupando pixels por local
  5. Encontre trabalhos em que a autoria esteja em dúvida e veja em quem o modelo votará

PS


Este artigo foi originalmente um projeto de graduação para um curso de aprendizado de máquina , mas várias pessoas recomendaram transformá-lo em material para a Habr.

Espero que você esteja interessado.

Todo o código usado no trabalho está disponível no github .

All Articles