Interpolation


Topics in Digital Heritage:
Numerical Methods for Digital Reconstruction

Jin Woo Lee

KAIST

Fall 2026

Recap

From Last Class: Fitting a Curve to Data

Regression (last class)

  • We chose a model \(y = ce^{ax}\) in advance.
  • Minimized \(\tfrac12\sum_i (y_i - f(x_i))^2\).
  • The curve passes near the data, since data are noisy.

Interpolation (today)

  • We trust the samples exactly: \(f(x_i) = y_i\).
  • We do not know the model.
  • Fill in the gaps between samples “reasonably”.

Why Interpolation? Everything Digital Is Sampled

We only know \(f(\mathbf{x}_i)=y_i\) at finitely many \(\mathbf{x}_i\). But, in many scenarios, we need \(f(\mathbf{x})\) between the samples.

(e.g., image rotation, audio fractional delay, surface reconstruction, coordinate transformation, etc.)


Interpolation problem: given \((\mathbf{x}_1,y_1),\dots,(\mathbf{x}_k,y_k)\), construct \(f\) with \(f(\mathbf{x}_i)=y_i\) that is smooth and adds no spurious features.

Today’s ILO

  • Formulate interpolation as choosing a basis and solving for coefficients, and build the same polynomial interpolant in the monomial, Lagrange, and Newton bases, comparing their cost and stability.
  • Explain why high-degree polynomial interpolation fails (nonlocality, Runge phenomenon) and how Chebyshev nodes or piecewise interpolation fix it.
  • Extend interpolation to 2D with nearest neighbor, barycentric, and bilinear schemes, and apply them to image resampling, shading, and audio fractional delay.
  • Preview where interpolants reappear: numerical integration and differentiation (next classes) and delay lines in instrument models (later this semester).

Polynomial Interpolation

Interpolation as Linear Algebra

Searching over all functions is hopeless.

Instead, pick basis functions \(\phi_1,\dots,\phi_k\) and write

\[ f(x) = \sum_{i=1}^k c_i\,\phi_i(x). \]

The interpolation conditions \(f(x_j)=y_j\) become a \(k\times k\) linear system in the coefficients: \[ \underbrace{\begin{bmatrix} \phi_1(x_1) & \phi_2(x_1) & \cdots & \phi_k(x_1)\\ \vdots & \vdots & & \vdots\\ \phi_1(x_k) & \phi_2(x_k) & \cdots & \phi_k(x_k) \end{bmatrix}}_{\boldsymbol{\Phi}} \begin{bmatrix} c_1\\ \vdots\\ c_k\end{bmatrix} = \begin{bmatrix} y_1\\ \vdots\\ y_k\end{bmatrix}. \]

Choosing the basis = choosing the structure of \(\boldsymbol{\Phi}\).

This decides cost and stability of our interpolant.

Monomial Basis

As one of the simplest basis functions, we can take the monomials \(\phi_i(x)=x^{i-1}\), \(i=1,\dots,k\). With this, \(\boldsymbol{\Phi}\) is the Vandermonde matrix:

\[ \begin{bmatrix} 1 & x_1 & x_1^2 & \cdots & x_1^{k-1}\\ 1 & x_2 & x_2^2 & \cdots & x_2^{k-1}\\ \vdots & & & & \vdots\\ 1 & x_k & x_k^2 & \cdots & x_k^{k-1} \end{bmatrix} \begin{bmatrix} a_0\\ a_1\\ \vdots\\ a_{k-1}\end{bmatrix} = \begin{bmatrix} y_1\\ y_2\\ \vdots\\ y_k\end{bmatrix} \]

V = np.vander(xk, increasing=True)   # k x k
a = np.linalg.solve(V, yk)           # O(k^3): solving V*a=yk
p = np.polynomial.polynomial.polyval(x, a) # O(k): interpolation

Why Monomials Are a Bad Basis

As \(n\) grows, \(x^n\) on \([0,1]\) all look alike.

