Forum: Compiler & IDEs Ideen für Umlaufzahl-Berechnung?


von Johann L. (gjlayde) Benutzerseite


Angehängte Dateien:

Lesenswert?

Hi,

für meine Snake on Scope brauche ich an einer Stelle die Umlaufzahl 
einer Kurve um einen Punkt.

Die Kurve ist eine Jordan-Kurve, und es ist zu bestimmen, ob der Punkt 
im Innern der Kurve liegt oder ausserhalb.

Zur Erinnerung: Eine Jordankurve teilt die Ebene in ein Inneres und ein 
Äusseres, sie ist geschlossen, und sie schneidet sich nicht selbst (also 
nicht 8-förmig oder so).

Die Elemente der Kurve sind Geradenstücke in den Koordinatenrichtungen.
Die Kurve ist weder konvex noch sternförmig.

Momentan mache ich das wie im Anhang in snake_umlauf zu sehen: Für alle 
Mauerelemente wird der Winkel bestimmt, unter dem das Element von dem zu 
testenden Punkt aus zu sehen ist.
Diese Winkel werden für alle Segmente aufaddiert.

Für Punkte, die nicht auf der Kurve liegen (was der hier Fall ist), ist 
die so bestimmt Umlaufzahl ein ganzzahliges Vielfaches des Vollwinkels. 
Ein Punkt liegt im Innern wenn die Umlaufzahl 360° ist.

Irgendwei hab ich den Eindruck, ich mach das viel zu kompliziert, aber 
selbst nach einigem Überlegen ist mir kein einfacherer Weg eingefallen, 
das zu programmieren. Es funktioniert zwar, aber die 
Arcustangens-Tabelle nimmt einiges an Platz weg.

Allein diese Umlaufzahl-Bestimmung brauch an die 500 Bytes Flash!

Codetechnisch muss das ganze natürlich -- wie immer -- schnell sein und 
soll wenig Platz brauchen.

Es läuft auf einem ATmega168 @ 24MHz.
Per 37.5 kHz brauch ich einen neuen Pixel für die Röhre (also alle 640 
Ticks), was nicht sooo viel Zeit ist.

Also: Ideen sind gesucht, wie und ob man die Umlaufzahl einfacher 
berechnen kann.

Hier nochmal das wesentliche Codefragment: Es ist über alle 
Kurvensegmente aufzurufen, welche jeweils von P0 bis P1 reichen.
1
#include "atan2tab.h"
2
3
// (A-B)/4 berechnen und so runden, dass das Vorzeichen gleich bleibt.
4
// ./4 damit die Differenz in 8 Bit (signed) passt
5
int8_t subr (uint8_t a, uint8_t b)
6
{
7
    return (a < b)
8
        ? (a-b) >> 2
9
        : -((b-a) >> 2);
10
}
11
12
void ...
13
{
14
    // P0: Beginn des Mauerelements
15
    uint8_t x0 = ...
16
    uint8_t y0 = ...
17
18
    // P1: Ende des Mauerelements
19
    uint8_t x1 = ...
20
    uint8_t y1 = ...
21
22
    // Richtungsvektoren vom Punkt, fuer den der Umlauf
23
    // zu bestimmen ist (s->food) zu den Endpunkten des
24
    // Mauersegments
25
    // P2 = P0 - Food
26
    // P3 = P1 - Food
27
    int8_t x2 = subr (x0, s->food.x);
28
    int8_t y2 = subr (y0, s->food.y);
29
    int8_t x3 = subr (x1, s->food.x);
30
    int8_t y3 = subr (y1, s->food.y);
31
32
    // Element so hindrehen, daß P3 auf (K,0) mit K >= 0
33
    // abgebildet wird. Matrix ist M=((x3,y3),(y3,-x3))
34
    // P4 = M*P2
35
    // M*P3 zeigt in positive x-Richtung
36
    int16_t x4 = x3*x2 + y3*y2;
37
    int16_t y4 = y3*x2 - x3*y2;
38
39
    // Auf 8 Bits (signed) zusammenschieben
40
    // P4 >>= ?
41
    while (1)
42
    {
43
        if (x4 < 128 && x4 > -128)
44
            if (y4 < 128 && y4 > -128)
45
                break;
46
        x4 >>= 1;
47
        y4 >>= 1;
48
    }
49
50
    // Auf 4 Bits (signed) zusammenschieben
51
    // P5 = P4 >> ?
52
    int8_t x5 = x4;
53
    int8_t y5 = y4;
54
55
    while (1)
56
    {
57
        if (x5 < 8 && x5 > -8)
58
            if (y5 < 8 && y5 > -8)
59
                break;
60
        x5 >>= 1;
61
        y5 >>= 1;
62
    }
63
64
    // Winkel von P5 gegen die positive X-Achse bestimmen
65
    // Das ist der Winkel, unter dem (P0,P1) von Food aus
66
    // erscheint.
67
68
    // Index in atan2-Tabelle errechnen
69
    uint8_t x6 = x5+7;
70
    uint8_t y6 = y5+7;
71
    uint8_t index = (uint8_t) (x6 + 15*y6);
72
73
    // Winkel aus Tabelle lesen (Vollwinkel ~ 100)
74
    int8_t phi = pgm_read_byte (& atan2tab[index]);
75
76
    s->wall_umlauf += phi;
77
}

