Was ist der schnellste Weg, um die 2D-Bounding-Box-Schnittmenge zu ermitteln?


62

Angenommen, jedes Box-Objekt hat die Eigenschaften x, y, width, height und hat seinen Ursprung in der Mitte und weder die Objekte noch die Begrenzungsboxen drehen sich.


Handelt es sich um achsen- oder objektorientierte Begrenzungsrahmen?
16.07.10

3
Wenn Sie diese Frage stellen, müssen Sie in Zukunft sicherlich andere Schnittpunkttypen testen;). Deshalb schlage ich DIE LISTE über Objekt / Objektschnitt vor. Die Tabelle enthält Schnittpunkte zwischen allen gängigen Objekttypen (Kästchen, Kugeln, Dreiecke, Zyklinder, Kegel usw.) in statischen und dynamischen Situationen.
Dave O.

2
Bitte formulieren Sie Ihre Frage neu. Aus meiner Sicht impliziert Box ein 3D-Objekt.
Dave O.

Antworten:


55

(C-ish Pseudocode - Sprachoptimierungen entsprechend anpassen)

bool DoBoxesIntersect(Box a, Box b) {
  return (abs(a.x - b.x) * 2 < (a.width + b.width)) &&
         (abs(a.y - b.y) * 2 < (a.height + b.height));
}

Auf Englisch: Überprüfen Sie auf jeder Achse, ob die Zentren der Kästchen so nah sind, dass sie sich schneiden. Wenn sie sich auf beiden Achsen schneiden, schneiden sich die Kästchen. Wenn nicht, dann nicht.

Sie können die <'s in <= ändern, wenn Sie die Kantenberührung als Schnittmenge zählen möchten. Wenn Sie eine bestimmte Formel nur für Kantenberührungen verwenden möchten, können Sie == nicht verwenden. Dadurch erfahren Sie, ob sich die Ecken berühren, und nicht, ob sich die Kanten berühren. Sie möchten etwas tun, das logisch äquivalent zu ist return DoBoxesIntersectOrTouch(a, b) && !DoBoxesIntersect(a, b).

Es ist erwähnenswert, dass Sie eine kleine, aber signifikante Geschwindigkeitssteigerung erzielen können, indem Sie die halbe Breite und die halbe Höhe zusätzlich zu (oder anstelle von) der vollen Breite und der vollen Höhe speichern. Andererseits ist es selten, dass der 2d-Bounding-Box-Schnittpunkt der Leistungsengpass ist.


9
Dies setzt natürlich voraus, dass die Kästchen achsenausgerichtet sind.
16.07.10

1
Bauchmuskeln sollten nicht besonders langsam sein - zumindest nicht langsamer als eine Bedingung, und der einzige Weg, dies ohne Bauchmuskeln zu tun (dessen bin ich mir bewusst), beinhaltet zusätzliche Bedingungen.
ZorbaTHut

4
Ja, es werden Achsen-ausgerichtete Boxen vorausgesetzt. Die beschriebenen Strukturen haben jedoch keine Möglichkeit, eine Rotation anzuzeigen, sodass ich der Meinung war, dass sie sicher sind.
ZorbaTHut

3
Hier einige gute Tipps zur Beschleunigung der Berechnungen in Actionscript (hauptsächlich Ganzzahlberechnung): lab.polygonal.de/2007/05/10/bitwise-gems-fast-integer-math Ich poste dies, weil es auch eine schnellere enthält Ersatz für Math.abs (), der in der Tat dazu neigt, Dinge in Actionscript zu verlangsamen (wenn man natürlich von leistungskritischen Dingen spricht).
Mistzack

2
Sie vermissen, dass sie den Ursprung in der Mitte haben, nicht am linken Rand. Eine Box, die von 0 bis 10 läuft, hat tatsächlich "x = 5", während eine Box, die von 8 bis 12 läuft, "x = 10" hat. Sie landen mit abs(5 - 10) * 2 < (10 + 4)=> 10 < 14. Sie müssen einige einfache Anpassungen vornehmen, damit es mit der oberen linken Ecke und der Größe funktioniert.
ZorbaTHut

37

Dies funktioniert für zwei Rechtecke, die an der X- und Y-Achse ausgerichtet sind.
Jedes Rechteck hat die Eigenschaften:
"left", die x-Koordinate seiner linken Seite,
"top", die y-Koordinate seiner oberen Seite,
"right", die x-Koordinate seiner rechten Seite,
"bottom", die y-Koordinate von seine Unterseite,

function IntersectRect(r1:Rectangle, r2:Rectangle):Boolean {
    return !(r2.left > r1.right
        || r2.right < r1.left
        || r2.top > r1.bottom
        || r2.bottom < r1.top);
}

