web-dev-qa-db-fra.com

Point le plus proche sur une courbe cubique bézier?

Comment puis-je trouver le point B(t) le long d'une courbe cubique bézier qui est la plus proche d'un point arbitraire P dans l'avion?

37
Adrian Lopez

Après de nombreuses recherches, j'ai trouvé un papier qui discute d'une méthode de recherche du point le plus proche d'une courbe de Bézier à un point donné:

Algorithme algébrique amélioré sur la projection de points pour les courbes de Bezier , par Xiao-dio Chen, Yin Zhou, Zhenyu Shu, Hua Su et Jean-Claude Paul.

En outre, j'ai trouvé Wikipedia et Mathworld's Descriptions de séquences Sturm utiles pour comprendre la première partie de l'algoritme, car le papier lui-même n'est pas très clair dans sa propre description.

18
Adrian Lopez

J'ai écrit un code rapide et sale qui estime cela pour les courbes de Bézier de toute mesure. (REMARQUE: Ceci est une force pseudo-brute, pas une solution de forme fermée.)

Démo: http://phrogz.net/svg/closest-point-on-bezier.html

/** Find the ~closest point on a Bézier curve to a point you supply.
 * out    : A vector to modify to be the point on the curve
 * curve  : Array of vectors representing control points for a Bézier curve
 * pt     : The point (vector) you want to find out to be near
 * tmps   : Array of temporary vectors (reduces memory allocations)
 * returns: The parameter t representing the location of `out`
 */
function closestPoint(out, curve, pt, tmps) {
    let mindex, scans=25; // More scans -> better chance of being correct
    const vec=vmath['w' in curve[0]?'vec4':'z' in curve[0]?'vec3':'vec2'];
    for (let min=Infinity, i=scans+1;i--;) {
        let d2 = vec.squaredDistance(pt, bézierPoint(out, curve, i/scans, tmps));
        if (d2<min) { min=d2; mindex=i }
    }
    let t0 = Math.max((mindex-1)/scans,0);
    let t1 = Math.min((mindex+1)/scans,1);
    let d2ForT = t => vec.squaredDistance(pt, bézierPoint(out,curve,t,tmps));
    return localMinimum(t0, t1, d2ForT, 1e-4);
}

/** Find a minimum point for a bounded function. May be a local minimum.
 * minX   : the smallest input value
 * maxX   : the largest input value
 * ƒ      : a function that returns a value `y` given an `x`
 * ε      : how close in `x` the bounds must be before returning
 * returns: the `x` value that produces the smallest `y`
 */
function localMinimum(minX, maxX, ƒ, ε) {
    if (ε===undefined) ε=1e-10;
    let m=minX, n=maxX, k;
    while ((n-m)>ε) {
        k = (n+m)/2;
        if (ƒ(k-ε)<ƒ(k+ε)) n=k;
        else               m=k;
    }
    return k;
}

/** Calculate a point along a Bézier segment for a given parameter.
 * out    : A vector to modify to be the point on the curve
 * curve  : Array of vectors representing control points for a Bézier curve
 * t      : Parameter [0,1] for how far along the curve the point should be
 * tmps   : Array of temporary vectors (reduces memory allocations)
 * returns: out (the vector that was modified)
 */
function bézierPoint(out, curve, t, tmps) {
    if (curve.length<2) console.error('At least 2 control points are required');
    const vec=vmath['w' in curve[0]?'vec4':'z' in curve[0]?'vec3':'vec2'];
    if (!tmps) tmps = curve.map( pt=>vec.clone(pt) );
    else tmps.forEach( (pt,i)=>{ vec.copy(pt,curve[i]) } );
    for (var degree=curve.length-1;degree--;) {
        for (var i=0;i<=degree;++i) vec.lerp(tmps[i],tmps[i],tmps[i+1],t);
    }
    return vec.copy(out,tmps[0]);
}

Le code ci-dessus utilise la bibliothèque VMATH sur efficacement LERP entre les vecteurs (en 2D, 3D ou 4D), mais il serait trivial de remplacer l'appel lerp() dans bézierPoint() Avec votre propre code.

Tuning l'algorithme

La fonction closestPoint() fonctionne en deux phases:

  • Tout d'abord, calculez les points tout au long de la courbe (valeurs espacées uniformément du T Paramètre). Enregistrez la valeur de t a la plus petite distance du point.
  • Ensuite, utilisez la fonction localMinimum() pour chasser la région autour de la plus petite distance, à l'aide d'une recherche binaire pour trouver le T et point qui produit la vraie la plus petite distance.

La valeur de scans in closestPoint() détermine le nombre d'échantillons à utiliser lors de la première passe. Moins d'analyses est plus rapide, mais augmente les chances de manquer le vrai point minimum.

