실시간 얼굴 인식 프로그램 - 1차 구현 : 따라해보기

흐짜짜! 🫒 올리브·2021년 1월 9일
4

이 블로그를 참고하여 구현하였다.

Python, OpenCV


얼굴 인식 프로그램은 전체적으로,

  1. 인식할 얼굴의 data set 만들기
  2. data set을 이용해 얼굴을 학습시키기
  3. 학습 시킨 것을 바탕으로 현재 얼굴이 누구인지 알아내기

의 과정을 거친다.
하나씩 차근차근 알아보자.

1. 인식할 얼굴의 data set 만들기


얼굴의 data set을 만들기 위해서는,

  1. 먼저 이미지에서 얼굴을 검출할 줄 알아야 한다.
    이는, 영상처리 프레임워크 OpenCV에서 제공하는 Haar Cascade Classifier를 통해 구현할 수 있다.

  2. 그리고, 얼굴을 파일로 저장해야 한다.

1) 얼굴 검출하기

Haar Cascade Classifier

OpenCV의 Haar Cascade Classifier는 이미지의 밝기 차를 이용하여 특징을 찾아내고, 특징에 따라 대상의 분류를 해주는 알고리즘이다.

Haar Feature


이런 다양한 작은 네모(윈도우, 커널, Haar feature)를 이미지에 옮겨가면서(sliding) 검은색 부분과 흰색 부분 각각의 밝기 값을 빼서, 특징을 찾아낸다.

Integral Images

검은색과 흰색 부분 각각의 밝기 값은, 픽셀의 합을 구하는 방식으로 진행되는데, 옮겨가며 하나하나 더하기 어려우므로, Integral Images(적분 이미지)를 사용하여 빠르게 구한다. (이에 대한 자세한 내용은 생략)

Adaboost


그리고 예를 들어 이미지에서 얼굴을 찾아낸다고 할 때, 위의 사진처럼 얼굴의 특징을 나타내는 Haar Feature은, 즉 의미 있는 Haar Feature은 밝기 차이가 특정 임계값(Threshold) 이상인 값이다. 이는 Adaboost라는 과정으로 불린다.

  1. 검흰으로 이루어진 네모(Haar feature)가 처음에는 모두 같은 가중치를 가지고 있다가,
  2. 특정한 학습 data set을 이용하여 어떤 네모가 잘 찾아내는지 가중치를 조정하는 방식으로 진행된다.

다시 말하자면, 얼굴을 찾기 위해 Adaboost라는 과정을 거치면, 얼굴이라는 정보에 해당하는 Haar Feature를 찾을 수 있다. 그리고 이때, 얼굴이 포함된 데이터/포함되지 않은 데이터 가 각각 필요하다.

Cascade Classifier

이미지에서 얼굴에 해당하는 Haar Feature를 활용하여, 얼굴을 찾는다.
(정확하게는 윈도우 내에 Haar Feature가 있어, 윈도우가 더 큰 개념인 것 같다. 약간 '초점을 맞춘다' 해야하나)
이미지 대부분 공간은 얼굴이 없는 영역이기 때문에, 지금 현재 윈도우 영역에 얼굴이 있는지 빠르게 판별하기 위해서 단계별로 진행한다.
낮은 단계에서 얼굴이 존재하지 않는다고 판단되면, 다음 단계는 확인도 하지 않고 넘어가는 식이다.

여기까지 참고 블로그
OpenCV Haar Cascade

OpenCV Haar Cascade 제공 분류기

OpenCV에서는 이러한 검출을 위해, 미리 학습시켜 놓은 분류기를 제공해준다.

다양한 분류기 중, 우리는 얼굴을 인식할 것이기 때문에, 먼저 haarcascade_frontalface_default.xml을 사용한다.

doc tutorial

전체 코드
아래 참조

먼저 opencv 모듈/프레임워크와 numpy를 불러온다. 덧붙여, 검출한 얼굴을 후에 학습 및 인식에 사용하기 위해 파일로 저장해야 하므로, os 모듈도 불러온다.

import cv2 #OpenCV 영상처리
#import numpy as np #배열 계산 용이
#import os #파일 입출력을 위해

분류기를 데려온다. (파일 경로 유의)

#classifier
faceCascade = cv2.CascadeClassifier('haarcascades/haarcascade_frontalface_default.xml')

카메라에서 정보를 받아오자.

