Dessiner la fractale de Mandelbrot en Python et en C/C++

Python 3, mon amour

Image d'entête

par skywodd | | Licence (voir pied de page)

Catégories : Projets | Mots clefs : C/C++ Mandelbrot Fractale Python PPM


Dans ce follow-up, je vous propose de voir ensemble deux extraits de code ayant servi de prototype à un précédent article sur la fractale de Mandelbrot. En bonus, on verra rapidement la structure d'un fichier PPM qui permet de stocker une image très facilement sans compression.

Sommaire

Bonjour à toutes et à tous !

Fractale de Mandelbrot

Fractale de Mandelbrot

Dans mon précédent article, je vous expliquai comment dessiner une fractale de Mandelbrot avec une carte Arduino / Genuino.

Le code de ce précédent article n'est pas sorti de nulle part, j'ai tout d'abord dû faire des prototypes de code sur PC pour tester le bon fonctionnement de l'algorithme. Dans cet article, je vous propose de voir ensemble ces prototypes de code.

N.B. La structure des codes ci-dessous est la même que pour l'article précédent. Je vous invite donc à lire l'article en question pour plus d'informations ;)

Dessin d'une fractale de Mandelbrot en Python

Le premier prototype de code que j'ai réalisé à était écrit en Python 3.4, mon langage de programmation préféré.

Celui-ci utilise la bibliothèque Pillow pour générer des images affichables sur ordinateur.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
"""
Générateur de fractale de Mandelbrot
"""

from PIL import Image


# Nombre de couleurs
MAX_ITERATION = 142

# Taille de l'écran
SCREEN_WIDTH = 640
SCREEN_HEIGHT = 480

# Tableaux de couleurs (142 couleurs, tirées d'un nuancier trouvé sur le web)
COLOR_TABLE = (
 0xf7df, 0xff5a, 0x07ff, 0x7ffa, 0xf7ff, 0xf7bb, 0xff38, 0xff59, 0x001f, 0x895c, 
 0xa145, 0xddd0, 0x5cf4, 0x7fe0, 0xd343, 0xfbea, 0x64bd, 0xffdb, 0xd8a7, 0x07ff, 
    0x0011, 0x0451, 0xbc21, 0xad55, 0x0320, 0xbdad, 0x8811, 0x5345, 0xfc60, 0x9999, 
    0x8800, 0xecaf, 0x8df1, 0x49f1, 0x2a69, 0x067a, 0x901a, 0xf8b2, 0x05ff, 0x6b4d, 
    0x1c9f, 0xd48e, 0xb104, 0xffde, 0x2444, 0xf81f, 0xdefb, 0xffdf, 0xfea0, 0xdd24, 
    0x8410, 0x0400, 0xafe5, 0xf7fe, 0xfb56, 0xcaeb, 0x4810, 0xfffe, 0xf731, 0xe73f, 
    0xff9e, 0x7fe0, 0xffd9, 0xaedc, 0xf410, 0xe7ff, 0xffda, 0xd69a, 0x9772, 0xfdb8, 
    0xfd0f, 0x2595, 0x867f, 0x839f, 0x7453, 0xb63b, 0xfffc, 0x07e0, 0x3666, 0xff9c, 
    0xf81f, 0x8000, 0x6675, 0x0019, 0xbaba, 0x939b, 0x3d8e, 0x7b5d, 0x07d3, 0x4e99, 
    0xc0b0, 0x18ce, 0xf7ff, 0xff3c, 0xff36, 0xfef5, 0x0010, 0xffbc, 0x8400, 0x6c64, 
    0xfd20, 0xfa20, 0xdb9a, 0xef55, 0x9fd3, 0xaf7d, 0xdb92, 0xff7a, 0xfed7, 0xcc27, 
    0xfe19, 0xdd1b, 0xb71c, 0x8010, 0xf800, 0xbc71, 0x435c, 0x8a22, 0xfc0e, 0xf52c, 
    0x2c4a, 0xffbd, 0xa285, 0xc618, 0x867d, 0x6ad9, 0x7412, 0xffdf, 0x07ef, 0x4416, 
    0xd5b1, 0x0410, 0xddfb, 0xfb08, 0x471a, 0xec1d, 0xd112, 0xf6f6, 0xffff, 0xf7be, 
    0xffe0, 0x9e66, 0x0000
)

# Alloue l'espace pour l'image de sortie
im = Image.new('RGB', (SCREEN_WIDTH, SCREEN_HEIGHT))
pixels = im.load()

# Point de départ de la fenêtre de dessin dans le plan réel / complexe de mandelbrot
start_x = -0.75
start_y = 0.0
zoom = 0.8

