Intersecting Lines (Part 2)
Recently, Lucio and I wrote a post on detecting line segment intersections. We confessed in the post that we had not been exhaustive in our treatment. We also received many interesting comments. This post introduces a 3rd method, one that several readers had mentioned; the post includes several variations to deal with various edge cases as cleanly and numerically soundly as possible, particularly ones related to having a vertical line segment, i.e., a segment with infinite slope.
Contents
Third Algorithm to Account for Vertical Segments
The previous two algorithms fail when there is a vertical line segment. Some research on the web indicates that one of the preferred solutions for this problem is to parameterize the line segments as two vectors. Several of our blog readers have already commented about this approach.
where and (2x1) indicate the coordinates of the endpoints of line A, and (2x1) indicate the coordinates of the endpoints of line B, indicates the coordinates of the intersecting point (if one exists) and and are unknown scalars.
This leads to a system of equations; testing for an intersection consists on solving for and and checking that both are in [0,1]. Note that we use linsolve rather than left matrix divide (or inv) as linsolve has better numerical properties when the system of equations is ill-conditioned.
A = @(lineA,lineB) [lineA(1,:) - lineA(2,:); lineB(2,:) - lineB(1,:)]'; pq = @(lineA,lineB) linsolve(A(lineA,lineB),(lineB(2,:) - lineA(2,:))'); isIntersect = @(lineA,lineB) all(all(pq(lineA,lineB)*[1 -1]+[0 1;0 1]>=0));
where and (x-coordinates are the first column and y-coordinates are the second column, as in our previous post).
In order to simplify our writing, we put all plot commands inside a utility function:
dbtype plotlines
1 function plotlines(line1, line2) 2 % Plots two line segments and annotates the endpoints 3 figure 4 hold all 5 h(1) = plot(line1(:,1),line1(:,2),'b^-'); 6 h(2) = plot(line2(:,1),line2(:,2),'rs-'); 7 set(h,'linewidth',2) 8 g(1) = text(line1(1,1),line1(1,2),... 9 sprintf(' (%1.20g,%1.20g) ',line1(1,1),line1(1,2)),... 10 'verticalalignment','top','color','b'); 11 g(2) = text(line1(2,1),line1(2,2),... 12 sprintf(' (%1.20g,%1.20g) ',line1(2,1),line1(2,2)),... 13 'verticalalignment','top','color','b'); 14 g(3) = text(line2(1,1),line2(1,2),... 15 sprintf(' (%1.20g,%1.20g) ',line2(1,1),line2(1,2)),... 16 'verticalalignment','bottom','color','r'); 17 g(4) = text(line2(2,1),line2(2,2),... 18 sprintf(' (%1.20g,%1.20g) ',line2(2,1),line2(2,2)),... 19 'verticalalignment','bottom','color','r'); 20 ext = cell2mat(get(g,'Extent')); 21 axis([min(ext(:,1)),max(ext(:,1)+ext(:,3)),... 22 min(ext(:,2)),max(ext(:,2)+ext(:,4))]) 23 hold off 24 end
Now we test the new algorithm with two intersecting segments with one of them having infinite slope:
lineA = [.5 1; ... .5 0]; lineB = [0.6 0.5; ... 0 0.5]; plotlines(lineA,lineB) plotlines(lineB,lineA) isIntersect(lineA,lineB) isIntersect(lineB,lineA)
ans = 1 ans = 1
Test with two non-intersecting segments:
lineA = [.5 1; ... .5 0]; lineB = [0.4 0.5; ... 0 0.5]; plotlines(lineA,lineB) plotlines(lineB,lineA) isIntersect(lineA,lineB) isIntersect(lineB,lineA)
ans = 0 ans = 0
Add Tolerance for Rounding Errors
In many applications, the computer numerical precision is not sufficient to represent an exact quantity; therefore special care needs to be taken when testing if an intersection point exists. For example observe the following inconsistency with our current algorithm:
lineA = [0 0; ... 1 3]; lineB = [1/3 1; ... 0 1]; lineC = [1/3 1; ... 1 1]; plotlines(lineA,lineB) isIntersect(lineA,lineB) plotlines(lineA,lineC) isIntersect(lineA,lineC)
ans = 0 ans = 1
One endpoint for both lineB and lineC lies on the segment lineA. This should result in an intersection in both cases. We can fix this problem by introducing a tolerance in our final testing. A conservative tolerance to use is sqrt(eps), however one may prefer to do something more robust that depends on the values involved in the computations. This has already been discussed in a previous blog post.
isIntersect = @(lineA,lineB) all(all(pq(lineA,lineB)*[1 -1]+[0 1;0 1]>=-sqrt(eps))); isIntersect(lineA,lineB) isIntersect(lineA,lineC)
ans = 1 ans = 1
Detecting Parallel Lines
One of the nice properties of the formulation we are using is that parallel lines (segments with the same slope) can be easily detected by testing if A is singular. This can be calculated by checking that det(A)==0. For example:
lineA = [0 0; ... 1 1]; lineB = [1 2; ... 2 3]; plotlines(lineA,lineB) det(A(lineA,lineB))
ans = 0
The astute reader will promptly notice that this straightforward calculation is also vulnerable to numerical precision problems, for instance:
lineC = [0 1; ... 1 1/3]; lineD = [1 2/3; ... 2 0]; plotlines(lineC,lineD) det(A(lineC,lineD))
ans = -1.1102e-016
The proper way to test for singularity is to use the function rank, as it incorporates proper tolerances to overcome rounding errors.
isParallel = @(lineA,lineB) rank(A(lineA,lineB))<2; isParallel(lineA,lineB) isParallel(lineC,lineD)
ans = 1 ans = 1
We now incorporate the test for parallel lines. For clarity, we now created a function in a file rather than an anonymous function.
dbtype isIntersect2
1 function tf = isIntersect2(lineA, lineB) 2 A = [lineA(1,:) - lineA(2,:); lineB(2,:) - lineB(1,:)]'; 3 if rank(A) < 2 4 disp('Parallel') 5 tf = false; 6 else 7 pq = linsolve(A,(lineB(2,:) - lineA(2,:))'); 8 tf = all(pq>=-sqrt(eps)) & all(pq<=1+sqrt(eps)); 9 end
isIntersect2(lineA,lineB) isIntersect2(lineC,lineD)
Parallel ans = 0 Parallel ans = 0
Testing for Collinearity
Collinearity is a special case of parallel lines; when this occurs, it the line segments might overlap. In a previous blog post, we have already talked about testing for collinearity; we'll incorporate our previous findings here. Notice that we also use rank for numerical stability. Also observe that since collinearity implies parallel segments, we only need to test any three end-points of the two line segments for collinearity. In our implementation the second endpoint of lineB is not used for testing collinearity.
lineA = [0 1; ... 1 1/3]; lineB = [2.5 -2/3; ... 1.5 0]; plotlines(lineA,lineB) dbtype isIntersect3
1 function tf = isIntersect3(lineA, lineB) 2 A = [lineA(1,:) - lineA(2,:); lineB(2,:) - lineB(1,:)]'; 3 if rank(A) < 2 4 disp('Parallel') 5 B = [lineA(1,:) - lineA(2,:); lineA(1,:) - lineB(1,:)]'; 6 if rank(B) < 2 7 disp('Collinear') 8 tf = NaN; 9 else 10 tf = false; 11 end 12 else 13 pq = linsolve(A,(lineB(2,:) - lineA(2,:))'); 14 tf = all(pq>=-sqrt(eps)) & all(pq<=1+sqrt(eps)); 15 end
isIntersect3(lineA,lineB)
Parallel Collinear ans = NaN
Once we confirm that two segments are collinear, then the segments can be projected either to the x- or y-axis. We now need to determine if the endpoints of the segments are interleaved or not, to figure out if they overlap and hence intersect. Once again, a tolerance must be incorporated into the testing.
To check if the x-coordinates of the points are interleaved, we implement the following tests; both inequalities must be false in order to discard an intersection (or segment overlapping):
dbtype isIntersect4
1 function tf = isIntersect4(lineA, lineB) 2 A = [lineA(1,:) - lineA(2,:); lineB(2,:) - lineB(1,:)]'; 3 if rank(A) < 2 4 disp('Parallel') 5 B = [lineA(1,:) - lineA(2,:); lineA(1,:) - lineB(1,:)]'; 6 if rank(B) < 2 7 disp('Collinear') 8 if all( (sort(lineA(:,1),'descend')-sort(lineB(:,1))) ... 9 .*[-1;1] <= sqrt(eps) ) 10 tf = true; 11 else 12 tf = false; 13 end 14 else 15 tf = false; 16 end 17 else 18 pq = linsolve(A,(lineB(2,:) - lineA(2,:))'); 19 tf = all(pq>=-sqrt(eps)) & all(pq<=1+sqrt(eps)); 20 end
isIntersect4(lineA,lineB)
Parallel Collinear ans = 0
lineA = [0 1; ... 1-2*eps 1/3-eps]; lineB = [1+2*eps 1/3; ... 1.5 eps]; plotlines(lineA,lineB) isIntersect4(lineA,lineB)
Parallel Collinear ans = 1
Degenerate Lines
Observe that the tests for singularity and collinearity take care properly of the degenerate lines:
lineA = [0 0;2 2]; lineB = [1 1;1 1]; lineC = [1 2;1 2]; lineD = [2 2;2 2]; plotlines(lineA,lineB) isIntersect4(lineA,lineB) plotlines(lineA,lineC) isIntersect4(lineA,lineC) plotlines(lineB,lineD) isIntersect4(lineB,lineD)
Parallel Collinear ans = 1 Parallel ans = 0 Parallel Collinear ans = 0
We are almost done, however there is one edge case that will still break our algorithm. Can you identify it (HINT: it is not associated with our conservative tolerances)? Let us know what you find here.
- Category:
- Computational Geometry,
- How To,
- Puzzles