#video caputure setting
capture = cv2.VideoCapture(0) # initialize, # is camera number
capture.set(cv2.CAP_PROP_FRAME_WIDTH,1280) #CAP_PROP_FRAME_WIDTH == 3
capture.set(cv2.CAP_PROP_FRAME_HEIGHT,720) #CAP_PROP_FRAME_HEIGHT == 4

모듈을 사용한다는 것은 JAVA나 C++에서처럼, class를 선언하고, 그 안에 변수나 함수를 선언하여 중복적인 선언없이 자유롭게 사용할 수 있게 도와주는 것이라고 생각하면 된다.
그러므로 생성자를 이용하여 초기화Initialize를 해주어야 하고, 그 후에 모듈이 가진 변수나 함수를 사용할 수 있다.

  • VideoCapture(camera #) : 카메라 번호는 0부터 시작한다. 내장카메라는 0, 외장은 1부터이다. 컴퓨터에 내장 카메라가 아예 없는 경우, 연결된 외장 카메라는 0이다.
  • capture.set(option, n) : 너비와 높이 값 등 초기 설정이 가능하다.

다양한 얼굴마다 파일 저장 및 처리를 용이하기 위하여 int 형 user id를 입력 받는다.

#console message
face_id = input('\n enter user id end press <return> ==> ')
print("\n [INFO] Initializing face capture. Look the camera and wait ...")

각 user마다 몇 장의 얼굴 사진을 data set으로 저장할 것인지, count 변수를 통해 세고,
while문을 이용하여 각 프레임마다 영상 처리 및 출력을 반복한다.

count = 0 # # of caputre face images
#영상 처리 및 출력
while True: 
    ret, frame = capture.read() #카메라 상태 및 프레임
    # cf. frame = cv2.flip(frame, -1) 상하반전
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) #흑백으로
    faces = faceCascade.detectMultiScale(
        gray,#검출하고자 하는 원본이미지
        scaleFactor = 1.2, #검색 윈도우 확대 비율, 1보다 커야 한다
        minNeighbors = 6, #얼굴 사이 최소 간격(픽셀)
        minSize=(20,20) #얼굴 최소 크기. 이것보다 작으면 무시
    )

    #얼굴에 대해 rectangle 출력
    for (x,y,w,h) in faces:
        cv2.rectangle(frame,(x,y),(x+w,y+h),(255,0,0),2)
        #inputOutputArray, point1 , 2, colorBGR, thickness)
        count += 1
        cv2.imwrite("dataset/User."+str(face_id)+'.'+str(count)+".jpg",gray[y:y+h, x:x+w])
        
    cv2.imshow('image',frame)

	#종료조건
    if cv2.waitKey(1) > 0 : break #키 입력이 있을 때 반복문 종료
    elif count >= 100 : break #100 face sample
  • caputre.read() : 카메라의 상태 및 프레임을 받아온다.
    카메라가 정상 작동할 경우 ret은 true이고, frame에는 현재 프레임이 저장된다.
  • cv2.cvtColor : 이미지의 색을 변경해준다. OpenCV는 기본적으로 이미지 픽셀을 BGR로 받아오고, haar cascade는 흑백으로 처리해주므로(컴퓨터에게 오브젝트 검출이란 색이 필요 없는 세계란다!) 변환해준다.
  • faceCascade.detectMultiScale : 이미지를 scaleFactor만큼 축소해가며 이미지 피라미드를 쌓고(참조), minNeighbor 횟수 이상 검출된 object를 '검출됐다'로 표시한다. 검출할 최소 크기(minSize)와 최대크기(maxSize)를 설정할 수 있다. 최소 및 최대 크기를 넘어서는 object는 무시한다.

이미지 피라미드와 scale 개념 (참조)

하나의 곡선을 보더라도 scale을 다르게 잡는다면, 곡선으로 인식할 수도 있고, 인식하지 못할 수도 있다. 따라서 대상을 제대로 알고, 검출 및 분석하기 위해서는 다양한 scale에 대해서 살펴보아야 한다.(multi-scale represntation)

그리고 위와 같이 여러 scale로 조정한 이미지를 쌓은 것을 이미지 피라미드라고 부른다.
오른쪽처럼 scale에 따라 윈도우를 옮겨가며 대상을 분석해낸다.
이때, 약간 헷갈릴 수 있는 개념이 scale의 크기이다.
scale이 크다는 것은 넓은 시야에서 보는 것이므로, 축소된 이미지 == 스케일이 크다, 이미지 크기 축소 == 이미지 스케일 증가. 즉 크기(size)와 스케일(scale)은 서로 반비례 관계에 있다.
위와 같이 scaleFactor의 값을 1.2로 설정했을 때, 1/1.2 배씩 축소하며 이미지 피라미드를 쌓는다. 1배, 1/1.2배, 1/(1.2*1.2)배 ...

