[python] Albero binario in Python

Riporto qui un classico dell’informatica di base: la realizzazione di un albero binario per l’ordinamento.

Quello dell’albero binario è un algoritmo che consente di ordinare dei valori in base al criterio di inserimento e successivamente di lettura.

Per far capire meglio il procedimento vediamo anzitutto come funziona.

Immaginiamo di avere la sequenza di numeri 3, 4, 1, 7, 6, 5, 8, 11, 9. L’algoritmo prevede la creazione di una struttura ad albero, composta di nodi, dove ogni nodo contiene un valore e un ramo di sinistra e uno di destra, che puntano rispettivamente al valore minore e maggiore, rispetto al valore nel nodo. La creazione dell’albero procederà quindi come nel video soprastante.

La lettura avverrà invece a partire dal primo nodo, procedendo anzitutto sul nodo di sinistra, poi stampando il valore del nodo corrente e poi procedendo sul nodo di destra. Come illustrato nella seguente animazione:

 

Procedendo in questo modo otterremo tutti i valori ordinati.

Questo algoritmo può essere scritto in Python nel modo seguente:

 

Vedi articolo

[python] Semplice simulazione dell’espansione di un contagio tipo COVID19 con PyGame e OpenGL

Ripropongo un esercizio in Python simile a quello svolto in JavaScript in Simulazione sull’espansione del contagio da Coronavirus (COVID-19)

Questa volta utilizzeremo PyGame e OpenGL per costruire la simulazione, similmente a quanto fatto nel precedente articolo. Il programma è stato svolto in Python 3.9.

Il risultato produrrà qualcosa di simile a questo:

Vedi articolo

[python] Calcolare la dimensione delle cartelle sul computer e trovare quelle più grandi

Ecco un veloce script in python per ricavare la dimensione delle cartelle sul computer (vale sia per Windows che Mac e Linux) e stampare l’elenco ordinato (dalla più grande alla più piccola) di tutte quelle trovate sopra una certa dimensione.

A me è stato molto utile per svuotare C: che si era riempito di cartelle temporanee, di cache e cose totalmente dimenticate.

Lo script è stato scritto per Python 3.9

L’ouput finale genera qualcosa di simile (nel mio caso vedo che le macchine virtuali di Android occupano da sole 24GB!)

Faccio notare che nella stampa finale ho ridotto l’elenco a cartelle maggiori di 200MB.

Vedi articolo

[python] Semplice esercizio per riconoscere immagini di frutti con TensorFlow (machine learning)

Vogliamo realizzare un semplice programma in Python che sia in grado di riconoscere, utilizzando TensorFlow e machine learning, delle immagini di frutti.

Questo esercizio riprende i medesimi concetti già visti in: [python] Semplice esercizio su TensorFlow e il riconoscimento delle immagini nel gioco del Tris (Machine Learning)

Dato il seguente set di immagini (qui unite in una singola), vogliamo addestrare il programma a riuscire a riconoscere correttamente i frutti rappresentati:

Per farlo addestreremo il programma con altre immagini di training, suddivise rispettivamente in mele (che forse assomigliano più a dei pomodori), pere e banane:

 

 

 

 

 

Il risultato che vogliamo ottenere analizzando la prima immagine sarà qualcosa del genere (abbiamo tagliato l’immagine in tutte le sottoparti):

I tre numeri rappresentano la percentuale di probabilità per cui l’immagine sia, rispettivamente, una mela, una pera o una banana. Questo significa che la prima immagine è al 100% di probabilità una pera, mentre la seconda una banana ecc.

Il progetto si sviluppa in Python 3.7. Prima di procedere assicuriamoci di aver installato tutte le librerie necessarie, in particolare:

A questo punto prepariamo le immagini per il training ed il test. Attraverso le immagini di training alleneremo il nostro algoritmo, mentre useremo quelle per il test per verificare l’efficacia. Sottolineo ancora una volta come questo sia un esempio semplificato al massimo, utilizzando immagini semplici per non dover produrre una grande mole di dati sia per il training che per il test.

Per preparare le immagini dobbiamo tagliare i quattro file precedentemente preparati. A tale scopo creiamo una classe per elaborare le immagini nel modo seguente (nei commenti c’è la descrizione delle singole operazioni):

