Как вы можете определить точку между двумя другими точками на отрезке?

Допустим, у вас есть двумерная плоскость с 2 точками (называемыми a и b), представленными целым числом x и y для каждой точки.

Как вы можете определить, находится ли другая точка c на отрезке, определяемом a и b?

Я чаще использую python, но примеры на любом языке были бы полезны.

21 ответ

Решение

Убедитесь, что перекрестное произведение (ba) и (ca) равно 0, как говорит Дарий Бэкон, сообщает ли выровнены точки a, b и c.

Но, как вы хотите знать, находится ли c между a и b, вы также должны проверить, что скалярное произведение (ba) и (ca) положительное и меньше квадрата расстояния между a и b.

В неоптимизированном псевдокоде:

def isBetween(a, b, c):
    crossproduct = (c.y - a.y) * (b.x - a.x) - (c.x - a.x) * (b.y - a.y)

    # compare versus epsilon for floating point values, or != 0 if using integers
    if abs(crossproduct) > epsilon:
        return False

    dotproduct = (c.x - a.x) * (b.x - a.x) + (c.y - a.y)*(b.y - a.y)
    if dotproduct < 0:
        return False

    squaredlengthba = (b.x - a.x)*(b.x - a.x) + (b.y - a.y)*(b.y - a.y)
    if dotproduct > squaredlengthba:
        return False

    return True

Вот как я это сделаю:

def distance(a,b):
    return sqrt((a.x - b.x)**2 + (a.y - b.y)**2)

def is_between(a,c,b):
    return distance(a,c) + distance(c,b) == distance(a,b)

Проверьте, если перекрестное произведение b-a а также c-a является0: это означает, что все точки коллинеарны. Если они есть, проверьте, если cкоординаты между aи b"S. Используйте координаты x или y, если a а также b разделены на этой оси (или они одинаковы на обеих).

def is_on(a, b, c):
    "Return true iff point c intersects the line segment from a to b."
    # (or the degenerate case that all 3 points are coincident)
    return (collinear(a, b, c)
            and (within(a.x, c.x, b.x) if a.x != b.x else 
                 within(a.y, c.y, b.y)))

def collinear(a, b, c):
    "Return true iff a, b, and c all lie on the same line."
    return (b.x - a.x) * (c.y - a.y) == (c.x - a.x) * (b.y - a.y)

def within(p, q, r):
    "Return true iff q is between p and r (inclusive)."
    return p <= q <= r or r <= q <= p

Этот ответ был беспорядок трех обновлений. Полезная информация от них: глава Брайана Хейса в " Красивом коде" охватывает пространство разработки для функции проверки коллинеарности - полезный фон. Ответ Винсента помог улучшить этот. И именно Хейс предложил проверить только одну из координат x или y; Первоначально код имел and на месте if a.x != b.x else,

Вот еще один подход:

  • Предположим, что две точки - это A (x1,y1) и B (x2,y2)
  • Уравнение линии, проходящей через эти точки, имеет вид (x-x1)/(y-y1)=(x2-x1)/(y2-y1) .. (просто приравнивая наклоны)

Точка C (x3,y3) будет лежать между A и B, если:

  • x3, y3 удовлетворяет приведенному выше уравнению.
  • x3 лежит между x1 & x2 и y3 лежит между y1 & y2 (тривиальная проверка)

Длина сегмента не важна, поэтому использование квадратного корня не требуется, и его следует избегать, поскольку мы можем потерять некоторую точность.

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

class Segment:
    def __init__(self, a, b):
        self.a = a
        self.b = b

    def is_between(self, c):
        # Check if slope of a to c is the same as a to b ;
        # that is, when moving from a.x to c.x, c.y must be proportionally
        # increased than it takes to get from a.x to b.x .

        # Then, c.x must be between a.x and b.x, and c.y must be between a.y and b.y.
        # => c is after a and before b, or the opposite
        # that is, the absolute value of cmp(a, b) + cmp(b, c) is either 0 ( 1 + -1 )
        #    or 1 ( c == a or c == b)

        a, b = self.a, self.b             

        return ((b.x - a.x) * (c.y - a.y) == (c.x - a.x) * (b.y - a.y) and 
                abs(cmp(a.x, c.x) + cmp(b.x, c.x)) <= 1 and
                abs(cmp(a.y, c.y) + cmp(b.y, c.y)) <= 1)

