11: MODIFICARE LE IMMAGINI

IL SOTTOMODULO transform

In questo sottomodulo di pygame sono raggruppate alcune funzioni che servono a manipolare le immagini e che possono essere molto utili. Tutte queste funzioni agiscono su una Surface, che compare come primo parametro, e ne restituiscono un'altra, che dovremo assegnare ad una variabile. In questo modo la Surface di partenza non viene modificata dalla funzione (a meno che non la riassegniamo a sè stessa). Vediamo le più importanti (nella tabella ho omesso alcuni parametri opzionali per semplicità, se volete approfondire ecco il link alla documentazione ufficiale del modulo https://www.pygame.org/docs/ref/transform.html):

Funzione Significato
flip(Surface, xbool, ybool) Esegue un ribaltamento dell'immagine, sinistra-destra se xbool è True o alto-basso se lo è ybool (anche tutti e due insieme)
scale(Surface, (width, height)) Scala l'immagine in modo da portarla alle dimensioni richieste. width e height devono essere numeri interi altrimenti provocherà un errore.
smoothscale(Surface, (width, height)) Come la precedente, usa un algoritmo che da risultati più precisi ma si può usare solo con colori a 24-32 bit. In genere conviene usare questa al posto della scale().
rotate(Surface, angle) Ruota l'immagine dell'angolo angle (in gradi). Un angolo positivo corrisponde ad una rotazione antioraria e viceversa. Se l'angolo è di 90°, 180° o 270° restituisce una Surface con le stesse dimensioni di quella di partenza, altrimenti esse saranno diverse (e bisognerà usare get_rect() o qualche altro metodo simile per conoscerle)
rotozoom(Surface, angle, scale) Ruota e scala l'immagine contemporaneamente. scale è un numero float che determina il fattore di scala (2.0 = dimensioni raddoppiate,etc.).
scale2x(Surface) Raddoppia velocemente entrambe le dimensioni dell'immagine.

Come ho detto queste funzioni prendono una Surface come parametro e ne restituiscono un'altra trasformata, lasciando invariata la Surface originale. Se vogliamo modificare una Surface dobbiamo quindi riassegnare il risultato della funzione alla Surface stessa; ad esempio:

surf = pygame.transform.smoothscale(surf, (100, 200))

porta le dimensioni della Surface surf a 100 x 200 pixel.

La documentazione ci avvisa però che in genere queste trasformazioni sono distruttive, cioè fanno perdere qualità all'immagine restituita, quindi dobbiamo fare attenzione: se ci serve una sola trasformazione possiamo anche riassegnare il risultato della funzione all'immagine originale, mentre nel caso di molte trasformazioni consecutive dovremo usare un'altra tecnica, usando un'immagine come base ed un'altra per le trasformazioni (altrimenti l'immagine originale verrebbe rapidamente degradata).

USARE LE TRASFORMAZIONI

Ecco un esempio del primo caso. Le funzioni scale() e smoothscale() sono utili per adattare le dimensioni di un'immagine ad una determinata base ed altezza. Per esempio possiamo usarle per inserire come sfondo un'unica grande immagine, quando non ha le dimensioni giuste per la nostra finestra.

NUOVO PROGRAMMA: panda_sfondo2.py
ESERCIZIO 11.1: Riprendete il programma panda.py (qui) e salvatelo con il nuovo nome. Inserite adesso queste due istruzioni:

.   .   .
surf_back = pygame.image.load(os.path.join("Backgrounds", "desert_02.jpg").convert()
.   .   .
screen,blit(surf_back, (0, 0))
.   .   .
La prima va inserita all'inizio del programma, possibilmente vicino a quella che carica l'immagine del panda; la seconda deve invece sostituire la fill() che colora di verde lo schermo. Dovreste ormai essere in grado di trovare queste istruzioni da soli e di capire che cosa fanno le nuove istruzioni.
SOLUZIONI

Provate a lanciare il programma, e ... vedrete che qualcosa non va! Infatti lo sfondo è un'immagine di 2048x1536 pixel, ed è troppo grande per la nostra finestra.

ESERCIZIO 11.2: Inserite ora, subito dopo l'istruzione che carica la Surface surf_back, quest'altra istruzione che la porta alle giuste dimensioni.

surf_back = pygame.transform.smoothscale(surf_back, screen.get_size())
(per l'uso del metodo get_size() guardate qui)
SOLUZIONI

Lanciando il programma vediamo che ora lo sfondo "entra" perfettamente nella finestra, con una perdita di qualità accettabile. Possiamo quindi usare un'istruzione simile per portare un'immagine alle dimensioni volute, tenendo sempre conto che in caso di variazioni molto grandi o molto piccole nelle dimensioni rischiamo di degradare eccessivamente la nostra immagine originale.

Vediamo ora un esempio più complicato, in cui faremo ruotare e cambiare dimensioni al nostro panda. In questo esempio useremo la Surface originale come base per le trasformazioni, ed un'altra Surface per le immagini trasformate.

NUOVO PROGRAMMA: panda_rotante.py

Potete copiare il programma qui sotto oppure (ve lo consiglio) partire ancora dal programma panda.py, salvarlo con il nuovo nome e modificarlo. Lanciando il programma il panda, oltre a muoversi, presenterà anche un movimento rotatorio e varierà la sua dimensione.


import pygame
from pygame.locals import *
from os.path import join

pygame.init()
screen = pygame.display.set_mode((800, 600))
clk = pygame.time.Clock()

# carica le immagini
surf_image = pygame.image.load(join("Sprites", "panda.png")).convert_alpha()
surf_back = pygame.image.load(join("Backgrounds", "desert_02.jpg")).convert()
surf_back =  pygame.transform.smoothscale(surf_back, screen.get_size())

# altre variabili
angle = 0.0
angle_inc = 5                             
scale = 1.0
scale_inc = 0.1
n = 0

# posizione e velocita' del panda
center_panda = [100, 100]
vel_panda = [2, 2]

# ciclo principale
done = False
while not done:
    # sottociclo degli eventi
    for ev in pygame.event.get():
        if ev.type == QUIT:
            done = True
                
    # trasformazione dell'immagine
    angle = (angle + angle_inc) % 360
    scale += scale_inc
    n = (n + 1) % 4
    if n == 0:
        scale_inc *= -1
          
    surf_panda = pygame.transform.rotozoom(surf_image, angle, scale)
    rect_panda = surf_panda.get_rect() 
    rect_panda.center = center_panda
    
    # movimento
    if rect_panda.left < 0 or rect_panda.right > screen.get_width():
        vel_panda[0] *= -1
    if rect_panda.top < 0 or rect_panda.bottom > screen.get_height():
        vel_panda[1] *= -1

    center_panda[0] += vel_panda[0]
    center_panda[1] += vel_panda[1]    
   
    # aggiornamento dello schermo 
    screen.blit(surf_back, (0, 0))
    screen.blit(surf_panda, rect_panda)
    pygame.display.flip()
        
    clk.tick(30)

pygame.quit()

Ci sono, rispetto al vecchio programma, parecchie differenze che è meglio analizzare in dettaglio:

C'è però un problema, che potete facilmente notare se lasciate l'animazione attiva per qualche tempo: a volte il panda, anzichè rimbalzare, sembra "strisciare" contro una parete rimanendovi attaccato per un certo tempo prima di staccarsene.

Capite il motivo di questo strano comportamento? Se ci pensate le cause sono semplici da individuare: dato che il Rect che contiene il panda varia le sue dimensioni ad ogni ciclo, può succedere che il programma si accorga che stia urtando una parete e cambi il verso della sua velocità. Nel ciclo successivo, però, il Rect potrebbe essere aumentato di dimensioni e quindi urtare di nuovo la parete anche se il suo centro si è allontanato. Allora il programma invertirebbe ancora la velocità "spingendolo" di nuovo contro la parete e così via. Fortunatamente non è difficile correggere questo errore modificando le righe che controllano il rimbalzo. Come fareste? Potete pensarci un po' e poi provate il prossimo esercizio:

ESERCIZIO 11.3: Modificate le due istruzioni if che controllano il rimbalzo:
  1. La componente orizzontale deve essere invertita se:
    • la x del lato sinistro del Rect è < 0 e la componente orizzontale è negativa oppure
    • la x del lato sinistro del Rect è > del lato destro di screen e la componente orizzontale è positiva
  2. La componente verticale deve essere invertita se:
    • la y del lato superiore del Rect è < 0 e la componente verticale è negativa oppure
    • la y del lato inferiore del Rect è > del lato inferiore di screen e la componente verticale è positiva
SOLUZIONI

Dovete utilizzare quindi due if piuttosto complessi, dopodichè il problema dovrebbe essere risolto.

ALCUNE FUNZIONI

Per finire vi propongo alcune funzioni scritte da me che potrebbero essere utili, soprattutto nella fase iniziale o finale di un gioco, per ottenere degli effetti sulle immagini. Dato che contengono dei cicli temporizzati (con la wait()) non sono adatte ad essere inserite nel ciclo principale, perchè lo rallenterebbero. La prima funzione, flash_surface(), produce un lampeggiamento di un'immagine accendendola e spegnandola più volte. I parametri sono:

Parametro Tipo di dato Significato
surf_little Surface La nostra immagine, che faremo lampeggiare
center dupla di coordinate (x, y) Il centro del Rect nel quale posizioneremo l'immagine
surf_big Surface La Surface di sfondo sulla quale disegneremo surf_little (tipicamente sarà la nostra screen).

def flash_surface(surf_little, center, surf_big):
    rect = surf_little.get_rect()
    rect.center = center                              # centra surf_little
    surf_big_old = surf_big.copy()                    # fa una copia dello sfondo originale   
    for i in range(10):                               # ripete 10 volte (i = 0 ... 9)
        surf_big.blit(surf_big_old, (0, 0))           # disegna lo sfondo
        pygame.display.flip()
        pygame.time.wait(80)        
        surf_big.blit(surf_little, rect)              # disegna l'immagine
        pygame.display.flip()
        pygame.time.wait(80)    

La funzione rotate_surface() fa fare tre giri alla Surface surf_little. I parametri hanno lo stesso significato di quelli della funzione precedente (l'immagine ruoterà attorno al punto center).


def rotate_surface(surf_little, center, surf_big):
    surf_big_old = surf_big.copy()
    for i in range(55):                               # ripete 55 volte (i = 0 ... 54)
        surf = pygame.transform.rotate(surf_little, i * 20)
                                                      # ruota di 20 gradi (20 x 54 = tre giri)
        rect = surf.get_rect()                        # trova il Rect e lo posiziona
        rect.center = center
        surf_big.blit(surf_big_old, (0, 0))
        surf_big.blit(surf, rect)
        pygame.display.flip()
        pygame.time.wait(20)

Infine la funzione resize_surface() provoca un effetto di ingrandimento, partendo dalla Surface surf_little rimpicciolita di 10 volte ed ingrandendola fino alle sue dimensioni originali.


def resize_surface(surf_little, center, surf_big):
    w_min = surf_little.get_width() / 10            # larghezza iniziale (10 volte piu' piccola)
    h_min = surf_little.get_height() / 10           # altezza iniziale
    w_step = (surf_little.get_width() - w_min) / 20 # incrementi ad ogni passo (1/20 della differenza)
    h_step = (surf_little.get_height() - h_min) / 20
    surf_big_old = surf_big.copy()                  # fa una copia dello sfondo originale
    for i in range(21):                             # ripete 21 volte (i = 0 ... 20)
        w = int(w_min + i * w_step)                 # trova la nuova larghezza
        h = int(h_min + i * h_step)                 # trova la nuova altezza
        surf = pygame.transform.smoothscale(surf_little, (w, h))
                                                    # scala l'immagine
        rect = surf.get_rect()                      # trova il Rect e lo posiziona
        rect.center = center
        surf_big.blit(surf_big_old, (0, 0))
        surf_big.blit(surf, rect)
        pygame.display.flip()
        pygame.time.wait(20)
		
	
NUOVO PROGRAMMA: effetti_grafici.py

Scrivete ora un programma che mostri questi effetti grafici.

ESERCIZIO 11.4: Seguite questi passi:
SOLUZIONI

Non ho spiegato dettagliatamente il funzionamento delle funzioni, ma dai commenti dovreste riuscire a capirlo. Potete poi provare a modificarle.

ESERCIZIO 11.5: Nella flash_surface() modificate il numero di lampeggi e la velocità del lampeggio, nella rotate_surface() modificate il numero dei giri, l'angolo e la velocità di rotazione, nella resize_surface() modificate il numero dei passi e la velocità dell'effetto.
ESERCIZIO 11.6 (più difficile): Volendo rendere le funzioni più versatili potete inserire questi valori nei parametri delle funzioni, in modo che il loro comportamento possa essere modificato dal programma chiamante.
SOLUZIONI

Fine della lezione