Per elaborare i due gruppi di immagini ci sarà sufficiente chiamare:

A questo punto creiamo una classe per elaborare questa base dati, nel modo seguente:

Fatto tutto questo possiamo anzitutto costruire il nostro modello, che andremo a salvare, nella stessa cartella dell’eseguibile come modello_frutta.

Per creare il modello anzitutto preleviamo le immagini con:

Costruiamo poi il nostro modello:

I tre layer servono rispettivamente per:

  1. keras.layers.Flatten(input_shape=(100,100,3)) ci permette di ridurre ad una dimensione la matrice tridimensionale delle immagini (100 righe x 100 colonne x 3 byte di colore) contenente 30.000 byte. Per farlo dobbiamo ricordarci lo shape dei dati passati in input, che provengono da una lista, contenente a sua volte una lista tridimensionale.
  2. keras.layers.Dense(128, activation="relu") applica l’algoritmo relu ai dati ottenuti dal primo layer, su 128 nodi (il numero è arbitrario, scelto per via principalmente sperimentale)
  3. keras.layers.Dense(3, activation="softmax") applichiamo l’algoritmo softmax per ridurre tutte le informazioni a 3 nodi, rappresentati i 3 dati di output che vogliamo ottenere.

Aggiungo una nota per comprendere meglio il passaggio dei layer intermedi. Il primo layer è necessario per ridurre i dati in input ad una forma univoca, mentre l’ultimo layer serve a portare in output i dati che passiamo come “descrizione”, ovvero output conosciuto, durante il training. I layer intermedi invece possono essere molteplici, e servono per manipolare i diversi aspetti dell’informazione, tentando di ridurla ad una schematizzazione ricorrente. Per capire meglio questo processo immaginiamo di voler interpretare un’immagine (non succede la medesima cosa, ma l’esempio descrive bene il concetto generale).

Se avessimo un immagine come questa di seguito, avremmo a che fare con un’enorme quantità di dettagli diversi da analizzare. Essendo l’immagine grande 1200 x 761 pixel, potremmo dire di aver bisogno di 1200×761 = 913.200 nodi per interpretare ogni informazione singolarmente, quindi potremmo decidere di utilizzare un layer con 913.200 nodi (o neuroni). Questo vorrebbe dire che diamo importanza ad OGNI singolo nodo e quindi ad OGNI singolo dettaglio dell’informazione. Laddove volessimo confrontare immagini diverse sarebbe molto complesso cercare di trovare un’affinità tra i singoli nodi e quindi un percorso che riconduca al medesimo output desiderato.

Albania Sud | Vagabondo

Dello stesso posto potremmo avere ad esempio la seguente immagine:

Turismo dentale Albania | EliteDental

Le due immagini risulterebbero, per il computer, prese tali e quali, fondamentalmente diverse. Adesso proviamo a ridurre il dettaglio dei pixel, nel modo seguente:

Adesso possiamo identificare, nelle immagini divese, zone analoghe associabili per forma e colore tra di loro (nonostante le differenze ancora esistenti). In questo caso abbiamo ridotto le immagini in quadrati da 36×36 pixel ciascuno, quindi in totale abbiamo ridotto il tutto a circa 700 nodi. Ovviamente si è ridotta la complessità e di conseguenza il livello di dettaglio. Potremo apprezzare meno dettagli, ma confrontare meglio i macro-elementi presenti in entrambe le immagini.

Detto tutto questo compiliamo il nostro modello:

E infine compiliamolo e salviamolo:

Per usare il modello salvato lo possiamo caricare usando:

A questo punto carichiamo le immagini di TEST e mettiamo a prova il nostro modello:

Adesso facciamo prevedere al modello i risultati:

Infine stampiamoli a video per ottenere il risultato iniziale:

Riporto anche tutto il codice insieme:

Qui il progetto con le immagini scaricabile in formato zip.

Vedi articolo

[python] Semplice esercizio per creare il gioco del campo minato in Python (da console)

In questo semplice esercizio vogliamo riprodurre in Python, in modo un po’ rudimentale, quello che era il gioco del campo minato (poi campo fiorito) per Windows.

Quello che vogliamo ottenere assomiglierà a questo all’avvio:

Mentre alla vittoria otterremo qualcosa di simile:

Il gioco sarà costruito su una griglia 10×10, di 100 caselle in totale, che faremo selezionare all’utente con un numero da 1 a 100.

Anzitutto costruiamo la griglia fatta di oggetti di tipo Casella, definiti nel modo seguente:

Per costruire il campo useremo la funzione random.randint() per distribuire le mine in modo casuale, nel modo seguente:

In questo caso la variabile difficolta viene usata per bilanciare la distribuzione casuale. Se impostiamo difficolta su 2, avremo in media il 50% di caselle con mine (1/2). Se impostiamo difficolta su 10 allora avremo circa il 10% di mine (1/10), ecc.

Fatto questo dobbiamo costruire due metodi che si occuperanno rispettivamente di conteggiare le mine e aprire le celle. All’interno di entrambi i metodi useremo due cicli for ricordandoci che rispetto alla posizione attuale quelle intorno partiranno da [-1,-1] fino a [1,1]

Riporto di seguito il codice, commentato, dell’intero giochino realizzato a partire da queste idee:

 

Vedi articolo

[python] Semplice esercizio su TensorFlow e il riconoscimento delle immagini nel gioco del Tris (Machine Learning)

Vogliamo realizzare un semplice programma in Python che sfrutti il riconoscimento delle immagini mediante TensorFlow e machine learning.

Il programma consentirà all’utente di pescare il risultato di una partita a Tris, da una cartella di immagini come la seguente:

Selezionando una partita il programma ci dirà chi ritiene che sia il vincitore e mostrerà lo schema della partita che ha dedotto a partire dall’immagine che gli abbiamo passato, nella maniera seguente:

1. Configurazione iniziale e librerie utilizzate

Per affrontare l’esercizio in questione abbiamo bisogno di alcune librerie. Il progetto verrà sviluppato in Python 3.7.

Possiamo installare le opportune librerie utilizzando PIP, nel caso specifico avremo bisogno delle seguenti installazioni:

Creiamo un file elaboratore.py nel quale svilupperemo l’intero programma. All’inizio inseriamo le seguenti istruzioni di importazione:

Faccio notare che la prima istruzione from __future__ import absolute_import, division, print_function, unicode_literals deve trovarsi all’inizio e non è una vera e propria importazione. Si tratta di una configurazione sul funzionamento di Python, che importa “funzionalità future” dal modulo fittizio __future__. E’ un modo per rendere disponibili, nella versione attuale, funzionalità previste in versioni successive, ma che ancora non sono state ufficialmente implementate.

Nel nostro caso specifico questa importazione non è essenziale (stiamo già lavorando in Python 3.7 e la funzione print non è più uno statement, bensì una funzione appunto, ecc), ma la possiamo lasciare per rendere compatibile il programma anche con versioni precedenti.

Detto questo procediamo allo sviluppo del necessario.

2. Dati iniziali

Per sviluppare il gioco utilizzeremo i seguenti set di dati (qui l’allegato da cui scaricarli):

  1. Set delle immagini per l’addestramento del modello, ricaveremo le immagini da un’unica grande immagine originale chiamata training.jpg (questa è un’immagine contenente 144 disegni, metà sono O e l’altra metà sono X, disegnate a mano sul computer, ciascun elemento è grande 100×100 px, per un totale di 1200×1200 px, ovvero 12 righe x 12 colonne)

  2. Piccolo set di immagini di test nel file test.jpg (solita dimensione 100×100 px per ciascuna immagine, per un totale di 4×4 = 16 elementi)
  3. Archivio con 10 partite giocate a Tris di cui conosciamo il risultato e vogliamo farlo riconoscere al computer

3. Struttura del programma

Vogliamo creare 3 classi che si occuperanno di diversi aspetti del programma:

  1. ElaboraImmagini – con questa classe elaboreremo le immagini di test, di addestramento e delle singole partite. In tutti e tre i casi si tratta di suddividere un’immagine originale in immagini più piccole; nei primi due casi, quello di test e addestramento, salveremo le immagini in una cartella da cui leggerle successivamente, mentre nel terzo caso, quello delle immagini che compongono una partita, salveremo il risultato in un vettore
  2. CreaDBImmagini – con questa classe vogliamo elaborare le immagini di test e di addestramento salvate nelle rispettive cartelle, ottenendo dei vettori contenenti le immagini medesime e un elenco di output (o etichette) associati a ciascuna immagine
  3. GiocoTris – la vera e propria classe che si occuperà dello sviluppo del gioco, preparando il modello di apprendimento, salvandolo e valutando il punteggio su ciascuna partita che verrà passata dal giocatore

