Delaunay Image Triangulation

Hi,这是一篇介绍如何将德劳内三角剖分算法应用于风格化任意图像的博客。
原理部分请参考 Delaunay Image Triangulation 等一系列博客。
本文关注于具体地 Python 实现。

1. 效果

原图 输出
Delaunay Image Triangulation_第1张图片 Delaunay Image Triangulation_第2张图片
Delaunay Image Triangulation_第3张图片 Delaunay Image Triangulation_第4张图片

2. 算法简介

好的,下边的 3 页 PPt 应该非常清晰地阐述了该算法的具体原理。完整 PPt 请私聊博主获取,欢迎读者指点讨论哈!

(1)准备阶段

Delaunay Image Triangulation_第5张图片
非常好懂哈,
1️⃣ 先对图像用 Sobel 算子计算边缘梯度,这里其实也是可以用 Canny 算子计算,就可以忽略质量百分比阈值化(% Mass Threshold)的处理,直接采样关键点;
2️⃣ 所谓百分比阈值化就是:将梯度谱拉成长向量(np.flatten),升序排序,设置某个比例,比如 0.9,取第 90%-th 的值为阈值,作二值化;之后用 np.where 获取为 1 的点的坐标集合,混匀采样,比如取 10% 的点,得到点的集合 points
3️⃣ 初始化三角剖分的结果为上图 4 的两个大△。

(2)关键点循环(Point-loop)

点循环阶段,遍历上边 points 的每个点,下边两个图演示了两个点——
Delaunay Image Triangulation_第6张图片
对于点 p 0 p_0 p0
1️⃣ 假设有三角形栈 triangles,遍历每个△,判断点 p 0 p_0 p0 是否在这个△的外接圆内?若是,则将这个三角形的三角边都添加到边栈 edges (initialized as []) 上,如上图 2 的由浅至深的蓝色的线段;
2️⃣ 检查 edges 中是否有重复出现的边,若有,说明它是某两个△的公共边,删除它,如上图 3 的两条红色线段;
3️⃣ 以点 p 0 p_0 p0 为中心辐射,与 edges 中剩下的每一条边构造新的△装载到 cache 中,用于更新三角形栈 triangles = cache
4️⃣ 结束。

Delaunay Image Triangulation_第7张图片
对于点 p 1 p_1 p1,大概四个步骤都是相同的,特别地是:
1️⃣ 点 p 1 p_1 p1 并没有在上边和左边的两个△的外接圆内,因此,这两个△保留,直接装载到 cache 中,其边缘不需要添加到 edges 中。

3. 代码

Fine,上边的图示已经很清晰了,讲解代码比较麻烦,就直接贴了,主要的函数是 Delaunay.insertget_triangle,完整可执行代码如下,其中保留了一些可视化中间结果的代码:

import math
import matplotlib.pyplot as plt 
import numpy as np 
import os 
import cv2 
from tqdm import tqdm 
from matplotlib import gridspec as gridspec
import random
from PIL import Image


class Circle(object):
    def __init__(self, x, y, r2):
        '''
        @param
            `x` --x_coordinate_of_center --type=floaat
            `y` --y_coordinate_of_center --type=float
            `r` --radius --type=float
        '''
        self.x = x 
        self.y = y 
        self.r2 = r2


class Edge(object):
    def __init__(self, p0, p1):
        '''
        @param
            `p0, p1` --type=np.array --shape=(2,)
        '''
        self.p0 = p0 
        self.p1 = p1


class Triangle(object):
    def __init__(self):
        self.circumcircle = None # --type=Circle
        self.vertices = None        # --type=np.array   --shape=(3,2)
        self.edges = None        # --type=list --len=3


def eq_point(p, q):
    '''
    @param
        `p,q` --type=np.array --shape=(2,)
    '''
    dx = p[0]-q[0]
    dy = p[1]-q[1]
    dx = abs(dx)
    dy = abs(dy)
    if dx < 1e-4 and dy < 1e-4:
        return True
    return False 


