4

So I've written the following code based on examples of the Gift Wrapping Algorithm for finding the Convex Hull of a group of points:

std::vector<sf::Vector2f> convexHull(const std::vector<sf::Vector2f>& _shape)
{
    std::vector<sf::Vector2f> returnValue;    
    returnValue.push_back(leftmostPoint(_shape));
    for (std::vector<sf::Vector2f>::const_iterator it = _shape.begin(), end = _shape.end(); it != end; ++it)
    {
        if (elementIncludedInVector(*it, returnValue)) continue;
        bool allPointWereToTheLeft = true;
        for (std::vector<sf::Vector2f>::const_iterator it1 = _shape.begin(); it1 != end; ++it1)
        {
            if (*it1 == *it || elementIncludedInVector(*it1, returnValue)) continue;
            if (pointPositionRelativeToLine(returnValue.back(), *it, *it1) > 0.0f)
            {
                allPointWereToTheLeft = false;
                break;
            }
        }
        if (allPointWereToTheLeft)
        {
            returnValue.push_back(*it);
            it = _shape.begin();
        }
    }
    return returnValue;
}

Here is my function for determining on which side of a line a third point is:

float pointPositionRelativeToLine(const sf::Vector2f& A, const sf::Vector2f& B, const sf::Vector2f& C)
{
    return (B.x - A.x)*(C.y - A.y) - (B.y - A.y)*(C.x - A.x);
}

Returning a negative number means the point is on one side, positive on the other, 0 means the three points are collinear. And now, the question: How can the above code be modified so that it works correctly even when there are collinear points in _shape?

Stefan Dimitrov
  • 302
  • 1
  • 14

3 Answers3

7

If some points are collinear, you have to choose the farthest point from them (with max distance to current point)

MBo
  • 77,366
  • 5
  • 53
  • 86
  • The farthest point isn't necessarily such that all other points are to the left of the newly formed line, causing an error. – Stefan Dimitrov Aug 31 '15 at 23:12
  • The farthest from collinear ones! – MBo Sep 01 '15 at 02:43
  • This rule is quite correct. Actually you implement a lexicographic comparison: first on the angle (via the signed area), then on the distance in case of ties. –  Sep 02 '15 at 07:59
  • That rule is true, but it does require some extra caution. When there is multiple "left most point"s, I think you should either start from top most one or the bottom most one of them. Because if you start from the middle one, you'll come back to either top most for ccw or bottom most for cw, and depending on your loop conditions you may do unnecessary iterations or fall in an infinite loop if you're checking whether you have reached the first element. I'm not sure if this implementation is affected by that or not, but since it has almost 0 cost to do, I think it's better to always do it. – TheGrayed May 24 '20 at 13:51
1

You can base your reasoning on an "exclude" relation between two points (around a common center), with the meaning that A excludes B if the relative placement of A and B proves that B cannot be on the convex hull.

On the figure, the green points exclude the blue one, while the red don't. Among two aligned points, the farthest from the center excludes the other. The exclusion locus is an open half-plane and a half-line.

enter image description here

Note that "excludes" is transitive and defines a total ordering.

0

This is a bit trickier to do correctly than the code you demonstrate. I'll only focus on the stability of your predicate, not how you deal with collinear points. The predicate is where you do the geometric computations - pointPositionRelativeToLine.

Your code is nicely designed in that you only do geometric computations in the predicate. That's a must to make it robust. Alas, your predicate should not return a float, but one result from a small set: either LEFT, RIGHT or COLLINEAR:

enum RelPos { LEFT, RIGHT, COLLINEAR };

RelPos pointPositionRelativeToLine(const sf::Vector2f& A, const sf::Vector2f& B, const sf::Vector2f& C)
{
    auto result = (B.x - A.x)*(C.y - A.y) - (B.y - A.y)*(C.x - A.x);
    if (result < 0.0) return LEFT;
    else if (result > 0.0) return RIGHT;
    return COLLINEAR;
}

You can then figure out how to guarantee that given any three points, the right answer is returned for any permutation of them. That's necessary, otherwise, your algorithm is not guaranteed to work.

There are two general approaches:

  1. Use a proper data type that guarantees exact results when used in your predicate.

  2. Accept that with the inexact data types that you're using, there are some inputs for which the result cannot be computed. Specifically, you can have the predicate offer a fourth value, INDETERMINATE, and return it in such cases.

The 2nd approach is easy to implement by invoking the original predicate for all permutations of input:

enum RelPos { LEFT, RIGHT, COLLINEAR, INDETERMINATE };
typedef sf::Vector2f Point_2;

RelPos ppImpl(const Point_2 & A, const Point_2 & B, const Point_2 & C)
{
    auto result = (B.x - A.x)*(C.y - A.y) - (B.y - A.y)*(C.x - A.x);
    if (result < 0.0) return LEFT;
    else if (result > 0.0) return RIGHT;
    return COLLINEAR;
}

bool inverse(RelPos a, RelPos b) {
  return a == LEFT && b == RIGHT || a == RIGHT && b == LEFT;
}

bool equal(RelPos a, RelPos b, RelPos c, RelPos d, RelPos e, RelPos f) {
  return a==b && b==c && c==d && d==e && e==f;
}

RelPos pointPositionRelativeToLine(const Point_2 & A, const Point_2 & B, const Point_2 & C) {
  auto abc = ppImpl(A, B, C);
  auto bac = ppImpl(B, A, C);
  auto acb = ppImpl(A, C, B);
  auto cab = ppImpl(C, A, B);
  auto bca = ppImpl(B, C, A);
  auto cba = ppImpl(C, B, A);
  if (abc == COLLINEAR) return equal(abc, bac, acb, cab, bca, cba) ?
    COLLINEAR : INDETERMINATE;
  if (!inverse(abc, bac) || !inverse(acb, cab) || !inverse(bca, cba))
    return INDETERMINATE;
  if (abc != bca || abc != cab)
    return INDETERMINATE;
  return abc;
}

There may be a mistake in the logic above, let's hope I got it right. But that's the general approach here. At least the above tests on a given data set must pass for the algorithm to work on the data set. But I don't recall offhand if that's a sufficient condition.

Of course, the algorithm must terminate when an INDETERMINATE result is obtained from the predicate:

const auto errVal = std::vector<sf::Vector2f>();
...
auto rel = pointPositionRelativeToLine(returnValue.back(), *it, *it1);
if (rel == INDETERMINATE) return errVal;
if (rel == RIGHT) {
  allPointWereToTheLeft = false;
  break;
}
Kuba hasn't forgotten Monica
  • 95,931
  • 16
  • 151
  • 313