Skip to content
Permalink
master
Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Go to file
 
 
Cannot retrieve contributors at this time
executable file 457 lines (375 sloc) 15.6 KB
#!/usr/bin/python3
r'''Studies the 2d surface b-spline interpolation
The expressions for the interpolated surface I need aren't entirely clear, so I
want to study this somewhat. This script does that.
I want to use b-splines to drive the generic camera models. These provide
local support in the control points at the expense of not interpolating the
control points. I don't NEED an interpolating spline, so this is just fine.
Let's assume the knots lie at integer coordinates.
I want a function that takes in
- the control points in a neighborhood of the spline segment
- the query point x, scaled to [0,1] in the spline segment
This is all nontrivial for some reason, and there're several implementations
floating around with slightly different inputs. I have
- sample_segment_cubic()
From a generic calibration research library:
https://github.com/puzzlepaint/camera_calibration/blob/master/applications/camera_calibration/scripts/derive_jacobians.py
in the EvalUniformCubicBSpline() function. This does what I want (query
point in [0,1], control points around it, one segment at a time), but the
source of the expression isn't given. I'd like to re-derive it, and then
possibly extend it
- splev_local()
wraps scipy.interpolate.splev()
- splev_translated()
Followed sources of scipy.interpolate.splev() to the core fortran functions
in fpbspl.f and splev.f. I then ran these through f2c, pythonified it, and
simplified it. The result is sympy-able
- splev_wikipedia()
Sample implementation of De Boor's algorithm from wikipedia:
https://en.wikipedia.org/wiki/De_Boor%27s_algorithm
from EvalUniformCubicBSpline() in
camera_calibration/applications/camera_calibration/scripts/derive_jacobians.py.
translated to [0,1] from [3,4]
'''
import sys
import numpy as np
import numpysane as nps
import gnuplotlib as gp
skip_plots = False
import scipy.interpolate
from scipy.interpolate import _fitpack
def sample_segment_cubic(x, a,b,c,d):
A = (-x**3 + 3*x**2 - 3*x + 1)/6
B = (3 * x**3/2 - 3*x**2 + 2)/3
C = (-3 * x**3 + 3*x**2 + 3*x + 1)/6
D = (x * x * x) / 6
return A*a + B*b + C*c + D*d
def splev_local(x, t, c, k, der=0, ext=0):
y = scipy.interpolate.splev(x, (t,c,k), der, ext)
return y.reshape(x.shape)
def splev_translated(x, t, c, k, l):
# assumes that t[l] <= x <= t[l+1]
# print(f"l = {l}")
# print((t[l], x, t[l+1]))
l += 1 # l is now a fortran-style index
hh = [0] * 19
h__ = [0] * 19
h__[-1 + 1] = 1
i__1 = k
for j in range(1,i__1+1):
i__2 = j
for i__ in range(1,i__2+1):
hh[-1 + i__] = h__[-1 + i__]
h__[-1 + 1] = 0
i__2 = j
for i__ in range(1,i__2+1):
li = l + i__
lj = li - j
if t[-1 + li] != t[-1 + lj]:
h__[-1 + i__] += (t[-1 + li] - x) * hh[-1 + i__] / (t[-1 + li] - t[-1 + lj])
h__[-1 + i__ + 1] = (x - t[-1 + lj]) * hh[-1 + i__] / (t[-1 + li] - t[-1 + lj])
else:
h__[-1 + i__ + 1] = 0
sp = 0
ll = l - (k+1)
i__2 = (k+1)
for j in range(1,i__2+1):
ll += 1
sp += c[-1 + ll] * h__[-1 + j]
return sp
# from https://en.wikipedia.org/wiki/De_Boor%27s_algorithm
def splev_wikipedia(k: int, x: int, t, c, p: int):
"""Evaluates S(x).
Arguments
---------
k: Index of knot interval that contains x.
x: Position.
t: Array of knot positions, needs to be padded as described above.
c: Array of control points.
We will look at c[k-p .. k]
p: Degree of B-spline.
"""
# make sure I never reference c out of bounds
if k-p < 0: raise Exception("c referenced out of min bounds")
if k >= len(c): raise Exception("c referenced out of max bounds")
d = [c[j + k - p] for j in range(0, p+1)]
for r in range(1, p+1):
for j in range(p, r-1, -1):
alpha = (x - t[j+k-p]) / (t[j+1+k-r] - t[j+k-p])
d[j] = (1 - alpha) * d[j-1] + alpha * d[j]
return d[p]
##############################################################################
# First I confirm that the functions from numpy and from wikipedia produce the
# same results. I don't care about edge cases for now. I query an arbitrary
# point
N = 30
t = np.arange( N, dtype=int)
k = 3
c = np.random.rand(len(t))
x = 4.3
# may have trouble exactly AT the knots. the edge logic isn't quite what I want
l = np.searchsorted(np.array(t), x)-1
y0 = splev_local(np.array((x,)),t,c,k)[0]
y1 = splev_translated(x, t, c, k, l)
y2 = splev_wikipedia(l, x, t, c, k)
print(f"These should all match: {y0} {y1} {y2}")
print(f" err1 = {y1-y0}")
print(f" err2 = {y2-y0}")
##############################################################################
# OK, good. I want to make sure that the spline roughly follows the curve
# defined by the control points. There should be no x offset or anything of that
# sort
N = 30
t = np.arange( N, dtype=int)
k = 3
c = np.random.rand(len(t))
Npad = 10
x = np.linspace(Npad, N-Npad, 1000)
########### sample_segment_cubic()
@nps.broadcast_define(((),), ())
def sample_cubic(x, cp):
i = int(x//1)
q = x-i
try: return sample_segment_cubic(q, *cp[i-1:i+3])
except: return 0
y = sample_cubic(x, c)
c2 = c.copy()
c2[int(N//2)] *= 1.1
y2 = sample_cubic(x, c2)
if not skip_plots:
plot1 = gp.gnuplotlib(title = 'Cubic splines: response to control point perturbation',
# hardcopy = '/tmp/cubic-spline-perturbations.svg',
# terminal = 'svg size 800,600 noenhanced solid dynamic font ",12"'
)
plot1.plot( (x, nps.cat(y,y2),
dict(_with = np.array(('lines lc "blue"',
'lines lc "sea-green"')),
legend = np.array(('Spline: baseline',
'Spline: tweaked one control point')))),
(t[:len(c)], nps.cat(c,c2),
dict(_with = np.array(('points pt 1 ps 1 lc "blue"',
'points pt 2 ps 1 lc "sea-green"')),
legend= np.array(('Control points: baseline',
'Control points: tweaked one control point')))),
(x, y-y2,
dict(_with = 'lines lc "red"',
legend = 'Difference',
y2 = 1)),
_xrange = (10.5,19.5),
y2max = 0.01,
ylabel = 'Spline value',
y2label = 'Difference due to perturbation',)
########### sample_wikipedia()
@nps.broadcast_define(((),), ())
def sample_wikipedia(x, t, c, k):
return splev_wikipedia(np.searchsorted(np.array(t), x)-1,x,t,c,k)
@nps.broadcast_define(((),), ())
def sample_wikipedia_integer_knots(x, c, k):
t = np.arange(len(c) + k, dtype=int)
l = int(x//1)
offset = int((k+1)//2)
return splev_wikipedia(l,x,t,c[offset:],k)
if 1:
y = sample_wikipedia_integer_knots(x,c,k)
else:
offset = int((k+1)//2)
y = sample_wikipedia(x,t, c[offset:], k)
if not skip_plots:
plot2 = gp.gnuplotlib(title = 'sample_wikipedia')
plot2.plot( (x, y, dict(_with='lines')),
(t[:len(c)], c, dict(_with='linespoints pt 7 ps 2')) )
print("these two plots should look the same: we're using two implementation of the same algorithm to interpolate the same data")
plot2.wait()
plot1.wait()
# Now I use sympy to get the polynomial coefficients from sample_wikipedia.
# These should match the ones in sample_segment_cubic()
import sympy
c = sympy.symbols(f'c:{len(c)}')
x = sympy.symbols('x')
# Which interval we're in. Arbitrary. In the middle somewhere
l = int(N//2)
print("Should match A,B,C,D coefficients in sample_segment_cubic()")
s = splev_wikipedia(l,
# I want 'x' to be [0,1] within the interval, but this
# function wants x in the whole domain
x+l,
np.arange( N, dtype=int), c, k).expand()
print(s.coeff(c[12]))
print(s.coeff(c[13]))
print(s.coeff(c[14]))
print(s.coeff(c[15]))
print("Should also match A,B,C,D coefficients in sample_segment_cubic()")
s = splev_translated(# I want 'x' to be [0,1] within the interval, but this
# function wants x in the whole domain
x+l,
np.arange( N, dtype=int), c, k, l).expand()
print(s.coeff(c[12]))
print(s.coeff(c[13]))
print(s.coeff(c[14]))
print(s.coeff(c[15]))
#########################################################
# Great. More questions. Here I have a cubic spline (k==3). And to evaluate the
# spline value I need to have 4 control point values available, 2 on either
# side. Piecewise-linear splines are too rough, but quadratic splines could work
# (k==2). What do the expressions look like? How many neighbors do I need? Here
# the control point values c represent the function value between adjacent
# knots, so each segment uses 3 neighboring control points, and is defined in a
# region [-0.5..0.5] off the center control point
print("======================== k = 2")
N = 30
t = np.arange( N, dtype=int)
k = 2
# c[0,1,2] corresponds to is t[-0.5 0.5 1.5 2.5 ...
c = np.random.rand(len(t))
x = 4.3
# may have trouble exactly AT the knots. the edge logic isn't quite what I want
l = np.searchsorted(np.array(t), x)-1
y0 = splev_local(np.array((x,)),t,c,k)[0]
y1 = splev_translated(x, t, c, k, l)
y2 = splev_wikipedia(l, x, t, c, k)
print(f"These should all match: {y0} {y1} {y2}")
print(f" err1 = {y1-y0}")
print(f" err2 = {y2-y0}")
##############################################################################
# OK, good. I want to make sure that the spline roughly follows the curve
# defined by the control points. There should be no x offset or anything of that
# sort
if not skip_plots:
N = 30
t = np.arange( N, dtype=int)
k = 2
c = np.random.rand(len(t))
Npad = 10
x = np.linspace(Npad, N-Npad, 1000)
offset = int((k+1)//2)
y = sample_wikipedia(x,t-0.5, c[offset:], k)
xm = (x[1:] + x[:-1]) / 2.
d = np.diff(y) / np.diff(x)
plot1 = gp.gnuplotlib(title = 'k==2; sample_wikipedia')
plot1.plot( (x, y, dict(_with='lines', legend='spline')),
(xm, d, dict(_with='lines', y2=1, legend='diff')),
(t[:len(c)], c, dict(_with='linespoints pt 7 ps 2', legend='control points')))
@nps.broadcast_define(((),), ())
def sample_splev_translated(x, t, c, k):
l = np.searchsorted(np.array(t), x)-1
return splev_translated(x,t,c,k,l)
y = sample_splev_translated(x,t-0.5, c[offset:], k)
xm = (x[1:] + x[:-1]) / 2.
d = np.diff(y) / np.diff(x)
plot2 = gp.gnuplotlib(title = 'k==2; splev_translated')
plot2.plot( (x, y, dict(_with='lines', legend='spline')),
(xm, d, dict(_with='lines', y2=1, legend='diff')),
(t[:len(c)], c, dict(_with='linespoints pt 7 ps 2', legend='control points')))
# These are the functions I'm going to use. Derived by the sympy steps
# immediately after this
def sample_segment_quadratic(x, a,b,c):
A = (4*x**2 - 4*x + 1)/8
B = (3 - 4*x**2)/4
C = (4*x**2 + 4*x + 1)/8
return A*a + B*b + C*c
@nps.broadcast_define(((),), ())
def sample_quadratic(x, cp):
i = int((x+0.5)//1)
q = x-i
try: return sample_segment_quadratic(q, *cp[i-1:i+2])
except: return 0
y = sample_quadratic(x, c)
xm = (x[1:] + x[:-1]) / 2.
d = np.diff(y) / np.diff(x)
plot3 = gp.gnuplotlib(title = 'k==2; sample_quadratic')
plot3.plot( (x, y, dict(_with='lines', legend='spline')),
(xm, d, dict(_with='lines', y2=1, legend='diff')),
(t[:len(c)], c, dict(_with='linespoints pt 7 ps 2', legend='control points')))
plot3.wait()
plot2.wait()
plot1.wait()
print("these 3 plots should look the same: we're using different implementation of the same algorithm to interpolate the same data")
# ##################################
# # OK, these match. Let's get the expression of the polynomial in a segment. This
# # was used to construct sample_segment_quadratic() above
# c = sympy.symbols(f'c:{len(c)}')
# x = sympy.symbols('x')
# l = int(N//2)
# print("A,B,C for k==2 using splev_wikipedia()")
# s = splev_wikipedia(l,
# # I want 'x' to be [-0.5..0.5] within the interval, but this
# # function wants x in the whole domain
# x+l,
# np.arange( N, dtype=int) - sympy.Rational(1,2), c, k).expand()
# print(s)
# print(s.coeff(c[13]))
# print(s.coeff(c[14]))
# print(s.coeff(c[15]))
# # I see this:
# # c13*x**2/2 - c13*x/2 + c13/8 - c14*x**2 + 3*c14/4 + c15*x**2/2 + c15*x/2 + c15/8
# # x**2/2 - x/2 + 1/8
# # 3/4 - x**2
# # x**2/2 + x/2 + 1/8
############## compare cubic, quadratic
# I now have nice expressions for quadratic and cubic interpolations. Both are
# C1 continuous, but the cubic interpolation is C2 continuous too. How much do I
# care? Let's at least compare them visually
N = 30
t = np.arange( N, dtype=int)
c = np.random.rand(len(t))
Npad = 10
x = np.linspace(Npad, N-Npad, 1000)
y2 = sample_quadratic(x,c)
y3 = sample_cubic( x,c)
if not skip_plots:
gp.plot( (x, nps.cat(y2,y3), dict(_with='lines',
legend=np.array(('quadratic',
'cubic')))),
(t[:len(c)], c, dict(_with='linespoints pt 7 ps 2',
legend='control points')),
title = "Comparing quadratic and cubic b-spline interpolation",
wait = True)
# Visually, neither looks better than the other. The quadratic spline follows
# the control points closer. Looks like it's trying to match the slope at the
# halfway point. Maybe that's bad, and the quadratic curve is too wiggly? We'll
# see
####################################################################
# Great. Final set of questions: how do you we make a 2D spline surface? The
# generic calibration research library
# (https://github.com/puzzlepaint/camera_calibration) Does a set of 1d
# interpolations in one dimension, and then interpolates the 4 interpolated
# values along the other dimension. Questions:
#
# Does order matter? I can do x and then y or y then x
#
# And is there a better way? scipy has 2d b-spline interpolation routines. Do
# they do something better?
#
# ############### x-y or y-x?
import sympy
from sympy.abc import x,y
cp = sympy.symbols('cp:4(:4)')
# x then y
xs = [sample_segment_cubic( x,
cp[i*4 + 0],
cp[i*4 + 1],
cp[i*4 + 2],
cp[i*4 + 3] ) for i in range(4)]
yxs = sample_segment_cubic(y, *xs)
# y then x
ys = [sample_segment_cubic( y,
cp[0*4 + i],
cp[1*4 + i],
cp[2*4 + i],
cp[3*4 + i] ) for i in range(4)]
xys = sample_segment_cubic(x, *ys)
print(f"Bicubic interpolation. x-then-y and y-then-x difference: {(xys - yxs).expand()}")
########### Alright. Apparently either order is ok. Does scipy do something
########### different for 2d interpolation? I compare the 1d-then-1d
########### interpolation above to fitpack results:
from fpbisp import fpbisp_
N = 30
t = np.arange( N, dtype=int)
k = 3
cp = np.array(sympy.symbols('cp:40(:40)'), dtype=np.object).reshape(40,40)
lx = 3
ly = 5
z = fpbisp_(t, t, k, cp, x+lx, lx, y+ly, ly)
err = z - yxs
print(f"Bicubic interpolation. difference(1d-then-1d, FITPACK): {err.expand()}")
# Apparently chaining 1D interpolations produces identical results to what FITPACK is doing