37

对于个人项目,我需要确定两条三次贝塞尔曲线是否相交。我不需要知道在哪里:我只需要知道他们是否这样做。但是,我需要快速完成。

我一直在搜寻这个地方,并找到了一些资源。大多数情况下,这里的这个问题有一个有希望的答案。

因此,在我弄清楚什么是Sylvester 矩阵、什么是行列式、什么是结果以及它为什么有用之后,我想我知道了解决方案是如何工作的。然而,现实要求有所不同,而且效果并不好。


到处乱混

我用我的图形计算器绘制了两条相交的贝塞尔样条曲线(我们称之为 B 0和 B 1)。它们的坐标如下(P 0 , P 1 , P 2 , P 3):

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

结果如下,B 0是“水平”曲线,B 1是另一条曲线:

两条相交的三次贝塞尔曲线

按照上述问题的最高投票答案的指示,我将 B 0减去B 1。它给我留下了两个方程(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,因为曲线相交。但是,为我的程序提供上面制作的 Sylvester 矩阵,它是 -2916。

这是我的错误还是他们的错误?找出两条三次贝塞尔曲线是否相交的正确方法是什么?

4

8 回答 8

22

贝塞尔曲线的交点是由(非常酷的)渐近线矢量图形语言完成的:在intersect() 此处查找。

虽然他们没有解释他们在那里实际使用的算法,只是说它来自 p。“The Metafont Book”的第 137 页,看来它的关键是 Bezier 曲线的两个重要属性(尽管我现在找不到该页面,但在该站点的其他地方进行了解释):

  • 贝塞尔曲线始终包含在由其 4 个控制点定义的边界框内
  • 一条贝塞尔曲线总是可以在任意t值处细分为 2 条子贝塞尔曲线

使用这两个属性和相交多边形的算法,您可以递归到任意精度:

bezInt(B 1 , B 2 ):

  1. bbox(B 1 ) 是否与 bbox(B 2 ) 相交?
    • 否:返回假。
    • 是:继续。
  2. 面积(bbox(B 1 )) + 面积(bbox(B 2 )) < 阈值吗?
    • 是:返回真。
    • 否:继续。
  3. t = 0.5时将 B 1拆分为 B 1a和 B 1b
  4. t = 0.5时将 B 2拆分为 B 2a和 B 2b
  5. 返回 bezInt(B 1a , B 2a ) || bezInt(B 1a , B 2b ) || bezInt(B 1b , B 2a ) || bezInt(B 1b , B 2b )。

如果曲线不相交,这将很快——这是通常的情况吗?

[编辑]看起来将贝塞尔曲线一分为二的算法称为de Casteljau's algorithm

于 2010-10-28T09:00:29.487 回答
9

如果您要为生产代码执行此操作,我建议使用 Bezier 裁剪算法。这个免费的在线 CAGD 文本 (pdf) 的第 7.7 节对此进行了很好的解释,适用于任何程度的贝塞尔曲线,并且快速且稳健。

虽然从数学角度来看,使用标准寻根器或矩阵可能更直接,但贝塞尔裁剪相对容易实现和调试,并且实际上浮点错误更少。这是因为每当它创建新数字时,它都会进行加权平均(凸组合),因此没有机会根据噪声输入进行推断。

于 2012-12-03T11:03:33.670 回答
3

这是我的错误还是他们的错误?

您是否基于此答案所附的第四条评论对决定因素的解释?如果是这样,我相信这就是错误所在。在此处复制评论:

如果行列式为零,则 X 和 Y 中的根在 * 的 t 值完全相同,因此两条曲线有交点。(虽然 t 可能不在 0..1 区间内)。

我认为这部分没有任何问题,但作者继续说:

如果行列式是 <> 零,则可以确定曲线不会在任何地方相交。

我不认为这是正确的。两条曲线完全有可能在 t 值不同的位置相交,在这种情况下,即使矩阵具有非零行列式,也会存在相交。我相信这就是你的情况。

于 2010-10-28T06:15:21.590 回答
2

我不知道它有多快,但如果你有两条曲线 C1(t) 和 C2(k),如果 C1(t) == C2(k),它们相交。因此,对于两个变量(t,k),您有两个方程(每个 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

我绝不是这类事情的专家,但我关注了一个很好的博客,其中讨论了很多关于曲线的内容。他有两篇关于您的问题的好文章的链接(第二个链接有一个交互式演示和一些源代码)。其他人可能对问题有更好的了解,但我希望这会有所帮助!

http://cagd.cs.byu.edu/~557/text/ch7.pdf存档副本

于 2010-10-28T02:30:50.063 回答
0

是的,我知道,这个帖子被接受并关闭了很长时间,但是......

首先,我要感谢zneak的启发。你的努力可以找到正确的方法。

其次,我对接受的解决方案不太满意。这种用于我最喜欢的免费软件 IPE 中,它的 bugzilla 有很多关于交叉问题的低准确性和可靠性的抱怨,我就是其中之一。

允许以zneak提出的方式解决问题的缺失技巧:将一条曲线缩短一个因子k >0就足够了,那么 Sylvester 矩阵的行列式将等于零。很明显,如果一条缩短的曲线会相交,那么原来的曲线也会相交。现在,任务变为为k因子寻找合适的值。这导致了求解 9 次单变量多项式的问题。该多项式的实数和正根负责潜在的交点。(这不足为奇,两条三次贝塞尔曲线最多可以相交 9 次。)执行最终选择以仅找到那些k因子,它们提供 0<=对于两条曲线, t <=1。

现在是千里马代码,起点是zneak提供的 8 个点:

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 位于第二条曲线(或其延长线)上。可以尝试交换曲线。
  • 极其罕见的情况,当两条曲线都属于一个 K 系列时(例如,它们的无限延伸是相同的)。
  • 留意(sqr(c3x)+sqr(c3y))=0(sqr(d3x)+sqr(3y))=0的情况,这里的二次曲线伪装成三次贝塞尔曲线。

有人可能会问,为什么缩短只执行一次。这已经足够了,因为逆反定律是偶然发现,但这是另一个故事。

于 2016-11-05T10:14:33.957 回答
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 的最小值添加第二个标志。

我们保留一个活动边列表。

我们遍历 y 排序的段列表,当我们遇到前导段时,我们将其添加到活动列表中。当我们点击 small-y 标记值时,我们会从活动列表中删除该段。

然后我们可以简单地迭代整个段集,相当于一条扫描线,在我们简单地迭代列表时单调增加我们的 y。我们遍历排序列表中的值,这通常会删除一个段并添加一个新段(或者对于拆分和合并节点,添加两个段或删除两个段)。从而保持相关段的活动列表。

当我们将一个新的活动段添加到活动段列表中时,我们运行快速失败相交检查,仅针对该段和当前活动段。

因此,当我们遍历曲线的采样段时,我们始终准确地知道哪些线段是相关的。我们知道这些段在 y 坐标中共享重叠。我们可以快速失败任何与其 x 坐标不重叠的新段。在极少数情况下它们在 x 坐标中重叠,然后我们检查这些段是否相交。

这可能会将线交叉检查的数量减少到基本上交叉点的数量。

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

checkAgainstActive() 将简单地检查此段和活动列表中的任何段是否与它们的 x 坐标重叠,如果它们重叠,则对它们运行线相交检查,并采取适当的措施。


另请注意,这适用于任何数量的曲线、任何类型的曲线、任何曲线顺序、任何混合。如果我们遍历整个段列表,它将找到每个交集。它将找到贝塞尔曲线与圆相交的每个点或十几个二次贝塞尔曲线彼此(或它们自身)之间的每个交点,并且都在同一瞬间。

对于细分算法,经常引用的第 7 章文档注释:

“一旦一对曲线被细分到足以使它们都可以通过线段近似到公差范围内”

我们可以直接跳过中间人。我们可以足够快地做到这一点,以便简单地将线段与曲线中可容忍的误差量进行比较。最后,这就是典型的答案。


其次,请注意,此处碰撞检测的大部分速度增加,即根据其最高 y 排序的分段有序列表添加到活动列表中,以及从活动列表中删除最低 y,同样可以完成直接用于贝塞尔曲线的外壳多边形。我们的线段只是一个 2 阶多边形,但我们可以同样简单地处理任意数量的点,并以我们正在处理的任何曲线顺序检查所有控制点的边界框。因此,我们将有一个活动船体多边形点的列表,而不是一个活动段列表。我们可以简单地使用 De Casteljau 的算法来分割曲线,从而将这些曲线采样为细分曲线而不是线段。所以不是 2 分,我们会得到 4 或 7 或诸如此类的东西,

在 max y 处添加相关的点组,在 min y 处删除它,并仅比较活动列表。因此,我们可以快速更好地实现贝塞尔细分算法。通过简单地找到边界框重叠,然后细分那些重叠的曲线,并删除那些没有重叠的曲线。同样,我们可以做任意数量的曲线,甚至是从前一次迭代中的曲线细分的曲线。就像我们的分段近似一样,可以非常快速地快速解决数百条不同曲线(甚至不同阶数)之间的每个相交位置。只需检查所有曲线以查看边界框是否重叠,如果重叠,我们将它们细分,直到我们的曲线足够小或者我们用完它们。

于 2016-07-11T10:20:08.923 回答