차곡차곡 성 쌓기
article thumbnail

프로젝트를 위해 처음 AI 모델을 사용해 본 4학년 학생이 쓴 글입니다! 지적과 피드백 언제나 환영합니다.🤗


 

얼굴 인식으로 인한 로그인 기능을 구현하기 위해선 학습 된 사람 중 어떤 사람인지 분류할 수 있어야한다.

SVM 모델은 학습된 데이터를 바탕으로 입력으로 들어온 데이터가 어떤 클래스에 속하는지 분류해준다. 따라서 이 글에서는 얼굴 인식을 위해 SVM 모델을 사용하였다.

 

SVM 모델에 데이터를 훈련시키기 위해서는 데이터 전처리 과정이 필요하다. 이때 사용한 것이 keras의 FaceNet 라이브러리였다. 

FaceNet 라이브러리는 160x160 크기의 얼굴 사진을 입력으로 받아 128개의 의미있는 임베딩 벡터를 생성해주는 전처리 모델이다.

 

지금부터 얼굴 Detection 부터 전처리, 얼굴 학습, 얼굴 인식 순으로 글을 작성한다.

 

1. Face Detecion

얼굴을 탐지하기 위해서는 사진에서 얼굴 부분만으로 탐지할 수 있는 모델을 사용해야 한다. 

 

얼굴 탐지 모델로 OpenCV에서 제공해주는 캐스케이드 분류기 중 `haarcascade_frontalface_default.xml` 모델을 사용하였다.  

