Unknown Face Classifier - 모르는 얼굴 분류하기
이 글은 파이썬 얼굴 인식 시리즈의 3편 중 두번째 입니다.
- Python Face Recognition - 아는 사람의 얼굴을 인식하기
- Unknown Face Classifier - 모르는 사람의 얼굴을 분류하기
- Visitor Alarm Telegram Bot - 인식된 얼굴을 텔레그램으로 보내기
들어가며
이 포스트에서는 기존에 올렸던 얼굴 인식 구현인 Python Face Recognition을 개선합니다. 아는 사람의 이름을 표시하는 것 뿐 아니라, 모르는 얼굴을 모으고 비슷한 얼굴을 찾아내어 새로운 사람을 찾아내는 간단한 방법을 구현합니다. 소스코드는 여기에서 받으실 수 있습니다.
얼굴 인식의 원리
얼굴 인식의 원리에 대해서는 이전에 포스트했던 Python Face Recognition나 Python Face Clustering에서 소개를 했습니다만, 여기에서 다시 한 번 설명을 하겠습니다.
얼굴 인식의 과정
얼굴을 인식하기 위해서는 다음과 같은 세 과정을 거칩니다.

- 그림에서 얼굴이 있는 영역을 알아낸다. (face location)
- 얼굴 영역에서 눈, 코, 입 등 68개의 주요 좌표를 추출한다. (facial landmarks)
- 68개의 좌표를 128개의 숫자로 변환한다. (face encoding)
파이썬 패키지 dlib에는 이 과정이 구현되어 있습니다. face_recognition 패키지의 face_locations()과 face_encodings() 함수는 dlib와 numpy에 쉽게 접근할 수 있도록 wrapping해 놓은 함수입니다.
위 과정에 대한 기술적인 내용은 이해하기 어려운 주제이므로, 여기에서는 face encoding의 결과물인 128개 숫자의 특성에 주목합시다.
- 128개 숫자는 deep learning의 결과물이기 때문에, 각 숫자의 의미는 알지 못합니다.
- 하지만 같은 사람의 얼굴을 입력하면 비슷한 숫자가 나옵니다.
얼굴 간의 거리 (유사성) 구하기
우리는 사전에 알고 있는 사람의 face_encoding 값을 가지고 있습니다. 그림에서 얼굴을 인식하고, 그 얼굴의 face_encoding을 사전에 알고 있는 사람의 face_encoding과 비교하면, 가장 거리가 가까운 (즉, 가장 비슷한) 사람을 찾아낼 수 있습니다.
face_encoding은 128 차원의 벡터 입니다. 이 벡터가 서로 얼마나 가까운지 어떻게 비교를 할까요? 수학적으로 L2 norm (유클리드 거리)를 이용하면 두 벡터의 거리를 구할 수 있고, 파이썬에서는 numpy의 linalg.norm() 함수를 이용하면 됩니다.
실제로 파이썬 패키지 face_recognition의 face_distance() 함수는 numpy의 linalg.norm() 함수의 wrapper로 구현되어 있습니다. 두 face_encoding의 거리는 0 ~ 1 사이의 값으로 구해지고, 이 값은 두 얼굴이 얼마나 비슷한지에 대한 척도로 사용할 수 있습니다.
Similarity Threshold
이 때, 단순히 거리가 가장 가까운 사람만 찾는 것이 아니라, 사전에 정해진 기준 보다 가까워야 합니다. 기준보다 멀다면 “비슷하게 생겼지만 다른 사람”의 의미 입니다. 이 기준은 코드상의 similarity_threshold 입니다.
Threshold 값은 실험적으로 동양인의 얼굴은 이 값이 0.4 ~ 0.45 정도의 값이 적당하고, 서양인의 얼굴은 0.5 ~ 0.55정도의 값이 적당했습니다. (추측이지만, 아마도 dlib가 서양인 위주로 학습이 된 것이 아닐까요?)
이 값은 얼굴의 표정, 각도, 이미지 크기, 조명 상태 등 여러 인자에 의해 따라 달라집니다. 다양한 조건에서 이 값이 얼마가 적당한지는 많은 실험과 별도의 알고리즘으로 정해야 하는 문제로, 또 하나의 큰 주제가 될 것입니다.
알고리즘의 구현
얼굴 인식
이미지에서 얼굴을 인식하는 알고리즘은 face_recognition 패키지를 이용하여 아래와 같이 간단히 구현할 수 있습니다.
def detect_faces(self, frame):
rgb = frame[:, :, ::-1] # 이미지를 RGB format으로 변환
# 얼굴 영역을 알아낸다
boxes = face_recognition.face_locations(rgb, model="hog")
if not boxes:
return []
# 얼굴 영역을 찾았음. face_encoding을 계산
encodings = face_recognition.face_encodings(rgb, boxes)
# Face 생성하여 리턴
faces = []
for i, box in enumerate(boxes):
face_image = self.get_face_image(frame, box) # 얼굴 이미지 추출
face = Face(str_ms + str(i) + ".png", face_image, encodings[i])
faces.append(face)
return faces
아는 사람의 얼굴과 비교
새로 인식된 얼굴을 기존에 아는 사람의 얼굴과 비교하는 알고리즘은 아래와 같습니다.
- 새로 얼굴이 인식되면, 아는 사람들의
face_encoding과의 거리를 구한다. - 가장 가까운 사람과의 거리가
similarity_threshold보다 작으면 그 사람의 얼굴이라고 판단하고, 그 사람의 얼굴 DB에 얼굴을 추가한다. - 새로 추가된 얼굴을 포함하여 그 사람의
face_encoding을 업데이트 한다. (얼굴의 face_encoding의 평균)
얼굴이 추가될 때마다 그 사람의 모든 얼굴의 face_encoding의 평균을 구하여 놓습니다.
def compare_with_known_persons(self, face, persons):
# distance를 구함
encodings = [person.encoding for person in persons]
distances = face_recognition.face_distance(encodings, face.encoding)
index = np.argmin(distances)
min_value = distances[index] # distance의 최소값을 구함
if min_value < self.similarity_threshold:
# distance의 최소값이 simalarity_threshold보다 작으면 같은 사람의 얼굴임
persons[index].add_face(face)
persons[index].calculate_average_encoding() # 얼굴의 face_encoding의 평균
return persons[index]
새로운 사람 찾아내기
모르는 사람의 얼굴을 비교하여 새로운 사람을 찾아내는 것은 아래와 같은 간단한 알고리즘으로 구현했습니다.
- 모르는 얼굴은
unknown_faces에 따로 저장해 놓는다. - 새로 인식된 얼굴과 모르는 얼굴들(
unknown_faces)과의 거리를 구한다. - 가장 가까운 얼굴와의 거리가
similarity_threshold보다 작으면 두 얼굴은 같은 사람의 얼굴이라고 판단히고, 새로운 사람을 만든다. - 가장 가까운 얼굴와의 거리가
similarity_threshold보다 크면unknown_faces에 추가한다.
아래 코드는 이 알고리즘을 구현한 것입니다.
def compare_with_unknown_faces(self, face, unknown_faces):
# distance를 구함
encodings = [face.encoding for face in unknown_faces]
distances = face_recognition.face_distance(encodings, face.encoding)
index = np.argmin(distances)
min_value = distances[index] # distance의 최소값을 구함
if min_value < self.similarity_threshold:
# two faces are similar - create new person with two faces
person = Person()
newly_known_face = unknown_faces.pop(index)
person.add_face(newly_known_face)
person.add_face(face)
person.calculate_average_encoding() # 얼굴의 face_encoding의 평균
return person
else:
# unknown face
unknown_faces.append(face)
return None
새로 만들어진 사람의 이름은 person_## 으로 자동으로 부여되고, ##은 사람이 추가될 때마다 증가합니다 (person_db.py). 이 이름은 나중에 수정할 수 있습니다.
Example Result
유튜브에 올려진 영화 ‘기생충’의 캐릭터 영상으로 테스트한 결과를 보여드리겠습니다.
youtube-dl로 위 동영상을 다운 받아서 아래 명령을 실행했습니다. (threshold=0.4, 0.5초 간격)
$ python face_classifier.py -i ~/Videos/prc.mp4 -t 0.4 -c -S 0.5
Unknown Face
영상은 총 2:06 길이이고, 아래 그림은 대략 1:14초의 결과입니다.