Infine metteremo tutto in un semplice ciclo while che permetterà all’utente di scegliere un’immagine dall’elenco di quelle disponibili e farla analizzare.

4. Elaboriamo le immagini di addestramento e test

Creiamo una classe nella maniera seguente (nel codice ho inserito il commento sui singoli passaggi):

Il metodo ritaglia(s, coor, i) accetta coordinate in (x,y) sull’immagine, le x rappresentano le “colonne”, mentre le y rappresentano le “righe”, nella maniera seguente:

La prima immagine verrà ritagliata con coordinate (0, 0, 100, 100), la seconda con (100, 0, 200, 100) ecc.

Possiamo subito elaborare le immagini di addestramento e di test aggiungendo le seguenti due righe di codice:

5. Preleviamo le immagini di addestramento e test, associandoci gli opportuni output

Adesso vogliamo creare la classe che ci consentirà di prelevare le immagini, inserendole in un vettore, ed associare a ciascuna immagine nel vettore l’opportuno output.

Ogni immagine può rappresentare o una O oppure un X, a queste due “etichette” vogliamo associare un valore numerico, che nel mio caso sarà 0 per la O e 1 per la X.

Il comportamento che ci aspettiamo sarà il seguente: train_img, train_desc = CreaDBImmagini(...).get()

train_img conterrà i valori RGB per ciascuna immagine, in una lista di immagini così codificate. Ciascuna immagine è una griglia di pixel, per esempio un’immagine di 3×3 px diventerebbe una matrice di questo tipo [[1,2,3],[4,5,6],[7,8,9]]. I pixel in realtà non sono numerati in questo modo, ma ciascun pixel ha un valore RGB da 0 a 255, per ciascun colore. Quindi se il primo pixel fosse completamente nero al posto dell’1 avremmo [0,0,0], una lista con 3 zeri. Se fosse completamente bianco avremmo [255,255,255], se fosse rosso avremmo [255,0,0] ecc. Questo significa che l’immagine del nostro esempio avrebbe una rappresentazione tridimensionale 3x3x3 diventando così per esempio: [[[0,0,0],[0,0,0],[0,0,0]],[[0,0,0],[255,255,255],[0,0,0]],[[0,0,0],[0,0,0],[0,0,0]]]. In questo caso il pixel centrale, quello che abbiamo numerato come 5, sarebbe bianco e gli altri tutti neri. Se avessimo 100 di queste immagini, avremmo un oggetto di dimensione 100x3x3x3. Ovvero una lista di 100 elementi, di cui ciascun elemento è questa lista, di liste di liste, rappresentante la singola immagine.

train_desc è invece una semplice lista monodimensionale, se avessimo 100 immagini questa sarebbe una lista di 100 elementi. Ogni elemento descrive l’immagine, nel nostro caso se l’immagine corrispondente in train_img fosse una O allora avremo uno 0, nel caso di una X avremo un 1. Sarà quindi una lista tipo [0,1,1,0,0,....]. In questo esempio vorrebbe dire che le immagini in train_img rappresentano in ordine: O, X, X, O, O, ecc.

Tutti gli altri commenti sono nel codice:

6. Sviluppiamo la classe di gioco

A questo punto sviluppiamo la classe di gioco integrando le precedenti classi.

Anche su questo facciamo un paio di appunti. Quando calcoliamo le previsioni con previsione = s.__modello.predict(gioco) otteniamo una lista di liste di probabilità. Ogni sub-lista contiene tante probabilità quanti sono i neuroni (o nodi) impostati nell’ultimo layer del modello. Nel nostro caso all’istruzione keras.layers.Dense(2, activation='softmax') abbiamo impostato 2 neuroni, quindi il vettore di previsione conterrà un elenco di liste del tipo [p1, p2], dove p1 e p2 sono le probabilità dell’output 0 e dell’output 1 (che ricordiamoci corrispondono a O e a X, rispettivamente). Il risultato di previsione sarà qualcosa del tipo [[1. 0.],[0.51 0.49], [0. 1.]]. Nel caso specifico vorrebbe dire che secondo il ML la prima immagina è una O, poi uno spazio vuoto, poi una X. Per quanto riguarda l’uso di softmax e relu fare riferimento a questa pagina.

