본문 바로가기

AIFFLE/PROJECT

[CV] 고양이 수염 스티커 합성하기 (OpenCV 완전 정복기)

728x90
학습 내용
  • OpenCV 라이브러리 실습
  • 얼굴 검출 face detection 실습
  • 얼굴 랜드마크 face landmark 실습

 

개요

  • 이번 프로젝트는 얼굴을 인식하여 얼굴에 고양이 수염 스티커를 붙이는 프로젝트이다
  • 이번 프로젝트를 통해 얼굴 검출기술, 얼굴 랜드마크 기술, OpenCV 이미지 다루는 방법 들을 익힐 수 있었다.

프로젝트 결과

OpenCV 라이브러리

import cv2

# 이미지 읽기
cv2.imread('img.png', 0)

# 이미지 보기
cv2.imshow()

# 이미지 저장하기
cv2.imwrite()

 

이미지 읽기의 flag

cv2.IMREAD_COLOR : 이미지 파일을 Color로 읽어들입니다. 투명한 부분은 무시되며, Default값입니다. cv2.IMREAD_GRAYSCALE : 이미지를 Grayscale로 읽어 들입니다. 실제 이미지 처리시 중간단계로 많이 사용합니다. cv2.IMREAD_UNCHANGED : 이미지파일을 alpha channel까지 포함하여 읽어 들입니다.
  • 3개의 flag대신에 1, 0, -1을 사용해도 됨

 

얼굴 검출 face detection

  • dlib의 face detector
    • HOG(Histogram of Oriented Gradients)와 SVM(Support Vector Machine)을 사용해서 얼굴을 찾음
    • HOG : 이미지에서 색상의 변화량, 딥러닝이 나오기 이전에 다양하게 사용되던 방식
# dlib을 활용한 hog detector를 선언
detector_hog = dlib.get_frontal_face_detector() # 기본 얼굴 감지기를 반환

# dlib은 rgb 이미지를 입력으로 받기 때문에 cvtColor() 를 이용해서 opencv 의 bgr 이미지를 rgb로 변환
img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
dlib_rects = detector_hog(img_rgb, 1)   # (image, num of image pyramid)

 

 

얼굴 랜드마크 face landmark

  • face landmark localization 기술 : 이목구비의 위치를 추론하는 것
    • detection 의 결과물인 bounding box 로 잘라낸(crop) 얼굴 이미지를 이용
  • Object keypoint estimation 알고리즘 : Face landmark와 같이 객체 내부의 점을 찾는 기술
    • top-down : bounding box를 찾고 box 내부의 keypoint를 예측
    • bottom-up : 이미지 전체의 keypoint를 먼저 찾고 point 관계를 이용해 군집화 해서 box 생성

 

Dlib landmark localization

  • top-down 방식
  • 잘라진 얼굴 이미지에서 아래 68개의 이목구비 위치를 찾음

Dlib에 사용 되는 랜드마크 순서

 

이 때 점의 개수는 데이터셋과 논문마다 다름

예를 들면, AFLW 데이터셋은 21개를 사용하고 ibug 300w 데이터셋은 68개를 사용

 

Landmark 데이터셋 요약

 

Dlib의 제공되는 모델을 사용하여 이번 프로젝트를 진행

  • 먼저 공개되어 있는 weight file을 다운로드
  • 해당 모델파일은 bz2 압축파일 형태로 제공되어 압축을 풀어준 후 사용
wget http://dlib.net/files/shape_predictor_68_face_landmarks.dat.bz2
bzip2 -d ./models/shape_predictor_68_face_landmarks.dat.bz2

 

모델 불러오기 

model_path = 'aiffel/camera_sticker/models/shape_predictor_68_face_landmarks.dat'
landmark_predictor = dlib.shape_predictor(model_path)

 

landmark 실행 결과 확인

# 얼굴 영역 박스 마다 face landmark를 찾아내고 face landmark 좌표를 저장
list_landmarks = []
for dlib_rect in dlib_rects:
    points = landmark_predictor(img_rgb, dlib_rect)

    # 모든 landmark의 위치정보를 points 변수에 저장
    list_points = list(map(lambda p: (p.x, p.y), points.parts()))
    # 각각의 landmark 위치정보를 (x,y) 형태로 변환하여 list_points 리스트로 저장
    list_landmarks.append(list_points)

