OpenCV: 카메라 스티커 만들기

Choi_JM·2022년 1월 21일
0
post-thumbnail

목차

    1. 얼굴이 포함된 사진과 스티커 사진을 준비한다.
    1. 사진으로부터 얼굴의 bounding box를 찾는다.
    1. 사진으로부터 얼굴 영역 face landmark를 찾아낸다.
    1. 마지막으로 찾아진 영역으로부터 스티커를 붙힌다.

★ 들어가기전 유의사항!! ★

스티커 사진은 png 파일 유형으로 준비하는게 좋습니다. png 파일은 무손실 압축을 사용하기 때문에 이미지 손실이 없고 고품질 이미지를 생성하기 때문에 이미지 편집에 용이합니다.

또, png 파일은 배경이 투명하기 때문에 배경 이미지 위에 png 파일을 얹어 자연스럽게 합성 시킬 수 있습니다. jpeg 등과 같은 이미지는 배경을 지우는 등 추가 처리가 필요하기 때문에 png 파일을 사용하는 것이 간편하고 빠릅니다.


1단계

필요한 라이브러리를 선언해보자.

import cv2
import matplotlib.pyplot as plt
import numpy as np
import dlib

☞ 유의사항
혹시나 import error가 뜰 경우 라이브러리를 설치해줘야 합니다.
본인이 사용하는 환경에 따라 설치해주시면 됩니다!
ex) pip install numpy, !pip install numpy, conda install numpy 등...

준비한 얼굴 이미지를 불러옵니다.
image_path 부분은 자기가 저장한 이미지 위치를 넣어주면 됩니다.

image_path = os.getenv('HOME')+'/me/camera_sticker/images/My_face.png'
img_bgr = cv2.imread(image_path) 
img_show = img_bgr.copy() 
img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
plt.imshow(img_rgb)
plt.show()

출력 결과

이 뭐 아바타도 아니고.. 얼굴 색이 이상하죠?? 그 이유에 대해서 설명 드리겠습니다.

오늘 사용하게 될 matplotlib, dlib 등의 이미지 라이브러리는 이미지 채널을 기본적으로 RGB(빨강, 녹색, 파랑) 순으로 사용합니다. 하지만 opencv는 예외적으로 BGR(파랑, 녹색, 빨강)을 사용합니다. 그래서 원본 사진에서 색상이 바뀐 상태로 출력하게 되는겁니다.

이를 정상적으로 처리해주기 위해서는 간단합니다. plt.imshow를 선언하기 전에 RGB 이미지로 변환 시켜 주면 됩니다.

# cv2.cvtColor의 cv2.COLOR_BGR2RGB를 통해 변환 시켜줍니다.

img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
plt.imshow(img_rgb)
plt.show()

출력 결과

자, 정상으로 출력되었습니다. 이제 얼굴 인식하러 가보죠!

2단계

이번 단계에서는 Object detection을 통해 얼굴의 위치를 찾게 됩니다. 간단하게 찾기 위해 패키지를 사용해보죠. dlib의 face detector을 이용하면 간단하게 얼굴의 bounding box를 찾아줍니다.

detector를 통해 bounding box를 찾아보죠.

detector = dlib.get_frontal_face_detector()
img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
dlib_rects = detector(img_rgb, 1)   # (image, num of image pyramid)

dlib는 RGB로 입력 받기 때문에 cvtColor()를 이용해서 opencv의 BGR이미지를 RGB로 변환시켜 줍니다.

찾은 얼굴을 화면에 출력해보겠습니다.

for dlib_rect in dlib_rects:
    left = dlib_rect.left()
    top = dlib_rect.top()
    right = dlib_rect.right()
    bottom = dlib_rect.bottom()

    cv2.rectangle(img_show, (left,top), (right,bottom), (0,255,0), 2, lineType=cv2.LINE_AA)

show_rgb =  cv2.cvtColor(img_show, cv2.COLOR_BGR2RGB)
plt.imshow(show_rgb)
plt.show()

출력 결과

정상적으로 얼굴을 잘 찾아내었습니다..!!

dlib.rectangleleft(), top(), right, bottom(), width(), height() 등의 멤버 함수를 가지고 있습니다. 이를 통해 인식한 얼굴의 좌표를 가지게 되고 이를 통해 얼굴의 꼭지점을 찾아 line을 나타내주는 역할을 합니다.


3단계

Face Landmark

