이전 글에서 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))
\n cv2.circle(a,idx_to_coordinates[start_idx],radius=1, color=(255,255,255))
\n cv2.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/
댓글