views:

4392

answers:

11

Let's say you have a two dimensional plane with 2 points (called a and b) on it represented by an x integer and a y integer for each point.

How can you determine if another point c is on the line segment defined by a and b?

I use python most, but examples in any language would be helpful.

+4  A: 

Check if the cross product of (b-a) and (c-a) is 0. Ah, wait, you say you want to know if it's on the line segment, not the same line. That's a little more work and I don't have time to answer; I'll delete this partial answer after someone fills in a proper one.

Update: Two more notes: first, Brian Hayes's chapter in Beautiful Code covers the design space for a collinearity-test function -- useful background. Second, [points that have since been answered].

Update 2: I like vincent's approach best now (and I'm embarrassed I didn't see it). But the comparison could still be done in a cleaner way, I think, like this:

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

Update 3: Brian Hayes pointed out that you only need to range-check one coordinate, once you know the points are collinear. (Previously my code had "and" instead of "if a.x != b.x".)

Darius Bacon
+10  A: 

Check if the cross product of (b-a) and (c-a) is 0, as tells Darius Bacon, tells you if the points a, b and c are aligned.

But, as you want to know if c is between a and b, you also have to check that the dot product of (b-a) and (c-a) is positive and is less than the square of the distance between a and b.

In non-optimized pseudocode:

def isBetween(a, b, c):
    crossproduct = (c.y - a.y) * (b.x - a.x) - (c.x - a.x) * (b.y - a.y)
    if abs(crossproduct) > epsilon : return False   # (or != 0 if using integers)

    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
ckarmann
`-epsilon < crossproduct < epsilon and min(a.x, b.x) <= c.x <= max(a.x, b.x) and min(a.y, b.y) <= c.y <= max(a.y, b.y)` is sufficient, isn't it?
J.F. Sebastian
Yes, silly me. That's the answer of Sridhar Iyer, with a crossproduct instead of slopes. As I said, there is several possible answers. :)
ckarmann
The absolute value of the crossproduct is twice the area of the triangle formed by the three points (with the sign indicating the side the third point) so IMHO you should use an epsilon that is proportional to the distance between the two endpoints.
bart
This works in real space, not integer space as the poster asked.
cletus
Can you tell us why wouldn't it work with integers ? I don't see the problem, provided that the epsilon check is replaced by "!= 0".
ckarmann
Because either a) the approximated line between the two points is important and those vectors don't match in real terms or b) you are only interested in an exact match in which case I'm sure you could reduce it to a greatest common factor type calculation.
cletus
+2  A: 

The scalar product between (c-a) and (b-a) must be equal to the product of their lengths (this means that the vectors (c-a) and (b-a) are aligned and with the same direction). Moreover, the length of (c-a) must be less than or equal to that of (b-a). Pseudocode:

# 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
Federico Ramponi
Shouldn't the last condition be more like: ABS(product - lengthca * lengthba) < epsilon?
Jonathan Leffler
Shouldn't you be comparing squared lengths instead? Square roots are to be avoided. Also, if this is unavoidable due to overflow, you can use math.hypot instead of math.sqrt (with the appropriate change of arguments).
Darius Bacon
I wonder about that epsilon, too. Can you explain it? Of course, if we must deal with floats, we must be careful about comparisons, but it's not clear to me why an epsilon makes this particular comparison more accurate.
Darius Bacon
I concur. There is several good answer to this question, and this one is fine. But this code needs to be amended to not use sqrt, and the last comparison fixed.
ckarmann
@Jonathan: indeed the code is more familiar and elegant using abs. Thanks.
Federico Ramponi
As for the epsilon stuff: That is only a matter of not using == to compare floating point numbers. You should never use x==y with floats. You should use abs(x-y) <= epsilon (small) instead. In the revised code I use > epsilon to express !=.
Federico Ramponi
Note that I don't like any solution which involves cross products. The dot product and/or the distance work also in 4-dimensional or n-dimensional spaces, the cross product (as an operation on two vectors) does not.
Federico Ramponi
Now it doesn't use sqrt anymore.
Federico Ramponi
Sorry, my epsilon question was stupid; I'd managed to confuse your equality and less-than comparisons. That's a good question about generalizing to higher dimensions; my hunch is you could answer it with the exterior product, but I can't be arsed to figure it out now.
Darius Bacon
+4  A: 

