37

個人的なプロジェクトの場合、2つの立方ベジェ曲線が交差するかどうかを確認する必要があります。私はどこにいるのかを知る必要はありません:私は彼らがそうするかどうかを知る必要があります。しかし、私はそれを速くする必要があります。

私はその場所を掃除していて、いくつかのリソースを見つけました。ほとんどの場合、有望な答えがあったこの質問がここにあります。

したがって、シルベスター行列とは何か、行列式とは何か、結果とは何か、なぜそれが役立つのかを理解した後、ソリューションがどのように機能するかを理解したと思いました。しかし、現実は違うように頼み、それはあまりうまく機能しません。


いじり

グラフ電卓を使用して、交差する2つのベジェスプライン( B0とB1と呼びます)を描画しました。それらの座標は次のとおりです(P 0、P 1、P 2、P 3):

(1, 1) (2, 4) (3, 4) (4, 3)
(3, 5) (3, 6) (0, 1) (3, 1)

結果は次のようになります。B0は「水平」曲線であり、B1はもう1つの曲線です。

交差する2つの立方ベジェ曲線

前述の質問の上位投票の回答からの指示に従って、 B0をB1に減算しました。私の計算機によると、2つの方程式(X軸とY軸)が残りました。

x = 9t^3 - 9t^2 - 3t + 2
y = 9t^3 - 9t^2 - 6t + 4

シルベスター行列

そして、そこから次のシルベスター行列を作成しました。

9  -9  -3   2   0   0
0   9  -9  -3   2   0
0   0   9  -9  -3   2
9  -9  -6   4   0   0
0   9  -9  -6   4   0
0   0   9  -9  -6   4

その後、余因子展開を使用して行列式を計算するC++関数を作成しました。

template<int size>
float determinant(float* matrix)
{
    float total = 0;
    float sign = 1;
    float temporaryMatrix[(size - 1) * (size - 1)];
    for (int i = 0; i < size; i++)
    {
        if (matrix[i] != 0)
        {
            for (int j = 1; j < size; j++)
            {
                float* targetOffset = temporaryMatrix + (j - 1) * (size - 1);
                float* sourceOffset = matrix + j * size;
                int firstCopySize = i * sizeof *matrix;
                int secondCopySize = (size - i - 1) * sizeof *matrix;
                memcpy(targetOffset, sourceOffset, firstCopySize);
                memcpy(targetOffset + i, sourceOffset + i + 1, secondCopySize);
            }
            float subdeterminant = determinant<size - 1>(temporaryMatrix);
            total += matrix[i] * subdeterminant * sign;
        }
        sign *= -1;
    }
    return total;
}

template<>
float determinant<1>(float* matrix)
{
    return matrix[0];
}

比較的小さなマトリックス(2x2、3x3、4x4)でかなりうまく機能するように見えるので、6x6マトリックスでも機能すると思います。ただし、徹底的なテストは行っていないので、壊れている可能性があります。


問題

他の質問の答えを正しく理解した場合、曲線が交差するため、行列式は0になります。ただし、上記で作成したシルベスター行列をプログラムにフィードすると、-2916になります。

それは私の側の間違いですか、それとも彼らの側の間違いですか?2つの立方ベジェ曲線が交差するかどうかを確認する正しい方法は何ですか?

4

8 に答える 8

22

ベジェ曲線の交差は、(非常にクールな)Asymptoteベクトルグラフィックス言語によって行われます。ここを探してintersect() ください

彼らは実際にそこで使用しているアルゴリズムを説明していませんが、それがpからのものであると言うことを除いて。「TheMetafontBook」の137では、その鍵はベジェ曲線の2つの重要な特性であるように見えます(現在、ページは見つかりませんが、そのサイトの他の場所で説明されています)。

  • ベジェ曲線は、常に4つのコントロールポイントによって定義されるバウンディングボックス内に含まれます
  • ベジェ曲線は、常に任意のt値で2つのサブベジェ曲線に分割できます。

これらの2つのプロパティと、ポリゴンを交差させるためのアルゴリズムを使用すると、任意の精度で再帰できます。

bezInt(B 1、B 2):

  1. bbox(B 1 )はbbox(B 2)と交差しますか?
    • いいえ:falseを返します。
    • はい:続行します。
  2. area(bbox(B 1))+ area(bbox(B 2))<しきい値ですか?
    • はい:trueを返します。
    • いいえ:続行します。
  3. t =0.5B1をB1aB1bに分割します
  4. t =0.5でB2B2aB2bに分割します
  5. bezInt(B 1a、B 2a)を返す|| bezInt(B 1a、B 2b)|| bezInt(B 1b、B 2a)|| bezInt(B 1b、B 2b)。

