确定点是否在线段上

32

我有由这两个点定义的线段:A(x1,y1,z1)B(x2,y2,z2)。我还有一个点p(x,y,z)。如何检查该点是否在线段上?


2
因为我需要C#的任何示例代码。 - AMH
1
是的,对我来说听起来很明显 :) - Leggy7
我尝试回复MetaMapper的帖子,但是我没有50的声望。 MetaMapper的解决方案是错误的。我个人花了很多时间调试,我不希望其他人也经历同样的事情。Andy的解决方案是正确的,只需要将其转换为C#即可。我希望这能为某人节省一些时间。 - Ruell Black
上面的问题只处理了2D情况。 - user202729
12个回答

54

找出点P到线段端点A、B的距离。如果AB = AP + PB,则P位于线段AB上。

AB = sqrt((x2-x1)*(x2-x1)+(y2-y1)*(y2-y1)+(z2-z1)*(z2-z1));
AP = sqrt((x-x1)*(x-x1)+(y-y1)*(y-y1)+(z-z1)*(z-z1));
PB = sqrt((x2-x)*(x2-x)+(y2-y)*(y2-y)+(z2-z)*(z2-z));
if(AB == AP + PB)
    return true;

1
我知道现在已经很晚了,但是这个答案比被采纳的答案要好得多。特别是当一个点在线段的起点或终点时,它仍然有效。 - Roy T.
6
优秀的回答。你可能需要考虑浮点数舍入误差。假设AB = 12.0000001,AP + PB = 12.000003,则根据你所做的事情,仍然可能要考虑事物是否“足够接近”。 - Joel B
9
它的速度如何?与除法相比,平方根运算速度较慢。 - Sushi271
2
完全不是这样,处理器有一个专门用于Math.Sqrt()的指令。它所需的时间与除法一样长。 - Hans Passant
这个问题能不能不用平方根解决呢?因为你可以将两边平方,得到AB^2 = AP^2 + PB^2,这样可以稍微提高一点性能。 - WDUK
@WDUK x²+y² ≠ (x+y)² – 大一学生的梦想 - 但是使用叉积时不需要除法。 - user202729

23

如果这个点这条线上,那么:

(x - x1) / (x2 - x1) = (y - y1) / (y2 - y1) = (z - z1) / (z2 - z1)

计算这三个值,如果它们在某种程度上相同,则您的点在直线上。

要测试点是否在段中,而不仅仅是在直线上,您可以检查

x1 < x < x2, assuming x1 < x2, or
y1 < y < y2, assuming y1 < y2, or
z1 < z < z2, assuming z1 < z2

其中一个是您正在检查的点,另外两个是线段的端点。无论您给每个点取什么名称,只要保持一致即可。 - Karl Knechtel
AMH是对的 - 对于任何点(x,y,z),只有当该点在直线上时才成立。这基本上是@Konstantin的参数化直线方程答案,但消除了参数p。您实际上并不关心p的确切值,只需它在x,y和z上具有相同的值即可。 - Rob Agar
27
如果 x1 == x2 或者 y1 == y2,你的测试会失败。 - Jeriho
2
只是为了完善这个答案,在这里你可以找到完整的数学解释。 - Leggy7
3
如果x接近x1,y接近y1或z接近z1时,由于无法修复的浮点精度问题,该解决方案会失败。不要使用这个解决方案。对于数学考试来说可以接受,但对于C#代码则完全是错误的答案。 - Robin Davies
正如@Jeriho所说,当x1 == x2y1 == y2时,这个方法是不可行的。会出现除以0的情况。 - aheze

9

首先,取AB和AP的叉积。如果它们共线,则结果为0。

此时,它仍可能在延伸过B或在A之前的较长直线上,因此我认为您只需检查pz是否在az和bz之间即可。

实际上,这是一个重复的问题,正如其中一个回答所提到的,在优美的代码中已经有相关内容。


你能给我一个数值例子吗?我对叉积后面的部分有误解。 - AMH
1
@AMH 最好直接看一下这个讨论:https://dev59.com/xnRC5IYBdhLWcg3wVvjL - Cade Roux
这是一个关于编程的内容翻译:它是二维的,但我有三维问题。 - AMH

6

如果有人需要内联版本:

public static bool PointOnLine2D (this Vector2 p, Vector2 a, Vector2 b, float t = 1E-03f)
{
    // ensure points are collinear
    var zero = (b.x - a.x) * (p.y - a.y) - (p.x - a.x) * (b.y - a.y);
    if (zero > t || zero < -t) return false;

    // check if x-coordinates are not equal
    if (a.x - b.x > t || b.x - a.x > t)
        // ensure x is between a.x & b.x (use tolerance)
        return a.x > b.x
            ? p.x + t > b.x && p.x - t < a.x
            : p.x + t > a.x && p.x - t < b.x;

    // ensure y is between a.y & b.y (use tolerance)
    return a.y > b.y
        ? p.y + t > b.y && p.y - t < a.y
        : p.y + t > a.y && p.y - t < b.y;
}

除了 epsilon (即 t) 零检查之外,共线检查可以写成 if (Vector.crossProduct(u = new Vector(a, b), new Vector(u, new Vector(a, p))) != 0) return false; - Borgboy

3

这里是一些针对二维情况的 C# 代码:

public static bool PointOnLineSegment(PointD pt1, PointD pt2, PointD pt, double epsilon = 0.001)
{
  if (pt.X - Math.Max(pt1.X, pt2.X) > epsilon || 
      Math.Min(pt1.X, pt2.X) - pt.X > epsilon || 
      pt.Y - Math.Max(pt1.Y, pt2.Y) > epsilon || 
      Math.Min(pt1.Y, pt2.Y) - pt.Y > epsilon)
    return false;

  if (Math.Abs(pt2.X - pt1.X) < epsilon)
    return Math.Abs(pt1.X - pt.X) < epsilon || Math.Abs(pt2.X - pt.X) < epsilon;
  if (Math.Abs(pt2.Y - pt1.Y) < epsilon)
    return Math.Abs(pt1.Y - pt.Y) < epsilon || Math.Abs(pt2.Y - pt.Y) < epsilon;

  double x = pt1.X + (pt.Y - pt1.Y) * (pt2.X - pt1.X) / (pt2.Y - pt1.Y);
  double y = pt1.Y + (pt.X - pt1.X) * (pt2.Y - pt1.Y) / (pt2.X - pt1.X);

  return Math.Abs(pt.X - x) < epsilon || Math.Abs(pt.Y - y) < epsilon;
}

3
您的线段最好由参数方程定义:
对于线段上的所有点,以下方程成立: x = x1 + (x2 - x1) * p y = y1 + (y2 - y1) * p z = z1 + (z2 - z1) * p 其中p是[0;1]中的数字。
因此,如果存在一个p使得您的点坐标满足这三个方程,则您的点位于该直线上。而且如果p在0和1之间,则它也在线段上。

你的意思是我可以使用p例如等于1并进行检查? - AMH
不,你只需解3个关于p的方程-如果所有3个值在合理误差范围内相等(它是浮点数-没有精确匹配),那么你的点就在那条直线上。 如果p在0和1之间,则它在线段内。 - Konstantin Pribluda
@KonstantinPribluda - 谢谢你的解释。我基于你的答案添加了一个答案。 - Andy

1
如果你使用Visual Studio,可以让DotNet为你完成繁重的工作,使用GraphicsPath也可以实现这一点。这还允许你添加公差,以防只是在线外单击。
using (Drawing2D.GraphicsPath gp = new Drawing2D.GraphicsPath())
{
    gp.AddLine(new Point(x1, y1), new Point(x2, y2));

    // Make the line as wide as needed (make this larger to allow clicking slightly outside the line) 
    using (Pen objPen = new Pen(Color.Black, 6))
    {
        gp.Widen(objPen);
    }

    if (gp.IsVisible(Mouse.x, Mouse.y))
    {
        // The line was clicked
    }
}

0

我使用这个来计算点a和点b之间的距离AB。

static void Main(string[] args)
{
        double AB = segment(0, 1, 0, 4);
        Console.WriteLine("Length of segment AB: {0}",AB);
}

static double segment (int ax,int ay, int bx, int by)
{
    Vector a = new Vector(ax,ay);
    Vector b = new Vector(bx,by);
    Vector c = (a & b);
    return Math.Sqrt(c.X + c.Y);
}

struct Vector
{
    public readonly float X;
    public readonly float Y;

    public Vector(float x, float y)
    {
        this.X = x;
        this.Y = y;
    }
    public static Vector operator &(Vector a, Vector b)  
    {
        return new Vector((b.X - a.X) * (b.X - a.X), (b.Y - a.Y) * (b.Y - a.Y));
    }
}

基于计算从A点开始沿着线段AB给定距离处的点


0

设V1为向量(B-A),V2=(p-A),对V1和V2进行归一化处理。

如果V1==(-V2),则点p在直线上,但在A之前,因此不在线段中。 如果V1==V2,则点p在直线上。获取(p-A)的长度并检查是否小于或等于(B-A)的长度,如果是,则该点在线段上,否则它超过了B。


0

这是我的代码,可以在WPF中运行

    public static class Math2DExtensions
    {
        public static bool CheckIsPointOnLineSegment(Point point, Line line, double epsilon = 0.1)
        {
            // Thank you @Rob Agar           
            // (x - x1) / (x2 - x1) = (y - y1) / (y2 - y1)
            // x1 < x < x2, assuming x1 < x2
            // y1 < y < y2, assuming y1 < y2          

            var minX = Math.Min(line.APoint.X, line.BPoint.X);
            var maxX = Math.Max(line.APoint.X, line.BPoint.X);

            var minY = Math.Min(line.APoint.Y, line.BPoint.Y);
            var maxY = Math.Max(line.APoint.Y, line.BPoint.Y);

            if (!(minX <= point.X) || !(point.X <= maxX) || !(minY <= point.Y) || !(point.Y <= maxY))
            {
                return false;
            }
            
            if (Math.Abs(line.APoint.X - line.BPoint.X) < epsilon)
            {
                return Math.Abs(line.APoint.X - point.X) < epsilon || Math.Abs(line.BPoint.X - point.X) < epsilon;
            }

            if (Math.Abs(line.APoint.Y - line.BPoint.Y) < epsilon)
            {
                return Math.Abs(line.APoint.Y - point.Y) < epsilon || Math.Abs(line.BPoint.Y - point.Y) < epsilon;
            }

            if (Math.Abs((point.X - line.APoint.X) / (line.BPoint.X - line.APoint.X) - (point.Y - line.APoint.Y) / (line.BPoint.Y - line.APoint.Y)) < epsilon)
            {
                return true;
            }
            else
            {
                return false;
            }
        }
    }

    public record Line
    {
        public Point APoint { get; init; }

        public Point BPoint { get; init; }
    }

我的代码在GitHub

感谢@Rob Agar和@MetaMapper


如果 ba 具有相同的 y,那么这个方法将无法工作,因为你会得到除以零的错误。如果它们也具有相同的 x,同样也是如此。 - WDUK
@WDUK Math.Abs(line.APoint.Y - line.BPoint.Y) < epsilon - lindexi

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