Несколько случайных примеров использования:

a = Point(0,0)
b = Point(50,100)
c = Point(25,50)
d = Point(0,8)

print Segment(a,b).is_between(c)
print Segment(a,b).is_between(d)

Вы можете использовать продукт клина и точки:

def dot(v,w): return v.x*w.x + v.y*w.y
def wedge(v,w): return v.x*w.y - v.y*w.x

def is_between(a,b,c):
   v = a - b
   w = b - c
   return wedge(v,w) == 0 and dot(v,w) > 0

Вот другой способ сделать это, с кодом на C++. Учитывая две точки, l1 и l2, тривиально выразить отрезок между ними как

l1 + A(l2 - l1)

где 0 <= A <= 1. Это называется векторным представлением линии, если вам интересно больше, чем просто использовать ее для этой задачи. Мы можем разделить компоненты x и y этого, давая:

x = l1.x + A(l2.x - l1.x)
y = l1.y + A(l2.y - l1.y)

Возьмите точку (x, y) и подставьте ее компоненты x и y в эти два выражения, чтобы решить для A. Точка находится на прямой, если решения для A в обоих выражениях равны и 0 <= A <= 1. Поскольку решение для A требует деления, есть особые случаи, которые требуют обработки, чтобы остановить деление на ноль, когда отрезок прямой является горизонтальным или вертикальным. Окончательное решение заключается в следующем:

// Vec2 is a simple x/y struct - it could very well be named Point for this use

bool isBetween(double a, double b, double c) {
    // return if c is between a and b
    double larger = (a >= b) ? a : b;
    double smaller = (a != larger) ? a : b;

    return c <= larger && c >= smaller;
}

bool pointOnLine(Vec2<double> p, Vec2<double> l1, Vec2<double> l2) {
    if(l2.x - l1.x == 0) return isBetween(l1.y, l2.y, p.y); // vertical line
    if(l2.y - l1.y == 0) return isBetween(l1.x, l2.x, p.x); // horizontal line

    double Ax = (p.x - l1.x) / (l2.x - l1.x);
    double Ay = (p.y - l1.y) / (l2.y - l1.y);

    // We want Ax == Ay, so check if the difference is very small (floating
    // point comparison is fun!)

    return fabs(Ax - Ay) < 0.000001 && Ax >= 0.0 && Ax <= 1.0;
}

Используя более геометрический подход, рассчитайте следующие расстояния:

ab = sqrt((a.x-b.x)**2 + (a.y-b.y)**2)
ac = sqrt((a.x-c.x)**2 + (a.y-c.y)**2)
bc = sqrt((b.x-c.x)**2 + (b.y-c.y)**2)

и проверьте, равняется ли ac + bc ab:

is_on_segment = abs(ac + bc - ab) < EPSILON

Это потому, что есть три варианта:

  • 3 точки образуют треугольник => ac + bc> ab
  • Они коллинеарны и c находится вне сегмента ab => ac + bc> ab
  • Они коллинеарны и c находится внутри отрезка ab => ac + bc = ab

Хорошо, много упоминаний о линейной алгебре (перекрестное произведение векторов), и это работает в реальном (т. Е. В непрерывном или с плавающей запятой) пространстве, но в вопросе конкретно указано, что две точки были выражены как целые числа, и, таким образом, перекрестное произведение не является правильным решение, хотя оно может дать приблизительное решение.

Правильное решение состоит в том, чтобы использовать Алгоритм Линии Брезенхема между двумя точками и посмотреть, является ли третья точка одной из точек на линии. Если точки достаточно далеки, чтобы вычисление алгоритма было неэффективным (и оно должно быть действительно большим, чтобы это имело место), я уверен, что вы могли бы покопаться и найти оптимизацию.