曲線が交差しない場合、これは高速になります-それは通常の場合ですか?

[編集]ベジェ曲線を2つに分割するアルゴリズムは、deCasteljauのアルゴリズムと呼ばれているようです。

于 2010-10-28T09:00:29.487 に答える
9

プロダクションコードでこれを行う場合は、ベジェクリッピングアルゴリズムをお勧めします。この無料のオンラインCAGDテキスト(pdf)のセクション7.7で詳しく説明されており、あらゆる程度のベジェ曲線で機能し、高速で堅牢です。

標準のルートファインダーまたは行列を使用することは、数学的な観点からはより簡単かもしれませんが、ベジェクリッピングは実装とデバッグが比較的簡単であり、実際には浮動小数点エラーが少なくなります。これは、新しい数値を作成するときは常に加重平均(凸結合)を実行するため、ノイズの多い入力に基づいて外挿する可能性がないためです。

于 2012-12-03T11:03:33.670 に答える
3

それは私の側の間違いですか、それとも彼らの側の間違いですか?

この回答に添付された4番目のコメントに基づいて行列式を解釈していますか?もしそうなら、私はそれが間違いがあると信じています。ここにコメントを再現する:

行列式がゼロの場合、XとYのルートは*まったく同じtの値であるため、2つの曲線の交点があります。(ただし、tは間隔0..1にない場合があります)。

この部分に問題はありませんが、作者は続けて次のように述べています。

行列式が<>ゼロの場合、曲線がどこでも交差していないことを確認できます。

私はそれが正しいとは思いません。t値が異なる場所で2つの曲線が交差する可能性は完全にあります。その場合、行列式にゼロ以外の行列式が含まれていても、交差が発生します。これがあなたの場合に起こっていることだと思います。

于 2010-10-28T06:15:21.590 に答える
2

どれくらい速くなるかはわかりませんが、2つの曲線C1(t)とC2(k)がある場合、C1(t)== C2(k)の場合は交差します。したがって、2つの変数(t、k)に対して2つの方程式(xごととyごと)があります。十分な精度で数値解法を使用してシステムを解くことができます。t、kパラメータを見つけたら、[0、1]にパラメータがあるかどうかを確認する必要があります。もしそうなら、それらは[0、1]で交差します。

于 2010-10-28T07:42:06.830 に答える
2

これは難しい問題です。2つのベジェ曲線のそれぞれをたとえば5〜10個の離散線分に分割し、線線交叉を行います。

ここに画像の説明を入力してください

foreach SampledLineSegment line1 in Bezier1
    foreach SampledLineSegment line2 in Bezier2
        if( line1 intersects line2 )
            then Bezier1 intersects Bezier2
于 2013-04-03T14:41:47.960 に答える
1

私はこの種のことの専門家ではありませんが、曲線について多くのことを話している素敵なブログをフォローしています。彼はあなたの問題について話している2つの素晴らしい記事へのリンクを持っています(2番目のリンクにはインタラクティブなデモンストレーションといくつかのソースコードがあります)。他の人は問題についてはるかに良い洞察を持っているかもしれませんが、これが役立つことを願っています!

http://cagd.cs.byu.edu/~557/text/ch7.pdfアーカイブされたコピー

于 2010-10-28T02:30:50.063 に答える
0

最も簡単で最も速い答えは、それらを非常に小さな線に分割し、実際に交差する場合は、曲線が交差する点を見つけることです。

public static void towardsCubic(double[] xy, double x0, double y0, double x1, double y1, double x2, double y2, double x3, double y3, double t) {
    double x, y;
    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;
    xy[0] = x;
    xy[1] = y;
}

public static void towardsQuad(double[] xy, double x0, double y0, double x1, double y1, double x2, double y2, double t) {
    double x, y;
    x = (1 - t) * (1 - t) * x0 + 2 * (1 - t) * t * x1 + t * t * x2;
    y = (1 - t) * (1 - t) * y0 + 2 * (1 - t) * t * y1 + t * t * y2;
    xy[0] = x;
    xy[1] = y;
}

明らかに、ブルートフォースの答えは悪いです。bo{4}の答えを参照してください。実際には、非常に役立つ線形ジオメトリと衝突検出がたくさんあります。


カーブに必要なスライスの数を選択します。100のようなものは素晴らしいはずです。

すべてのセグメントを取得し、それらが持つyの最大値に基づいて並べ替えます。次に、そのセグメントのyの最小値のリストに2番目のフラグを追加します。

