Spaces:
Running
Running
"""This code is taken from <https://github.com/alexandre01/deepsvg> | |
by Alexandre Carlier, Martin Danelljan, Alexandre Alahi and Radu Timofte | |
from the paper >https://arxiv.org/pdf/2007.11301.pdf> | |
""" | |
from __future__ import annotations | |
from .geom import * | |
from src.preprocessing.deepsvg.deepsvg_difflib.tensor import SVGTensor | |
from .util_fns import get_roots | |
from enum import Enum | |
import torch | |
import math | |
from typing import List, Union | |
Num = Union[int, float] | |
class SVGCmdEnum(Enum): | |
MOVE_TO = "m" | |
LINE_TO = "l" | |
CUBIC_BEZIER = "c" | |
CLOSE_PATH = "z" | |
ELLIPTIC_ARC = "a" | |
QUAD_BEZIER = "q" | |
LINE_TO_HORIZONTAL = "h" | |
LINE_TO_VERTICAL = "v" | |
CUBIC_BEZIER_REFL = "s" | |
QUAD_BEZIER_REFL = "t" | |
svgCmdArgTypes = { | |
SVGCmdEnum.MOVE_TO.value: [Point], | |
SVGCmdEnum.LINE_TO.value: [Point], | |
SVGCmdEnum.CUBIC_BEZIER.value: [Point, Point, Point], | |
SVGCmdEnum.CLOSE_PATH.value: [], | |
SVGCmdEnum.ELLIPTIC_ARC.value: [Radius, Angle, Flag, Flag, Point], | |
SVGCmdEnum.QUAD_BEZIER.value: [Point, Point], | |
SVGCmdEnum.LINE_TO_HORIZONTAL.value: [XCoord], | |
SVGCmdEnum.LINE_TO_VERTICAL.value: [YCoord], | |
SVGCmdEnum.CUBIC_BEZIER_REFL.value: [Point, Point], | |
SVGCmdEnum.QUAD_BEZIER_REFL.value: [Point], | |
} | |
class SVGCommand: | |
def __init__(self, command: SVGCmdEnum, args: List[Geom], start_pos: Point, end_pos: Point): | |
self.command = command | |
self.args = args | |
self.start_pos = start_pos | |
self.end_pos = end_pos | |
def copy(self): | |
raise NotImplementedError | |
def from_str(cmd_str: str, args_str: List[Num], pos=None, initial_pos=None, prev_command: SVGCommand = None): | |
if pos is None: | |
pos = Point(0.) | |
if initial_pos is None: | |
initial_pos = Point(0.) | |
cmd = SVGCmdEnum(cmd_str.lower()) | |
# Implicit MoveTo commands are treated as LineTo | |
if cmd is SVGCmdEnum.MOVE_TO and len(args_str) > 2: | |
l_cmd_str = SVGCmdEnum.LINE_TO.value | |
if cmd_str.isupper(): | |
l_cmd_str = l_cmd_str.upper() | |
l1, pos, initial_pos = SVGCommand.from_str(cmd_str, args_str[:2], pos, initial_pos) | |
l2, pos, initial_pos = SVGCommand.from_str(l_cmd_str, args_str[2:], pos, initial_pos) | |
return [*l1, *l2], pos, initial_pos | |
nb_args = len(args_str) | |
if cmd is SVGCmdEnum.CLOSE_PATH: | |
assert nb_args == 0, f"Expected no argument for command {cmd_str}: {nb_args} given" | |
return [SVGCommandClose(pos, initial_pos)], initial_pos, initial_pos | |
expected_nb_args = sum([ArgType.num_args for ArgType in svgCmdArgTypes[cmd.value]]) | |
assert nb_args % expected_nb_args == 0, f"Expected {expected_nb_args} arguments for command {cmd_str}: {nb_args} given" | |
l = [] | |
i = 0 | |
for _ in range(nb_args // expected_nb_args): | |
args = [] | |
for ArgType in svgCmdArgTypes[cmd.value]: | |
num_args = ArgType.num_args | |
arg = ArgType(*args_str[i:i+num_args]) | |
if cmd_str.islower(): | |
arg.translate(pos) | |
if isinstance(arg, Coord): | |
arg = arg.to_point(pos) | |
args.append(arg) | |
i += num_args | |
if cmd is SVGCmdEnum.LINE_TO or cmd is SVGCmdEnum.LINE_TO_VERTICAL or cmd is SVGCmdEnum.LINE_TO_HORIZONTAL: | |
cmd_parsed = SVGCommandLine(pos, *args) | |
elif cmd is SVGCmdEnum.MOVE_TO: | |
cmd_parsed = SVGCommandMove(pos, *args) | |
elif cmd is SVGCmdEnum.ELLIPTIC_ARC: | |
cmd_parsed = SVGCommandArc(pos, *args) | |
elif cmd is SVGCmdEnum.CUBIC_BEZIER: | |
cmd_parsed = SVGCommandBezier(pos, *args) | |
elif cmd is SVGCmdEnum.QUAD_BEZIER: | |
cmd_parsed = SVGCommandBezier(pos, args[0], args[0], args[1]) | |
elif cmd is SVGCmdEnum.QUAD_BEZIER_REFL or cmd is SVGCmdEnum.CUBIC_BEZIER_REFL: | |
if isinstance(prev_command, SVGCommandBezier): | |
control1 = pos * 2 - prev_command.control2 | |
else: | |
control1 = pos | |
control2 = args[0] if cmd is SVGCmdEnum.CUBIC_BEZIER_REFL else control1 | |
cmd_parsed = SVGCommandBezier(pos, control1, control2, args[-1]) | |
prev_command = cmd_parsed | |
pos = cmd_parsed.end_pos | |
if cmd is SVGCmdEnum.MOVE_TO: | |
initial_pos = pos | |
l.append(cmd_parsed) | |
return l, pos, initial_pos | |
def __repr__(self): | |
cmd = self.command.value.upper() | |
return f"{cmd}{self.get_geoms()}" | |
def to_str(self): | |
cmd = self.command.value.upper() | |
return f"{cmd}{' '.join([arg.to_str() for arg in self.args])}" | |
def to_tensor(self, PAD_VAL=-1): | |
raise NotImplementedError | |
def from_tensor(vector: torch.Tensor): | |
cmd_index, args = int(vector[0]), vector[1:] | |
cmd = SVGCmdEnum(SVGTensor.COMMANDS_SIMPLIFIED[cmd_index]) | |
radius = Radius(*args[:2].tolist()) | |
x_axis_rotation = Angle(*args[2:3].tolist()) | |
large_arc_flag = Flag(args[3].item()) | |
sweep_flag = Flag(args[4].item()) | |
start_pos = Point(*args[5:7].tolist()) | |
control1 = Point(*args[7:9].tolist()) | |
control2 = Point(*args[9:11].tolist()) | |
end_pos = Point(*args[11:].tolist()) | |
return SVGCommand.from_args(cmd, radius, x_axis_rotation, large_arc_flag, sweep_flag, start_pos, control1, control2, end_pos) | |
def from_args(command: SVGCmdEnum, radius: Radius, x_axis_rotation: Angle, large_arc_flag: Flag, | |
sweep_flag: Flag, start_pos: Point, control1: Point, control2: Point, end_pos: Point): | |
if command is SVGCmdEnum.MOVE_TO: | |
return SVGCommandMove(start_pos, end_pos) | |
elif command is SVGCmdEnum.LINE_TO: | |
return SVGCommandLine(start_pos, end_pos) | |
elif command is SVGCmdEnum.CUBIC_BEZIER: | |
return SVGCommandBezier(start_pos, control1, control2, end_pos) | |
elif command is SVGCmdEnum.CLOSE_PATH: | |
return SVGCommandClose(start_pos, end_pos) | |
elif command is SVGCmdEnum.ELLIPTIC_ARC: | |
return SVGCommandArc(start_pos, radius, x_axis_rotation, large_arc_flag, sweep_flag, end_pos) | |
def draw(self, *args, **kwargs): | |
from .svg_path import SVGPath | |
return SVGPath([self]).draw(*args, **kwargs) | |
def reverse(self): | |
raise NotImplementedError | |
def is_left_to(self, other: SVGCommand): | |
p1, p2 = self.start_pos, other.start_pos | |
if p1.y == p2.y: | |
return p1.x < p2.x | |
return p1.y < p2.y or (np.isclose(p1.norm(), p2.norm()) and p1.x < p2.x) | |
def numericalize(self, n=256): | |
raise NotImplementedError | |
def get_geoms(self): | |
return [self.start_pos, self.end_pos] | |
def get_points_viz(self, first=False, last=False): | |
from .svg_primitive import SVGCircle | |
color = "red" if first else "purple" if last else "deepskyblue" # "#C4C4C4" | |
opacity = 0.75 if first or last else 1.0 | |
return [SVGCircle(self.end_pos, radius=Radius(0.4), color=color, fill=True, stroke_width=".1", opacity=opacity)] | |
def get_handles_viz(self): | |
return [] | |
def sample_points(self, n=10, return_array=False): | |
return [] | |
def split(self, n=2): | |
raise NotImplementedError | |
def length(self): | |
raise NotImplementedError | |
def bbox(self): | |
raise NotImplementedError | |
class SVGCommandLinear(SVGCommand): | |
def __init__(self, *args, **kwargs): | |
super().__init__(*args, **kwargs) | |
def to_tensor(self, PAD_VAL=-1): | |
cmd_index = SVGTensor.COMMANDS_SIMPLIFIED.index(self.command.value) | |
return torch.tensor([cmd_index, | |
*([PAD_VAL] * 5), | |
*self.start_pos.to_tensor(), | |
*([PAD_VAL] * 4), | |
*self.end_pos.to_tensor()]) | |
def numericalize(self, n=256): | |
self.start_pos.numericalize(n) | |
self.end_pos.numericalize(n) | |
def copy(self): | |
return self.__class__(self.start_pos.copy(), self.end_pos.copy()) | |
def reverse(self): | |
return self.__class__(self.end_pos, self.start_pos) | |
def split(self, n=2): | |
return [self] | |
def bbox(self): | |
return Bbox(self.start_pos, self.end_pos) | |
class SVGCommandMove(SVGCommandLinear): | |
def __init__(self, start_pos: Point, end_pos: Point=None): | |
if end_pos is None: | |
start_pos, end_pos = Point(0.), start_pos | |
super().__init__(SVGCmdEnum.MOVE_TO, [end_pos], start_pos, end_pos) | |
def get_points_viz(self, first=False, last=False): | |
from .svg_primitive import SVGLine | |
points_viz = super().get_points_viz(first, last) | |
points_viz.append(SVGLine(self.start_pos, self.end_pos, color="red", dasharray=0.5)) | |
return points_viz | |
def bbox(self): | |
return Bbox(self.end_pos, self.end_pos) | |
class SVGCommandLine(SVGCommandLinear): | |
def __init__(self, start_pos: Point, end_pos: Point): | |
super().__init__(SVGCmdEnum.LINE_TO, [end_pos], start_pos, end_pos) | |
def sample_points(self, n=10, return_array=False): | |
z = np.linspace(0., 1., n) | |
if return_array: | |
points = (1-z)[:, None] * self.start_pos.pos[None] + z[:, None] * self.end_pos.pos[None] | |
return points | |
points = [(1 - alpha) * self.start_pos + alpha * self.end_pos for alpha in z] | |
return points | |
def split(self, n=2): | |
points = self.sample_points(n+1) | |
return [SVGCommandLine(p1, p2) for p1, p2 in zip(points[:-1], points[1:])] | |
def length(self): | |
return self.start_pos.dist(self.end_pos) | |
class SVGCommandClose(SVGCommandLinear): | |
def __init__(self, start_pos: Point, end_pos: Point): | |
super().__init__(SVGCmdEnum.CLOSE_PATH, [], start_pos, end_pos) | |
def get_points_viz(self, first=False, last=False): | |
return [] | |
class SVGCommandBezier(SVGCommand): | |
def __init__(self, start_pos: Point, control1: Point, control2: Point, end_pos: Point): | |
if control2 is None: | |
control2 = control1.copy() | |
super().__init__(SVGCmdEnum.CUBIC_BEZIER, [control1, control2, end_pos], start_pos, end_pos) | |
self.control1 = control1 | |
self.control2 = control2 | |
def p1(self): | |
return self.start_pos | |
def p2(self): | |
return self.end_pos | |
def q1(self): | |
return self.control1 | |
def q2(self): | |
return self.control2 | |
def copy(self): | |
return SVGCommandBezier(self.start_pos.copy(), self.control1.copy(), self.control2.copy(), self.end_pos.copy()) | |
def to_tensor(self, PAD_VAL=-1): | |
cmd_index = SVGTensor.COMMANDS_SIMPLIFIED.index(SVGCmdEnum.CUBIC_BEZIER.value) | |
return torch.tensor([cmd_index, | |
*([PAD_VAL] * 5), | |
*self.start_pos.to_tensor(), | |
*self.control1.to_tensor(), | |
*self.control2.to_tensor(), | |
*self.end_pos.to_tensor()]) | |
def to_vector(self): | |
return np.array([ | |
self.start_pos.tolist(), | |
self.control1.tolist(), | |
self.control2.tolist(), | |
self.end_pos.tolist() | |
]) | |
def from_vector(vector): | |
return SVGCommandBezier(Point(vector[0]), Point(vector[1]), Point(vector[2]), Point(vector[3])) | |
def reverse(self): | |
return SVGCommandBezier(self.end_pos, self.control2, self.control1, self.start_pos) | |
def numericalize(self, n=256): | |
self.start_pos.numericalize(n) | |
self.control1.numericalize(n) | |
self.control2.numericalize(n) | |
self.end_pos.numericalize(n) | |
def get_geoms(self): | |
return [self.start_pos, self.control1, self.control2, self.end_pos] | |
def get_handles_viz(self): | |
from .svg_primitive import SVGLine, SVGCircle | |
anchor_1 = SVGCircle(self.control1, radius=Radius(0.4), color="lime", fill=True, stroke_width=".1") | |
anchor_2 = SVGCircle(self.control2, radius=Radius(0.4), color="lime", fill=True, stroke_width=".1") | |
handle_1 = SVGLine(self.start_pos, self.control1, color="grey", dasharray=0.5, stroke_width=".1") | |
handle_2 = SVGLine(self.end_pos, self.control2, color="grey", dasharray=0.5, stroke_width=".1") | |
return [handle_1, handle_2, anchor_1, anchor_2] | |
def eval(self, t): | |
return (1 - t)**3 * self.start_pos + 3 * (1 - t)**2 * t * self.control1 + 3 * (1 - t) * t**2 * self.control2 + t**3 * self.end_pos | |
def derivative(self, t, n=1): | |
if n == 1: | |
return 3 * (1 - t)**2 * (self.control1 - self.start_pos) + 6 * (1 - t) * t * (self.control2 - self.control1) + 3 * t**2 * (self.end_pos - self.control2) | |
elif n == 2: | |
return 6 * (1 - t) * (self.control2 - 2 * self.control1 + self.start_pos) + 6 * t * (self.end_pos - 2 * self.control2 + self.control1) | |
raise NotImplementedError | |
def angle(self, other: SVGCommandBezier): | |
t1, t2 = self.derivative(1.), -other.derivative(0.) | |
if np.isclose(t1.norm(), 0.) or np.isclose(t2.norm(), 0.): | |
return 0. | |
angle = np.arccos(np.clip(t1.normalize().dot(t2.normalize()), -1., 1.)) | |
return np.rad2deg(angle) | |
def sample_points(self, n=10, return_array=False): | |
b = self.to_vector() | |
z = np.linspace(0., 1., n) | |
Z = np.stack([np.ones_like(z), z, z**2, z**3], axis=1) | |
Q = np.array([[1., 0., 0., 0.], | |
[-3, 3., 0., 0.], | |
[3., -6, 3., 0.], | |
[-1, 3., -3, 1]]) | |
points = Z @ Q @ b | |
if return_array: | |
return points | |
return [Point(p) for p in points] | |
def _split_two(self, z=.5): | |
b = self.to_vector() | |
Q1 = np.array([[1, 0, 0, 0], | |
[-(z - 1), z, 0, 0], | |
[(z - 1) ** 2, -2 * (z - 1) * z, z ** 2, 0], | |
[-(z - 1) ** 3, 3 * (z - 1) ** 2 * z, -3 * (z - 1) * z ** 2, z ** 3]]) | |
Q2 = np.array([[-(z - 1) ** 3, 3 * (z - 1) ** 2 * z, -3 * (z - 1) * z ** 2, z ** 3], | |
[0, (z - 1) ** 2, -2 * (z - 1) * z, z ** 2], | |
[0, 0, -(z - 1), z], | |
[0, 0, 0, 1]]) | |
return SVGCommandBezier.from_vector(Q1 @ b), SVGCommandBezier.from_vector(Q2 @ b) | |
def split(self, n=2): | |
b_list = [] | |
b = self | |
for i in range(n - 1): | |
z = 1. / (n - i) | |
b1, b = b._split_two(z) | |
b_list.append(b1) | |
b_list.append(b) | |
return b_list | |
def length(self): | |
p = self.sample_points(n=100, return_array=True) | |
return np.linalg.norm(p[1:] - p[:-1], axis=-1).sum() | |
def bbox(self): | |
return Bbox.from_points(self.find_extrema()) | |
def find_roots(self): | |
a = 3 * (-self.p1 + 3 * self.q1 - 3 * self.q2 + self.p2) | |
b = 6 * (self.p1 - 2 * self.q1 + self.q2) | |
c = 3 * (self.q1 - self.p1) | |
x_roots, y_roots = get_roots(a.x, b.x, c.x), get_roots(a.y, b.y, c.y) | |
roots_cat = [*x_roots, *y_roots] | |
roots = [root for root in roots_cat if 0 <= root <= 1] | |
return roots | |
def find_extrema(self): | |
points = [self.start_pos, self.end_pos] | |
points.extend([self.eval(root) for root in self.find_roots()]) | |
return points | |
class SVGCommandArc(SVGCommand): | |
def __init__(self, start_pos: Point, radius: Radius, x_axis_rotation: Angle, large_arc_flag: Flag, sweep_flag: Flag, end_pos: Point): | |
super().__init__(SVGCmdEnum.ELLIPTIC_ARC, [radius, x_axis_rotation, large_arc_flag, sweep_flag, end_pos], start_pos, end_pos) | |
self.radius = radius | |
self.x_axis_rotation = x_axis_rotation | |
self.large_arc_flag = large_arc_flag | |
self.sweep_flag = sweep_flag | |
def copy(self): | |
return SVGCommandArc(self.start_pos.copy(), self.radius.copy(), self.x_axis_rotation.copy(), self.large_arc_flag.copy(), | |
self.sweep_flag.copy(), self.end_pos.copy()) | |
def to_tensor(self, PAD_VAL=-1): | |
cmd_index = SVGTensor.COMMANDS_SIMPLIFIED.index(SVGCmdEnum.ELLIPTIC_ARC.value) | |
return torch.tensor([cmd_index, | |
*self.radius.to_tensor(), | |
*self.x_axis_rotation.to_tensor(), | |
*self.large_arc_flag.to_tensor(), | |
*self.sweep_flag.to_tensor(), | |
*self.start_pos.to_tensor(), | |
*([PAD_VAL] * 4), | |
*self.end_pos.to_tensor()]) | |
def _get_center_parametrization(self): | |
r = self.radius | |
p1, p2 = self.start_pos, self.end_pos | |
h, m = 0.5 * (p1 - p2), 0.5 * (p1 + p2) | |
p1_trans = h.rotate(-self.x_axis_rotation) | |
sign = -1 if self.large_arc_flag.flag == self.sweep_flag.flag else 1 | |
x2, y2, rx2, ry2 = p1_trans.x**2, p1_trans.y**2, r.x**2, r.y**2 | |
sqrt = math.sqrt(max((rx2*ry2 - rx2*y2 - ry2*x2) / (rx2*y2 + ry2*x2), 0.)) | |
c_trans = sign * sqrt * Point(r.x * p1_trans.y / r.y, -r.y * p1_trans.x / r.x) | |
c = c_trans.rotate(self.x_axis_rotation) + m | |
d, ns = (p1_trans - c_trans) / r, -(p1_trans + c_trans) / r | |
theta_1 = Point(1, 0).angle(d, signed=True) | |
delta_theta = d.angle(ns, signed=True) | |
delta_theta.deg %= 360 | |
if self.sweep_flag.flag == 0 and delta_theta.deg > 0: | |
delta_theta = delta_theta - Angle(360) | |
if self.sweep_flag == 1 and delta_theta.deg < 0: | |
delta_theta = delta_theta + Angle(360) | |
return c, theta_1, delta_theta | |
def _get_point(self, c: Point, t: float_type): | |
r = self.radius | |
return c + Point(r.x * np.cos(t), r.y * np.sin(t)).rotate(self.x_axis_rotation) | |
def _get_derivative(self, t: float_type): | |
r = self.radius | |
return Point(-r.x * np.sin(t), r.y * np.cos(t)).rotate(self.x_axis_rotation) | |
def to_beziers(self): | |
""" References: | |
https://www.w3.org/TR/2018/CR-SVG2-20180807/implnote.html | |
https://mortoray.com/2017/02/16/rendering-an-svg-elliptical-arc-as-bezier-curves/ | |
http://www.spaceroots.org/documents/ellipse/elliptical-arc.pdf """ | |
beziers = [] | |
c, theta_1, delta_theta = self._get_center_parametrization() | |
nb_curves = max(int(abs(delta_theta.deg) // 45), 1) | |
etas = [theta_1 + i * delta_theta / nb_curves for i in range(nb_curves+1)] | |
for eta_1, eta_2 in zip(etas[:-1], etas[1:]): | |
e1, e2 = eta_1.rad, eta_2.rad | |
alpha = np.sin(e2 - e1) * (math.sqrt(4 + 3 * np.tan(0.5 * (e2 - e1))**2) - 1) / 3 | |
p1, p2 = self._get_point(c, e1), self._get_point(c, e2) | |
q1 = p1 + alpha * self._get_derivative(e1) | |
q2 = p2 - alpha * self._get_derivative(e2) | |
beziers.append(SVGCommandBezier(p1, q1, q2, p2)) | |
return beziers | |
def reverse(self): | |
return SVGCommandArc(self.end_pos, self.radius, self.x_axis_rotation, self.large_arc_flag, ~self.sweep_flag, self.start_pos) | |
def numericalize(self, n=256): | |
raise NotImplementedError | |
def get_geoms(self): | |
return [self.start_pos, self.radius, self.x_axis_rotation, self.large_arc_flag, self.sweep_flag, self.end_pos] | |
def split(self, n=2): | |
raise NotImplementedError | |
def sample_points(self, n=10, return_array=False): | |
raise NotImplementedError | |