Beachten Sie, dass dies für ein Koordinatensystem vorgesehen ist, in dem die + y-Achse nach unten und die + x-Achse nach rechts zeigt (dh typische Bildschirm- / Pixelkoordinaten). Um dies an ein typisches kartesisches System anzupassen, bei dem + y nach oben gerichtet ist, würden die Vergleiche entlang der vertikalen Achsen umgekehrt, z.

return !(r2.left > r1.right
    || r2.right < r1.left
    || r2.top < r1.bottom
    || r2.bottom > r1.top);

Die Idee ist, alle möglichen Bedingungen zu erfassen, unter denen sich die Rechtecke nicht überlappen, und dann die Antwort zu negieren, um zu sehen, ob sie sich überlappen. Unabhängig von der Richtung der Achsen ist leicht zu erkennen, dass sich zwei Rechtecke nicht überlappen, wenn:

  • Die linke Kante von r2 ist weiter rechts als die rechte Kante von r1

     ________     ________
    |        |   |        |
    |   r1   |   |   r2   |
    |        |   |        |
    |________|   |________|
    
  • oder die rechte Kante von r2 ist weiter links als die linke Kante von r1

     ________     ________
    |        |   |        |
    |   r2   |   |   r1   |
    |        |   |        |
    |________|   |________|
    
  • oder die Oberkante von r2 liegt unter der Unterkante von r1

     ________ 
    |        |
    |   r1   |
    |        |
    |________|
     ________ 
    |        |
    |   r2   |
    |        |
    |________|
    
  • oder die Unterkante von r2 liegt über der Oberkante von r1

     ________ 
    |        |
    |   r2   |
    |        |
    |________|
     ________ 
    |        |
    |   r1   |
    |        |
    |________|
    

Die ursprüngliche Funktion und eine alternative Beschreibung der Funktionsweise finden Sie hier: http://tekpool.wordpress.com/2006/10/11/rectangle-intersection-determine-if-two-given-rectangles-intersect- einander oder nicht /


1
Überraschend intuitiv und zeigt einmal mehr, dass der Versuch, die Antwort auf eine andere Frage zu finden, hilfreich sein kann, wenn es zu schwierig ist, die Antwort zu finden. Vielen Dank!
Lodewijk

1
Sie sollten erwähnen, dass die y-Achse nach unten zeigt (wie in einem Bild). Andernfalls müssen die Ungleichungen r2.top> r1.bottom und r2.bottom <r1.top umgekehrt werden.
user1443778

@ user1443778 guten Fang! Ich ging voran und erklärte die Logik hinter diesem Algorithmus auch auf koordinatensystemunabhängige Weise.
Ponkadoodle

11

Wenn Sie objektorientierte Begrenzungsrahmen wünschen, versuchen Sie dieses Tutorial auf der Trennachse Theorem von Metanet: http://www.metanetsoftware.com/technique/tutorialA.html

SAT ist nicht die schnellste Lösung, aber relativ einfach. Sie versuchen, eine einzelne Linie (oder eine Ebene, wenn 3D) zu finden, die Ihre Objekte trennt. Wenn diese Linie existiert, ist es garantiert, parallel zur Kante einer Ihrer Boxen zu sein. Sie durchlaufen also alle Kanten, um zu prüfen, ob sie die Boxen trennt.

Dies funktioniert auch für achsenausgerichtete Rahmen, indem nur die x / y-Achse berücksichtigt wird.


Ich habe nicht an Rotation gedacht, aber danke, das ist ein interessanter Link.
Iain

5

Das obige DoBoxesIntersect ist eine gute paarweise Lösung. Wenn Sie jedoch viele Boxen haben, haben Sie immer noch ein O (N ^ 2) -Problem, und Sie werden möglicherweise feststellen, dass Sie zusätzlich etwas tun müssen, wie es Kaj meint. (In der 3D-Kollisionserkennungsliteratur wird davon ausgegangen, dass es sowohl einen breitphasigen als auch einen engphasigen Algorithmus gibt. Wir werden sehr schnell alle möglichen Überlappungspaare finden und dann etwas teureres, um zu prüfen, ob dies möglich ist Paare sind tatsächliche Paare.)