アクティブなエッジのリストを保持します。

yでソートされたセグメントのリストを繰り返し処理し、先頭のセグメントに遭遇すると、それをアクティブリストに追加します。small-yフラグの値に達すると、そのセグメントがアクティブリストから削除されます。

次に、スキャンラインに相当するセグメントのセット全体を単純に反復し、リストを単純に反復するときにyを単調に増加させます。ソートされたリストの値を反復処理します。これにより、通常、1つのセグメントが削除され、新しいセグメントが追加されます(または、ノードの分割とマージの場合は、2つのセグメントを追加するか、2つのセグメントを削除します)。これにより、関連するセグメントのアクティブなリストを保持します。

アクティブなセグメントのリストに新しいアクティブなセグメントを追加するときに、そのセグメントと現在アクティブなセグメントに対してのみ、高速フェイル交差チェックを実行します。

したがって、曲線のサンプリングされたセグメントを反復処理するときに、どの線分が関連しているかを常に正確に把握できます。これらのセグメントがy座標でオーバーラップしていることはわかっています。x座標に関して重複しない新しいセグメントはすぐに失敗する可能性があります。まれに、x座標でオーバーラップする場合は、これらのセグメントが交差するかどうかを確認します。

これにより、線交差チェックの数が基本的に交差の数に減る可能性があります。

foreach(segment in sortedSegmentList) {
    if (segment.isLeading()) {
        checkAgainstActives(segment);
        actives.add(segment);
    }
    else actives.remove(segment)
}

checkAgainstActive()は、このセグメントとアクティブリスト内のセグメントがx座標と重複しているかどうかを確認し、重複している場合は、それらに対して線交差チェックを実行し、適切なアクションを実行します。


また、これは、任意の数の曲線、任意のタイプの曲線、任意の順序の曲線、任意の混合で機能することに注意してください。そして、セグメントのリスト全体を反復処理すると、すべての交差点が見つかります。ベジェが円と交差するすべての点、または1ダースの2次ベジェ曲線が互いに(またはそれ自体)持つすべての交差を、すべて同じ一瞬で見つけます。

細分化アルゴリズムについて、よく引用される第7章のドキュメントノート:

「曲線のペアが十分に細分化されたら、それぞれを線分で許容範囲内に近似できます。」

文字通り仲介者をスキップすることができます。これを十分に速く行うことができるので、曲線からの許容できる量の誤差で線分を単純に比較できます。結局、それが典型的な答えです。


次に、ここでの衝突検出の速度向上の大部分、つまり、アクティブリストに追加するための最高のyと、アクティブリストから削除するための最低のyに基づいてソートされたセグメントの順序付きリストも同様に実行できることに注意してください。ベジェ曲線の船体ポリゴンに直接適用します。私たちの線分は単純に2次の多角形ですが、同じように簡単に任意の数の点を実行でき、処理している曲線の順序に関係なく、すべての制御点の境界ボックスをチェックできます。したがって、アクティブなセグメントのリストではなく、アクティブなハルポリゴンポイントのリストがあります。De Casteljauのアルゴリズムを使用して曲線を分割するだけで、線分ではなく細分化された曲線としてサンプリングできます。つまり、2ポイントではなく、4または7かそれ以外のポイントになります。

最大yで関連するポイントのグループを追加し、最小yでそれを削除し、アクティブリストのみを比較します。したがって、ベジェ細分割アルゴリズムを迅速かつ適切に実装できます。バウンディングボックスのオーバーラップを見つけ、オーバーラップしたカーブを細分化し、オーバーラップしなかったカーブを削除するだけです。繰り返しになりますが、前の反復で曲線から細分化された曲線であっても、任意の数の曲線を実行できます。そして、私たちのセグメント近似のように、何百もの異なる曲線(異なる次数であっても)間のすべての交差位置を非常に迅速に解決します。すべての曲線をチェックして、境界ボックスが重なっているかどうかを確認します。重なっている場合は、曲線が十分に小さくなるか、なくなるまで、それらを細分化します。

于 2016-07-11T10:20:08.923 に答える
0

はい、私は知っています、このスレッドは長い間受け入れられて閉じられていますが...

まず、インスピレーションを与えてくれたzneakに感謝します。あなたの努力は正しい方法を見つけることを可能にします。

第二に、私は受け入れられた解決策にあまり満足していませんでした。この種は私のお気に入りのフリーウェアIPEで使用されており、そのバグジラは交差点の問題についての精度と信頼性の低さについて多くの不満を持っています。