Grüß, Johann

von Johann L. (gjlayde) Benutzerseite


Angehängte Dateien:

Lesenswert?

Und noch ein Bild.

Die Kurve, für die so eine Berechnung zu machen ist, sieht aus wie die 
gestrichelte Kurve auf dem Bild, der Rand des Spielfeldes.

Die Kurve kann natürlich auch komplizierter aussehen.

von Karl H. (kbuchegg)


Angehängte Dateien:

Lesenswert?

Es gibt mehrere Möglichkeiten für einen Punkt/Polygon Test.
Und ja eine davon ist die Methode des Umlaufwinkels.

Allerdings muss der Winkel nicht sehr exakt berechnet werden. Es reicht 
völlig aus, wenn er so ungefähr hinkommt. Die Summe kann ja nur 2 Werte 
haben: 0 oder 2*PI (oder Vielfache davon).

Es reicht völlig aus, den Gesamt-Winkel mit 4 Bit Genauigkeit zu 
bestimmen und für jeden Einzelwinkel eine 2 Bit Genauigkeit zu haben. 
Effektiv läuft es damit darauf hinaus, Quadranten abzuzählen :-)
Damit ist deine Tangens-Tabelle nicht notwendig.

Anbei eine Implementierung dieses Verfahrens von Kevin Weiler aus dem 
Buch 'Graphics Gems IV'. Der Code darf beliebig weiter verwendet werden.

(Anpassen an deine Datenstruktur musst du ihn noch selber)

von Karl H. (kbuchegg)


Angehängte Dateien:

Lesenswert?

Und das Header File dazu

von Karl H. (kbuchegg)


Angehängte Dateien:

Lesenswert?

Anbei noch eine Testsuite für verschiedene Point in Polygon Verfahren 
aus dem gleichen Buch. Diesmal von Eric Haines. Auch hier wieder: Der 
Code kann beliebig verwendet werden.

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Hi, danke!

Also kommt man am Berechnen der Umluuf- resp. Windungszahl nicht 
vorbei...

Meinen Algorithmus hab ich etwas verbessert und Symmetrien von atan 
ausgenutzt, so daß es jetzt nur noch 150 Bytes insgesamt braucht.

Weilers Algorithmus sieht sehr interessant aus!
Den muss ich mir mal näher anschauen. Die unangenehme Division kann man 
rauserweitern, dabei geht es ja nur um einen Vorzeichentest. Und 16-Bit 
Arithmetik reicht hoffentlich, ansonsten wird das zu komplex.

Johann

von yalu (Gast)


Lesenswert?

> Also kommt man am Berechnen der Umluuf- resp. Windungszahl nicht
> vorbei...

Also geht es dir nicht primär darum, den Umlaufwinkel zu bestimmen,
sondern tatsächlich herauszufinden, ob sich ein Punkt innmerhalb eines
gegebenen Polygons befindet? Der Thread-Titel und dein Initialpost haben
mich da etwas verunsichert :)

Eine andere Möglichkeit des Punkt-im-Polygon-Tests besteht darin, dass
man abzählt, wie oft eine in dem zu testenden Punkt beginnende
Halbgerade die Polygonlinie durchquert. Ist die Anzahl ungerade, liegt
der Punkt im Innern.

