1-Immagini digitali
Un'immagine digitale bianco nero viene descritta da una matrice mxn di pixels (contrazione di picture elements). Il generico pixel p(i,j), il cui valore numerico puo' essere intero o reale, fornisce la luminosita' del punto di coordinate (i,j) dell'immagine.
Formato PGM (Portable Gray Map). Nella convenzione PGM il file che contiene le informazioni sull'immagine e' organizzato nel modo seguente:
P2
# eventuali commenti
numero_righe, numero_colonne
valore_livello_bianco
p(1,1)
p(1,2)
.
.
p(1,n)
p(2,1)
p(2,2)
.
.
dove:
P2 indica al sistema che visualizzera'
il file sullo schermo che si tratta di una immagine bainco nero nel formato
PGM
# eventual commenti sono
commenti arbitrari preceduti da #
numero_righe e numero_colonne sono
due interi che determinano le dimensioni dell'immagine (matrice dei pixels)
valore_livello_bianco e' un intero
che determina il valore numerico del bianco (generalmente 255), mentre
il nero e' 0.
p(i,j) sono i valori numerici dei
pixels (fra 0 e valore_livello_bianco) relativi alla posizione (i,j). Essi
sono ordinati per righe.
Esistono diversi programmi per visualizzare un'immagine sullo schermo
di un computer. Due pacchetti che girano sotto linux sono xv
e gimp.
Ad esempio, se immagine.pgm e' il nome di un file che contiene un'immagine
codificata come file PGM, allora il comando
xv immagine.pgm
visualizza sullo schermo il contenuto di immagine.pgm.
Esempio di file PGM
P2
# esempio semplice
4,4
255
0
0
0
0
0
255
255
0
0
255
255
0
0
0
0
0
La stessa immagine poteva essere memorizzata come
P2
# esempio semplice
4,4
255
0 0 0 0
0 255 255 0
0 255 255 0
0 0 0 0
File ASCII e file RAW
Un file PGM puo' essere memorizzato nella modalita' ASCII o nella modalita'
RAW.
Nella modalita' ASCII il valore del generico pixel viene scritto nella
sua rappresentazione decimale con caratteri ASCII. Ad esempio. se il valore
del pixel e' 123, vengono scritti i numeri 1,2 e 3, consumando quindi tre
byte di memoria. Nella rappresentazione RAW, il pixel il cui valore e'
123 viene memorizzato con il singolo byte il cui valore e' 123. In questo
modo un solo byte e' sufficiente per ciascun pixel. La rappresentazione
ASCII e' piu' comoda perche' permette di essere visualizzata e manipolata
piu' facilmente. Ha lo svantaggio che e' piu' ingombrante. Noi useremo
la rappresentazione ASCII.
Per semplicita' eviteremo di scrivere commenti nei file PGM.
Esempio 1. Costruiamo un'immagine 100x100 che abbia valori di grigio che sfumano da 0 a 255 secondo la regola p(i,j)=i+j mod 256. Usiamo il linguaggio FORTRAN 90. E' possibile comunque usare un qualsiasi altro linguaggio di programmazione visto che non svolgiamo operazioni particolarmente complesse.
Program grigi
implicit none
integer :: i,j
open(file="grigi.pgm", unit=2)
write(2,'(A)') "P2"
write(2,*)100,100
write(2,*) 255
do i=1,100
do j=1,100
write(2,*)mod(i+j,256)
end do
end do
end program grigi
L'immagine che si ottiene in questo modo e' la seguente:
Esempio 2. Costruiamo una immagine 200x200 tale che il pixel di posto (i,j) sia 255 (bianco) se i+j e' un numero primo, sia 0 (nero) altrimenti. Per questo ci occorre una funzione (function in fortran) che dato un intero p ci dice se p e' primo o no.
Program evidenzia_primi
implicit none
integer :: i,j,p,n
logical :: primo
n=200
open(file="primi.pgm", unit=2)
write(2,'(A)') "P2"
write(2,*)n,n
write(2,*) 255
do i=1,n
do j=1,n
if (primo(n*(i-1)+j))
then
write(2,*) 255
else
write(2,*)0
end if
end do
end do
end program evidenzia_primi
function primo(p)
implicit none
integer :: i,p,q
primo :: logical
q=sqrt(p*1.0)
primo=.true.
do i=2,q
if(mod(p,i)==0) then
primo=.false.
exit
end if
end do
end function primo
La figura che si ottiene e' la seguente
Un pochino piu' complicato, ma vale la pena farlo,
e' numerare i pixels a spirale che si dipana dal centro del quadrato. In
questo modo si ottiene la spirale di Ulam.
Il programma che la genera e' il seguente
program spirale_di_ulam
implicit none
integer :: i,j,n,p,c,lato,primo,k,ell,giri
integer, dimension(:,:),allocatable
:: a
write(*,*)"n= (semiampiezza)"
read(*,*)c
n=2*c-1
allocate(a(n,n))
a=0
lato=0
a(c,c)=0
p=1
i=c
j=c
giri=c-1
! traccia i quattro lati della spirale
do k=1,giri
! tratto orizzontale dx
lato=lato+1
do ell=1,lato
p=p+1
j=j+1
a(i,j)=primo(p)
end do
!tratto verticale basso
do ell=1,lato
p=p+1
i=i+1
a(i,j)=primo(p)
end do
!tratto orizzontale sx
lato=lato+1
do ell=1,lato
p=p+1
j=j-1
a(i,j)=primo(p)
end do
!tratto verticale alto
do ell=1,lato
p=p+1
i=i-1
a(i,j)=primo(p)
end do
end do
! scrivi spirale
open(unit=2,file='ulam.pgm')
write(2,'(A)')'P2'
write(2,*)n,n
write(2,*)255
do i=1,n
do j=1,n
write(2,*)a(i,j)
end do
end do
end program spirale_di_ulam
function primo(p)
implicit none
integer :: p,primo,i,sp,q
sp=sqrt(p*1.d0)
q=255
do i=2,sp
if (mod(p,i)==0)
then
q=0
exit
end if
end do
primo=q
end function primo
2-Manipolazione dei pixel
Esempio 3. Vediamo come e' possibile trasformare
un'immagine in negativo. Supponiamo di avere nel file foto.pgm una fotografia
che vogliamo leggere, trasformare in negativo e registrarla in un file
diverso. La trasformazione che dobbiamo applicare a ciascun pixel e' f(p)=255-p
Program negativo
implicit none
integer :: i,j,p,m,n
character(length=2) :: ch
open(file="foto.pgm", unit=2)
open(file="foto_negativa.pgm", unit=3)
read(2,'(A)')ch
write(3,'(A)') ch
read(2,*)m,n
write(2,*)m,n
read(3,*)p
write(2,*) p
do i=1,m
do j=1,n
read(2,*)p
write(3,*)255-p
end do
end do
end program negativo
Esempio 4. Cosa succede dell'immagine se applichiamo la trasformazione f(p)=min(255,p+20) oppure f(p)=max(0,p-20). Cosa succede invece se la trasformmazione e' f(p)=min(255, 2p) oppure f(p)=parte_intera(p/3)? Osservate che le operazioni di min e max servono a tenere il valore della funzione dentro il segmento consentito [0,255]. Osservate ancora che occorre prendere la parte intera. Mediante queste trasformazioni si cambiano globalmente la luminosita' e il contrasto di una immagine. Volendo, potete aumentare il contrasto e schiarire le zone scure lasciando la situazione inalterata nelle zone chiare, ad esempio con
f(p)=p se p>128
f(p)=max(2p,128) se p<=128
Ancora provate a portare in negativo solo le zone "buie" dell'immagine con
f(p)=255-p se p<=128
f(p)=p se p>128.
Ecco cosa si ottiene a partire dalla seguente immagine a colori
se usiamo come soglia p=50 (per come si codificano le immagini a colori
si rimanda al successivo punto)
Immagini a colori, modalita'
RGB
Una immagine a colori puo' essere memorizzata mediante la modalita'
RGB (Red, Green, Blue). In questo modo ad ogni pixel viene assegnata una
terna di numeri r,g,b, dove r fornisce l'intensita' del rosso, g l'intensita'
del verde e b l'intensita' del blu. Il file che viene costruito e' organizzato
come segue
P3
# eventuali commenti
numero_righe, numero_colonne
massima_luminosita'
r(1,1) g(1,1) b(1,1)
r(1,2) g(1,2) b(1,2)
.
.
.
Cio' che cambia rispetto ad un file b/n e' la prima riga che contiene P3 a indicare che e' un file a colori nella modalita' RGB e la presenza delle terne di pixels (un valore per ogni colore) anziche' dei singoli valori. Le terne dei valori rgb possono non stare sulla stessa riga ma essere distribuite su righe consecutive. I file di immagini a colori hanno generalmente l'estensione PNM o PPM.
Potete provare a effettuare le manipolazioni di contrasto e luminosita'
sui singoli colori in modo diverso da colore a colore, oppure potete scambiare
il rosso col blu o portare solo il verde in negativo o mescolare i colori
su ciascun pixel secondo una regola prefissata, ad esempio, r'=3r+g-2b
mod 256, g'=r+2g+b mod 256, b'=r-g+b mod 256 e vedere l'effetto sull'immagine.
Come e' possibile trasformare un'immagine da colori a b/n?
Esempio 5. (Insiemi di Julia e bacini di attrazione). Sapreste costruire un'immagine che rappresenti i bacini di attrazione del metodo di Newton per risolvere equazioni del tipo x^n-1=0?
Altri formati
La codifica di tipo PGM, anche nella modalita' RAW ha lo svantaggio di
essere molto ingombrante. Esistono altri modi di rappresentare file di
immagini piu o meno vantaggiosi. Un formato particolarmente usato, che
pero' conduce ad un degrado qualitativo dell'immagine e' il formato JPG.
Esso si basa su tecniche di compressione ottenute mediante trasformate
di coseni. Vedremo piu' avanti cosa questo significhi.
Esempio 6. videnziare i contorni. Sapreste affrontare il problema
di evidenziare i contorni di una immagine assegnata?
3-Manipolazioni geometriche.
E' possibile ruotare una immagine attorno al pixel di coordinate (i0,j0)
semplicemente applicando una rotazione alle coordinate di ciascun pixel,
cioe' copiando nel pixel di coordinate (i,j) il valore del pixel di coordinate
(i',j') per i=1,...m, j=1,..n, dove
i'=i0+[ (i-i0)*cos(a)+(j-j0)*sin(a)]
j'=j0+[(i-i0)*sin(a)-(j-j0)*cos(a)]
Il programma in fortran 90 che realizza questa trasformazione e' il
seguente
! ruota un'immagine attorno il pixel (i0,j0)
program rotazione
implicit none
integer :: i, j, ip, jp, q, m, n
integer :: red, green,
blue, i0, j0
integer, dimension(:,:), allocatable
:: r, g, b
character(len=2):: ch
real(kind(0.d0)) :: a
write(*,*)"assegna l'angolo di rotazione
in radianti"
read(*,*) a
! apri i file di lettura e scrittura
open(unit=2,file='image.pgm')
open(unit=3,file='rotated.pgm')
read(2,'(A)') ch
write(3,'(A)') ch
read(2,*) n,m
write(3,*) n,m
read(2,*) q
write(3,*) q
! leggi l'immagine
allocate(r(n,m),g(n,m),b(n,m))
do i=1,n
do j=1,m
read(2,*) r(i,j), g(i,j), b(i,j)
end do
end do
! ruota l'immagine attorno al centro
i0=n/2; j0=m/2
do i=1,n
do j=1,m
ip= i0+(i-i0)*cos(a)+(j-j0)*sin(a)
jp=j0+(i-i0)*sin(a)-(j-j0)*cos(a)
if(ip>0 .and. ip<=n .and. jp>0 .and. jp<=m) then
red=r(ip,jp)
green=g(ip,jp)
blue=b(ip,jp)
else
red=0
green=0
blue=0
end if
write(3,*)red,green,blue
end do
end do
end program rotazione
Ecco il risultato che si ottiene con a=1
a partire dall'immagine originale dei cammelli
Sapreste deformare l'immagine in modo che l'angolo di rotazione sia
inversamente proporzionale alla distanza del pixel dal centro dell'immagine
di coordinate (i0,j0)? Per questo basta cambiare la parte in cui
si calcolano ip e jp ad esempio con
a=sqrt(float((i0-i)**2+(j0-j)**2))/10+0.01
a=6.28/a
ip= i0+(i-i0)*cos(a)+(j-j0)*sin(a)
jp=j0+(i-i0)*sin(a)-(j-j0)*cos(a)
il 0.01 che e' stato aggiunto ad a, serve per evitare situazioni di
singolarita' nel centro di rotazione. Ecco il risultato che si ottiene
Esempio 7. Immagini anamorfiche. Un modo interessante per applicare deformazioni ad una immagine assegnata consiste nel fare riflettere l'immagine originale in uno specchio cilindrico o in una sfera. Ad esempio, supponete di avere uno specchio cilindrico posto su un piano orizzontale P e considerate un generico raggio che parte dal vostro occhio, colpisce la superficie del cilindro, viene riflesso e interseca il piano orizzontale P in un punto p. Lo stesso raggio se non e' riflesso, prosegue oltre il cilindro e interseca un piano obliquo Q posto dietro il cilindro in un punto q. Sapreste scrivere le relazioni che legano le coordinate di p e di q? Se siete in grado di fare questo, potete scrivere un programma che data un'immagine generica genera una immagine anamorfica che puo' essere "riletta" fisicamente riflettendola in uno specchio cilindrico.