MinNeighbor 개념
k-Nearest Neighbor 개념을 먼저 이해한다면, 쉽게 이해 가능하다.
값을 낮게 설정할수록, valid한 값이 너무 많다고 판단할 것이며,(invalid한 값까지 포함)
(즉 얼굴이 아닌 것도 얼굴이라고 검출할 것이다.)
값을 높게 설정할수록, valid한 값인데도 무시할 수도 있다.
(즉 얼굴인데도 검출하지 못하고 무시하는 경우가 발생한다)

faceCascade.detectMultiScale을 통해 반환받은 값은 (x,y,w,h)와 같은 튜플 형태이다. (x,y)는 검출된 얼굴의 좌상단 위치이며, (w,h)는 가로, 세로 크기이다.
이를 이용해 얼굴이 검출되었음을 표시해주자.

  • cv2.rectangle : rectangle을 출력한다. (이미지,좌상단 좌표, 우하단 좌표, 색상, 선두께)
  • cv2.imwrite : 프레임을 파일로 저장한다. (fileName, image)
  • cv2.imshow : 이미지를 보여준다.
  • cv2.waitKey(time) : time마다 키 입력상태를 받아온다. 아스키 값을 반환한다.
    cv2.waitKey(1) > 0의 의미는 어떤 키라도 입력이 받아졌다면 while문을 종료하겠다는 것을 의미한다. cv2.waitKey(1) == ord('q')q를 눌렀거나 cv2.waitKey(1) & 0xff) == 27 ESC를 눌렀을 때 종료하도록 설정할 수도 있다. 키 번호 참고
capture.release() #메모리 해제
cv2.destroyAllWindows()#모든 윈도우 창 닫기
  • cv2.destroyWindow("윈도우 창 제목")을 통해 특정 윈도우 창만 닫을 수도 있다.

전체 코드

dataset 폴더 생성 후 실행해 준다.

import cv2 #OpenCV 영상처리

#classifier
faceCascade = cv2.CascadeClassifier('haarcascades/haarcascade_frontalface_default.xml')

#video caputure setting
capture = cv2.VideoCapture(0) # initialize, # is camera number
capture.set(cv2.CAP_PROP_FRAME_WIDTH,1280) #CAP_PROP_FRAME_WIDTH == 3
capture.set(cv2.CAP_PROP_FRAME_HEIGHT,720) #CAP_PROP_FRAME_HEIGHT == 4

#console message
face_id = input('\n enter user id end press <return> ==> ')
print("\n [INFO] Initializing face capture. Look the camera and wait ...")

count = 0 # # of caputre face images
#영상 처리 및 출력
while True: 
    ret, frame = capture.read() #카메라 상태 및 프레임
    # cf. frame = cv2.flip(frame, -1) 상하반전
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) #흑백으로
    faces = faceCascade.detectMultiScale(
        gray,#검출하고자 하는 원본이미지
        scaleFactor = 1.2, #검색 윈도우 확대 비율, 1보다 커야 한다
        minNeighbors = 6, #얼굴 사이 최소 간격(픽셀)
        minSize=(20,20) #얼굴 최소 크기. 이것보다 작으면 무시
    )

    #얼굴에 대해 rectangle 출력
    for (x,y,w,h) in faces:
        cv2.rectangle(frame,(x,y),(x+w,y+h),(255,0,0),2)
        #inputOutputArray, point1 , 2, colorBGR, thickness)
        count += 1
        cv2.imwrite("dataset/User."+str(face_id)+'.'+str(count)+".jpg",gray[y:y+h, x:x+w])
    cv2.imshow('image',frame)

    #종료 조건
    if cv2.waitKey(1) > 0 : break #키 입력이 있을 때 반복문 종료
    elif count >= 100 : break #100 face sample

print("\n [INFO] Exiting Program and cleanup stuff")

capture.release() #메모리 해제
cv2.destroyAllWindows()#모든 윈도우 창 닫기

2) data set을 이용해 얼굴을 학습시키기

이제 data set을, 얼굴을 수집했으니 학습시켜 주자.

import cv2
import numpy as np #배열 계산 용이
from PIL import Image #python imaging library
import os

