Algorithme de tracé de segment de Bresenham
Lâalgorithme de tracĂ© de segment de Bresenham est un algorithme dĂ©veloppĂ© par Jack E. Bresenham en , alors quâil travaillait dans un laboratoire informatique dâIBM et cherchait Ă piloter un traceur attachĂ© Ă une console texte. Cet algorithme a Ă©tĂ© prĂ©sentĂ© Ă la convention de lâACM en 1963, puis publiĂ© en 1965 dans la revue IBM Systems Journal.
Lâalgorithme dĂ©termine quels sont les points dâun plan discret qui doivent ĂȘtre tracĂ©s afin de former une approximation de segment de droite entre deux points donnĂ©s. Cet algorithme est souvent utilisĂ© pour dessiner des segments de droites sur lâĂ©cran dâun ordinateur ou une image calculĂ©e pour lâimpression. Il est considĂ©rĂ© comme lâun des premiers algorithmes dĂ©couverts dans le domaine de la synthĂšse d'image [1].
Utilisations
Le principe du calcul est largement documentĂ© et a depuis Ă©tĂ© repris pour tracer de façon incrĂ©mentale nâimporte quelle courbe conique (cercle, ellipse, arc, parabole, hyperbole) ou courbes de BĂ©zier grĂące aux propriĂ©tĂ©s de leur fonction polynomiale de dĂ©finition, dont les dĂ©rivĂ©es permettent de calculer les orientations de segments Ă©lĂ©mentaires avec de simples opĂ©rations entiĂšres. On peut mĂȘme lâadapter Ă lâapproximation de courbes dont on ne connaĂźt quâun dĂ©veloppement limitĂ© (dont on ne prendra que les premiers termes de faible degrĂ©), utilisable avec une bonne prĂ©cision sur un domaine suffisant par rapport Ă la rĂ©solution (sinusoĂŻdes, exponentielles, puissances non entiĂšres).
Lâalgorithme est Ă©galement facilement adaptable au calcul de courbes et surfaces dans un espace discret de plus de 2 dimensions (notamment pour le pilotage de machines-outils). MĂȘme en deux dimensions seulement, on peut discrĂ©tiser avec cet algorithme une courbe avec une fonction de lissage prenant en compte lâerreur commise entre deux points candidats afin dâajuster leur couleur, lâerreur incrĂ©mentale Ă©tant convertible en coefficient de transparence, ce qui permet de conserver la graisse (Ă©paisseur visuelle) dâune courbe tout en limitant lâeffet dâescalier (crĂ©nelage).
Cet algorithme intervient aussi dans le lissage de rendus de textures 2D appliquĂ©es sur des surfaces dâune scĂšne 3D oĂč la texture subit des rĂ©ductions ou agrandissements. On lâemploie aussi pour le lissage dâagrandissements photographiques, ou pour lâinterpolation de couleurs intermĂ©diaires sur une Ă©chelle discrĂšte.
Explication de lâalgorithme de base dans le premier octant
La ligne est tracĂ©e entre deux points (x1, y1) et (x2, y2), oĂč chaque paire indique la colonne et la rangĂ©e, respectivement, croissant vers la droite et le bas. On supposera au dĂ©part que notre segment descend vers le bas et la droite, et que la distance horizontale x2âx1 excĂšde la distance verticale y2ây1 (câest-Ă -dire que le segment a une pente comprise entre 0 et -1). Notre but est, pour chaque colonne x entre x1 et x2, dâidentifier la rangĂ©e y dans cette colonne qui est la plus proche du segment idĂ©al et de tracer un pixel en (x, y).
Détermination des ordonnées
Il s'agit maintenant de dĂ©terminer quel pixel est le plus proche de la droite pour une colonne donnĂ©e. La formule gĂ©nĂ©rale dâune droite entre les deux points est donnĂ©e par :
- . (1)
Puisque la colonne est connue, la rangĂ©e du pixel le plus proche de lâordonnĂ©e exacte du point dâabscisse sur la droite est donnĂ©e en arrondissant cette ordonnĂ©e y Ă lâentier le plus proche :
- . (2)
La valeur 0,5 dans le membre de droite est une approximation des coordonnées (le point est au milieu du carré).
Cependant, le calcul explicite de cette valeur pour chaque colonne est coĂ»teux. Or commence en , et que chaque fois qu'on ajoute 1 Ă l'abscisse, on ajoute la valeur constante Ă la valeur de lâordonnĂ©e y du point de la droite correspondant. Cette valeur est la pente de la droite, et en vertu de notre hypothĂšse initiale, elle est comprise entre 0 et -1. AprĂšs lâarrondi, pour chaque colonne , on utilise donc soit la valeur prĂ©cĂ©dente (ordonnĂ©e du pixel d'abscisse ), soit cette valeur augmentĂ©e de 1.
On peut dĂ©cider laquelle de ces deux valeurs prendre en conservant une valeur dâerreur qui reprĂ©sente la distance verticale entre la valeur courante et la valeur y exacte pour la droite Ă lâabscisse courante. Au dĂ©part cette valeur dâerreur e est nulle et chaque fois qu'on incrĂ©mente , on augmente la valeur dâerreur par la valeur de pente ci-dessus. Chaque fois que lâerreur dĂ©passe 0,5, la droite est devenue plus proche de la valeur suivante, aussi on ajoute 1 Ă en retranchant simultanĂ©ment 1,0 Ă lâerreur e.
La procédure ressemble à ceci, en supposant que tracerPixel(x, y)
est une primitive graphique traçant le pixel de rangĂ©e x et de colonne y ; exprimĂ© en pseudo-code, lâalgorithme de base est :
procĂ©dure tracerSegment(entier x1, entier y1, entier x2, entier y2) est dĂ©clarer entier x, y, dx, dy ; dĂ©clarer rationnel e, e(1,0), e(0,1) ; // valeur dâerreur et incrĂ©ments dy â y2 - y1 ; dx â x2 - x1 ; y â y1 ; // rangĂ©e initiale e â 0,0 ; // valeur dâerreur initiale e(1,0) â dy / dx ; e(0,1) â -1.0 ; pour x variant de x1 jusquâĂ x2 par incrĂ©ment de 1 faire tracerPixel(x, y) ; si (e â e + e(1,0)) â„ 0,5 alors // erreur pour le pixel suivant de mĂȘme rangĂ©e y â y + 1 ; // choisir plutĂŽt le pixel suivant dans la rangĂ©e supĂ©rieure e â e + e(0,1) ; // ajuste lâerreur commise dans cette nouvelle rangĂ©e fin si ; fin pour ; fin procĂ©dure ;
AmĂ©lioration de lâalgorithme pour le calcul avec des entiers
Le problĂšme avec cet algorithme simple est que les microprocesseurs dâordinateur sont relativement lents dans le calcul sur des nombres en virgule flottante (et la reprĂ©sentation suggĂ©rĂ©e ci-dessus sous forme de nombres rationnels pour e et e(1,0) est nettement plus complexe et non nativement prise en charge par les processeurs ce qui augmente le nombre dâinstructions pour travailler sur de tels nombres) ; de plus, les erreurs dâapproximation du nombre flottant e(1,0) sâaccumulent Ă chaque addition de e(1,0) dans e. Travailler avec uniquement des entiers permettrait un calcul Ă la fois plus exact et plus rapide.
La premiĂšre astuce est de remarquer dâabord quâon peut remplacer e par eâ0,5, ce qui permet de ne tester que le signe de la valeur dâerreur au lieu de comparer deux rationnels, le test de proximitĂ© par rapport Ă la droite exacte revient alors Ă savoir lequel des deux pixels candidats se situe en dessous de la droite exacte parallĂšle dont les ordonnĂ©es sont augmentĂ©es de 0,5, et de remplacer lâarrondi de la formule (1) ci-dessus Ă lâentier le plus proche par un arrondi Ă lâentier Ă©gal ou infĂ©rieur avec cette nouvelle droite, ce qui ne change effectivement pas la formule (2) ci-dessus, mais e reprĂ©sentera lâerreur commise lors de l'approximation de cette seconde droite par les pixels tracĂ©s :
procĂ©dure tracerSegment(entier x1, entier y1, entier x2, entier y2) est dĂ©clarer entier x, y, dx, dy ; dĂ©clarer rationnel e, e(1,0), e(0,1) ; // valeur dâerreur et incrĂ©ments dy â y2 - y1 ; dx â x2 - x1 ; y â y1 ; // rangĂ©e initiale e â -0,5 ; // valeur dâerreur initiale e(1,0) â dy / dx ; e(0,1) â -1.0 ; pour x variant de x1 jusquâĂ x2 par incrĂ©ment de 1 faire tracerPixel(x, y) ; si (e â e + e(1,0)) â„ 0 alors // erreur pour le pixel suivant de mĂȘme rangĂ©e y â y + 1 ; // choisir plutĂŽt le pixel suivant dans la rangĂ©e supĂ©rieure e â e + e(0,1) ; // ajuste lâerreur commise dans cette nouvelle rangĂ©e fin si ; fin pour ; fin procĂ©dure ;
Ensuite en multipliant tous les rationnels ci-dessus par dx, le calcul ne demande plus dâincrĂ©ments rationnels (ce qui Ă©limine l'accumulation d'erreurs dâapproximation des flottants). Cependant la valeur initiale de e= â0,5Ădx nâest pas encore entiĂšre, mĂȘme si ses incrĂ©ments sont entiers.
procĂ©dure tracerSegment(entier x1, entier y1, entier x2, entier y2) est dĂ©clarer entier x, y, dx, dy ; dĂ©clarer rationnel e ; // valeur dâerreur dĂ©clarer entier e(1,0), e(0,1) ; // incrĂ©ments dy â y2 - y1 ; dx â x2 - x1 ; y â y1 ; // rangĂ©e initiale e â -0,5 Ă dx ; // valeur dâerreur initiale e(1,0) â dy ; e(0,1) â -dx ; pour x variant de x1 jusquâĂ x2 par incrĂ©ment de 1 faire tracerPixel(x, y) ; si (e â e + e(1,0)) â„ 0 alors // erreur pour le pixel suivant de mĂȘme rangĂ©e y â y + 1 ; // choisir plutĂŽt le pixel suivant dans la rangĂ©e supĂ©rieure e â e + e(0,1) ; // ajuste lâerreur commise dans cette nouvelle rangĂ©e fin si ; fin pour ; fin procĂ©dure ;
Toutefois en doublant e (et les valeurs de ses incrĂ©ments), cela ne change rien au test de son signe : e reprĂ©sentera alors la distance par rapport Ă la droite exacte dâordonnĂ©es augmentĂ©es de 0,5, cette distance Ă©tant multipliĂ©e par le facteur constant positif 2Ădy (ce qui ne change pas le signe de la valeur dâerreur testĂ©e). La valeur de pente utilisĂ©e comme incrĂ©ment de e Ă©tant aussi multipliĂ©e par le mĂȘme facteur devient simplement 2Ădy, sa valeur initiale devient âdx, le second dĂ©crĂ©ment conditionnel devient 2Ădx (que lâon peut aussi prĂ©calculer). Lâalgorithme devient alors :
procĂ©dure tracerSegment(entier x1, entier y1, entier x2, entier y2) est dĂ©clarer entier x, y, dx, dy ; dĂ©clarer entier e ; // valeur dâerreur dĂ©clarer entier e(1,0), e(0,1) ; // incrĂ©ments dy â y2 - y1 ; dx â x2 - x1 ; y â y1 ; // rangĂ©e initiale e â -dx ; // valeur dâerreur initiale e(1,0) â dy Ă 2 ; e(0,1) â -dx Ă 2; pour x variant de x1 jusquâĂ x2 par incrĂ©ment de 1 faire tracerPixel(x, y) ; si (e â e + e(1,0)) â„ 0 alors // erreur pour le pixel suivant de mĂȘme rangĂ©e y â y + 1 ; // choisir plutĂŽt le pixel suivant dans la rangĂ©e supĂ©rieure e â e + e(0,1) ; // ajuste lâerreur commise dans cette nouvelle rangĂ©e fin si ; fin pour ; fin procĂ©dure ;
RĂ©duction des variables et simplifications
On pourra enfin changer le signe de e en testant le signe opposĂ©, puis rĂ©duire le nombre de variables, en constatant que x1 et y1 ci-dessus ne sont plus utilisĂ©s dĂšs que lâerreur initiale et les incrĂ©ments sont calculĂ©s ; il suffit de changer aussi le signe des incrĂ©ments et dĂ©crĂ©ments :
procĂ©dure tracerSegment(entier x1, entier y1, entier x2, entier y2) est dĂ©clarer entier dx, dy ; dĂ©clarer entier e ; // valeur dâerreur e â x2 - x1 ; // -e(0,1) dx â e Ă 2 ; // -e(0,1) dy â (y2 - y1) Ă 2 ; // e(1,0) tant que x1 †x2 faire tracerPixel(x1, y1) ; x1 â x1 + 1 ; // colonne du pixel suivant si (e â e - dy) †0 alors // erreur pour le pixel suivant de mĂȘme rangĂ©e y1 â y1 + 1 ; // choisir plutĂŽt le pixel suivant dans la rangĂ©e supĂ©rieure e â e + dx ; // ajuste lâerreur commise dans cette nouvelle rangĂ©e fin si ; fin faire ; // Le pixel final (x2, y2) nâest pas tracĂ©. fin procĂ©dure ;
Cet algorithme est optimal et suffisant pour tracer tout vecteur horizontal diagonal ou oblique dans le premier octant, de colonnes et rangĂ©es croissantes. Si le langage de programmation le permet, les deux variables locales dĂ©clarĂ©es x et y peuvent ĂȘtre remplacĂ©es par rĂ©utilisation des variables x1 et y1 des paramĂštres. Ce cas est traitĂ© ci-dessus.
Toutefois il faut remarquer dans lâalgorithme ci-dessus que le test du signe de e peut aussi bien inclure lâĂ©galitĂ© avec zĂ©ro ou ne pas lâinclure. Cela correspond au fait que les deux pixels suivants candidats sont Ă©quidistants de la droite exacte. Si on choisit un dĂ©placement diagonal le deuxiĂšme point suivant sera toujours obtenu par un dĂ©placement horizontal ; si on choisit un dĂ©placement horizontal le deuxiĂšme point suivant sera toujours obtenu par un dĂ©placement diagonal. Si on inversait la direction du vecteur, les pixels choisis seraient inversĂ©s donc diffĂ©rents, et on devra en tenir compte si on souhaite un recouvrement exact des pixels de deux vecteurs obliques de sens opposĂ©s, lors de la gĂ©nĂ©ralisation de lâalgorithme Ă des vecteurs obliques de directions quelconques (ce cas ne peut pas se produire pour le tracĂ© de vecteurs horizontaux, verticaux ou diagonaux).
Algorithme général optimisé
La gĂ©nĂ©ralisation de lâalgorithme de base au tracĂ© de vecteurs de direction quelconque est obtenue par simple symĂ©trie.
Lâalgorithme est ici dĂ©veloppĂ© et optimisĂ© dans chacun des huit octants. Toutefois, afin de sâassurer que les mĂȘmes pixels seront toujours tracĂ©s pour deux vecteurs identiques mais de direction opposĂ©e, on inversera les cas limites oĂč un dĂ©placement diagonal est Ă Ă©galitĂ© avec un dĂ©placement droit, en choissant la diagonale quand le vecteur est orientĂ© vers la gauche (abscisses dĂ©croissantes) plutĂŽt que vers la droite (abscisses croissantes) comme dans le cas simplifiĂ© ci-dessus :
procĂ©dure tracerSegment(entier x1, entier y1, entier x2, entier y2) est dĂ©clarer entier dx, dy; si (dx â x2 - x1) â 0 alors si dx > 0 alors si (dy â y2 - y1) â 0 alors si dy > 0 alors // vecteur oblique dans le 1er quadrant si dx â„ dy alors // vecteur diagonal ou oblique proche de lâhorizontale, dans le 1er octant dĂ©clarer entier e ; dx â (e â dx) Ă 2 ; dy â dy Ă 2 ; // e est positif boucle sans fin // dĂ©placements horizontaux tracePixel(x1, y1) ; interrompre boucle si (x1 â x1 + 1) = x2 ; si (e â e - dy) < 0 alors y1 â y1 + 1 ; // dĂ©placement diagonal e â e + dx ; fin si ; fin boucle ; sinon // vecteur oblique proche de la verticale, dans le 2d octant dĂ©clarer entier e ; dy â (e â dy) Ă 2 ; dx â dx Ă 2 ; // e est positif boucle sans fin // dĂ©placements verticaux tracePixel(x1, y1) ; interrompre boucle si (y1 â y1 + 1) = y2 ; si (e â e - dx) < 0 alors x1 â x1 + 1 ; // dĂ©placement diagonal e â e + dy ; fin si ; fin boucle ; fin si ; sinon // dy < 0 (et dx > 0) // vecteur oblique dans le 4e quadrant si dx â„ -dy alors // vecteur diagonal ou oblique proche de lâhorizontale, dans le 8e octant dĂ©clarer entier e ; dx â (e â dx) Ă 2 ; dy â dy Ă 2 ; // e est positif boucle sans fin // dĂ©placements horizontaux tracePixel(x1, y1) ; interrompre boucle si (x1 â x1 + 1) = x2 ; si (e â e + dy) < 0 alors y1 â y1 - 1 ; // dĂ©placement diagonal e â e + dx ; fin si ; fin boucle ; sinon // vecteur oblique proche de la verticale, dans le 7e octant dĂ©clarer entier e ; dy â (e â dy) Ă 2 ; dx â dx Ă 2 ; // e est nĂ©gatif boucle sans fin // dĂ©placements verticaux tracePixel(x1, y1) ; interrompre boucle si (y1 â y1 - 1) = y2 ; si (e â e + dx) > 0 alors x1 â x1 + 1 ; // dĂ©placement diagonal e â e + dy ; fin si ; fin boucle ; fin si ; fin si ; sinon // dy = 0 (et dx > 0) // vecteur horizontal vers la droite rĂ©pĂ©ter tracePixel(x1, y1) ; jusquâĂ ce que (x1 â x1 + 1) = x2 ; fin si ; sinon // dx < 0 si (dy â y2 - y1) â 0 alors si dy > 0 alors // vecteur oblique dans le 2d quadrant si -dx â„ dy alors // vecteur diagonal ou oblique proche de lâhorizontale, dans le 4e octant dĂ©clarer entier e ; dx â (e â dx) Ă 2 ; dy â dy Ă 2 ; // e est nĂ©gatif boucle sans fin // dĂ©placements horizontaux tracePixel(x1, y1) ; interrompre boucle si (x1 â x1 - 1) = x2 ; si (e â e + dy) â„ 0 alors y1 â y1 + 1 ; // dĂ©placement diagonal e â e + dx ; fin si ; fin boucle ; sinon // vecteur oblique proche de la verticale, dans le 3e octant dĂ©clarer entier e ; dy â (e â dy) Ă 2 ; dx â dx Ă 2 ; // e est positif boucle sans fin // dĂ©placements verticaux tracePixel(x1, y1) ; interrompre boucle si (y1 â y1 + 1) = y2 ; si (e â e + dx) †0 alors x1 â x1 - 1 ; // dĂ©placement diagonal e â e + dy ; fin si ; fin boucle ; fin si ; sinon // dy < 0 (et dx < 0) // vecteur oblique dans le 3e quadrant si dx †dy alors // vecteur diagonal ou oblique proche de lâhorizontale, dans le 5e octant dĂ©clarer entier e ; dx â (e â dx) Ă 2 ; dy â dy Ă 2 ; // e est nĂ©gatif boucle sans fin // dĂ©placements horizontaux tracePixel(x1, y1) ; interrompre boucle si (x1 â x1 - 1) = x2 ; si (e â e - dy) â„ 0 alors y1 â y1 - 1 ; // dĂ©placement diagonal e â e + dx ; fin si ; fin boucle ; sinon // vecteur oblique proche de la verticale, dans le 6e octant dĂ©clarer entier e ; dy â (e â dy) Ă 2 ; dx â dx Ă 2 ; // e est nĂ©gatif boucle sans fin // dĂ©placements verticaux tracePixel(x1, y1) ; interrompre boucle si (y1 â y1 - 1) = y2 ; si (e â e - dx) â„ 0 alors x1 â x1 - 1 ; // dĂ©placement diagonal e â e + dy ; fin si ; fin boucle ; fin si ; fin si ; sinon // dy = 0 (et dx < 0) // vecteur horizontal vers la gauche rĂ©pĂ©ter tracePixel(x1, y1) ; jusquâĂ ce que (x1 â x1 - 1) = x2 ; fin si ; fin si ; sinon // dx = 0 si (dy â y2 - y1) â 0 alors si dy > 0 alors // vecteur vertical croissant rĂ©pĂ©ter tracePixel(x1, y1) ; jusquâĂ ce que (y1 â y1 + 1) = y2 ; sinon // dy < 0 (et dx = 0) // vecteur vertical dĂ©croissant rĂ©pĂ©ter tracePixel(x1, y1) ; jusquâĂ ce que (y1 â y1 - 1) = y2 ; fin si ; fin si ; fin si ; // le pixel final (x2, y2) nâest pas tracĂ©. fin procĂ©dure ;
Notes :
- Ci-dessus, les affectations sont parfois regroupĂ©es au sein des expressions qui en rĂ©utilisent la valeur affectĂ©e. Si le langage utilisĂ© ne le permet pas, il suffit d'effectuer les affectations internes dans des instructions dâaffectation prĂ©alables sĂ©parĂ©es, et de relire cette variable dans lâexpression contenante.
- Les primitives graphiques tracePixel(x1, y1) sont regroupĂ©es ci-dessus pour tracer dans la boucle interne des segments horizontaux (cas du premier octant ci-dessus) ou verticaux (second octant), et peuvent ĂȘtre regroupĂ©es en un seul appel (il suffit de compter le nombre de passages dans la boucle interne, et dâeffectuer le tracĂ© de segment horizontal (ou vertical) en sortie de cette boucle.
Références
- (en) Jack E. Bresenham, « Algorithm for computer control of a digital plotter », IBM Systems Journal, vol. 4, no 1,â , p. 25â30 (DOI 10.1147/sj.41.0025, lire en ligne)