1

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]
Watchduck
  • 1,076
  • 1
  • 9
  • 29

3 Answers3

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.

Mike 'Pomax' Kamermans
  • 49,297
  • 16
  • 112
  • 153
  • Thanks for the hint. The B-spline from `interpolate` is the kind of smooth curve I want (with high sphericity), while the shape in your image is too close to a triangle for my taste. But with the slider below the image it can be adjusted to be the kind of curve I want. Anyway, I am curious if someone has an answer to the initial question, so I will leave it open for a while. – Watchduck Jul 03 '19 at 08:37
  • Do you happen to know a way to get the area and arc length of that Catmull–Rom curve? Because if I use your approach, I will have to optimize the tension for sphericity. – Watchduck Jul 03 '19 at 08:55
  • 1
    Did you notice the slider? As for arc length: same as for a bezier curve, since they're the same hermite curve, just expressed differently. I'd be surprised if scypy didn't already have that baked in, but if it doesn't, implementing https://pomax.github.io/bezierinfo/#arclength with n somewhere in the 7~10 range should be fairly easy. But for "nice sperical" you probably just want to set the CR tension to 0.5 so that in the code above you end up with `c2.x + (c3.x - c1.x)/3` etc. – Mike 'Pomax' Kamermans Jul 03 '19 at 19:06
1

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.

fang
  • 3,473
  • 1
  • 13
  • 19
  • 1
    I suppose that the example B-spline above corresponds to three Bezier curves. Do you have an idea how they can be constructed from the shown information (`knots`, `coefficients`, `parameter`) that Scipy offers about the B-spline? – Watchduck Jul 10 '19 at 19:12
0

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.

John
  • 1,856
  • 2
  • 22
  • 33