Мне нужно было это для JavaScript для использования на холсте html5 для определения, был ли курсор пользователя над определенной линией или рядом с ней. Поэтому я изменил ответ Дариуса Бэкона на coffeescript:

is_on = (a,b,c) ->
    # "Return true if point c intersects the line segment from a to b."
    # (or the degenerate case that all 3 points are coincident)
    return (collinear(a,b,c) and withincheck(a,b,c))

withincheck = (a,b,c) ->
    if a[0] != b[0]
        within(a[0],c[0],b[0]) 
    else 
        within(a[1],c[1],b[1])

collinear = (a,b,c) ->
    # "Return true if a, b, and c all lie on the same line."
    ((b[0]-a[0])*(c[1]-a[1]) < (c[0]-a[0])*(b[1]-a[1]) + 1000) and ((b[0]-a[0])*(c[1]-a[1]) > (c[0]-a[0])*(b[1]-a[1]) - 1000)

within = (p,q,r) ->
    # "Return true if q is between p and r (inclusive)."
    p <= q <= r or r <= q <= p

Скалярное произведение между (ca) и (ba) должно быть равно произведению их длин (это означает, что векторы (ca) и (ba) выровнены и имеют одинаковое направление). Кроме того, длина (ca) должна быть меньше или равна длине (ba). псевдокод:

# epsilon = small constant

def isBetween(a, b, c):
    lengthca2  = (c.x - a.x)*(c.x - a.x) + (c.y - a.y)*(c.y - a.y)
    lengthba2  = (b.x - a.x)*(b.x - a.x) + (b.y - a.y)*(b.y - a.y)
    if lengthca2 > lengthba2: return False
    dotproduct = (c.x - a.x)*(b.x - a.x) + (c.y - a.y)*(b.y - a.y)
    if dotproduct < 0.0: return False
    if abs(dotproduct*dotproduct - lengthca2*lengthba2) > epsilon: return False 
    return True

Вы также можете воспользоваться очень удобным scikit-spatial библиотека .

Например, вы можете создать объект, определяемый двумя точками aа также b:

      >>> point_a = [0, 0]
>>> point_b = [1, 0]

>>> line = Line.from_points(point_a, point_b)

то вы можете использовать side_pointметод Lineкласс, чтобы проверить, лежит ли точка на или нет.

      >>> line.side_point([0.5, 0])
0

Если выход 0, затем укажите cлежит на line.

Вот код Java, который работал для меня:

boolean liesOnSegment(Coordinate a, Coordinate b, Coordinate  c) {

    double dotProduct = (c.x - a.x) * (c.x - b.x) + (c.y - a.y) * (c.y - b.y);
    if (dotProduct < 0) return true;
    return false;
}

C# От http://www.faqs.org/faqs/graphics/algorithms-faq/-> Тема 1.02: Как мне найти расстояние от точки до прямой?

Boolean Contains(PointF from, PointF to, PointF pt, double epsilon)
        {

            double segmentLengthSqr = (to.X - from.X) * (to.X - from.X) + (to.Y - from.Y) * (to.Y - from.Y);
            double r = ((pt.X - from.X) * (to.X - from.X) + (pt.Y - from.Y) * (to.Y - from.Y)) / segmentLengthSqr;
            if(r<0 || r>1) return false;
            double sl = ((from.Y - pt.Y) * (to.X - from.X) - (from.X - pt.X) * (to.Y - from.Y)) / System.Math.Sqrt(segmentLengthSqr);
            return -epsilon <= sl && sl <= epsilon;
        }

Ответ в C# с использованием класса Vector2D

public static bool IsOnSegment(this Segment2D @this, Point2D c, double tolerance)
{
     var distanceSquared = tolerance*tolerance;
     // Start of segment to test point vector
     var v = new Vector2D( @this.P0, c ).To3D();
     // Segment vector
     var s = new Vector2D( @this.P0, @this.P1 ).To3D();
     // Dot product of s
     var ss = s*s;
     // k is the scalar we multiply s by to get the projection of c onto s
     // where we assume s is an infinte line
     var k = v*s/ss;
     // Convert our tolerance to the units of the scalar quanity k
     var kd = tolerance / Math.Sqrt( ss );
     // Check that the projection is within the bounds
     if (k <= -kd || k >= (1+kd))
     {
        return false;
     }
     // Find the projection point
     var p = k*s;
     // Find the vector between test point and it's projection
     var vp = (v - p);
     // Check the distance is within tolerance.
     return vp * vp < distanceSquared;
}

