Elaborazione digitale di immagini

Argomenti trattati:
1-Immagini digitali
2-Manipolazioni dei pixel
3-Manipolazioni geometriche
4-Decomposizione spettrale, filtraggio e compressione
5-Restauro di immagini degradate
6-Crittografia con le immagini
 

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.