La limite ε transmise à la fonction localMinimum() contrôle la durée de la longueur de la meilleure valeur. Une valeur de 1e-2 Quantiquez la courbe en ~ 100 points et vous pouvez ainsi voir les points renvoyés de closestPoint() apparaissant le long de la ligne. Chaque point décimal supplémentaire de la précision -1e-3, 1e-4, ... -Costs environ 6-8 appels supplémentaires à bézierPoint().

14
Phrogz

En fonction de vos tolérances. Force brute et être accepté d'erreur. Cet algorithme pourrait avoir tort pour certains cas rares. Mais, dans la majorité d'entre eux, il trouvera un point très proche de la bonne réponse et les résultats amélioreront les tranche plus élevées que vous définissez les tranches. Il essaie juste chaque point le long de la courbe à intervalles réguliers et renvoie le meilleur qu'il a trouvé.

public double getClosestPointToCubicBezier(double fx, double fy, int slices, double x0, double y0, double x1, double y1, double x2, double y2, double x3, double y3)  {
    double tick = 1d / (double) slices;
    double x;
    double y;
    double t;
    double best = 0;
    double bestDistance = Double.POSITIVE_INFINITY;
    double currentDistance;
    for (int i = 0; i <= slices; i++) {
        t = i * tick;
        //B(t) = (1-t)**3 p0 + 3(1 - t)**2 t P1 + 3(1-t)t**2 P2 + t**3 P3
        x = (1 - t) * (1 - t) * (1 - t) * x0 + 3 * (1 - t) * (1 - t) * t * x1 + 3 * (1 - t) * t * t * x2 + t * t * t * x3;
        y = (1 - t) * (1 - t) * (1 - t) * y0 + 3 * (1 - t) * (1 - t) * t * y1 + 3 * (1 - t) * t * t * y2 + t * t * t * y3;

        currentDistance = Point.distanceSq(x,y,fx,fy);
        if (currentDistance < bestDistance) {
            bestDistance = currentDistance;
            best = t;
        }
    }
    return best;
}

Vous pouvez obtenir beaucoup mieux et plus vite en trouvant simplement le point le plus proche et en recrutant ce point.

public double getClosestPointToCubicBezier(double fx, double fy, int slices, int iterations, double x0, double y0, double x1, double y1, double x2, double y2, double x3, double y3) {
    return getClosestPointToCubicBezier(iterations, fx, fy, 0, 1d, slices, x0, y0, x1, y1, x2, y2, x3, y3);
}

private double getClosestPointToCubicBezier(int iterations, double fx, double fy, double start, double end, int slices, double x0, double y0, double x1, double y1, double x2, double y2, double x3, double y3) {
    if (iterations <= 0) return (start + end) / 2;
    double tick = (end - start) / (double) slices;
    double x, y, dx, dy;
    double best = 0;
    double bestDistance = Double.POSITIVE_INFINITY;
    double currentDistance;
    double t = start;
    while (t <= end) {
        //B(t) = (1-t)**3 p0 + 3(1 - t)**2 t P1 + 3(1-t)t**2 P2 + t**3 P3
        x = (1 - t) * (1 - t) * (1 - t) * x0 + 3 * (1 - t) * (1 - t) * t * x1 + 3 * (1 - t) * t * t * x2 + t * t * t * x3;
        y = (1 - t) * (1 - t) * (1 - t) * y0 + 3 * (1 - t) * (1 - t) * t * y1 + 3 * (1 - t) * t * t * y2 + t * t * t * y3;


        dx = x - fx;
        dy = y - fy;
        dx *= dx;
        dy *= dy;
        currentDistance = dx + dy;
        if (currentDistance < bestDistance) {
            bestDistance = currentDistance;
            best = t;
        }
        t += tick;
    }
    return getClosestPointToCubicBezier(iterations - 1, fx, fy, Math.max(best - tick, 0d), Math.min(best + tick, 1d), slices, x0, y0, x1, y1, x2, y2, x3, y3);
}

Dans les deux cas, vous pouvez faire le quad tout aussi facilement:

x = (1 - t) * (1 - t) * x0 + 2 * (1 - t) * t * x1 + t * t * x2; //quad.
y = (1 - t) * (1 - t) * y0 + 2 * (1 - t) * t * y1 + t * t * y2; //quad.

En éteignant l'équation là-bas.

Bien que la réponse acceptée ait raison, vous pouvez vraiment comprendre les racines et comparer ce genre de choses. Si vous avez vraiment besoin de trouver le point le plus proche sur la courbe, cela le fera.


En ce qui concerne Ben dans les commentaires. Vous ne pouvez pas courte main la formule dans les nombreux centaines de centaines de points de contrôle, comme je l'ai fait pour les formes cubes et quad. Parce que la quantité demandée par chaque nouvel ajout d'une courbe de Bézier signifie que vous construisez des pyramides pythagoriennes pour eux, et nous traitons essentiellement des chaînes de nombres encore plus et plus massives. Pour Quad, vous allez 1, 2, 1, pour Cubic, vous allez 1, 3, 3, 1. Vous finissez par construire des pyramides plus grosses et plus grandes, et finissez-la avec l'algorithme de Casteljau (j'ai écrit cela pour une solide vitesse):