def eq_edge(e, f):
    '''
    @param
        `e,f` --type=Edge
    '''
    if eq_point(e.p0, f.p0) and eq_point(e.p1, f.p1) or eq_point(e.p0, f.p1) and eq_point(e.p1, f.p0):
        return True
    return False 
        

def get_triangle(p0, p1, p2):
    '''
    @param
        `p0, p1, p2` --type=np.array --shape=(2,)
    '''
    p0.astype(np.float32)
    p1.astype(np.float32)
    p2.astype(np.float32)

    triangle = Triangle()
    triangle.vertices = np.array([p0, p1, p2], dtype=np.float32)
    triangle.edges = [Edge(p0, p1), Edge(p1, p2), Edge(p2, p0)]
    
    ## 计算外接圆
    ax, ay = p1[0]-p0[0], p1[1]-p0[1]
    bx, by = p2[0]-p0[0], p2[1]-p0[1]
    m = p1[0]*p1[0]-p0[0]*p0[0]+p1[1]*p1[1]-p0[1]*p0[1]#np.power(p1, 2).sum() - np.power(p0, 2).sum()
    u = p2[0]*p2[0]-p0[0]*p0[0]+p2[1]*p2[1]-p0[1]*p0[1]#np.power(p2, 2).sum() - np.power(p0, 2).sum()
    s = 1. / (2. * (ax*by-bx*ay) + 1e-6)

    ctr_x = (m*(p2[1]-p0[1]) + u*(p0[1]-p1[1])) * s
    ctr_y = (m*(p0[0]-p2[0]) + u*(p1[0]-p0[0])) * s

    dx = p0[0] - ctr_x
    dy = p0[1] - ctr_y 
    r2 = math.pow(dx, 2) + math.pow(dy, 2)
    triangle.circumcircle = Circle(ctr_x, ctr_y, r2)

    return triangle


def in_triangle(p0, p1, p2, q):
    '''
    @param
        `p0,p1,p2,q` --type=np.array --shape=(2,)
    '''
    e0 = p1-p0
    e1 = p2-p1
    e2 = p0-p2
    c0 = np.cross(e0, q-p0)
    c1 = np.cross(e1, q-p1)
    c2 = np.cross(e2, q-p2)
    if c0 >= 0 and c1 >= 0 and c2 >= 0 or c0 <= 0 and c1 <= 0 and c2 <= 0:
        return True
    return False


class Delaunay(object):
    def __init__(self, width, height):
        self.width = width
        self.height = height
        self.triangles = None

        self.clear()
    
    def clear(self):
        p0 = np.array([0., 0.], dtype=np.float32)
        p1 = np.array([self.width, 0.], dtype=np.float32)
        p2 = np.array([self.width, self.height], dtype=np.float32)
        p3 = np.array([0., self.height], dtype=np.float32)
        self.triangles = [get_triangle(p0, p1, p2), get_triangle(p0, p2, p3)]

    def insert(self, points):
        '''
        @param
            `points` --type=np.array --shape=(N,2)
        '''
        for point in points:
            x, y = point
            triangles = self.triangles
            edges = []
            cache = []  ## cache for new triangles

            min_d = float("inf")
            for triangle in triangles:
                circle = triangle.circumcircle
                dx = circle.x - x 
                dy = circle.y - y 
                dist2 = math.pow(dx, 2) + math.pow(dy, 2)
                if dist2 < circle.r2:
                    edges.extend(triangle.edges)
                else:
                    cache.append(triangle)
            polygons = [] 
            # Check whether there is any duplication of edges; if yes, delete it.
            for edge in edges:
                polygons_tmp = []
                f = True
                for i, polygon in enumerate(polygons):
                    if eq_edge(edge, polygon):
                        polygons = polygons[:i]+polygons[i+1:]
                        f = False
                        break
                if not f:
                    continue
                polygons.append(edge)

            for polygon in polygons:
                cache.append(get_triangle(polygon.p0, polygon.p1, point))

            self.triangles = cache 


