如何在Python中将SVG路径插值到像素坐标(而不仅仅是光栅)

2024-05-16 06:21:12 发布

您现在位置:Python中文网/ 问答频道 /正文

我需要转换路径的SVG描述,例如:

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

所有像素的列表(假设画布是监视器的大小)。如你所见,我需要处理的路径相当于潦草,而且相当复杂。理想情况下,我希望生成这样的路径,然后将整个内容转换为逐像素的描述,即

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

或者,我会满足于使用曲线和直线成分生成路径的任何方法(例如,SVG只是我迄今为止实现这一点的方法)。这个背景有点特殊,这是认知心理学的一个实验,我需要逐步地模拟一个点穿过一条根据特定规则生成的路径,将该路径导出为像素数据。在

为了制作动画,我打算简单地在路径上的每个x,y位置重新绘制点,因此需要上述列表。在

我的数学技能不是很好-我是从设计而来的代码,而不是CS,而且路径会变得相当复杂,这意味着仅用数学计算这些点是。。。也许不超过我,但绝对比我的目标要求更高。在

图书馆、技巧、策略都是受欢迎和赞赏的。在


Tags: 方法svg路径列表像素c124c310c602
3条回答

为了一些奇怪的目的,我需要将SVG路径转换为离散点。显然没有光库来做这件事。最后我创建了自己的解析器。在

输入文件主要由贝塞尔曲线组成。我为此写了一个快速函数:

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)

可按如下方式生成10个样本:

^{pr2}$

代码需要numpy。如果你不想要这个产品,你也可以。我可以用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()

Figure 1

svg path interpolatorJavaScript库使用样本大小和保真度选项将svg路径转换为多边形点数据。这个库支持完整的SVG规范,并将考虑路径上的转换。它接受svg输入并生成表示插值点的JSON。在

我在unutbudifferent post中找到了我的大部分答案(第二个答案)。在

这是我对他的基本功能的修改,还有一些额外的功能来解决我上面的问题。我将不得不为直线段编写一个类似的函数,但这显然要容易得多,它们之间可以拼凑出曲线和直线段的任何组合,以实现上述目标。在

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

相关问题 更多 >