본문 바로가기
Projects/Face Toy Project

[GUI 기반 Face Toy Project -5] face alignment opencv

by apsdfjoi 2023. 4. 10.
728x90
반응형

이전 글에서 real time face swap (replacement)를 했다.

 

[GUI 기반 Face Toy Project -4] real time face swap

이전 글에서 2개의 얼굴 사진을 이용해서 face swap을 했다. [GUI 기반 Face Toy Project -3] 들로네 삼각 변환(Delaunay triangulation) 이전 글에 실시간 face mesh 출력을 해보았다. GUI 기반 Face Toy Project -2 지난 시

yeeca.tistory.com

이번에는 face morphing을 시도했는데 결과가 mask값이 0혹은 255로 고정된 출력으로 잘 되지 않았다.

그 이유는 face morphing을 위해서는 두 얼굴의 특징점(landmarks)이 서로 매핑이 되어야 하기 때문이다.

따라서 두 얼굴의 alignment가 선행되어야 한다. 이번 글에서는 alignment에 대해 다룬다.

mediapipe를 사용하여 landmarks detection을 진행할 것이다.

chatGPT가 써준 코드는 다음과 같다.

import cv2
import mediapipe as mp


def align_face(img):
    mp_drawing = mp.solutions.drawing_utils
    mp_face_mesh = mp.solutions.face_mesh
    with mp_face_mesh.FaceMesh(
            static_image_mode=True,
            max_num_faces=1,
            min_detection_confidence=0.5) as face_mesh:
        # Convert the BGR image to RGB.
        image = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
        # To improve performance, optionally mark the image as not writeable to
        # pass by reference.
        image.flags.writeable = False
        results = face_mesh.process(image)

        # Draw the face mesh annotations on the image.
        image.flags.writeable = True
        image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
        if results.multi_face_landmarks:
            for face_landmarks in results.multi_face_landmarks:
                # Convert landmarks to numpy array
                for lmk in face_landmarks.landmark:
                    landmarks = mp_drawing._normalized_to_pixel_coordinates(
                    lmk.x,lmk.y, img.shape[1], img.shape[0])
                landmarks = landmarks.astype(int)

                # Calculate the center of mass for the face landmarks
                center = landmarks.mean(axis=0)

                # Calculate the angle between the eyes and the horizontal plane
                left_eye = landmarks[mp_face_mesh.FACEMESH_LEFT_EYE]
                right_eye = landmarks[mp_face_mesh.FACEMESH_RIGHT_EYE]
                dY = left_eye[1] - right_eye[1]
                dX = left_eye[0] - right_eye[0]
                angle = np.degrees(np.arctan2(dY, dX))

                # Rotate and crop the image around the center of mass
                M = cv2.getRotationMatrix2D(tuple(center), angle, 1.0)
                out_img = cv2.warpAffine(img, M, (img.shape[1], img.shape[0]),
                                         flags=cv2.INTER_CUBIC)
                out_img = out_img[landmarks[:, 1].min():landmarks[:, 1].max(),
                          landmarks[:, 0].min():landmarks[:, 0].max()]
                return out_img
        else:
            return img

라이브러리 사용법이 틀려서 이 코드를 그대로 사용하면 절대 실행이 안된다.

하지만 과정은 참고할 수 있다.

핵심은 landmarks detection -> left, right eye landmarks를 사용하여 center, angle 계산 및 회전이다.

위 코드와 다른 reperence를 참고하여 내가 작성한 코드는 아래와 같다.

import cv2
import matplotlib.pyplot as plt
import mediapipe as mp
import numpy as np

from devlib import *


mpFaceMesh = mp.solutions.face_mesh

# img1,img2 = imgs_read_rgb('./image.jpg','./image2.jpg')
img1 = imgs_read_rgb('./image.jpg')[0]
img1 = rotate_img(img1,11)
# landmarks1, landmarks2 = imgs_get_landmarks(img1,img2)
idx_to_coordinates,landmarks = get_idx_to_coordinates(img1)
plt.imshow(img1)

원본 이미지에 11도 회전

def align(img,idx_to_coordinates,scale):
    connections = [mpFaceMesh.FACEMESH_LEFT_EYE,mpFaceMesh.FACEMESH_RIGHT_EYE]
    left_eye = get_connection_points(idx_to_coordinates,connections[0])
    right_eye = get_connection_points(idx_to_coordinates,connections[1])

    leftEyeCenter, rightEyeCenter, eyesCenter = get_eye_center(left_eye,right_eye)
    angle = get_angle(rightEyeCenter,leftEyeCenter)

    M = cv2.getRotationMatrix2D(eyesCenter, angle, scale)
    out_img = cv2.warpAffine(img, M, (img.shape[1], img.shape[0]),
                            flags=cv2.INTER_CUBIC)
    # out_img = out_img[landmarks[:, 1].min():landmarks[:, 1].max(),
    #             landmarks[:, 0].min():landmarks[:, 0].max()]
    plt.imshow(out_img)
    return out_img

img1= align(img1,idx_to_coordinates,0.75)

위 align() 코드에서 주석 해제했을 때

 

landmarks detection 결과는 468개의 좌표 값이다. 이 중 face alignment에 사용되는 좌표는 양 눈에 해당하는 좌표만 사용한다. 양 눈 좌표의 ID는 mediapipe의 face_mesh.py 파일에서 확인할 수 있다.

