Fondamenti di Programmazione

Liste e Tuple

In questa lezione useremo Python per implementare semplici operazioni su immagini digitali. Il materiale necessario per questa lezione si puo' scaricare in questo pacchetto.

Immagini Digitali

Le immagini digitali sono rappresentazioni al calcolatore di foto e grafica. In generale, rappresentiamo immagini digitali come una "matrice" di "colori". Ogni elemento della matrice di chiama pixel. Piu' alte e' il numero di righe e colonne, maggionre e' la risoluzione, cioe' precisione, dell'immagine. Contiamo le righe dall'alto e le colonne da sinistra a partire da 0 per semplicita'.

I colori sono rapressentati con combinazioni di tre colori primari, detti canali: red (rosso), green (verde) e blue (blu); RGB in breve. Per ogni canale useremo valori tra 0 e 255 per indicare l'intensita' di quelo colore primario. La ragione per cui usiamo questa combinazioni e' perche' gli umani sono tricromatici, cioe' la percezione del colore e' basata su tre tipi di sensori nell'occhio (e nelle macchine fotografiche digitali). I monitor di ogni tipo usano tipicamente tre tipi di "lampade" di colori diverso per visualizzare le immagini. La scienza del colore e' complicatissima e non possiamo scriverne un sommario. Gli interessati possono riferirsi a wikipedia.

Tuple

Le tuple in Python sono sequenze immutabili di valori (anche di tipo diverso). Sono molto simili alle liste, che spesso possono essere usate per risolvere problemi simili. In Python le tuple sono usate in molti contesti differenti. In generale consigliamo di usare le tuple per "impacchettare" una sequenza di valori che ha sempre la stessa lunghezza. Le tuple in python si creano mettendo valori tra parentesi (). As esempio

>>> bianco = (255,255,255)
>>> nero = (0,0,0)
>>> arancio = (255,128,0)
>>> arancio
(255, 128, 0)

Si puo' convertire tra tuple e liste usando le funzioni list() e tuple().

>>> arancio_list = list(arancio)
>>> arancio_list
[255, 128, 0]
>>> arancio_tuple = tuple(arancio_list)
>>> arancio_tuple
(255, 128, 0)

Si puo' accedere agli elementi di una tuple usando il loro indice (come nelle liste). La lunghezza di una tuple di ottiene con len(). Come per le liste, le tuple di concatenano con + e ripetono con *. Una tuple di un singolo elemento si indica con (valore,) (notate la virgola lascaiata).

>>> arancio[0]
255
>>> arancio[1]
128
>>> arancio[2]
0
>>> len(arancio)
3
>>> grigio = (128,)
>>> grigio
(128,)

Si puo' anche accedere agli elementi di una tuple assegnandoli a variabili con nomi diversi, separati da virgole. Questo rende il Python molto eleganthe e il construtto delle tuple molto utile. Questo assegnmento non e' possibile per le liste. Si puo' anche iterare sui valori di una tuple usando il for.

>>> r, g, b = arancio
>>> r, g, b
(255, 128, 0)
>>> r
255
>>> for canale in arancio:
        print canale
255
128
0

L'immutabilita' delle tuple significa che non possiamo assegnarne gli elementi, ne' combiarne la lunghezza (ad esempio con append).

>>> bianco[0] = 1    
Traceback (most recent call last):
  File "<pyshell#27>", line 1, in <module>
    bianco[0] = 1
TypeError: 'tuple' object does not support item assignment

>> bianco.append(1)
Traceback (most recent call last):
  File "<pyshell#37>", line 1, in <module>
    bianco.append(1)
AttributeError: 'tuple' object has no attribute 'append'

Facciamo ora alcuni esempi di uso della tuple nel linguaggio Python. Nelle lezioni precedenti abbiamo visto il metodo items per i dizionari. Adesso possiamo spiegare che items ritorna una lista di tuple e il for usato per iterare sulla lista usa la notazione di assegnamento di elementi della tuple.

>>> d = {"red": 255, "green": 128, "blue": 0}
>>> d.items()
[('blue', 0), ('green', 128), ('red', 255)]
>>> for key, value in d.items():
        print key, value
blue 0
green 128
red 255

Ad esempio possiamo iterare su una lista di colori con

