# -*- coding: utf-8 -*-
##------------------------------------------
## Face recognition Program step2 Ver 0.02
## platform: linux / windows
##
## 2022.07.05 Masahiro Izutsu
##------------------------------------------
## face_rec2.py
## ver 0.02 2022.07.14 ソース修正
# import処理
import cv2
import numpy as np
import face_recognition
import cvui
import os
import argparse
import my_logging
from my_print import printG, printR, printY, printC, printB, printN
import my_puttext
import my_csv
import my_file
import my_winstat
import my_fps
import my_datetime
import my_movedlg
import my_dialog
# 定数定義
title = 'Face recognition (step2) Ver 0.02'
IMAGE_EXT = {'.bmp', '.png', '.jpeg', '.jpg', '.tif'}
FACE_FOLDER = 'face_img' # 登録画像フォルダパス
FACE_LIST = 'facelist.txt' # 登録リストファイル
ATTEND_LOG = 'attend.csv' # 記録ファイル名
ATTEND_COUNT = 10 # 同一認識フレーム数
ATTEND_DSEC = 10 # 記録制限秒数
STATUS_H = 40 # ステータスラインの高さ
STATUS_LV = 200 # ステータスラインの背景レベル
WINDOW_WIDTH = 640 # 表示ウインドウの幅 (静止画)
INPUT_IMAGE_DEF = 'cam' # 入力イメージ・パラメータ初期値
#INPUT_IMAGE_DEF = os.path.expanduser('./face_test/face_test1.mp4')
#INPUT_IMAGE_DEF = os.path.expanduser('./face_test/face_test.png')
# 入力パラメータの定義
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument('-i', '--input', metavar = 'INPUT_IMAGE', type=str, default = INPUT_IMAGE_DEF,
help = 'Absolute path to image file or cam for camera stream.'
'Default value is \'cam\'')
parser.add_argument('-f', '--face', metavar = 'FACE_FOLDER', type=str, default = FACE_FOLDER,
help = 'Absolute path to image file or cam for camera stream.'
'Default value is \'' + FACE_FOLDER +'\'')
parser.add_argument('--tol', metavar = 'TOLERANCE', default = '0.6',
help = 'Strict the comparison parameter (0<<1) Default value is \'0.6\'')
parser.add_argument('--log', metavar = 'LOG', default = '3',
help = 'Log level(-1/0/1/2/3/4/5) Default value is \'3\'')
parser.add_argument('-t', '--title', metavar = 'TITLE', default = 'y',
help = 'Program title flag.(y/n) Default value is \'y\'')
parser.add_argument('-s', '--speed', metavar = 'SPEED', default = 'y',
help = 'Speed display flag.(y/n) Default calue is \'y\'')
parser.add_argument('-o', '--out', metavar = 'IMAGE_OUT', default = 'non',
help = 'Processed image file path. Default value is \'non\'')
return parser
# 入力パラメータの表示
def display_info(input_str, facefol, toler, log, titleflg, speedflg, outpath):
printG(f' - Program title : {title}')
printG(f' - OpenCV version : {cv2.__version__}')
printY(f' - Input image : {input_str}')
printY(f' - Face Folder : {facefol}')
printY(f' - tolerance : {toler}')
printY(f' - Log level : {log}')
printY(f' - Program Title : {titleflg}')
printY(f' - Speed flag : {speedflg}')
printY(f' - Processed out : {outpath}')
# 128次元の顔エンコーディングのリスト
def find_encodings(images):
encode_list = []
for img in images:
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
encode = face_recognition.face_encodings(img)[0]
encode_list.append(encode)
return encode_list
# 顔検出記録
def mark_attendance(attend_logfile, name):
result = False
encoding = 'utf_8_sig'
key = 'Name'
attime ='Time'
my_csv_treatment = my_csv.CSVtreatment(attend_logfile, encoding)
s = my_csv_treatment.read_csv()
if len(s) == 0: # 新規ファイル作成
data = [key, attime]
my_csv_treatment.write_csv(data)
time_now = my_datetime.now_strftime('%Y/%m/%d %H:%M:%S')
time = my_csv_treatment.read_csv_row_value(key, name, attime)
if time is None: # 記録なし
result = True
else: # 記録あり
result = my_datetime.time_check(time, time_now, ATTEND_DSEC)
if result:
data = [name, time_now]
my_csv_treatment.append_csv(data)
return result
# 顔登録
# in: facefol 顔登録フォルダ
# img 登録する顔のイメージ
# my_csv_treatment CSV リストファイルのオブジェクト
# logger ログ出力オブジェクト
# out: 登録フラグ
#
def face_registration(facefol, img, my_csv_treatment, logger):
befor, after = my_csv_treatment.read_list2_csv()
file_path = my_datetime.now_strftime('%Y%m%d_%H%M%S') + '.jpg'
retf = True
savef = False
ttle = f"'{file_path}'の登録"
msg = '登録する名前を入力してください。'
msg_r = ''
while(retf):
file_name = my_movedlg.input_dialog(ttle, msg)
if file_name == None or len(file_name) == 0: # [X] ボタン キャンセル
msg_r = 'キャンセル'
retf = False # リトライ・ループ終了
break
# 登録名が入力された場合
retf = False # リトライ・ループ終了
row_data = [file_path, file_name] # 登録データ
if len(befor) <= 0:
msg_r = '初回登録'
my_csv_treatment.write_csv(row_data)
savef = True
else:
regf = False
for f in after:
if f == file_name:
regf = True
break
if regf:
if my_movedlg.yesno_dialog('TEST', '登録済みです。入れ替えますか?'):
msg_r = '入れ替え'
for n in range(len(after)):
if after[n] == file_name:
os.remove(f'{facefol}/{befor[n]}')
befor[n] = file_path
my_csv_treatment.write_list2_csv(befor, after)
savef = True
else:
msg_r = '再設定'
retf = True
else:
msg_r = '追加登録'
befor.insert(0, file_path)
after.insert(0, file_name)
my_csv_treatment.write_list2_csv(befor, after)
savef = True
if savef:
cv2.imwrite(f'{facefol}/{file_path}', img)
logger.debug(f'{msg_r}: {file_path} → {file_name}')
return savef
# 登録画像のリストアップ
# in: facefol 顔登録フォルダ
# my_file_treatment ファイル処理のオブジェクト
# my_csv_treatment CSV リストファイルのオブジェクト
# logger ログ出力オブジェクト
# out: face_names 登録された顔の名前(日本語)
# encode_list_known 登録された顔のエンコード・リスト
#
def get_encode_list(facefol, my_file_treatment, my_csv_treatment, logger):
face_images = [] # 登録画像のイメージリスト
face_names = [] # 登録画像の名前リスト(日本語名)
facelist = my_file_treatment.get_file_list_sel(f'{facefol}/*', IMAGE_EXT)
fname, jname = my_csv_treatment.read_list2_csv()
for reg_face in facelist:
current_img = cv2.imread(reg_face)
face_images.append(current_img)
logger.debug(f"'{reg_face}' read <{facelist is not None}>")
f = os.path.basename(reg_face)
s = os.path.splitext(f)[0]
# 日本語の登録名を設定する
n = len(fname)
if n > 0:
for i in range(n):
if f == fname[i]:
s = jname[i]
break
face_names.append(s)
logger.debug(face_names)
encode_list_known = find_encodings(face_images)
logger.debug(f'Find {len(encode_list_known)} face registration images !!')
return face_names, encode_list_known
# ** main関数 **
def main():
# 日本語フォント指定
fontFace = my_puttext.get_font()
# 入力パラメータの処理
ARGS = parse_args().parse_args()
input_str = ARGS.input
facefol = ARGS.face
toler = ARGS.tol
log = ARGS.log
logsel = int(log)
titleflg = ARGS.title
speedflg = ARGS.speed
outpath = ARGS.out
# ファイル処理クラスのインスタンス
my_file_treatment = my_file.FileTreatment(logsel)
my_csv_treatment = my_csv.CSVtreatment(f'{facefol}/{FACE_LIST}', 'utf_8_sig', logsel)
if input_str == '0':
input_str = my_dialog.select_image_movie_file()
if len(input_str) <= 0:
printR('Program canceled.')
return 0
input_stream = input_str
if input_str.lower() == "cam" or input_str.lower() == "camera":
input_stream = 0
isstream = True
else:
filetype = my_file_treatment.is_pict(input_stream)
isstream = filetype == 'None'
if (filetype == 'NotFound'):
printR(f"'{input_stream}': Input file Not found.")
return 0
if isstream and outpath != 'non' and os.path.splitext(outpath)[1] != '.mp4':
printR(f"'{outpath}': Output file type is '.mp4'")
return 0
# 記録ファイル名
s = input_str if input_stream == 0 else os.path.basename(input_str)
s = s.replace('.','_')
attend_logfile = f'{s}_{ATTEND_LOG}'
# アプリケーション・ログ設定
module = os.path.basename(__file__)
module_name = os.path.splitext(module)[0]
logger = my_logging.get_module_logger_sel(module_name, logsel)
logger.info('Starting..')
# 情報表示
display_info(input_str, facefol, toler, log, titleflg, speedflg, outpath)
# 登録画像のリストアップ
face_names, encode_list_known = get_encode_list(facefol, my_file_treatment, my_csv_treatment, logger)
window_name = title + " (hit 'q' or 'esc' key to exit)"
window_cap = 'Capture window'
match_name = '' # 一致した名前
match_count = 0 # 連続で一致した回数
facerec_flg = False # 顔検出フラグ
save_flg = False # 登録フラグ
frame_cap_still = None # 静止画フレーム
# 入力準備
if (isstream):
# カメラ
cap = cv2.VideoCapture(input_stream)
success, img_frame = cap.read()
loopflg = cap.isOpened()
img_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
img_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
else:
# 画像ファイル読み込み
img_frame = cv2.imread(input_stream)
if img_frame is None:
logger.error(f'unable to read the input.')
return 0
# アスペクト比を固定してリサイズ
img_h, img_w = img_frame.shape[:2]
if (img_w > WINDOW_WIDTH):
height = round(img_h * (WINDOW_WIDTH / img_w))
img_frame = cv2.resize(img_frame, dsize = (WINDOW_WIDTH, height))
loopflg = True # 1回ループ
img_width = img_frame.shape[1]
img_height = img_frame.shape[0]
btn_x = 40 # 'Save'ボタン位置 X
btn_y = img_height + 6 # Y
btn_w = 70 # ボタン 幅
btn_h = 32 # 高さ
# 画面ステータス領域 (画面下部 STATUS_H pixel)の確保
frame = np.zeros((img_height + STATUS_H, img_width, 3), np.uint8)
frame[:,:,:] = STATUS_LV # 背景色 (200, 200, 200)
# ウインドウ初期設定
cv2.namedWindow(window_name, flags=cv2.WINDOW_AUTOSIZE | cv2.WINDOW_GUI_NORMAL)
cv2.moveWindow(window_name, 20, 0)
cvui.init(window_name)
# 処理結果の記録 step1
if (outpath != 'non'):
if (isstream):
fps = int(cap.get(cv2.CAP_PROP_FPS))
fourcc = cv2.VideoWriter_fourcc('m', 'p', '4', 'v')
outvideo = cv2.VideoWriter(outpath, fourcc, fps, (img_width, img_height))
# 計測値初期化
fpsWithTick = my_fps.fpsWithTick()
frame_count = 0
fps_total = 0
fpsWithTick.get() # fps計測開始
# メインループ
while (loopflg):
if img_frame is None:
logger.error(f'unable to read the input.')
return 0
frame_cap = img_frame.copy() # 現在のフレームを保存
img_resize = cv2.resize(img_frame, (0, 0), None, 0.25, 0.25)
img_resize = cv2.cvtColor(img_resize, cv2.COLOR_BGR2RGB)
face_frame = face_recognition.face_locations(img_resize)
encode_frame = face_recognition.face_encodings(img_resize, face_frame)
# 画面上部に画像を配置
frame[0:img_height,:] = img_frame
frame[img_height:,:,:] = STATUS_LV
# カメラフレーム内の顔検出
n = len(face_frame)
c = ( 0,102, 51) if n > 0 else (71, 99, 255)
s = f'face:{n:2}'
cv2.rectangle(frame, (10, 42), (68, 59), c, -1)
cv2.putText(frame, s, (15, 54), cv2.FONT_HERSHEY_DUPLEX, fontScale=0.4, color=(255, 255, 255), lineType=cv2.LINE_AA)
for encode_face, face_loc in zip(encode_frame, face_frame):
# フレーム内に顔が検出された場合
matches = face_recognition.compare_faces(encode_list_known, encode_face, tolerance=float(toler))
face_dis = face_recognition.face_distance(encode_list_known, encode_face)
logger.debug(f'count: {match_count:03} distance: {face_dis}')
match_idx = np.argmin(face_dis)
if matches[match_idx]:
name = face_names[match_idx].upper()
color = (102, 102, 0)
if match_name == '':
match_name = name
if match_count < 500:
match_count = match_count + 1
if match_count == ATTEND_COUNT:
result = mark_attendance(attend_logfile, name) # 名前登録
logger.debug(f'Mark attendancs !! {result}')
else:
name = '未登録'
color = (71, 99, 255)
match_name = ''
match_count = 0
y1, x2, y2, x1 = face_loc
y1, x2, y2, x1 = y1*4, x2*4, y2*4, x1*4
cv2.rectangle(frame, (x1, y1-30), (x2, y1), color, cv2.FILLED)
cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2)
my_puttext.cv2_putText(frame, name, (x1+6, y1-6), fontFace, 16, (255, 255, 255), 0)
facerec_flg = True
if len(face_frame) <= 0:
# フレーム内に顔が検出できない場合
logger.debug("Face can't be detected !!")
match_name = ''
match_count = 0
facerec_flg = False
# FPSを計算する
fps = fpsWithTick.get()
st_fps = 'fps: {:>6.2f}'.format(fps)
if (speedflg == 'y'):
cv2.rectangle(frame, (80, 42), (165, 59), (90, 90, 90), -1)
cv2.putText(frame, st_fps, (85, 54), cv2.FONT_HERSHEY_DUPLEX, fontScale=0.4, color=(255, 255, 255), lineType=cv2.LINE_AA)
# タイトル描画
if (titleflg == 'y'):
cv2.putText(frame, title, (12, 32), cv2.FONT_HERSHEY_DUPLEX, fontScale=0.8, color=(0, 0, 0), lineType=cv2.LINE_AA)
cv2.putText(frame, title, (10, 30), cv2.FONT_HERSHEY_DUPLEX, fontScale=0.8, color=(200, 200, 0), lineType=cv2.LINE_AA)
# -----------------------------------------
# ステータスライン・イベント処理
if (isstream):
# 'Save' ボタン・イベント処理
if facerec_flg:
if cvui.button(frame, btn_x, btn_y, "&Save"):
logger.debug("'Save' button pushed !!")
frame_cap_still = frame_cap.copy() # 静止画フレーム
cv2.namedWindow(window_cap, flags=cv2.WINDOW_AUTOSIZE | cv2.WINDOW_GUI_NORMAL)
cv2.moveWindow(window_cap, img_width + 100, 0)
cv2.imshow(window_cap, frame_cap_still)
save_flg = True
# 'Name' ボタン・イベント処理
if save_flg:
if cvui.button(frame, btn_x + btn_w + 10, btn_y, "&Name"):
logger.debug("'Name' button pushed !!")
if face_registration(facefol, frame_cap_still, my_csv_treatment, logger): # 顔登録
# 登録画像の再リストアップ
face_names, encode_list_known = get_encode_list(facefol, my_file_treatment, my_csv_treatment, logger)
cv2.destroyWindow(window_cap)
save_flg = False
# -----------------------------------------
# ウインドウの更新/画像表示
cvui.update()
cv2.imshow(window_name, frame)
# 処理結果の記録 step2
if (outpath != 'non'):
if (isstream):
outvideo.write(frame)
else:
cv2.imwrite(outpath, frame)
# 何らかのキーが押されたら終了
breakflg = False
while(True):
key = cv2.waitKey(10) # (カメラ・アクセス速度調整)
if key == 27 or key == 113: # 'esc' or 'q'
breakflg = True
break
if not my_winstat._is_visible(window_name): # 'Close' button
breakflg = True
break
if (isstream):
break
if ((breakflg == False) and isstream):
# 次のフレームを読み出す
success, img_frame = cap.read()
if success == False:
break
loopflg = cap.isOpened()
else:
loopflg = False
# 終了処理
if (isstream):
cap.release()
# 処理結果の記録 step3
if (outpath != 'non'):
if (isstream):
outvideo.release()
cv2.destroyAllWindows()
printC('FPS average: {:>10.2f}'.format(fpsWithTick.get_average()))
logger.info('Finished.\n')
# main関数エントリーポイント(実行開始)
if __name__ == "__main__":
main()