日本画と西洋画をAIで判別してみた

はじめまして。
美術鑑賞が趣味なのですが、日頃疑問に思ったことをきっかけに今回作成したアプリ
(https://jepic.herokuapp.com/) をブログにて共有させていただきます。

## 目次
・動機と目的
・方法
・結果
・感想
・今後の課題と展望
・参考資料

##動機と目的
現代アートの場合、日本画なのか西洋画なのか区別がつかないものが増えてきたように感じます。
そもそも、浮世絵や印象派などといった古典的な日本画や西洋画と呼ばれているものをAIは識別
できるのか疑問に思い、今回このようなアプリを制作しました。
美術に興味がない人にも少しでも興味をもってもらえるような、美術に興味がある人にはAI面白いな
と思ってもらえるようなきっかけとなれば幸いです。

##方法

1. 画像収集
2. 画像選別
3. 画像の水増し
4. 画像の判定

####画像収集
メトロポリタン美術館のopenaccessの画像 (https://github.com/metmuseum/openaccess) 
を用いて、日本画と西洋画それぞれの画像収集を行いました。
画像ファイルが大きいため、Git LFS (https://git-lfs.github.com/)
をインストールしていない場合はまず、インストールする必要があります。
Git LFSインストール後、以下のコードでCSVの画像ファイルを取得します。


```python:画像収集1

git lfs clone https://github.com/metmuseum/openaccess.git

```

CSVファイルを取得後、日本画と西洋画の画像収集を行いました。
下のコードは日本画の場合です。


```python:画像収集2

import requests
import urllib
import json
import pandas as pd
import os
import time

# 作品マスターより、条件を指定し美術品を抽出
df_met = pd.read_csv('/content/drive/MyDrive/openaccess/MetObjects.csv')
df_met_pd = df_met[df_met['Is Public Domain']==True]  # パブリックドメインの作品を指定
df_met_pd_japan = df_met_pd[(df_met_pd['Culture']=='Japan')]  # Culture: Japan
df_met_pd_japan_paint = df_met_pd_japan[df_met_pd_japan['Classification']=='Paintings']  # Classification: Paintings

# 対象美術品のIDが格納されたリストを作成
object_id_list = list(df_met_pd_japan_paint['Object ID'])
# ObjectID確認用
# print(object_id_list)

# 画像ダウンロードのための関数を定義
def download_file(url, path):
    try:
        with urllib.request.urlopen(url) as web_file, open(path, 'wb') as local_file:
            local_file.write(web_file.read())
    except urllib.error.URLError as e:
        print(e)

MET_API = 'https://collectionapi.metmuseum.org/public/collection/v1/objects/'
target_image_key = 'primaryImageSmall'  # 小さい方の画像キー

directry = "metropdjp_img/"  # 保存先ディレクトリ

if not os.path.isdir(directry):
    os.makedirs(directry)

for obj_id in object_id_list:
    target_url = MET_API + str(obj_id)
    res = requests.get(target_url)
    res_dict = json.loads(res.text)
    
    url = res_dict[target_image_key]
    file_path = directry + str(obj_id) + ".jpg"
    download_file(url, file_path)
    time.sleep(1)  # サーバに負荷をかけすぎないよう1枚取得するごとに1秒停止

```

####画像選別

日本画の枚数が少なかったので、西洋画の枚数をできるだけ近い枚数になるよう、調整して
います。
なお絵画の中には文字が大部分を占める絵や、彫刻の一部に絵が描かれているものなど、
絵以外のものが大部分を占めているものは除外させていただきました。

####画像の水増し

日本画も西洋画も収集できた枚数が少なかったので、CNNのImageDataGeneratorを用いて
画像の水増しや、回転、反転、色彩を変えたりなどを行いました。
下のコードは日本画の場合です。

```python:画像の水増し
import os
import glob
import numpy as np
from keras.preprocessing.image import ImageDataGenerator, load_img, img_to_array, array_to_img
def draw_images(generator, x, dir_name, index):
    # 出力ファイルの設定
    save_name = 'extened-' + str(index)
    g = generator.flow(x, batch_size=1, save_to_dir=output_dir, save_prefix=save_name, save_format='jpg')
    # 1つの入力画像から何枚拡張するかを指定
    # g.next()の回数分拡張される
    for i in range(10):
        bach = g.next()
if __name__ == '__main__':
    # 出力先ディレクトリの設定
    output_dir = "/content/drive/MyDrive/metropdjpcn_img"
    if not(os.path.exists(output_dir)):
        os.mkdir(output_dir)
    # 拡張する画像群の読み込み
    images = glob.glob(os.path.join('/content/drive/MyDrive/metropdjp2_img', "*.jpg"))#ファイル名を指定
    # 拡張する際の設定
    generator = ImageDataGenerator(
                    rotation_range=90, # 90°まで回転
                    width_shift_range=0.1, # 水平方向にランダムでシフト
                    height_shift_range=0.1, # 垂直方向にランダムでシフト
                    channel_shift_range=50.0, # 色調をランダム変更
                    shear_range=0.39, # 斜め方向(pi/8まで)に引っ張る
                    horizontal_flip=True, # 垂直方向にランダムで反転
                    vertical_flip=True # 水平方向にランダムで反転
                    )
    # 読み込んだ画像を順に拡張
    for i in range(len(images)):
        img = load_img(images[i])
        # 画像を配列化して転置a
        x = img_to_array(img)
        x = np.expand_dims(x, axis=0)
        # 画像の拡張
        draw_images(generator, x, output_dir, i)
```


####画像の判定

VGG16を用いて日本画か西洋画かAIに判定してもらいました。

```python:画像の判定
import os
import cv2
import numpy as np
import matplotlib.pyplot as plt
from tensorflow.keras.utils import to_categorical
from tensorflow.keras.layers import Dense, Dropout, Flatten, Input
from tensorflow.keras.applications.vgg16 import VGG16
from tensorflow.keras.models import Model, Sequential
from tensorflow.keras import optimizers
number = 100

path_japan = os.listdir('/content/drive/MyDrive/metropdjpcn_img/')
path_europe = os.listdir('/content/drive/MyDrive/metropdeucn_img/')

img_japan = []
img_europe = []

for i in range(len(path_japan)):
    img = cv2.imread('/content/drive/MyDrive/metropdjpcn_img/' + path_japan[i])
    img = cv2.resize(img, (50,50))
    img_japan.append(img)

for i in range(len(path_europe)):
    img = cv2.imread('/content/drive/MyDrive/metropdeucn_img/' + path_europe[i])
    img = cv2.resize(img, (50,50))
    img_europe.append(img)
    
X = np.array(img_japan + img_europe)
y =  np.array([0]*len(img_japan) + [1]*len(img_europe))

rand_index = np.random.permutation(np.arange(len(X)))
X = X[rand_index]
y = y[rand_index]

X_train = X[:int(len(X)*0.8)]
y_train = y[:int(len(y)*0.8)]
X_test = X[int(len(X)*0.8):]
y_test = y[int(len(y)*0.8):]

y_train = to_categorical(y_train)
y_test = to_categorical(y_test)

input_tensor = Input(shape=(50, 50, 3))
vgg16 = VGG16(include_top=False, weights='imagenet', input_tensor=input_tensor)

top_model = Sequential()
top_model.add(Flatten(input_shape=vgg16.output_shape[1:]))
top_model.add(Dense(256, activation='relu'))
top_model.add(Dropout(0.5))
top_model.add(Dense(2, activation='softmax'))

model = Model(inputs=vgg16.input, outputs=top_model(vgg16.output))

for layer in model.layers[:15]:
    layer.trainable = False

model.compile(loss='categorical_crossentropy',
              optimizer=optimizers.SGD(lr=1e-4, momentum=0.9),
              metrics=['accuracy'])

model.fit(X_train, y_train, batch_size=100, epochs=10, validation_data=(X_test, y_test))

def pred_picture(img):
    img = cv2.resize(img, (50, 50))
    pred = np.argmax(model.predict(np.array([img])))
    if pred == 0:
        return 'japan'
    else:
        return 'europe'

img = cv2.imread('/content/drive/MyDrive/metropdjpcn_img/' + path_japan[0])
b,g,r = cv2.split(img) 
img = cv2.merge([r,g,b])
plt.imshow(img)
plt.show()
print(pred_picture(img))

pred_picture = os.listdir('/content/drive/MyDrive/metropdjpcn_img/')
for i in range(len(path_japan)):
    img = cv2.imread('/content/drive/MyDrive/metropdjpcn_img/' + path_japan[i])
    plt.imshow(img)
    plt.show()
    print(pred_picture(img))

pred_picture = os.listdir('/content/drive/MyDrive/metropdeucn_img/')
for i in range(len(path_europe)):
    img = cv2.imread('/content/drive/MyDrive/metropdeucn_img/' + path_europe[i])
    plt.imshow(img)
    plt.show()
    print(pred_picture(img))
```


##結果
幅広い年代の作品の絵画を用いたため、精度はあまり高くないかなと予想していたのですが、
予想していたよりもかなり高精度なものとなりました。

```python:結果

58892288/58889256 [==============================] - 0s 0us/step
Epoch 1/10
154/154 [==============================] - 561s 4s/step - loss: 1.5118 - accuracy: 0.8207 - val_loss: 0.1786 - val_accuracy: 0.9235
Epoch 2/10
154/154 [==============================] - 559s 4s/step - loss: 0.1701 - accuracy: 0.9334 - val_loss: 0.1624 - val_accuracy: 0.9522
Epoch 3/10
154/154 [==============================] - 556s 4s/step - loss: 0.1342 - accuracy: 0.9470 - val_loss: 0.1449 - val_accuracy: 0.9569
Epoch 4/10
154/154 [==============================] - 556s 4s/step - loss: 0.1374 - accuracy: 0.9505 - val_loss: 0.1121 - val_accuracy: 0.9627
Epoch 5/10
154/154 [==============================] - 551s 4s/step - loss: 0.0899 - accuracy: 0.9611 - val_loss: 0.1022 - val_accuracy: 0.9640
Epoch 6/10
154/154 [==============================] - 547s 4s/step - loss: 0.0730 - accuracy: 0.9691 - val_loss: 0.1013 - val_accuracy: 0.9671
Epoch 7/10
154/154 [==============================] - 548s 4s/step - loss: 0.0600 - accuracy: 0.9710 - val_loss: 0.0968 - val_accuracy: 0.9694
Epoch 8/10
154/154 [==============================] - 549s 4s/step - loss: 0.0521 - accuracy: 0.9746 - val_loss: 0.1032 - val_accuracy: 0.9723
Epoch 9/10
154/154 [==============================] - 554s 4s/step - loss: 0.0514 - accuracy: 0.9759 - val_loss: 0.0996 - val_accuracy: 0.9697
Epoch 10/10
154/154 [==============================] - 555s 4s/step - loss: 0.0388 - accuracy: 0.9814 - val_loss: 0.1007 - val_accuracy: 0.9705

```


日本画は掛け軸の正解率が低く、それ以外は正解率が高かったです。
西洋画の正解率の高いものも低いものも共通点は見つけられなかったです。

#####日本画の正解率の高かった絵画の例
![45704.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1199331/95363569-8476-ba26-53d5-e9762cbdbbf5.jpeg)

#####日本画の正解率の低かった絵画の例
![49072.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1199331/c25d37da-a1f7-ac75-3122-157a05f1874a.jpeg)



##感想
日本画は掛け軸の割合が高かったので、かえって正解率が高くなるかと思いましたが、
逆の結果だったので驚きました。
西洋画で共通点が見つけられなかったのは残念ですが、今後画像の枚数を増やしたり、
印象派など会派別に分けてみると正解率との因果関係が見えてくるかもしれないと
感じました。


##今後の課題と展望

今後はさらに画像を収集し、より高精度なものにしていきたいです。
日本画は掛け軸の絵の部分だけ抽出したらどうなるのか、精度は変わるのか、挑戦したい
と思います。
制作した後でなんですが、そもそもジャポニスムが流行ってヨーロッパで日本風の作品が
生まれたり、日本の場合は中国から水墨画が伝わってきたりと、文化が混ざり合って今に
至っていると思われるので、現代アートで見分けがつきにくいというのは無理もないこと
なのかもしれません。
今後、現代アートでもどこの誰が作ったかAIが判定してくれるアプリが作れたらと思って
います。
ゆくゆくは描かれた絵が本物かどうかAIが判断するアプリを作りたいです。
その為にはまだまだ技術不足であり知識不足なので、日々精進してまいりたいと思います。

##参考資料
[メトロポリタン美術館のパブリック・ドメイン絵画画像をPythonで収集する](https://qiita.com/dzbt_dzbt/items/10168ee503645e105faf)
[metmuseum]
(https://github.com/metmuseum)