>>> colori = [(255,0,0),(0,255,0),(0,0,255)]
>>> for colore in colori:
        print colore # colore e' una tuple
(255, 0, 0)
(0, 255, 0)
(0, 0, 255)
>>> for red, green, blue in colori:
        print red, green, blue
255 0 0
0 255 0
0 0 255
>>> for colore in colori:
        red, green, blue = colore
        print red, green, blue
255 0 0
0 255 0
0 0 255

Le tuple si possono anche usare per ritonare piu' valori da una funzione. Ad esempio

>>> def divisione_con_resto(a,b):
        return a/b, a%b    
>>> d, r = divisione_con_resto(5,3)
>>> print d, r
(1, 2)
# definizione
bianco = (255,255,255)
nero = (0,0,0)
arancio = (255,128,0)

# conversione
arancio_list = list(arancio)
arancio_tuple = tuple(arancio_list)
print arancio_list
print arancio_tuple

# accesso agli elementi
print arancio[0]
r, g, b = arancio
print r, g, b
for canale in arancio:
    print canale

# iteratione
colori = [(255,0,0),(0,255,0),(0,0,255)]
for colore in colori:
    print colore # colore e' una tuple
for red, green, blue in colori:
    print red, green, blue
for colore in colori:
    red, green, blue = colore
    print red, green, blue

# funzioni
def divisione_con_resto(a,b):
    return a/b, a%b
d, r = divisione_con_resto(5,3)
print d, r

Liste di Liste

In Python, le liste possono contenere elementi di qualunque tipo. Per creare matrici di valori, possiamo creare una lista di liste, facendo attenzione a non assegnare la a piu' righe la stessa lista. As esempio possiamo creare una matrice 2 per 2 di interi con

>>> m = [ [0,1], [2,3] ]
>>> m
[[0, 1], [2, 3]]

L'accesso agli elementi avviene accedendo prima alla riga e poi alla colonna come liste separate.

>>> m[0]
[0, 1]
>>> m[1]
[2, 3]
>>> m[0][0]
0
>>> m[0][1]
1
>>> m[1][0]
2
>>> m[1][1]
3
>>> for riga in m:
        for valore in riga:
            print valore
0
1
2
3

Visualizzandolo:

# crea una matrice
m = [ [0,1], [2,3] ]
print m

# accesso agli elementi
r0, r1 = m[0], m[1]
print r0, r1
print m[0][0], m[0][1], m[1][0], m[1][1]
for riga in m:
    for colonna in riga:
        print m

Creazione di Immagini

Possiamo creare immagini creando una matrice di colori. As esempio una piccolissima immagine due per due si puo' creare con:

>>> img = [ [ (255,0,0), (0,255,0) ], [ (0,0,255), (255,128,0) ] ]

Da ora in avanti pero' sara' difficile visualizzare i risultati dei programmi usando print o in visualizzatore dato che vogliamo creare immagini piu' larghe. Per vedere i risultati, salveremo le nostre immagini su disco in formato binario PNG che potete visualizzare direttamente come immagine. Per questo, usate le funzioni nel modulo image.py in dotazione a questa lezione. In quel modulo troverete le funzioni load e save per caricare e salvare le immagini e molti degli esempio che faremo nelle prossime lezioni. Per far girare gli esempi sulla vostra macchina, lanciate il programma program_image.py anch'esso incluso. As esempio per salvare una immagine possiamo fare

>>> import image
>>> image.save('small.png',img)

Questo creare una immagine piccolissima, che inseriamo in seguito.

Questa immagine e' quasi troppo piccola per essere vista. Potete ingrandirla nell'applicazione che usate per visualizzare le immagini. Ecco lo screenshot dell'immagine allargata (il grigio e' il background dell'applicazione).

Per creare immagini piu' gradi, definiamo una funzione che crea un'immagina a partire dalla sua larghezza iw (width, cioe' il numero di colonne) e altezza ih (height, cioe' il numero di righe) e setta tutti i pixel ad un colore predefinito c. Al codice segue il risultato della funzione create(256,256,(0,0,0)).

def create(iw,ih,c):
    img = []
    for _ in range(ih):
        row = []
        for _ in range(iw):
            row.append(c)
        img.append(row)
    return img

Accesso ai Pixel