person_03과 person_13은 이전 화면에 나온 적이 있어서 이미 알고 있는 사람입니다. 사진 오른쪽의 두 아이는 이 프레임에서 처음 나왔기 때문에, 둘 다 unknown으로 표시 되었습니다.
New Person

하지만 0.5초 뒤의 프레임에서는 두 아이가 각각 person_14, person_15로 표시되었습니다. 이전 프레임에서 unknowns에 저장된 사진과 비교해 보니 서로 거리가 가까워서 새로 출현한 사람이라고 판단한 것입니다.
실행하기
소스코드는 GitHub에 올려져 있고, 마음대로 다운로드 받을 수 있습니다.
필요 패키지 설치
python 3.x로 실행해야 하고, virtualenv로 환경을 분리시키기를 추천합니다. 사전에 다음과 같은 패키지가 설치되어 있어야 합니다.
$ pip install opencv-python
$ pip install opencv-contrib-python
$ pip install dlib
$ pip install face_recognition
$ pip install imutils
실행
face_classifier.py에는 아래와 같은 옵션을 줄 수 있습니다.
$ python face_classifier.py -h
usage: face_classifier.py [-h] [-t THRESHOLD] [-S SECONDS] [-s STOP] [-k SKIP]
[-d] [-c CAPTURE]
inputfile
positional arguments:
inputfile video file to detect or '0' to detect from web cam
optional arguments:
-h, --help show this help message and exit
-t THRESHOLD, --threshold THRESHOLD
threshold of the similarity (default=0.44)
-S SECONDS, --seconds SECONDS
seconds between capture
-s STOP, --stop STOP stop detecting after # seconds
-k SKIP, --skip SKIP skip detecting for # seconds from the start
-d, --display display the frame in real time
-c CAPTURE, --capture CAPTURE
save the frames with face in the CAPTURE directory
적당한 비디오 파일을 아래와 같이 -d 옵션을 주고 한 번 돌려보세요. 얼굴 영역이 화면에 표시되고, 분류된 사람의 이름이 화면에 나올 것입니다. 처음 실행하면, 사람 얼굴이 모두 person_##으로 출력됩니다.
$ python face_classifier.py <video_file> -d
종료
동영상 파일을 끝까지 읽었거나 중간에 ^C 또는 'q'를 누르면 인식 과정을 멈추고, 결과를 저장하고, 통계를 출력합니다.
$ python face_classifier.py ~/Videos/prc.mp4 -d
...
Start saving persons in the directory 'result'.
montages saved
result/face_encodings saved
Saving persons finished in 0.920 sec.
8 persons, 60 known faces, 10 unknown faces
person_01 [ 0.000 0.398 0.326 0.529 0.433 0.604 0.369 0.654 ] 0.238, 0.316, 0.432, 18 faces
person_02 [ 0.398 0.000 0.440 0.533 0.485 0.628 0.530 0.591 ] 0.237, 0.289, 0.336, 12 faces
person_03 [ 0.326 0.440 0.000 0.546 0.471 0.623 0.363 0.725 ] 0.243, 0.294, 0.383, 12 faces
person_04 [ 0.529 0.533 0.546 0.000 0.423 0.493 0.528 0.748 ] 0.088, 0.122, 0.161, 3 faces
person_05 [ 0.433 0.485 0.471 0.423 0.000 0.418 0.465 0.717 ] 0.124, 0.190, 0.303, 4 faces
person_06 [ 0.604 0.628 0.623 0.493 0.418 0.000 0.600 0.770 ] 0.210, 0.210, 0.210, 2 faces
person_07 [ 0.369 0.530 0.363 0.528 0.465 0.600 0.000 0.638 ] 0.214, 0.288, 0.370, 7 faces
person_08 [ 0.654 0.591 0.725 0.748 0.717 0.770 0.638 0.000 ] 0.164, 0.164, 0.164, 2 faces
결과 파일
인식의 얼굴과 분류된 사람은 result 디렉토리에 저장됩니다. result 디렉토리의 내용을 볼까요?
$ ls -l result
total 1744
-rw-rw-r-- 1 rostude rostude 77504 Aug 16 17:51 face_encodings
-rw-rw-r-- 1 rostude rostude 278451 Aug 16 17:51 montage.person_01-00.png
-rw-rw-r-- 1 rostude rostude 110646 Aug 16 17:51 montage.person_01-01.png
-rw-rw-r-- 1 rostude rostude 276885 Aug 16 17:51 montage.person_02-00.png
-rw-rw-r-- 1 rostude rostude 296456 Aug 16 17:51 montage.person_03-00.png
-rw-rw-r-- 1 rostude rostude 96322 Aug 16 17:51 montage.person_04-00.png
-rw-rw-r-- 1 rostude rostude 95596 Aug 16 17:51 montage.person_05-00.png
-rw-rw-r-- 1 rostude rostude 58560 Aug 16 17:51 montage.person_06-00.png
-rw-rw-r-- 1 rostude rostude 153792 Aug 16 17:51 montage.person_07-00.png
-rw-rw-r-- 1 rostude rostude 41319 Aug 16 17:51 montage.person_08-00.png
-rw-rw-r-- 1 rostude rostude 238057 Aug 16 17:51 montage.unknowns-00.png
drwxrwxr-x 2 rostude rostude 4096 Aug 16 17:51 person_01/Start saving
drwxrwxr-x 2 rostude rostude 4096 Aug 16 17:51 person_02/
drwxrwxr-x 2 rostude rostude 4096 Aug 16 17:51 person_03/
drwxrwxr-x 2 rostude rostude 4096 Aug 16 17:51 person_04/
drwxrwxr-x 2 rostude rostude 4096 Aug 16 17:51 person_05/
drwxrwxr-x 2 rostude rostude 4096 Aug 16 17:51 person_06/
drwxrwxr-x 2 rostude rostude 4096 Aug 16 17:51 person_07/
drwxrwxr-x 2 rostude rostude 4096 Aug 16 17:51 person_08/
drwxrwxr-x 2 rostude rostude 4096 Aug 16 17:51 unknowns/
| file | 설명 |
|---|---|
face_encodings |
모든 얼굴의 encoding 값이 저장된 pickle 파일 |
| 디렉토리 (사람이름) | 같은 사람으로 판단한 얼굴의 png 파일이 저장됨 |
| montage.*.png | 각 디렉토리에 저장된 얼굴을 보기 쉽게 몽타쥬 형식으로 모은 파일 |
결과 재 분류
montage 파일을 열어보고, 같은 사람으로 분류된 얼굴을 확인해 보세요. similarity_threshold 값에 따라 결과가 다르겠지만, 대체로 썩 쓸만하게 분류가 된 것을 알 수 있습니다.
디렉토리의 이름은 그 사람의 이름입니다. 디렉토리의 이름을 실제 사람의 이름으로 바꾸어 줍니다.
각 디렉토리에 있는 .png 파일을 열고, 분류된 얼굴이 실제로 그 사람의 얼굴이 맞는지 확인해 봅니다. 몇몇 잘못 분류된 얼굴이 보일 수 있습니다. 잘못 분류된 얼굴 파일은 제 디렉토리로 옮겨 줍니다. (복사가 아니라 옮깁니다.) 이 작업을 하면 다음에 실행할 때 얼굴 인식의 정확도가 향상됩니다.
분류할 수 없는 얼굴은 unknowns 디렉토리에 옮깁니다.
재분류된 데이터 로딩
다음과 같이, 사람의 이름을 바꾸고 다시 한 번 돌려 봅시다.
$ mv result/person_02 result/gitaek
$ mv result/person_05 result/choongsuk
$ mv result/person_04 result/gijung
$ mv result/person_03 result/giwoo
$ python face_classifier.py ~/Videos/prc.mp4 -d -t 0.42
이전에 실행할 때에 비해서 세 가지가 달라진 것을 눈치 채셨나요?
(1) 아래와 같이, 이전에 저정해 두었던 사람과 얼굴 정보를 로딩합니다.
$ python face_classifier.py ~/Videos/prc.mp4 -d -t 0.42
...
Start loading persons in the directory 'result'.
78 face_encodings in result/face_encodings
unknowns has 12 faces
giwoo has 7 faces
gijung has 8 faces
person_01 has 14 faces
person_07 has 4 faces
person_06 has 3 faces
person_08 has 2 faces
person_09 has 5 faces
gitaek has 16 faces
choongsuk has 7 faces
Loading persons finished in 0.565 sec.
9 persons, 66 known faces, 12 unknown faces
...
(2) 로딩한 사람의 이름이 giwoo, gijung, gitaek, choongsuk 등으로 변경되었습니다.
(3) 아래 그림과 같이, 화면에 출력할 때에도 변경된 이름이 출력됩니다.