detector = cv2.CascadeClassifier('haarcascades/haarcascade_frontalface_default.xml')

파일에 대한 경로를 받아, 이미지를 가져와 얼굴 샘플을 가져와 배열에 담는 함수를 작성한다.

def getImagesAndLabels(path):
    imagePaths = [os.path.join(path,f) for f in os.listdir(path)]
    #listdir : 해당 디렉토리 내 파일 리스트
    #path + file Name : 경로 list 만들기

    faceSamples = []
    ids = []
    for imagePath in imagePaths: #각 파일마다
        #흑백 변환
        PIL_img = Image.open(imagePath).convert('L') #L : 8 bit pixel, bw
        img_numpy = np.array(PIL_img, 'uint8')

        #user id
        id = int(os.path.split(imagePath)[-1].split(".")[1])#마지막 index : -1

        #학습을 위한 얼굴 샘플
        faces = detector.detectMultiScale(img_numpy)
        for(x,y,w,h) in faces:
            faceSamples.append(img_numpy[y:y+h,x:x+w])
            ids.append(id)

    return faceSamples, ids
  • join : 파일 경로를 설정해주기 위해 디렉토리 이름과 파일 이름을 이어 붙여준다.
  • os.listdir : 해당 디렉토리 안의 파일의 리스트
  • Image.open() : 이미지를 열고
  • convert() : 8bit pixel 이미지로 바꾸어준다. 0~255의 수로 표현가능한 흑백 이미지를 생성한다는 의미이다.
  • np.array : 픽셀 처리를 용이하게 하기 위해 numpy를 사용한다.
  • split(file full path)[-1] : user.1.99.jpg 의 형식으로 되어 있는 파일을 처리하기 위하여
    파일 이름을 먼저 split분리해내고,
  • split(file full path)[-1].split(".")[1] : (0번째 user) 1번째 id값을 가져온다.

data set의 경로를 설정해준 후, LBPHFaceRecognizer를 생성한다.

path = 'dataset'
recognizer = cv2.face.LBPHFaceRecognizer_create()

LBPHFaceRecognizer
Local-Binary-Pattern 알고리즘은, 지역적인 주변의 값을 2진수로 표현한 뒤 값을 계산한다.

3*3 픽셀에서 정중앙을 기준으로 주위 픽셀 8개와 크기를 비교한다. 중심 픽셀의 값보다 크거나 같으면 1, 작으면 0으로 설정하는 것이다. 위의 경우 오른쪽과 같은 값을 얻을 수 있고, 이진수로 11001011로 표현할 수 있다.
이렇게 모든 픽셀에 대해 계산해준다.

(이런 식의 처리는 이미지의 밝기가 변해도 문제없이 처리가 가능하다)

히스토그램(분포표)을 만들어, 값의 비교(제곱오차)를 통해 유사한 얼굴을 찾아낸다.

학습시킨다.

print('\n [INFO] Training faces. It will take a few seconds. Wait ...')
faces, ids = getImagesAndLabels(path)

recognizer.train(faces,np.array(ids)) #학습

recognizer.write('trainer/trainer.yml')
print('\n [INFO] {0} faces trained. Exiting Program'.format(len(np.unique(ids))))

전체 코드

import cv2
import numpy as np #배열 계산 용이
from PIL import Image #python imaging library
import os

path = 'dataset' #경로 (dataset 폴더)
recognizer = cv2.face.LBPHFaceRecognizer_create()
detector = cv2.CascadeClassifier('haarcascades/haarcascade_frontalface_default.xml')

def getImagesAndLabels(path):
    imagePaths = [os.path.join(path,f) for f in os.listdir(path)]
    #listdir : 해당 디렉토리 내 파일 리스트
    #path + file Name : 경로 list 만들기

    faceSamples = []
    ids = []
    for imagePath in imagePaths: #각 파일마다
        #흑백 변환
        PIL_img = Image.open(imagePath).convert('L') #L : 8 bit pixel, bw
        img_numpy = np.array(PIL_img, 'uint8')

        #user id
        id = int(os.path.split(imagePath)[-1].split(".")[1])#마지막 index : -1

        #얼굴 샘플
        faces = detector.detectMultiScale(img_numpy)
        for(x,y,w,h) in faces:
            faceSamples.append(img_numpy[y:y+h,x:x+w])
            ids.append(id)

    return faceSamples, ids

