使用Cairo绘制固定的均匀三次B样条

3 投票
2 回答
3338 浏览
提问于 2025-04-15 20:57

我有一组坐标,这些坐标是一个在二维平面上被固定的均匀三次B样条曲线的控制点。我想用Cairo库来画这个曲线(在Python中,使用Cairo的Python绑定),但我知道Cairo只支持贝塞尔曲线。我也知道,B样条曲线中两个控制点之间的段可以用贝塞尔曲线来画,但我找不到具体的公式。给定控制点的坐标,我该如何推导出相应贝塞尔曲线的控制点呢?有没有什么高效的算法可以做到这一点?

2 个回答

2

B样条曲线转换为贝塞尔样条曲线 这个方法有帮助吗?

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的贝塞尔曲线,控制点是F和G,这相当于在点A、B、C和D之间的开放B样条。具体可以参考这份很棒的文档。顺便提一下,上面的方法叫做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样条的两个端点再重复三次:

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)

撰写回答