Point projection onto a curve

Point projection onto a curve#

The point-inversion problem#

Given a curve \(\mathbf C(u)\) and an external point \(\mathbf P\), point projection (also called point inversion) finds the parameter \(u^*\) that minimizes the distance between them:

\[u^* = \operatorname*{argmin}_{u\,\in\,[0,1]} \left\lVert \mathbf C(u) - \mathbf P \right\rVert.\]

At an interior minimum, \(\mathbf C(u^*)\) is the closest point on the curve to \(\mathbf P\), and the vector between them is orthogonal to the tangent there:

(1)#\[\big(\mathbf C(u^*) - \mathbf P\big)\cdot \mathbf C'(u^*) = 0.\]

If the closest point lies outside the parameter domain, the minimizer sits at an endpoint (\(u^*=0\) or \(u^*=1\)) instead, where this condition need not hold: the projection is limited by the domain boundary rather than by the curve bending away from \(\mathbf P\).

How nurbspy solves it#

curve.project_point_to_curve(P) minimizes \(\lVert\mathbf C(u)-\mathbf P\rVert\) directly with SciPy’s L-BFGS-B, using the analytic gradient of that objective, which is exactly the orthogonality residual (1) normalized by the distance. To reduce the chance of converging to the wrong local minimum, it first evaluates the objective at the midpoint of every knot span and starts the local optimization from whichever gives the smallest distance.

Multi-start from knot-span midpoints is not a global-optimality guarantee: a curve with several closely spaced local minima can still trap the optimizer at the wrong one. Check contentious cases against (1), or against an independent grid search, as done below.

maxiter, ftol, and gtol control the underlying L-BFGS-B solve and default to 100, 1e-6, and 1e-6 respectively; pass tighter tolerances or a larger maxiter for hard-to-converge projections, or looser ones when only an approximate foot point is needed.

Script#

Run python demos/documentation/point_projection_curve.py, or download the script.

"""Project external points onto a NURBS curve by minimizing distance to it."""
import numpy as np
import matplotlib.pyplot as plt
import nurbspy as nrb

nrb.set_plot_options()

P = np.array([[0., 1., 2., 3., 4.],
              [0., 2., -1., 2., 0.]])
curve = nrb.NurbsCurve(control_points=P, degree=3)

targets = np.array([[0.5, 3.5, 1.0],
                     [2.5, 2.5, -1.0]])

fig, ax = plt.subplots(figsize=(7, 4.5), layout="constrained")
curve.plot_curve(fig, ax, color=nrb.COLORS_MATLAB[0], linewidth=2)
ax.lines[-1].set_label("Curve")

for i in range(targets.shape[1]):
    target = targets[:, i]
    # maxiter, ftol, and gtol are optional overrides for the underlying
    # L-BFGS-B solve; shown here at their defaults.
    u = curve.project_point_to_curve(target, maxiter=100, ftol=1e-6, gtol=1e-6)
    foot = curve.get_value(u)[:, 0]
    tangent = curve.get_derivative(u, 1)[:, 0]
    orthogonality = np.dot(foot - target, tangent)
    print(f"Target {target}: u={u:.6f}, foot={foot.round(6)}, "
          f"(C(u)-P)*C'(u)={orthogonality:.2e}")
    ax.plot([target[0], foot[0]], [target[1], foot[1]], "k--", marker="o",
            markerfacecolor="w", linewidth=1)

ax.set(xlabel="x", ylabel="y", title="Point projection onto a NURBS curve")
ax.set_aspect("equal", adjustable="box")
ax.grid(alpha=0.2)
ax.legend()
plt.show()

Output#

Target

\(u^*\)

Foot point

Orthogonality

\((0.5,\ 2.5)\)

\(0.1722\)

\((0.8758,\ 1.0252)\)

\(-3.8\times10^{-7}\)

\((3.5,\ 2.5)\)

\(0.8278\)

\((3.1242,\ 1.0252)\)

\(3.8\times10^{-7}\)

\((1.0,\ -1.0)\)

\(0.0000\)

\((0.0,\ 0.0)\)

\(6.0\)

A NURBS curve with three external points connected by dashed lines to their closest points on the curve.

Two projections land in the curve’s interior; the third is closest to its clamped start point.#

The first two targets project to interior points, where the orthogonality residual is at solver tolerance. The third target, \((1.0,-1.0)\), is closest to the curve’s clamped start point \(\mathbf C(0)=(0,0)\): the minimizer sits at the domain boundary \(u^*=0\), so (1) does not hold there, and the nonzero residual reflects that, not a solver failure.