# Pour chaque pixel en X et en Y
for x in range(SCREEN_WIDTH):
    p_r = 1.5 * (x - SCREEN_WIDTH / 2.0) / (0.5 * zoom * SCREEN_WIDTH) + start_x

    for y in range(SCREEN_HEIGHT):
        p_i = (y - SCREEN_HEIGHT / 2.0) / (0.5 * zoom * SCREEN_HEIGHT) + start_y
        new_r = new_i = old_r = old_i = 0
        i = 0

        # Magie noir mathématique (merci Wikipedia)
        while (new_r * new_r + new_i * new_i) < 4.0 and i < MAX_ITERATION:
            old_r = new_r
            old_i = new_i
            new_r = old_r * old_r - old_i * old_i + p_r
            new_i = 2.0 * old_r * old_i + p_i
            i += 1

        # Dessine le pixel (conversion RGB565 -> RGB888)
        color = COLOR_TABLE[i]
        r = ((color >> 11) & 0x1F) << 3
        g = ((color >> 5) & 0x3F) << 2
        b = (color & 0x1F) << 3
        pixels[x, y] = (r, g, b)

# Affiche le résultat
im.show()
im.save("output.png", "png")

L'extrait de code ci-dessus est disponible en téléchargement sur cette page.

Dessin d'une fractale de Mandelbrot en C/C++

Une fois le bon fonctionnement du code validé, j'ai traduit le code Python en code C. L'image de sortie est au format PPM, dont on parlera dans le chapitre bonus.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
/**
 * Générateur de fractale de Mandelbrot
 */

#include <stdio.h>
#include <stdlib.h>


// Nombre de couleurs
const unsigned int MAX_ITERATION = 142;

// Taille de l'image de sortie
const unsigned int OUTPUT_WIDTH = 1920;
const unsigned int OUTPUT_HEIGHT = 1080;

// Point de départ de la fenêtre de dessin dans le plan réel / complexe de mandelbrot
const double START_X = -0.75;
const double START_Y = 0.0;
const double ZOOM = 0.8;

// Tableaux de couleurs (142 couleurs, tirées d'un nuancier trouvé sur le web)
const unsigned int COLOR_TABLE[] = {
    0xf7df, 0xff5a, 0x07ff, 0x7ffa, 0xf7ff, 0xf7bb, 0xff38, 0xff59, 0x001f, 0x895c, 
    0xa145, 0xddd0, 0x5cf4, 0x7fe0, 0xd343, 0xfbea, 0x64bd, 0xffdb, 0xd8a7, 0x07ff, 
    0x0011, 0x0451, 0xbc21, 0xad55, 0x0320, 0xbdad, 0x8811, 0x5345, 0xfc60, 0x9999, 
    0x8800, 0xecaf, 0x8df1, 0x49f1, 0x2a69, 0x067a, 0x901a, 0xf8b2, 0x05ff, 0x6b4d, 
    0x1c9f, 0xd48e, 0xb104, 0xffde, 0x2444, 0xf81f, 0xdefb, 0xffdf, 0xfea0, 0xdd24, 
    0x8410, 0x0400, 0xafe5, 0xf7fe, 0xfb56, 0xcaeb, 0x4810, 0xfffe, 0xf731, 0xe73f, 
    0xff9e, 0x7fe0, 0xffd9, 0xaedc, 0xf410, 0xe7ff, 0xffda, 0xd69a, 0x9772, 0xfdb8, 
    0xfd0f, 0x2595, 0x867f, 0x839f, 0x7453, 0xb63b, 0xfffc, 0x07e0, 0x3666, 0xff9c, 
    0xf81f, 0x8000, 0x6675, 0x0019, 0xbaba, 0x939b, 0x3d8e, 0x7b5d, 0x07d3, 0x4e99, 
    0xc0b0, 0x18ce, 0xf7ff, 0xff3c, 0xff36, 0xfef5, 0x0010, 0xffbc, 0x8400, 0x6c64, 
    0xfd20, 0xfa20, 0xdb9a, 0xef55, 0x9fd3, 0xaf7d, 0xdb92, 0xff7a, 0xfed7, 0xcc27, 
    0xfe19, 0xdd1b, 0xb71c, 0x8010, 0xf800, 0xbc71, 0x435c, 0x8a22, 0xfc0e, 0xf52c, 
    0x2c4a, 0xffbd, 0xa285, 0xc618, 0x867d, 0x6ad9, 0x7412, 0xffdf, 0x07ef, 0x4416, 
    0xd5b1, 0x0410, 0xddfb, 0xfb08, 0x471a, 0xec1d, 0xd112, 0xf6f6, 0xffff, 0xf7be, 
    0xffe0, 0x9e66, 0x0000
};