dlib의 face landmark는 다음과 같이 각 지점마다 index 값을 가진 형태로 얼굴을 인식합니다.

각 지점의 index 값을 통해 스티커를 원하는 위치에 넣을 수 있습니다.

자 이제 dlib에 제공되는 모델을 사용하겠습니다. 먼저 공개되어 있는 file을 다운로드하고 압축을 풀어주겠습니다.

# colab 환경에서는 앞에 !를 붙혀야 명령어를 정상적으로 사용할 수 있습니다.
$ wget http://dlib.net/files/shape_predictor_68_face_landmarks.dat.bz2
$ mv shape_predictor_68_face_landmarks.dat.bz2 ~/me/camera_sticker/models
$ cd ~/me/camera_sticker && bzip2 -d ./models/shape_predictor_68_face_landmarks.dat.bz2

저장한 모델을 불러옵니다.

dlib_model_path = os.getenv('HOME')+'/me/camera_sticker/models/shape_predictor_68_face_landmarks.dat'
landmark = dlib.shape_predictor(dlib_model_path)

이제 모델을 사용해봐야죠 ! 아까 찾아낸 bounding box에서 face landmark를 찾아봅시다!

landmarks = []

# 얼굴 영역 박스 마다 face landmark를 찾아줍니다.
for dlib_rect in dlib_rects:
    location = landmark(img_rgb, dlib_rect)
    
    # face landmark 좌표를 list_location에 저장합니다.
    list_location = list(map(lambda p: (p.x, p.y), location.parts()))
    
    # 저장한 좌표를 landmarks에 넣어줍니다.
    landmarks.append(list_location)

print(len(landmarks[0]))

찾아낸 랜드마크를 영상으로 출력해보겠습니다.

for landmark in landmarks:
    for point in landmark:
        cv2.circle(img_show, point, 2, (0, 255, 255), -1)

show_rgb = cv2.cvtColor(img_show, cv2.COLOR_BGR2RGB)
plt.imshow(show_rgb)
plt.show()

출력 결과

정상적으로 나왔죠? 굉장히 뿌듯합니다 하하

자 아직 할일이 남았죠? 마지막 스티커를 붙혀야합니다. 다음으로 가보죠 고고씽~

4단계

이번 단계는 스티커를 원하는 좌표에 삽입하는 작업을 할 것입니다.

실습에 사용될 스티커

저는 고양이 스티커를 준비해보았습니다!! 여러분들은 각자 원하시는 스티커를 가져오셔서 사용하시면 됩니다!! (png 파일 적극 권장)

먼저 스티커를 넣기 전 어디에 넣을지 확인해야겠죠? 아까 3단계에서 face landmark에서는 각 지점마다 index 값을 갖는다고 했습니다. 저는 코에 넣어야하니 30번째에 넣도록 하겠습니다. 좌표값을 확인해보죠!

# 저장된 list 내에 있는 좌표점 중 30번째 index에 위치한 부위는 코부분입니다.
# 이 코의 중심점 부위의 좌표점을 출력해줍니다. 

for dlib_rect, landmark in zip(dlib_rects, landmarks):
    print (landmark[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))

출력 결과 :
(568, 471)
(x,y) : (568,278)
(w,h) : (386,386)

다음은 w,h 크기만큼 resize 해주고 x,y 좌표를 조정해줍니다. 추가적인 설명은 주석을 확인해주세용~~

# 스티커 이미지를 불러와 위에서 계산한 w,h의 크기만큼 resize해준다.
# 또 x,y 좌표를 조정한다. 그 이유는 이미지 시작점이 top-left부터이기 때문이다.

sticker_path = os.getenv('HOME')+'/me/camera_sticker/images/cat_sticker.png'
cat_sticker = cv2.imread(sticker_path)
cat_sticker = cv2.resize(cat_sticker, (w,h))
print (cat_sticker.shape)
x = x - w // 2
print('(x,y) : (%d,%d)'%(x, y))

출력 결과 :
(386, 386, 3)
(x,y) : (375,278)

자 이제 살을 조금 더 붙혀주죠! 이번 작업은 얼굴 이미지에서 인식된 얼굴이 범위 근처 또는 경계선에 겹쳐있을 수도 있는데, 그 상태에서 스티커를 넣게되면 이미지를 벗어나게 되는 상황이 생기게 됩니다. 이를 방지하기 위해 x,y값이 음수 일 경우 스티커가 잘리게 해주는 작업을 해보자!