Обратите внимание, что

s * s

является точечным произведением вектора сегмента через перегрузку оператора в C#

Ключ в том, чтобы воспользоваться преимуществом проекции точки на бесконечную линию и наблюдать, что скалярное количество проекции тривиально сообщает нам, находится ли проекция на отрезке или нет. Мы можем отрегулировать границы скалярного количества, чтобы использовать нечеткий допуск.

Если проекция находится в пределах границ, мы просто проверяем, находится ли расстояние от точки до проекции в пределах границ.

Преимущество над подходом к нескольким продуктам заключается в том, что допуск имеет значимое значение.

Любая точка на отрезке (a, b) (где a и b - векторы) может быть выражена как линейная комбинация двух векторов a и b:

Другими словами, если c лежит на отрезке (a, b):

c = ma + (1 - m)b, where 0 <= m <= 1

Решив за м, получим:

m = (c.x - b.x)/(a.x - b.x) = (c.y - b.y)/(a.y - b.y)

Итак, наш тест становится (в Python):

def is_on(a, b, c):
    """Is c on the line segment ab?"""

    def _is_zero( val ):
        return -epsilon < val < epsilon

    x1 = a.x - b.x
    x2 = c.x - b.x
    y1 = a.y - b.y
    y2 = c.y - b.y

    if _is_zero(x1) and _is_zero(y1):
        # a and b are the same point:
        # so check that c is the same as a and b
        return _is_zero(x2) and _is_zero(y2)

    if _is_zero(x1):
        # a and b are on same vertical line
        m2 = y2 * 1.0 / y1
        return _is_zero(x2) and 0 <= m2 <= 1
    elif _is_zero(y1):
        # a and b are on same horizontal line
        m1 = x2 * 1.0 / x1
        return _is_zero(y2) and 0 <= m1 <= 1
    else:
        m1 = x2 * 1.0 / x1
        if m1 < 0 or m1 > 1:
            return False
        m2 = y2 * 1.0 / y1
        return _is_zero(m2 - m1)

Как насчет того, чтобы убедиться, что наклон одинаков и точка находится между остальными?

заданные точки (x1, y1) и (x2, y2) (с x2 > x1) и точка-кандидат (a, b)

если (b-y1) / (a-x1) = (y2-y2) / (x2-x1) и x1

Тогда (a, b) должно быть на линии между (x1, y1) и (x2, y2)

Вот как я это сделал в школе. Я забыл, почему это не очень хорошая идея.

РЕДАКТИРОВАТЬ:

@Darius Bacon: цитирует книгу "Красивый код", которая содержит объяснение, почему приведенный ниже код не является хорошей идеей.

#!/usr/bin/env python
from __future__ import division

epsilon = 1e-6

class Point:
    def __init__(self, x, y):
        self.x, self.y = x, y

class LineSegment:
    """
    >>> ls = LineSegment(Point(0,0), Point(2,4))
    >>> Point(1, 2) in ls
    True
    >>> Point(.5, 1) in ls
    True
    >>> Point(.5, 1.1) in ls
    False
    >>> Point(-1, -2) in ls
    False
    >>> Point(.1, 0.20000001) in ls
    True
    >>> Point(.1, 0.2001) in ls
    False
    >>> ls = LineSegment(Point(1, 1), Point(3, 5))
    >>> Point(2, 3) in ls
    True
    >>> Point(1.5, 2) in ls
    True
    >>> Point(0, -1) in ls
    False
    >>> ls = LineSegment(Point(1, 2), Point(1, 10))
    >>> Point(1, 6) in ls
    True
    >>> Point(1, 1) in ls
    False
    >>> Point(2, 6) in ls 
    False
    >>> ls = LineSegment(Point(-1, 10), Point(5, 10))
    >>> Point(3, 10) in ls
    True
    >>> Point(6, 10) in ls
    False
    >>> Point(5, 10) in ls
    True
    >>> Point(3, 11) in ls
    False
    """
    def __init__(self, a, b):
        if a.x > b.x:
            a, b = b, a
        (self.x0, self.y0, self.x1, self.y1) = (a.x, a.y, b.x, b.y)
        self.slope = (self.y1 - self.y0) / (self.x1 - self.x0) if self.x1 != self.x0 else None

    def __contains__(self, c):
        return (self.x0 <= c.x <= self.x1 and
                min(self.y0, self.y1) <= c.y <= max(self.y0, self.y1) and
                (not self.slope or -epsilon < (c.y - self.y(c.x)) < epsilon))

    def y(self, x):        
        return self.slope * (x - self.x0) + self.y0

