我的相交检测算法有什么问题?

4
我知道有许多网站可以解释如何检查两条直线的相交,但是对于这样一个简单的数学任务,我觉得只是复制和粘贴代码非常无聊。更让我沮丧的是,我无法让我的代码工作。我知道带有“我的代码有什么问题?”的问题很蠢,但我不知道我的数学/代码到底哪里出了问题,尽管我的代码已经被很好地注释(除了可耻的变量命名),所以我认为应该会有人对其背后的数学感兴趣:
bool segment::checkforIntersection(QPointF a, QPointF b) { //line 1: a+bx, line 2: c+dx, note that a and c are called offset and bx and dx are called gradients in this code
QPointF bx = b-a;
double firstGradient = bx.y() / bx.x(); //gradient of line 1
//now we have to calculate the offset of line 1: we have b from a+bx. Since QPointF a is on that line, it is:
//a + b * a.x = a.y with a as free variable, which yields a = a.y - b*a.x.
//One could also use the second point b for this calculation.
double firstOffset = a.y() - firstGradient * a.x();
double secondGradient, secondOffset;
for (int i = 0; i < poscount-3; i++) { //we dont check with the last line, because that could be the same line, as the one that emited intersection checking
    QPointF c = pos[i];
    QPointF d = pos[i+1];
    QPointF dx = d-c;
    secondGradient = dx.y() / dx.x(); //same formula as above
    secondOffset = c.y() - secondGradient * c.x();
    //a+bx=c+dx <=> a-c = (d-b)x <=> (a-c)/(d-b) = x
    double x = (firstOffset - secondOffset) / (secondGradient - firstGradient);
    //we have to check, if those lines intersect with a x \in [a.x,b.x] and x \in [c.x,d.x]. If this is the case, we have a collision
    if (x >= a.x() && x <= b.x() && x >= c.x() && x <= d.x()) {
        return true;
    }
}
return false;
}

所以这个算法有4个点a、b、c、d(第1行:a--b,第2行:c--d),它们都有绝对x和y值。首先通过计算deltay/deltax来计算出线的梯度。然后通过使用点a(或相应的c)在线上的事实来计算偏移量。这样我们就将这4个点转换成了这些线的数学表示形式,即方程a + bx,其中x为0表示我们在第一个点(a / c),x为1表示我们在第二个点(b / d)。接下来,我们计算这两条线的交点(基本代数运算)。之后,我们检查交点的x值是否有效。据我所知,一切都正确。有人看到错误了吗?
这已经被经验证是不正确的。该代码不会给出任何错误的正面结果(表示存在交点时),但它会给出错误的负面结果(当实际上存在交点时,它会说不存在交点)。因此,当它说存在交点时是正确的,但如果它说不存在交点,则您不能总是相信我的算法。
同样,我在网上查过,但算法不同(含有一些方向技巧和其他内容),我只是想自己想出一个算法,如果有人能帮忙,我会非常高兴。 :)
编辑:这里是一个最小的不起作用的可重现示例,这次不是Qt,而只是C++:
#include <iostream>
#include <math.h>

using namespace std;
class Point {
private:
    double xval, yval;
public:
    // Constructor uses default arguments to allow calling with zero, one,
    // or two values.
    Point(double x = 0.0, double y = 0.0) {
        xval = x;
        yval = y;
    }

    // Extractors.
    double x() { return xval; }
    double y() { return yval; }

    Point sub(Point b)
    {
        return Point(xval - b.xval, yval - b.yval);
    }
};

bool checkforIntersection(Point a, Point b, Point c, Point d) { //line 1: a+bx, line 2: c+dx, note that a and c are called offset and bx and dx are called gradients in this code
    Point bx = b.sub(a);
    double firstGradient = bx.y() / bx.x(); //gradient of line 1
    //now we have to calculate the offset of line 1: we have b from a+bx. Since Point a is on that line, it is:
    //a + b * a.x = a.y with a as free variable, which yields a = a.y - b*a.x.
    //One could also use the second point b for this calculation.
    double firstOffset = a.y() - firstGradient * a.x();
    double secondGradient, secondOffset;
    Point dx = d.sub(c);
    secondGradient = dx.y() / dx.x(); //same formula as above
    secondOffset = c.y() - secondGradient * c.x();
    //a+bx=c+dx <=> a-c = (d-b)x <=> (a-c)/(d-b) = x
    double x = (firstOffset - secondOffset) / (secondGradient - firstGradient);
    //we have to check, if those lines intersect with a x \in [a.x,b.x] and x \in [c.x,d.x]. If this is the case, we have a collision
    if (x >= a.x() && x <= b.x() && x >= c.x() && x <= d.x()) {
        return true;
    }
    return false;
}

int main(int argc, char const *argv[]) {
    if (checkforIntersection(Point(310.374,835.171),Point(290.434,802.354), Point(333.847,807.232), Point(301.03,827.172)) == true) {
        cout << "These lines do intersect so I should be printed out\n";
    } else {
        cout << "The algorithm does not work, so instead I do get printed out\n";
    }
    return 0;
}

举个例子,我拿到了这些点 (310,835) -- (290,802) 和 (333,807) -- (301,827)。这两条线相交:

\documentclass[crop,tikz]{standalone}
\begin{document}
\begin{tikzpicture}[x=.1cm,y=.1cm]
\draw (310,835) -- (290,802);
\draw (333,807) -- (301,827);
\end{tikzpicture}
\end{document}

