Les algorithmes géométriques sont utiles pour les applications en graphisme par ordinateur ainsi que pour les applications de CAO qui travaillent sur des structures géométriques en 2 ou 3D. Les principaux algorithmes utilisés sont discutés ici, à l'exception des algorithmes classiques de dessin, découpage et surfaces cachées (qui sont couverts spécifiquement par les cours d'infographie).
Les structures suivantes sont à la base des algorithmes présentés par la suite:
Afin de déterminer si deux lignes s'intersectent, on peut calculer leur équation paramétrique et vérifier pour quelle valeur de t les deux droites on leur x et y identiques. Si cela arrive pour t entre 0 et 1 dans les deux droites, elles s'intersectent.
On peut aussi au préalable vérifier que les intervalles en x et y des deux segments de droite s'intersectent. Si on s'attend à ce qu'il y ait peu d'intersections, ce simple test peut rapidement éliminer plusieurs cas où les droites sont éloignées, ne demandant le calcul plus complexe seulement lorsque les droites ont une forte probabilité de se recouper.
Il existe aussi un autre algorithme intéressant:
int ccw(point p0, point p1, point p2)
{
int dx1, dx2, dy1, dy2;
dx1 = p1.x - p0.x;
dx2 = p2.x - p1.x;
dy1 = p1.y - p0.y;
dy2 = p2.y - p1.y;
if(dx1 * dy2 > dy1 * dx2) return(1);
if(dx1 * dy2 < dy1 * dx2) return(-1);
if(dx1 * dx2 < 0 || dy1 * dy2 < 0) return(-1);
if(dx1 * dx1 + dy1 * dy1 >= dx2 * dx2 + dy2 * dy2) return(0);
return(1);
}
int intersect(ligne l1, ligne l2)
{
return(ccw(l1.p1, l1.p2, l2.p1) * ccw(l1.p1,l1.p2,l2.p2) <= 0 &&
ccw(l2.p1, l2.p2, l1.p1) * ccw(l2.p1,l2.p2,l1.p2) <= 0)
}
La première fonction compare la pente des deux lignes formées par les trois points et détermine donc si elles forment un tournant en sens horaire ou non. La seconde fonction vérifie si on tourne dans des direction différentes d'une ligne vers les deux points de l'autre (ce qui indique que la droite associée à la première ligne coupe la seconde ligne) et vice-versa. Si c'est le cas pour les deux, on a automatiquement intersection.
Cet algorithme géométrique est intéressant et pas nécessairement intuitif. Il est robuste et simple à programmer mais, sous sa forme présente n'est pas nécessairement le plus efficace.
La plupart des applications qui calculent l'intersection entre deux courbes de Bézier transforment ces courbes en une suite de lignes ou retardent cette étape pour travailler au niveau pixel. Ceci s'explique par le fait que l'intersection entre deux courbes peut produire 9 points et son calcul est équivalent à trouver la racine d'un polynome du troisième degré.
Heureusement il est aussi possible de découper une telle courbe en quelques segments ayant des propriétés particulières de monotonocité se sorte qu'on puisse utiliser un algorithme itératif entre chaque paire de segments avec une convergence assurée. Une telle technique est très peu utilisée cependant.
Il arrive qu'on ait un ensemble de points par lesquels on veut passer sans jamais croiser son chemin. Pour trouver un tel chemin (et non pas pour trouver le meilleur chemin de ce genre) il existe un algorithme simple et relativement efficace. Il s'agit de prendre un point de départ puis de tracer une ligne de ce point vers chaque autre point pour calculer l'angle associé et ensuite visiter les points par ordre croissant de cet angle.
Dans la mesure où seulement le tri nous intéresse, il est possible d'utiliser une fonction autre que arctangente(dy/dx) à la condition que cette fonction ait des propriétés similaires.
float theta(point p1, point p2)
{
float dx, dy, ax, ay;
dx = p2.x - p1.x;
ax = abs(dx);
dy = p2.y - p1.y;
ay = abs(dy);
if(dx == 0 && dy == 0) t = 0;
else t = dy / (ax + ay);
if(dx < 0) t = 2 - t;
else if(dy < 0) t = 4 + t;
theta = t * 90;
}
Ceci évite de calculer la relation non linéaire entre l'angle et sa tangeante. Il suffit de placer le tout dans le bon quadrant et de multiplier par 90 si on veut, pour des raisons cosmétiques.
Pour savoir si un point est à l'intérieur d'un polygone, la méthode la plus utilisée est de tracer une droite de ce point vers l'infini et de compter les intersections entre cette droite et les segments du polygone qui la croisent. Il faut bien sûr faire attention aux coins qui tombent juste sur la demi-droite.
int inside(point t, polygon p, int N)
{
count = 0;
j = 0;
p[0] = p[N];
p[N + 1] = p[1];
lt.p1 = t;
lt.p2.x = maxint;
lt.p2.y = t.y;
for(i = 1; i <= N ; i++)
{ lp.p1 = p[i]; lp.p2 = p[i];
if(intersect(lt,lp)) continue;
lp.p2 = p[j];
j = i;
if(intersect(lp,lt)) count++;
}
return(count % 1);
}
Lorsqu'on a un nuage de points, un sous-ensemble de ceux-ci forment le polygone convexe qui les englobe tous. Une droite, qui vient de l'infini et se rapproche des points, tombe automatiquement sur un des points de ce polygone convexe. On peut dont prendre les points avec x min, x max, y min ou y max pour obtenir immédiatement jusqu'à 4 de ces points. Pour le reste, les algorithmes ressemblent passablement au tri (en terme de complexité également qui est n log n).
La première méthode est celle de l'
emballage. On prend un point
garanti d'être sur ce polygone et on calcule l'angle entre ce point
et chaque autre point pour enfin choisir le minimum (utilisant la
fonction theta présentée plus tot). Le point choisi devient le
nouveau point de départ. Le polygone est terminé lorsque le
point choisi est notre point de départ initial. La complexité de
cet algorithme est N * M ou N est le nomrbe de points dans le nuage et
M (M
N) est le nombre de points sur le polygone.
Un second algorithme est appelé le balayage de graham. On commence par faire un polygone fermé avec tous les points, tel que vu précédemment. D'un point à l'autre on vérifie que l'on tourne toujours vers la gauche. Sinon, on obtient une portion concave à éliminer en enlevant le point correspondant. Il arrive parfois que plusieurs points soient enlevés lorsqu'un nouveau point est ajouté et que l'on vérifie la convexité.
La complexité de l'algorithme est donc n log n pour le tri initial puis n pour le reste puisque chaque point peut être visité et ensuite enlevé une seule fois. Cette procédure peut s'exprimer comme suit:
min = 1;
for(i = 2 ; i <= N ; i++)
{ if(p[i].y =< p[min].y)
if(p[i].y != p[min].y || p[i].x > p[min].x) min = i;
}
t = p[1];
p[1] = p[min];
p[min] = t;
sort(p,N);
p[0] = p[N];
M = 3;
for(i = 4 ; i <= N ; i++)
{ while( ccw(p[M],p[M - 1],p[i]) >= 0 ) M--;
M++;
t = p[M];
p[M] = p[i];
p[i] = t;
}
On peut remarquer que le premier point rencontré est nécessairement sur le polygone circonscrit. Les points candidats sont accumulés au début du vecteur et sont enlevés par la suite et mis entre M et i lorsqu'on découvre qu'ils sont dans une baie concave.
Une dernière technique mérite d'être examinée puisqu'elle ne peut garantir une meilleure complexité mais permet en général de réduire considérablement le nombre de points à examiner. L'idée est fort simple, il s'agit de choisir 4 points et d'enlever tous les points qui tombent à l'intérieur de ce quadrilatère.
En résumé, la méthode de l'emballage donne en moyenne d'aussi bons résultats que le balayage lorsque les points sont distribués uniformément et que M est d'environ log N. Par ailleurs, la méthode du quadrilatère suivie du balayage donne en moyenne une complexité linéaire est est la plus avantageuse si les points sont bien répartis sur le plan.
Dans l'espace unidimensionnel, la recherche des objets qui sont dans un intervalle donné revient à une recherche dans un arbre binaire avec un intervalle de clés plutôt qu'une clé unique. La complexité est donc n log n pour bâtir l'arbre et R log n pour une recherche d'un intervalle qui retourne R éléments.
Regardons maintenant le problème en 2 dimensions, avec les axes x et y. On veut trouver les objets qui tombent dans un rectangle donné. Il est possible de faire le tri sur x et une recherche linéaire pour vérifier ceux qui satisfont la condition sur y. Il est cependant possible de faire mieux.
On peut dessiner une grille et avoir une matrice qui donne pour chaque carreau la liste des objets contenus à l'intérieur. Le problème est de choisir la granularité de la grille, un peu comme pour les hash table.
Plus flexible est la méthode de diviser l'espace récursivement de l'arbre binaire/ quaternaire qui divise récursivement l'espace en deux (quatre) selon x et y en alternance.
{
divide = X;
val = p.x;
for(;;)
{ old_val = val;
if(divide == X)
{ val = p.x;
divide = Y;
}
else
{ val = p.y;
divide = X;
}
if(old_val < node.boundary)
{ if(node->left == NULL)
{ node->left = new(node);
node->left.boundary = val;
return;
}
node = node->left;
}
else
{ if(node->right == NULL)
{ node->right = new(node);
node->right.boundary = val;
return;
}
node = node->right;
}
}
}
Le problème devient plus complexe lorsque l'on veut trouver tous les rectangles qui sont dans (intersectent) une certaine région. En effet, lorsqu'on applique les subdivisions selon x et y, il arrive souvent qu'on tombe sur un rectangle qui chevauche la frontière. Dans un tel cas, on peut:
Une telle application se retrouve dans l'affichage de formes géométriques sur un écran. En effet, il faut trouver tous les rectangles dans le plan dont une portion intersecte la fenêtre virtuelle associée à l'écran.
Une autre méthode efficace pour cette application est celle du R-tree qui permet à plusieurs régions de se recouper plutôt que d'avoir les rectangles qui recoupent plus d'une région. Le principe d'opération est le suivant pour bâtir l'arbre ou lorsque des rectangles sont ajoutés:
Dans plusieurs applications comme la vérification des règles de
dessin des masques de fabrication en micro-électronique,
il faut vérifier si des intersections
(court-circuits) sont présentes parmi un très grand nombre de
polygones. On peut simplement vérifier tous les objets par paires
mais avec des millions de rectangles c'est plutôt impratiquable (
).
La première méthode vue est celle du livre et concerne des segments de droites horizontaux et verticaux. La procédure est comme suit:
Lorsque l'on veut trouver l'intersection entre des polygones, il faut utiliser un arbre plus complexe puisque les rectangles peuvent intersecter. On peut aussi utiliser une simple liste qui ne fournit pas la performance de n log n + Intersections de l'algorithme précédent.
Il arrive souvent que l'on veuille trouver les deux plus proches voisins dans un nuage. Pour ce faire, on divise récursivement en 2 l'espace selon x et on maintient les y triés à mesure des fusions en x. Ceci donne la procédure:
On peut généraliser la procédure précédente pour trouver toutes les paires de voisins les plus proches. Ceci est un problème similaire à celui de trouver le diagramme de Voronoi.
Il suffit de décomposer récursivement l'espace en 2 selon x puis de relier les deux points laissés ensemble dans une partition. Ensuite, de partition en partition, les points en périphérie des polygones circonscrits formés, qui sont du côté de la frontière où se fait la fusion sont joints en suivant les y triés.
En conclusion, les algorithmes géométriques forment un domaine
encore jeune et très actif qui s'est développé avec la CAO et
l'infographie. La plupart des algorithmes requièrent un temps
de l'ordre de
,
ou
, ce qui n'est pas trop mal.
Ce domaine se situe entre l'infographie qui s'intéresse aux aspects plus visuels de la chose et la théorie des graphes qui fait abstraction de la position géométrique des objets et se concentre sur les liens qui les unissent.