int main(void) {
    
    // Ouvre le fichier pour l'image de sortie
    FILE *fp = fopen("output.ppm", "wb");
    if (fp == NULL) {
        puts("Impossible d'ouvrir le ficher de sortie");
        return -2;
    }
    
    // Ecrit l'entête du fichier PPM binaire
    fprintf(fp, "P6\n%d %d\n255\n", OUTPUT_WIDTH, OUTPUT_HEIGHT);
    
    // Tableau temporaire pour une ligne de pixels
    unsigned char line_buffer[OUTPUT_WIDTH * 3];
    
    // Pour chaque pixel en Y
    for (unsigned int y = 0; y < OUTPUT_HEIGHT; ++y) {
        double p_i = (y - OUTPUT_HEIGHT / 2.0) / (0.5 * ZOOM * OUTPUT_HEIGHT) + START_Y;
        
        // Pour chaque pixel en X
        for (unsigned int x = 0; x < OUTPUT_WIDTH; ++x) {
            double p_r = 1.5 * (x - OUTPUT_WIDTH / 2.0) / (0.5 * ZOOM * OUTPUT_WIDTH) + START_X;
            double new_r = 0, new_i = 0, old_r = 0, old_i = 0;
            unsigned int i = 0;

            // Magie noir mathématique (merci Wikipedia)
            while ((new_r * new_r + new_i * new_i) < 4.0 && i < MAX_ITERATION) {
                old_r = new_r;
                old_i = new_i;
                new_r = old_r * old_r - old_i * old_i + p_r;
                new_i = 2.0 * old_r * old_i + p_i;
                ++i;
            }

            // Dessine le pixel (avec conversion RGB565 -> RGB888)
            unsigned int color = COLOR_TABLE[i];
            line_buffer[3 * x] = ((color >> 11) & 0x1F) << 3;
            line_buffer[3 * x + 1] = ((color >> 5) & 0x3F) << 2;
            line_buffer[3 * x + 2] = (color & 0x1F) << 3;
        }
        
        // Sauvegarde la ligne de pixels dans le fichier PPM
        fwrite(line_buffer, 1, sizeof(line_buffer), fp);
    }
    
    // Ferme le fichier de sortie
    fclose(fp);
    
    // All done
    return 0;
}

L'extrait de code ci-dessus est disponible en téléchargement sur cette page.

C'est à partir de cette version en C que j'ai écrit la version Arduino présentée dans mon précédent article ;)

En annexe, voici le fichier makefile permettant de compiler le code avec la commande make :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
CC=gcc
CFLAGS=-W -Wall
LDFLAGS=
EXEC=mandelbrot
SRC= mandelbrot.c
OBJ= $(SRC:.c=.o)

all: $(EXEC)

$(EXEC): $(OBJ)
    $(CC) -o $@.exe $^ $(LDFLAGS)

%.o: %.c
    $(CC) -std=c99 -o $@ -c $< $(CFLAGS)

clean:
    rm -rf *.o
    
mrproper: clean
    rm -rf $(EXEC).exe
    
run:
    @./$(EXEC).exe

L'extrait de code ci-dessus est disponible en téléchargement sur cette page.

Le fichier source et son makefile sont disponibles sous forme d'archive en téléchargement sur cette page.

Bonus : le format PPM

Je me suis retrouvé plusieurs fois par le passé face à un problème simple en apparence, mais complexe en réalité : stocker des images dans des fichiers.

Le format d'image le plus répandu est le format JPEG, suivi de prés par le format PNG. Ces deux formats ont l'avantage d'être très communs et de compresser les images à des tailles exploitables. Seulement, pour les utiliser, il faut des bibliothèques de code très complexe et beaucoup de ressources système.

Une solution assez classique en programmation embarquée consiste à utiliser le format Bitmap pour stocker les images sous une forme non compressée. C'est simple, c'est rapide et ça ne demande aucune ressource système. Le problème c'est que cela reste un format assez complexe et avec des défauts historiques incontournables (comme l'obligatoire d'avoir des multiples de quatre pour la taille en octets des lignes de pixels).

Il existe pourtant un format de fichier d'image compatible avec la plupart des logiciels de traitement d'images et extrêmement simple : le format PPM.

Le format est le suivant (chaque fin de ligne est un \n):

1
2
3
4
P6
LARGEUR HAUTEUR
255
... les valeurs de chaque pixel au format RGB888 directement en binaire (3 octets par pixel) ...
  • P6 signifie que l'on utilise le format PPM binaire,

  • LARGEUR correspond à la largeur de l'image en pixels, sous forme de texte,

  • HAUTEUR correspond à la hauteur de l'image en pixels, sous forme de texte,

  • 255 correspond à la valeur maximum de chaque composante des pixels (8 bits).

Ce format d'image est tellement simple que l'on pourrait presque éditer les images à la main avec un simple éditeur hexadécimal ;)

Conclusion

Ce follow-up est désormais terminé.

Si ce follow-up vous a plu, n'hésitez pas à le commenter sur le forum, à le diffuser sur les réseaux sociaux et à soutenir le site si cela vous fait plaisir.