Measure it with the function inner product \(\langle f,g\rangle=\int_0^1 f(x)g(x)\,dx\): \[ \frac{\langle x^n, x^m\rangle}{\|x^n\|\,\|x^m\|} = \frac{\sqrt{(2n+1)(2m+1)}}{n+m+1}. \] e.g. \(n=9, m=10\), cosine similarity \(\approx 0.997\)

Nearly parallel basis vectors

\(\Rightarrow\) cancelling coefficients in column space

\(\Rightarrow\) ill-conditioned \(\boldsymbol{\Phi}\)

(same lesson as least squares with nearly dependent columns).

Make \(\boldsymbol{\Phi}\) the Identity

Lagrange Basis

\[ \phi_i(x) \equiv \frac{\prod_{j\neq i}(x-x_j)}{\prod_{j\neq i}(x_i-x_j)} \] is \(1\) at its own node and \(0\) at every other node: \[ \phi_i(x_\ell)=\begin{cases}1 & \ell=i\\ 0 & \ell\neq i\end{cases} \] So \(\boldsymbol{\Phi}=\mathbf{I}\) and the coefficients are just the data: \[ f(x)=\sum_i y_i\,\phi_i(x). \] No linear solve at all.

Show code
def lagrange_eval(xk, yk, x):
    """Evaluate the interpolating polynomial in the Lagrange basis."""
    x = np.asarray(x, dtype=float)
    out = np.zeros_like(x)
    for i in range(len(xk)):
        phi = np.ones_like(x)
        for j in range(len(xk)):
            if j != i:
                phi *= (x - xk[j]) / (xk[i] - xk[j])
        out += yk[i] * phi
    return out

xk = np.array([0., 2., 3., 4.])
xs = np.linspace(-0.2, 4.2, 300)
fig, ax = plt.subplots(figsize=(4, 3.3))
for i in range(len(xk)):
    e = np.zeros(len(xk)); e[i] = 1
    ax.plot(xs, lagrange_eval(xk, e, xs), lw=2, label=f"$\\phi_{i+1}$")
ax.plot(xk, np.zeros_like(xk), "ks", ms=4)
ax.plot(xk, np.ones_like(xk), "k.", ms=6)
ax.axhline(0, color="k", lw=0.5); ax.axhline(1, color="gray", lw=0.5, ls=":")
ax.set_ylim(-2, 2)
ax.set_xticks(xk)
ax.set_title("Basis for $0,2,3,4$", fontsize=9)
ax.legend(fontsize=8, ncol=4, loc="upper center")
plt.tight_layout()
plt.show()

Cost: \(O(k^2)\) per evaluation, and the denominator can be tiny when nodes cluster.

Make \(\boldsymbol{\Phi}\) Triangular

Newton Basis

\[ \phi_1(x)=1,\qquad \phi_i(x)=\prod_{j=1}^{i-1}(x-x_j). \]

Since \(\phi_i(x_\ell)=0\) for \(\ell<i\), the system is lower triangular: \[ \begin{bmatrix} 1 & 0 & 0 & \cdots\\ 1 & \phi_2(x_2) & 0 & \cdots\\ 1 & \phi_2(x_3) & \phi_3(x_3) & \cdots\\ \vdots & \vdots & \vdots & \ddots \end{bmatrix}\mathbf{c}=\mathbf{y} \] Solving for the coefficients is \(O(k^2)\).

Coefficients via divided differences:

def newton_coeffs(xk, yk):
    c = yk.astype(float).copy()
    for j in range(1, len(xk)):
        c[j:] = (c[j:] - c[j-1:-1]) / (xk[j:] - xk[:-j])
    return c

def newton_eval(c, xk, x):     # Horner-like, O(k)
    p = c[-1]
    for j in range(len(c) - 2, -1, -1):
        p = p * (x - xk[j]) + c[j]
    return p
  • No large denominators: more stable than Lagrange.
  • Adding a new point \((x_{k+1},y_{k+1})\) only appends one new coefficient.