zneakによって提案された方法で問題を解決することを可能にする欠落しているトリック:曲線の1つを係数k > 0だけ短縮するだけで十分であり、シルベスター行列の行列式はゼロに等しくなります。短縮された曲線が交差する場合、元の曲線も交差することは明らかです。ここで、タスクはkファクターの適切な値の検索に変更されます。これにより、9度の単変量多項式を解くという問題が発生しました。この多項式の実数と正の根は、潜在的な交点の原因です。(これは驚くべきことではありません。2つの3次ベジェ曲線は最大9回交差する可能性があります。)最終的な選択は、0<=を提供するk個の因子のみを見つけるために実行されます。両方の曲線でt <=1。

ここで、開始点がzneakによって提供される8つの点のセットであるMaximaコード:

p0x:1; p0y:1;
p1x:2; p1y:4;
p2x:3; p2y:4;
p3x:4; p3y:3;

q0x:3; q0y:5;
q1x:3; q1y:6;
q2x:0; q2y:1;
q3x:3; q3y:1;

c0x:p0x;
c0y:p0y;
c1x:3*(p1x-p0x);
c1y:3*(p1y-p0y);
c2x:3*(p2x+p0x)-6*p1x;
c2y:3*(p2y+p0y)-6*p1y;
c3x:3*(p1x-p2x)+p3x-p0x;
c3y:3*(p1y-p2y)+p3y-p0y;

d0x:q0x;
d0y:q0y;
d1x:3*(q1x-q0x);
d1y:3*(q1y-q0y);
d2x:3*(q2x+q0x)-6*q1x;
d2y:3*(q2y+q0y)-6*q1y;
d3x:3*(q1x-q2x)+q3x-q0x;
d3y:3*(q1y-q2y)+q3y-q0y;

x:c0x-d0x + (c1x-d1x*k)*t+ (c2x-d2x*k^2)*t^2+ (c3x-d3x*k^3)*t^3;
y:c0y-d0y + (c1y-d1y*k)*t+ (c2y-d2y*k^2)*t^2+ (c3y-d3y*k^3)*t^3;

z:resultant(x,y,t);

この時点で、マキシマは答えました:

(%o35)−2*(1004*k^9−5049*k^8+5940*k^7−1689*k^6+10584*k^5−8235*k^4−2307*k^3+1026*k^2+108*k+76)

Maximaにこの方程式を解かせましょう。

rr: float( realroots(z,1e-20))  

答えは次のとおりです。

(%o36) [k=−0.40256438624399,k=0.43261490325108,k=0.84718739982868,k=2.643321910825066,k=2.71772491293651]

ここで、 kの正しい値を選択するコード:

for item in rr do ( 
  evk:ev(k,item),
  if evk>0 then (  
    /*print("k=",evk),*/ 
    xx:ev(x,item),  rx:float( realroots(xx,1e-20)),/*print("x(t)=",xx,"   roots: ",rx),*/
    yy:ev(y,item),  ry:float( realroots(yy,1e-20)),/*print("y(t)=",yy,"   roots: ",ry),*/
    for it1 in rx do (  t1:ev(t,it1),
    for it2 in ry do (  t2:ev(t,it2),
       dt:abs(t1-t2),
       if dt<1e-10 then (
         /*print("Common root=",t1,"  delta t=",dt),*/
         if (t1>0) and (t1<=1) then ( t2:t1*evk,
         if (t2>0) and (t2<=1) then (                           
                 x1:c0x + c1x*t1+ c2x*t1^2+ c3x*t1^3,
                 y1:c0y + c1y*t1+ c2y*t1^2+ c3y*t1^3,
                 print("Intersection point:     x=",x1, "      y=",y1)
)))))/*,disp ("-----")*/
));

マキシマの答え:

"Intersection point:     x="1.693201254437358"      y="2.62375005067273
(%o37) done

しかし、蜂蜜だけではありません。次の場合、提示されたメソッドは使用できません。

  • P0 = Q0、またはより一般的には、P0が2番目の曲線(またはその延長)上にある場合。曲線を入れ替えてみることができます。
  • 非常にまれなケースで、両方の曲線が1つのKファミリに属している場合(たとえば、それらの無限の拡張が同じである場合)。
  • ( sqr (c3x)+ sqr(c3y))= 0または(sqr(d3x)+ sqr(3y))= 0の場合に注意してください。ここでは、2次方程式は3次ベジェ曲線のふりをしています。

なぜ短縮が一度だけ行われるのかと疑問に思うかもしれません。アンパッサンで発見された逆逆法のおかげで十分ですが、これは別の話です。

于 2016-11-05T10:14:33.957 に答える