저장된 결과 지우기
result 디렉토리를 삭제하면 다시 실행할 때 이전 결과를 로딩하지 않게 됩니다.
응용하기
웹캠으로 실시간 인식
다음과 같이 inputfile을 0으로 주면 비디오 파일이 아닌 웹캠으로 실시간 캡처를 하고 결과를 모니터 화면에 출력합니다.
저해상도 웹캠 또는 고성능 PC를 사용하시는 분은 -S 옵션을 낮게 주면 더 자연스러운 영상을 얻을 수 있습니다.
$ python face_classifier.py 0 -d -S 0.5
친구들을 웹캠으로 찍어보세요. 결과물의 디렉토리 이름을 친구 이름으로 바꾸고 다시 돌려 보시고, 친구 얼굴 아래 이름이 제대로 출력되는지 보세요. -t 옵션으로 정확도를 조절해 보세요.
Visitor Alarm Telegram Bot
이 글에서는 사람이 인식되면 비디오 화면에 이름을 표시했습니다. 이것을 더 확장해서, 웹캠에서 인식된 사람의 사진을 실시간으로 사용자의 스마트폰으로 보내면 어떨까요? 텔레그램 봇을 이용하면 이 기능을 쉽게 구현할 수 있습니다.
Visitor Alarm Telegram Bot에서는 인식된 사람의 사진을 실시간으로 전송하는 텔레그램 봇을 만들었습니다. 스마트폰에 설치된 텔레그램으로 실시간 알람을 받고 실시간 모니터링을 할 수 있습니다. 웹캠을 집의 현관이나 복도 등 주요 장소에 설치하면 스마트 초인종, 스마트 CCTV 등을 만들 수 있습니다.