Per i dettagli sull’optimizer Adam fare riferimento a questa pagina.

Il calcolo della griglia di vittoria è molto semplice, perché abbiamo un vettore lineare e non una matrice (o griglia vera e propria). In questo caso una partita come quella del gioco_1.jpg sarà rappresentata da una lista di 0, 1 e 2, in questo modo (dove 0 = vuoto, 1 = O, 2 = X): [2, 2, 1, 2, 1, 0, 2, 1, 1]

Quando il primo valore è diverso da 0, quindi da vuoto, basta controllare che a distanza di 3 siano uguali, come qui: [2, 2, 1, 2, 1, 0, 2, 1, 1]

In questo modo stiamo controllando le colonne.

Per controllare le righe basta verificare le triplette, così: [2, 2, 1, 2, 1, 0, 2, 1, 1]

7. Chiudiamo il programma

Infine costruiamo il ciclo while per poter far usare il programma:

Per completezza riporto di seguito l’intero codice di programmazione:

 

Vedi articolo

[python] Esercizio su funzioni ricorsive e calcolo del percorso minore possibile

Immaginiamo di avere la seguente situazione: su una griglia rettangolare abbiamo due punti, A e B e vogliamo calcolare il minore percorso possibile per arrivare da A a B, evitando degli ostacoli. Da una cella all’altra ci si può spostare solamente in verticale ed orizzontale.

Possiamo schematizzare il problema in qualcosa di simile.

Anzitutto impostiamo le variabili necessarie:

In particolare faccio notare che terremo tutti i percorsi in una lista percorsi = [] e vogliamo anche tenere traccia della minima lunghezza trovata con minlen. Questo accorgimento, come vedremo successivamente, ci servirà per ridurre notevolmente la quantità di calcoli da dover effettuare, eliminando tutti i percorsi che via via dovessero rivelarsi più lunghi del minimo percorso trovato (fino a quel momento).

A questo punto generiamo la matrice del mondo:

Adesso scriviamo la funzione per popolare il mondo di blocchi, le X del disegno precedente. Per farlo utilizzeremo la libreria random, per cui ricordiamoci di importarla all’inizio con import random

Utilizziamo random.randint per generare una posizione casuale su righe e colonne per i blocchi, verifichiamo che tale punto non sia già occupato da un blocco, in tal caso procediamo ad aggiungerlo.

In modo analogo creiamo un metodo che aggiunga la posizione iniziale A e quella finale B, registrandole nelle rispettive variabili.

Per trovare il percorso procediamo nella maniera seguente:

Faccio notare una peculiarità, legata al modo in cui Python tratta le liste. Per passare il percorso attuale, copiandolo, alla funzione medesima, dobbiamo usare un piccolo trucco scrivendo []+percorso. In questo modo evitiamo che il percorso sia passato per riferimento, forzando il programma a crearne una copia.

Fatto tutto questo aggiungiamo un metodo per la stampa della matrice:

E un metodo per prendere uno dei percorsi più corti calcolati (ricordiamoci che potrebbero esserci più percorsi):

Infine uniamo tutto quanto insieme, importando anche la libreria temporale con import time per valutare la velocità di elaborazione.

Di seguito il codice completo:

L’output finale darà qualcosa di simile a questo:

Vedi articolo

[python] Calcolare le soluzioni di un sistema lineare di primo grado utilizzando il metodo di Cramer (senza librerie aggiuntive)

Obiettivo: Vogliamo calcolare, utilizzando Python, tutte le soluzioni per un sistema lineare di primo grado utilizzando le matrici e il metodo di Cramer. Per farlo non utilizzeremo librerie aggiuntive, se non quanto disponibile nativamente dentro Python.

Ovviamente lo scopo dell’esercizio è l’utilizzo di Python, visto che si potrebbe risolvere agilmente con librerie come SciPy e NumPy.

