Проблемы точности с кодом пересечения сегмента сегмента - PullRequest
1 голос
/ 21 декабря 2011

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

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

double fRand(double fMin, double fMax)
{
    double f = (double)rand() / RAND_MAX; // On my machine RAND_MAX = 2147483647
    return fMin + f * (fMax - fMin);
}
// ... testing code below:  
srand(1);
for(int j=3;j<5000;j+=5) {
    std::vector<E_Point> geometry;
    for(int i=0;i<j;i++) {
        double radius = fRand(0.6,1.0);
        double angle = fRand(0,2*3.1415926535);
        E_Point pt(cos(angle),sin(angle));
        pt *= radius;
        geometry.push_back(pt); // sending in a pile of shit
    }
    // run algorithm on this geometry

Это был обзор того, насколько это актуально и как я попал туда, где я сейчас.Есть намного больше деталей, которые я опускаю.

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

bool intersect(const E_Point& a0, const E_Point& a1,
               const E_Point& b0, const E_Point& b1,
               E_Point& intersectionPoint) {

    if (a0 == b0 || a0 == b1 || a1 == b0 || a1 == b1) return false;
    double x1 = a0.x; double y1 = a0.y;
    double x2 = a1.x; double y2 = a1.y;
    double x3 = b0.x; double y3 = b0.y;
    double x4 = b1.x; double y4 = b1.y;

    //AABB early exit
    if (b2Max(x1,x2) < b2Min(x3,x4) || b2Max(x3,x4) < b2Min(x1,x2) ) return false;
    if (b2Max(y1,y2) < b2Min(y3,y4) || b2Max(y3,y4) < b2Min(y1,y2) ) return false;

    float ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3));
    float ub = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3));
    float denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1);
    // check against epsilon (lowest normalized double value)
    if (fabs(denom) < DBL_EPSILON) {
        //Lines are too close to parallel to call
        return false;
    }
    ua /= denom;
    ub /= denom;

    if ((0 < ua) && (ua < 1) && (0 < ub) && (ub < 1)) {
        intersectionPoint.x = (x1 + ua * (x2 - x1));
        intersectionPoint.y = (y1 + ua * (y2 - y1));
        return true;
    }
    return false;
}

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

enter image description here

Вертикальная линия определяется точками (0.3871953044519425, -0.91857980824611341), (0.36139704793723609, 0.91605957361605106)

зеленая почти горизонтальная линияпо точкам (0.8208980020500205, 0.52853407296583088), (0.36178501611208552, 0.88880385168617226)

и белой линии по (0.36178501611208552, 0.88880385168617226), (-0.43211245441046209, 0.68034202227710472)

Как видно, две последние линии имеют общую точку.

Моя функция дает мне решение (0.36178033094571277, 0.88880245640159794) для ОБА этих пересечений (одно из которых вы видите на рисунке красной точкой).

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

Чтобы исправить эту проблему, я, вероятно, могу сделать много вещей, но я не хочу искать во всех списках пересечений, чтобы проверить по другим точкам, чтобы увидеть, совпадают ли позиции.Лучшим решением было бы попытаться повысить точность моей функции пересечения.

Итак, что я спрашиваю, почему точка решения такая неточная?Это потому, что одна из линий почти вертикальная?Должен ли я сначала выполнить какое-то преобразование?В обоих случаях почти вертикальная линия была передана как a0 и a1.

Обновление: Эй, посмотрите на это:

TEST(intersection_precision_test) {
    E_Point problem[] = {

    {0.3871953044519425, -0.91857980824611341}, // 1559
    {0.36139704793723609, 0.91605957361605106}, // 1560
    {-0.8208980020500205, 0.52853407296583088}, // 1798
    {0.36178501611208552, 0.88880385168617226}, // 1799
    {-0.43211245441046209, 0.6803420222771047} // 1800
    };
    std::cout.precision(16);
    E_Point result;
    intersect(problem[0],problem[1],problem[2],problem[3],result);
    std::cout << "1: " << result << std::endl;
    intersect(problem[0],problem[1],problem[3],problem[2],result);
    std::cout << "2: " << result << std::endl;
    intersect(problem[1],problem[0],problem[2],problem[3],result);
    std::cout << "3: " << result << std::endl;
    intersect(problem[1],problem[0],problem[3],problem[2],result);
    std::cout << "4: " << result << std::endl;

    intersect(problem[2],problem[3],problem[0],problem[1],result);
    std::cout << "rev: " << result << std::endl;
    intersect(problem[3],problem[2],problem[0],problem[1],result);
    std::cout << "revf1: " << result << std::endl;
    intersect(problem[2],problem[3],problem[1],problem[0],result);
    std::cout << "revf2: " << result << std::endl;
    intersect(problem[3],problem[2],problem[1],problem[0],result);
    std::cout << "revfboth: " << result << std::endl;
}

Вывод:

Starting Test intersection_precision_test, at Polygon.cpp:1830
1: <0.3617803309457128,0.8888024564015979>
2: <0.3617803309457128,0.8888024564015979>
3: <0.3617803314022162,0.8888024239374175>
4: <0.3617803314022162,0.8888024239374175>
rev: <0.3617803635476076,0.8888024344185281>
revf1: <0.3617803313928456,0.8888024246235207>
revf2: <0.3617803635476076,0.8888024344185281>
revfboth: <0.3617803313928456,0.8888024246235207>

На самом деле у меня заканчиваются биты мантиссы или я могу значительно лучше работать с более умным алгоритмом?

Проблема в том, что у меня нет простого способа определить, когда вершина установлена ​​действительно очень близко кдругая строка.Я не возражал бы переместить это или даже полностью обстреливать это, потому что ни один из них не испортит мой вид!

1 Ответ

2 голосов
/ 21 декабря 2011

Если вы измените свои промежуточные числа с плавающей запятой (ua, ub, denom) на удвоенные значения и напечатаете значения ua (после деления), вы получите:

0x1.f864ab6b36458p-1 in the first case
0x1.f864af01f2037p-1 in the second case

Я напечатал их в шестнадцатеричном виде, чтобы было удобно смотреть на биты. Эти два значения совпадают в первых 22 битах (1.f864a плюс старший бит b и f). У поплавка всего 23 бита! Неудивительно, что если вы вычислите свои промежуточные значения в числах с плавающей точкой, они округляются до одного и того же ответа.

В этом случае вы, возможно, можете обойти эту проблему, вычисляя промежуточных звеньев, используя двойные числа вместо числа с плавающей запятой. (Я заставил свою структуру точек использовать double для x и y. Если вы используете float, я не знаю, поможет ли вычисление промежуточных в double.)

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

Что бы вы сделали, если бы вертикальный сегмент проходил точно через общую конечную точку горизонтальных сегментов? Я полагаю, вы правильно справились с этим делом

Если вы посмотрите на значения ub (после деления), вычисленные с помощью double, вы получите следующее:

0.9999960389052315
5.904388076838819e-06

Это означает, что пересечение очень, очень близко к общей конечной точке горизонтальных сегментов.

Так вот, что я думаю, вы можете сделать. Каждый раз, когда вы вычисляете пересечение, смотрите на ub. Если оно достаточно близко к 1, переместите , чтобы конечная точка была точкой пересечения. Затем обработайте пересечение так, как если бы оно было точным сквозным случаем. На самом деле вам не нужно изменять данные, но вы должны рассматривать конечную точку как перемещенную для обоих сегментов, которые разделяют конечную точку, что означает текущий тест пересечения и тест на следующем отрезке линии.

...