相交证明。然而,当运行以上C++代码时,它会显示它们不相交。


6
解决这类问题的正确工具是你的调试器。在询问Stack Overflow之前,应逐行步进代码。如果需要更多帮助,请参阅Eric Lippert的“How to debug small programs”。至少,您应该编辑您的问题,包括一个最小化、完整、可验证的示例,以重现您的问题,并附上你在调试器中做出的观察结果。 - πάντα ῥεῖ
这并不是说我没有尝试自己调试。我尝试过调试,但是在半个小时内完全没有头绪之后,我最终放弃了。也许我只是误解了一些数学知识,这就是问题所在。既然真的已经尝试过自己解决,那么询问别人有什么问题呢? - Magnus2552
你自己尝试解决问题后,询问问题有什么问题吗?但你仍然缺少 [mcve]。 - m.s.
@m-s 我确实忘记了一个算法不能正常工作的点元组的例子。对于造成的不便,我深感抱歉。我已经编辑了原始帖子以符合您的要求。现在你所要做的就是复制代码并在 g++ 或 clang++ 上运行。我希望现在你能够提供帮助 :) - Magnus2552
2个回答

1
首先,通过计算deltax/deltay来计算线的梯度。
当deltax非常接近于零时会发生什么?
看,你正在暴露自己面临病态情况 - 当涉及到计算几何时,始终要注意除法和与0.0的直接比较。
替代方案:
1.如果两条线不平行,则它们将相交。 2.如果定义向量的两条不同的线平行,则它们将平行。
你的(a,b)x(c,d)的叉积=(ax-bx)*(cy-dy)-(ay-by)*(cx-dx) - 如果这个值足够接近于零,那么在实际情况下,你的线之间就没有交点(交点距离如此遥远,无关紧要)。
现在,还有什么需要说的:
  • 在计算叉积之前,需要进行“这些线是否不同?”的测试。更重要的是,您需要处理退化情况(其中一个或两个线段由重合端点缩减为一个点 - 如a==b和/或c==d)。
  • 如果不对定义向量进行归一化,则“足够接近于零”的测试是含糊的 - 想象一下,一个长度为1光秒的向量定义了第一条线,而另一个长度为1秒差距的向量定义了另一条线(在这种情况下,您应该使用什么“接近于零”的测试?)。要归一化这两个向量,只需执行除法... (你说除法?我已经感到恐惧了) ...嗯...我是说将所得的叉积除以hypot(ax-bx, ay-by)*hypot(cx-dx,cy-dy)(您看出为什么需要提前处理退化情况了吗?)
  • 在归一化之后,又一次,对于所得的叉积,什么是一个好的“接近于零”的测试呢?好吧,我想我可以继续分析一个小时左右(例如,当与您的{a,b,c,d}多边形的范围进行比较时,交点会有多远),但是...由于两个单位向量(归一化后)的叉积是sin(angle-between-versors),您可以运用常识并说“如果角度小于1度,这足以考虑两条线平行吗?不行?1角秒呢?”

1

如果您想查看线段是否相交,则依赖于两个线段的参数化表示,解决两个参数的系统,并查看两个参数的解是否都落在[0,1]范围内。

线段[a, b]的参数化表示,分量为

{x,y}(t)= {(1-t)* ax + t * bx,(1-t)* ay + t * by} ,其中t[0,1]范围内

快速检查-在t = 0时,您会得到a,在t = 1时,您会得到b,表达式在t中是线性的,因此您就有了答案。

因此,您的(a,b)(c,d)交点问题变成:

// for some t1 and t2, the x coordinate is the same
(1-t1)*ax+t*bx=(1-t2)*cx+t2*dx; 
(1-t1)*ay+t*by=(1-t2)*cy+t2*dy; // and so is the y coordinate

t1t2中解决系统。如果t1[0,1]范围内,则交点位于ab之间,对于t2也是如此,关于cd的位置。
留给读者作为练习的是,研究上述条件对系统的影响以及应该实施哪些检查来实现鲁棒性算法:
  1. 线段退化 - 一个或两个线段的端点重合
  2. 共线线段具有非空重叠部分。特殊情况是只有一个重叠点(必要的,该点将是其中一个端点)
  3. 共线线段没有重叠部分
  4. 平行线段

我真的很喜欢你计算s,t∈[0,1]的想法。顺便提一下,在你的第二个方程中,你忘记用y替换x了。你的方程也不太容易解出s或t。所以我使用了非常相似的方程a.x+s*(b.x-a.x)=c.x+t*(d.x-c.x)(对于x使用y相同)。这个方程具有相同的属性,但我能够更快地解决s的问题。所以我只是实现了它并运行了代码,它像魔术一样工作!感谢好主意。虽然这个实现速度较慢,因为它必须进行更多的计算。但是,它正在工作,这就是我想要的。 - Magnus2552
关于 y 的感谢。关于备用形式...哦,我没有说要按照它们本来的样子解方程 - 我使用这个表达式来表示一条线的参数化表示,因为这样更容易理解和记忆 - 它是两端之间的t1-t权重的线性组合。 - Adrian Colomitchi
@Magnus2552 “虽然这种实现方式速度较慢,因为它需要进行更多的计算。” 请记住我的话:“没有比不工作的东西更慢了。没有比产生高速错误的有缺陷代码更具破坏性的了。” - Adrian Colomitchi
是的,我同意。过早地进行优化总是一种不好的编程习惯。 - Magnus2552

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接