Three Bases, Same Polynomial

Basis \(\boldsymbol{\Phi}\) Setup Evaluate Stability
Monomial dense \(O(k^3)\) \(O(k)\) poor (Vandermonde)
Lagrange identity none \(O(k^2)\) tiny denominators
Newton triangular \(O(k^2)\) \(O(k)\) good, easy to extend

Exactly one polynomial of degree \(\le k-1\) passes through \(k\) points

So all three give the same \(f\) but with different costs and stability.

When Polynomials Misbehave

Problem 1: Polynomials Are Nonlocal

Global support of Lagrange basis functions

Changing one \(y_i\) changes \(f\) everywhere.

Problem 2: The Runge Phenomenon

Simply increasing the number of equispaced nodes can make the interpolant worse (Runge, 1901).

Fix: Use nonuniform nodes (Chebyshev) or piecewise interpolation.

Chebyshev Nodes

\[x_i = \cos\!\Big(\frac{i\pi}{k-1}\Big),\quad i=0,\dots,k-1\]

Catch: only possible when we choose where to sample.

But: most of our data (graphics, audio, simulation) is on a fixed grid.

Demo: 1D Interpolation Playground

Piecewise Interpolation

Compact Support: Only Neighbors Matter

Piecewise constant (nearest neighbor): \[f(x)=y_i,\quad i=\arg\min_j |x-x_j|.\]

Piecewise linear: for \(x\in[x_i,x_{i+1}]\), with \(t=\frac{x-x_i}{x_{i+1}-x_i}\), \[f(x) = (1-t)\,y_i + t\,y_{i+1}.\]

Smoother Pieces: Cubic Splines

Pieces Continuity
constant discontinuous
linear \(C^0\)
quadratic up to \(C^1\)
cubic up to \(C^2\)

