# ---
# jupyter:
# jupytext:
# formats: ipynb,py:percent
# text_representation:
# extension: .py
# format_name: percent
# format_version: '1.3'
# jupytext_version: 1.15.2
# kernelspec:
# display_name: Python 3 (ipykernel)
# language: python
# name: python3
# ---
# %% RECAP [markdown] slideshow={"slide_type": "slide"}
# # Fondamenti di Programmazione
#
#
# **Andrea Sterbini**
#
# lezione 13 - 6 novembre 2023
# %% [markdown] slideshow={"slide_type": "subslide"}
# # RECAP: Immagini
# - creazione/load/save
# - rotazione
# - disegno di linee verticali/orizzontali/diagonali
# - disegno di rettangoli ed ellissi
#
# %% slideshow={"slide_type": "notes"} init_cell=true
# %load_ext nb_mypy
# %%% Immagini come matrici di pixel slideshow={"slide_type": "subslide"} init_cell=true
from images import load, visd
# definiamo qualche colore
black = 0, 0, 0
white = 255, 255, 255
red = 255, 0, 0
green = 0, 255, 0
blue = 0, 0, 255
cyan = 0, 255, 255
yellow= 255, 255, 0
purple= 255, 0, 255
gray = 128, 128, 128
# definizione di tipi
Colore = tuple[int,int,int]
Immagine = list[list[Colore]]
def crea_immagine(larghezza : int, altezza : int, colore : Colore=black) -> Immagine :
return [ [ colore ]*larghezza
for i in range(altezza)
]
def draw_pixel(img : Immagine, x : int, y : int, colore : Colore) -> None:
altezza = len(img)
larghezza = len(img[0])
if 0 <= x < larghezza and 0 <= y < altezza:
img[y][x] = colore
# %% GIOVEDI': copiare parti di immagini, crop, filtri [markdown] slideshow={"slide_type": "subslide"}
# ## Ritagliare una immagine (crop)
# %%% ritagliare una immagine (crop) slideshow={"slide_type": "fragment"}
# tolgo una striscia attorno all'immagine di spessori dati
def crop_image(img : Immagine, alto : int,basso : int, sx : int, dx :int) -> Immagine :
# controllo sui parametri
L,A = len(img[0]), len(img)
# i valori di crop non devono sommare a più di L ed A
assert 0<= alto None:
# FIXME: controllo sui parametri
# ATTENZIONE: assumo che i parametri siano corretti
HS = len(imgS)
WS = len(imgS[0])
# prima creo il frammento da copiare
# FIXME: prima di ritagliare calcoliamo quante righe e quante colonne
# entreranno nell'immagine di destinazione e ritagliamo solo quella parte
frammento = crop_image(imgS, ys1, HS-ys2, xs1, WS-xs2 )
# per tutte le righe da copiare
larghezza = len(frammento[0])
for yF,riga in enumerate(frammento):
# uso un assegnamento a slice
imgD[yF+YD][XD:XD+larghezza] = riga
# OPPURE senza creare il cropped, copiando solo i pixel giusti (più efficiente)
# %% [markdown]
# ### E' SBAGLIATO usare **list.copy** sulla immagine
# Perchè copia solo la lista esterna di righe
#
# Usate **copy.deepcopy** oppure una list-comprehension che copia una riga per volta
# %% jupyter={"source_hidden": true}
from pygraphviz import AGraph
G = AGraph(directed=True, rankdir='TD')
for i in range(5):
G.add_edge(f'img[{i}]', f'riga {i}')
G.add_edge(f'riga {i}', f'copia[{i}]', dir='back')
if i: G.add_edge(f'riga {i-1}', f'riga {i}',color='white')
if i: G.add_edge(f'copia[{i-1}]', f'copia[{i}]',color='white')
if i: G.add_edge(f'img[{i-1}]', f'img[{i}]',color='white')
for i in range(5):
G.subgraph([f'img[{i}]', f'riga {i}', f'copia[{i}]', ], rank='same')
G.layout('dot')
G
# %%% filtri ed elaborazioni slideshow={"slide_type": "subslide"}
img=load('3cime.png')
# copio l'immagine delle 3 cime di Lavaredo
img_copiata = crop_image(img, 0,0,0,0)
# altro modo di copiare una immagine
def copia(img: Immagine) -> Immagine:
return [ riga.copy() for riga in img ] # NON BASTA usare solo copy sulla lista esterna!!!
# oppure
from copy import deepcopy # SE vi è permesso importare da 'copy'
# %%% filtri ed elaborazioni slideshow={"slide_type": "subslide"}
# %time img_copiata = copia(img)
# %time img2 = deepcopy(img)
# ne copio un pezzettino in un altro punto
cut_paste_img(img, img_copiata, 50,50,100,100, 200,10 )
visd(img_copiata), visd(img)
# %% [markdown] slideshow={"slide_type": "subslide"}
# ## Aggiungere un bordo
#
# - creiamo una immagine più grande colorata come il bordo
# - ci incolliamo la immagine
# %% slideshow={"slide_type": "fragment"}
# per aggiungere un bordo
def add_border(img : Immagine, spessore : int, colore : Colore ) -> Immagine :
L, A = len(img[0]), len(img)
# creiamo una immagine più grande col colore del bordo
nuova = crea_immagine(L+2*spessore,A+2*spessore, colore)
# ci incolliamo l'immagine originale
cut_paste_img(img,nuova,0,0,L-1,A-1,spessore,spessore)
return nuova
# %% [markdown]
# ### Oppure la creiamo riga per riga
# %% slideshow={"slide_type": "fragment"}
# oppure la costruiamo riga per riga
def add_border2(img : Immagine, spessore : int, colore : Colore ) -> Immagine :
L, A = len(img[0]), len(img)
bordata = []
# - prima spessore righe del colore
bordata += [ [colore] * (L+2*spessore) for i in range(spessore) ]
# - poi per ogni riga dell'immagine
for riga in img:
# - spessore pixel + riga + spessore pixel
bordata.append( [colore]*spessore + riga + [colore]*spessore )
# - dopo spessore righe del colore
bordata += [ [colore] * (L+2*spessore) for i in range(spessore) ]
return bordata
# %% slideshow={"slide_type": "subslide"}
# %time bordata = add_border(img, 20, green)
# %time bordata2 = add_border2(img, 20, cyan)
visd(bordata) , visd(bordata2)
# %% [markdown] slideshow={"slide_type": "subslide"}
# # Filtri da applicare ai colori
# - ogni pixel della immagine viene trasformato in un nuovo colore. Esempi
# - toni di grigio
# - negativo
# - incremento/riduzione della luminosità
# - incremento/riduzione del contrasto
#
# **NOTA:** questi filtri dipendono solo dal pixel in esame e non dalla sua posizione
# %% [markdown] slideshow={"slide_type": "subslide"}
# ## Trasformiamo in una immagine in **toni di grigio**
# %%%% gray slideshow={"slide_type": "fragment"}
# filtro grigio che trasforma un colore in grigio con la stessa luminosità
def filtro_grigio(colore : Colore) -> Colore :
# tutti i pixel devono essere grigi ma con la stessa luminosità totale
# ovvero R=G=B e R+G+B uguale a prima, quindi bisogna mediare
media = round(sum(colore)/3) # round torna un intero se il numero di cifre decimali è 0
return media, media, media
# per trasformare una immagine in livelli di grigio
def grey(img : Immagine) -> Immagine :
# la copio
grigia = copia(img)
# e sostituisco ogni pixel col grigio corrispondente
for y, riga in enumerate(img):
for x, pixel in enumerate(riga):
grigia[y][x] = filtro_grigio(pixel)
return grigia
# esempio
img_grigia = grey(img)
visd(img_grigia)
# %% [markdown] slideshow={"slide_type": "subslide"}
# ## Cambiamo la **luminosità**
# Amplifichiamo/riduciamo di **k** volte la luminosità dell'immagine
# %%%% luminosità (schiarimento/scurimento) slideshow={"slide_type": "fragment"}
from copy import deepcopy
# Ci conviene definire una funzione che vincola il risultato
# ad essere INTERO ed entro un dato INTERVALLO [m,M] compresi
def bound(canale : float|int, m:int=0, M:int=255 ) -> int:
"trasformo il valore in intero all'interno di [m..M]"
canale = round(canale)
return min(max(canale, m), M)
def filtro_lumi(colore : Colore, k : float) -> Colore:
"cambiamo la luminosità del pixel di un fattore k su tutti i canali"
R,G,B = colore
# mi assicuro che i valori risultanti siano interi nel range 0..255
return bound(R*k), bound(G*k), bound(B*k)
def luminosità(img : Immagine, k : float) -> Immagine:
'per schiarire/scurire una immagine di un fattore k (float)'
copia = deepcopy(img) # creo una nuova immagine con deepcopy
# tutti i pixel devono avere una luminosità moltiplicata per k
for y, riga in enumerate(img):
for x, colore in enumerate(riga):
copia[y][x] = filtro_lumi(colore, k) # sostituisco il pixel
return copia
# %%%% luminosità (schiarimento/scurimento) slideshow={"slide_type": "subslide"}
# esempio
img_luminosa = luminosità(img, 1.5)
img_scura = luminosità(img, 0.8)
visd(img_luminosa), visd(img_scura)
None
# %% [markdown] slideshow={"slide_type": "subslide"}
# ## Generalizziamo l'applicazione del filtro
# - definendo una trasformazione generica
# - che accetta come parametro **la funzione che trasforma il pixel**
# %%% applicazione di un filtro generico slideshow={"slide_type": "fragment"}
from typing import Callable
Filtro = Callable[[Colore], Colore] # funzione che accetta un Colore e produce un Colore
def applica_filtro( img : Immagine, filtro : Filtro ) -> Immagine:
'creo una nuova immagine in cui ciascun pixel è trasformato con la funzione filtro(Colore)->Colore'
# copio l'immagine
copia = deepcopy(img)
# tutti i pixel vengono sostituiti con il risultato del filtro
for y, riga in enumerate(img):
for x, colore in enumerate(riga):
copia[y][x] = filtro(colore) ### QUI eseguo il filtro sul pixel corrente
return copia
# %%% applicazione di un filtro generico slideshow={"slide_type": "subslide"}
# esempio
img_ingrigita = applica_filtro(img, filtro_grigio)
visd(img_ingrigita)
# %% slideshow={"slide_type": "subslide"}
# il filtro deve accettare un solo parametro
def piu_scura(pixel):
"scurisco l'immagine dimezzando la luminosità"
return filtro_lumi(pixel, 0.5)
scura1 = applica_filtro(img, piu_scura)
# oppure posso usare una lambda
scura2 = applica_filtro(img, lambda pixel: filtro_lumi(pixel, 0.5))
visd(scura1), visd(scura2)
None
# %% [markdown] slideshow={"slide_type": "subslide"}
# ## Cambiamo il **contrasto**
# - per cambiare il contrasto di un fattore **k**
# - ogni pixel chiaro deve diventare più chiaro
# - ogni pixel scuro deve diventare più scuro
# - ovvero si devono allontanare/avvicinare di un fattore **k** dal grigio **128,128,128**
# %%%% contrasto slideshow={"slide_type": "fragment"}
def filtro_contrasto(colore : Colore, k : float) -> Colore:
"aumento di un fattore k la distanza del colore da 128, per ciascun canale RGB"
return tuple( bound((componente-128)*k+128) for componente in colore)
# PROBLEMA! : filtro_contrasto vuole DUE parametri, ma applica_filtro vuole un Filtro che ne prende solo 1
# NOTA: mypy non sa che la list comprehension è su 3 elementi per cui gli sembra sbagliato
# %%%% contrasto slideshow={"slide_type": "subslide"}
# SOLUZIONE : definisco una lambda che aggiunge Kalla chiamata
# esempio
img_più_contrastata = applica_filtro(img, lambda colore: filtro_contrasto(colore, 1.2))
img_meno_contrastata = applica_filtro(img, lambda colore: filtro_contrasto(colore, 0.8))
visd(img_meno_contrastata), visd(img_più_contrastata)
None
# %% [markdown] slideshow={"slide_type": "subslide"}
# ## effetto **Negativo**
# %% slideshow={"slide_type": "fragment"}
def negativo(colore : Colore ) -> Colore :
"invertiamo la luminosità di ciascun canale RGB"
return tuple( 255-componente for componente in colore )
img_negata = applica_filtro(img, negativo)
visd(img_negata)
# di nuovo, mypy non sa dedurre che produrremo sempre una tupla di 3 componenti
# %% [markdown] slideshow={"slide_type": "subslide"}
# ## Sfocatura (**blur**)
# - facciamo la media dei colori fino a distanza **k** dal pixel
#
# NOTA: questo filtro deve **conoscere la posizione del pixel**
# %%%% blur (sfocatura) slideshow={"slide_type": "subslide"}
# calcolo la media di un gruppo di colori
def colore_medio(listaColori : list[Colore]) -> Colore :
N = len(listaColori)
R,G,B = 0, 0, 0
for r,g,b in listaColori:
R += r
G += g
B += b
return bound(R/N), bound(G/N), bound(B/N)
#oppure
# return tuple(map(lambda X: bound(sum(X)/N), zip(*listaColori)))
# %%%% blur (sfocatura) slideshow={"slide_type": "fragment"}
# per sfocare una immagine entro una distanza k
# genero una nuova immagine
# con i pixel che sono la media del gruppo di pixel
# attorno a quello indicato fino a distanza k
def blur(img : Immagine, k : int) -> Immagine:
W = len(img[0])
H = len(img)
copia = [ riga.copy() for riga in img ] # invece che deepcopy
for x in range(W):
for y in range(H):
# raccolgo i colori del vicinato (potrei essere sul bordo)
vicinato = []
for X in range(x-k,x+k+1):
for Y in range(y-k, y+k+1):
if 0 <= X < W and 0 <= Y < H: # se sono dentro
vicinato.append(img[Y][X])
copia[y][x] = colore_medio(vicinato)
return copia
# blur è una operazione molto lenta ....
# TODO: realizzarlo come filtro che dipende dalla posizione -> vedi sotto
# %%%% blur (sfocatura) slideshow={"slide_type": "subslide"}
# esempio
img_sfocata1 = blur(img, 1)
img_sfocata2 = blur(img, 2)
img_sfocata3 = blur(img, 3)
visd(img_sfocata1), visd(img_sfocata2), visd(img_sfocata3)
None
# %% [markdown] slideshow={"slide_type": "subslide"}
# ## Inseriamo del rumore nella immagine
# - possiamo aggiungere del colore a ciascun pixel
# - oppure scegliere un pixel vicino
# %%%% random noise slideshow={"slide_type": "fragment"}
from random import randint
# per aggiungere rumore casuale ad una immagine
# possiamo aggiungere a ciascun pixel un piccolo valore random
def rumore_casuale(colore : Colore, k : int) -> Colore:
"aggiungiamo a ciascuna componente RGB un piccolo valore in [-k, k]"
return tuple( bound(C + randint(-k,k)) for C in colore )
# %% slideshow={"slide_type": "subslide"}
# esempio
poco_rumore = applica_filtro(img, lambda C: rumore_casuale(C, 20))
tanto_rumore = applica_filtro(img, lambda C: rumore_casuale(C, 50))
visd(img), visd(poco_rumore), visd(tanto_rumore)
None
# %% [markdown] slideshow={"slide_type": "subslide"}
# ## Filtri che dipendono dalla posizione
# Generalizziamo i filtri in modo che conoscano:
# - la posizione **x,y** del pixel corrente
# - l'immagine sorgente (per leggere altri pixel)
# - le dimensioni dell'immagine (per evitare di ricalcolarle)
# %% [markdown] slideshow={"slide_type": "subslide"}
# ## Esempio: Pixellazione
# possiamo colorare tutti i pixel di ciascun quadratino di dimensioni S in modo simile
# - coloro il pixel corrente come il **centro del suo quadratino**
# - oppure come la **media del suo quadratino**
# %%%% pixellation slideshow={"slide_type": "fragment"}
# - devo sapere dove sono nella immagine e avere accesso a tutta l'immagine!
# x y img L A
FiltroXY = Callable[[int, int, Immagine, int, int], Colore] # funzione filtro che conosce x,y,img,L,A
def applica_filtro_XY( img : Immagine, filtro : FiltroXY ) -> Immagine:
'applicazione di un filtro che dipende da x,y, dalla immagine e dalle dimensioni L,A'
W,H = len(img[0]),len(img)
# ricevo nell'argomento 'filtro' una funzione che calcola
# per ogni colore e posizione X,Y il nuovo colore
copia = deepcopy(img)
for y in range(H):
for x in range(W):
copia[y][x] = filtro(x, y, img, W, H) ### QUI chiamo il filtro
return copia
# %% slideshow={"slide_type": "subslide"}
# ad ogni quadrato sostituiamo il colore del suo centro
def pixella(x : int, y : int, img : Immagine, W : int, H : int, S : int) -> Colore :
'FiltroXY che legge il pixel al centro del suo quadretto'
X = bound(x-x%S+S/2, 0, W-1) # X del centro
Y = bound(y-y%S+S/2, 0, H-1) # Y del centro
return img[Y][X]
pixellata = applica_filtro_XY(img,
lambda x,y,imm,W,H: pixella(x,y,imm,W,H,5))
visd(pixellata)
# %% slideshow={"slide_type": "subslide"}
# ad ogni quadrato sostituiamo la *media* dei colori
def pixelmedio(x : int, y : int, img : Immagine, W : int, H : int, S : int) -> Colore :
'FiltoXY che fa la media dei pixel del quadretto'
R,G,B, N = 0,0,0, 0
minx = x-x%S
miny = y-y%S
vicini = [ img[Y][X] for X in range(minx, min(W,minx+S))
for Y in range(miny, min(H,miny+S)) ]
return colore_medio(vicini)
## INEFFICIENTE: ricalcola la media per ogni pixel
## MEGLIO: calcolo la media una volta per ogni quadrato
# - ad esempio ricordando il risultato per ogni xmin,ymin,xmax,ymax
pixellata2 = applica_filtro_XY(img, lambda x,y,imm,W,H: pixelmedio(x,y,imm,W,H,5))
visd(pixellata2)
# %% [markdown]
# ## Blur come filtro
# - per ogni pixel calcolo la media del vicinato
# %%
def blur_filter(x : int, y : int, img : Immagine, W : int, H : int, k : int) -> Colore:
"calcolo la media dei vicini fino a distanza k"
vicini = []
for X in range(bound(x-k,0,W),bound(x+k+1, 0, W)):
for Y in range(bound(y-k,0,H),bound(y+k+1, 0, H)):
vicini.append(img[Y][X])
# ne torno la media
return colore_medio(vicini)
# %%
# Esempio con k=3
sfumata = applica_filtro_XY(img, lambda x,y,imm,W,H: blur_filter(x,y,imm,W,H, 3 ))
visd(sfumata)
# %% [markdown] slideshow={"slide_type": "subslide"}
# ## immagine rumorosa per spostamento di pixels
# - scegliamo a caso un pixel entro una distanza **k** dal pixel da colorare
# %%%% random noise slideshow={"slide_type": "fragment"}
# sostituiamo ciascun pixel con un suo vicino preso a caso
def scegli_vicino_a_caso(x : int, y : int, img : Immagine, W : int, H : int, k : int) -> Colore:
"FiltroXY che legge un pixel a caso entro una distanza k, ma tenendosi dentro l'immagine"
dx = randint(-k, k)
dy = randint(-k, k)
X = bound(x+dx, 0, W-1) # mi tengo dentro l'immagine
Y = bound(y+dy, 0, H-1) # mi tengo dentro l'immagine
return img[Y][X]
# Esempio con k=5
rumore = applica_filtro_XY(img, lambda x, y, imm, W, H: scegli_vicino_a_caso(x, y, imm, W, H, 2))
visd(rumore)
# %% [markdown] slideshow={"slide_type": "subslide"}
# ## Effetto **lente**
#
# Voglio ingrandire/rimpicciolire una zona:
# - centrata alle coordinate **x,y**
# - di un raggio **r**
# - ingrandendo/rimpicciolendo di un fattore **k**
# - fuori dalla zona lasciamo l'immagine com'è
# %% [markdown] slideshow={"slide_type": "subslide"}
# ![](lens.png)
# %%%% lens slideshow={"slide_type": "fragment"}
from math import dist
# per dare l'effetto lente
# nella zona della lente
# fino a un raggio r
# mettiamo dei pixel che stanno a distanza K volte
# la loro distanza dal centro x1,y1 della lente
def lente(x : int, y : int, img : Immagine, W : int, H : int,
x1 : int, y1 : int, r : int, k : float) -> Colore:
"FiltroXY che allontana/avvicina i pixel attrorno al centro x,y di un fattore k"
D = dist((x,y), (x1,y1)) # distanza dal centro
if D > r: # se siamo fuori dal raggio
return img[y][x] # lasciamo il pixel com'è (lo leggiamo)
# altrimenti amplifichiamo le due proiezioni dx e dy di un fattore k
dx = (x-x1)*k
dy = (y-y1)*k
# ci assicuriamo di essere nella immagine
X = bound(x1+dx,0,W-1) # alla peggio prendo il pixel del bordo più vicino
Y = bound(y1+dy,0,H-1)
return img[Y][X] # e torniamo il pixel più lontano/vicino al centro
# %% slideshow={"slide_type": "subslide"}
# esempio
# se k<1 prendo i pixel più vicini al centro e l'effetto lente INGRANDISCE (qui k=0.5)
ingrandita = applica_filtro_XY(img,
lambda x, y, img, W, H: lente(x, y, img, W, H, 100, 100, 100, 0.5) )
# se k>1 prendo i pixel più lontani dal centro e l'effetto lente RIMPICCIOLISCE (qui k=2)
rimpicciolita = applica_filtro_XY(img,
lambda x, y, img, W, H: lente(x, y, img, W, H, 100, 100, 100, 2 ) )
visd(ingrandita), visd(rimpicciolita)
None