스노우 같은 카메라 스티커앱은 어떻게 작동할까? 일단 입력값인 동영상(연속된 사진)에서 얼굴을 인식해내야 한다. 그리고 카메라나 사람이 계속 움직이므로 얼굴을 추적해내기도 해야한다. 추적한 얼굴의 걸맞은 위치에 스티커를 부착해야 한다. 만약 사람이 가까이 다가오면 스티커도 커지고 사람이 멀어지면 스티커는 작아져야한다. 이런 여러가지 과제를 가지고 스티커앱 만들기에 도전해보자!
스티커를 잘 붙이려면 이목구비의 위치를 파악하는 것이 중요하다. 이런 중요 포인트의 위치를 찾아내는 기술을 랜드마크(landmark) 또는 조정(alignment) 이라고 한다. 얼굴의 경우 눈, 코, 입, 턱이 주요 포인트가 된다. 머리의 위치는 눈과 코의 위치로 추정할 수 있다.
스티커앱을 구현하기 위해 다음과 같은 과정을 거친다:
1) 얼굴이 포함된 사진을 준비한다.
2) 사진으로부터 얼굴의 bounding box를 찾는다.
3) 눈, 코, 입과 같은 face landmark를 파악한다.
4) 원하는 위치에 스티커를 부착한다.
import os
import cv2
import matplotlib.pyplot as plt
import numpy as np
import dlib
이미지 처리를 위한 opencv와 이미지 출력을 위한 matplotlib을 불러온다.
opencv는 굉장히 유용한 컴퓨터 비전 라이브러리이다. 그런데 OpenCV는 BGR(blue, green, red) 이미지채널을 사용해서 RGB로 변경해 주어야 한다.
my_image_path = os.getenv('HOME')+'/aiffel/camera_sticker/images/image1.png'
img_bgr = cv2.imread(my_image_path) #OpenCV로 이미지를 불러온다
img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) #BGR이미지를 RGB이미지로 바꿔준다
img_show = img_bgr.copy() # 출력용 이미지를 따로 보관한다
plt.imshow(img_bgr)
plt.show()
OpenCV의 이미지 호출 함수 cv2.imread(filename, flag)
이미지 읽기의 flag는 3가지가 있다.
- cv2.IMREAD_COLOR : 이미지 파일을 Color로 읽어들인다. 투명한 부분은 무시되며, Default값
- cv2.IMREAD_GRAYSCALE : 이미지를 Grayscale로 읽어 들인다. 실제 이미지 처리시 중간단계로 많이 사용
- cv2.IMREAD_UNCHANGED : 이미지파일을 alpha channel까지 포함하여 읽어 들인다.
object detection을 수행하는 패키지가 여러개 있는데, 먼저 dlib의 HOG + Linear SVM face detector을 활용할 것이다.
HOG와 SVM에 더하여 sliding window를 사용해 얼굴의 위치를 파악한다.
sliding window는 작은 영역의 window를 이동해가며 확인하는 방법이다.
다음과 같이 코드를 작성한다↓
# detector를 선언한다
detector_hog = dlib.get_frontal_face_detector()
얼굴의 bounding box 추출↓
img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) #bgr이미지 rgb로 변경
dlib_rects = detector_hog(img_rgb, 1) # (image, num of image pyramid)
*image pyramid: upsampling을 이용해 이미지의 크기를 키움으로써 더 정확한 검출을 돕는다.
찾은 얼굴을 출력해보자.
# 찾은 얼굴 영역 박스 리스트
# 여러 얼굴이 있을 수 있음
print(dlib_rects)
for dlib_rect in dlib_rects:
l = dlib_rect.left()
t = dlib_rect.top()
r = dlib_rect.right()
b = dlib_rect.bottom()
cv2.rectangle(img_show, (l,t), (r,b), (0,255,0), 2, lineType=cv2.LINE_AA)
img_show_rgb = cv2.cvtColor(img_show, cv2.COLOR_BGR2RGB)
plt.imshow(img_show_rgb)
plt.show()
bounding box로 잘라낸 얼굴 이미지에서 이목구비의 위치를 추론하는 것을 face landmark localization이라고 함.
Dlib에서 제공하는 pretrained model을 이용하여 얼굴의 랜드마크를 찾을 것이다.
공개되어있는 weight file을 다운 받아 압축을 푼 후 사용한다.
이미지에서 찾은 얼굴 박스 마다 68개의 랜드마크가 찾아져서 list_landmarks에 저장된다.
list_landmarks = []
# 얼굴 영역 박스 마다 face landmark를 찾아낸다.
for dlib_rect in dlib_rects:
points = landmark_predictor(img_rgb, dlib_rect)
# face landmark 좌표를 저장해둡니다
list_points = list(map(lambda p: (p.x, p.y), points.parts()))
list_landmarks.append(list_points)
print(len(list_landmarks[0]))
>> 68
랜드마크를 이미지 위에 출력하는 코드는 다음과 같다.
for landmark in list_landmarks:
for point in landmark:
cv2.circle(img_show, point, 2, (0, 255, 255), -1)
img_show_rgb = cv2.cvtColor(img_show, cv2.COLOR_BGR2RGB)
plt.imshow(img_show_rgb)
plt.show()
이제 스티커를 얼굴에 붙여야한다. 두 가지 사항을 고려해야 한다.
1) 스티커의 위치
랜드마크인 코나 눈썹을 기준으로 x 픽셀 위에 스티커를 붙이면 된다.
2) 스티커의 크기
코의 좌표를 확인해 보자.
for dlib_rect, landmark in zip(dlib_rects, list_landmarks):
print (landmark[30]) # 코의 index는 30 입니다
x = landmark[30][0]
y = landmark[30][1] - dlib_rect.height()//2
w = h = dlib_rect.width()
print ('(x,y) : (%d,%d)'%(x,y))
print ('(w,h) : (%d,%d)'%(w,h))
준비해 둔 스티커 이미지를 얼굴 블럭 크기로 변환한다.
sticker_path = os.getenv('HOME')+'/aiffel/camera_sticker/images/heart.png'
img_sticker = cv2.imread(sticker_path) # 스티커 이미지를 불러옵니다
img_sticker = cv2.resize(img_sticker, (w,h))
print (img_sticker.shape)
이미지 시작점이 왼쪽 위(top-left) 좌표이기 때문에 x, y 값을 조정해준다.
refined_x = x - w // 2
refined_y = y - h
print ('(x,y) : (%d,%d)'%(refined_x, refined_y))
그런데, y축 좌표가 음수가 나오는 문제가 발생한다.
스티커의 y값이 얼굴 이미지의 영역을 벗어났기 때문이다. opencv가 사용하는 ndarray는 음수 인덱스를 쓸 수 없기 때문에 음수 값을 처리해주어야 한다. (x, y 모두)
if refined_x < 0:
img_sticker = img_sticker[:, -refined_x:]
refined_x = 0
if refined_y < 0:
img_sticker = img_sticker[-refined_y:, :]
refined_y = 0
print ('(x,y) : (%d,%d)'%(refined_x, refined_y))
음수 값만큼 스티커를 crop하고 x, y좌표를 얼굴 이미지의 경계 값으로 설정한다.
드디어 얼굴 이미지에 스티커를 적용한다.
sticker_area = img_show[refined_y:refined_y+img_sticker.shape[0], refined_x:refined_x+img_sticker.shape[1]]
img_show[refined_y:refined_y+img_sticker.shape[0], refined_x:refined_x+img_sticker.shape[1]] = \
np.where(img_sticker==0,sticker_area,img_sticker).astype(np.uint8)
결과 이미지 출력
plt.imshow(cv2.cvtColor(img_show, cv2.COLOR_BGR2RGB))
plt.show()
bounding box와 landmark 점을 없애고 최종 결과만 출력해보자.
sticker_area = img_bgr[refined_y:refined_y +img_sticker.shape[0], refined_x:refined_x+img_sticker.shape[1]]
img_bgr[refined_y:refined_y +img_sticker.shape[0], refined_x:refined_x+img_sticker.shape[1]] = \
np.where(img_sticker==0,sticker_area,img_sticker).astype(np.uint8)
plt.imshow(cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB))
plt.show()
[F-09] Numpy, Pandas 이해하기 (0) | 2022.02.25 |
---|---|
탐색적 데이터 분석(EDA) (0) | 2022.01.21 |
[EXPLORATION 4] 작사가 인공지능 만들기 (0) | 2022.01.13 |