web-dev-qa-db-ja.com

3次ベジェ曲線の最も近い点は?

平面内の任意の点Pに最も近い3次ベジェ曲線に沿った点B(t)を見つけるにはどうすればよいですか?

37
Adrian Lopez

何度も検索した後、ベジエ曲線上の特定の点に最も近い点を見つける方法について説明した論文を見つけました。

ベジェ曲線の点射影の代数アルゴリズムの改善 、Xiao-Diao Chen、Yin Zhou、Zhenyu Shu、Hua Su、およびJean-Claude Paulによる。

さらに、論文自体の説明はあまり明確ではないため、アルゴリズムの最初の部分を理解するのに役立つSturmシーケンスの Wikipedia および MathWorld's の説明を見つけました。

18
Adrian Lopez

私は、任意の次数のベジェ曲線に対してこれを推定する簡単で汚いコードをいくつか書いています。 (注:これは疑似ブルートフォースであり、閉じた形式のソリューションではありません)

デモ: 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]);
}

上記のコードは vmathライブラリ を使用してベクトル間(2D、3D、または4D)を効率的に許容しますが、lerp()bézierPoint()呼び出しを置き換えるのは簡単です。 ]あなた自身のコードで。

アルゴリズムの調整

closestPoint()関数は2つのフェーズで機能します。

  • 最初に、曲線に沿ったすべてのポイントを計算します(tパラメーターの等間隔の値)。 tのどの値がポイントまでの距離が最も短いかを記録します。
  • 次に、localMinimum()関数を使用して最短距離の周囲の領域をハントし、バイナリ検索を使用してtと真の最短距離を生成するポイントを見つけます。

closestPoint()scansの値は、最初のパスで使用するサンプルの数を決定します。スキャンが少ないほど高速ですが、真の最小点を見逃す可能性が高くなります。

localMinimum()関数に渡されるε制限は、最適な値を探し続ける時間を制御します。 1e-2の値は、曲線を約100ポイントに量子化します。したがって、closestPoint()から返されたポイントが線に沿って飛び出すのがわかります。小数点の精度を追加するたびに(1e-31e-4、…)、bézierPoint()の呼び出しが約6〜8回追加されます。

14
Phrogz

あなたの公差に応じて。ブルートフォースとエラーの受け入れ。このアルゴリズムは、まれなケースで間違っている場合があります。しかし、それらの大部分では、正しい答えに非常に近いポイントが見つかり、スライスを高く設定するほど結果が向上します。一定の間隔でカーブに沿って各ポイントを試行し、見つかった最良のポイントを返します。

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;
}

最も近いポイントを見つけて、そのポイントの周りを再帰するだけで、はるかに良くて速くなります。

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);
}

どちらの場合も、クワッドを簡単に実行できます。

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.

そこで方程式を切り替えることによって。

受け入れられた答えは正しいです、そしてあなたは本当にルーツを理解してそのものを比較することができます。本当に曲線上の最も近い点を見つける必要があるだけなら、これでうまくいきます。


コメントのベンに関して。立方体や四角形で行ったように、何百もの制御点範囲で数式を簡略化することはできません。ベジェカーブを新しく追加するたびに要求される量は、それらのピタゴラスピラミッドを構築することを意味するため、基本的には、ますます多くの数の文字列を扱います。クワッドの場合は1、2、1、キュービックの場合は1、3、3、1になります。ピラミッドをどんどん構築していき、それをCasteljauのアルゴリズムで分解してしまいます(これは確かな速度で書いたものです)。

/**
 * 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;
}

基本的に、アルゴリズムを直接使用する必要があります。曲線自体で発生するx、yの計算だけでなく、実際の適切なベジェサブディビジョンアルゴリズムを実行するためにも必要です(他にもありますが、それは私がしたいことです)推奨)、それを線分に分割することで与える近似だけでなく、実際の曲線の近似を計算します。または、カーブを確実に含むポリゴンハルです。

これを行うには、上記のアルゴリズムを使用して、所定のtで曲線を細分割します。したがって、T = 0.5でカーブを半分にカットします(0.2ではカーブ全体で20%80%カットされることに注意してください)。次に、ベースから構築されたピラミッドの側面とピラミッドの反対側のさまざまなポイントにインデックスを付けます。したがって、たとえば立方体で:

   9
  7 8
 4 5 6
0 1 2 3

アルゴリズム0 1 2 3を制御点としてフィードし、完全に細分割された2つの曲線を0、4、7、9および9、8、6、3にインデックス付けします。これらの曲線の開始と終了を確認してください。同じ時点で。曲線上の点である最後のインデックス9は、他の新しいアンカーポイントとして使用されます。これにより、ベジェカーブを完全に細分割できます。

次に、最も近いポイントを見つけるために、曲線をさまざまな部分に細分割し続けます。ベジェカーブのカーブ全体がコントロールポイントのハル内に含まれている場合に注意してください。つまり、ポイント0、1、2、3を0、3を接続する閉じたパスに変換すると、そのカーブmustは完全にそのポリゴンハル内に収まります。つまり、与えられた点Pを定義し、1つの曲線の最も遠い点が別の曲線の最も近い点よりも近いことがわかるまで、曲線を細分割し続けます。この点Pを単純に、曲線のすべての制御点およびアンカー点と比較します。そして、最も近い点(アンカーまたはコントロール)が別の曲線の最も遠い点よりも離れているアクティブリストから曲線を破棄します。次に、すべてのアクティブなカーブを細分割し、これをもう一度行います。最終的には、エラーが基本的に無視できるようになるまで、各ステップの約半分(つまり、O(n log n)でなければならない)を破棄する非常に細分割された曲線になります。この時点で、アクティブカーブをそのポイントに最も近いポイント(複数ある場合があります)と呼びます。カーブの高度に分割されたビットのエラーは基本的にポイントに等しいことに注意してください。または、2つのアンカーポイントのどちらが最も近いかが、ポイントPに最も近いポイントであると言って問題を決定します。エラーは非常に特定の程度でわかります。

ただし、これには、実際に堅牢なソリューションがあり、確実に正しいアルゴリズムを実行し、確実に最も近いポイントとなる曲線のごく一部を正しく見つけることが必要です。そして、それはまだ比較的速いはずです。

9
Tatarize