使用Cairo绘制一个被夹住的均匀三次B样条曲线

3

我有一堆坐标,它们是二维平面上一个被夹紧的均匀三次B样条的控制点。我想要使用Cairo调用(在Python中使用Cairo的Python绑定)来绘制这条曲线,但据我所知,Cairo仅支持贝塞尔曲线。我也知道,在两个控制点之间的B样条段可以使用贝塞尔曲线来绘制,但我找不到确切的公式。给定控制点的坐标,如何推导出相应贝塞尔曲线的控制点?是否有任何高效算法可以实现?

2个回答

7

好的,我使用谷歌进行了大量搜索,并且我认为我想出了一个适合我的目的的合理解决方案。我在这里发布它 - 也许对其他人也有用。

首先,让我们从一个简单的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样条替代一个夹紧的一样。四个连续的控制点定义了一个单独的贝塞尔段,因此控制点0到3定义了第一个贝塞尔段,控制点1到4定义了第二个线段,以此类推。贝塞尔样条的控制点可以通过适当地在B样条的控制点之间进行线性插值来确定。让A,B,C和D成为B样条的四个控制点。计算以下辅助点:
  1. 找出分割A-B线段比例为2:1的点,将其称为A'。
  2. 找出分割C-D线段比例为1:2的点,将其称为D'。
  3. 将B-C线段分成三等份,令两点为F和G。
  4. 找出A'和F之间的中点,这将是E。
  5. 找出G和D'之间的中点,这将是H。
从E到H的贝塞尔曲线与点A、B、C和D之间的开放B样条是等效的。请参见此优秀文档的1-5节。顺便说一下,上述方法被称为Böhm's算法,如果以适当的数学方式表示非均匀或非立方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样条的两个端点重复三次:

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改为range。此外,一个带有右括号的闭合曲线,one_thirds = [p1.interpolate(p2, 1/3.) for p1, p2 in pairs),正在抛出错误。 - blissweb
1
我目前没有Python 3.x,但我认为这些是你需要做出的更改:1)将zip(...)替换为list(zip(...)),因为我们将两次迭代列表,2)用range()替换xrange(),3)在您当前看到右圆括号的地方使用方括号,因为那是一个错别字(我现在要编辑我的答案来修复它)。 - Tamás
谢谢,我已经弄明白了并且让它正常工作了。这是有用的代码。 - blissweb

2

1
@ΤΖΩΤΖΙΟΥ +1 谢谢,这对我找到正确方向很有帮助。请参见我的答案,其中包含我找到的完整解决方案和算法简化描述。 - Tamás

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接