Das Verfahren könnte in deinem Fall ganz geschickt sein, da alle
Polygonseiten Achsenparallel liegen, so dass die Überprüfung der
Durchquerung relativ leicht ist, wenn man die Halbgerade ebenfalls
achsenparallel wählt. Ob es allerdings schneller ist als das von Karl
Heinz genannte Verfahren, weiß ich nicht.

von Karl H. (kbuchegg)


Lesenswert?

yalu wrote:
> Das Verfahren könnte in deinem Fall ganz geschickt sein, da alle
> Polygonseiten Achsenparallel liegen,

Ja das hab ich mir auch überlegt, hab den Vorschlag aber dann wieder 
verworfen, weil ich nicht sicher war, ob er die Achsparallelität nicht 
irgendwann aufgeben möchte.
Ansonsten wäre das Schnittpunkte zählen speziell in diesem Fall sicher 
die beste Lösung, weil man so gut wie nichts rechnen muss und letztendes 
alles auf ein paar Vergleiche rausläuft.

Allerdings gibt es da ein paar unangnehmen Sonderfälle, die Beachtung 
brauchen


(vom Punkt ausgehend, nach rechts die Schnittpunkte zählen

  +------------------+
  |                  |
  |    *  +------+   |
  |       |      |   |
  +-------+      +---+


3 'Schnittpunkte'   -> Punkt ist drinnen, wie es sein soll



                   +-----+
                   |     |
       *     +-----+     |
             |           |
             +-----------+

ebenfalls 3 Schnittpunkte, aber diesmal ist der Punkt draussen


Das Problem besteht in der Behandlung eines 'Schnittpunktes', wenn der 
SChnittpunkt mit dem Endpunkt einer Polygonseite identisch ist.
Ich hab das meist so gelöst, dass in so einem Fall der Schnittpunkt nur 
dann zählt, wenn er mit dem unteren (also dem mit dem kleineren y Wert) 
der beiden Endpunkte identisch ist und im anderen Fall nicht.

von yalu (Gast)


Lesenswert?

Habe mal etwas reingehackt, weil gerade Sonntag ist :)
1
#include <stdio.h>
2
#include <stdint.h>
3
4
typedef struct {
5
  uint8_t x, y;
6
} point_t, poly_t[];
7
8
// Polygondaten: Es ist nur jeder zweite Punkt eingetragen. Ein Punkt ist mit
9
// seinem Nachfolger durch ein L-Stück verbunden, dessen erster Schnekel
10
// horizontal und dessen zweiter vertiakl verläuft.
11
12
// Beispielpolygon
13
poly_t poly = {
14
  {1, 1}, {3, 2}, {4, 1}, {6, 3}, {5, 4},
15
  {6, 6}, {4, 5}, {3, 6}, {1, 4}, {2, 3}
16
};
17
18
uint8_t isinside(poly_t poly, uint8_t n, point_t point) {
19
  uint8_t yold, i, cnt=0;
20
21
  yold = poly[n-1].y;
22
  for(i=0; i<n; i++) {
23
    if(poly[i].x>point.x && ((yold<=point.y) ^ (poly[i].y<=point.y)))
24
      cnt++;
25
    yold = poly[i].y;
26
  }
27
  return cnt%2;
28
}
29
30
int main(void) {
31
  uint8_t ins;
32
33
  // Beispielaufruf
34
  ins = isinside(poly, sizeof poly / sizeof poly[0], (point_t){3, 4});
35
  return 0;
36
}

Die Funktion isinside liefert eine 1 zurück, wenn der Punkt innerhalb
und eine 0, wenn er außerhalb des Polygons liegt. Linien, die das
Polygon nach links oder unten begrenzen, gehören zum Polygon dazu,
Linien, die es nach oben oder rechts begrenzen, nicht. Wenn ich den
Eingangspost richtig verstanden habe, liegt der Punkt aber nie auf dem
Polygonumfang, so dass dies keine Rolle spielt. Wenn doch, müsste man
die if-Abfrage etwas erweitern, um zu erreichen, dass der Umfang
entweder komplett zum Polygon dazugehört oder komplett nicht.

Das Polygon wird, da die Seiten achsenparallel sind, durch die Hälfte
seiner Eckpunkte vollständig beschrieben (s. Kommentar im Programm).

Die Funktion braucht 74 Bytes Programmspeicher (AVR-GCC 4.2.4) und
sollte auch einigermaßen schnell sein, da pro Polygonpunkt nur einige
wenige Billigoperation ausgeführt werden.