Possiamo creare immagini po' piu' interessanti assegnando valori diversi per ogni pixel. Per fare questo, prima creiamo un'immagine con create per poi disegnarli sopra con varie funzioni che chiameremo draw_*. Ad esempio, possiamo disegnare un quadrato con draw_quad che prende in input la posizione dell'angolo superiore sinistro del quadratino in (x, y), la sua dimensione in (w,h) e il colore c.

def draw_quad_simple(img,x,y,w,h,c):
    for j in range(y,y+h):
        for i in range(x,x+w):
            img[j][i] = c

Il risultato di disegnare un quadrato su un'immagine nera e' a seguito del codice.

img = create(256,256,(0,0,0))
draw_quad_simple(img,32,32,192,192,(255,128,0))

In questa funzione assumiamo che l'utente specifichi coordinate corrette, che non escono mai dall'immagine. Per essere piu' sicuri possiamo modificare la funzione precedente mettendo un test per validare che le coordinate sono valide. Per controllare questo, confrontiamo gli indici di riga j e colonna i con le dimensioni dell'immagine iw = len(img[0]) e ih = len(img).

def inside(img,i,j):
    iw = len(img[0])
    ih = len(img)
    return i >= 0 and i < iw and j >= 0 and j < ih

def draw_quad(img,x,y,w,h,c):
    for j in range(y,y+h):
        for i in range(x,x+w):
            if not inside(img,i,j): continue
            img[j][i] = c

Il risultato di disegnare un quadrato su un'immagine nera e' a seguito del codice. Notate che in questo caso la funzione precedente a vrebbe dato essero.

img = create(256,256,(0,0,0))
draw_quad(img,32,32,512,512,(255,128,0))

La funzione precedente avrebbe dato un'errore simile a:

Traceback (most recent call last):
  File "./program_image.py", line 19, in <module>
    draw_quad_simple(img,32,32,512,512,(255,128,0))
  File "image.py", line 39, in draw_quad_simple
    img[j][i] = c
IndexError: list assignment index out of range

Per disegnare una scacchiera di quadratini di dimensione s, draw_gradient_checkers calcola l'indice del quadratino (ii,jj) semplicemente dividendo le coordinate del pixel (i,j) per la dimensione del quadratino s. Fatto questo, possiamo sceglierne il colore facendo la somma delle coordinate modulo 2 per saltare a righe alterne. Ecco il codice, seguito dal risulato draw_gradient_checkers(img,32,(0,0,0),(255,255,255)).

def draw_gradient_checkers(img,s,c0,c1):
    for jj in range(len(img)/s):
        for ii in range(len(img[0])/s):
            if (ii + jj) % 2: c = c1
            else: c = c0
            draw_quad(img,ii*s,jj*s,s,s,c)

Operazioni sui Colori

Per creare immagini piu' interessanti, possiamo operare direttamente sui colori. As esempio possiamo creare un gradiente di colore orizzontale con draw_gradient_horizontal interpolando due colori c0 e c1 date le coordinate di un pixel. Di seguito dimostriamo la creazione di un gradiente orizzontale, seguito dal risultato di draw_gradient_horizontal(img,(255,0,0),(0,255,0)). A seguito mostreremo la creazione di un gradient verticale.

def draw_gradient_horizontal(img,c0,c1):
    r0, g0, b0 = c0
    r1, g1, b1 = c1
    for j in range(len(img)):
        for i in range(len(img[0])):
            u = float(i) / float(len(img[0]))
            r = round(r0 * (1-u) + r1 * u)
            g = round(g0 * (1-u) + g1 * u)
            b = round(b0 * (1-u) + b1 * u)
            img[j][i] = (r,g,b)

def draw_gradient_vertical(img,c0,c1):
    r0, g0, b0 = c0
    r1, g1, b1 = c1
    for j in range(len(img)):
        for i in range(len(img[0])):
            v = float(j) / float(len(img))
            r = round(r0 * (1-v) + r1 * v)
            g = round(g0 * (1-v) + g1 * v)
            b = round(b0 * (1-v) + b1 * v)
            img[j][i] = (r,g,b)

Si puo' ottenere gradienti in tutte e due le dimensione, interpolando i colori su tutte e due gli assi. In pratica questo combina i due esempi precedenti.