print('\n [INFO] Training faces. It will take a few seconds. Wait ...')
faces, ids = getImagesAndLabels(path)

recognizer.train(faces,np.array(ids)) #학습

recognizer.write('trainer/trainer.yml')
print('\n [INFO] {0} faces trained. Exiting Program'.format(len(np.unique(ids))))

3. 학습 시킨 것을 바탕으로 현재 얼굴이 누구인지 알아내기

#2에서 학습시킨 recognizer를 가져온다.

recognizer = cv2.face.LBPHFaceRecognizer_create()
recognizer.read('trainer/trainer.yml')
cascadePath = 'haarcascades/haarcascade_frontalface_default.xml'
faceCascade = cv2.CascadeClassifier(cascadePath)

user id에 해당하는 이름 배열을 만들고, 카메라를 설정한다.

names = ['None','sumin','dongjun','minji']

cam = cv2.VideoCapture(0)
cam.set(cv2.CAP_PROP_FRAME_WIDTH, 1980)
cam.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)

minW = 0.1 * cam.get(cv2.CAP_PROP_FRAME_WIDTH)
minH = 0.1 * cam.get(cv2.CAP_PROP_FRAME_HEIGHT)
  • cam.get(속성) : 속성을 반환한다.

while문 내에서 프레임을 받아와 얼굴을 검출하고,

while True:
    ret, img = cam.read()
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

    faces = faceCascade.detectMultiScale(
        gray,
        scaleFactor=1.2,
        minNeighbors=6,
        minSize=(int(minW), int(minH))
    )

얼굴에 대한 예측을 한다.

    for(x,y,w,h) in faces:
        cv2.rectangle(img, (x,y), (x+w,y+h), (0,255,0),2)
        id, confidence = recognizer.predict(gray[y:y+h, x:x+w])

        if confidence < 55 :
            id = names[id]
        else:
            id = "unknown"
        
        confidence = "  {0}%".format(round(100-confidence))

        cv2.putText(img,str(id), (x+5,y-5),font,1,(255,255,255),2)
        cv2.putText(img,str(confidence), (x+5,y+h-5),font,1,(255,255,0),1)
  • recognizer.predict(src) : 얼굴을 예측한다. 반환값으로 label과 confidence 값을 받아온다. 즉 user id와, 확률값을 가져온다.(confidence는 0에 가까울수록 label과 일치한다는 뜻. label과 얼마나 가까운가-를 생각하면 된다)
  • cv2.putText(img, text, bottom-left corner, font, fontScale, color, thickness) : label과 예측값을 이미지에 폰트로 출력한다.

전체 코드

import cv2
import numpy as np

recognizer = cv2.face.LBPHFaceRecognizer_create()
recognizer.read('trainer/trainer.yml')
cascadePath = 'haarcascades/haarcascade_frontalface_default.xml'
faceCascade = cv2.CascadeClassifier(cascadePath)

font = cv2.FONT_HERSHEY_SIMPLEX

id = 0

names = ['None','sumin','dongjun','minji']

cam = cv2.VideoCapture(0)
cam.set(cv2.CAP_PROP_FRAME_WIDTH, 1980)
cam.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)

minW = 0.1 * cam.get(cv2.CAP_PROP_FRAME_WIDTH)
minH = 0.1 * cam.get(cv2.CAP_PROP_FRAME_HEIGHT)

while True:
    ret, img = cam.read()
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

    faces = faceCascade.detectMultiScale(
        gray,
        scaleFactor=1.2,
        minNeighbors=6,
        minSize=(int(minW), int(minH))
    )

    for(x,y,w,h) in faces:
        cv2.rectangle(img, (x,y), (x+w,y+h), (0,255,0),2)
        id, confidence = recognizer.predict(gray[y:y+h, x:x+w])

        if confidence < 55 :
            id = names[id]
        else:
            id = "unknown"
        
        confidence = "  {0}%".format(round(100-confidence))

        cv2.putText(img,str(id), (x+5,y-5),font,1,(255,255,255),2)
        cv2.putText(img,str(confidence), (x+5,y+h-5),font,1,(255,255,0),1)
    
    cv2.imshow('camera',img)
    if cv2.waitKey(1) > 0 : break

print("\n [INFO] Exiting Program and cleanup stuff")
cam.release()
cv2.destroyAllWindows()

조금 아쉬운 인식률을 가진 '실시간 얼굴 인식 프로그램'이 완성됐다.
아쉬운 부분을 뜯어 고쳐 보자.

0개의 댓글