杰瑞科技汇

Catmull-Rom样条曲线Python实现方法?

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.

Catmull-Rom样条曲线Python实现方法?-图1
(图片来源网络,侵删)

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 alpha or tension) 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:

Catmull-Rom样条曲线Python实现方法?-图2
(图片来源网络,侵删)
  • P_{i-1}: The point before P_i.
  • P_i: The start of the segment.
  • P_{i+1}: The end of the segment.
  • P_{i+2}: The point after P_{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}) / 2
  • m_{i+1} = (P_{i+2} - P_i) / 2

This creates the standard Catmull-Rom spline. A more general form includes a tension parameter alpha:

Catmull-Rom样条曲线Python实现方法?-图3
(图片来源网络,侵删)
  • 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.
分享:
扫描分享到社交APP
上一篇
下一篇