Der Breitbandalgorithmus, den ich zuvor verwendet habe, ist "Sweep-and-Prune". Für 2D pflegen Sie zwei sortierte Listen für den Anfang und das Ende jeder Box. Solange sich die Box nicht >> von Frame zu Frame bewegt, wird sich die Reihenfolge dieser Listen nicht wesentlich ändern. Sie können also die Blase- oder Einfügesortierung verwenden, um sie beizubehalten. Das Buch "Real-Time Rendering" enthält eine schöne Beschreibung der Optimierungen, die Sie durchführen können, aber es läuft in der breiten Phase auf die O (N + K) -Zeit hinaus, für N Boxen, von denen sich K überlappen, und mit hervorragender realer Welt Leistung, wenn Sie sich N ^ 2 Boolesche Werte leisten können, um zu verfolgen, welche Kästchenpaare sich von Bild zu Bild schneiden. Sie haben dann insgesamt O (N + K ^ 2) Zeit, was << O (N ^ 2) ist, wenn Sie viele Kästchen, aber nur wenige Überlappungen haben.


5

Eine Menge Mathe hier für ein sehr einfaches Problem. Nehmen wir an, wir haben 4 Punkte für ein rechtes, oberes, linkes, unteres, rechtes ...

Wenn Sie feststellen möchten, ob zwei Rechtecke kollidieren, müssen Sie nur prüfen, ob alle möglichen Extreme, die Kollisionen verhindern würden, erfüllt sind. Wenn Sie Grenzkollisionen einbeziehen möchten, MÜSSEN Sie die beiden Rechtecke kollidieren. Ersetzen Sie einfach die Zeichen> und < mit entsprechendem> = und = <.

struct aRect{
  float top;
  float left;
  float bottom;
  float right;
};

bool rectCollision(rect a, rect b)
{
  return ! ( b.left > a.right || b.right < a.left || b.top < a.bottom || b.bottom > a.top);
}

Ich bin mir ehrlich gesagt nicht sicher, warum dies nicht die am häufigsten gewählte Antwort ist. Es ist einfach, korrekt und effizient.
3Dave

3

Alternative Version der Antwort von ZorbaTHut:

bool DoBoxesIntersect(Box a, Box b) {
     return (abs(a.x - b.x) < (a.width + b.width) / 2) &&
     (abs(a.y - b.y) < (a.height + b.height) / 2);
}

Eigentlich funktioniert diese Arithmetik so oder so. Sie können auf beiden Seiten des <eine beliebige arithmetische Operation ausführen, die sich jedoch nicht ändert (Multiplikation mit einem negativen Wert bedeutet, dass Sie den Wert kleiner als ändern müssen). In diesem Beispiel sollten die Boxen nicht kollidieren. Wenn die Mitte von Box A bei 1 liegt, reicht sie von -4 bis 6. Box b liegt bei 10 und reicht von 7,5 bis 12,5, es liegt dort keine Kollision vor ... Nun ist die von Wallacoloo veröffentlichte Methode nicht nur korrekt. Bei Sprachen, die Kurzschlüsse implementieren, ist die Ausführung jedoch schneller, da die meisten Überprüfungen ohnehin falsch sind. Der Kurzschluss kann nach
Deleter

Ja, das habe ich gemerkt, als ich heute Morgen aufgewacht bin. Chris hat mich mit seiner <> Verwechslung verarscht.
Iain

1
Zwei Probleme: Erstens ist die Division in der Regel wesentlich langsamer als die Multiplikation. Zweitens, wenn es sich um Ganzzahlen handelt, kann dies zu Problemen beim Abschneiden von Ganzzahlen führen (ax = 0, bx = 9, a.width = 9, b.width = 10: abs (0-9) <(9 + 10) / 2, 9 <19/2, 9 <9, Funktion gibt false zurück, obwohl sich die Felder definitiv kreuzen.)
ZorbaTHut

2

Je nachdem, welches Problem Sie lösen möchten, ist es möglicherweise besser, den Überblick über Ihr Objekt zu behalten, während Sie es verschieben. Führen Sie also eine Liste mit sortierten x Start- und Endpositionen und eine Liste mit Start- und Endpositionen. Wenn Sie eine Menge Überlappungsprüfungen durchführen müssen und daher optimieren müssen, können Sie dies zu Ihrem Vorteil nutzen, da Sie sofort nachsehen können, wer links von Ihnen endet. Jeder, der links davon endet, kann beschnitten werden sofort. Gleiches gilt für oben, unten und rechts.
Die Buchhaltung kostet natürlich Zeit, daher eignet sie sich eher für Situationen mit wenigen sich bewegenden Objekten, aber vielen Überlappungsprüfungen.
Eine andere Option ist räumliches Hashing, bei dem Sie die Objekte basierend auf der ungefähren Position (Größe kann in mehrere Eimer aufgeteilt werden) in einen Eimer packen, aber nur, wenn es viele Objekte gibt, von denen sich aufgrund der Buchhaltungskosten relativ wenige pro Iteration bewegen.
Grundsätzlich hilft alles, was (n * n) / 2 vermeidet (wenn Sie Objekt a gegen b prüfen, müssen Sie b nicht offensichtlich gegen a prüfen), mehr als die Optimierung der Begrenzungsrahmenprüfungen. Wenn Bounding-Box-Checks einen Engpass darstellen, würde ich ernsthaft empfehlen, nach alternativen Lösungen für das Problem zu suchen.


