How to interpolate SVG path into a pixel coordinates (not simply raster) in Python
How to interpolate SVG path into a pixel coordinates (not simply raster) in Python
I need to convert an SVG description of a path, ie. something like:
M400 597 C235 599 478 607 85 554 C310 675 2 494 399 718 C124 547 569 828 68 400 C-108 317 304 703 96 218 L47 215 L400 290 C602 -146 465 467 550 99 L548 35 L706 400 L580 686 C546 614 591 672 529 629 L400 597 Z
M400 597 C235 599 478 607 85 554 C310 675 2 494 399 718 C124 547 569 828 68 400 C-108 317 304 703 96 218 L47 215 L400 290 C602 -146 465 467 550 99 L548 35 L706 400 L580 686 C546 614 591 672 529 629 L400 597 Z
into a list of all the pixels that would fall along that path (assuming the canvas is the size of the monitor. As you can see, the paths I need to work with amount to scribbles and are quite complex. Ideally, I'd like to generate such a path and then convert the entire thing to a pixel-by-pixel description, ie.
p= [(403, 808), (403, 807), (403, 805), (403, 802), (403, 801), (403, 800), (403, 799),
(403, 797), (403, 794), (403, 792), (402, 789), (401, 787), (400, 785), (399, 784),
(399, 783), (398, 782)] # ... it'd be much longer, but you get the idea
Alternatively, I'd be as content with any means of generating paths with curve and line constituents (as in, SVG is simply how I've achieved this so far). The context is a bit peculiar; it's for an experiment in cognitive psychology, wherein I need to gradually animate a dot traversing a path generated according to certain rules, and export that path as pixel data.
To do the animation, I'm intending to simply redraw the dot at each x,y position along the path—hence the need for said list.
My math skills aren't great—I'm came to code from design, not CS—and the paths will get quite complex, meaning computing these points with math alone is... maybe not beyond me but definitely more demanding than I'm aiming for.
Libraries, tricks, strategies—all welcome & appreciated.
Yeah I follow you, but alas I also need the actual pixel positions for data files, which is a constraint of the project's scientific needs, alas. But I actually think I may have cracked this using a couple of similar questions on SO, I'll post my solution if it proves to work.
– Jonline
May 1 '16 at 22:14
3 Answers
3
I needed to convert an SVG path into discrete points for some weird purposes. Apparently there is no light library for doing that. I ended up creating my own parser.
Input files mostly consist of Bezier curves. I wrote a quick function for that:
def cubic_bezier_sample(start, control1, control2, end):
inputs = np.array([start, control1, control2, end])
cubic_bezier_matrix = np.array([
[-1, 3, -3, 1],
[ 3, -6, 3, 0],
[-3, 3, 0, 0],
[ 1, 0, 0, 0]
])
partial = cubic_bezier_matrix.dot(inputs)
return (lambda t: np.array([t**3, t**2, t, 1]).dot(partial))
def quadratic_sample(start, control, end):
# Quadratic bezier curve is just cubic bezier curve
# with the same control points.
return cubic_bezier_sample(start, control, control, end)
It can be used as follow to generate 10 samples:
n = 10
curve = cubic_bezier_sample((50,0), (50,100), (100,100), (50,0))
points = [curve(float(t)/n) for t in xrange(0, n + 1)]
The code requires numpy. You can also do the dot product without numpy if you want. I am able to get the parameters with svg.path.
import numpy as np
import matplotlib.pyplot as plt
def cubic_bezier_sample(start, control1, control2, end):
inputs = np.array([start, control1, control2, end])
cubic_bezier_matrix = np.array([
[-1, 3, -3, 1],
[ 3, -6, 3, 0],
[-3, 3, 0, 0],
[ 1, 0, 0, 0]
])
partial = cubic_bezier_matrix.dot(inputs)
return (lambda t: np.array([t**3, t**2, t, 1]).dot(partial))
# == control points ==
start = np.array([0, 0])
control1 = np.array([60, 5])
control2 = np.array([40, 95])
end = np.array([100, 100])
# number of segments to generate
n_segments = 100
# get curve segment generator
curve = cubic_bezier_sample(start, control1, control2, end)
# get points on curve
points = np.array([curve(t) for t in np.linspace(0, 1, n_segments)])
# == plot ==
controls = np.array([start, control1, control2, end])
# segmented curve
plt.plot(points[:, 0], points[:, 1], '-')
# control points
plt.plot(controls[:,0], controls[:,1], 'o')
# misc lines
plt.plot([start[0], control1[0]], [start[1], control1[1]], '-', lw=1)
plt.plot([control2[0], end[0]], [control2[1], end[1]], '-', lw=1)
plt.show()
I found most of my answer in a different post from unutbu (second answer).
This is my modification of his basic function, with some extra functionality to solve my problem above. I'll have to write a similar function for line segments, but that's obviously much easier, and between them will be able to piece together any combination of curved and linear segments to achieve my goal as described above.
def pascal_row(n):
# This is over my designer's brain, but unutbu says:
# "This returns the nth row of Pascal's Triangle"
result = [1]
x, numerator = 1, n
for denominator in range(1, n//2+1):
# print(numerator,denominator,x)
x *= numerator
x /= denominator
result.append(x)
numerator -= 1
if n&1 == 0:
# n is even
result.extend(reversed(result[:-1]))
else:
result.extend(reversed(result))
return result
def bezier_interpolation(origin, destination, control_o, control_d=None):
points = [origin, control_o, control_d, destination] if control_d else [origin, control_o, destination]
n = len(points)
combinations = pascal_row(n - 1)
def bezier(transitions):
# I don't really understand any of this math, but it works!
result =
for t in transitions:
t_powers = (t ** i for i in range(n))
u_powers = reversed([(1 - t) ** i for i in range(n)])
coefficients = [c * a * b for c, a, b in zip(combinations, t_powers, u_powers)]
result.append(
list(sum([coef * p for coef, p in zip(coefficients, ps)]) for ps in zip(*points)))
return result
def line_segments(points, size):
# it's more convenient for my purposes to have the pairs of x,y
# coordinates that eventually become the very small line segments
# that constitute my "curves
for i in range(0, len(points), size):
yield points[i:i + size]
# unutbu's function creates waaay more points than needed, and
# these extend well through the "destination" point I want to end at, so,
# I keep inspecting the line segments until one of them passes through
# my intended stop point (ie. "destination") and then manually stop
# collecting, returning the subset I want; probably not efficient,
# but it works
break_next = False
segments =
for pos in line_segments(bezier([0.01 * t for t in range(101)]), 2):
if break_next:
segments.append([break_next, destination])
break
try:
if [int(i) for i in pos[0]] == destination:
break_next = pos[0]
continue
segments.append(pos)
except IndexError:
# not guaranteed to get an even number of points from bezier()
break
return segments
The svg path interpolator JavaScript library converts svg paths to polygon point data with options for sample size and fidelity. This library supports the full SVG specification and will account for transforms on paths. It takes an svg input and produces JSON representing the interpolated points.
By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.
You can move a dot along a path with regular SVG without requiring this kind of contortion. You can do it by using a custom stroke dash array and animating the offset. You could also do it by using textPath if you can find the right character that works for your dot.
– Michael Mullany
May 1 '16 at 21:07