Watchduck
Watchduck

Reputation: 1156

How to create Bezier curves from B-Splines in Sympy?

I need to draw a smooth curve through some points, which I then want to show as an SVG path. So I create a B-Spline with scipy.interpolate, and can access some arrays that I suppose fully define it. Does someone know a reasonably simple way to create Bezier curves from these arrays?

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)      )

enter image description here

The red points are the 3 initial points in x and y. The green points are the 6 coefficients in cx and cy. (Their values repeat after the 3rd, so each green point has two green index numbers.)

Return values tck and u are described scipy.interpolate.splprep documentation

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]

Upvotes: 1

Views: 1592

Answers (3)

John
John

Reputation: 2008

Here is an almost direct answer to your question (but for the non-periodic case):

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

I didn't look much into the periodic case, but hopefully it's not too difficult.

Upvotes: 0

fang
fang

Reputation: 3633

A B-spline curve is just a collection of Bezier curves joined together. Therefore, it is certainly possible to convert it back to multiple Bezier curves without any loss of shape fidelity. The algorithm involved is called "knot insertion" and there are different ways to do this with the two most famous algorithm being Boehm's algorithm and Oslo algorithm. You can refer this link for more details.

Upvotes: 1

Not sure starting with a B-Spline makes sense: form a catmull-rom curve through the points (with the virtual "before first" and "after last" overlaid on real points) and then convert that to a bezier curve using a relatively trivial transform? E.g. given your points p0, p1, and p2, the first segment would be a catmull-rom curve {p2,p0,p1,p2} for the segment p1--p2, {p0,p1,p2,p0} will yield p2--p0, and {p1, p2, p0, p1} will yield p0--p1. Then you trivially convert those and now you have your SVG path.

As demonstrator, hit up https://editor.p5js.org/ and paste in the following code:

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
    );
  }
}

Which will look like this:

Converting that to Python code should be an almost effortless exercise: there is barely any code for us to write =)

And, of course, now you're left with creating the SVG path, but that's hardly an issue: you know all the Bezier points now, so just start building your <path d=...> string while you iterate.

Upvotes: 1

Related Questions