使用 OpenCV 进行 Delaunay 三角剖分

1. 引言

人脸复杂形变,换脸,人脸融合等技术均使用到了三角剖分,因此本文介绍下Delaunay三角剖分及OpenCV实现

2. 介绍

使用 OpenCV 进行 Delaunay 三角剖分_第1张图片
图 1. 左:使用 dlib 检测到地标的奥巴马总统图像。右:Delaunay 三角剖分图

给定平面中的一组点,三角剖分是指将平面细分为三角形,这些点为顶点。在图 1 中,左侧图像描绘了人脸的关键点,中间图像对应其三角剖分。一组关键点可能会有多种三角剖分的方式,但 Delaunay 三角剖分特别在其具有不错的属性:选择三角形时应确保没有点位于任何三角形的外接圆之内

使用 OpenCV 进行 Delaunay 三角剖分_第2张图片


图 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 所示)并继续,直到没有三角形的外接圆包含一个点。

3. OpenCV实现

3.1 获取人脸关键点坐标

首先我们需要获取人脸 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 中格式如下

使用 OpenCV 进行 Delaunay 三角剖分_第3张图片

3.2 创建人脸框范围及及Subdiv2D实例

利用图像大小创建一个矩形范围( 因为脸部特征点都是图中),创建一个 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)

3.3 绘制 Delaunay 三角剖分

在原图上绘制 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)

3.4 完整代码

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)


预览效果如下:

使用 OpenCV 进行 Delaunay 三角剖分_第4张图片

4. 总结

Delaunay 三角剖分广泛应用于人脸识别、融合、换脸等人脸算法,本文仅通过 OpenCV 的 Subdiv2D API 实现此功能,关于详细原理仍需参考原论文。

5. 参考

利用 OpenCV-Python 绘制人脸 Delaunay 三角剖分和 Voronoi 图 - 编程猎人
Delaunay Triangulation & Voronoi Diagram With OpenCV( C++/Python)

你可能感兴趣的:(人脸关键点检测,opencv,人工智能,计算机视觉)