# pylint: disable=unused-import
from mediapipe.python.solutions.face_mesh_connections import FACEMESH_CONTOURS
from mediapipe.python.solutions.face_mesh_connections import FACEMESH_FACE_OVAL
from mediapipe.python.solutions.face_mesh_connections import FACEMESH_IRISES
from mediapipe.python.solutions.face_mesh_connections import FACEMESH_LEFT_EYE
from mediapipe.python.solutions.face_mesh_connections import FACEMESH_LEFT_EYEBROW
from mediapipe.python.solutions.face_mesh_connections import FACEMESH_LEFT_IRIS
from mediapipe.python.solutions.face_mesh_connections import FACEMESH_LIPS
from mediapipe.python.solutions.face_mesh_connections import FACEMESH_RIGHT_EYE
from mediapipe.python.solutions.face_mesh_connections import FACEMESH_RIGHT_EYEBROW
from mediapipe.python.solutions.face_mesh_connections import FACEMESH_RIGHT_IRIS
from mediapipe.python.solutions.face_mesh_connections import FACEMESH_TESSELATION
# pylint: enable=unused-import

idx_to_coordinates 변수와 위 connection 값을 사용하여 시각화 할 수 있다.

vis_coordinates(img1,idx_to_coordinates,connection=mpFaceMesh.FACEMESH_CONTOURS)

vis_coordinates(img1,idx_to_coordinates,connection=mpFaceMesh.FACEMESH_LEFT_EYE)

vis_coordinates(img1,idx_to_coordinates,connection=mpFaceMesh.FACEMESH_RIGHT_EYE)

왜인지 모르겠지만 양눈 좌우 좌표값이 바꼈다. 당장 코드에는 크게 지장없으니 일단 넘어간다.

vis_coordinates(img1,idx_to_coordinates,connection=mpFaceMesh.FACEMESH_TESSELATION)

지난번에 직접 구한 들로네 삼각 변환, 직접 구할 필요 없었다.

코드 설명

def get_idx_to_coordinates(img):
    '''
    a = np.zeros_like(img)  
     
    for conn in mpFaceMesh.FACEMESH_CONTOURS:
    start_idx = conn[0]
    end_idx = conn[1]
    cv2.line(a,idx_to_coordinates[start_idx],idx_to_coordinates[end_idx],color=(255,255,255))        
    \ncv2.circle(a,idx_to_coordinates[start_idx],radius=1, color=(255,255,255))        
    \ncv2.circle(a,idx_to_coordinates[end_idx],radius=2,color=(255,255,255))        

    plt.imshow(a)
    '''
    scale = 1
    #img = cv2.resize(img,(img.shape[1]*2,img.shape[0]*2))
    image_cols, image_rows = img.shape[1]*scale,img.shape[0]*scale
    landmark_list = []
    with mp.solutions.face_mesh.FaceMesh() as face_mesh:
        results = face_mesh.process(img)
        if results.multi_face_landmarks:
            for face_landmarks in results.multi_face_landmarks:
                landmark_list.append(face_landmarks)
               
    idx_to_coordinates = {}
    points = []
    for idx,landmark in enumerate(landmark_list[0].landmark):
        landmark_px = normalized_to_pixel_coordinates(landmark.x, landmark.y,
                                                    image_cols, image_rows)
        if landmark_px:
            idx_to_coordinates[idx] = landmark_px
            points.append(landmark_px)
    if idx_to_coordinates:
        return idx_to_coordinates,np.array(points)
    else:
        return None

mediapipe의 face_mesh로 landmarks detection을 수행한 결과 x,y은 0~1사이 값이다. 이미지 사이즈에 맞게 곱해주는 normalized_to_pixel_coordinates()함수를 사용하여 실제 좌표값을 얻는다. 이 함수는 mediapipe의 drawing_utills.py에서 복사하여 가져왔다. 그 다음 각 좌표값에 ID를 부여한다. 이는 이미 지정된 face_mesh의 eye에 해당하는 ID값만 사용하기 위함이다.

{0: (142, 100), 1: (141, 89), 2: (141, 93), 3: (136, 80), 4: (140, 86), 5: (139, 82),,,,,

align() 함수에 있는 get_connection_points()에서 이 값을 사용하여 양 눈 좌표를 가져온다.

def get_connection_points(idx_to_coordinates,connection):
    return np.array([[idx_to_coordinates[conn[0]],idx_to_coordinates[conn[1]]] for conn in connection]).reshape(-1,2)

get_eye_center는 각 눈 좌표들의 중앙 값과 회전축이될 center 값을 반환한다.

def get_eye_center(left_eye,right_eye):
    '''
    return:
        ndarray:
        left_eye_center,
        right_eye_center,
        eyes_center
    '''

    leftEyeCenter = left_eye.mean(axis=0)
    rightEyeCenter = right_eye.mean(axis=0)
    eyesCenter = ((leftEyeCenter[0] + rightEyeCenter[0]) // 2,
                (leftEyeCenter[1] + rightEyeCenter[1]) // 2)
    return leftEyeCenter,rightEyeCenter, np.array(eyesCenter,dtype=np.float16)

get_angle()은 두 좌표에서 각도를 계산한다.

def get_angle(pts1,pts2):
    dY = pts1[1] - pts2[1]
    dX = pts1[0] - pts2[0]
    angle = np.degrees(np.arctan2(dY, dX))
    if dX < 0:
        angle -= 180
    return angle

위에서 왼쪽 눈과 오른쪽 눈의 좌표값이 반대였으므로 조건문으로 pts1과 pts2 위치를 바꾸던가 dY,dX에 -1을 곱하던가 회전각을 180도 빼주거나 추가하면 된다.

이렇게 얻은 angle, center 값으로 이미지 회전을 위한 행렬을 만들 수 있다. 이미지에 이 행렬을 사용하여 어핀 연산을 하면 alignment 끝이다. scale은 줄이면 좀 더 멀리서 본 시점의 이미지를 얻을 수 있다.

 

reference : chatGPT, https://pyimagesearch.com/2017/05/22/face-alignment-with-opencv-and-python/

728x90
반응형

댓글