Of course! Here's a comprehensive guide to implementing and using Catmull-Rom splines in Python, including the mathematical background, a pure Python implementation, and a practical example using the popular matplotlib library.

What is a Catmull-Rom Spline?
A Catmull-Rom spline is a type of cubic Hermite spline that passes through all of its control points. This is a key advantage over Bézier curves, which only pass through their first and last points.
The "Catmull-Rom" part refers to its specific way of calculating the tangent vectors at each control point. The tangent at a point is calculated using its neighboring points, resulting in a smooth, natural-looking curve that is widely used in computer graphics, animation (e.g., camera paths), and data visualization.
Key Characteristics:
- Interpolating: It passes through all given control points.
- Local Control: Moving one control point only affects the curve segment between its immediate neighbors.
- Tension Control: A parameter (often called
alphaortension) can be adjusted to make the curve "tighter" or "looser" around the control points.
The Mathematical Background
A Catmull-Rom spline is defined by a cubic polynomial for each segment between two control points, P_i and P_{i+1}.
To calculate the curve segment from P_i to P_{i+1}, we need four points:

P_{i-1}: The point beforeP_i.P_i: The start of the segment.P_{i+1}: The end of the segment.P_{i+2}: The point afterP_{i+1}.
For points at the beginning or end of the list, we can create "virtual" points by reflecting the endpoints.
The curve is parameterized by t, which goes from 0 to 1 as we move from P_i to P_{i+1}.
The formula can be derived from the general cubic Hermite spline formula by defining the tangents m_i and m_{i+1} as:
m_i = (P_{i+1} - P_{i-1}) / 2m_{i+1} = (P_{i+2} - P_i) / 2
This creates the standard Catmull-Rom spline. A more general form includes a tension parameter alpha:

