Как создать кривые Безье из B-сплайнов в Sympy?

Мне нужно нарисовать плавную кривую через некоторые точки, которую я затем хочу показать как путь SVG. Итак, я создаю B-сплайн с scipy.interpolate и могу получить доступ к некоторым массивам, которые, я полагаю, полностью его определяют. Кто-нибудь знает достаточно простой способ создания кривых Безье из этих массивов?

import numpy as np
from scipy import interpolate

x = np.array([-1, 0, 2])
y = np.array([ 0, 2, 0])

x = np.r_[x, x[0]]
y = np.r_[y, y[0]]

tck, u = interpolate.splprep([x, y], s=0, per=True)

cx = tck[1][0]
cy = tck[1][1]

print(          'knots: ', list(tck[0]) )
print( 'coefficients x: ', list(cx)     )
print( 'coefficients y: ', list(cy)     )
print(         'degree: ', tck[2]       )
print(      'parameter: ', list(u)      )

введите здесь описание изображения

Красные точки — это 3 начальные точки в x и y. Зеленые точки — это 6 коэффициентов в cx и cy. (Их значения повторяются после третьего, поэтому каждая зеленая точка имеет два зеленых индекса.)

Возвращаемые значения tck и u описаны scipy.interpolate.splprep документация

knots:  [-1.0, -0.722, -0.372, 0.0, 0.277, 0.627, 1.0, 1.277, 1.627, 2.0]

#                   0       1       2       3       4       5
coefficients x:  [ 3.719, -2.137, -0.053,  3.719, -2.137, -0.053]
coefficients y:  [-0.752, -0.930,  3.336, -0.752, -0.930,  3.336]

degree:  3

parameter:  [0.0, 0.277, 0.627, 1.0]

person Watchduck    schedule 02.07.2019    source источник


Ответы (3)


Не уверен, что начинать с B-сплайна имеет смысл: сформируйте кривую Катмалла через точки (с виртуальными «до первой» и «после последней», наложенными на реальные точки), а затем преобразуйте ее в кривую Безье, используя относительно тривиальное преобразование? Например. учитывая ваши точки p0, p1 и p2, первый отрезок будет кривой Катмулла {p2,p0,p1,p2} для отрезка p1--p2, {p0,p1,p2,p0} даст p2- -p0 и {p1, p2, p0, p1} дадут p0--p1. Затем вы тривиально конвертируете их, и теперь у вас есть путь SVG.

В качестве демонстратора нажмите https://editor.p5js.org/ и вставьте следующий код:

var points = [{x:150, y:100 },{x:50, y:300 },{x:300, y:300 }];

// add virtual points:
points = points.concat(points);

function setup() {
  createCanvas(400, 400);
  tension = createSlider(1, 200, 100);
}

function draw() {
  background(220);
  points.forEach(p => ellipse(p.x, p.y, 4));

  for (let n=0; n<3; n++) {
    let [c1, c2, c3, c4] = points.slice(n,n+4);
    let t = 0.06 * tension.value();

    bezier(
      // on-curve start point
      c2.x, c2.y,
      // control point 1
      c2.x + (c3.x - c1.x)/t,
      c2.y + (c3.y - c1.y)/t,
      // control point 2
      c3.x - (c4.x - c2.x)/t,
      c3.y - (c4.y - c2.y)/t,
      // on-curve end point
      c3.x, c3.y
    );
  }
}

Что будет выглядеть так:

Преобразование этого в код Python должно быть практически легким упражнением: нам почти не нужно писать код =)

И, конечно же, теперь вам осталось создать путь SVG, но это вряд ли проблема: теперь вы знаете все точки Безье, так что просто начните строить свою строку <path d=...> во время итерации.

