はじめに
こんにちは。以前から画像認識に興味があり,漠然とした知識はあったのですがまだコードを書いたことはありませんでした。今回は初心者の方に向けての記事を書きたいと思います。よろしくお願いします。
手順
- 画像のスクレイピング
- 同じ画像を削除
- 画像の名前を連番にする
- 顔の部分のみ切り取る
- 教師データとテストデータを分ける
- 教師データを水増しする
- Google Colaboratoryを用いて学習を行う
1.画像のスクレイピング
まずメンバー画像を集めます。Bingやヤフーなどの画像検索サービスがありますが,この記事ではGoogle画像検索の結果をスクレイピングすることで画像を集めます。
こちらからオリジナルのコードを手に入れることができます。ただしオリジナルのコードではダウンロードができません(2020年4月現在)。この問題に関してはPull requestsとしてパッチが出ていますので今回はそちらのコードを利用していきます。
google_images_download.pyの使い方例
cd google_images_download/
python google_images_download.py -k "齋藤飛鳥"
python google_images_download.py -k "site:twitter.com 齋藤飛鳥"
このプログラムでは1度に100枚までしかダウンロードできません。より多くの画像を集めたい場合は検索ワード変更することで対応してください。また”site:url”という検索ワードを使うとurlに該当する画像のみダウンロードできます。
参考記事
google image downloadが動かなかったのでその対応
2. 同じ画像を削除
次に1.で集めた画像から全く同じ画像データを削除します。アルゴリズムを以下の図にまとめました。
1.では8bit×3(RGB)ベクトルを1bit(白黒)に変換します。このときフロイド-スタインバーグ・ディザリングというアルゴリズムを用いてモノクロ変換を行っています。(a, b, c))。
モノクロ変換のコードを以下に示します。こちらのコードを参考にしました。
def img2vec(filename):
img = Image.open(filename)
img = img.resize((200, 200), resample=Image.BILINEAR) #縮小
img = img.convert('1') #2値化
#img.save(get_mono_filename(filename)) # 画像確認したい場合
img = np.array(img)
#int型の行列に変換する
int_img = img.astype(np.int)
return int_img
次にノルムを計算します。
def calnorm(vec):
norm = np.linalg.norm(vec)
return norm
ノルムを降べきの順に並べます。sort関数を用いて任意の列の値に関して,順序並び替えを行います。
def fig_norm_list_cal(list_fig):
#各々のfigとnormをまとめたlistを作る
#例
# fig_norm_list = [[fig, norm], [fig.A, 2], [fig.B, 3],...[figZ, 5]]
fig_norm_list = fig_norm_mat(list_fig)
#normの大きさを降下の順に並べる
#http://sota1235.com/blog/2015/04/23/python_sort_twodime.html
fig_norm_list = sorted(fig_norm_list, key = lambda x: x[1])
#nd.array型に変換
fig_norm_list = np.array(fig_norm_list)
return fig_norm_list
ノルムの値を1つずつ調べて,同じ値のものは削除します。
def del_fig_list_cal(fig_norm_list):
#消すべきファイルをリストアップする
del_fig_list = []
#1番下の行はカウントしない
for i in range(fig_norm_list.shape[0] - 1):
if (fig_norm_list[i, 1] == fig_norm_list[i+1, 1]):
del_fig_list.append(str(fig_norm_list[i, 0]))
return del_fig_list
上で算出したdel_fig_listとos.remove()を用いてファイルを削除します。
3. 画像の名前を連番にする
このコードでは拡張子判定も同時に行っています。
#画像の名前を連番に変える
def fig_rename():
i = 1
j = 1
k = 1
list_fig = listfig()
for pre_filename in list_fig:
if ((".jpg" in pre_filename) == True ):
os.rename(pre_filename, path + "/fig_data/" + str(i) + '.jpg')
i += 1
elif ((".png" in pre_filename) == True):
os.rename(pre_filename, path + "/fig_data/" + str(j) + '.png')
j += 1
elif ((".jpeg" in pre_filename) == True):
os.rename(pre_filename, path + "/fig_data/" + str(k) + '.jpeg')
k += 1
else:
pass
return
参考記事
4. 顔の部分のみ切り取る
画像は顔以外のものが含まれていたり,顔が複数含まれていたりします。これを学習に使えるような顔のみの画像に整形する必要があります。ここではOpenCVに標準搭載されているHaar-Cascadeによる顔検出を使いました。
import cv2
import os
import glob
path = os.getcwd()
def listfig():
#.py以外のファイルを取ってくる
#https://qiita.com/AAAAisBraver/items/8d40d9c2d624ecee105d
filename = path + "/face_detect_test/" + "*[!.py]"
list_fig = glob.glob(filename)
return list_fig
def jud_ext(filename):
if ((".jpg" in filename) == True):
ext = '.jpg'
elif ((".png" in filename) == True):
ext = '.png'
elif ((".jpeg" in filename) == True):
ext = '.jpeg'
else:
ext = 'end'
return ext
def main():
face_cascade_path = '/home/usr/anaconda3/pkgs/libopencv-4.2.0-py36_2'\
'/share/opencv4/haarcascades/haarcascade_frontalface_default.xml'
face_cascade = cv2.CascadeClassifier(face_cascade_path)
#画像リスト作成
fig_list = listfig()
#画像listからfor文で抽出
j = 1 #顔を検出する前に画像に番号をつける
k = 0 # 小さすぎる画像をカウントする
for fig_name in fig_list:
#fig_nameの拡張子を識別
ext = jud_ext(fig_name)
#endであれば終了
if (ext == 'end'):
break
#画像読み込み
src = cv2.imread(fig_name)
src_gray = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY)
faces = face_cascade.detectMultiScale(src_gray)
#faces部分を切り取り
i = 1 # 1枚の画像に2つ以上顔があったときに備えて
for x, y, w, h in faces:
#cv2.rectangle(src, (x, y), (x + w, y + h), (255, 0, 0), 2)
#srcから顔部分のみ切り取り
face = src[y: y + h, x: x + w]
#face_gray = src_gray[y: y + h, x: x + w]
#1辺が64ピクセル以上あることを確認する
if face.shape[0] < 64:
print('\ntoo small!!\n')
print('\n{}\n'.format(fig_name))
k += 1
continue
#注意: (64,64)の場合64x64ピクセル以下は保存されない
face = cv2.resize(face, (64, 64))
#顔保存
cv2.imwrite(path + '/face_detect_test/clip_face/' + 'face' + str(i) + str(j) + ext, face)
i += 1
j += 1
print("too small fig is {}".format(k))
if __name__ == '__main__':
main()
部分的にコードを抜粋して説明していきます。まず画像の拡張子判定に以下のコードを用います。
def jud_ext(filename):
if ((".jpg" in filename) == True):
ext = '.jpg'
elif ((".png" in filename) == True):
ext = '.png'
elif ((".jpeg" in filename) == True):
ext = '.jpeg'
else:
ext = 'end'
return ext
私が使用した画像ではjpg, png, jpeg形式しかなかったのでその3つを判定するコードを書きました。画像の拡張子を判定したのは,顔画像を切り取ったあとに同じ拡張子で画像を保存するためです。
顔切り取る際には以下のコードを用いています。このとき解像度を64×64に変換しています。これはデータサイズを小さくするための処置です。cv2.resize
関数では64×64以下の画像は保存されません。
ただし後で気がついたのですが,手作業で顔画像を判定していく際にものすごく見にくいので128×128で保存しても良いと思います。
def main():
face_cascade_path = '/home/usr/anaconda3/pkgs/libopencv-4.2.0-py36_2'\
'/share/opencv4/haarcascades/haarcascade_frontalface_default.xml'
face_cascade = cv2.CascadeClassifier(face_cascade_path)
#画像リスト作成
fig_list = listfig()
#画像listからfor文で抽出
j = 1 #顔を検出する前に画像に番号をつける
k = 0 # 小さすぎる画像をカウントする
for fig_name in fig_list:
#fig_nameの拡張子を識別
ext = jud_ext(fig_name)
#endであれば終了
if (ext == 'end'):
break
#画像読み込み
src = cv2.imread(fig_name)
src_gray = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY)
faces = face_cascade.detectMultiScale(src_gray)
#faces部分を切り取り
i = 1 # 1枚の画像に2つ以上顔があったときに備えて
for x, y, w, h in faces:
#cv2.rectangle(src, (x, y), (x + w, y + h), (255, 0, 0), 2)
#srcから顔部分のみ切り取り
face = src[y: y + h, x: x + w]
#face_gray = src_gray[y: y + h, x: x + w]
#1辺が64ピクセル以上あることを確認する
if face.shape[0] < 64:
print('\ntoo small!!\n')
print('\n{}\n'.format(fig_name))
k += 1
continue
#注意: (64,64)の場合64x64ピクセル以下は保存されない
face = cv2.resize(face, (64, 64))
#顔保存
cv2.imwrite(path + '/face_detect_test/clip_face/' + 'face' + str(i) + str(j) + ext, face)
i += 1
j += 1
print("too small fig is {}".format(k))
5. 教師データとテストデータを分ける
教師データ8割,テストデータ2割の配分で顔写真を分けます。以下のコードを用いました。
import os
import random
import shutil
import glob
path = os.getcwd()
names = ["ikuta_face", "saito_asuka_face", "shiraishi_face"]
#画像の名前をlistにまとめる
def listfig(tar_dir):
#.py以外のファイルを取ってくる
#https://qiita.com/AAAAisBraver/items/8d40d9c2d624ecee105d
filename = path + tar_dir + "*[!.py]"
list_fig = glob.glob(filename)
return list_fig
def main():
for name in names:
# 画像ディレクトリリスト取得
face_list = listfig("/dataset_face_fig/" + name + "/")
# face_listをシャッフル
random.shuffle(face_list)
# face_listの上から2割をtestディレクトリに移動させる
for i in range(len(face_list)//5):
shutil.move(str(face_list[i]), str(path + "/dataset_face_fig/test/" + name))
if __name__ == "__main__":
main()
画像データを分けたあと,正しいデータとなっているか確認してください。Google画像検索の中に間違いがあると全く関係ない画像が保存されていたりします。私の場合,以下のような事例がありました。
- 齋藤飛鳥の検索結果に早川聖来の顔写真が入っている。
- 齋藤飛鳥と西野七瀬の区別がつかなくなる。
- ゲシュタルト崩壊を起こして,顔が識別しにくくなってくる。
先程も述べましたがこの作業をしているときに64×64の画像に苦労させられました。
6. 教師データを水増しする
画像の選別が終わったら次は画像を加工することで,教師データを増やします。以下にソースコードを示します。
import os
import glob
import traceback
import cv2
from scipy import ndimage
path = os.getcwd()
#画像の名前をlistにまとめる
def listfig(tar_dir):
#.py以外のファイルを取ってくる
#https://qiita.com/AAAAisBraver/items/8d40d9c2d624ecee105d
filename = path + tar_dir + "*[!.py]"
list_fig = glob.glob(filename)
return list_fig
#画像回転させる
def fig_rot(img, ang):
img_rot = ndimage.rotate(img,ang)
img_rot = cv2.resize(img_rot,(64,64))
return img_rot
#拡張子判定
def jud_ext(filename):
if ((".jpg" in filename) == True):
ext = '.jpg'
elif ((".png" in filename) == True):
ext = '.png'
elif ((".jpeg" in filename) == True):
ext = '.jpeg'
else:
ext = 'end'
try:
raise Exception
except:
traceback.print_exc()
return ext
def main():
#増やす教師データが入っているディレクトリを指定
names = ["ikuta_face", "saito_asuka_face", "shiraishi_face"]
for name in names:
# /dataset_face_fig/name/ディレクトリから画像リストをとってくる
train_fig_list = listfig("/dataset_face_fig/" + "/" + name + "/")
i = 1 # 画像名前がかぶらないように
for train_fig in train_fig_list:
ext = jud_ext(train_fig)
#画像load
img = cv2.imread(train_fig)
#回転
for ang in [-10, 0, 10]:
# j = 1 # 画像名前がかぶらないように
img_rot = fig_rot(img, ang)
#画像保存
cv2.imwrite(path + '/dataset_face_fig/train/' + name + "/" + str(i) + '_' + str(ang) + ext, img_rot)
# 閾値処理
img_thr = cv2.threshold(img_rot, 100, 255, cv2.THRESH_TOZERO)[1]
cv2.imwrite(path + '/dataset_face_fig/train/' + name + "/" + str(i) + '_' + str(ang) + 'thr' + ext, img_thr)
# ぼかし処理
img_filter = cv2.GaussianBlur(img_rot, (5, 5), 0)
cv2.imwrite(path + '/dataset_face_fig/train/' + name + "/" + str(i) + '_' + str(ang) + 'fil' + ext, img_filter)
i += 1
return
if __name__ == "__main__":
main()
1つの画像データに対して回転処理をして3枚に増やします。その後それぞれの画像に対して閾値処理とボカシ処理を行います。画像データ1枚に対して3×3で9枚の画像がてに入ります。以下にコアとなるコードを抜粋します。
def main():
#増やす教師データが入っているディレクトリを指定
names = ["ikuta_face", "saito_asuka_face", "shiraishi_face"]
for name in names:
# /dataset_face_fig/name/ディレクトリから画像リストをとってくる
train_fig_list = listfig("/dataset_face_fig/" + "/" + name + "/")
i = 1 # 画像名前がかぶらないように
for train_fig in train_fig_list:
ext = jud_ext(train_fig)
#画像load
img = cv2.imread(train_fig)
#回転
for ang in [-10, 0, 10]:
# j = 1 # 画像名前がかぶらないように
img_rot = fig_rot(img, ang)
#画像保存
cv2.imwrite(path + '/dataset_face_fig/train/' + name + "/" + str(i) + '_' + str(ang) + ext, img_rot)
# 閾値処理
img_thr = cv2.threshold(img_rot, 100, 255, cv2.THRESH_TOZERO)[1]
cv2.imwrite(path + '/dataset_face_fig/train/' + name + "/" + str(i) + '_' + str(ang) + 'thr' + ext, img_thr)
# ぼかし処理
img_filter = cv2.GaussianBlur(img_rot, (5, 5), 0)
cv2.imwrite(path + '/dataset_face_fig/train/' + name + "/" + str(i) + '_' + str(ang) + 'fil' + ext, img_filter)
i += 1
return
7. Google Colaboratoryを用いて学習を行う
このあと,1.画像データのラベル付けと2.学習を行う必要があります。ここからはGoogle Colaboratory(Colab)を使ってみます。
ColabではハイスペックなCPUとGPUが提供されており,計算を高速に行うことができます。ColabはGoogleアカウントさえ持っていれば誰でも使うことができます。
まず以下のコードを実行し,Google driveをマウントします。これによってGoogle drive内のデータにアクセスできます。実行はShift + Enter
です。
from google.colab import drive
drive.mount('/content/drive')
import tensorflow as tf
tf.test.gpu_device_name()
次に画像データのラベル付けと学習を行います。まずソースコードを以下に示します。
# ラベル付けから学習まで全て行う
# デバッガ
%pdb off
import os
import glob
import cv2
import numpy as np
from keras.layers import Activation, Conv2D, Dense, Flatten, MaxPooling2D
from keras.models import Sequential
from keras.utils.np_utils import to_categorical
from tqdm import tqdm
import matplotlib.pyplot as plt
import pickle
import datetime
path = os.getcwd()
print(path)
%cd /content/drive/My\ Drive/dataset_face_fig
#画像の名前をlistにまとめる
def listfig(tar_dir):
#.py以外のファイルを取ってくる
#.py以外のファイルを取ってくる
#https://qiita.com/AAAAisBraver/items/8d40d9c2d624ecee105d
filename = tar_dir + "*[!.py]"
list_fig = glob.glob(filename)
return list_fig
#人物判定
def jud_hum(filename):
if (("ikuta" in filename) == True):
ext = 0
elif (("saito_asuka" in filename) == True):
ext = 1
elif (("shiraishi" in filename) == True):
ext = 2
else:
ext = 'end'
try:
raise Exception
except:
traceback.print_exc()
return ext
# 教師データラベル付け
def label_training(names):
# 教師データのラベル付け
x_train = []
y_train = []
for name in tqdm(names):
face_fig_list = glob.glob("/content/drive/My Drive/dataset_face_fig/train/" + name + "/" + "*")
print(face_fig_list)
for face_fig_filename in tqdm(face_fig_list):
face_img = cv2.imread(face_fig_filename)
b,g,r = cv2.split(face_img)
face_img = cv2.merge([r,g,b])
x_train.append(face_img)
kind_hum = jud_hum(face_fig_filename)
y_train.append(kind_hum)
print(y_train)
print("y_train length")
print(len(y_train))
# x_train, y_trainを保存
f = open('x_train.txt', 'wb')
# listをfにダンプ
pickle.dump(x_train, f)
f = open('y_train.txt', 'wb')
# listをfにダンプ
pickle.dump(y_train, f)
return x_train, y_train
# テストデータラベル付け
def label_test(names):
# テストデータのラベル付け
x_test = []
y_test = []
for name in tqdm(names):
# face_fig_list = listfig("/content/drive/My Drive/dataset_face_fig/" + name)
face_fig_list = glob.glob("/content/drive/My Drive/dataset_face_fig/test/" + name + "/" + "*")
# print("/content/drive/My Drive/dataset_face_fig/" + name)
print(face_fig_list)
for face_fig_filename in tqdm(face_fig_list):
face_img = cv2.imread(face_fig_filename)
b,g,r = cv2.split(face_img)
face_img = cv2.merge([r,g,b])
x_test.append(face_img)
kind_hum = jud_hum(face_fig_filename)
y_test.append(kind_hum)
# x_test, y_testを保存
f = open('x_test.txt', 'wb')
# listをfにダンプ
pickle.dump(x_test, f)
f = open('y_test.txt', 'wb')
# listをfにダンプ
pickle.dump(y_test, f)
print(y_test)
print("y_test length")
print(len(y_test))
return x_test, y_test
def cnn(x_train, y_train, x_test, y_test):
# モデルの定義
model = Sequential()
model.add(Conv2D(input_shape=(64, 64, 3), filters=32,kernel_size=(3, 3),
strides=(1, 1), padding="same"))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Conv2D(filters=32, kernel_size=(3, 3),
strides=(1, 1), padding="same"))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Conv2D(filters=32, kernel_size=(3, 3),
strides=(1, 1), padding="same"))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Flatten())
model.add(Dense(256))
model.add(Activation("sigmoid"))
model.add(Dense(128))
model.add(Activation('sigmoid'))
model.add(Dense(3))
model.add(Activation('softmax'))
# コンパイル
model.compile(optimizer='sgd',
loss='categorical_crossentropy',
metrics=['accuracy'])
# 学習
history = model.fit(x_train, y_train, batch_size=32,
epochs=50, verbose=1, validation_data=(x_test, y_test))
# 汎化制度の評価・表示
score = model.evaluate(x_test, y_test, batch_size=32, verbose=0)
print('validation loss:{0[0]}\nvalidation accuracy:{0[1]}'.format(score))
model.save("my_model.h5")
type(history)
return history
def learn_monitor_plot(history):
#acc, val_accのプロット
plt.plot(history.history["accuracy"], label="acc", ls="-", marker="o")
plt.plot(history.history["val_accuracy"], label="val_acc", ls="-", marker="x")
plt.ylabel("accuracy")
plt.xlabel("epoch")
plt.legend(loc="best")
now = datetime.datetime.now()
plt.savefig("learning_cpu_" + now.strftime('%Y%m%d_%H%M%S') + '.png')
def main():
print("ok")
names = ["ikuta_face", "saito_asuka_face", "shiraishi_face"]
# 教師データ,訓練データのラベル付け
x_train, y_train = label_training(names)
x_test, y_test = label_test(names)
# 画像データ整形
x_train = np.array(x_train)
x_test = np.array(x_test)
# ラベルデータ整形
y_train = to_categorical(y_train)
y_test = to_categorical(y_test)
# 深層学習
learn_history = cnn(x_train, y_train, x_test, y_test)
# グラフ
learn_monitor_plot(learn_history)
main()
%cd /content
ラベル付けを行う部分を以下に示します。
#人物判定
def jud_hum(filename):
if (("ikuta" in filename) == True):
ext = 0
elif (("saito_asuka" in filename) == True):
ext = 1
elif (("shiraishi" in filename) == True):
ext = 2
else:
ext = 'end'
try:
raise Exception
except:
traceback.print_exc()
return ext
# 教師データラベル付け
def label_training(names):
# 教師データのラベル付け
x_train = []
y_train = []
for name in tqdm(names):
face_fig_list = glob.glob("/content/drive/My Drive/dataset_face_fig/train/" + name + "/" + "*")
print(face_fig_list)
for face_fig_filename in tqdm(face_fig_list):
face_img = cv2.imread(face_fig_filename)
b,g,r = cv2.split(face_img)
face_img = cv2.merge([r,g,b])
x_train.append(face_img)
kind_hum = jud_hum(face_fig_filename)
y_train.append(kind_hum)
print(y_train)
print("y_train length")
print(len(y_train))
# x_train, y_trainを保存
f = open('x_train.txt', 'wb')
# listをfにダンプ
pickle.dump(x_train, f)
f = open('y_train.txt', 'wb')
# listをfにダンプ
pickle.dump(y_train, f)
return x_train, y_train
# テストデータラベル付け
def label_test(names):
# テストデータのラベル付け
x_test = []
y_test = []
for name in tqdm(names):
# face_fig_list = listfig("/content/drive/My Drive/dataset_face_fig/" + name)
face_fig_list = glob.glob("/content/drive/My Drive/dataset_face_fig/test/" + name + "/" + "*")
# print("/content/drive/My Drive/dataset_face_fig/" + name)
print(face_fig_list)
for face_fig_filename in tqdm(face_fig_list):
face_img = cv2.imread(face_fig_filename)
b,g,r = cv2.split(face_img)
face_img = cv2.merge([r,g,b])
x_test.append(face_img)
kind_hum = jud_hum(face_fig_filename)
y_test.append(kind_hum)
# x_test, y_testを保存
f = open('x_test.txt', 'wb')
# listをfにダンプ
pickle.dump(x_test, f)
f = open('y_test.txt', 'wb')
# listをfにダンプ
pickle.dump(y_test, f)
print(y_test)
print("y_test length")
print(len(y_test))
return x_test, y_test
cv2.imread(画像データ)
では3次元のndarray型に変換します。例えば64×64ピクセルの画像をロードした場合dataの型は以下のようになります。
data = cv2.imread('filepath')
print(data.shape)
# (64, 64, 3)
cv2.split(ndarray型データ)でデータを2次元配列を3つ取り出します。このときblue, green, redの順で取り出されることに注意してください。
cv2.merge([r, g, b])でlistを生成し,appendでx_train or x_testに追加をしていきます。
GPUで計算したときは画像を読み込むのに時間を要したので,画像データだけDriveに一時保存するコードを書きました。
# x_test, y_testを保存
f = open('x_test.txt', 'wb')
# listをfにダンプ
pickle.dump(x_test, f)
f = open('y_test.txt', 'wb')
# listをfにダンプ
pickle.dump(y_test, f)
CPUでは1epochの計算で5秒程かかりますが,GPUでは1秒もかからないです。ぜひ試してみてください。
結果は以下のようになります。
およそ80%の精度は出ます。
まとめ
はじめて本格的なコードを書いて画像認識を行いました。今後は別のネットワークも勉強して使ってみたいと思います。ご質問・コメント等お待ちしております。ここまで読んでいただきありがとうございました。