m_i = (P_{i+1} - P_{i-1}) / 2 * (1 - alpha)m_{i+1} = (P_{i+2} - P_i) / 2 * (1 - alpha)
When alpha = 0, we get the standard spline. When alpha = 1, the curve becomes a simple polyline connecting the points.
Pure Python Implementation
This implementation will be a simple, easy-to-understand function that takes a list of 2D points and returns a list of points along the smooth curve.
import math
def catmull_rom_spline(points, num_points_per_segment=10):
"""
Calculates a Catmull-Rom spline curve from a set of control points.
Args:
points (list of tuples): A list of 2D control points [(x0, y0), (x1, y1), ...].
num_points_per_segment (int): The number of points to calculate for each segment.
Returns:
list of tuples: A list of points representing the smooth curve.
"""
if len(points) < 2:
return []
# Handle the special case of having only two points
if len(points) == 2:
# Just interpolate linearly between the two points
p0, p1 = points
return [(p0[0] + t * (p1[0] - p0[0]), p0[1] + t * (p1[1] - p0[1]))
for t in [i / num_points_per_segment for i in range(num_points_per_segment + 1)]]
# Create a list to store the resulting curve points
curve_points = []
# Add the first point to the curve
curve_points.append(points[0])
# Iterate through the points to create segments
# We need to iterate up to len(points) - 2 to get P_{i+1} and P_{i+2}
for i in range(len(points) - 1):
# Get the four points defining the segment
p0 = points[i-1] if i > 0 else points[0]
p1 = points[i]
p2 = points[i+1]
p3 = points[i+2] if i < len(points) - 2 else p2
# Generate points for this segment
for t in [j / num_points_per_segment for j in range(1, num_points_per_segment + 1)]:
# Catmull-Rom formula
t2 = t * t
t3 = t2 * t
x = 0.5 * (
(2 * p1[0]) +
(-p0[0] + p2[0]) * t +
(2 * p0[0] - 5 * p1[0] + 4 * p2[0] - p3[0]) * t2 +
(-p0[0] + 3 * p1[0] - 3 * p2[0] + p3[0]) * t3
)
y = 0.5 * (
(2 * p1[1]) +
(-p0[1] + p2[1]) * t +
(2 * p0[1] - 5 * p1[1] + 4 * p2[1] - p3[1]) * t2 +
(-p0[1] + 3 * p1[1] - 3 * p2[1] + p3[1]) * t3
)
curve_points.append((x, y))
# The last point of the last segment might be duplicated, so we can remove it,
# but for simplicity, we'll just add the final control point to ensure it's included.
if curve_points[-1] != points[-1]:
curve_points.append(points[-1])
return curve_points
# --- Example Usage ---
if __name__ == '__main__':
# Define some control points
control_points = [(0, 0), (1, 5), (4, 2), (6, 6), (8, 1)]
# Generate the spline curve
smooth_curve = catmull_rom_spline(control_points, num_points_per_segment=20)
# Print the first 10 points to see the output
print("Control Points:", control_points)
print("\nFirst 10 points of the smooth curve:")
for i, point in enumerate(smooth_curve[:10]):
print(f" {i}: {point}")
# The rest of the points can be used for plotting...
Practical Example with Matplotlib
This example uses the pure Python function above to plot the control points and the resulting smooth curve.
import matplotlib.pyplot as plt
# (Include the catmull_rom_spline function from above here)
# --- Main execution for plotting ---
if __name__ == '__main__':
# Define some control points
control_points = [(0, 0), (1, 5), (4, 2), (6, 6), (8, 1)]
# Generate the spline curve
smooth_curve = catmull_rom_spline(control_points, num_points_per_segment=50)
# Unpack points for plotting
ctrl_x, ctrl_y = zip(*control_points)
smooth_x, smooth_y = zip(*smooth_curve)
# Create the plot
plt.figure(figsize=(10, 6))
plt.plot(ctrl_x, ctrl_y, 'o--', color='gray', label='Control Points')
plt.plot(smooth_x, smooth_y, '-', color='blue', linewidth=2, label='Catmull-Rom Spline')
# Add labels and title
plt.title("Catmull-Rom Spline Example")
plt.xlabel("X-axis")
plt.ylabel("Y-axis")
plt.grid(True)
plt.axis('equal') # Ensures the aspect ratio is equal, so circles look like circles
plt.legend()
# Show the plot
plt.show()
Running this code will produce a plot like this:
Advanced: Using SciPy for a More Robust Solution
For serious applications, it's better to use a well-tested library like SciPy. It's faster, more numerically stable, and more flexible. The scipy.interpolate module has a CubicSpline class, but for a direct Catmull-Rom equivalent, we use interpolate.make_interp1d with kind='cubic'.
However, make_interp1d requires x and y to be separated. We also need to handle the "virtual" points at the boundaries ourselves to ensure the curve passes through the first and last points.
Here's how you'd do it with SciPy:
import numpy as np
from scipy.interpolate import make_interp1d
import matplotlib.pyplot as plt
def scipy_catmull_rom(points, num_points_per_segment=50):
"""
Calculates a Catmull-Rom spline using SciPy.
"""
if len(points) < 2:
return np.array([])
# Unpack points
x, y = zip(*points)
x, y = np.array(x), np.array(y)
# To ensure the spline passes through the first and last points,
# we add "virtual" points at the boundaries.
# For the left boundary, we reflect the first two points.
# For the right boundary, we reflect the last two points.
x_extended = np.concatenate(([2*x[0] - x[1]], x, [2*x[-1] - x[-2]]))
y_extended = np.concatenate(([2*y[0] - y[1]], y, [2*y[-1] - y[-2]]))
# Create the interpolation function for x and y separately
# `kind='cubic'` gives a cubic spline, which is equivalent to Catmull-Rom
# when the boundary conditions are set up this way.
fx = make_interp1d(x_extended, y_extended, kind='cubic', fill_value="extrapolate")
fy = make_interp1d(y_extended, x_extended, kind='cubic', fill_value="extrapolate")
# Generate new t values for the original segment boundaries
t_original = np.linspace(x[0], x[-1], len(x))
# Generate a finer set of t values for the smooth curve
t_fine = np.linspace(x[0], x[-1], len(points) * num_points_per_segment)
# Calculate the smooth curve points
smooth_x = t_fine
smooth_y = fx(t_fine)
return np.column_stack((smooth_x, smooth_y))
# --- Example Usage with SciPy ---
if __name__ == '__main__':
control_points = [(0, 0), (1, 5), (4, 2), (6, 6), (8, 1)]
# Generate the spline using SciPy
scipy_curve = scipy_catmull_rom(control_points, num_points_per_segment=50)
# Plotting
ctrl_x, ctrl_y = zip(*control_points)
plt.figure(figsize=(10, 6))
plt.plot(ctrl_x, ctrl_y, 'o--', color='gray', label='Control Points')
plt.plot(scipy_curve[:, 0], scipy_curve[:, 1], '-', color='red', linewidth=2, label='SciPy Catmull-Rom')
plt.title("Catmull-Rom Spline with SciPy")
plt.xlabel("X-axis")
plt.ylabel("Y-axis")
plt.grid(True)
plt.axis('equal')
plt.legend()
plt.show()
Summary: Which one should you use?
| Method | Pros | Cons | Best for... |
|---|---|---|---|
| Pure Python | Easy to understand, no external dependencies, good for learning. | Slower, may have numerical issues, less flexible. | Learning the algorithm, simple scripts, environments without SciPy. |
| SciPy | Fast, robust, highly optimized, flexible. | Requires an external library (scipy), slightly more complex setup for boundary conditions. |
Production code, data analysis, performance-critical applications. |