2

Der Abstand zwischen den Mittelpunkten ist nicht der gleiche wie der Abstand zwischen den Ecken (wenn sich eine Box zum Beispiel in der anderen befindet). Im Allgemeinen ist diese Lösung die richtige (denke ich).

Abstand zwischen Zentren (zB x): abs(x1+1/2*w1 - x2+1/2*w2)oder1/2 * abs(2*(x1-x2)+(w1-w2)

Mindestabstand ist 1/2 w1 + 1/2 w2 or 1/2 (w1+w2). die hälften stornieren also ..

return 
ABS(2*(x1 - x2) + (w1-w2) ) < (w1+w2)) &&
ABS(2*(y1 - y2) + (h1-h2) ) < (h1+h2));

1
Was ist mit der "return" -Anweisung drin?
Doppelgreener

1

Hier ist meine Implementierung in Java unter der Annahme einer Zweierkomplementarchitektur . Wenn Sie keine Zweierkomplementierung haben, verwenden Sie stattdessen einen Standardfunktionsaufruf von Math.abs :

boolean intersects(IntAxisAlignedBox left, IntAxisAlignedBox right) {
    return
        (
            lineDeltaFactor(left.min.x, left.max.x, right.min.x, right.max.x) |
            lineDeltaFactor(left.min.y, left.max.y, right.min.y, right.max.y) |
            lineDeltaFactor(left.min.z, left.max.z, right.min.z, right.max.z)
        ) == 0;
}

int lineDeltaFactor(int leftMin, int leftMax, int rightMin, int rightMax) {
    final int
            leftWidth = leftMax - leftMin,
            rightWidth = rightMax - rightMin,

            leftMid = leftMin + ((leftMax - leftMin) >> 1),
            rightMid = rightMin + ((rightMax - rightMin) >> 1);

    return (abs(leftMid - rightMid) << 1) / (leftWidth + rightWidth + 1);
}

int abs(int value) {
    final int mask = value >> (Integer.SIZE - 1);

    value ^= mask;
    value += mask & 1;
    return value;
}

Angenommen, ein halbwegs vernünftiger Compiler / LLVM inline erweitert diese Funktionen, um teures Stack-Jonglieren und V-Table-Lookups zu vermeiden. Dies schlägt bei Eingabewerten nahe den 32-Bit-Extremwerten (dh Integer.MAX_VALUEund Integer.MIN_VALUE) fehl .


0

Der schnellste Weg besteht darin, alle 4 Werte in einem einzigen Vektorregister zu kombinieren.

Speichern Sie die Boxen in einem Vektor mit folgenden Werten [ min.x, min.y, -max.x, -max.y ]. Wenn Sie Boxen wie diese speichern, werden für den Schnittpunkttest nur 3 CPU-Anweisungen benötigt:

_mm_shuffle_ps um die zweite Box neu zu ordnen, indem man die min und max Hälften spiegelt.

_mm_xor_psmit magischer Zahl _mm_set1_ps(-0.0f), um die Vorzeichen aller 4 Werte im zweiten Feld umzudrehen.

_mm_cmple_ps Um alle 4 Werte miteinander zu vergleichen, werden die folgenden zwei Register verglichen:

[ a.min.x, a.min.y, -a.max.x, -a.max.y ] < [ b.max.x, b.max.y, -b.min.x, -b.min.y ]

Schließlich, falls erforderlich, _mm_movemask_psum das Ergebnis aus der Vektoreinheit in ein Skalarregister zu bekommen. Der Wert 0 bedeutet, dass sich die Kästchen schneiden. Wenn Sie mehr als 2 Boxen haben, ist dies nicht erforderlich. Lassen Sie die Werte in den Vektorregistern und verwenden Sie bitweise Operationen, um Ergebnisse aus mehreren Boxen zu kombinieren.

Sie haben weder eine Sprache noch eine Plattform angegeben, aber die Unterstützung für SIMD wie diese oder ähnliche ist in allen Plattformen und Sprachen verfügbar. Auf dem Handy hat ARM NEON SIMD mit sehr ähnlichen Sachen. .NET hat Vector128 im Namespace System.Runtime.Intrinsics und so weiter.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.