時代は顔認証
中国の中でも特にIT的にめちゃくちゃ発展している都市「深セン」。聞くところによると、コンビニの支払いから自動販売機まで、顔をかざすだけで決済ができるとか。
良いか悪いかは置いておいて、すごいですよね。近未来的でかっこいい。
じゃあ僕も作っちゃお!w みたいなノリで去年作ったのですが、なかなか記事にできていなかったので、今回記事にすることにしました。
今回ご紹介するシステム、プログラムは、初めてまともにPythonに触った作品で、かつ初めて機械学習に触れた記念すべき作品です。
概要
PCのインカメラを使って自分の顔の画像を撮影し、ライブラリOpenCVを使って画像の加工、水増しを行ったあと、traincascadeで顔の学習を行い個人を判別するプログラムを作成する。
使用言語はPython3を使用した。
プログラムの流れ
今回の仕組みは顔画像の処理、学習プログラムと顔認証プログラムの2種類を作成しました。
顔画像処理、学習プログラムの流れ
① ビデオキャプチャーでPC のカメラを起動する
② 顔を認識したフレームを画像として40 枚保存する
③ 様々な形で40 枚の画像を加⼯して学習データを⽔増しする
④ 学習コマンドを⽣成する
⑤ ⽣成されたコマンドをコンソールに打ち込み学習開始
⑥ 学習完了後カスケードファイルを⽣成
顔認証プログラムの流れ
① 登録者のカスケードファイルを読み込む
② 各フレームをカスケードファイルに通し、個⼈識別を⾏う
フォルダ構造
顔画像処理、学習プログラム
face_learner/
├─face_learner.py
├─haarcascade_frontalface_default.xml
├─neg.txt
├─pos.txt
├─cascade/
├─neg/
├─neg1.jpg
├─neg2.jpg
├─neg3.jpg
…
└─neg265.jpg
├─pic/
├─pic1.jpg
├─pic2.jpg
├─pic3.jpg
…
└─pic1639.jpg
└─pos.vec
このフォルダ構造は、学習プログラムの周りのファイルのみです。
個人判別プログラムは含まれていません。
プログラムの説明(学習プログラム)
ソースコードの中⾝にも簡単に何を⾏う部分かはコメントアウトで説明を施しているので、そちらも参考にしてください。。
学習には、正解画像(ポジティブ画像)を不正解画像(ネガティブ画像)の2種類が必要となります。正解画像というのは、学習させたい顔画像のことで、画像の中のどこに顔があるかという座標情報も必要です。不正解画像というのは、正解画像でなければなんでもよく、今回は265 枚の可愛い動物の画像を⽤意してあって、あらかじめneg ディレクトリに格納してあります。
まずはこの正解画像を集める作業から始めます。
正解画像は、まず顔画像を40 枚撮影して、その後学習精度を上げるために1640 枚ほどに⽔増しを⾏います。この正解画像は枚数が多ければ多いほど学習精度が上がるので、適宜枚数を増やすとよいでしょう。そこは、PC スペックとの相談な気がします。
新たに顔を学習させるので、前に使った不必要なデータをまずは全て削除する必要があります。ここはもうちょい頭良く処理できるはずですね(笑)。顔画像の収集が本プログラム最⼤の⽬的です。あらかじめ読み込んでいた顔認識⽤のカスケードファイルを使って、顔を認識し、まずは40 枚の顔画像を撮影します。
ここから⽔増しを開始します。⽔増しは画像を様々な加⼯を加えて⾏います。この部分はすでにツールがあるみたいなのでそちらを使う⽅が絶対に早いです。今回はプログラミングの練習もかねて自作しました。
【画像加⼯の種類】
• 左右反転
• 彩度変更
• コントラスト変更
• 明度変更
• シャープネス変更
• ±15 度回転
ここの処理はかなり改善の余地がありまくりです。まずそれぞれの加⼯処理部分は関数化した⽅が絶対に良いです。本プログラムでは、加⼯毎回写真の枚数を数えて、加⼯して、の繰り返しなので、もう少しスマートにかけると良いですね。画像を集取したら今度は正解画像の情報をまとめるテキストファイルを作成する必要があります。これは、どの画像のどの部分に顔があるかというのを画像ファイルのパス、顔がある部分を囲う四⾓形の4つ⾓の座標情報です。書式は【パス 顔の数 座標(x) 座標(y) 座標(w) 座標(h)】です。w は横幅、h は⾼さ。
以下に例を示します。
pic/pic1389.jpg 1 454 328 547 547
pic/pic302.jpg 1 565 85 380 380
pic/pic464.jpg 1 947 389 31 31
pic/pic470.jpg 1 663 238 341 341
pic/pic1438.jpg 1 658 341 540 540
pic/pic316.jpg 1 620 88 376 376
pic/pic1376.jpg 1 795 414 376 376
pic/pic1410.jpg 1 476 408 379 379
pic/pic1404.jpg 1 497 394 383 383
pic/pic1362.jpg 1 754 400 372 372
pic/pic869.jpg 1 625 235 420 420
pic/pic855.jpg 1 380 185 381 381
pic/pic666.jpg 1 361 241 424 424
pic/pic100.jpg 1 312 91 360 360
pic/pic64.jpg 1 622 101 371 371
pic/pic114.jpg 1 279 84 375 375
pic/pic70.jpg 1 618 89 382 382
pic/pic672.jpg 1 1213 216 43 43
pic/pic672.jpg 1 369 274 385 385
pic/pic882.jpg 1 721 313 353 353
.
.
.
テキストを⽣成したら、あとはコマンドを⽣成するだけです。まずは正解画像の情報を使ってベクトルファイルに変換するコマンド。次に学習を⾏うコマンド。python にはos.system()関数やsubprocess.call()関数など、python のプログラムからLinux のコマンドを実⾏する機能が存在しますが、僕はうまくいかなかったので⼿動で実⾏できるようにコマンドを⽣成するようにしています。誰かここの部分、教えていただけると勉強になります!
学習が終了したら、cascade ディレクトリにcascade.xml ファイルができているので、それを読み込んで個⼈判別を⾏います。ファイル名は適宜個⼈名などにするとわかりやすいですね。
プログラムの説明(判別プログラム)
こちらは、先ほど作成したカスケードファイルを使って個⼈判別を⾏います。
本プログラムで読み込むライブラリはOpenCV(cv2)のみです。
毎回カメラで捉えたフレームをカスケードファイルに通して、顔が⼀致したら⼀致した部分を枠で囲います。
まずは⼈間の顔を認識したら⽩の枠で囲み、個⼈を認識したら⻘の枠で囲み、その上に名前を描画しています。
こんな感じで、インカメでリアルタイムで個人の判別ができます!
実際のプログラム
本当は汚いコードを晒すのは気が引けますが、きっと何かの役に立ってくれると信じて、全コードを公開しようと思います。
死ぬほど長いのはご了承ください。
画像収集+学習プログラム
import os
import re
import numpy as np
import time
import glob
import shutil
import PIL.Image
from PIL import ImageEnhance
import subprocess
import cv2
#uer_name = input("Who are you")
cascade = cv2.CascadeClassifier('haarcascade_frontalface_default.xml')
#----------------------------------------------
#mov, pic, posをクリアする
os.chdir('cascade')
for x in glob.glob('*.xml'):
os.remove(x)
os.chdir('../')
os.chdir('pic')
for x in glob.glob('*.jpg'):
os.remove(x)
os.chdir('../')
os.chdir('pos')
for x in glob.glob('*.jpg'):
os.remove(x)
os.chdir('../')
os.remove('pos.txt')
#----------------------------------------------
#pos.txtを作成
f = open('pos.txt', 'a')
#----------------------------------------------
#顔の画像を集める
cap = cv2.VideoCapture(0)
cascade_path1 = "haarcascade_frontalface_default.xml"
cascade_path2 = 'lbpcascade_profileface.xml'
cascade1 = cv2.CascadeClassifier(cascade_path1)
cascade2 = cv2.CascadeClassifier(cascade_path2)
color = (255,255,255)
picture_num = 1
while True:
ret, frame = cap.read()
facerect1 = cascade1.detectMultiScale(frame, scaleFactor=1.7, minNeighbors=4, minSize=(100,100))
font = cv2.FONT_HERSHEY_SIMPLEX
cv2.putText(frame, str(picture_num), (10,500), font, 4,(0,0,0),2,cv2.LINE_AA)
if len(facerect1) > 0:
for (x,y,w,h) in facerect1:
#cv2.rectangle(frame,(x,y),(x+w,y+h),(255,0,0),2)
picture_name = 'pic/pic' + str(picture_num) + '.jpg'
cv2.imwrite(picture_name, frame)
#text = picture_name + ' 1 ' + str(x) + ' ' + str(y) + ' ' + str(w) + ' ' + str(h) + '\n'
#f.write(text)
picture_num = picture_num + 1
cv2.imshow("frame", frame)
if picture_num == 41:
break
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cap.release()
cv2.destroyAllWindows()
#----------------------------------------------
#水増し開始
#picturesの写真の数を数える
dir = os.getcwd()
dirPic = dir + "/pic"
files = os.listdir(dirPic)
count = 0
for file in files:
count = count + 1
os.chdir('pic')
#写真の枚数
imageNum = count
#左右反転
for num in range(1, count+1):
name = 'pic' + str(num) + '.jpg'
if os.path.exists(name) :
pass
else :
continue
if os.path.getsize(name) == 0:
os.remove(name)
continue
img = cv2.imread(name)
yAxis = cv2.flip(img, 1)
newName = 'pic' + str(imageNum) + '.jpg'
cv2.imwrite(newName,yAxis)
imageNum = imageNum + 1
print('OK')
print('NEXT STAGE')
#写真の数をカウント
dir = os.getcwd()
files = os.listdir(dir)
count = 0
for file in files:
count = count + 1
print(count)
#写真の枚数
imageNum = count
#imageNumを固定しておく
picNum = imageNum
SATURATION = 0.5
CONTRAST = 0.5
BRIGHTNESS = 0.5
SHARPNESS = 2.0
# 彩度を変える
for num in range(1, count+1):
name = 'pic' + str(num) + '.jpg'
if os.path.exists(name) :
pass
else :
print('NO')
continue
if os.path.getsize(name) == 0:
os.remove(name)
continue
img = PIL.Image.open(name)
saturation_converter = ImageEnhance.Color(img)
saturation_img = saturation_converter.enhance(SATURATION)
newName = 'pic' + str(imageNum) + '.jpg'
saturation_img.save(newName)
imageNum = imageNum + 1
print('OK')
print('NEXT STAGE')
#写真の数をカウント
dir = os.getcwd()
files = os.listdir(dir)
count = 0
for file in files:
count = count + 1
print(count)
#写真の枚数
imageNum = count
count = picNum
# コントラストを変える
for num in range(1, count+1):
name = 'pic' + str(num) + '.jpg'
if os.path.exists(name) :
pass
else :
print('NO')
continue
if os.path.getsize(name) == 0:
os.remove(name)
continue
img = PIL.Image.open(name)
contrast_converter = ImageEnhance.Contrast(img)
contrast_img = contrast_converter.enhance(CONTRAST)
newName = 'pic' + str(imageNum) + '.jpg'
contrast_img.save(newName)
imageNum = imageNum + 1
print('OK')
print('NEXT STAGE')
#写真の数をカウント
dir = os.getcwd()
files = os.listdir(dir)
count = 0
for file in files:
count = count + 1
print(count)
#写真の枚数
imageNum = count
count = picNum
# 明度を変える
for num in range(1, count+1):
name = 'pic' + str(num) + '.jpg'
if os.path.exists(name) :
pass
else :
print('NO')
continue
if os.path.getsize(name) == 0:
os.remove(name)
continue
img = PIL.Image.open(name)
brightness_converter = ImageEnhance.Brightness(img)
brightness_img = brightness_converter.enhance(BRIGHTNESS)
newName = 'pic' + str(imageNum) + '.jpg'
brightness_img.save(newName)
imageNum = imageNum + 1
print('OK')
print('NEXT STAGE')
#写真の数をカウント
dir = os.getcwd()
files = os.listdir(dir)
count = 0
for file in files:
count = count + 1
print(count)
#写真の枚数
imageNum = count
count = picNum
# シャープネスを変える
for num in range(1, count+1):
name = 'pic' + str(num) + '.jpg'
if os.path.exists(name) :
pass
else :
print('NO')
continue
if os.path.getsize(name) == 0:
os.remove(name)
continue
img = PIL.Image.open(name)
sharpness_converter = ImageEnhance.Sharpness(img)
sharpness_img = sharpness_converter.enhance(SHARPNESS)
newName = 'pic' + str(imageNum) + '.jpg'
sharpness_img.save(newName)
imageNum = imageNum + 1
print('OK')
print('NEXT STAGE')
#写真の数をカウント
dir = os.getcwd()
files = os.listdir(dir)
count = 0
for file in files:
count = count + 1
print(count)
#写真の枚数
imageNum = count
#imageNumを固定しておく
picNum = imageNum
#15度回転
for num in range(1, count+1):
name = 'pic' + str(num) + '.jpg'
if os.path.exists(name) :
pass
else :
print('NO')
continue
if os.path.getsize(name) == 0:
os.remove(name)
continue
# 画像読み込み
img = cv2.imread(name)
h, w = img.shape[:2]
size = (w, h)
# 回転角の指定
angle = 15
angle_rad = angle/180.0*np.pi
# 回転後の画像サイズを計算
w_rot = int(np.round(h*np.absolute(np.sin(angle_rad))+w*np.absolute(np.cos(angle_rad))))
h_rot = int(np.round(h*np.absolute(np.cos(angle_rad))+w*np.absolute(np.sin(angle_rad))))
size_rot = (w_rot, h_rot)
# 元画像の中心を軸に回転する
center = (w/2, h/2)
scale = 1.0
rotation_matrix = cv2.getRotationMatrix2D(center, angle, scale)
# 平行移動を加える (rotation + translation)
affine_matrix = rotation_matrix.copy()
affine_matrix[0][2] = affine_matrix[0][2] -w/2 + w_rot/2
affine_matrix[1][2] = affine_matrix[1][2] -h/2 + h_rot/2
img_rot = cv2.warpAffine(img, affine_matrix, size_rot, flags=cv2.INTER_CUBIC)
cv2.imwrite(newName, img_rot)
newName = 'pic' + str(imageNum) + '.jpg'
saturation_img.save(newName)
imageNum = imageNum + 1
print('OK')
print('NEXT STAGE')
#写真の数をカウント
dir = os.getcwd()
files = os.listdir(dir)
count = 0
for file in files:
count = count + 1
print(count)
#写真の枚数
imageNum = count
#-15度回転
for num in range(1, count+1):
name = 'pic' + str(num) + '.jpg'
if os.path.exists(name) :
pass
else :
print('NO')
continue
if os.path.getsize(name) == 0:
os.remove(name)
continue
# 画像読み込み
img = cv2.imread(name)
h, w = img.shape[:2]
size = (w, h)
# 回転角の指定
angle = -15
angle_rad = angle/180.0*np.pi
# 回転後の画像サイズを計算
w_rot = int(np.round(h*np.absolute(np.sin(angle_rad))+w*np.absolute(np.cos(angle_rad))))
h_rot = int(np.round(h*np.absolute(np.cos(angle_rad))+w*np.absolute(np.sin(angle_rad))))
size_rot = (w_rot, h_rot)
# 元画像の中心を軸に回転する
center = (w/2, h/2)
scale = 1.0
rotation_matrix = cv2.getRotationMatrix2D(center, angle, scale)
# 平行移動を加える (rotation + translation)
affine_matrix = rotation_matrix.copy()
affine_matrix[0][2] = affine_matrix[0][2] -w/2 + w_rot/2
affine_matrix[1][2] = affine_matrix[1][2] -h/2 + h_rot/2
img_rot = cv2.warpAffine(img, affine_matrix, size_rot, flags=cv2.INTER_CUBIC)
cv2.imwrite(newName, img_rot)
newName = 'pic' + str(imageNum) + '.jpg'
saturation_img.save(newName)
imageNum = imageNum + 1
print('OK')
print('NEXT STAGE')
#写真の数をカウント
dir = os.getcwd()
files = os.listdir(dir)
count = 0
for file in files:
count = count + 1
print(count)
print('OK')
print('COMPLETE')
#------------------------------------------------------
#テキストファイル作成
#cascade = cv2.CascadeClassifier('haarcascade_frontalface_default.xml')
#os.chdir('pic')
for num in glob.glob('*.jpg'):
img = cv2.imread(num)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
faces = cascade.detectMultiScale(gray)
for (x,y,w,h) in faces:
text = 'pic/' + num + ' 1 ' + str(x) + ' ' + str(y) + ' ' + str(w) + ' ' + str(h) + '\n'
f.write(text)
cmd = 'opencv_createsamples -info pos.txt -vec pos.vec -num ' + str(count)
print(cmd)
cmd = 'opencv_traincascade -data ./cascade -vec pos.vec -bg neg.txt -numPos 1500 numNeg 255'
print(cmd)
print('COMPLETE.')
個人判別プログラム
# coding: utf-8
import cv2
font = cv2.FONT_HERSHEY_SIMPLEX
if __name__ == "__main__":
cap = cv2.VideoCapture(0)
cascade_path_human = 'haarcascade_frontalface_default.xml'
cascade_path_hirosugu = "cascade_hirosugu.xml"
cascade_path_kenta = 'cascade_kenta.xml'
cascade_hirosugu = cv2.CascadeClassifier(cascade_path_hirosugu)
cascade_human = cv2.CascadeClassifier(cascade_path_human)
cascade_kenta = cv2.CascadeClassifier(cascade_path_kenta)
color = (255,0,0)
while True:
ret, frame = cap.read()
facerect_human = cascade_human.detectMultiScale(frame, scaleFactor=1.7, minNeighbors=4, minSize=(100,100))
facerect_hirosugu = cascade_hirosugu.detectMultiScale(frame, scaleFactor=1.7, minNeighbors=4, minSize=(100,100))
facerect_kenta = cascade_kenta.detectMultiScale(frame, scaleFactor=1.7, minNeighbors=4, minSize=(100,100))
if len(facerect_human) > 0:
for rect in facerect_human:
cv2.rectangle(frame, tuple(rect[0:2]), tuple(rect[0:2]+rect[2:4]), (255,255,255), thickness=2)
if len(facerect_hirosugu) > 0:
for rect in facerect_hirosugu:
cv2.rectangle(frame, tuple(rect[0:2]), tuple(rect[0:2]+rect[2:4]), color, thickness=2)
cv2.putText(frame, 'Hirosugu Takeshita', tuple(rect[0:2]), font, 2,(0,0,0),2,cv2.LINE_AA)
if len(facerect_kenta) > 0:
for rect in facerect_kenta:
cv2.rectangle(frame, tuple(rect[0:2]), tuple(rect[0:2]+rect[2:4]), color, thickness=2)
cv2.putText(frame, 'Kenta Suzuki', tuple(rect[0:2]), font, 2,(0,0,0),2,cv2.LINE_AA)
cv2.imshow("frame", frame)
# qキーを押すとループ終了
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cap.release()
cv2.destroyAllWindows()
ちなみに、生成したカスケードファイル(.xmlの学習ファイル)は、顔認証プログラムと同じ階層に入れておくことをお勧めします!
最後に
ね?簡単でしょう?専門の方からしたらこんなゴミプログラム!と思われてしまいますが、エセMLエンジニアを名乗ることができます。
本当であれば、特徴量の抽出とか、NNを自分で書いたりとか(そんなに詳しくないのでわからないですが)を自分でやるとさらに精度があがります。
また、OpenCVについての記事はたくさんあるので、そちらも参考に。今回の記事はかなり薄くなぞっているだけなので、詳しい仕組みやその他学習時のコマンドなどはしっかり調べることをお勧めします。
まあ今回はなんちゃって機械学習によるなんちゃって個人判別ですので、こんなふうにすれば顔画像の収集が楽になるんだ、という知見だけ持って帰ってもらって、もっと精度の高いface IDを作って紹介してくださると嬉しいです!
長々と読んでいただきありがとうございました!