def main(img_pth, ratio=.9, percent=.1):
    rgb = np.array(Image.open(img_pth).convert("RGB"))
    ## 1. Read
    arr = cv2.imread(img_pth, cv2.IMREAD_GRAYSCALE)
    plt.subplot(grid[:, :2])
    plt.imshow(arr)

    ## 2. Sobel
    sobel_x = cv2.Sobel(arr, cv2.CV_64F, 1, 0, ksize=3)
    sobel_x_abs = cv2.convertScaleAbs(sobel_x)
    plt.subplot(grid[0, 2])
    plt.imshow(sobel_x_abs)

    sobel_y = cv2.Sobel(arr, cv2.CV_64F, 0, 1, ksize=3)
    sobel_y_abs = cv2.convertScaleAbs(sobel_y)
    plt.subplot(grid[0, 3])
    plt.imshow(sobel_y_abs)

    sobel_xy = cv2.addWeighted(sobel_x_abs, .5, sobel_y_abs, .5, 0)
    sobel_xy_abs= cv2.convertScaleAbs(sobel_xy)

    plt.subplot(grid[1, 2])
    plt.imshow(sobel_xy_abs)

    ## 3. Threshold and sample
    # 3.1 Threshold: (1-ratio)*100% mass left
    grays = np.array(sobel_xy_abs).flatten()
    grays.sort()
    gray = grays[int(grays.shape[0]*ratio)]
    Pset = np.array(sobel_xy_abs > gray).astype(np.uint8)
    plt.subplot(grid[1, 3])
    plt.imshow(Pset)
    ys, xs = np.where(Pset > 0)
    points = np.concatenate([xs[:, np.newaxis], ys[:, np.newaxis]], axis=1)
    # 3.2 Sample
    idxs = list(range(points.shape[0]))
    select_ids = random.sample(idxs, int(len(idxs)*percent))
    select_ids.sort()
    points = points[select_ids, :]

    ## 
    height, width = Pset.shape[:2]
    delaunay = Delaunay(width, height)
    delaunay.insert(points)
    
    plt.subplot(grid[:, 4:])
    plt.subplot(grid[:, :])
    # plt.subplot(grid[:, :3])
    plt.imshow(rgb, alpha=1.0)
    plt.show()
    # for triangle in delaunay.triangles:
    #     p0, p1, p2 = triangle.vertices
    #     pp = (p0+p1+p2)/3
    #     plt.plot([pp[0]], [pp[1]], "b+")
    # plt.subplot(grid[:, 3:])
    # plt.imshow(rgb, alpha=1.0)
    # plt.show()
    ## 画点
    # for point in points:
    #     plt.plot([point[0]], [point[1]], "r+")
    triangles = delaunay.triangles
    for triangle in triangles:
        p0, p1, p2 = (triangle.vertices).astype(np.float32)
        ## 画三角形
        # plt.plot([p0[0], p1[0]], [p0[1], p1[1]], "b-")
        # plt.plot([p1[0], p2[0]], [p1[1], p2[1]], "b-")
        # plt.plot([p2[0], p0[0]], [p2[1], p0[1]], "b-")
        pp = (p0+p1+p2)/3
        # plt.plot([pp[0]], [pp[1]], "b+")
        #''
        clr = np.ones((4,), dtype=np.float32)*255
        # print(pp, rgb[int(pp[1]), int(pp[0])])
        clr[:3] = rgb[int(pp[1]), int(pp[0])].astype(np.float32)/255
        clr[3] = 1.
        # print(clr)
        tri = plt.Polygon(triangle.vertices, color=clr)
        plt.gca().add_patch(tri)
        #'''

        ## 画外接圆
        # c = triangle.circumcircle
        # x = np.linspace(c.x-c.r2**.5, c.x+c.r2**.5, 100)
        # x1 = np.abs(c.r2-(x-c.x)**2.)
        # y1 = np.sqrt(x1)+c.y
        # y2 =-np.sqrt(x1)+c.y
        # plt.plot(x, y1, c='pink')
        # plt.plot(x, y2, c='pink')

    plt.show()


if __name__ == "__main__":
    img_pth = "./sources/sample2.jpeg"

    grid = gridspec.GridSpec(2, 6)
    main(img_pth)

By the end, 很久没写博客了,就以一个特别好玩的简单算法打声招呼哈!

你可能感兴趣的