Ich habe sie an folgendem Polygon getestet, bei das Ergebnis für alle
Punkte x, y in [0, 7] gestimmt hat:
1
6_   ___   ___
2
5_  |   |_|   |
3
4_  |_       _|
4
3_   _|     |_
5
2_  |    _    |
6
1_  |___| |___|
7
0_  
8
  | | | | | | |
9
  0 1 2 3 4 5 6

Das heißt natürlich nicht, dass keine Bugs vorhanden wären ;-)

Man kann das Verfahren auch auf Polygone mit nicht achsenparallelen
Seiten erweitern. Dann muss die Schleife zum einen über alle Eckpunkte
laufen, zum anderen ist dann die Abfrage poly[i].x>point.x durch einen
komplizierteren Ausdruck zu ersetzen, der prüft, ob der Punkt links oder
rechts von der (möglicherweisen schiefen) Polygonseite liegt. Dazu sind
einige zusätzliche Additionen und Multiplikationen erforderlich, die
teilweise in 16-Bit gerechnet werden müssen.

von yalu (Gast)


Lesenswert?

Kleiner Nachtrag: Weitere 4 Bytes können eingespart werden, wenn man die
Zeilen
1
      cnt++;
2
    ...
3
  return cnt%2;

durch
1
      cnt=~cnt;
2
    ...
3
  return cnt;

ersetzt. Als Funktionswert erhält man jetzt statt der 1 eine 255, was
aber egal sein sollte, da er in einer logischen Abfrage beide als "wahr"
zählen.

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

yalu schrob:
> Also geht es dir nicht primär darum, den Umlaufwinkel zu bestimmen,
> sondern tatsächlich herauszufinden, ob sich ein Punkt innmerhalb eines
> gegebenen Polygons befindet? Der Thread-Titel und dein Initialpost haben
> mich da etwas verunsichert :)

Für ne geschlossene Kurve ist das ja gleichbedeutend.

> Eine andere Möglichkeit des Punkt-im-Polygon-Tests besteht darin, dass
> man abzählt, wie oft eine in dem zu testenden Punkt beginnende
> Halbgerade die Polygonlinie durchquert. Ist die Anzahl ungerade, liegt
> der Punkt im Innern.

ARRRGH Tischkante-beiss, das ist genau das was ich gesucht hab!

Ich wusste doch daß ich wieder viel zu umständlich dachte...

> Ob es allerdings schneller ist als das von Karl
> Heinz genannte Verfahren, weiß ich nicht.

Ich würde sagen ja, weil man kein * oder / braucht.
Und es ist schneller als mein Tabellen-Verfahren.

Es braucht 60 Bytes freu.
1
void snake_umlauf (snake_t * s, uint8_t dir, uint8_t len)
2
{
3
    if (dir & 1)
4
        // NORD || SUED
5
        return;
6
7
    // Q: Testpunkt
8
    // P0/P1: Beginn/Ende des Mauerelements
9
    uint8_t yq = s->food.y;
10
    uint8_t y0 = s->draw.y;
11
    
12
    // Teststrahl soll nach oben gehen
13
    if (y0 < yq)
14
        return;
15
        
16
    // Laenge des Mauerelements
17
    uint8_t mlen = len << WALL_SKIP;
18
    
19
    // Trifft der Strahl die Mauer?    
20
    uint8_t xq = s->food.x;
21
    uint8_t x0 = s->draw.x;
22
    uint8_t x1 = x0;
23
    
24
    if (dir == SN_E)
25
    {
26
        // Ost
27
        x1 += mlen;
28
        if (xq < x0 || xq > x1)
29
            // Nein
30
            return;
31
    }
32
    else
33
    {
34
        // West
35
        x1 -= mlen;
36
        if (xq < x1 || xq > x0)
37
            // Nein
38
            return;
39
    }
40
    
41
    // Ja, der Strahl trifft
42
    // WEST: +1
43
    // Ost:  -1
44
    s->wall_umlauf -= dir -1;
45
}

...und da sie nur 1x verwendet wird braucht's nach demm Inlinen nur noch 
32 Bytes.

Und es ist wiklich die Umlaufzahl. Wenn man es über mehrere Kurven macht 
gehen auch Löcher und so :-))

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.