Hi,这是一篇介绍如何将德劳内三角剖分算法应用于风格化任意图像的博客。
原理部分请参考 Delaunay Image Triangulation 等一系列博客。
本文关注于具体地Python
实现。
原图 | 输出 |
---|---|
好的,下边的 3 页 PPt 应该非常清晰地阐述了该算法的具体原理。完整 PPt 请私聊博主获取,欢迎读者指点讨论哈!
非常好懂哈,
1️⃣ 先对图像用 Sobel 算子计算边缘梯度,这里其实也是可以用 Canny 算子计算,就可以忽略质量百分比阈值化(% Mass Threshold)的处理,直接采样关键点;
2️⃣ 所谓百分比阈值化就是:将梯度谱拉成长向量(np.flatten
),升序排序,设置某个比例,比如 0.9,取第 90%-th 的值为阈值,作二值化;之后用 np.where
获取为 1 的点的坐标集合,混匀采样,比如取 10% 的点,得到点的集合 points
;
3️⃣ 初始化三角剖分的结果为上图 4 的两个大△。
点循环阶段,遍历上边 points
的每个点,下边两个图演示了两个点——
对于点 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️⃣ 结束。
对于点 p 1 p_1 p1,大概四个步骤都是相同的,特别地是:
1️⃣ 点 p 1 p_1 p1 并没有在上边和左边的两个△的外接圆内,因此,这两个△保留,直接装载到 cache
中,其边缘不需要添加到 edges
中。
Fine,上边的图示已经很清晰了,讲解代码比较麻烦,就直接贴了,主要的函数是 Delaunay.insert
和 get_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, 很久没写博客了,就以一个特别好玩的简单算法打声招呼哈!