/**
 * Performs deCasteljau's algorithm for a bezier curve defined by the given control points.
 *
 * A cubic for example requires four points. So it should get at least an array of 8 values
 *
 * @param controlpoints (x,y) coord list of the Bezier curve.
 * @param returnArray Array to store the solved points. (can be null)
 * @param t Amount through the curve we are looking at.
 * @return returnArray
 */
public static float[] deCasteljau(float[] controlpoints, float[] returnArray, float t) {
    int m = controlpoints.length;
    int sizeRequired = (m/2) * ((m/2) + 1);
    if (returnArray == null) returnArray = new float[sizeRequired];
    if (sizeRequired > returnArray.length) returnArray = Arrays.copyOf(controlpoints, sizeRequired); //insure capacity
    else System.arraycopy(controlpoints,0,returnArray,0,controlpoints.length);
    int index = m; //start after the control points.
    int skip = m-2; //skip if first compare is the last control point.
    for (int i = 0, s = returnArray.length - 2; i < s; i+=2) {
        if (i == skip) {
            m = m - 2;
            skip += m;
            continue;
        }
        returnArray[index++] = (t * (returnArray[i + 2] - returnArray[i])) + returnArray[i];
        returnArray[index++] = (t * (returnArray[i + 3] - returnArray[i + 1])) + returnArray[i + 1];
    }
    return returnArray;
}

Vous devez essentiellement utiliser l'algorithme directement, non seulement pour le calcul du X, Y qui se produisent sur la courbe elle-même, mais vous en avez également besoin pour effectuer un algorithme de subdivision de Bezier réel et approprié (il y en a d'autres mais c'est ce que je Recommandez), de calculer non seulement une approximation que je donne en la divisant en segments de ligne, mais des courbes réelles. Ou plutôt la coque du polygone qui est certaine de contenir la courbe.

Vous le faites en utilisant l'algorithme ci-dessus pour subdiviser les courbes à la t. Tellement t = 0,5 pour couper les courbes en deux (note 0,2 la couperait 20% 80% à travers la courbe). Ensuite, vous indexez les différents points sur le côté de la pyramide et de l'autre côté de la pyramide comme construit à partir de la base. Donc, par exemple en cubique:

   9
  7 8
 4 5 6
0 1 2 3

Vous nourririez l'algorithme 0 1 2 3 en tant que points de contrôle, puis vous indexiez les deux courbes parfaitement subdivisées à 0, 4, 7, 9 et 9, 8, 6, 3. Prenez une note spéciale pour voir que ces courbes commencent et se terminent. au même point. et l'indice final 9 qui est le point sur la courbe est utilisé comme autre point d'ancrage. Compte tenu de cela, vous pouvez parfaitement subdiviser une courbe de Bézier.

Ensuite, pour trouver le point le plus proche que vous souhaiteriez continuer à subdiviser la courbe dans différentes parties notant que c'est le cas que toute la courbe d'une courbe de Bézier est contenue dans la coque des points de contrôle. Ce qui est de dire si nous tournons les points 0, 1, 2, 3 dans un chemin fermé Connexion de 0,3 cette courbe Doit automne complètement dans cette coque de polygone. Donc, ce que nous faisons est de définir notre point Point P, puis nous continuons de subdiviser les courbes jusqu'à ce que nous sachions que le point le plus éloigné d'une courbe est plus proche que le point le plus proche d'une autre courbe. Nous comparons simplement ce point P à tous les points de contrôle et d'ancrage des courbes. Et jeter toute courbe de notre liste active dont le point le plus proche (l'ancrage ou le contrôle) est plus éloigné que le point le plus éloigné d'une autre courbe. Ensuite, nous subdivisons toutes les courbes actives et faites-la encore. Finalement, nous aurons des courbes très subdivisées, jetant environ la moitié de chaque étape (ce qui signifie qu'il devrait être O (n journal n)) jusqu'à ce que notre erreur soit essentiellement négligeable. À ce stade, nous appelons nos courbes actives le point le plus proche de ce point (il pourrait y en avoir plus d'un) et notez que l'erreur dans ce bit de courbe très subdivisé est essentiellement égale à un point. Ou simplement décider de la question en disant que le point le plus proche du point d'ancrage est le point le plus proche de notre point P. et nous connaissons l'erreur dans un degré très spécifique.

Ceci, cependant, exige que nous ayons une solution robuste et que nous effectuons un algorithme correct certainement et trouvez correctement la petite fraction de courbe qui sera certainement le point le plus proche de notre point. Et il devrait être relativement rapide encore.

9
Tatarize