web-dev-qa-db-ja.com

2つの立方ベジェ曲線が交差するかどうかの確認

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

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

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


いじり

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

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

結果は次のとおりです、B 「水平」曲線であり、B1 別のもの:

Two cubic Bézier curves that intersect

前述の質問の上位投票の回答からの指示に従って、Bを減算しました Bへ1。私の計算機によると、次の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つの立方ベジェ曲線が交差するかどうかを確認する正しい方法は何ですか?

32
zneak

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

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

  • ベジェ曲線は、常に4つの制御点によって定義される境界ボックス内に含まれます
  • ベジェ曲線は、常に任意のt値で2つのサブベジェ曲線に分割できます。

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

bezInt(B1、B2):

  1. Bbox(B1)bbox(Bと交差する2)?
    • いいえ:falseを返します。
    • はい:続行します。
  2. Area(bbox(B1))+ area(bbox(B2))<しきい値?
    • はい:trueを返します。
    • いいえ:続行します。
  3. スプリットB1 Bに1a およびB1b att= 0.5
  4. スプリットB2 Bに2a およびB2b att= 0.5
  5. BezInt(Bを返す1a、B2a)|| bezInt(B1a、B2b)|| bezInt(B1b、B2a)|| bezInt(B1b、B2b)。

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

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

17
j_random_hacker

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

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

8
tfinniga

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

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

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

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

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

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

3
user200783

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

2
Andrew

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

http://cagd.cs.byu.edu/~557/text/ch7.pdf

2
GWW

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

enter image description here

foreach SampledLineSegment line1 in Bezier1
    foreach SampledLineSegment line2 in Bezier2
        if( line1 intersects line2 )
            then Bezier1 intersects Bezier2
2
bobobobo

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

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

0
Tatarize

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

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

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

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

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

0
Biły