if __name__ == '__main__':
    import  doctest
    doctest.testmod()

С # версия ответа Жюля:

public static double CalcDistanceBetween2Points(double x1, double y1, double x2, double y2)
{
    return Math.Sqrt(Math.Pow (x1-x2, 2) + Math.Pow (y1-y2, 2));
}

public static bool PointLinesOnLine (double x, double y, double x1, double y1, double x2, double y2, double allowedDistanceDifference)
{
    double dist1 = CalcDistanceBetween2Points(x, y, x1, y1);
    double dist2 = CalcDistanceBetween2Points(x, y, x2, y2);
    double dist3 = CalcDistanceBetween2Points(x1, y1, x2, y2);
    return Math.Abs(dist3 - (dist1 + dist2)) <= allowedDistanceDifference;
}

Вот мое решение с C# в Unity.

private bool _isPointOnLine( Vector2 ptLineStart, Vector2 ptLineEnd, Vector2 ptPoint )
{
    bool bRes = false;
    if((Mathf.Approximately(ptPoint.x, ptLineStart.x) || Mathf.Approximately(ptPoint.x, ptLineEnd.x)))
    {
        if(ptPoint.y > ptLineStart.y && ptPoint.y < ptLineEnd.y)
        {
            bRes = true;
        }
    }
    else if((Mathf.Approximately(ptPoint.y, ptLineStart.y) || Mathf.Approximately(ptPoint.y, ptLineEnd.y)))
    {
        if(ptPoint.x > ptLineStart.x && ptPoint.x < ptLineEnd.x)
        {
            bRes = true;
        }
    }
    return bRes;
}

Вы можете сделать это, решив линейное уравнение для этого линейного сегмента с координатами точки, вы узнаете, находится ли эта точка на линии, а затем проверив границы сегмента, чтобы узнать, находится ли он внутри или снаружи. Вы можете применить некоторый порог, потому что он находится где-то в пространстве, скорее всего, определяется значением с плавающей запятой, и вы не должны попадать в точное значение. Пример на php

function getLineDefinition($p1=array(0,0), $p2=array(0,0)){
    
    $k = ($p1[1]-$p2[1])/($p1[0]-$p2[0]);
    $q = $p1[1]-$k*$p1[0];
    
    return array($k, $q);
    
}

function isPointOnLineSegment($line=array(array(0,0),array(0,0)), $pt=array(0,0)){
    
    // GET THE LINE DEFINITION y = k.x + q AS array(k, q) 
    $def = getLineDefinition($line[0], $line[1]);
    
    // use the line definition to find y for the x of your point
    $y = $def[0]*$pt[0]+$def[1];

    $yMin = min($line[0][1], $line[1][1]);
    $yMax = max($line[0][1], $line[1][1]);

    // exclude y values that are outside this segments bounds
    if($y>$yMax || $y<$yMin) return false;
    
    // calculate the difference of your points y value from the reference value calculated from lines definition 
    // in ideal cases this would equal 0 but we are dealing with floating point values so we need some threshold value not to lose results
    // this is up to you to fine tune
    $diff = abs($pt[1]-$y);
    
    $thr = 0.000001;
    
    return $diff<=$thr;
    
}
Другие вопросы по тегам