Here's another approach:

  • Lets assume the two points be A (x1,y1) and B (x2,y2)
  • The equation of the line passing through those points is (x-x1)/(y-y1)=(x2-x1)/(y2-y1) .. (just making equating the slopes)

Point C (x3,y3) will lie between A & B if:

  • x3,y3 satisfies the above equation.
  • x3 lies between x1 & x2 and y3 lies between y1 & y2 (trivial check)
Sridhar Iyer
That doesn't take rounding errors (inexactness of coordinates) into account.
bart
This is the right idea, I think, but short on detail (how do we check that equation in practice?) and a bit buggy: the last y3 ought to be y2.
Darius Bacon
@Darius: fixed that typo
Harley
+2  A: 

Using a more geometric approach, calculate the following distances:

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)

and test whether ac+bc equals ab:

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

That's because there are three possibilities:

  • The 3 points form a triangle => ac+bc > ab
  • They are collinear and c is outside the ab segment => ac+bc > ab
  • They are collinear and c is inside the ab segment => ac+bc = ab
efotinis
As Jonathan Leffler mentions in another comment, this has numerical issues that other approaches like the cross-product avoid. The chapter I link to in my answer explains.
Darius Bacon
+3  A: 

Here's how I'd do it:

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)
Jules
This is an elegant solution.
Paul D. Eden
The only problem with this is the numerical stability - taking differences of numbers and so on is apt to lose precision.
Jonathan Leffler
`-epsilon < (distance(a, c) + distance(c, b) - distance(a, b)) < epsilon`
J.F. Sebastian
+1  A: 

Here's how I did it at school. I forgot why it is not a good idea.

EDIT:

@Darius Bacon: cites a "Beautiful Code" book which contains an explanation why the belowed code is not a good idea.

#!/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()
J.F. Sebastian
+3  A: 

The length of the segment is not important, thus using a square root is not required and should be avoided since we could lose some precision.

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)

Some random example of usage :

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)
vincent
If c.x or c.y are float then the first `==` in `is_between()` could fail (btw it is a crossproduct in disguise).
J.F. Sebastian
add to `is_between()`: `a, b = self.a, self.b`
J.F. Sebastian
oops, the example worked because a and b are in the scope !
vincent
It looks like that will return true if all three points are the same (which is all right, imho) but false if exactly two of the points are the same -- a pretty inconsistent way to define betweenness. I posted an alternative in my answer.
Darius Bacon
fixed that by another cmp trick, but this code starts to smell ;-)
vincent
+2  A: 

Ok, lots of mentions of linear algebra (cross product of vectors) and this works in a real (ie continuous or floating point) space but the question specifically stated that the two points were expressed as integers and thus a cross product is not the correct solution although it can give an approximate solution.

The correct solution is to use Bresenham's Line Algorithm between the two points and to see if the third point is one of the points on the line. If the points are sufficiently distant that calculating the algorithm is non-performant (and it'd have to be really large for that to be the case) I'm sure you could dig around and find optimisations.

cletus
Bresenham's Line Algorithm solves different problem.
J.F. Sebastian
It solves how to draw a line through a two-dimensional integer space between two arbitrary points and its mathematically correct. If the third point is one of the points on that line then it is, by definition, between those two points.
cletus
No, Bresenham's Line Algorithm solves how to create an *approximation* of a line segment in a two-dimensional integer space. I don't see from the original poster's message that it was a question about rasterization.
ckarmann
"Let's say you have a two dimensional plane with 2 points (called a and b) on it represented by an x INTEGER and a y INTEGER for each point." (emphasis added by me).
cletus
A: 

How thick is the line?

Dipstick
A: 

how about just ensuring that the slope is the same and the point is between the others?

given points (x1, y1) and (x2, y2) ( with x2 > x1) and candidate point (a,b)

if (b-y1) / (a-x1) = (y2-y2) / (x2-x1) And x1 < a < x2

Then (a,b) must be on line between (x1,y1) and (x2, y2)

Charles Bretana