【问题标题】:Calculating the exact length of an SVG Arc in Python?在 Python 中计算 SVG 弧的确切长度?
【发布时间】:2019-10-31 20:00:55
【问题描述】:

我希望能够计算 SVG 弧的确切长度。我可以很容易地完成所有操作。但是,我不确定是否有解决方案或解决方案的确切实施。

这是椭圆周长的精确解。使用流行的库很好。我完全理解没有简单的解决方案,因为它们所有都需要精确的超几何函数。

from scipy import pi, sqrt
from scipy.special import hyp2f1

def exact(a, b):
    t = ((a - b) / (a + b)) ** 2
    return pi * (a + b) * hyp2f1(-0.5, -0.5, 1, t)

a = 2.667950e9
b = 6.782819e8
print(exact(a, b))

我的想法是,如果您碰巧安装了scipy,则将其作为可选代码,它将使用精确的超级解决方案,否则它将退回到较弱的近似代码(逐渐变小的线段直到误差很小)。问题是这里的数学水平高于我。而且我不知道是否有一些方法可以为此指定起点和终点。

大多数近似解都是针对椭圆的,但我只想要圆弧。可能还有一个我不知道的解决方案,用于计算椭圆上的弧长,但因为起点和终点位置可以在任何地方。说后掠角是总可能角度的 15%,因此它是椭圆周长的 15%,这似乎不是立即可行的。

更有效的不太花哨的弧近似也可能很好。有越来越好的椭圆近似值,但我不能从椭圆周长到弧长,所以这些目前没有帮助。


假设弧参数化是椭圆上的起点和终点。因为这就是 SVG 的参数化方式。但是,像 arc_length 参数化这样不是重言式的东西都是正确的答案。

【问题讨论】:

  • 目标弧是如何定义的?
  • 椭圆上从中心到点的角度,椭圆上的特定起点和终点。假设因为我说的是“svg”,所以它的参数化方式就像 svg 在弧上的开始和结束位置一样。基本上任何不是 arc_length 参数化的东西都可以,因为这有点重言式。
  • 你说的“超级解决方案”是指scipy.special.ellipeinc,我猜?
  • 类似的东西,正确实施可以轻松地为正确的弧线生成正确的答案。超几何函数的功能超出了我的范围。可以产生一个答案似乎很可能,那个答案是什么。我不知道。
  • 更正:可以用ellipenc 减去phi1 和phi2 来完成。

标签: python svg scipy automatic-ref-counting ellipse


【解决方案1】:

如果你想用你的双手和标准库来计算这个,你可以基于following formula来计算。由于acos,这仅对椭圆上半部分的两个点有效,但我们将直接将其与角度一起使用。

计算包括以下步骤:

  1. 从 SVG 数据开始:起点、a 、b 旋转、长弧、扫掠、终点
  2. 旋转坐标系以匹配椭圆的水平轴。
  3. 求解具有 4 个未知数的 4 个方程组,得到中心点以及与起点和终点相对应的角度
  4. 通过小段上的离散和来近似积分。按照 cmets 中的建议,您可以在此处使用 scipy.special.ellipeinc

第 2 步很简单,只需使用旋转矩阵(注意角度rot 顺时针方向为正):

 m = [
        [math.cos(rot), math.sin(rot)], 
        [-math.sin(rot), math.cos(rot)]
   ]

第 3 步在this answer 中有很好的解释。请注意,为a1 获得的值是模 pi,因为它是通过atan 获得的。这意味着您需要计算 t1t2 两个角度的中心点并检查它们是否匹配。如果没有,请将 pi 添加到 a1 并再次检查。

第 4 步非常简单。将区间[t1,t2]分成n段,得到每段结束时函数的值,再乘以段长,求和。您可以尝试通过在每个段的中点取函数的值来改进它,但我不确定这样做有多大好处。段数可能对精度的影响更大。

这是上面的一个非常粗略的 Python 版本(请忍受丑陋的编码风格,我在旅行时在手机上这样做?)

import math

PREC = 1E-6

# matrix vector multiplication
def transform(m, p):
    return ((sum(x * y for x, y in zip(m_r, p))) for m_r in m)