person Mike 'Pomax' Kamermans    schedule 03.07.2019
comment
Спасибо за подсказку. B-сплайн из interpolate представляет собой гладкую кривую, которую я хочу (с высокой сферичностью), в то время как форма на вашем изображении слишком близка к треугольнику, на мой вкус. Но с помощью ползунка под изображением его можно настроить так, чтобы он был такой кривой, какой я хочу. В любом случае, мне любопытно, есть ли у кого-то ответ на первоначальный вопрос, поэтому я оставлю его открытым на некоторое время. - person Watchduck; 03.07.2019
comment
Вы случайно не знаете способ получить площадь и длину дуги этой кривой Катмулла-Рома? Потому что, если я воспользуюсь вашим подходом, мне придется оптимизировать натяжение для сферичности. - person Watchduck; 03.07.2019
comment
Вы заметили слайдер? Что касается длины дуги: то же, что и для кривой Безье, поскольку это одна и та же кривая Эрмита, просто выраженная по-разному. Я был бы удивлен, если бы в scypy этого еще не было, но если это не так, реализация pomax.github.io/bezierinfo/#arclength с n где-то в диапазоне 7–10 должно быть довольно легко. Но для хорошей спирали вы, вероятно, просто захотите установить натяжение CR на 0,5, чтобы в приведенном выше коде вы получили c2.x + (c3.x - c1.x)/3 и т. д. - person Mike 'Pomax' Kamermans; 03.07.2019

Кривая B-сплайна — это просто набор кривых Безье, соединенных вместе. Поэтому, безусловно, можно преобразовать его обратно в несколько кривых Безье без потери точности формы. Используемый алгоритм называется «вставка узла», и существуют разные способы сделать это, причем двумя наиболее известными алгоритмами являются алгоритм Бема и алгоритм Осло. Вы можете перейти по этой ссылке для получения более подробной информации.

person fang    schedule 09.07.2019
comment
Я предполагаю, что приведенный выше пример B-сплайна соответствует трем кривым Безье. У вас есть идея, как их можно построить из показанной информации (knots, coefficients, parameter), которую предлагает Scipy о B-сплайне? - person Watchduck; 10.07.2019

Вот почти прямой ответ на ваш вопрос (но для непериодического случая):

import aggdraw
import numpy as np
import scipy.interpolate as si
from PIL import Image

# from https://stackoverflow.com/a/35007804/2849934
def scipy_bspline(cv, degree=3):
    """ cv:       Array of control vertices
        degree:   Curve degree
    """
    count = cv.shape[0]

    degree = np.clip(degree, 1, count-1)
    kv = np.clip(np.arange(count+degree+1)-degree, 0, count-degree)

    max_param = count - (degree * (1-periodic))
    spline = si.BSpline(kv, cv, degree)
    return spline, max_param

# based on https://math.stackexchange.com/a/421572/396192
def bspline_to_bezier(cv):
    cv_len = cv.shape[0]
    assert cv_len >= 4, "Provide at least 4 control vertices"
    spline, max_param = scipy_bspline(cv, degree=3)
    for i in range(1, max_param):
        spline = si.insert(i, spline, 2)
    return spline.c[:3 * max_param + 1]

def draw_bezier(d, bezier):
    path = aggdraw.Path()
    path.moveto(*bezier[0])
    for i in range(1, len(bezier) - 1, 3):
        v1, v2, v = bezier[i:i+3]
        path.curveto(*v1, *v2, *v)
    d.path(path, aggdraw.Pen("black", 2))

cv = np.array([[ 40., 148.], [ 40.,  48.],
               [244.,  24.], [160., 120.],
               [240., 144.], [210., 260.],
               [110., 250.]])

im = Image.fromarray(np.ones((400, 400, 3), dtype=np.uint8) * 255)
bezier = bspline_to_bezier(cv)
d = aggdraw.Draw(im)
draw_bezier(d, bezier)
d.flush()
# show/save im

b-spline-curve-as-bezier-curves

Я не особо вникал в периодический случай, но, надеюсь, это не слишком сложно.

person John    schedule 22.10.2020