【问题标题】:Drawing a clamped uniform cubic B-spline using Cairo使用 Cairo 绘制一个固定的均匀三次 B 样条
【发布时间】:2010-03-28 22:13:24
【问题描述】:

我有一堆坐标,它们是 2D 平面上固定的均匀三次 B 样条的控制点。我想使用 Cairo 调用(在 Python 中,使用 Cairo 的 Python 绑定)绘制这条曲线,但据我所知,Cairo 仅支持 Bézier 曲线。我也知道可以使用贝塞尔曲线绘制两个控制点之间的 B 样条线段,但我在任何地方都找不到确切的公式。给定控制点的坐标,如何推导出相应贝塞尔曲线的控制点?有什么有效的算法吗?

【问题讨论】:

    标签: python cairo bezier


    【解决方案1】:

    好的,所以我使用谷歌搜索了很多,我想我想出了一个适合我目的的合理解决方案。我把它贴在这里——也许它对其他人也有用。

    首先,让我们从一个简单的Point 类开始:

    from collections import namedtuple
    
    class Point(namedtuple("Point", "x y")):
        __slots__ = ()
    
        def interpolate(self, other, ratio = 0.5):
            return Point(x = self.x * (1.0-ratio) + other.x * float(ratio), \
                         y = self.y * (1.0-ratio) + other.y * float(ratio))
    

    三次 B 样条只不过是 Point 对象的集合:

    class CubicBSpline(object):
        __slots__ = ("points", )
    
        def __init__(self, points):
            self.points = [Point(*coords) for coords in points]
    

    现在,假设我们有一个开放的均匀三次 B 样条,而不是一个钳位的。三次 B 样条的四个连续控制点定义了一个 Bézier 线段,因此控制点 0 到 3 定义了第一个 Bézier 线段,控制点 1 到 4 定义了第二个线段,依此类推。 Bézier 样条的控制点可以通过在 B 样条的控制点之间以适当的方式进行线性插值来确定。令 A、B、C 和 D 为 B 样条的四个控制点。计算以下辅助点:

    1. 找到将 A-B 线以 2:1 的比例分开的点,设为 A'。
    2. 找到以 1:2 的比例划分 C-D 线的点,设为 D'。
    3. 将 B-C 线分成三等份,让两点分别为 F 和 G。
    4. 找到 A' 和 F 之间的中间点,这将是 E。
    5. 找到 G 和 D' 之间的中间点,这将是 H。

    带有控制点 F 和 G 的从 E 到 H 的 Bézier 曲线等效于点 A、B、C 和 D 之间的开放 B 样条曲线。请参阅this excellent document 的第 1-5 节。顺便说一下,上述方法称为 Böhm 算法,如果以适当的数学方式表示,同时考虑非均匀或非三次 B 样条,则要复杂得多。

    我们必须对 B 样条的每组 4 个连续点重复上述过程,因此最后我们将需要几乎任何连续控制点对之间的 1:2 和 2:1 分割点。这是以下BSplineDrawer 类在绘制曲线之前所做的:

    class BSplineDrawer(object):
        def __init__(self, context):
            self.ctx = context
    
        def draw(self, bspline):
            pairs = zip(bspline.points[:-1], bspline.points[1:])
            one_thirds = [p1.interpolate(p2, 1/3.) for p1, p2 in pairs]
            two_thirds = [p2.interpolate(p1, 1/3.) for p1, p2 in pairs]
    
            coords = [None] * 6
            for i in xrange(len(bspline.points) - 3):
                start = two_thirds[i].interpolate(one_thirds[i+1])
                coords[0:2] = one_thirds[i+1]
                coords[2:4] = two_thirds[i+1]
                coords[4:6] = two_thirds[i+1].interpolate(one_thirds[i+2])
    
                self.context.move_to(*start)
                self.context.curve_to(*coords)
                self.context.stroke()
    

    最后,如果我们想要绘制钳位 B 样条而不是开放 B 样条,我们只需将钳位 B 样条的两个端点再重复 3 次即可:

    class CubicBSpline(object):
        [...]
        def clamped(self):
            new_points = [self.points[0]] * 3 + self.points + [self.points[-1]] * 3
            return CubicBSpline(new_points)
    

    最后,代码应该是这样使用的:

    import cairo
    
    surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 600, 400)
    ctx = cairo.Context(surface)
    
    points = [(100,100), (200,100), (200,200), (100,200), (100,400), (300,400)]
    spline = CubicBSpline(points).clamped()
    
    ctx.set_source_rgb(0., 0., 1.)
    ctx.set_line_width(5)
    BSplineDrawer(ctx).draw(spline)
    

    【讨论】:

    • 你能让这段代码在 Python 3.x 中工作吗?我试过了,但它有一些奇怪的异常。 xrange 到范围。此外,带有右曲括号的 one_thirds = [p1.interpolate(p2, 1/3.) for p1, p2 in pairs) 会引发错误。
    • 我目前没有 Python 3.x,但我认为这些是您需要进行的更改:1) 将 zip(...) 替换为 list(zip(...)),因为我们将进行迭代在列表中两次,2)将xrange()替换为range(),3)使用方括号,您当前看到右圆括号,因为无论如何这是一个错字(我现在要编辑我的答案以修复它)
    • 谢谢,我确实弄明白了,让它工作了。有用的代码。
    【解决方案2】:

    【讨论】:

    • @ΤZΩΤZΙΟΥ +1 谢谢,这有助于我朝着正确的方向开始。有关完整解决方案和我找到的算法的简化描述,请参见我上面的答案。
    猜你喜欢
    • 1970-01-01
    • 2017-02-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-06-07
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多