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 * | |
import src.preprocessing.deepsvg.deepsvg_svglib.geom as geom | |
import re | |
import torch | |
from typing import List, Union | |
from xml.dom import minidom | |
import math | |
#import shapely.geometry | |
import numpy as np | |
from .geom import union_bbox | |
from .svg_command import SVGCommand, SVGCommandMove, SVGCommandClose, SVGCommandBezier, SVGCommandLine, SVGCommandArc | |
COMMANDS = "MmZzLlHhVvCcSsQqTtAa" | |
COMMAND_RE = re.compile(r"([MmZzLlHhVvCcSsQqTtAa])") | |
FLOAT_RE = re.compile(r"[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?") | |
empty_command = SVGCommandMove(Point(0.)) | |
class Orientation: | |
COUNTER_CLOCKWISE = 0 | |
CLOCKWISE = 1 | |
class Filling: | |
OUTLINE = 0 | |
FILL = 1 | |
ERASE = 2 | |
class SVGPath: | |
def __init__(self, path_commands: List[SVGCommand] = None, origin: Point = None, closed=False, filling=Filling.OUTLINE): | |
self.origin = origin or Point(0.) | |
self.path_commands = path_commands | |
self.closed = closed | |
self.filling = filling | |
def start_command(self): | |
return SVGCommandMove(self.origin, self.start_pos) | |
def start_pos(self): | |
return self.path_commands[0].start_pos | |
def end_pos(self): | |
return self.path_commands[-1].end_pos | |
def to_group(self, *args, **kwargs): | |
from .svg_primitive import SVGPathGroup | |
return SVGPathGroup([self], *args, **kwargs) | |
def set_filling(self, filling=True): | |
self.filling = Filling.FILL if filling else Filling.ERASE | |
return self | |
def __len__(self): | |
return 1 + len(self.path_commands) | |
def __getitem__(self, idx): | |
if idx == 0: | |
return self.start_command | |
return self.path_commands[idx-1] | |
def all_commands(self, with_close=True): | |
close_cmd = [SVGCommandClose(self.path_commands[-1].end_pos.copy(), self.start_pos.copy())] if self.closed and self.path_commands and with_close \ | |
else () | |
return [self.start_command, *self.path_commands, *close_cmd] | |
def copy(self): | |
return SVGPath([path_command.copy() for path_command in self.path_commands], self.origin.copy(), self.closed, filling=self.filling) | |
def _tokenize_path(path_str): | |
cmd = None | |
for x in COMMAND_RE.split(path_str): | |
if x and x in COMMANDS: | |
cmd = x | |
elif cmd is not None: | |
yield cmd, list(map(float, FLOAT_RE.findall(x))) | |
def from_xml(x: minidom.Element): | |
stroke = x.getAttribute('stroke') | |
dasharray = x.getAttribute('dasharray') | |
stroke_width = x.getAttribute('stroke-width') | |
fill = not x.hasAttribute("fill") or not x.getAttribute("fill") == "none" | |
filling = Filling.OUTLINE if not x.hasAttribute("filling") else int(x.getAttribute("filling")) | |
s = x.getAttribute('d') | |
return SVGPath.from_str(s, fill=fill, filling=filling) | |
def from_str(s: str, fill=False, filling=Filling.OUTLINE, add_closing=False): | |
path_commands = [] | |
pos = initial_pos = Point(0.) | |
prev_command = None | |
for cmd, args in SVGPath._tokenize_path(s): | |
cmd_parsed, pos, initial_pos = SVGCommand.from_str(cmd, args, pos, initial_pos, prev_command) | |
prev_command = cmd_parsed[-1] | |
path_commands.extend(cmd_parsed) | |
return SVGPath.from_commands(path_commands, fill=fill, filling=filling, add_closing=add_closing) | |
def from_tensor(tensor: torch.Tensor, allow_empty=False): | |
return SVGPath.from_commands([SVGCommand.from_tensor(row) for row in tensor], allow_empty=allow_empty) | |
def from_commands(path_commands: List[SVGCommand], fill=False, filling=Filling.OUTLINE, add_closing=False, allow_empty=False): | |
from .svg_primitive import SVGPathGroup | |
if not path_commands: | |
return SVGPathGroup([]) | |
svg_paths = [] | |
svg_path = None | |
for command in path_commands: | |
if isinstance(command, SVGCommandMove): | |
if svg_path is not None and (allow_empty or svg_path.path_commands): # SVGPath contains at least one command | |
if add_closing: | |
svg_path.closed = True | |
if not svg_path.path_commands: | |
svg_path.path_commands.append(empty_command) | |
svg_paths.append(svg_path) | |
svg_path = SVGPath([], command.start_pos.copy(), filling=filling) | |
else: | |
if svg_path is None: | |
# Ignore commands until the first moveTo commands | |
continue | |
if isinstance(command, SVGCommandClose): | |
if allow_empty or svg_path.path_commands: # SVGPath contains at least one command | |
svg_path.closed = True | |
if not svg_path.path_commands: | |
svg_path.path_commands.append(empty_command) | |
svg_paths.append(svg_path) | |
svg_path = None | |
else: | |
svg_path.path_commands.append(command) | |
if svg_path is not None and (allow_empty or svg_path.path_commands): # SVGPath contains at least one command | |
if add_closing: | |
svg_path.closed = True | |
if not svg_path.path_commands: | |
svg_path.path_commands.append(empty_command) | |
svg_paths.append(svg_path) | |
return SVGPathGroup(svg_paths, fill=fill) | |
def __repr__(self): | |
return "SVGPath({})".format(" ".join(command.__repr__() for command in self.all_commands())) | |
def to_str(self, fill=False): | |
return " ".join(command.to_str() for command in self.all_commands()) | |
def to_tensor(self, PAD_VAL=-1): | |
return torch.stack([command.to_tensor(PAD_VAL=PAD_VAL) for command in self.all_commands()]) | |
def _get_viz_elements(self, with_points=False, with_handles=False, with_bboxes=False, color_firstlast=False, with_moves=True): | |
points = self._get_points_viz(color_firstlast, with_moves) if with_points else () | |
handles = self._get_handles_viz() if with_handles else () | |
return [*points, *handles] | |
def draw(self, viewbox=Bbox(24), *args, **kwargs): | |
from .svg import SVG | |
return SVG([self.to_group()], viewbox=viewbox).draw(*args, **kwargs) | |
def _get_points_viz(self, color_firstlast=True, with_moves=True): | |
points = [] | |
commands = self.all_commands(with_close=False) | |
n = len(commands) | |
for i, command in enumerate(commands): | |
if not isinstance(command, SVGCommandMove) or with_moves: | |
points_viz = command.get_points_viz(first=(color_firstlast and i <= 1), last=(color_firstlast and i >= n-2)) | |
points.extend(points_viz) | |
return points | |
def _get_handles_viz(self): | |
handles = [] | |
for command in self.path_commands: | |
handles.extend(command.get_handles_viz()) | |
return handles | |
def _get_unique_geoms(self): | |
geoms = [] | |
for command in self.all_commands(): | |
geoms.extend(command.get_geoms()) | |
return list(set(geoms)) | |
def translate(self, vec): | |
for geom in self._get_unique_geoms(): | |
geom.translate(vec) | |
return self | |
def rotate(self, angle): | |
for geom in self._get_unique_geoms(): | |
geom.rotate_(angle) | |
return self | |
def scale(self, factor): | |
for geom in self._get_unique_geoms(): | |
geom.scale(factor) | |
return self | |
def filter_consecutives(self): | |
path_commands = [] | |
for command in self.path_commands: | |
if not command.start_pos.isclose(command.end_pos): | |
path_commands.append(command) | |
self.path_commands = path_commands | |
return self | |
def filter_duplicates(self, min_dist=0.2): | |
path_commands = [] | |
current_command = None | |
for command in self.path_commands: | |
if current_command is None: | |
path_commands.append(command) | |
current_command = command | |
if command.end_pos.dist(current_command.end_pos) >= min_dist: | |
command.start_pos = current_command.end_pos | |
path_commands.append(command) | |
current_command = command | |
self.path_commands = path_commands | |
return self | |
def duplicate_extremities(self): | |
self.path_commands = [SVGCommandLine(self.start_pos, self.start_pos), | |
*self.path_commands, | |
SVGCommandLine(self.end_pos, self.end_pos)] | |
return self | |
def is_clockwise(self): | |
if len(self.path_commands) == 1: | |
cmd = self.path_commands[0] | |
return cmd.start_pos.tolist() <= cmd.end_pos.tolist() | |
det_total = 0. | |
for cmd in self.path_commands: | |
det_total += geom.det(cmd.start_pos, cmd.end_pos) | |
return det_total >= 0. | |
def set_orientation(self, orientation): | |
""" | |
orientation: 1 (clockwise), 0 (counter-clockwise) | |
""" | |
if orientation == self.is_clockwise(): | |
return self | |
return self.reverse() | |
def set_closed(self, closed=True): | |
self.closed = closed | |
return self | |
def reverse(self): | |
path_commands = [] | |
for command in reversed(self.path_commands): | |
path_commands.append(command.reverse()) | |
self.path_commands = path_commands | |
return self | |
def reverse_non_closed(self): | |
if not self.start_pos.isclose(self.end_pos): | |
return self.reverse() | |
return self | |
def simplify_arcs(self): | |
path_commands = [] | |
for command in self.path_commands: | |
if isinstance(command, SVGCommandArc): | |
if command.radius.iszero(): | |
continue | |
if command.start_pos.isclose(command.end_pos): | |
continue | |
path_commands.extend(command.to_beziers()) | |
else: | |
path_commands.append(command) | |
self.path_commands = path_commands | |
return self | |
def _get_topleftmost_command(self): | |
topleftmost_cmd = None | |
topleftmost_idx = 0 | |
for i, cmd in enumerate(self.path_commands): | |
if topleftmost_cmd is None or cmd.is_left_to(topleftmost_cmd): | |
topleftmost_cmd = cmd | |
topleftmost_idx = i | |
return topleftmost_cmd, topleftmost_idx | |
def reorder(self): | |
if self.closed: | |
topleftmost_cmd, topleftmost_idx = self._get_topleftmost_command() | |
self.path_commands = [ | |
*self.path_commands[topleftmost_idx:], | |
*self.path_commands[:topleftmost_idx] | |
] | |
return self | |
def to_video(self, wrapper, clips=None, svg_commands=None, color="grey"): | |
from .svg import SVG | |
from .svg_primitive import SVGLine, SVGCircle | |
if clips is None: | |
clips = [] | |
if svg_commands is None: | |
svg_commands = [] | |
svg_dots, svg_moves = [], [] | |
for command in self.all_commands(): | |
start_pos, end_pos = command.start_pos, command.end_pos | |
if isinstance(command, SVGCommandMove): | |
move = SVGLine(start_pos, end_pos, color="teal", dasharray=0.5) | |
svg_moves.append(move) | |
dot = SVGCircle(end_pos, radius=Radius(0.1), color="red") | |
svg_dots.append(dot) | |
svg_path = SVGPath(svg_commands).to_group(color=color) | |
svg_new_path = SVGPath([SVGCommandMove(start_pos), command]).to_group(color="red") | |
svg_paths = [svg_path, svg_new_path] if svg_commands else [svg_new_path] | |
im = SVG([*svg_paths, *svg_moves, *svg_dots]).draw(do_display=False, return_png=True, with_points=False) | |
clips.append(wrapper(np.array(im))) | |
svg_dots[-1].color = "grey" | |
svg_commands.append(command) | |
svg_moves = [] | |
return clips, svg_commands | |
def numericalize(self, n=256): | |
for command in self.all_commands(): | |
command.numericalize(n) | |
def smooth(self): | |
# https://github.com/paperjs/paper.js/blob/c7d85b663edb728ec78fffa9f828435eaf78d9c9/src/path/Path.js#L1288 | |
n = len(self.path_commands) | |
knots = [self.start_pos, *(path_commmand.end_pos for path_commmand in self.path_commands)] | |
r = [knots[0] + 2 * knots[1]] | |
f = [2] | |
p = [Point(0.)] * (n + 1) | |
# Solve with the Thomas algorithm | |
for i in range(1, n): | |
internal = i < n - 1 | |
a = 1 | |
b = 4 if internal else 2 | |
u = 4 if internal else 3 | |
v = 2 if internal else 0 | |
m = a / f[i-1] | |
f.append(b-m) | |
r.append(u * knots[i] + v * knots[i + 1] - m * r[i-1]) | |
p[n-1] = r[n-1] / f[n-1] | |
for i in range(n-2, -1, -1): | |
p[i] = (r[i] - p[i+1]) / f[i] | |
p[n] = (3 * knots[n] - p[n-1]) / 2 | |
for i in range(n): | |
p1, p2 = knots[i], knots[i+1] | |
c1, c2 = p[i], 2 * p2 - p[i+1] | |
self.path_commands[i] = SVGCommandBezier(p1, c1, c2, p2) | |
return self | |
def simplify_heuristic(self): | |
return self.copy().split(max_dist=2, include_lines=False) \ | |
.simplify(tolerance=0.1, epsilon=0.2, angle_threshold=150) \ | |
.split(max_dist=7.5) | |
def simplify(self, tolerance=0.1, epsilon=0.1, angle_threshold=179., force_smooth=False): | |
# https://github.com/paperjs/paper.js/blob/c044b698c6b224c10a7747664b2a4cd00a416a25/src/path/PathFitter.js#L44 | |
points = [self.start_pos, *(path_command.end_pos for path_command in self.path_commands)] | |
def subdivide_indices(): | |
segments_list = [] | |
current_segment = [] | |
prev_command = None | |
for i, command in enumerate(self.path_commands): | |
if isinstance(command, SVGCommandLine): | |
if current_segment: | |
segments_list.append(current_segment) | |
current_segment = [] | |
prev_command = None | |
continue | |
if prev_command is not None and prev_command.angle(command) < angle_threshold: | |
if current_segment: | |
segments_list.append(current_segment) | |
current_segment = [] | |
current_segment.append(i) | |
prev_command = command | |
if current_segment: | |
segments_list.append(current_segment) | |
return segments_list | |
path_commands = [] | |
def computeMaxError(first, last, curve: SVGCommandBezier, u): | |
maxDist = 0. | |
index = (last - first + 1) // 2 | |
for i in range(1, last - first): | |
dist = curve.eval(u[i]).dist(points[first + i]) ** 2 | |
if dist >= maxDist: | |
maxDist = dist | |
index = first + i | |
return maxDist, index | |
def chordLengthParametrize(first, last): | |
u = [0.] | |
for i in range(1, last - first + 1): | |
u.append(u[i-1] + points[first + i].dist(points[first + i-1])) | |
for i, _ in enumerate(u[1:], 1): | |
u[i] /= u[-1] | |
return u | |
def isMachineZero(val): | |
MACHINE_EPSILON = 1.12e-16 | |
return val >= -MACHINE_EPSILON and val <= MACHINE_EPSILON | |
def findRoot(curve: SVGCommandBezier, point, u): | |
""" | |
Newton's root finding algorithm calculates f(x)=0 by reiterating | |
x_n+1 = x_n - f(x_n)/f'(x_n) | |
We are trying to find curve parameter u for some point p that minimizes | |
the distance from that point to the curve. Distance point to curve is d=q(u)-p. | |
At minimum distance the point is perpendicular to the curve. | |
We are solving | |
f = q(u)-p * q'(u) = 0 | |
with | |
f' = q'(u) * q'(u) + q(u)-p * q''(u) | |
gives | |
u_n+1 = u_n - |q(u_n)-p * q'(u_n)| / |q'(u_n)**2 + q(u_n)-p * q''(u_n)| | |
""" | |
diff = curve.eval(u) - point | |
d1, d2 = curve.derivative(u, n=1), curve.derivative(u, n=2) | |
numerator = diff.dot(d1) | |
denominator = d1.dot(d1) + diff.dot(d2) | |
return u if isMachineZero(denominator) else u - numerator / denominator | |
def reparametrize(first, last, u, curve: SVGCommandBezier): | |
for i in range(0, last - first + 1): | |
u[i] = findRoot(curve, points[first + i], u[i]) | |
for i in range(1, len(u)): | |
if u[i] <= u[i-1]: | |
return False | |
return True | |
def generateBezier(first, last, uPrime, tan1, tan2): | |
epsilon = 1e-12 | |
p1, p2 = points[first], points[last] | |
C = np.zeros((2, 2)) | |
X = np.zeros(2) | |
for i in range(last - first + 1): | |
u = uPrime[i] | |
t = 1 - u | |
b = 3 * u * t | |
b0 = t**3 | |
b1 = b * t | |
b2 = b * u | |
b3 = u**3 | |
a1 = tan1 * b1 | |
a2 = tan2 * b2 | |
tmp = points[first + i] - p1 * (b0 + b1) - p2 * (b2 + b3) | |
C[0, 0] += a1.dot(a1) | |
C[0, 1] += a1.dot(a2) | |
C[1, 0] = C[0, 1] | |
C[1, 1] += a2.dot(a2) | |
X[0] += a1.dot(tmp) | |
X[1] += a2.dot(tmp) | |
detC0C1 = C[0, 0] * C[1, 1] - C[1, 0] * C[0, 1] | |
if abs(detC0C1) > epsilon: | |
detC0X = C[0, 0] * X[1] - C[1, 0] * X[0] | |
detXC1 = X[0] * C[1, 1] - X[1] * C[0, 1] | |
alpha1 = detXC1 / detC0C1 | |
alpha2 = detC0X / detC0C1 | |
else: | |
c0 = C[0, 0] + C[0, 1] | |
c1 = C[1, 0] + C[1, 1] | |
alpha1 = alpha2 = X[0] / c0 if abs(c0) > epsilon else (X[1] / c1 if abs(c1) > epsilon else 0) | |
segLength = p2.dist(p1) | |
eps = epsilon * segLength | |
handle1 = handle2 = None | |
if alpha1 < eps or alpha2 < eps: | |
alpha1 = alpha2 = segLength / 3 | |
else: | |
line = p2 - p1 | |
handle1 = tan1 * alpha1 | |
handle2 = tan2 * alpha2 | |
if handle1.dot(line) - handle2.dot(line) > segLength**2: | |
alpha1 = alpha2 = segLength / 3 | |
handle1 = handle2 = None | |
if handle1 is None or handle2 is None: | |
handle1 = tan1 * alpha1 | |
handle2 = tan2 * alpha2 | |
return SVGCommandBezier(p1, p1 + handle1, p2 + handle2, p2) | |
def computeLinearMaxError(first, last): | |
maxDist = 0. | |
index = (last - first + 1) // 2 | |
p1, p2 = points[first], points[last] | |
for i in range(first + 1, last): | |
dist = points[i].distToLine(p1, p2) | |
if dist >= maxDist: | |
maxDist = dist | |
index = i | |
return maxDist, index | |
def ramerDouglasPeucker(first, last, epsilon): | |
max_error, split_index = computeLinearMaxError(first, last) | |
if max_error > epsilon: | |
ramerDouglasPeucker(first, split_index, epsilon) | |
ramerDouglasPeucker(split_index, last, epsilon) | |
else: | |
p1, p2 = points[first], points[last] | |
path_commands.append(SVGCommandLine(p1, p2)) | |
def fitCubic(error, first, last, tan1=None, tan2=None): | |
# For convenience, compute extremity tangents if not provided | |
if tan1 is None and tan2 is None: | |
tan1 = (points[first + 1] - points[first]).normalize() | |
tan2 = (points[last - 1] - points[last]).normalize() | |
if last - first == 1: | |
p1, p2 = points[first], points[last] | |
dist = p1.dist(p2) / 3 | |
path_commands.append(SVGCommandBezier(p1, p1 + dist * tan1, p2 + dist * tan2, p2)) | |
return | |
uPrime = chordLengthParametrize(first, last) | |
maxError = max(error, error**2) | |
parametersInOrder = True | |
for i in range(5): | |
curve = generateBezier(first, last, uPrime, tan1, tan2) | |
max_error, split_index = computeMaxError(first, last, curve, uPrime) | |
if max_error < error and parametersInOrder: | |
path_commands.append(curve) | |
return | |
if max_error >= maxError: | |
break | |
parametersInOrder = reparametrize(first, last, uPrime, curve) | |
maxError = max_error | |
tanCenter = (points[split_index-1] - points[split_index+1]).normalize() | |
fitCubic(error, first, split_index, tan1, tanCenter) | |
fitCubic(error, split_index, last, -tanCenter, tan2) | |
segments_list = subdivide_indices() | |
if force_smooth: | |
fitCubic(tolerance, 0, len(points) - 1) | |
else: | |
if segments_list: | |
seg = segments_list[0] | |
ramerDouglasPeucker(0, seg[0], epsilon) | |
for seg, seg_next in zip(segments_list[:-1], segments_list[1:]): | |
fitCubic(tolerance, seg[0], seg[-1] + 1) | |
ramerDouglasPeucker(seg[-1] + 1, seg_next[0], epsilon) | |
seg = segments_list[-1] | |
fitCubic(tolerance, seg[0], seg[-1] + 1) | |
ramerDouglasPeucker(seg[-1] + 1, len(points) - 1, epsilon) | |
else: | |
ramerDouglasPeucker(0, len(points) - 1, epsilon) | |
self.path_commands = path_commands | |
return self | |
def split(self, n=None, max_dist=None, include_lines=True): | |
path_commands = [] | |
for command in self.path_commands: | |
if isinstance(command, SVGCommandLine) and not include_lines: | |
path_commands.append(command) | |
else: | |
l = command.length() | |
if max_dist is not None: | |
n = max(math.ceil(l / max_dist), 1) | |
path_commands.extend(command.split(n=n)) | |
self.path_commands = path_commands | |
return self | |
def bbox(self): | |
return union_bbox([cmd.bbox() for cmd in self.path_commands]) | |
def sample_points(self, max_dist=0.4): | |
points = [] | |
for command in self.path_commands: | |
l = command.length() | |
n = max(math.ceil(l / max_dist), 1) | |
points.extend(command.sample_points(n=n, return_array=True)[None]) | |
points = np.concatenate(points, axis=0) | |
return points | |
def to_shapely(self): | |
polygon = shapely.geometry.Polygon(self.sample_points()) | |
if not polygon.is_valid: | |
polygon = polygon.buffer(0) | |
return polygon | |
def to_points(self): | |
return np.array([self.start_pos.pos, *(cmd.end_pos.pos for cmd in self.path_commands)]) | |