人脸复杂形变,换脸,人脸融合等技术均使用到了三角剖分,因此本文介绍下Delaunay三角剖分及OpenCV实现
图 1. 左:使用 dlib 检测到地标的奥巴马总统图像。右:Delaunay 三角剖分图
给定平面中的一组点,三角剖分是指将平面细分为三角形,这些点为顶点。在图 1 中,左侧图像描绘了人脸的关键点,中间图像对应其三角剖分。一组关键点可能会有多种三角剖分的方式,但 Delaunay 三角剖分特别在其具有不错的属性:选择三角形时应确保没有点位于任何三角形的外接圆之内。
图 2:Delaunay 三角剖分有利于小角度
图 2. 显示了 4 点 A、B、C 和 D 的 Delaunay 三角剖分。在上图中,要使三角剖分成为有效的 Delaunay 三角剖分,C 点应位于三角形 ABD 的外接圆之外,而 A 点应位于三角形 ABD 的外接圆之外三角形 BCD 的外接圆。
Delaunay 三角剖分的一个有趣特性是它不支持“瘦”三角形(即具有一个大角度的三角形)。
图 2 显示了当点移动时三角剖分如何变化以选择“胖”三角形。在上图中,点 B 和 D 的 x 坐标位于 x = 1.5,在下图中,它们向右移动到 x = 1.75。在上图中,角度 ABC 和 ABD 很大,Delaunay 三角剖分在 B 和 D 之间创建了一条边,将两个大角度分成较小的角度 ABD、ADB、CDB 和 CBD。另一方面,在底部图像中,角度 BCD 太大,Delaunay 三角剖分创建边缘 AC 来分割大角度。
有许多算法可以找到一组点的 Delaunay 三角剖分。最明显(但不是最有效)的方法是从任何三角剖分开始,并检查任何三角形的外接圆是否包含另一个点。如果是,翻转边缘(如图 2 所示)并继续,直到没有三角形的外接圆包含一个点。
首先我们需要获取人脸 68 个特征点坐标,并写入 txt 文件,方便后续使用,这里我们使用OpenCV自带的dlib进行人脸检测及关键点检测
import dlib
import cv2
predictor_path = "./need_environment/shape_predictor_68_face_landmarks.dat"
png_path = "./need_environment/Obama.jpg"
txt_path = "./need_environment/points.txt"
f = open(txt_path, 'w+')
detector = dlib.get_frontal_face_detector()
# 相撞
predicator = dlib.shape_predictor(predictor_path)
win = dlib.image_window()
img1 = cv2.imread(png_path)
dets = detector(img1, 1)
print("Number of faces detected : {}".format(len(dets)))
for k, d in enumerate(dets):
print("Detection {} left:{} Top: {} Right {} Bottom {}".format(
k, d.left(), d.top(), d.right(), d.bottom()
))
lanmarks = [[p.x, p.y] for p in predicator(img1, d).parts()]
for idx, point in enumerate(lanmarks):
f.write(str(point[0]))
f.write("\t")
f.write(str(point[1]))
f.write('\n')
写入后,txt 中格式如下
利用图像大小创建一个矩形范围( 因为脸部特征点都是图中),创建一个 Subdiv2D 实例(后面两个图的绘制都会用到这个类),把点都插入创建的类中:
# Create an instance of Subdiv2d
subdiv = cv2.Subdiv2D(rect)
# Create an array of points
points = []
# Read in the points from a text file
with open("./need_environment/points.txt") as file:
for line in file:
x, y = line.split()
points.append((int(x), int(y)))
# Insert points into subdiv
for p in points:
subdiv.insert(p)
在原图上绘制 Delaunay 三角剖分并预览(逐线段绘制)
#Draw delaunay triangles
def draw_delaunay(img,subdiv,delaunay_color):
trangleList = subdiv.getTriangleList()
size = img.shape
r = (0,0,size[1],size[0])
for t in trangleList:
pt1 = (t[0],t[1])
pt2 = (t[2],t[3])
pt3 = (t[4],t[5])
if (rect_contains(r,pt1) and rect_contains(r,pt2) and rect_contains(r,pt3)):
cv2.line(img,pt1,pt2,delaunay_color,1)
cv2.line(img,pt2,pt3,delaunay_color,1)
cv2.line(img,pt3,pt1,delaunay_color,1)
#Insert points into subdiv
for p in points:
subdiv.insert(p)
#Show animate
if animate:
img_copy = img_orig.copy()
#Draw delaunay triangles
draw_delaunay(img_copy,subdiv,(255,255,255))
cv2.imshow(win_delaunary,img_copy)
cv2.waitKey(100)
import cv2
import numpy as np
import random
# Check if a point is insied a rectangle
def rect_contains(rect, point):
if point[0] < rect[0]:
return False
elif point[1] < rect[1]:
return False
elif point[0] > rect[2]:
return False
elif point[1] > rect[3]:
return False
return True
# Draw a point
def draw_point(img, p, color):
cv2.circle(img, p, 2, color)
# Draw delaunay triangles
def draw_delaunay(img, subdiv, delaunay_color):
trangleList = subdiv.getTriangleList()
size = img.shape
r = (0, 0, size[1], size[0])
for t in trangleList:
pt1 = (t[0], t[1])
pt2 = (t[2], t[3])
pt3 = (t[4], t[5])
if (rect_contains(r, pt1) and rect_contains(r, pt2) and rect_contains(r, pt3)):
cv2.line(img, pt1, pt2, delaunay_color, 1)
cv2.line(img, pt2, pt3, delaunay_color, 1)
cv2.line(img, pt3, pt1, delaunay_color, 1)
# Draw voronoi diagram
def draw_voronoi(img, subdiv):
(facets, centers) = subdiv.getVoronoiFacetList([])
for i in range(0, len(facets)):
ifacet_arr = []
for f in facets[i]:
ifacet_arr.append(f)
ifacet = np.array(ifacet_arr, np.int)
color = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
cv2.fillConvexPoly(img, ifacet, color)
ifacets = np.array([ifacet])
cv2.polylines(img, ifacets, True, (0, 0, 0), 1)
cv2.circle(img, (centers[i][0], centers[i][1]), 3, (0, 0, 0))
if __name__ == '__main__':
# Define window names;
win_delaunary = "Delaunay Triangulation"
win_voronoi = "Voronoi Diagram"
# Turn on animations while drawing triangles
animate = True
# Define colors for drawing
delaunary_color = (255, 255, 255)
points_color = (0, 0, 255)
# Read in the image
img_path = "./need_environment/Obama.jpg"
img = cv2.imread(img_path)
# Keep a copy around
img_orig = img.copy()
# Rectangle to be used with Subdiv2D
size = img.shape
rect = (0, 0, size[1], size[0])
# Create an instance of Subdiv2d
subdiv = cv2.Subdiv2D(rect)
# Create an array of points
points = []
# Read in the points from a text file
with open("./need_environment/points.txt") as file:
for line in file:
x, y = line.split()
points.append((int(x), int(y)))
# Insert points into subdiv
for p in points:
subdiv.insert(p)
# Show animate
if animate:
img_copy = img_orig.copy()
# Draw delaunay triangles
draw_delaunay(img_copy, subdiv, (255, 255, 255))
cv2.imshow(win_delaunary, img_copy)
cv2.waitKey(100)
# Draw delaunary triangles
draw_delaunay(img, subdiv, (255, 255, 255))
# Draw points
for p in points:
draw_point(img, p, (0, 0, 255))
# Allocate space for Voroni Diagram
img_voronoi = np.zeros(img.shape, dtype=img.dtype)
# Draw Voonoi diagram
draw_voronoi(img_voronoi, subdiv)
# Show results
cv2.imshow(win_delaunary, img)
cv2.imshow(win_voronoi, img_voronoi)
cv2.waitKey(0)
预览效果如下:
Delaunay 三角剖分广泛应用于人脸识别、融合、换脸等人脸算法,本文仅通过 OpenCV 的 Subdiv2D API 实现此功能,关于详细原理仍需参考原论文。
利用 OpenCV-Python 绘制人脸 Delaunay 三角剖分和 Voronoi 图 - 编程猎人
Delaunay Triangulation & Voronoi Diagram With OpenCV( C++/Python)