Cubic spline: cubic on each interval, matching \(f\), \(f'\), \(f''\) at the nodes. The unknown \(f''(x_i)\) satisfy a tridiagonal system, solvable in \(O(k)\).

from scipy.interpolate import CubicSpline
f = CubicSpline(xk, yk)       # natural / not-a-knot ends
f(x), f(x, 1), f(x, 2)        # value, f', f''

More smoothness = a stronger assumption on \(f\). Discontinuities (edges, shocks) get overshoot.

Application: Fractional Delay in Audio

Show code
fs, D = 48000, 10.37
n = np.arange(2048)
freqs = np.geomspace(100, 16000, 30)

def linear_delay(x, D):                    # 2-tap linear
    m = np.arange(len(x)) - D              # fractional read positions
    return np.interp(m, np.arange(len(x)), x)

def lagrange3(x, D):                       # 4-tap cubic Lagrange
    i0 = int(np.floor(D)) - 1; d = D - i0  # taps at offsets i0..i0+3
    taps = [np.prod([(d - j) / (i - j) for j in range(4) if j != i]) for i in range(4)]
    y = np.zeros_like(x)
    for i, h in enumerate(taps):
        y[i0 + i:] += h * x[:len(x) - i0 - i]
    return y

err_lin, err_lag = [], []
for f0 in freqs:
    x = np.sin(2 * np.pi * f0 * n / fs)
    ref = np.sin(2 * np.pi * f0 * (n - D) / fs)
    s = slice(64, -64)
    err_lin.append(np.abs(linear_delay(x, D) - ref)[s].max())
    err_lag.append(np.abs(lagrange3(x, D) - ref)[s].max())

fig, ax = plt.subplots(figsize=(4.6, 3.4))
ax.loglog(freqs, err_lin, "-o", ms=3, color="steelblue", label="linear (2 taps)")
ax.loglog(freqs, err_lag, "-o", ms=3, color="forestgreen", label="cubic Lagrange (4 taps)")
ax.set_xlabel("frequency [Hz]"); ax.set_ylabel("max error (unit sine)")
ax.set_title(f"delay by D = {D} samples at {fs//1000} kHz", fontsize=9)
ax.legend(fontsize=8)
plt.tight_layout()
plt.show()

Delay a signal by \(D\) samples: \[y[n] = x[n-D]\]

  • Integer \(D\): just shift the array.
  • \(D = 10.37\): we must read between samples, i.e., interpolate.

Used in chorus/flanger effects, Doppler, sample-rate conversion, and digital waveguide-ish string and wind models (later this semester), where the delay length sets the pitch.

Interpolation in Karplus-Strong String Model

Multivariable Interpolation

Nearest Neighbor and Voronoi Cells

Nearest neighbor: \(f(\mathbf{x}) = y_i\) with \(i=\arg\min_j\|\mathbf{x}-\mathbf{x}_j\|_2\).

Piecewise constant on Voronoi cells \[V_i=\{\mathbf{x}: \|\mathbf{x}-\mathbf{x}_i\|<\|\mathbf{x}-\mathbf{x}_j\|\ \forall j\ne i\}.\]

Fast lookup with a k-d tree (scipy.spatial.cKDTree).

Inverse distance weighting gives a continuous result: \[ f(\mathbf{x}) = \frac{\sum_i w_i(\mathbf{x})\,y_i}{\sum_i w_i(\mathbf{x})},\qquad w_i(\mathbf{x}) = \|\mathbf{x}-\mathbf{x}_i\|_2^{-p}. \] As \(p\to\infty\) it becomes nearest neighbor again.

Barycentric Interpolation on a Triangle

  • \(\mathbf{p}\) inside \(\iff\) all \(a_i\ge 0\).

Write \(\mathbf{p}\) as a weighted average of the vertices: \[ \mathbf{p} = a_1\mathbf{x}_1 + a_2\mathbf{x}_2 + a_3\mathbf{x}_3,\quad a_1+a_2+a_3 = 1 \] then interpolate \(f(\mathbf{p}) = a_1y_1+a_2y_2+a_3y_3\).

\(y_i\) can be color, normal, texture coordinate, etc.

  • Algebra: linear system \[\begin{bmatrix}\mathbf{x}_1 & \mathbf{x}_2 & \mathbf{x}_3\\ 1 & 1 & 1\end{bmatrix}\mathbf{a} = \begin{bmatrix}\mathbf{p}\\ 1\end{bmatrix}.\]
  • Geometry: \(a_i = A_i/A\), the area of the sub-triangle opposite \(\mathbf{x}_i\).
  • Gouraud shading
    • interpolate vertex colors, normals, texture coordinates across the triangle
    • GPU does this billions of times per frame

Grid Data: Bilinear Interpolation

Apply 1D linear interpolation one axis at a time. On a unit cell with \(t_x, t_y\in[0,1]\): \[ \begin{aligned} f(t_x,0) &= (1-t_x)f_{00} + t_x f_{10}\\ f(t_x,1) &= (1-t_x)f_{01} + t_x f_{11}\\ f(t_x,t_y) &= (1-t_y)f(t_x,0) + t_y f(t_x,1) \end{aligned} \]

Example: \(f_{00}=1,\ f_{10}=5,\ f_{01}=-3,\ f_{11}=-11\). Find \(f(\tfrac14,\tfrac12)\): \[ \begin{aligned} f(\tfrac14,0)&=\tfrac34\cdot1+\tfrac14\cdot5=2\\ f(\tfrac14,1)&=\tfrac34(-3)+\tfrac14(-11)=-5\\ f(\tfrac14,\tfrac12)&=\tfrac12\cdot2+\tfrac12(-5)=-\tfrac32 \end{aligned} \] Same answer if you interpolate in \(y\) first.

Demo: Interpolation in 2D

Code: Rotating an Image with Bilinear Interpolation

Show code
def make_image(n=48):
    u = np.linspace(-1, 1, n)
    X, Y = np.meshgrid(u, u)
    img = ((X**2 + Y**2) < 0.5).astype(float) * 0.7
    img += ((np.abs(X - 0.35) < 0.15) & (np.abs(Y + 0.4) < 0.4)) * 1.0
    img += 0.3 * ((np.floor(4 * (X + 1)) + np.floor(4 * (Y + 1))) % 2)
    return img

def bilinear(img, u, v):
    """Sample img at continuous (row=v, col=u) positions; zero outside."""
    H, W = img.shape
    i0, j0 = np.floor(v).astype(int), np.floor(u).astype(int)
    ty, tx = v - i0, u - j0
    def px(i, j):
        ok = (i >= 0) & (i < H) & (j >= 0) & (j < W)
        return np.where(ok, img[np.clip(i, 0, H - 1), np.clip(j, 0, W - 1)], 0.0)
    top = (1 - tx) * px(i0, j0) + tx * px(i0, j0 + 1)
    bot = (1 - tx) * px(i0 + 1, j0) + tx * px(i0 + 1, j0 + 1)
    return (1 - ty) * top + ty * bot

def nearest(img, u, v):
    return bilinear(img, np.round(u), np.round(v))

def rotate(img, deg, sampler):
    H, W = img.shape
    c = np.array([(H - 1) / 2, (W - 1) / 2])
    I, J = np.mgrid[0:H, 0:W].astype(float)
    th = np.deg2rad(deg)
    # inverse map: for every OUTPUT pixel, where do we read in the INPUT?
    v = np.cos(th) * (I - c[0]) - np.sin(th) * (J - c[1]) + c[0]
    u = np.sin(th) * (I - c[0]) + np.cos(th) * (J - c[1]) + c[1]
    return sampler(img, u, v)

img = make_image()
rep = img.copy()
for _ in range(12):
    rep = rotate(rep, 30, bilinear)

fig, axs = plt.subplots(1, 4, figsize=(9, 2.9))
for ax, im, ttl in zip(axs, [img, rotate(img, 30, nearest), rotate(img, 30, bilinear), rep],
                       ["original", "30°, nearest", "30°, bilinear", "12 × 30° bilinear"]):
    ax.imshow(im, cmap="gray", vmin=0, vmax=2, interpolation="nearest")
    ax.set_title(ttl, fontsize=9); ax.axis("off")
plt.tight_layout()
plt.show()

Repeated resampling accumulates blur: compose the transforms, then resample once.

Looking Ahead

Where You’ll See This Again

  • Numerical Integration (next class): integrate the interpolant. Piecewise linear \(\Rightarrow\) trapezoidal rule; piecewise quadratic \(\Rightarrow\) Simpson’s rule; orthogonal polynomials \(\Rightarrow\) Gaussian quadrature.
  • Numerical Differentiation: differentiate the interpolant to get finite-difference formulas, with the same \(O(\Delta x^p)\) analysis.
  • Immersed Boundary / Finite Element Methods: the solution is only known at grid or mesh points; interpolate to read values in between.
  • Stringed and Wind Instruments: fractional delay interpolation tunes digital delay lines to exact pitches.
  • How to CAD: Bézier curves and B-splines are piecewise polynomials in a well-chosen basis.

Recap

Recap on Today’s ILO

  • Formulate interpolation as basis + coefficients, and compare the monomial / Lagrange / Newton bases.
    • Vandermonde is ill-conditioned; Lagrange needs no solve; Newton is triangular. Same unique polynomial.
  • Explain the failure modes of high-degree polynomials and their fixes.
    • Global support makes them nonlocal; equispaced nodes cause Runge oscillations. Fix with Chebyshev nodes or piecewise interpolation.
  • Interpolate in 2D: nearest neighbor (Voronoi), barycentric on triangles, bilinear on grids.
    • Applied to image rotation, Gouraud shading, and fractional audio delay.
  • Preview: integration and differentiation of sampled data start by interpolating it (next classes).
    • All the ‘functions’ we want to integrate/differentiate are given as ‘discrete samples’ only.
    • Integration/differentiation of sampled data starts by interpolating the data first.