pythonで簡単に顔認証

#顔認証ゴール
pythonとopenCV使って顔認証。

完成形の画面には、PCのカメラのリアルタイム映像に人間の顔を認識した箇所が白い枠として出てくる。
加えて、青い正方形の枠が学習した顔を認識した箇所を示し、名前を上に出す。

#開発環境
Macbook Air (M1チップ)
anaconda
ANACONDA.NAVIGATORでGUIを使った仮想環境を用いる。
M1チップでもRosetta2で普通に使える。

python 3.7.9
openCVが3.7以降だと失敗した気がする。
 
numpy 1.19.2
opencv 3.4.2
pillow 8.1.0
#全体の流れ
2つのプログラムを書きました。
あとは不正解用画像を数十枚。

- face_learner.py (学習用プログラム)
カメラを起動し学習用顔写真(正解画像)を撮影
学習用顔写真を傘増しする
学習したデータをxmlとして吐き出す

- face_id.py (認証用プログラム)
カメラを起動
人間の顔認識用xmlから当てはまる箇所に白い正方形を表示
自分の顔xmlから当てはまる箇所に青い正方形と設定した名前を表示

長いので一番最後にコード公開してます。

#ファイル構成
Face_detectionディレクトリ
├ face_learner.py (学習用プログラム)
├ face_id.py (認証用プログラム)
├ pos
　└ 撮った正解画像と傘増しした画像
├ pos.txt (正解画像のリスト)
├ pos.vec (正解画像の特徴量を示したベクトルデータ)
├ neg
　└ 不正解画像
├ neg.txt (不正解画像のリスト)
├ haarcascade_frontalface_default.xml (人間の顔認識用xml)
├ cascade
　└ 認証する人の顔認識用xml

不正解画像は自分で適当に集め、以下のようにlistを作成。

```neg.txt
./neg/neg1.jpg
./neg/neg2.jpg
./neg/neg3.jpg
./neg/neg4.jpg
./neg/neg5.jpg
```

haarcascade_frontalface_default.xmlはgithubから持ってくる。
(opencv/data/haarcascades/haarcascade_frontalface_default.xml)

face_learner.pyで正解画像撮影/画像傘増し/学習をする。
実行するとカメラが起動し、自分の顔を撮影される。
終わるとcascadeフォルダにcascade.xmlが生成されるのでそれをトップディレクトリに移動させる。

face_id.pyで実際に認証を行なっている。
実行すると、カメラが起動し、青い正方形とその上に名前が出る。

###コード公開

コードは参考程度に公開してます。
動くものを作ったお話なので汚さはご了承ください。

