본문 바로가기
Projects/Face Toy Project

[GUI 기반 Face Toy Project -3] 들로네 삼각 변환(Delaunay triangulation)

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

이전 글에 실시간 face mesh 출력을 해보았다.

 

GUI 기반 Face Toy Project -2

지난 시간에 PyQt5를 이용해서 간단한 baseline을 만들고 스마트폰 카메라를 웹캠처럼 사용해보았다. GUI 기반 Face Toy Project - 1 더보기 PyQt5와 다양한 라이브러리 경험을 위한 Toy 프로젝트이다. Baseline

yeeca.tistory.com

추가적인 코드를 수정해서 배경을 제거하고 face mesh 출력만 하게 했다.

흉측한 모습

위에서 보이는 것처럼 점과 점의 연결이 모두 삼각형을 이루고 있으며 2D로 그려진 그물망이 3D처럼 보인다.

face mesh는 Delaunay triangulation를 이용하여 사람 얼굴의 3D mesh를 생성하는 작업이 포함된다. 

mediapipe의 FaceMesh를 사용하면 face landmarks의 x,y,z 예측 좌표값을 얻을 수 있다.

오늘은 그 좌표값 x,y를 직접 사용하여  Delaunay triangulation를 이용한 face swap을 해볼 것이다.

책과 참고한 사이트는 모두 dlib으로 face landmarks detection을 수행했지만 나는 앞으로 mediapipe를 좀 더 사용할 것 같으므로 mediapipe를 사용한다.

둘의 차이점은 dlib으로 탐지되는 landmark 개수는 68개지만 mediapipe는 468개다.

과정은 다음과 같다.

1. 이미지 로드

2. landmarks detection

3. 볼록 선체(convex hull) 구하기

- 검출된 얼굴 영역에서, cv2.convexHull() 함수를 사용하여 볼록 선체를 구한다. 볼록 선체는 얼굴의 경계를 구성하는 점들 중, 가장 바깥쪽에 위치한 점들을 연결하여 구성한 볼록 다각형이다.

4. 볼록 선체 안의 들로네 삼각형 좌표 구하기

- 볼록 선체 안의 점들을 cv2.Subdiv2D() 함수를 사용하여 들로네 삼각형으로 분할한다. cv2.Subdiv2D() 함수는 점들을 입력으로 받아들여, 삼각형 분할을 수행한다. 이 함수를 이용하여, 얼굴 이미지에서 검출된 볼록 선체 안의 점들을 입력으로 넣어주면, 들로네 삼각형으로 분할된 삼각형의 좌표를 얻을 수 있다.

5. 각 삼각형 좌료로 어핀 변환

- 세 개의 입력점과 세 개의 출력점을 입력으로 받아들여, 어핀 변환 행렬을 계산한다. 각 들로네 삼각형마다, 원본 이미지와 대상 이미지의 세 개의 좌표를 입력으로 넣어주어 어핀 변환 행렬을 계산한다.

6. 볼록 선체를 마스크로 써서 얼굴 합성

코드는 다음과 같다.

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

# Load face mesh model
mpFaceMesh = mp.solutions.face_mesh
faceMesh = mpFaceMesh.FaceMesh(max_num_faces=1)

# mpDraw.draw_landmarks() 함수 일부 문장 복사
def normalized_to_pixel_coordinates(
    normalized_x: float, normalized_y: float, image_width: int,
    image_height: int) :
  """Converts normalized value pair to pixel coordinates."""

  # Checks if the float value is between 0 and 1.
  def is_valid_normalized_value(value: float) -> bool:
    return (value > 0 or math.isclose(0, value)) and (value < 1 or
                                                      math.isclose(1, value))

  if not (is_valid_normalized_value(normalized_x) and
          is_valid_normalized_value(normalized_y)):
    return None
  x_px = min(math.floor(normalized_x * image_width), image_width - 1)
  y_px = min(math.floor(normalized_y * image_height), image_height - 1)
  return x_px, y_px

# face landmarks detection and get points
def getPoints(img,cvt_color=None,return_cvt_img=True):
    img_rows,img_cols = img.shape[:2]
    if cvt_color:
        img = cv2.cvtColor(img,cvt_color)
    else:
        img = cv2.cvtColor(img,cv2.COLOR_BGR2RGB)
         
    result = faceMesh.process(img) # one landmarks of faces
   
    # landmarks x,y pair
    points = []
    for landmark in result.multi_face_landmarks[0].landmark: # normalized x,y,z points
        points.append(normalized_to_pixel_coordinates(landmark.x,landmark.y,img_cols,img_rows))
        # print(landmark.x,landmark.y)

    if return_cvt_img:
        return points, img
    else:
        return points

# Delaunay triangulation
def getTriangles(img,points):
    h,w = img.shape[:2]
    subdiv = cv2.Subdiv2D((0,0,w,h));
    subdiv.insert(points)
    triangleList = subdiv.getTriangleList()
    triangles = []
    for t in triangleList:
        pt = t.reshape(-1,2)
        if not (pt < 0).sum() and not (pt[:,0] > h).sum() \
                              and not (pt[:,1] > w).sum():
            indice = []
            for i in range(0,3):
                for j in range(0,len(points)):
                    if abs(pt[i][0]-points[j][0]) < 1.0 \
                        and abs(pt[i][1]-points[j][1]) < 1.0:
                        indice.append(j)
            if len(indice) == 3:
                triangles.append(indice)
   
    return triangles