(학습 된 모델은 https://github.com/opencv/opencv/tree/master/data/haarcascades 에서 다운 가능하다)

 

캐스케이드란 직역하면 직렬로 연결되어 있다는 것을 말한다. 모든 특징에 대해 사진을 테스트하는 것이 아닌 단계별로 테스트를 해서 얼굴 특징이 아닌 것을 거르는 방법을 이용한다. 즉 전체 데이터가 6000개라면 1단계에서 얼굴 특징이 아닌 것이 걸려져 3000개로, 2단계에선 1000개로 ... 등 여러 단계를 거쳐 얼굴이 맞는 부분만 알아낸다.이러한 캐스케이드 방식으로 빠르게 얼굴을 찾을 수 있다.

 

이러한 방식으로 이미 학습된 모델이 OpenCV에서 제공해 주는 것이다. 캐스케이드 분류기는 여러 모델이 있다. 나는 정면 얼굴만 검출하면 되므로, `haarcascade_frontalface_default.xml` 모델을 사용했다.

우선 분류기 객체를 생성한다. 

face_classifier = cv2.CascadeClassifier(
    cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')

 

나는 카메라에서 바로 사진을 촬영하여 실시간으로 얼굴을 탐지하였기 때문에 카메라 관련 코드를 추가해줬다.

 

1. 카메라 로드 

import cv2
import sys
import os
import os.path

osName = platform.system()
#windows
#cam = cv2.VideoCapture(0)
#Linux
cam = cv2.VideoCapture(cv2.CAP_V4L)

cam.set(cv2.CAP_PROP_FRAME_WIDTH, 500)
cam.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)

def onCam():
    global cam
    if (cam == None):
        cam = cv2.VideoCapture(cv2.CAP_V4L)
        cam.set(cv2.CAP_PROP_FRAME_WIDTH, 500)
        cam.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
            # #리눅스
            # #cam = cv2.VideoCapture(cv2.CAP_V4L2)
            # #윈도우
            # cam = cv2.VideoCapture(0)
        print('onCam 얼굴인식 : 카메라 켜짐')
    return True

 

2. 촬영

사용자가 촬영 버튼을 누르면 `createCropImage` 함수를 실행했다. 여기부터 카메라로부터 촬영된 이미지를 받아오고 `face_extactor` 함수를 통해 얼굴 부분을 감지하여 잘라진 이미지가 리턴된다. 얼굴 전처리 모델을 사용하기 위해서는 반드시 160x160 사이즈여야 하므로, `resize`함수를 통해 맞춰준다. 그 후 적절한 폴더에 해당 이미지를 저장한다. 

def createCropImage(userName, dir_path, countN):
    global cam
    dir_path = os.path.join(dir_path, userName)
    count = 0
    #폴더 생성
    if not (os.path.exists(dir_path)):
        os.mkdir(dir_path)
        print(dir_path + "폴더생성 완료")

    if(cam.isOpened()):
        while True:
            ret, frame = cam.read()
            if face_extractor(frame) is not None:
                count += 1
                face = cv2.resize(face_extractor(frame), (160, 160))
                face = cv2.cvtColor(face, cv2.COLOR_BGR2GRAY)
                file_name_path = str(count) + '.jpg'
                #크롭된 이미지 저장
                #face/login/user
                cv2.imwrite(dir_path + '/'+file_name_path, face)
            else: 
                print("Face not Found")
                pass

            if cv2.waitKey(1) == 13 or count == countN:
                break

        cv2.destroyAllWindows()
        return dir_path

 

3. 얼굴 탐지  

실제 캐스케이드 모델을 사용하여 얼굴 부분을 탐지하는 부분이다. 이전에 객체로 생성했던 face_classifier를 통해 얼굴 부분의 좌표를 알아낸다. x, y, w, h 를 알아낼 수 있으며, 이를 활용하여 이미지에서 해당 영역을 자른 후 자른 이미지를 리턴한다.

def face_extractor(img):

    if(img is None):
        print("img is None")
        return None
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    faces = face_classifier.detectMultiScale(gray, 1.3, 5)

    #찾는 얼굴이 없으면 None Return
    if faces == ():
        return None

    for (x, y, w, h) in faces:
        cropped_face = img[y:y+h, x:x+w]

    return cropped_face

 

이로써 촬영된 사진으로부터 얼굴을 감지하여 얼굴 부분을 잘라 저장하는 과정이 끝났다. 다음은 저장된 사진을 이용하여 임베딩 벡터를 생성하는 과정이다.

 

2. FaceNet을 통한 데이터 전처리

 keras FaceNet 라이브러리를 사용하여 전처리한다. FaceNet 라이브러리는 160x160 크기의 얼굴 사진을 입력으로 받아 128개의 의미있는 임베딩 벡터를 생성해주는 전처리 모델이다.

 

임베딩 모델인 face_keras.h5를 먼저 로드해준다. 파일이 커서 로드하는데 많은 시간이 걸리기 때문에 가급적 로드는 한 번만 하는 것이 좋다. 모델은 https://drive.google.com/drive/folders/1pwQ3H4aJ8a6yyJHZkTwtjcL4wYWQb7bn 에서 다운 받을 수 있다.

from keras.models import load_model
import shutil

embeddingModel = load_model('facenet_keras.h5')

 

1. 폴더에 저장된 이미지들 라벨링

먼저 라벨링 작업이 필요하다. 사진이 속해있는 폴더의 이름으로 라벨링 해준다. 폴더의 이름은 사용자를 식별할 수 있는 고유 ID로 지정하거나, 식별할 수 있는 이름으로 지정한다. 이 작업을 통해 x 배열에는 얼굴 사진으로 픽셀 값으로 바꾼 데이터가, y 배열에는 해당 얼굴 사진을 가진 사용자의 ID나 이름이 저장된다.

# 라벨링 작업
trainX, trainy = load_dataset(os.path.join('mirror',mirror_id,'train'))
# 이미지를 포함하는 각 클래스에 대해 하나의 하위 디렉토리가 포함된 데이터셋을 불러오기
def load_dataset(directory):
    X, y = list(), list()
    # 클래스별로 폴더 열기
    directory = directory + os.path.sep
    print('directory : ' + directory)
    #directory : face\login\
    for subdir in os.listdir(directory):
       # 경로
       path =os.path.join( directory , subdir ) + os.path.sep
       # path: face\login\user\
       print('path: ' + path)
       # 디렉토리에 있을 수 있는 파일을 건너뛰기(디렉토리가 아닌 파일)
       if not os.path.isdir(path):
          continue
       # 하위 디렉토리의 모든 얼굴 불러오기
       faces = load_faces(path)
       # 레이블 생성
       labels = [subdir for _ in range(len(faces))]
    
       # 진행 상황 요약
       print('>%d개의 예제를 불러왔습니다. 클래스명: %s' % (len(faces), subdir))
       # 저장
       X.extend(faces)
       y.extend(labels)
    return np.asarray(X), np.asarray(y)    

 

2. 단일 압축 포맷 형태로 저장

배열 형태로 받은 `trainX`, `trainY`데이터를 하나의 numpy 배열을 저장하기 위한 압축형태로 저장한다. 이 작업은 x와 y값을 동시에 효율적으로 전달하기 위해 필요한 작업이다. 수십개의 데이터를 따로 넘겨주면 파일 접근도 불편하고 코드가 깔끔하지 않아 이렇게 압축하고 한 개의 파일만 전달하도록 하였다. 

 # 라벨링 작업
trainX, trainy = load_dataset(os.path.join('mirror',mirror_id,'train'))
    
# 단일 압축 포맷 파일로 저장
folder_path = (os.path.join('mirror', mirror_id,'files')) + os.path.sep
dataset_file_name = 'trainface.npz'
numpy.savez_compressed( folder_path +os.path.sep+ dataset_file_name, trainX, trainy)

 

3. 임베딩

단일 압축한 데이터를 facenet 모델을 통해 임베딩 작업을 실행한다.

# 라벨링한 데이터를 임베딩 작업
newTrainX, trainy = embedding(embeddingModel, folder_path + 'trainface.npz')

 

현재 trainX에는 픽셀 값이, trainY에는 라벨 값이 들어있는 상태이다.

`get_embedding`를 통해 픽셀 값을 임베딩 벡터로 변환한 데이터를 알아낸다. newTrainX 배열에 저장 후 배열을 리턴한다.

def embedding(embeddingModel, file_dir):

    # 얼굴 데이터셋 불러오기
    data = load(file_dir)
    trainX, trainy= data['arr_0'], data['arr_1']
    
    # 훈련 셋에서 각 얼굴을 임베딩으로 변환하기
    newTrainX = list()
    for face_pixels in trainX:
       embedding = get_embedding(embeddingModel, face_pixels)
       newTrainX.append(embedding)
    newTrainX = asarray(newTrainX)

    return newTrainX, trainy

 

픽셀 값을 통해 임베딩 벡터를 알아낸다.

  1. face_pixels은 입력으로 주어진 얼굴 이미지의 픽셀값이다. 
  2. face_pixels를 정수형으로 변환 한다(astype('int32'))
  3. 모델이 일관된 입력을 받을 수 있도록 이미지의 픽셀값을 픽셀값의 평균과 표준편차를 사용하여 정규화한다.
  4. 임베딩을 얻기 위해 이미지를 모델에 입력 가능한 형태로 변환합니다. 일반적으로 딥러닝 모델은 여러 이미지를 함께 처리할 수 있으므로, expand_dims를 사용하여 이미지를 하나의 샘플로 변환합니다.
  5. 변환된 이미지를 모델에 입력하여 임베딩 벡터를 얻는다.
# 하나의 얼굴의 얼굴 임베딩 얻기
def get_embedding(model, face_pixels):
    	# 픽셀 값의 척도
        face_pixels = face_pixels.astype('int32')
        # 채널 간 픽셀값 표준화(전역에 걸쳐)
        mean, std = face_pixels.mean(), face_pixels.std()
        face_pixels = (face_pixels - mean) / std
        # 얼굴을 하나의 샘플로 변환
        samples = expand_dims(face_pixels, axis=0)
        # 임베딩을 갖기 위한 예측 생성
        yhat = model.predict(samples)        
        return yhat[0]

 

4. 임베딩 한 데이터 단일 압축 포맷으로 저장

최종으로 (임베딩 백터 값 , 라벨) 형태로 단일 압축 파일을 만든다. 이 파일을 가지고 모델 훈련과 인식을 실행한다.

# 라벨링한 데이터를 임베딩 작업
newTrainX, trainy = embedding(embeddingModel, folder_path + 'trainface.npz')

# 배열을 하나의 압축 포맷 파일로 저장
numpy.savez_compressed(folder_path + os.path.sep+'trainfaces-embeddings.npz', newTrainX, trainy )

 

3. SVM 모델로 얼굴 학습

좀 전에 저장한 (임베딩 벡터 값, 라벨) 형태의 데이터를 인자로 넘겨주어 학습을 진행한다.

# 모델 학습
model_fit(folder_path +os.path.sep+ 'trainfaces-embeddings.npz',mirror_id)

 

sklearn.svm 패키지에 있는 SVC를 import하여 SVM 모델을  사용하여 학습 데이터를 적합시킨다. 

trainX는 임베딩 벡터 값을, trainY는 라벨 값을 넣어 `model.fit(trainX, trainy)`를 실행한다. 학습된 모델은 후에 얼굴인식을 할 때 사용해야 하므로, `joblib` 라이브러리를 사용하여 저장한다. 

from numpy import load
from numpy import expand_dims
from sklearn.preprocessing import LabelEncoder
from sklearn.preprocessing import Normalizer
from sklearn.svm import SVC
from random import choice
import joblib
from sklearn.metrics import accuracy_score
import os
import os.path

# 얼굴 학습
def model_fit(embedding_file_name, mirror_id):

    # 얼굴 임베딩 불러오기
    data = load(embedding_file_name)
    trainX_faces =data['arr_0']
    trainX, trainy = data['arr_0'], data['arr_1']
    
    # 입력 벡터 일반화
    in_encoder = Normalizer(norm='l2')
    trainX = in_encoder.transform(trainX)
    
    # 목표 레이블 암호화
    out_encoder = LabelEncoder()
    out_encoder.fit(trainy)
    trainy = out_encoder.transform(trainy)

    #만들어진 모델이 없다면 새롭게 만든다
    model_file = os.path.join('mirror', mirror_id,'files','model.pkl')
    # 모델 적합
    model = SVC(kernel='linear', probability=True)
    model.fit(trainX, trainy)
    #모델 저장
    joblib.dump(model, model_file)

    # 추측
    yhat_train = model.predict(trainX)
    # 정확도 점수
    score_train = accuracy_score(trainy, yhat_train)
    # 요약
    print('정확도: 훈련=%.3f' % (score_train*100))

 

 

4. 얼굴 인식

`embeddingModel`에는 face_keras.h5를 로드한 객체를 넣어주면 된다(2번 참고).

def login(embeddingModel, mirror_id):
    
    # 현재 파일의 디렉토리 경로. 작업 파일 기준
    curDir = os.path.dirname(os.path.realpath(__file__))
    os.chdir(curDir)
    
    # 훈련 데이터셋 불러오기
    trainX, trainy = load_dataset(os.path.join('mirror',mirror_id,'login'))
    # 배열을 단일 압축 포맷 파일로 저장
    savez_compressed(os.path.join('mirror',mirror_id, 'files','loginface.npz'), trainX, trainy)
    print(trainX.shape, trainy.shape)

    newTrainX, trainy = embedding(embeddingModel,os.path.join('mirror',mirror_id, 'files','loginface.npz'))
    # 배열을 하나의 압축 포맷 파일로 저장
    savez_compressed(os.path.join('mirror',mirror_id, 'files','login-embeddings.npz'), newTrainX, trainy )
    user = user_check(os.path.join('mirror',mirror_id, 'files','login-embeddings.npz'),mirror_id)
    if(user):
        return user     
    else :
        return 'NULL'
def user_check(embedding_file_name, mirror_id):
    pre_embedding_file= load(os.path.join('mirror', mirror_id, 'files','trainfaces-embeddings.npz'))
    label_y = pre_embedding_file['arr_1']
    # 얼굴 임베딩 파일 불러오기
    data = load(embedding_file_name)
    trainX = data['arr_0']
    # 입력 벡터 일반화
    in_encoder = Normalizer(norm='l2')
    trainX = in_encoder.transform(trainX)
    # 목표 레이블 암호화
    out_encoder = LabelEncoder()
    out_encoder.fit(label_y)

    # 모델 파일명 로드
    model_file = os.path.join('mirror',mirror_id,'files','model.pkl')
    if not os.path.isfile(model_file):
        print('모델이 없습니다')
        return

    model = joblib.load(model_file)
    count =[0 for i in range(100)]
    # 테스트 데이터셋에서 임의의 예제에 대한 테스트 모델
    for i in range(10):
        selection = i
        random_face_emb = trainX[selection]
        #얼굴 예측
        sample = expand_dims(random_face_emb, axis=0)
        yhat_class = model.predict(sample)
        yhat_prob = model.predict_proba(sample)

        class_index = yhat_class[0]
        class_probability = yhat_prob[0,class_index] * 100
        predict_names = out_encoder.inverse_transform(yhat_class)
        print('예상: %s (%.3f)' % (predict_names[0], class_probability))
        if (class_probability> 60):      
            count[class_index]  =  count[class_index] + 1

    # 같은 유저로 50%넘는 확률로 5번 이상 인식 한다면 해당 유저로 판별
    if(max(count) > 5):
        print(max(count))
        maxIndex = count.index(max(count))
        # 이름 얻기
        class_index = yhat_class[0]
        #class_probability = yhat_prob[0,maxIndex] * 100
        class_list = [maxIndex]
        #print(class_list.shape)      
        predict_names = out_encoder.inverse_transform(class_list)
        print('사용자: %s ' % (predict_names[0])) 
        return  predict_names[0]
    else:
        print(max(count))
        
        return 0
728x90
profile

차곡차곡 성 쌓기

@nagrang

포스팅이 좋았다면 "좋아요" 해주세요!