```face_lerner.py
# ======================================================================
# Project Name    : Face Identify 
# File Name       : face_lerner.py
# Encoding        : utf-8
# Creation Date   : 2021/02/20
# ======================================================================

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

def takePic(cascade, picnum_max):
    """take pictures for learning

    """
    cap = cv2.VideoCapture(0)
    color = (255,255,255)
    picture_num = 1
    while True:
        ret, frame = cap.read()
        facerect = cascade.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(facerect) > 0:
            for (x,y,w,h) in facerect:
                picture_name = './pos/pic' + str(picture_num) + '.jpg'
                cv2.imwrite(picture_name, frame)
                picture_num += 1
        cv2.imshow("frame", frame)
        if picture_num == picnum_max + 1:
            break
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break
    cap.release()
    cv2.destroyAllWindows()

def removePic():
    """remove files for initialize

    """
    os.chdir('cascade')
    for x in glob.glob('*.xml'):
        os.remove(x)
    os.chdir('../')

    os.chdir('pos')
    for x in glob.glob('*.jpg'):
        os.remove(x)
    os.chdir('../')

    if os.path.exists('pos.txt'):
        os.remove('pos.txt')

def countPic():
    """count a number of taken pictures

    """

    files = os.listdir("./pos")
    count = 0
    for file in files:
        count = count + 1

    return count
  
def bulkOut():
    """Bult out pics

    """
    # a number of taken pics
    originalnum = countPic()
    # a number of present total pics
    imageNum = countPic()+1

    # Flip horizontal
    for num in range(1, originalnum + 1 ):
        fileName = './pos/pic' + str(num) + '.jpg'
        if not os.path.exists(fileName):
            continue
        img = cv2.imread(fileName)
        yAxis = cv2.flip(img, 1)
        newFileName = './pos/pic' + str(imageNum) + '.jpg'
        cv2.imwrite(newFileName,yAxis)
        imageNum += 1
    print('*** Flip horizontal is finished *** \n')
    
    # Change Saturation
    SATURATION = 0.5
    CONTRAST = 0.5
    BRIGHTNESS = 0.5
    SHARPNESS = 2.0

    for num in range(1, 2 * originalnum + 1):
        fileName = './pos/pic' + str(num) + '.jpg'
        if not os.path.exists(fileName): 
            continue
        img = PIL.Image.open(fileName)
        saturation_converter = ImageEnhance.Color(img)
        saturation_img = saturation_converter.enhance(SATURATION)
        newFileName = './pos/pic' + str(imageNum) + '.jpg'
        saturation_img.save(newFileName)
        imageNum += 1
    print('*** Change Saturation is finished *** \n')

    # Change Contsract
    for num in range(1, 3 * originalnum + 1):
        fileName = './pos/pic' + str(num) + '.jpg'
        if not os.path.exists(fileName):
            continue
        img = PIL.Image.open(fileName)
        contrast_converter = ImageEnhance.Contrast(img)
        contrast_img = contrast_converter.enhance(CONTRAST)
        newFileName = './pos/pic' + str(imageNum) + '.jpg'
        contrast_img.save(newFileName)
        imageNum += 1
    print('*** Change Constract is finished *** \n')

    # Change Brightness
    for num in range(1, 4 * originalnum + 1):
        fileName = './pos/pic' + str(num) + '.jpg'
        if not os.path.exists(fileName):
            continue
        img = PIL.Image.open(fileName)
        brightness_converter = ImageEnhance.Brightness(img)
        brightness_img = brightness_converter.enhance(BRIGHTNESS)
        newFileName = './pos/pic' + str(imageNum) + '.jpg'
        brightness_img.save(newFileName)
        imageNum += 1
    print('*** Change Brightness is finished *** \n')

    # Change Sharpness
    for num in range(1, 5 * originalnum + 1):
        fileName = './pos/pic' + str(num) + '.jpg'
        if not os.path.exists(fileName):
            continue
        img = PIL.Image.open(fileName)
        sharpness_converter = ImageEnhance.Sharpness(img)
        sharpness_img = sharpness_converter.enhance(SHARPNESS)
        newFileName = './pos/pic' + str(imageNum) + '.jpg'
        sharpness_img.save(newFileName)
        imageNum += 1
    print('*** Change Sharpness is finished *** \n')

    # Rotate by 15 deg.
    for num in range(1, 6 * originalnum + 1):
        fileName = './pos/pic' + str(num) + '.jpg'
        if not os.path.exists(fileName):
            continue
        # read original file
        img = cv2.imread(fileName)
        h, w = img.shape[:2]
        size = (w, h)

        # define angle to rotare
        angle = 15
        angle_rad = angle/180.0*np.pi

        # caluclate a size of pic after rotation
        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)

        # rotate 
        center = (w/2, h/2)
        scale = 1.0
        rotation_matrix = cv2.getRotationMatrix2D(center, angle, scale)

        # add 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(newFileName, img_rot)
        newFileName = './pos/pic' + str(imageNum) + '.jpg'
        saturation_img.save(newFileName)
        imageNum += 1
    print('*** Rotation by 15 deg. is finished *** \n')

    # Rotate by -15 deg.
    for num in range(1, 7* originalnum + 1):
        fileName = './pos/pic' + str(num) + '.jpg'
        if not os.path.exists(fileName):
            continue
        # read original file
        img = cv2.imread(fileName)
        h, w = img.shape[:2]
        size = (w, h)

        # define angle to rotare
        angle = -15
        angle_rad = angle/180.0*np.pi

        # caluclate a size of pic after rotation
        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)

        # rotate 
        center = (w/2, h/2)
        scale = 1.0
        rotation_matrix = cv2.getRotationMatrix2D(center, angle, scale)

        # add 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(newFileName, img_rot)
        newFileName = './pos/pic' + str(imageNum) + '.jpg'
        saturation_img.save(newFileName)
        imageNum += 1
    print('*** Rotation by -15 deg. is finished ***\n')

    print('*** Bulking out is completed ***\n')

def generatePosFile(cascade):
    """make text file of face positions in pictures 

    """
    fpos = open('pos.txt', 'a')
    for fileName in glob.glob('./pos/*.jpg'):
        img = cv2.imread(fileName)
        gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
        faces = cascade.detectMultiScale(gray)
        for (x,y,w,h) in faces:
            text =  fileName + ' 1 ' + str(x) + ' ' + str(y) + ' ' + str(w) + ' ' + str(h) + '\n'
            fpos.write(text)
    print('*** making pos.txt is finished ***')

# user_fileName = input('Please input your fileName\n')
cascade = cv2.CascadeClassifier('haarcascade_frontalface_default.xml')
picnum_max = input('please input a number of pictures to take\n')

# remove pos and files in pic/pos
removePic()

# start video and take pictures
takePic(cascade, int(picnum_max))

# count a number of picutres
bulkOut()

# make text file of face positions in pictures 
generatePosFile(cascade)

subprocess.call('opencv_createsamples -info pos.txt -vec pos.vec -num ' + str(countPic()), shell=True) 

posnum = input('please input a number of created pos\n')
subprocess.call('opencv_traincascade -data ./cascade -vec pos.vec -bg neg.txt -numPos ' + posnum + ' -numNeg 40', shell=True)
```

```face_id.py
# ======================================================================
# Project Name    : Face Identify 
# File Name       : face_id.py
# Encoding        : utf-8
# Creation Date   : 2021/02/22
# ======================================================================

import cv2

font = cv2.FONT_HERSHEY_SIMPLEX

if __name__ == "__main__":
    cap = cv2.VideoCapture(0)
    cascade_path_human = 'haarcascade_frontalface_default.xml'
    cascade_path = 'cascade.xml'

    cascade_human = cv2.CascadeClassifier(cascade_path_human)
    cascade = cv2.CascadeClassifier(cascade_path)

    while True:
        ret, frame = cap.read()

        facerect_human = cascade_human.detectMultiScale(frame, scaleFactor=1.7, minNeighbors=4, minSize=(100,100))
        facerect = cascade.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) > 0:
            for rect in facerect:
                cv2.rectangle(frame, tuple(rect[0:2]), tuple(rect[0:2]+rect[2:4]), (255, 0, 0), thickness=2)
                cv2.putText(frame, 'name', tuple(rect[0:2]), font, 2,(0,0,0),2,cv2.LINE_AA)


        cv2.imshow("frame", frame)

        # quit with q key
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break

    cap.release()
    cv2.destroyAllWindows()

```

以下のnameを自分の名前に変更すればOK。

```
cv2.putText(frame, 'name', tuple(rect[0:2]), font, 2,(0,0,0),2,cv2.LINE_AA)
```

#まとめ
撮った写真40枚程度 (傘増し後1200枚程度)で、精度はともかくそれっぽい認識はできていた。
便利なライブラリを用いたので機械学習については何も学べていない。