Version 2025-06
Licence CC–BY–NC–ND
GENERATION DE TRACÉS
Christophe Schlick
schlick ಄ u-bordeaux • fr
L'objectif de cette feuille d'exercices est d'illustrer le principe de génération de tracés par algorithmes. Dans le vocabulaire lié à l'imagerie numérique, un tracé est une image créée par un ensemble de commandes graphiques permettant de décrire une image par le biais d'un langage de description. On ne va donc pas définir l'image pixel par pixel (comme on l'a fait dans la feuille d'exercices A) mais fournir des informations de plus haut niveau permettant à un interpréteur de construire l'image en fonction de la liste des commandes. Pour ce type de construction, on utilise également le terme d'image vectorielle par opposition à image matricielle (= la grille de pixels), mais le terme tracé (draw en anglais) s'impose de plus en plus, depuis quelques années.
L'ensemble des exercices vont mettre en oeuvre l'utilisation du langage Python pour générer ces tracés via l'écriture de code au format SVG (Scalable Vector Graphics). Nous verrons les éléments nécessaires du format SVG au fur et à mesure des exercices, mais un petit détour par un tutoriel SVG qui détaille plus longuement les notions abordées peut s'avérer très utile.
Dans le notebook 01-Jupyter.ipynb, nous avons vu le principe qui sera mis en oeuvre dans cette feuille d'exercices :
- On crée une chaîne de caractères multi-lignes contenant le code SVG du tracé à construire
- On affiche le tracé correspondant via la fonction
dp.SVGdu moduleIPython.display
Voici un rappel de l'exemple SVG présenté dans le notebook sur IPython :
import numpy as np # import du package 'numpy' avec alias 'np'
import IPython.display as dp # import du package 'Ipython.display' avec alias 'dp'
# On crée une chaîne de caractères multi-lignes contenant du code SVG
code = """
<svg viewBox='0 0 750 250' xmlns='http://www.w3.org/2000/svg'>
<g fill='#EEE' stroke='#000' stroke-width='2'>
<rect x='25' y='25' width='700' height='200' rx='25' stroke-width='4'/>
<ellipse cx='100' cy='125' rx='50' ry='75' fill='#F00'/>
<ellipse cx='210' cy='125' rx='50' ry='75' fill='#0F0'/>
<ellipse cx='320' cy='125' rx='50' ry='75' fill='#00F'/>
<ellipse cx='430' cy='125' rx='50' ry='75' fill='#0FF'/>
<ellipse cx='540' cy='125' rx='50' ry='75' fill='#F0F'/>
<ellipse cx='650' cy='125' rx='50' ry='75' fill='#FF0'/>
</g>
</svg>
"""
dp.SVG(code) # on affiche le tracé correspondant au code SVG via la fonction 'dp.SVG'
Le code précédent est appelé code SVG statique, c'est-à-dire que les commandes SVG sont écrites avec des paramètres fixes et sont insérées dans une chaîne de caractère Python. Ce mécanisme fonctionne, mais n'offre pas une grande flexibilité. Dans cette feuille d'exercices, nous allons au contraire générer du code SVG dynamique à l'aide de fonctions Python qui vont construire des commandes SVG en fonction des paramètres passés par l'utilisateur. Le principe général consiste à générer chaque commande SVG sous forme de chaînes de caractères et à les stocker dans une liste qui sera fusionnée puis insérée entre les balises <svg> et </svg> :
# fonction d'encapsulation de code SVG (taille viewbox = 640x320, mais affichage à 480 pixels de largeur)
svg = lambda code: f"""
<svg viewBox='0 0 640 320' width='480' xmlns='http://www.w3.org/2000/svg'>{''.join(code)}\n</svg>\n"""
# fonction de tracé de rectangle avec remplissage de couleur uniforme
rect = lambda x, y, w, h, fill: f"""
<rect x='{x:.2f}' y='{y:.2f}' width='{w:.2f}' height='{h:.2f}' fill='{fill}'/>"""
# fonction de tracé de disque avec bordure noire et remplissage de couleur uniforme
disk = lambda x, y, r, fill: f"""
<circle cx='{x:.2f}' cy='{y:.2f}' r='{r:.2f}' fill='{fill}' stroke='#000'/>"""
# fonction de tracé d'ellipse avec bordure noire et remplissage de couleur uniforme
oval = lambda x, y, rx, ry, fill: f"""
<ellipse cx='{x:.2f}' cy='{y:.2f}' rx='{rx:.2f}' ry='{ry:.2f}' fill='{fill}' stroke='#000'/>"""
code = [ # le code est défini par une liste d'appels de fonctions, chacune retournant du code SVG
rect(0, 0, 640, 320, '#000'), rect(20, 20, 600, 280, '#FFF'),
disk(120, 160, 60, '#F00'), oval(320, 160, 100, 120, '#0F0'), disk(520, 160, 60, '#00F')
]
print(svg(code)) # affichage du code SVG généré
#with open('test.svg','w') as file: print(svg(code), file=file) # sauvegarde du code dans un fichier SVG
#dp.SVG(svg(code)) # affichage du tracé au format SVG
#dp.display(dp.HTML(f"<center>{dp.SVG(svg(code)).data}</center>")) # idem avec centrage horizontal
Exercice B1 - Utilisation des formes de base
Ecrire les instructions Python/SVG permettant de générer les 12 images de la figure ci-dessous. Chaque image individuelle devra utiliser la balise SVG suivante, pour obtenir un tracé initial de taille 240 sur 240 (mais évidemment, comme c'est un tracé, on pourra le redimensionner de manière arbitraire) :
<svg viewBox='0 0 240 240' width='240' xmlns='http://www.w3.org/2000/svg'>
Vous aurez besoin des balises SVG suivantes :
<rect x=... y=... width=... height=... fill=... /><circle cx=... cy=... r=... fill=... /><ellipse cx=... cy=... rx=... ry=... fill=... />

W, H = 240, 240 # définition de la taille des images à générer
KW = '#000 #FFF'.split() # palette binaire (noir et blanc au format '#RGB')
RGBCMY = '#F00 #0F0 #00F #0FF #F0F #FF0'.split() # palette de 6 couleurs (RGBCMY)
# fonction d'encapsulation de code SVG (en définissant une taille d'image variable = width x height)
svg = lambda code, width=W, height=H: f"""
<svg viewBox='0 0 {width} {height}' width='{width}' xmlns='http://www.w3.org/2000/svg'>{''.join(code)}
</svg>\n"""
def stripes(rows, width, height, fills):
"""generate horizontal stripes with alternating filling colors"""
# TODO
return []
code = stripes(40, W, H, KW) # 40 lignes horizontales, alternance noir et blanc
#code = stripes(40, W, H, RGBCMY) # 40 lignes horizontales, alternance RGBCMY
def tiles(cols, rows, width, height, fills):
"""generate rectangular tiles with alternating filling colors"""
# TODO
return []
#code = tiles(8, 8, W, H, KW)
#code = tiles(8, 8, W, H, RGBCMY)
def boxes(steps, width, height, fills):
"""generate concentric boxes with alternating filling colors"""
# TODO
return []
#code = boxes(8, W, H, KW)
#code = boxes(8, W, H, RGBCMY)
#print(svg(code)) # enlever le commentaire pour voir le code SVG généré
dp.SVG(svg(code)) # affichage du tracé au format SVG
def quincunx(cols, rows, width, height, fills, pad=2):
"""generate a quincunx pattern of disks with alternating filling colors"""
# TODO
return []
code = quincunx(10, 6, W, H, ['#DDD'])
#code = quincunx(10, 6, W, H, RGBCMY[:3])
#code = quincunx(10, 6, W, H, RGBCMY[-3:], 0)
#print(svg(code)) # enlever le commentaire pour voir le code SVG généré
dp.SVG(svg(code)) # affichage du tracé au format SVG
def spiral(steps, width, height, fills):
"""create a spiral pattern of disks with alternating filling colors"""
# TODO
return []
code = spiral(300, W, H, ['#DDD'])
#code = spiral(300, W, H, KW)
#code = spiral(300, W, H, RGBCMY)
#print(svg(code)) # enlever le commentaire pour voir le code SVG généré
dp.SVG(svg(code)) # affichage du tracé au format SVG
def splats(steps, minscale, maxscale, width, height, fills):
"""generate elliptical splats with random positions/sizes/fills"""
# TODO
return []
code = splats(500, 0.03, 0.03, W, H, ['#DDD'])
#code = splats(200, 0.03, 0.12, W, H, RGBCMY)
#print(svg(code)) # enlever le commentaire pour voir le code SVG généré
dp.SVG(svg(code)) # affichage du tracé au format SVG
Exercice B2 - Dégradés, motifs et transformations
Ecrire les instructions Python permettant de générer les images de la figure ci-dessous. En plus des balises SVG et des fonctions Python de l'exercice précédent, vous aurez également besoin des balises suivantes :
<linearGradient id=... x1=... y1=... x2=... y2=...> ... </linearGradient><radialGradient id=... cx=... cy=... r=...> ... </radialGradient><stop offset=... stop-color=... /><pattern id=... width=... height=..> ... </pattern>

def lingrad(id, colors=('#000','#FFF'), box=(0,0,0,1)):
"""define a uniform linear color gradient on provided box"""
n = len(colors); x1, y1, x2, y2 = box
code = [f"\n<linearGradient id='{id}' x1='{x1}' y1='{y1}' x2='{x2}' y2='{y2}'>"]
code += [f" <stop offset='{p/(n-1)}' stop-color='{c}'/>" for p,c in enumerate(colors)]
code += ['</linearGradient>']; return '\n'.join(code)
code = []
#print(svg(code)) # enlever le commentaire pour voir le code SVG généré
dp.SVG(svg(code)) # affichage du tracé au format SVG
def radgrad(id, colors=('#FFF','#000'), disk=(0.5,0.5,0.5)):
"""define a uniform radial color gradient on provided disk"""
n = len(colors); cx, cy, r = disk
code = [f"\n<radialGradient id='{id}' cx='{cx}' cy='{cy}' r='{r}'>"]
code += [f" <stop offset='{p/(n-1)}' stop-color='{c}'/>" for p,c in enumerate(colors)]
code += ['</radialGradient>']; return '\n'.join(code)
code = []
#print(svg(code)) # enlever le commentaire pour voir le code SVG généré
dp.SVG(svg(code)) # affichage du tracé au format SVG
# fonction de création d'un motif répété 'nx' fois en horizontal, 'ny' fois en vertical
pattern = lambda id, code, nx=1, ny=1: f"""
<pattern id='{id}' width='{1/nx}' height='{1/ny}'>{''.join(code)}
</pattern>"""
code = []
#print(svg(code)) # enlever le commentaire pour voir le code SVG généré
dp.SVG(svg(code)) # affichage du tracé au format SVG
# fonction de transformation affine pour une liste de commandes SVG
transfo = lambda code, cx=0, cy=0, s=1, r=0, tx=0, ty=0: f"""
<g transform='translate({cx+tx},{cy+ty}) rotate({r}) scale({s}) translate({-cx},{-cy})'>{''.join(code)}
</g>"""
code = []
#print(svg(code)) # enlever le commentaire pour voir le code SVG généré
dp.SVG(svg(code)) # affichage du tracé au format SVG
Exercice B3 - Pixel Art
L'utilisation combinée des images et des tracés permet de bénéficier des avantages respectifs des deux modèles, et d'obtenir des résultats qu'il serait très difficile de produire en se limitant à un modèle unique. A titre d'illustration, on cherche à écrire une fonction qui transforme une image quelconque en un tracé dans lequel chaque pixel est remplacé par un disque coloré. Pour augmenter la variété des effets visuels, on rajoute une perturbation aléatoire (distribution normale) pour la position des disques, ainsi que pour leur taille, comme illustré sur la figure ci-dessous :
# TODO