본문 바로가기
A.I./구현

간단한 얼굴 인식 웹 애플리케이션을 만들어보자.

by 채소장사 2021. 10. 28.

 이 글은 How to Develop a Face Recognition System Using FaceNet in Keras 를 참고하여, 업로드한 이미지에서 얼굴을 인식하는 간단한 웹 애플리케이션을 구현한다. 얼굴 인식(Face Recognition)은 이미지나 영상 안의 인물이 누구인지를 판별해주는 작업으로서, 데이터베이스나 시스템에는 미리 판별 가능한 인물에 대한 정보가 필요하다. 이번 작업에서는 러블리즈 멤버들에 대한 판별기를 학습시킨 뒤에, 인식모델을 서빙하는 API와 이를 활용하는 웹 페이지를 만든다.

▶ 실행 코드 : [hayunjong83/lovelyzDetector]

 

1. MTCNN을 사용한 Face Detection

 우선, 이미지에서 얼굴 영역을 찾아야 한다. 앞으로 우리는 FaceNet 모델을 통해서 얼굴 영역을 임베딩벡터로 변환할 것이고, 이 임베딩벡터를 사람별로 분류하는 분류기(classifier)를 학습시킬 것이다. FaceNet 모델은 일정한 크기의 입력을 받아서(160 x 160), 128차원의 임베딩 벡터를 출력한다.

 최초의 전체이미지 크기가 다양하더라도 상대적으로 비슷한 크기와 비율의 얼굴 부분을 FaceNet의 입력으로 사용하고 싶다. 따라서 파이썬 패키지로 손쉽게 사용할 수 있는 MTCNN 모델을 사용해서 얼굴을 탐색하고, PIL 라이브러리의 resize 메소드를 통해 (160 x 160) 크기의 입력으로 바꿔준다.

from mtcnn import mtcnn
import PIL
import numpy as np

detector = mtcnn.MTCNN()
image = PIL.Image.open('kei.jpg').convert('RGB')
pixel = np.asarray(image)
result = detector.detect_faces(pixel)

x, y, w, h = result[0]['box']
face = pixel[y:y+h, x:x+w]
face_image = PIL.Image.fromarray(face)
face_image_resize = face_image.resize((160,160))
face_image_resize.save('kei_face.jpg')

더보기

MTCNN detector가 제공하는 정보는 사실 이보다 다양하다.

  • box : 탐색된 얼굴영역의 왼쪽 위 시작점 좌표(x, y)와 너비(w) 및 높이(h)
  • confidence : 제대로 얼굴영역을 탐색했는 지에 대한 확률
  • keypoints : (왼쪽 눈, 오른쪽 눈, 코, 입의 왼쪽 끝, 입의 오른쪽 끝)에 대한 좌표

2. FaceNet을 통해 임베딩 벡터로 변환

  파악한 얼굴 영역을 임베딩 벡터로 변환한 다음, 이로부터 어떤 인물인지를 인식하는 통합된 FaceNet 모델도 물론 가능할 것이다. 하지만 이 경우, 최종 classifier layer를 학습하는 fine-tuning 과정이 필요할 것이고, 무엇보다도 inference 시에 무거운 모델 사이즈로 인하여 속도가 느릴 것이다. 

 이 때문에 여기에서는 참고한 글과 같이 MS-Celeb-1M 데이터셋에서 미리 학습된 FaceNet 모델을 변경없이 로드하여 임베딩 벡터로 단순히 변환하는 전처리 단계로 이용한다. 이 때 사용한 FaceNet모델의 설정에 맞게, 입력 얼굴 이미지를 이미지 전체의 픽셀값의 평균과 표준편차로 표준화(standardization)해주고, 배치 차원으로 한 차원 증가 시켜줘야 한다.

from tensorflow import keras

face_pixels = np.asarray(face_image_resize).astype('float32')
m, s = face_pixels.mean(), face_pixels.std()

face_pixels = (face-pixels - m)/s
inupt = np.expand_dims(face_pixels, axis=0)

facenet = keras.models.load_model('facenet_keras.h5')
face_embedding = facenet.predict(input)[0]

3. 임베딩 벡터를 분류하는 선형 SVM 모델 훈련

 일단 얼굴 이미지 데이터가 임베딩 벡터로 변환되었으면, 벡터들을 클래스별로 분류하는 간단한 선형 SVM을 훈련시킨다. 이 과정에서 각 임베딩벡터를 단위벡터로 변환하는 Normalizer와 레이블을 원핫인코딩 해주는 LabelEncoder를 사용하는 전처리과정을 추가로 거친다. Numpy 배열로 표현된 훈련 데이터인 임베딩벡터 trainX와 각각의 레이블 trainy가 있을 때, scikit-learn의 메소드를 통해 간단하게 수행이 가능하다.

from sklearn import preprocessing, svm

in_encoder = preprocessing.Normalize(norm='l2')
trainX = in_encoder.transform(trainX)

out_encoder = preprocessing.LabelEncoder()
out_encoder.fit(trainy)

classifier = svm.SVC(kernel='linear', probability=True)
classifier.fit(trainX, trainy)