# affine
def warpTriangle(img1,img2,pts1,pts2):
    x1,y1,w1,h1 = cv2.boundingRect(np.float32([pts1])) # x,y : left-top points / w,h : length
    x2,y2,w2,h2 = cv2.boundingRect(np.float32([pts2]))

    roi1 = img1[y1:y1+h1, x1:x1+w1]
    roi2 = img2[y2:y2+h2, x2:x2+w2]
   
    offset1 = np.zeros((3,2),dtype=np.float32)
    offset2 = np.zeros((3,2),dtype=np.float32)
    for i in range(3):
        offset1[i][0], offset1[i][1] = pts1[i][0]-x1,pts1[i][1]-y1
        offset2[i][0], offset2[i][1] = pts2[i][0]-x2,pts2[i][1]-y2

    mtrx = cv2.getAffineTransform(offset1,offset2)
   
    warped = cv2.warpAffine(roi1,mtrx,(w2,h2),None,cv2.INTER_LINEAR,cv2.BORDER_REFLECT101)
    mask = np.zeros((h2,w2),dtype=np.uint8)
    cv2.fillConvexPoly(mask,np.int32(offset2),(255))
   
    warped_masked = cv2.bitwise_and(warped,warped,mask=mask)
    roi2_masked = cv2.bitwise_and(roi2,roi2,mask=cv2.bitwise_not(mask))
    roi2_masked = roi2_masked + warped_masked
   
    img2[y2:y2+h2, x2:x2+w2] = roi2_masked

# main
# 1. 이미지 로드
img2 = cv2.imread("images.jpg")
img1 = cv2.imread("images2.jfif")

# 2. landmarks detection
points1, img1 = getPoints(img1)
points2, img2 = getPoints(img2)

img_draw = img2.copy()

# 3. 볼록 선체(convex hull) 구하기
hullInedx = cv2.convexHull(np.array(points2),returnPoints=False)
hull1 = [points1[int(idx)] for idx in hullInedx]
hull2 = [points2[int(idx)] for idx in hullInedx]

# 4. 볼록 선체 안 들로네 삼각형 좌표 구하기
triangles = getTriangles(img2,hull2)

# 5. 각 삼각형 좌표로 어핀 변환
for i in range(0,len(triangles)):
    t1 = [hull1[triangles[i][j]] for j in range(3)]
    t2 = [hull2[triangles[i][j]] for j in range(3)]
    warpTriangle(img1,img_draw,t1,t2)
   
# 6. 볼록 선체를 마스크로 써서 얼굴 합성
mask = np.zeros(img2.shape[:2],dtype=img2.dtype)
cv2.fillConvexPoly(mask,np.int32(hull2),(255,255,255))
r = cv2.boundingRect(np.float32(hull2))
center = ((r[0]+int(r[2]/2), r[1]+int(r[3]/2)))
output = cv2.seamlessClone(np.uint8(img_draw),img2,mask,center,cv2.NORMAL_CLONE)

plt.imshow(output)
plt.show()

주피터 노트북에서 실행한 결과는 다음과 같다.

위 코드에서 img1의 landmarks만 따로 표기하면 다음과 같다.

a,img1 = getPoints(img1)
a = np.array(a)
plt.scatter(a[:,0],a[:,1],s=10,c='green')
plt.imshow(cv2.cvtColor(img1,cv2.COLOR_BGR2RGB))

볼록 선체 좌표를 이용해서 그리면

cv2.drawContours(img1,[hull1],0,(255,0,0),1)

이렇게 나온다

가장 중요한 face mesh 그리기도 해보면

# landmarks
a,img1 = getPoints(img1.copy())
# convex hull
cv2.drawContours(img1,[hull1],0,(255,0,0),1)
# delaunay triangulation
h,w = img1.shape[:2]
subdiv = cv2.Subdiv2D((0,0,w,h))
subdiv.insert(a)
triangleList = subdiv.getTriangleList()

for t in triangleList:
    pts = t.reshape(-1,2).astype(np.int32)
    if (pts < 0).sum() or (pts[:,0]>w).sum() or (pts[:,1] > h).sum():
        continue
    cv2.polylines(img1,[pts],True,(0,0,255),1,cv2.LINE_4)

plt.scatter(a[:,0],a[:,1],s=1,c='green')
plt.imshow(cv2.cvtColor(img1,cv2.COLOR_BGR2RGB))
plt.show()

face mesh 결과가 잘 안보여서 landmark의 굵기를 줄였다

 

전체 코드에서 dlib을 사용한다면 getPoints함수만 바꾸면 된다.

import dlib
import numpy as np
import cv2

img_path = r'/content/drive/MyDrive/testcode/sample_face.jpg'

detector = dlib.get_frontal_face_detector()
predictor = dlib.shape_predictor('./shape_predictor_68_face_landmarks.dat')

img = cv2.imread(img_path)
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)

rects = detector(gray)
points = []
for rect in rects:
  shape = predictor(gray,rect)
  for i in range(68):
    part = shape.part(i)
    points.append((part.x,part.y))

np.array(points).shape
 

실시간 적용 코드는 다음 글에 있다.

reference : 파이썬으로 만나는 OpenCV 프로젝트, https://www.analyticsvidhya.com/blog/2021/10/face-mesh-application-using-opencv-and-dlib/

728x90
반응형

댓글