if x < 0: 
    cat_sticker = cat_sticker[:, -x:]
    x = 0
if y < 0:
    cat_sticker = cat_sticker[-y:, :]
    y = 0
print ('(x,y) : (%d,%d)'%(x, y))

(x,y) : (375,278)

마지막입니다! 고양이 스티커를 지정해준 좌표에 넣겠습니다. 자세한 내용은 주석을 확인해주세요~~

# 모든 작업이 끝나고  np.where를 통해 cat_sticker가 255 즉, 횐색인 부분은
# sticker_area를 사용하고 255(흰색)가 아닌 부분을 cat_sticker를 사용한다고 보면 된다.
# 그래서 스티커 이미지의 배경색인 흰색은 얼굴 이미지 부분이 나오게 되고,
# 스티커 이미지의 스티커 부분은 스티커 이미지가 정상적으로 나오게 되는 것이다.

sticker_area = img_show[y:y+cat_sticker.shape[0], x:x+cat_sticker.shape[1]]
img_show[y:y+cat_sticker.shape[0], x:x+cat_sticker.shape[1]] = \
    np.where(cat_sticker==255,sticker_area,cat_sticker).astype(np.uint8)

plt.imshow(cv2.cvtColor(img_show, cv2.COLOR_BGR2RGB))
plt.show()

출력 결과

자 여기서 만약에 np.where 안의 값 cat_sticker==0이었다면 결과가 어떻게 나올까? 한번 해보자!

sticker_area = img_show[y:y+cat_sticker.shape[0], x:x+cat_sticker.shape[1]]
img_show[y:y+cat_sticker.shape[0], x:x+cat_sticker.shape[1]] = \
    np.where(cat_sticker==0,sticker_area,cat_sticker).astype(np.uint8)

plt.imshow(cv2.cvtColor(img_show, cv2.COLOR_BGR2RGB))
plt.show()

출력 결과

이렇게 우리가 원했던 바와는 정 반대로 나오게 될것이다.

자... 이제 마지막으로 bounding box와 landmark를 제거하고 최종 출력을 해보자! 나는 스티커를 투명하게 만든 상태를 출력 해보고 싶었다.
이때는 cv2.addWeighted()를 사용하면 된다!

# add 인스턴스에 cv2.addWeighted를 통해 sticker_area에 0.5만큼 곱해주고, cat_sticker에 0.3만큼 곱한 값을 주었다.
# 즉, 두 이미지를 addWeighted()를 통해 합치고 투명도를 준 것이다.
# 또 이 add를 12번째 줄 np.where에 cat_sticker가 0인 경우 sticker_area 대신 아까 합친 결과물인 add를 나타내게 해준다.

sticker_area = img_bgr[y:y +img_sticker.shape[0], x:x+img_sticker.shape[1]]

add = cv2.addWeighted(sticker_area, 0.5, img_sticker, 0.3, 0)

img_bgr[y:y +cat_sticker.shape[0], x:x+cat_sticker.shape[1]] = \
    np.where(cat_sticker==0,add,sticker_area).astype(np.uint8)

# 최종 이미지를 크게 하고싶다면 plt.figure를 이용해 사이즈를 조절하면 된다!
# plt.figure(figsize=(300,10)) 
plt.imshow(cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB))
plt.show()

출력 결과

만약에 나는 스티커를 투명하게 하기 싫다고 한다면 이렇게 출력해주면 된다.

sticker_area = img_bgr[y:y +img_sticker.shape[0], x:x+img_sticker.shape[1]]

img_bgr[y:y +cat_sticker.shape[0], x:x+cat_sticker.shape[1]] = \
    np.where(cat_sticker==255,sticker_area,cat_sticker).astype(np.uint8)

plt.imshow(cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB))
plt.show()

최종 결과

자 이렇게해서 카메라 스티커 작업이 모두 끝났다!
스티커를 투명하게 하는 부분에서 addWeighted 부분이 이해가 안가서 정말 고생 했지만 집단지성으로 성공했다.

또, 얼굴이 비스듬한 경우에 스티커를 어떻게 구현하는지 얼굴 크기 별로 스티커를 어떻게 조절해야하는지 여러가지 응용방법이 많았지만 오늘은 여기까지 하겠다. 시간이 난다면 응용한 결과를 추가로 올리도록 해야겠다... 끄읕~~~!!!

profile
Hello~ Welcome My Study Ground

0개의 댓글