Supponiamo di avere un sistema lineare come quello seguente (l’ho scopiazzato spudoratamente da YouMath, dove c’è anche un’esaustiva spiegazione sul metodo di Cramer):

\begin{cases}2x+y+z=1 \\ 4x-y+z=-5 \\ -x+y+2z=5 \end{cases}

La matrice associata ai coefficienti ed il relativo determinante sarebbe questo:

\mbox{D}=\left| \begin{matrix} 2 & 1 & 1 \\ 4 & -1 & 1 \\ -1 & 1 & 2 \end{matrix}\right|=-12

Mentre la matrice dei termini noti sarebbe:

\mbox{A}=\left| \begin{matrix} 1 \\ -5 \\ 5 \end{matrix}\right|

Giusto per curiosità la soluzione immediata con NumPy sarebbe la seguente:

Il risultato nel caso specifico darà: [-1. 2. 1.]

Noi vogliamo costruire qualcosa di simile in Python 2.7.

Cominciamo creando la base della nostra classe perché possa accettare i medesimi argomenti:

Fatto questo cominciamo a creare i metodi di cui avremo bisogno. Il metodo di Creamer con il calcolo del determinante implica, per matrici di dimensione superiore a 2×2, che si debba poter ridurre la matrice in sottomatrici.

Giusto per chiarezza, nel caso del determinante D della suddetta matrice, il calcolo si svolgerebbe in questo modo:

\mbox{D}=\left| \begin{matrix} 2 & 1 & 1 \\ 4 & -1 & 1 \\ -1 & 1 & 2 \end{matrix}\right|= 2 * \left| \begin{matrix} -1 & 1 \\ 1 & 2 \end{matrix}\right| - 1 * \left| \begin{matrix} 4 & 1 \\ -1 & 2 \end{matrix}\right| + 1 * \left| \begin{matrix} 4 & -1 \\ -1 & 1\end{matrix}\right| =\newline = 2*(-1*2-1*1)-1*(4*2-(-1)*1)+1*(4*1-(-1)*(-1)) = \newline = 2*(-3)-1*(9)+1*(3)=\newline =-6-9+3=-12

Ogni sottomatrice prende tutti i valori della matrice corrente eccetto quelli nella riga e nella colonna della posizione attuale. Scriviamo quindi la nostra funzione nella maniera seguente:

Per calcolare il determinante vogliamo fare due operazioni distinte: il calcolo per matrici 2×2 e il calcolo per riduzione delle matrici di dimensione superiore. Iniziamo scrivendo l’operazione per quelle 2×2:

Per calcolare gli altri determinanti procederemo invece con:

In particolare con (1-2*(c%2)) calcoliamo il segno per ogni elemento della prima riga. In particolare ricordiamo che gli elementi saranno calcolati in ordine da 0, 1, 2, 3, 4 ecc. Se calcoliamo il resto per ognuno di essi avremo 0, 1, 0, 1, 0 ecc. Eseguendo la suddetta operazione avremo quindi 1-0, 1-2, 1-0, 1-2, 1-0 ecc. Questo ci darà i segni come 1, -1, 1, -1, 1 ecc.

A questo punto ci manca solo un metodo che ci permetta di creare nuove matrici per calcolare i vari determinanti per x, y, z ecc. Ricordiamoci che i vari risultati saranno dati da:

x=\frac{\mbox{D}_x}{\mbox{D}}=\frac{12}{-12}=-1\\ \\ \\ y=\frac{\mbox{D}_y}{\mbox{D}}=\frac{-24}{-12}=2\\ \\ \\ z=\frac{\mbox{D}_z}{\mbox{D}}=\frac{-12}{-12}=1

Dobbiamo fare attenzione ad un dettaglio importante. In Python le liste vengono passate per riferimento, questo vuol dire che modificando ogni lista “copiata” si modifica in realtà la lista originale all’indirizzo di memoria. Per copiare una matrice multidimensionale è necessario quindi copiare i singoli valori nella nuova matrice, nella maniera seguente darebbe errore:

Quindi procediamo in questo modo:

La nostra funzione di sostituzione sostituirà quindi i termini noti nella colonna preselezionata.

A questo punto possiamo scrivere il metodo che calcolerà la soluzione, alla maniera seguente:

Infine implementiamo il metodo solve aggiungendo un paio di chiamate di errore, nel caso in cui la matrice non sia adatta.