# landmark 이미지 출력
for landmark in list_landmarks:
    for point in landmark:
    	# cv2.circle: OpenCV의 원을 그리는 함수
        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()

 

스티커 적용하기

  • 33번과 30번의 위치를 기준으로 고양이 수염을 붙여주는 식으로 코드를 작성하였다.
  • 여러가지 실험을 해봤는데 결과적으로 33번을 이용해서 자리를 지정하는 것이 가장 결과가 좋았다.

# dlib_rects : 얼굴 영역을 저장하고 있는 값
# list_landmarks : 68개의 랜드마크 값 저장(이목구비 위치(x,y))

for dlib_rect, landmark in zip(dlib_rects, list_landmarks):
    print (landmark[33]) 
    x = landmark[33][0]
    y = landmark[33][1]
    w = h = dlib_rect.width() 
    
sticker_path = '이미지 경로'
img_sticker = cv2.imread(sticker_path) # 스티커 이미지를 불러옵니다 // cv2.imread(이미지 경로) → image객체 행렬을 반환
img_sticker = cv2.resize(img_sticker, (w,h)) # 스티커 이미지 조정 → w,h는 얼굴 영역의 가로를 차지하는 픽셀의 수(187) // cv2.resize(image객체 행렬, (가로 길이, 세로 길이))

refined_x = x - w // 2
refined_y = y - h // 2

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==255,sticker_area,img_sticker).astype(np.uint8)
    
plt.imshow(cv2.cvtColor(img_show, cv2.COLOR_BGR2RGB))
plt.show()

 

결과

각도에 따른 회전

  • 얼굴이 돌아가는 만큼 고양이 수염도 돌아가서 붙여주는 것이 좋을 것 같아서 이를 반영하여 코드를 개선했다.
  • 2번과 14번의 위치를 기준으로 각도를 조정하면 알맞게 조정이 될 것 같아 이를 기준으로 회전을 시켰다