4. 새로운 데이터에 대한 예측결과를 반환하는 flask api

 이렇게 훈련된 SVM 모델파일을 저장한 뒤에, POST방식으로 전송된 이미지에서 ①얼굴영역을 파악하고, ② FaceNet모델을 로드하여 임베딩벡터로 변환한 뒤, ③ 저장된 classifier 모델로서 예측을 수행하여 그 결과를 반환하는 api를 만들어준다. 

 이번 글에서는 웹 애플리케이션이 프론트엔드 부분을 React 라이브러리로 구현하였는데, 리액트 컴포넌트가 손쉽게 활용하도록 API의 최종결과는 flask의 jsonify() 함수를 이용해 json 데이터로 전송한다. 이 때, 예측결과의 레이블, 예측 확률, 얼굴영역이 그려진 결과 이미지의 3가지 결과 데이터를 각각 label, prob, retImg 라는 키 값에 대응되도록 하였다.

 참고로 api에서 전역으로 로드하는 세 가지 파일은 FaceNet모델, 학습된 SVM classifier, classifier가 예측한 원핫벡터를 최초의 문자열 레이블로 바꿔주기 위한 LabelEncoder 다.

model = keras.models.load_model('facenet_keras.h5')
classifier = joblib.load('lovelyz_classifier.pkl')
out_encoder = joblib.load('lovelyz_labeler.pkl')

app = Flask(__name__)
@app.route('/lovelyz', method=['POST']
def predict():
	if request.method == 'POST':
    	file = requests.files.get('file', '')
        img_bytes = file.read()
        img = io.BytesIO(img_bytes)
        
        # 얼굴 영역 추출
        face = extract_face(img)
        # 임베딩벡터로 변환
        embedding = get_embedding(model, face)
        
        # classifier입력을 위한 전처리
        embedding = preprocess_something(embedding)
        
        # 결과
        output = dict()
        yhat = classifier.predict(embedding)
        output['label'] = out_encoder.inverse_transform(yhat)[0]
        
        output['prob'] = classifier.predict_prob(embedding)
        
        ret_img = draw_result(img)
        ret_bytes = io.BytesIO()
        ret_img.save(ret_bytes, format='PNG')
        output['retImg'] = base64.encodebytes(ret_bytes.getvalue()).decode('ascii')
        
        return jsonify(output)

위의 코드 내용은 api 전체 흐름을 간략하게 설명한 것으로 실제 구현과는 차이가 있다. 자세한 것은 실행코드를 참고한다.

5. React.js를 사용해 웹 페이지 구현

 이번 글에서는 웹 페이지 구현에 관해서는 크게 다루지 않으려 한다. 구현의 완성도나 효율성 보다는 딥러닝/머신러닝 모델을 활용하여 애플리케이션을 완성하는 전체 과정을 간단히 설명하는 것이 목표이기도 하였고, 무엇보다 프론트엔드를 설명하기에는 존재하는 센스가 하나도 없기 때문이다.

 간략한 내용만 정리해보자면, 테스트할 이미지를 업로드하고 api로부터 결과를 받아와 출력하는 과정은 리액트에서 단일 컴포넌트로 구성하였고, api와의 통신은 fetch 메소드를 사용하였다. api의 url은 flask에서 설정한 /lovelyz 로 설정하였고, 개인적으로는 단일페이지에서 여러 api를 테스트할 때가 많아서, 프록시설정은 여러 개의 프록시 설정을 손쉽게 도와주는 http-proxy-middleware 패키지를 사용하였다.

 


2024년 5월 30일 추가)

오래전 토이 프로젝트임에도 관심과 조언을 해주신 많은 분들에게 깊은 감사드립니다.

프로젝트를 올리던 당시에는 간단한 프로젝트이지만, 저작권 있는 사진을 사용하였기 때문에, 따로 SVM classifier 파일을 올리지는 않았습니다. 간단하게 테스트 해보시려는 분들을 위하여, 연관 파일인 lovelyz_classifier.pkl 파일과 lovelyz_labeler.pkl 파일을 업로드 합니다.

lovelyz_classifier.pkl
0.17MB
lovelyz_labeler.pkl
0.00MB

 

facenet_keras.h5 파일과 함께 src 폴더에 저장하여 사용하시면, 간단히 실행이 가능합니다.

 

이 당시 경험도 많이 부족하였지만, 더불어 프로젝트를 진행할 자원이 많이 없었습니다. 그래서 위에 말씀드린 것처럼 얼굴 인식 신경망 모델을 바로 사용한 후 유사한 임베딩을 찾는 방식 대신, SVM 분류기를 추가로 사용했던 것 같습니다. 

2015년 FaceNet 이후로 많은 모델들이 얼굴 인식(face recognition)과 사람 안면 인식(face identification)에서 발표되었습니다. Face Identification 모델 비교에서 볼 수 있듯이, 이제는 FaceNet보다 훨씬 좋은 성능의 모델이 경량화 되어 사용이 가능해졌습니다. 더불어 실행하는 하드웨어의 성능도 더 좋아졌기 때문에, 이제는 추가적인 분류기를 사용하는 것보다 모델에서 출력되는 임베딩을 그대로 사용하는 방식이 더 좋다고 생각합니다. 

이 경우 데이터를 추가할 때마다 분류기를 학습해야할 필요가 없습니다. 최근에 언어모델 사용에 힘입어 빠르게 사용이 증가되는 벡터 데이터베이스에 이런 임베딩을 저장시켜놓으면, 저장된 모든 임베딩과 비교하는 과정없이 임베딩 공간에서 유사도가 높은 결과값을 빠르게 제공해줍니다. 빠른 시간 안에 이러한 방법을 활용하는 예제 프로젝트를 포스팅 해보겠습니다(참고 - Large Scale Face Recognition with Pinecone Vector Database)

 질문 주셨던 몇몇 분들 덕분에 다시금 잘못된 부분들을 찾아보고 생각해 볼 수 있는 시간이 되었습니다. 덧붙이는 글을 통하여 깊은 감사의 말씀 한번 더 드리겠습니다.

댓글