# the partial integral function        
def ellipse_part_integral(t1, t2, a, b, n=100):

    # function to integrate
    def f(t):
        return math.sqrt(1 - (1 - a**2 / b**2) * math.sin(t)**2)


    start = min(t1, t2)
    seg_len = abs(t1 - t2) / n
    return - b * sum(f(start + seg_len * (i + 1)) * seg_len for i in range(n))


def ellipse_arc_length(x1, y1, a, b, rot, large_arc, sweep, x2, y2):
    if abs(x1 - x2) < PREC and abs(y1 - y2) < PREC:
        return 0

    # get rot in radians
    rot = math.pi / 180 * rot
    # get the coordinates in the rotated coordinate system
    m = [
        [math.cos(rot), math.sin(rot)], 
        [- math.sin(rot), math.cos(rot)]
    ]
    x1_loc, y1_loc, x2_loc, y2_loc = *transform(m, (x1,y1)), *transform(m, (x2,y2))

    r1 = (x1_loc - x2_loc) / (2 * a)
    r2 = (y2_loc - y1_loc) / (2 * b)

    # avoid division by 0 if both points have same y coord
    if abs(r2) > PREC:
        a1 = math.atan(r1 / r2)
    else:
        a1 = r1 / abs(r1) * math.pi / 2

    if abs(math.cos(a1)) > PREC:
        a2 = math.asin(r2 / math.cos(a1))
    else:
        a2 = math.asin(r1 / math.sin(a1))

    # calculate the angle of start and end point
    t1 = a1 + a2
    t2 = a1 - a2

    # calculate centre point coords
    x0 = x1_loc - a * math.cos(t1)
    y0 = y1_loc - b * math.sin(t1)

    x0s = x2_loc - a * math.cos(t2)
    y0s = y2_loc - b * math.sin(t2)


    # a1 value is mod pi so the centres may not match
    # if they don't, check a1 + pi
    if abs(x0 - x0s) > PREC or abs(y0 - y0s) > PREC:
        a1 = a1 + math.pi
        t1 = a1 + a2
        t2 = a1 - a2

        x0 = x1_loc - a * math.cos(t1)
        y0 = y1_loc - b * math.sin(t1)

        x0s = x2_loc - a * math.cos(t2)
        y0s = y2_loc - b * math.sin(t2)

    # get the angles in the range [0, 2 * pi]
    if t1 < 0:
        t1 += 2 * math.pi
    if t2 < 0:
        t2 += 2 * math.pi

    # increase minimum by 2 * pi for a large arc
    if large_arc:
        if t1 < t2:
            t1 += 2 * math.pi 
        else:
            t2 += 2 * math.pi

    return ellipse_part_integral(t1, t2, a, b)

print(ellipse_arc_length(0, 0, 40, 40, 0, False, True, 80, 0))

好消息是扫描标志并不重要,只要您只是在寻找弧的长度。

我不能 100% 确定模 pi 问题得到正确处理,并且上面的实现可能存在一些错误。 尽管如此,在半圆的简单情况下,它给了我一个很好的近似长度,所以我敢称它为 WIP。让我知道这是否值得追求,当我坐在电脑前时,我可以进一步看看。或者也许有人可以同时想出一个干净的方法来做到这一点?

【讨论】:

  • 粗略检查让我在几分之一秒内而不是更长的时间内给出的答案与我的点计算答案略有不同。除了直线的步长之外,我什至无法得到很好的椭圆估计。
  • from svg.elements import * Arc((0, 0), 40, 32, 0, False, True, (80, 0)).length() 113.4466715495692
  • print(ellipse_arc_length(0, 0, 40, 32, 0, False, True, 80, 0)) -113.4466715558979 --- 注意我将第二个半径改为 32 以避免圆弧快捷方式svg.elements 中的长度
  • 该错误似乎在高偏心率时激增。通常它低 8 个数量级。 200.000009720719476 == 200.000006916086477,差异:0.000002804632999 弧(开始=0,半径=(50+0.004901195889745281j),旋转=0,弧=真,扫=假,结束=1e-14)
  • 也许值得增加ellipse_part_integral 中的段数。或者使用ellipeinc 更好地估计积分?这有什么区别吗?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-06-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-09-16
  • 2011-08-09
  • 1970-01-01
相关资源
最近更新 更多