Major refactors

This commit is contained in:
Akko
2023-05-17 16:27:52 +02:00
parent e157af78bf
commit 6379bc5962
10 changed files with 209 additions and 353 deletions

1
.gitignore vendored
View File

@@ -1,4 +1,5 @@
build build
gradientmesh/test_data/examples
apps/results apps/results
apps/files apps/files
apps/__pycache__ apps/__pycache__

View File

@@ -4,36 +4,21 @@ import torch
from random import uniform from random import uniform
import math import math
import pydiffvg from util import rgb2hex, any_map
from util import rgb2hex
class Point: class Point:
"""2D point, optionally with control points; """2D point, optionally with control points;
alternatively, 2-vector""" alternatively, 2-vector"""
def __init__(self, x: float, y: float, def __init__(self, x: float, y: float,
controls: list[Point] = None,
round=False): round=False):
if isinstance(x, torch.Tensor):
# Convert from tensor
x = x.item()
y = y.item()
self.id = id(self)
self.x = x self.x = x
self.y = y self.y = y
self.controls = controls or []
if round: if round:
self.round() self.round()
def add_control(self, control: Point):
self.controls.append(control)
def add_to_patch(self, patch: Patch):
self.patches.append(patch)
def as_xy(self): def as_xy(self):
return [self.x, self.y] return [self.x, self.y]
@@ -41,27 +26,19 @@ class Point:
self.x = int(self.x * 100) / 100.0 self.x = int(self.x * 100) / 100.0
self.y = int(self.y * 100) / 100.0 self.y = int(self.y * 100) / 100.0
def replace(self, pt: Point): return self
""""Replace (x,y) coordinates of point while maintaining pointer."""
self.x = pt.x
self.y = pt.y
def add(self, pt: Point): def add(self, pt: Point):
self.x += pt.x self.x += pt.x
self.y += pt.y self.y += pt.y
for cp in self.controls:
cp.x += pt.x return self
cp.y += pt.y
def mult(self, pt: Point): def mult(self, pt: Point):
self.x *= pt.x self.x *= pt.x
self.y *= pt.y self.y *= pt.y
for cp in self.controls:
cp.x *= pt.x
cp.y *= pt.y
def equalize(self, other): return self
self.id = other.id
@classmethod @classmethod
def random(cls, rx=(0, 1), ry=(0, 1)): def random(cls, rx=(0, 1), ry=(0, 1)):
@@ -73,67 +50,48 @@ class Point:
out.round() out.round()
return out return out
def __eq__(self, other):
return self.id == other.id
def __hash__(self): def __hash__(self):
return self.id # Used for removing duplicate points
return id(self)
def __repr__(self): def __repr__(self):
return f"P<({self.x}, {self.y})[{len(self.controls)}]>" return f"P<({self.x}, {self.y})>"
def __str__(self): def __str__(self):
return self.__repr__() return self.__repr__()
class Patch: class Patch:
"""Cubic patch.""" """Bicubic patch."""
def __init__(self, points: list[Point], color=(0.2, 0.5, 0.7, 1.0)): def __init__(self,
points: list[Point],
controls: list[list[Point]],
color=(0.2, 0.5, 0.7, 1.0)):
self.points = points self.points = points
self.controls = controls
self.color = color self.color = color
def translate(self, pt: Point): def translate(self, pt: Point):
for p in self.points: for p in self.points:
p.add(pt) p.add(pt)
for q in self.controls:
for p in q:
p.add(pt)
def scale(self, pt: Point): def scale(self, pt: Point):
for p in self.points: for p in self.points:
p.mult(pt) p.mult(pt)
def as_path(self, width=256, height=256) -> pydiffvg.Path: for q in self.controls:
ppoints = [] for p in q:
for pt in self.points: p.mult(pt)
ppoints.append([pt.x * width, pt.y * height])
for cpt in pt.controls:
ppoints.append([cpt.x * width, cpt.y * height])
return pydiffvg.Path(
num_control_points=torch.tensor(
[len(p.controls) for p in self.points]
),
points=torch.tensor(ppoints, requires_grad=True),
is_closed=True
)
def as_shape_group(self, color=None) -> pydiffvg.ShapeGroup:
# TODO proper id handling
return pydiffvg.ShapeGroup(
shape_ids=torch.tensor([0]),
fill_color=torch.tensor(color or self.color, requires_grad=True)
)
def get_points(self):
out = []
for p in self.points:
out.append([p.x, p.y])
for cp in p.controls:
out.append([cp.x, cp.y])
return out
@classmethod @classmethod
def random(cls, degree=4, num_control_points=2): def random(cls, degree=4, num_control_points=2):
num_control_points = [num_control_points] * degree num_control_points = [num_control_points] * degree
"""Returns a random Patch with `degree` vertices
and `num_control_points` control points per edge."""
# Random tweaks to regular polygon base # Random tweaks to regular polygon base
angle = 2 * math.pi / degree angle = 2 * math.pi / degree
@@ -156,6 +114,7 @@ class Patch:
points.append(pt) points.append(pt)
control_points = []
for i in range(len(num_control_points)): for i in range(len(num_control_points)):
pt = points[i] pt = points[i]
npt = points[i+1 if i+1 < degree else 0] npt = points[i+1 if i+1 < degree else 0]
@@ -164,15 +123,14 @@ class Patch:
dx = (npt.x - pt.x) / (ncp + 1) dx = (npt.x - pt.x) / (ncp + 1)
dy = (npt.y - pt.y) / (ncp + 1) dy = (npt.y - pt.y) / (ncp + 1)
for j in range(1, ncp+1): control_points.append([
midpoint = Point( Point(
pt.x + j * dx * uniform(0.8, 1.2) + uniform(0, 0.2), pt.x + j * dx * uniform(0.8, 1.2) + uniform(0, 0.2),
pt.y + j * dy * uniform(0.8, 1.2) + uniform(0, 0.2) pt.y + j * dy * uniform(0.8, 1.2) + uniform(0, 0.2)
) ) for j in range(1, ncp+1)
])
pt.add_control(midpoint) out = cls(points, control_points)
out = cls(points)
out.color = ( out.color = (
uniform(0, 1), uniform(0, 1),
uniform(0, 1), uniform(0, 1),
@@ -189,8 +147,11 @@ class Patch:
class Quad(Patch): class Quad(Patch):
"""Quadrilateral bicubic patch."""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# Assign 4 vertices as self.{top, bototm, left, right}
self.top, self.right, self.bottom, self.left = self.points self.top, self.right, self.bottom, self.left = self.points
by_y = sorted(self.points, key=lambda pt: pt.y) by_y = sorted(self.points, key=lambda pt: pt.y)
self.top, self.bottom = by_y[0], by_y[-1] self.top, self.bottom = by_y[0], by_y[-1]
@@ -199,184 +160,141 @@ class Quad(Patch):
key=lambda pt: pt.x) key=lambda pt: pt.x)
self.left, self.right = by_x self.left, self.right = by_x
for pt in by_y: # Assign 4 edges as self.{northeat, southeast, southwest, northwest}
pt.round() self.northeast = self.controls[self.points.index(self.top)]
self.southeast = self.controls[self.points.index(self.right)]
self.southwest = self.controls[self.points.index(self.bottom)]
self.northwest = self.controls[self.points.index(self.left)]
@classmethod self.set_points()
def from_path_points(cls, pp, color=None):
pt = None
points = []
for i in range(len(pp)):
if i % (len(pp) // 4) == 0:
if pt:
points.append(pt)
pt = Point(*pp[i])
else:
pt.add_control(Point(*pp[i]))
points.append(pt) def set_points(self):
return cls(points, color) """Reset self.points and self.controls; used after mutating
e.g. self.top, self.southwest &c."""
self.points = [self.left, self.top,
self.right, self.bottom]
def to_path_points(self): self.controls = [self.northwest, self.northeast,
self.southeast, self.southwest]
class PointMapping:
"""Mapping of unique points in a mesh to separate shapes for diffvg."""
def __init__(self, points, controls, raw_points, colors):
self.points = points
self.controls = controls
self.raw_points = raw_points
self.data = torch.tensor(
[pt.as_xy() for pt in raw_points], requires_grad=True
)
self.colors = colors
def as_shapes(self):
out = [] out = []
for pt in self.points:
out.append(pt.as_xy())
for cp in pt.controls:
out.append(cp.as_xy())
return out for i in range(len(self.points)):
quad = self.points[i]
quadpoints = []
for j in range(len(quad)):
quadpoints.append(quad[j])
quadpoints += self.controls[i][j]
out.append(quadpoints)
return [torch.stack(x) for x in
any_map(lambda idx: self.data[idx], out)]
class GradientMesh: class GradientMesh:
"""Bicubic quadrilateral mesh."""
def __init__(self, *quads: Quad): def __init__(self, *quads: Quad):
self.quads = quads self.quads = quads
def as_shape_groups(self): def as_mapping(self):
sg = [quad.as_shape_group() for quad in self.quads] """Convert GradientMesh to PointMapping"""
for i in range(len(sg)):
sg[i].shape_ids = torch.tensor([i])
return sg
def as_shapes(self, width, height):
return [quad.as_path(width, height) for quad in self.quads]
def to_numbers(self):
points = [] points = []
controls = [] controls = []
for quad in self.quads: raw_points = []
qp = []
qcp = []
for p in quad.points:
qp.append([p.x, p.y])
qcp.append([[cp.x, cp.y] for cp in p.controls])
points.append(qp)
controls.append(qcp)
return [points, controls, [q.color for q in self.quads]]
def from_numbers(self, numbers):
points, controls, colors = numbers
for q in range(4):
self.quads[q].color = colors[q]
pts = points[q]
ctrls = controls[q]
for p in range(4):
self.quads[q].points[p].replace(Point(*pts[p]))
for c in range(len(self.quads[q].points[p].controls)):
self.quads[q].points[p].controls[c].replace(
Point(*ctrls[p][c])
)
@classmethod
def from_path_points(cls, pp, colors):
a, b, c, d = [Quad.from_path_points(pp[x], colors[x])
for x in range(len(pp))]
join_quads(a, b, c, d, scale=False, translate=False)
return cls(a, b, c, d)
def to_path_points(self):
out = []
for q in self.quads: for q in self.quads:
out.append(q.to_path_points()) points.append(q.points)
return out controls.append(q.controls)
raw_points += q.points
for cp in q.controls:
raw_points += cp
def to_point_map(self): raw_points = list(set(raw_points))
# XXX this doesn't work
# because of control points
pts: list[Point] = []
template: list[list[int]] = []
for quad in self.quads: points = any_map(lambda p: raw_points.index(p), points)
for pt in quad.points: controls = any_map(lambda p: raw_points.index(p), controls)
pts.append(pt)
pts = list(dict.fromkeys(pts)) colors = torch.tensor([q.color for q in self.quads],
requires_grad=True)
for idx in range(len(self.quads)): return PointMapping(points, controls, raw_points, colors)
template.append([
pts.index(pt) for pt in self.quads[idx].points
])
return (pts, template) def from_mapping(self, mapping: PointMapping):
pass
def from_point_map(self, pts, template):
# XXX this not either
mapped = []
for q in template:
mapped.append([
pts[x] for x in q
])
return mapped
def average_points(points: list[Point]) -> Point: def average_points(*points: list[Point]) -> Point:
"""Average (i.e. geometric center) of points."""
x = sum([pt.x for pt in points]) / len(points) x = sum([pt.x for pt in points]) / len(points)
y = sum([pt.y for pt in points]) / len(points) y = sum([pt.y for pt in points]) / len(points)
return Point(x, y) return Point(x, y)
def equalize_points(points: list[Point]): def join_quads(a: Quad, b: Quad, c: Quad, d: Quad,
first = points[0] scale=True, translate=True,
for pt in points: step=10):
pt.equalize(first) """Join 4 quadrilaterals so that they form a mesh with a center point."""
def combine_cp(cpa, cpb):
return [average_points(a, b) for a, b in zip(cpa, cpb)]
def equalize_cp(points: list[Point]): # If each quad occupies full space, make it so that they occupy 1/4 of space
first = points[0].controls[0]
for pt in points:
for cp in pt.controls:
cp.equalize(first)
def merge_points(points: list[Point]):
merged = average_points(points)
for pt in points:
pt.replace(merged)
def merge_cp(points: list[Point]):
ct1 = points[0].controls
ct2 = points[1].controls[::-1]
for i in range(len(ct1)):
merged = average_points([ct1[i], ct2[i]])
ct1[i].replace(merged)
ct2[i].replace(merged)
def join_quads(a: Quad, b: Quad, c: Quad, d: Quad, scale=True, translate=True):
if translate:
b.translate(a.top)
c.translate(a.right)
d.translate(a.bottom)
merge_points([a.right, b.bottom, c.left, d.top])
merge_points([a.top, b.left])
merge_points([a.bottom, d.left])
merge_points([b.right, c.top])
merge_points([c.bottom, d.right])
if scale: if scale:
a.scale(Point(0.5, 0.5)) a.scale(Point(0.5, 0.5))
b.scale(Point(0.5, 0.5)) b.scale(Point(0.5, 0.5))
c.scale(Point(0.5, 0.5)) c.scale(Point(0.5, 0.5))
d.scale(Point(0.5, 0.5)) d.scale(Point(0.5, 0.5))
merge_cp([a.right, d.left]) # If quads are on top of each other, translate so they are not
merge_cp([a.top, b.bottom]) if translate:
merge_cp([c.left, b.right]) b.translate(a.top)
merge_cp([c.bottom, d.top]) c.translate(a.right)
d.translate(a.bottom)
equalize_points([a.right, b.bottom, c.left, d.top]) # Equalize centerpoint
equalize_points([a.top, b.left]) a.right, b.bottom, c.left, d.top = [
equalize_points([a.bottom, d.left]) average_points(a.right, b.bottom, c.left, d.top)
equalize_points([b.right, c.top]) ] * 4
equalize_points([c.bottom, d.right])
equalize_cp([a.right, d.left]) # Equalize non-center shared points
equalize_cp([a.top, b.bottom]) a.top, b.left = [average_points(a.top, b.left)] * 2
equalize_cp([c.left, b.right]) a.bottom, d.left = [average_points(a.bottom, d.left)] * 2
equalize_cp([c.bottom, d.top]) b.right, c.top = [average_points(b.right, c.top)] * 2
c.bottom, d.right = [average_points(c.bottom, d.right)] * 2
# Equalize edges
a.northeast, b.southwest = (
(comb := combine_cp(a.northeast, b.southwest)),
comb[::-1]
)
b.southeast, c.northwest = (
(comb := combine_cp(b.southeast, c.northwest)),
comb[::-1]
)
c.southwest, d.northeast = (
(comb := combine_cp(c.southwest, d.northeast)),
comb[::-1]
)
d.northwest, a.southeast = (
(comb := combine_cp(d.northwest, a.southeast)),
comb[::-1]
)
# Update points
for q in [a, b, c, d]:
q.set_points()

View File

@@ -7,171 +7,95 @@ import torch
import random import random
from random import uniform from random import uniform
from gmtypes import GradientMesh, Quad, Patch, Point, join_quads from gmtypes import GradientMesh, Quad, PointMapping, join_quads
def get_mesh() -> GradientMesh:
"""Helper function to get a random mesh."""
a, b, c, d = [Quad.random() for _ in range(4)]
join_quads(a, b, c, d)
return GradientMesh(a, b, c, d)
def quads(): def render_mesh(mesh: PointMapping,
return [ filename='test_data/mesh.png',
Quad.random(), width=1024,
Quad.random(), height=1024,
Quad.random(), num_control_points=2,
Quad.random(), seed=None):
]
random.seed(seed)
def rand_quad_test(filename='random_quad.png', width=256, height=256,
degree=4, num_control_points=2):
pydiffvg.set_use_gpu(torch.cuda.is_available()) pydiffvg.set_use_gpu(torch.cuda.is_available())
render = pydiffvg.RenderFunction.apply render = pydiffvg.RenderFunction.apply
ncp = torch.tensor([num_control_points] * len(mesh.points))
patch = Patch.random() # Scale
# TODO non-uniform scaling
points = [x * width for x in mesh.as_shapes()]
shape_groups = [patch.as_shape_group()] shapes = [
shapes = [patch.as_path(width, height)] pydiffvg.Path(num_control_points=ncp,
points=pts,
is_closed=True)
for pts in points
]
scene_args = pydiffvg.RenderFunction.serialize_scene(width, height, shape_groups = [
shapes, shape_groups) pydiffvg.ShapeGroup(shape_ids=torch.tensor([i]),
fill_color=mesh.colors[i])
for i in range(len(mesh.points))
]
img = render(width, height, 2, 2, 0, None, *scene_args) scene_args = pydiffvg.RenderFunction.serialize_scene(
pydiffvg.imwrite(img.cpu(), f"test_data/{filename}", gamma=2.2) width,
height,
shapes,
shape_groups
)
img = render(width,
height,
2, # num smaples x
2, # num samples y
0, # seed
None,
*scene_args)
pydiffvg.imwrite(img.cpu(), filename, gamma=2.2)
return img return img
def mult_quad_test(filename='multiple_quads.png', width=1024, def test_render(filename='test_data/target.png',
height=1024, num_control_points=None, mask=None, seed=None): width=1024,
random.seed(seed) height=1024):
mask = mask or [1, 1, 1, 1] return render_mesh(get_mesh().as_mapping(),
pydiffvg.set_use_gpu(torch.cuda.is_available()) width=width,
render = pydiffvg.RenderFunction.apply height=height,
filename=filename)
a, b, c, d = quads()
join_quads(a, b, c, d)
to_render = [a, b, c, d]
to_render = [x for x in to_render if mask[to_render.index(x)]]
shape_groups = [patch.as_shape_group(color=(
uniform(0, 1),
uniform(0, 1),
uniform(0, 1),
0.8
)) for patch in to_render]
for i in range(len(to_render)):
shape_groups[i].shape_ids = torch.tensor([i])
shapes = [patch.as_path(width, height) for patch in to_render]
scene_args = pydiffvg.RenderFunction.serialize_scene(width, height,
shapes, shape_groups)
img = render(width, height, 2, 2, 0, None, *scene_args)
pydiffvg.imwrite(img.cpu(), f"test_data/{filename}", gamma=2.2)
return img.clone()
def om(): def optimize():
filename = 'optimize_test.png' width, height = 256, 256
pydiffvg.set_use_gpu(torch.cuda.is_available()) target = test_render(width=width, height=height).clone()
render = pydiffvg.RenderFunction.apply
target = mult_quad_test(width=256, height=256) mesh = get_mesh().as_mapping()
squad = quads() optimizer = torch.optim.Adam([mesh.data, mesh.colors], lr=1e-2)
join_quads(*squad) for t in range(150):
gm = GradientMesh(*squad)
points_n = []
for s in squad:
out = []
for pt in s.points:
out.append([pt.x, pt.y])
for cpt in pt.controls:
out.append([cpt.x, cpt.y])
points_n.append(out)
points_n = torch.tensor(points_n, requires_grad=True)
color = torch.tensor([s.color for s in squad], requires_grad=True)
paths = [s.as_path() for s in squad]
path_groups = [pydiffvg.ShapeGroup(shape_ids=torch.tensor([i]),
fill_color=torch.tensor(squad[i].color))
for i in range(len(squad))]
scene_args = pydiffvg.RenderFunction.serialize_scene(
256, 256, paths, path_groups
)
img = render(256, # width
256, # height
2, # num_samples_x
2, # num_samples_y
1, # seed
None,
*scene_args)
points, controls, color = [torch.tensor(x, requires_grad=True)
for x in gm.to_numbers()]
optimizer = torch.optim.Adam([points, color, points_n], lr=1e-2)
for t in range(180):
print(f"iteration {t}") print(f"iteration {t}")
optimizer.zero_grad() optimizer.zero_grad()
points_n.data = torch.tensor( img = render_mesh(mesh,
GradientMesh.from_path_points(points_n, color).to_path_points() filename=f"test_data/mesh_optim_{str(t).zfill(3)}.png",
) width=width,
height=height)
for i in range(len(paths)):
paths[i].points = points_n[i] * 256
for i in range(len(path_groups)):
path_groups[i].fill_color = color[i]
scene_args = pydiffvg.RenderFunction.serialize_scene(
256, 256, paths, path_groups)
img = render(256, # width
256, # height
2, # num_samples_x
2, # num_samples_y
t+1, # seed
None,
*scene_args)
pydiffvg.imwrite(img.cpu(),
f'test_data/test_curve/iter_{filename}_'
f'{str(t).zfill(5)}.png',
gamma=2.2)
loss = (img - target).pow(2).sum() loss = (img - target).pow(2).sum()
loss.backward() # FIXME no need to retain graph
loss.backward(retain_graph=True)
print(f'loss: {loss}') print(f'loss: {loss}')
print(f'points.grad {points.grad}')
print(f'color.grad {color.grad}')
optimizer.step() optimizer.step()
def slideshow(n=30, s=1, do_mask=False):
mask = None
for i in range(n):
if do_mask:
mask = [1] * 4
print(i % n)
mask[i % 4] = 0
print(mask)
mult_quad_test(mask=mask)
sleep(s)
def get_mesh():
a, b, c, d = quads()
join_quads(a,b,c,d)
gm = GradientMesh(a, b, c, d)
return gm

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -1,4 +1,6 @@
#!/usr/bin/env ipython #!/usr/bin/env ipython
from types import FunctionType
def clamp(val, low, high): def clamp(val, low, high):
return min(high, max(low, val)) return min(high, max(low, val))
@@ -10,3 +12,14 @@ def rgb2hex(r, g, b, a=None):
int(b * 255) int(b * 255)
) )
return hex_value return hex_value
def any_map(f: FunctionType, lst: list):
result = []
for itm in lst:
if isinstance(itm, list):
result.append(any_map(f, itm))
else:
result.append(f(itm))
return result