Un Explorateur de fractales en Python
Plongez dans des mondes visuels éblouissants créés à partir de simples équations mathématiques, les fractales. Découvrez des fractales emblématiques telles que Julia et Mandelbrot, personnalisez votre expérience avec des palettes de couleurs. Embarquez pour un voyage captivant au cœur de l’art mathématique !
Les prémices du projet
Tout débute durant mon année de seconde. En math, j’ai utilisé le script Mandelbrot initialement présent dans la calculatrice Numworks. Je me suis amusé à analyser le script et grâce à ça et à mon père, je suis tombé dans les fractales.
Ensuite, j’ai commencé la NSI en première et j’ai réalisé mon premier tutoriel sur ledit script. Et en fin d’année, j’ai modifié ce script afin de faire de plus belles couleurs.
Cette année, j’ai eu envie d’aller bien plus loin, l’idée vient du fait que pour réaliser de belles images de fractale, j’utilise des explorateurs de fractales trouvables facilement sur internet pour trouver des coordonnées intéressantes. Mais l’expression le dit bien, on n’est jamais mieux servi que par soi-même, et j’ai donc choisi pour mon dernier projet de lycée de faire mon propre explorateur de fractale.
Le début du projet
Je suis parti du script Julia que j’avais fait durant mon projet de fin d’année de première, vous pourrez retrouver l’explication du script ici (lien vers le tuto Julia palette). Ainsi, nous partons de ce script :
from kandinsky import* def julia(N_iteration): palette = [] xmax = 2 xmin = -2 ymax = 1.3875 ymin = -1.387 r = 255 g = 255 b = 255 for j in range(0,128): b = 255 -2 * j palette.append([r,g,b]) for j in range(128,256): r = 255 -2 * (j-128) g = 255 -2 * (j-128) palette.append([r,g,b]) for x in range(320): for y in range(222): i = 0 z = complex(xmin+(xmax-xmin)*x/320+(ymax-(ymax-ymin)*y/222)*1J) c = complex(0.36,0.36) while i < N_iteration and abs(z) < 2: i = i + 1 z = z*z+c couleur = palette[int(255*i/N_iteration)] col = color(couleur[0],couleur[1],couleur[2]) set_pixel(x,y,col)
La logique
I – optimisation
Pour faire mon explorateur, je veux générer une nouvelle fractale à chaque zoom, dézoome, déplacement, sur la droite, la gauche, le haut, le bas en modifiant les valeurs des coins de l’image. Mais si je suis cette logique, le script Julia pose déjà un problème. En effet, le script met des dizaines de secondes à fabriquer une seule image, vous conviendrez qu’un zoom en deux minutes, c’est un peu lent non ? Étrangement, un tutoriel a été écrit durant les vacances de Noël 2023 et bizarrement ce script présente exactement ce dont j’ai besoin ici.
Ainsi, j’ai un script qui me permet de générer des images en .png de fractale rapidement. Maintenant, j’ai remplacé la bibliothèque Kandinsky par Pillow et j’ai entièrement modifié les calculs de la fractale pour utiliser NumPy. Nous avons donc ce script :
from PIL import Image import time import tkinter as tk from tkinter import PhotoImage import keyboard import numpy as np global xmax, xmin, ymax, ymin, réso_x, réso_y #valeurs de base xmax = 2 xmin = -2 ymax = 1.3875 ymin = -1.387 réso_x = 30720 réso_y = 21312 def julia_PIL(N_iteration = 100): palette = [] r = 255 g = 255 b = 255 for j in range(0, 128): b = 255 - 2 * j palette.append((r, g, b)) for j in range(128, 256): r = 255 - 2 * (j - 128) g = 255 - 2 * (j - 128) palette.append((r, g, b)) palette = np.array(palette, dtype=np.uint8) # Créer une grille de coordonnées complexes x = np.linspace(xmin, xmax, réso_x) y = np.linspace(ymin, ymax, réso_y) X, Y = np.meshgrid(x, y) Z = X + 1J * Y # Initialiser une matrice d'indices pour la palette indices_matrix = np.zeros((réso_y, réso_x), dtype=int) for i in range(N_iteration): print(i) # Mettre à jour les pixels mask = np.logical_and(i < N_iteration, np.abs(Z) < 2) indices_matrix[mask] = np.round(255 * i / N_iteration).astype(int) # Mettre à jour Z pour les pixels actifs Z[mask] = Z[mask] ** 2 + complex(0.36, 0.36) # Créer une image à partir de la matrice d'indices img_array = palette[indices_matrix] # Créer une image PIL à partir de l'array img = Image.fromarray(img_array, 'RGB') # Sauvegarder l'image img.save('fractales_images/test.png')
II – Affichage de l’image
Pour afficher l’image, j’utilise la fonction Pillow mixé à Tkinter. Pillow génère une image en .png et l’enregistre dans un dossier et à chaque nouvelle génération la nouvelle image remplace l’ancienne.
Tkinter lui génère une fenêtre de la même résolution que l’image générée par Pillow. Ensuite, il va chercher dans le dossier cette image faite par Pillow et l’affiche dans la fenêtre précédemment créée.
def fractal_builder(N_iteration = 100,nom_img='Explorer_image/Image.png'): """ script de génération de la fractale """ # Créer une image à partir de la matrice des indices img_array = palette[indices_matrix] # Créer une image à partir de l'array img = Image.fromarray(img_array, 'RGB') # Sauvegarder l'image img.save(nom_img) def afficher_image(): img = PhotoImage(file=chemin_image) # stocke l'image dans la variable img canvas.create_image(0, 0, anchor=tk.NW, image=img) #affiche l'image
III – déplacement et zoom
Afin de me déplacer et de zoomer, je vais utiliser la bibliothèque keyboard. Elle me permet de détecter quand j’appuie sur les touches de mon clavier. Je crée donc une fonction “explorer” qui s’occupe des déplacements et de leur logique. Je vais ainsi modifier les valeurs des coordonnées des coins de l’image que je prends de la fractale puis la générer avec ses nouvelles valeurs dans la fenêtre de Tkinter.
def explorer(action): global xmax, xmin, ymax, ymin,img,nbr_img x = xmax - xmin y = ymax - ymin if keyboard.is_pressed('up'):# zoom avant xmax = xmax - (x/20) xmin = xmin - (x/20)*-1 ymax = ymax - (y/20) ymin = ymin - (y/20)*-1 fractal_builder() afficher_image() elif keyboard.is_pressed('down'):# zoom arrière xmax = xmax + (x/20) xmin = xmin + (x/20)*-1 ymax = ymax + (y/20) ymin = ymin + (y/20)*-1 fractal_builder() afficher_image() elif keyboard.is_pressed('z'): # déplacement haut ymax = ymax - (y/40) ymin = ymin - (y/40) fractal_builder() afficher_image() elif keyboard.is_pressed('s'): # déplacement bas ymax = ymax + (y/40) ymin = ymin + (y/40) fractal_builder() afficher_image() elif keyboard.is_pressed('q'): # déplacement gauche xmax = xmax - (x/40) xmin = xmin - (x/40) fractal_builder() afficher_image() elif keyboard.is_pressed('d'): # déplacement droite xmax = xmax + (x/40) xmin = xmin + (x/40) fractal_builder() afficher_image()
IV – Menu, accueil
Au début du projet, ma volonté était qu’à la fin, j’ai réussi à faire un explorateur de fractaleS. Pouvoir explorer des fractales comme Julia et Mandelbrot que je maîtrise déjà et peut-être même d’autres.
Mais pour faire ça il faut que l’utilisateur puisse choisir dans un menu quelle fractale veut-il explorer et faire un menu, quand j’ai commencé le projet été pour moi le plus dur à réaliser.
Durant les vacances de février 2024 il nous a été demandé de faire un gestionnaire de base de données, d’ailleurs voici ce que j’ai réalisé (lien vers le projet). Mais quel rapport avec des fractales ? Eh bien le fait que ce projet utilise Tkinter pour réaliser un menu, exactement ce que cherche à faire et surtout, c’est simple ! (même si honnêtement, on ne dirait pas).
Parallèlement à ce projet de base de données, j’ai donc piqué des bouts de mon code pour le mettre dans mon explorateur et en seulement quelques minutes, j’ai un menu ! Il ne sert à rien et il est moche (et il le restera, je suis pas designer).
Faire une fenêtre avec Tkinter est plutôt simple à comprendre, on crée des variables qui vont être chaque élément de la page, texte, menu déroulant, champs pour écrire, etc. Pour les construire, Tkinter nous offre des fonctions comme Label() pour les textes, Entry() pour les champs d’écritures entre autres. Il ne nous reste qu’à trouver sur internet les paramètres à mettre à l’intérieur (comme text = “le texte que l’on veut afficher” pour un texte).
titre_acceuil = tk.Label(cadre_acceuil, text="Bienvenue sur cet explorateur de fractales", font=("Consolas", 20), bg="#C2C2C2", fg="black") soustitre_acceuil = tk.Label(cadre_acceuil, text="Choisissez les paramètres de votre fractale", font=("Consolas", 15), bg="#C2C2C2", fg="black") titre_acceuil.pack(pady=10) soustitre_acceuil.place(x=130,y=50) #création de la liste déroulante de choix des palettes desc_liste_deroul_palette = tk.Label(cadre_acceuil, text="Choisissez la palette de couleur que vous voulez utiliser", font=("Consolas", 12), bg="#C2C2C2", fg="black") desc_liste_deroul_palette.place(x=15,y=100) liste_choix = ["Blanc - Jaune - Noir","Blanc - Noir"] # élément de la liste déroulante variable_palette = tk.StringVar() # élément initial variable_palette.set("Blanc - Noir") liste_deroulante_palette = ttk.Combobox(cadre_acceuil, textvariable=variable_palette, values=liste_choix) liste_deroulante_palette.place(x=15,y=125) select = liste_deroulante_palette.get() # sélection de l'élément choisi dans la liste #création de la liste déroulante de choix de la fractale desc_liste_deroul_fractale = tk.Label(cadre_acceuil, text="Choisissez la fractale que vous voulez voir", font=("Consolas", 12), bg="#C2C2C2", fg="black") desc_liste_deroul_fractale.place(x=15,y=145) liste_choix = ["Julia","Mandelbrot"] # élément de la liste déroulante variable_fractal = tk.StringVar() # élément intiaux variable_fractal.set("Julia") liste_deroulante_fractal = ttk.Combobox(cadre_acceuil, textvariable=variable_fractal, values=liste_choix) liste_deroulante_fractal.place(x=15,y=170) # création du champ de texte pour choisir la valeur de c desc1_chmp_val_julia = tk.Label(cadre_acceuil, text="Si vous avez choisi de générer une fractale de Julia :", font=("Consolas", 12), bg="#C2C2C2", fg="black") desc2_chmp_val_julia = tk.Label(cadre_acceuil, text="Choisissez la valeur de la constante c (où laisser par défaut), sachant c est un complexe", font=("Consolas", 10), bg="#C2C2C2", fg="black") desc1_chmp_val_julia.place(x=15,y=195) desc2_chmp_val_julia.place(x=25,y=220) variable_r = tk.StringVar() variable_r.set("0.36") chmp_str_c_r_julia = tk.Entry(cadre_acceuil, textvariable=variable_r, font=("Helvetica",12), bg="#ffffff", fg="black", width=5) chmp_str_c_r_julia.place(x=40,y=245) variable_i = tk.StringVar() variable_i.set("0.36") chmp_str_c_i_julia = tk.Entry(cadre_acceuil, textvariable=variable_i, font=("Helvetica",12), bg="#ffffff", fg="black", width=5) chmp_str_c_i_julia.place(x=100,y=245) #création de la liste déroulante de choix de la résolution desc_liste_deroul_resolution = tk.Label(cadre_acceuil, text="Choisissez la résolution de l'explorateurr", font=("Consolas", 12), bg="#C2C2C2", fg="black") desc_liste_deroul_resolution.place(x=15,y=270) liste_choix = ["320x222","480x333","720x555"] variable_resolution = tk.StringVar() variable_resolution.set("320x222") liste_deroulante_resolution = ttk.Combobox(cadre_acceuil, textvariable=variable_resolution, values=liste_choix) liste_deroulante_resolution.place(x=15,y=295)
V – Menu, faire devenir utile l’accueil
Maintenant qu’on a un superbe menu, rendons le utile. Pour cela, je crée des fonctions pour sélectionner la palette, la résolution, la fractale etc que j’affecte à une grande fonction start que j’affecte elle au bouton qui lance l’explorateur. Les fonctions de sélection récupèrent ce qui est écrit dans les champs de textes, menu déroulant pour pouvoir donner c’est paramètre à la fonction mère, start().
start (), elle initialise toutes les variables utiles à l’explorateur, supprime la fenêtre du menu, crée la fenêtre de l’explorateur, initialise l’explorateur avec la première image et lance les dernières fonctions nécessaires comme explorer().
def selection_reso(): # fonction de sélection de la résolution utilisée global réso_x,réso_y if liste_deroulante_resolution.get() == "320x222": réso_x = 320 réso_y = 222 if liste_deroulante_resolution.get() == "480x333": réso_x = 480 réso_y = 333 if liste_deroulante_resolution.get() == "720x555": réso_x = 720 réso_y = 555 def selection_palette(): # fonction de sélection de la palette utilisée global palette palette = [] select = liste_deroulante_palette.get() if select == "Blanc - Jaune - Noir": r, g, b = 255, 255, 255 for j in range(0, 128): b = 255 - 2 * j palette.append((r, g, b)) for j in range(128, 256): r = 255 - 2 * (j - 128) g = 255 - 2 * (j - 128) palette.append((r, g, b)) else : palette = [[i,i,i] for i in range(255)] def start(): # fonction de lancement de l'exploration global img,chemin_image,canvas,choix_fract,compl_r,compl_i, réso_x, réso_y selection_palette() # sélèction de la fractale if liste_deroulante_fractal.get() == "Julia": choix_fract = 0 if liste_deroulante_fractal.get() == "Mandelbrot": choix_fract = 1 selection_reso() compl_r,compl_i = chmp_str_c_r_julia.get(),chmp_str_c_i_julia.get() # sélection des valeurs de c fenetre.destroy() #supprime la fenêtre de l'accueil cadre_acceuil_explo = tk.Tk() # crée la fenètre de l'explorateur cadre_acceuil_explo.title("Affichage d'une Image") chemin_image = "Explorer_image/Image.png" # Charge l'image img = PhotoImage(file=chemin_image) # Crée un widget Canvas pour afficher l'image canvas = tk.Canvas(cadre_acceuil_explo, width=réso_x, height=réso_y) canvas.pack() # initialisation de la 1ère fractale fractal_builder() afficher_image() cadre_acceuil_explo.mainloop() # création et placement du bouton de lancement de l'explorateur bouton_start_explo = tk.Button(cadre_acceuil, text="Commencer l'exploration", font=("Consolas",15), bg="white", fg="black", command = start) bouton_start_explo.place(x=230,y=495)
VI – Menu, prévisualisation
Actuellement, j’ai donc un menu qui me permet d’explorer 2 fractales différentes avec x palette différente et une infinité de fractales de Julia. Mais pendant que je joue avec mon script, je me rends compte que c’est lourd de devoir mettre les paramètres, lancer l’exploration, se rendre compte que ce n’est pas ce qu’on veut, donc on relance le script et ainsi de suite… Une idée me vient alors en tête : “pouvoir prévisualiser la fractale qu’on génère dans le menu, ce serait bien non ?” et voilà un nouvel objectif et pas des moindres, il me demande de mettre plusieurs variables en global pour y accéder, d’utiliser les fonctions de sélection, etc.
Pour réaliser cette fonction, on fait la même chose que la fonction start(). Mais cette fois, on ne supprime ni ne crée aucune fenêtre, on vient juste placer un canvas, l’image de la fractale que nous voulons prévisualiser au bon endroit avec les bons paramètres.
def previsu(): global palette, choix_fract,compl_r,compl_i, réso_x, réso_y #sélèction des diffèrents paramètres réso_x, réso_y = 240,167 selection_palette() if liste_deroulante_fractal.get() == "Julia": choix_fract = 0 if liste_deroulante_fractal.get() == "Mandelbrot": choix_fract = 1 compl_r,compl_i = chmp_str_c_r_julia.get(),chmp_str_c_i_julia.get() fractal_builder() # génération d'une fractale avec ces paramètres prévi = PhotoImage(file="Explorer_image/Image.png") # récupération de l'image # création du cardre et placement de l'image dans le cadre label_image = tk.Label(cadre_acceuil, image=prévi) label_image.place(x=245,y=310) label_image.mainloop()
VII – Menu, tutoriel
La dernière barrière entre mon explorateur et l’utilisateur est ainsi ma logique. En effet, les touches que j’ai choisies pour mon explorateur ne sont sûrement pas les meilleures, donc je rajoute une nouvelle page à mon menu pour y écrire un petit guide des touches. Je profite de ça pour ajouter deux nouvelles fonctions. La première fonction récupère les coordonnées de la fractale qu’on voit pour les nerds. La seconde, plus complexe, prend des “screenshot” de ce qu’on voit dans l’explorateur. En réalité, elle génère une fractale avec les mêmes paramètres que celle que l’on voit dans l’explorateur, mais change son nom au moment de l’enregistrer.
#création du cadre de tutoriel dans la fenêtre de l'accueil cadre_acceuil = tk.Frame(fenetre, bg="#C2C2C2",heigh = 555,width=720) cadre_acceuil.pack_propagate(False) cadre_tuto = tk.Frame(fenetre, bg="#C2C2C2",heigh = 555,width=720) cadre_tuto.pack_propagate(False) # création et placement du texte du tuto titre_tuto = tk.Label(cadre_tuto, text="Guide des Touches", font=("Consolas", 20), bg="#C2C2C2", fg="black") soustitre_tuto = tk.Label(cadre_tuto, text="Vous vous sentez un peu perdu ?", font=("Consolas", 15), bg="#C2C2C2", fg="black") titre_tuto.pack() soustitre_tuto.pack(pady=10) texte_tuto = tk.Label(cadre_tuto, text="z - aller vers le haut \ns - aller vers le bas \nq - aller à droite \nd - aller à gauche \n\nflèche du haut - zoom avant \nflèche du bas - zoom arrière \n\nm - renvoie les coordonnées de \n où vous êtes dans la fractale \n\nc - sauvegarde un PNG de ce que \n vous voyez dans le dossier Explorer_image \n", font=("Consolas", 13), bg="#C2C2C2", fg="black") texte_tuto.pack() fin = tk.Label(cadre_tuto, text="Amusez-vous !", font=("Consolas", 15), bg="#C2C2C2", fg="black") fin.pack(pady=20) #nouvelles fonctions ajoutées à l'explorateur def explorer(action): global xmax, xmin, ymax, ymin,img,nbr_img x = xmax - xmin y = ymax - ymin """ différentes fonctions de déplacements """ elif keyboard.is_pressed('m'): # récupération de nos coordonnées (pour les nerds) print("xmax = ",xmax) print("xmin = ",xmin) print("ymax = ",ymax) print("ymin = ",ymin) elif keyboard.is_pressed('c'): # prend un screenshot de la fractale print("Image enregistrée !") nbr_img +=1 fractal_builder(100,'Explorer_image/Image_saved'+str(nbr_img)+'.png') afficher_image() print(nbr_img)
Conclusion
En conclusion, ce projet ma permis d’utiliser la plupart de mes compétences. Il m’a suivi durant la moitié de mon année de terminale.
Galerie
Quelques exemples de fractale que vous pourrez à votre tour explorer :
Script complet
Voici le script final en .7z, il vous suffit de l’extraire et vous pourrez directement commencer à explorer !
vous pouvez également voir les futures mise à jour sur GitHub ici.
Étudiant en spécialité NSI en classe de 1ère en 2022,
Streamer occasionnel.