Nel suo complesso la classe che avremo creato sarà la seguente:

Per utilizzarla sarà sufficiente scrivere:

Se abbiamo fatto tutto correttamente il risultato che otterremo sarà: [-1.0, 2.0, 1.0]

Vedi articolo

[python] Disegnare un fiocco di neve con OpenGL, Python e pygame

Visto che siamo in tema natalizio vediamo come disegnare un fiocco di neve con Python, OpenGL e pygame.

Questo esercizio riprende in parte quanto già visto per disegnare un poligono sempre con Python e OpenGL.

Anzitutto assicuriamoci di avere installate le librerie PyOpenGL e pygame, qualora non le avessimo sarà sufficiente installarle mediante pip con i seguenti due comandi:

Fatto questo possiamo cominciare a predisporre il nostro programma.

Anzitutto voglio fare un breve approfondimento su alcune funzioni che andremo ad utilizzare.

Cominciamo da gluPerspective. Questa funzione accetta 4 argomenti:

  • fovy: sostanzialmente l’angolo di visione in gradi, come illustrato nell’immagine di seguito
  • aspect ratio: il rapporto tra altezza e larghezza, sostanzialmente ci andiamo ad inserire il rapporto tra le dimensioni del nostro schermo (poi ne discutiamo meglio)
  • zNear: il piano più vicino dopo il quale comincia la visione
  • zFar: il piano più lontano al quale termina la visione
Risultato immagini per gluPerspective"
Immagine presa da GLUT and OpenGL by Robby T. Tan

Ricordiamoci che andremo a disegnare un oggetto in 3D, che dovrà essere posizionato all’interno del tronco di piramide definito dai due piani e dall’angolo di apertura. Se l’oggetto dovesse uscire da questo spazio semplicemente non risulterebbe visibile sullo schermo (o risulterebbe parzialmente tagliato). Da questo si capisce come l’angolo di apertura determinerà la prospettiva e lo spazio di interazione dell’oggetto. La situazione sostanzialmente è la seguente:

Per maggiori approfondimenti consiglio anche una lettura veloce a Perspective distortion (photography)

Dal momento che andrò a posizionare i nostri oggetti a partire da (0,0,0) voglio anche spostare il punto di visione (viewpoint) indietro, in modo da farceli entrare, altrimenti non sarebbero visibili. Per farlo utilizzo la funzione glTranslatef che prendere come argomenti i valori x, y e z sui quali effettuare la traslazione.

Infine voglio disegnare delle linee usando i rispettivi vertici. Per farlo dovrò aggiungere coppie di vertici alla “matrice del mondo” con il metodo glVertex3fv. Nel caso specifico posso disegnare una linea orizzontale che vada da (0,0,0) a (1,0,0) scrivendo le seguenti quattro righe di codice:

Se volessi disegnare un quadrato dovrei disegnare tutte le linee a coppie di vertici, nel modo seguente:

Sostanzialmente quello che facciamo in questo caso potrebbe essere riassunto col seguente schema:

Disegniamo le varie linee del quadrato seguendo le direzioni delle frecce rosse.

Adesso se volessimo limitarci a disegnare un quadrato e farlo ruotare davanti alla telecamera potremmo scrivere il seguente programma:

Grazie a glClearColor(0, 0, 0.1, 1) coloriamo lo sfondo di un blu scuro. Eseguendo il programma vedremo qualcosa di simile:

Il quadrato sarà in movimento ovviamente, grazie alla funzione glRotatef che ruota la matrice del mondo.

Arrivati a questo punto ci manca solo di disegnare il fiocco di neve, ovvero preparare le coppie di vertici di tutte le linee del fiocco di neve stesso.

Per farlo voglio seguire il seguente schema:

Partiamo dal basso e disegniamo un ramo di lunghezza L. Prendiamo come riferimento un angolo α di 45° (ovvero π/4). Alla fine del ramo teniamo conto dell’angolo di arrivo e disegniamo altri due rami, ciascuno spostato di 45°. Procediamo in avanti ripetendo questo schema per n iterazioni.

Creiamo quindi due classi, una per il Fiocco e una per ciascun Ramo, nel modo seguente (nei commenti in Python ulteriori dettagli):