for dlib_rect, landmark in zip(dlib_rects, list_landmarks): # 얼굴 영역을 저장하고 있는 값과 68개의 랜드마크를 저장하고 있는 값으로 반복문 실행
    print (landmark[33]) 
    x = landmark[33][0]
    y = landmark[33][1]
    w = h = dlib_rect.width() 
    
    # 2와 14번 위치를 찾는다
    dx = landmark[14][0] - landmark[2][0]
    dy = landmark[14][1] - landmark[2][1]
    angle = -np.degrees(np.arctan2(dy, dx))

    # Load the sticker image
    sticker_path = os.getenv('HOME') + '/aiffel/camera_sticker/images/cat-whiskers.png'
    img_sticker = cv2.imread(sticker_path)
    img_sticker = cv2.resize(img_sticker, (w, h))

    # Rotate the sticker image
    M = cv2.getRotationMatrix2D((w // 2, h // 2), angle, 1)
    img_sticker_rotated = cv2.warpAffine(img_sticker, M, (w, h))

    # Calculate the refined coordinates
    refined_x = x - w // 2
    refined_y = y - h // 2

    # Place the sticker on the image
    sticker_area = img_bgr[refined_y:refined_y + img_sticker_rotated.shape[0], refined_x:refined_x + img_sticker_rotated.shape[1]]
    
    img_bgr[refined_y:refined_y + img_sticker_rotated.shape[0], refined_x:refined_x + img_sticker_rotated.shape[1]] = \
        np.where(img_sticker_rotated == 255, sticker_area, img_sticker_rotated).astype(np.uint8)
    
plt.imshow(cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB))
plt.show()

  • 돌리니까 이미지가 약간 손실되는 문제가 발생함
  • 회전시킨뒤 정사각형의 형태로 다시 바꿔주어야 손실이 발생하지 않을 것으로 보인다

최종코드

def put_on_sticker(img_path):

    # Load dlib's face detector and landmark predictor
    detector = dlib.get_frontal_face_detector()
    predictor_path = '/aiffel/aiffel/camera_sticker/models/shape_predictor_68_face_landmarks.dat'
    predictor = dlib.shape_predictor(predictor_path)

    img_bgr = cv2.imread(img_path)

    # Detect faces in the image
    dlib_rects = detector(img_bgr, 1)

    # List to hold landmarks
    list_landmarks = []

    for dlib_rect in dlib_rects:
        landmarks = predictor(img_bgr, dlib_rect)
        list_landmarks.append([(p.x, p.y) for p in landmarks.parts()])

    # 각도에 맞게 돌린 이미지를 다시 정사각형의 형태로 맞춰주는 함수
    def get_largest_bounding_square(image, angle):
        (h, w) = image.shape[:2]
        center = (w // 2, h // 2)

        # Calculate the size of the new bounding box
        radians = np.deg2rad(angle)
        cos = np.abs(np.cos(radians))
        sin = np.abs(np.sin(radians))
        new_w = int((h * sin) + (w * cos))
        new_h = int((h * cos) + (w * sin))

        # The new dimension must be the larger of the two to make the box square
        dim = max(new_w, new_h)

        # Adjust rotation matrix to take into account translation
        M = cv2.getRotationMatrix2D(center, angle, 1)
        M[0, 2] += (dim - w) / 2
        M[1, 2] += (dim - h) / 2

        # Perform the rotation
        rotated = cv2.warpAffine(image, M, (dim, dim), flags=cv2.INTER_LINEAR, borderMode=cv2.BORDER_CONSTANT, borderValue=(255, 255, 255))

        return rotated

    # Process each detected face
    for dlib_rect, landmark in zip(dlib_rects, list_landmarks):
        print(landmark[33])
        x = landmark[33][0]
        y = landmark[33][1]
        w = h = dlib_rect.width()

        # 2와 14번 위치를 찾는다
        dx = landmark[14][0] - landmark[2][0]
        dy = landmark[14][1] - landmark[2][1]
        angle = -np.degrees(np.arctan2(dy, dx))

        # 스티커 이미지를 불러온다
        sticker_path = os.getenv('HOME') + '/aiffel/camera_sticker/images/cat-whiskers.png'
        img_sticker = cv2.imread(sticker_path)
        img_sticker = cv2.resize(img_sticker, (w, h))

        # 스티커 이미지를 돌리고 
        # 정사각형의 형태로 맞춰준다
        img_sticker_rotated = get_largest_bounding_square(img_sticker, angle)

        # Calculate the refined coordinates
        refined_x = x - img_sticker_rotated.shape[1] // 2
        refined_y = y - img_sticker_rotated.shape[0] // 2

        # Ensure the coordinates are within image bounds
        refined_x = max(0, min(refined_x, img_bgr.shape[1] - img_sticker_rotated.shape[1]))
        refined_y = max(0, min(refined_y, img_bgr.shape[0] - img_sticker_rotated.shape[0]))

        # Place the sticker on the image
        sticker_area = img_bgr[refined_y:refined_y + img_sticker_rotated.shape[0], refined_x:refined_x + img_sticker_rotated.shape[1]]
        img_bgr[refined_y:refined_y + img_sticker_rotated.shape[0], refined_x:refined_x + img_sticker_rotated.shape[1]] = \
            np.where(img_sticker_rotated == 255, sticker_area, img_sticker_rotated).astype(np.uint8)

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

 

성공적으로 나타난 것을 볼 수 있다.

 

최종정리

  • 손으로 얼굴을 많이 가리는 경우 : 인식안됨, 눈코입, 얼굴 윤곽이 보여야 인식이 되는 것으로 보임
  • 화질, 밝기 : 어느정도 얼굴의 형체만 보이면 인식가능
  • 회전정도 : 90도 이하의 회전정도는 인식이 가능한 것으로 보임

회고

  • 얼굴인식 오픈소스를 활용해서 여러가지 이미지 처리를 해볼 수 있어서 재미있었다
  • 90도까지 돌아간 얼굴, 정면에서 많이 벗어나서 얼굴 윤곽이 안잡히면 인식이 안되는 것을 보정할 수가 없어서 아쉽다

 

참고자료 / 최종 결과물

실습 과정 코드

 

aiffel/Exploration/Quest6/my_camera.ipynb at main · seongyeon1/aiffel

아이펠 딥러닝 개발자 과정. Contribute to seongyeon1/aiffel development by creating an account on GitHub.

github.com

 

실습 결과 확인 페이지

 

app

This app was built in Streamlit! Check it out and visit https://streamlit.io for more awesome community apps. 🎈

app-cat-whiskers.streamlit.app

 

 

728x90