Elaborazione digitale di immagini

Argomenti trattati:
1-Immagini digitali
   1.1-Immagini create dai numeri primi: la spirale di Ulam
2-Manipolazioni dei pixel
   2.1- Insiemi di Julia e bacini di attrazione
3-Manipolazioni geometriche
   3.1-Manipolazioni geometriche con funzioni di variabile complessa

4-Decomposizione spettrale, filtraggio e compressione
5-Restauro di immagini degradate
6-Crittografia con le immagini
 

1-Immagini digitali

Un'immagine digitale in bianco nero può essere descritta da una matrice m×n di pixel (contrazione di picture element). Il generico pixel p(i,j), il cui valore numerico può essere intero o reale, fornisce la luminosità del punto di coordinate (i,j) dell'immagine.

Formato PGM (Portable Gray Map). Nella convenzione PGM il file che contiene le informazioni sull'immagine è organizzato nel modo seguente:

P2
# eventuali commenti
numero_colonne, numero_righe
valore_livello_bianco
p(1,1)
p(1,2)
.
.
p(1,n)
p(2,1)
p(2,2)
.
.
 

dove:
P2 indica al sistema che visualizzerà il file sullo schermo che si tratta di una immagine bianco nero nel formato PGM
# eventuali commenti  sono commenti arbitrari preceduti da #
numero_colonne e numero_righe sono due interi che determinano le dimensioni dell'immagine (matrice dei pixels)
valore_livello_bianco è un intero che determina il valore numerico del bianco (generalmente 255), mentre il valore numerico del nero è 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.

Maggiori informazioni si trovano nel sito netpbm.sourceforge.net

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 è il nome di un file che contiene un'immagine codificata nel formato 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 puό essere memorizzato nella modalità ASCII o nella modalità RAW.
Nella modalità ASCII il valore del generico pixel viene scritto nella sua rappresentazione decimale con caratteri ASCII. Ad esempio. se il valore del pixel è 123, vengono scritte le tre cifre 1,2 e 3, consumando quindi tre byte di memoria, uno per ciascuna cifra, più altri tre byte per gli spazi di separazione. Nella rappresentazione RAW, il pixel il cui valore è 123 viene memorizzato con il singolo byte il cui valore è 123. In questo modo un solo byte è sufficiente per ciascun pixel. Nella rappresentazione RAW la stringa P2 va sostituita con P5. La rappresentazione ASCII è più comoda perché permette di essere visualizzata e manipolata più facilmente. Ha lo svantaggio che è più ingombrante. Noi useremo la rappresentazione ASCII.

Per semplicità eviteremo di scrivere commenti nei file PGM.

L'applicazione Gimp di Linux permette di aprire un file immagine in un qualsiasi formato e salvarla in un altro formato diverso. Ad esempio, una fotografia digitale salvata come file jpeg puό essere aperta da Gimp e salvata come file PGM.

Il linguaggio di programmazione Octave permette in modo semplice di trasformare una matrice di numeri in una immagine usando la stessa logica dello standard pgm. Se ad esempio la variabile A contiene una matrice di m righe e n colonne con valori numerici A(i,j), il comando

imshow(A, [nero,bianco]);

mostra sullo schermo una immagine formata da m×n pixel dove la luminosità del pixel di posizione (i,j) è data da A(i,j) e dove l'intervallo della scala dei grigi è [nero, bianco], cioe il valore numerico di nero corrisponde al nero mentre il valore numerico di bianco corrisponde al bianco. Ponendo il vettore vuoto [ ] al posto di [nero,bianco] i valori di nero e di bianco vengono presi uguali al minimo e al massimo dei valori di A. La sintassi del comando di imshow puό essere diversa a seconda della versione di Octave.

Esempio 1. Il programma Octave seguente costruisce un'immagine n×n con valori di grigio che sfumano da nero a bianco secondo la regola A(i,j)=i+j, e dove 2 corrisponde a nero e 2n a bianco.

function grigi (n)
  a=zeros(n);
  for i=1:n
  for j=1:n
  a(i,j)=i+j;
  end
  end
  imshow(a);
end

L'immagine che si ottiene in questo modo è la seguente:

se poi vogliamo creare un file che contiene i dati di questa immagine nel formato pgm basta inserire nella function precedente i seguenti comandi:

  fid=fopen('grigi.pgm', 'w');
  fprintf(fid, 'P2\n');
  fprintf(fid, '%d , %d \n' , n , n);
  fprintf(fid, '255\n');
  for i=1:n

    for j=1:n
      fprintf(fid, '%d\n', a(i,j) );
    end
  end

Il comando fopen di Octave apre un file per la lettura/scrittura la sintassi è fid=fopen(nomefile, modo) dove nomefile è una stringa col nome del file che si vuole aprire, modo è una stringa che contiene la modalità di apertura, in particolare:

'w' write (file di scrittura)
'r' read (file di lettura)

la variabile fid prende come valore un numero intero che servirà a identificare il file di lettura/scrittura. Infatti, l'istruzione

fprintf(fid, 'scrivo %d',n);

scrive il valore intero della variabile n preceduto dalla parola scrivo. La sintassi per i formati di scrittura è la stessa del linguaggio C.

2.1-Immagini create dai numeri primi: la spirale di Ulam

Immagini interessanti possono essere costruite per evidenziare proprietà dei numeri primi


Esempio 2. Costruiamo una immagine n×n tale che il pixel di posto (i,j) sia bianco se n(i-1)+j è un numero primo, sia nero altrimenti. Per questo ci occorre una funzione che dato un intero p ci dice se p è primo o no.

function evidenzia_primi (n)
     imag=zeros(n);

     for i=1:n
          for j=1:n
               if (primo(n*(i-1)+j))
                    imag(i,j)=1;

               end
          end
     end
     imshow(imag);

end

function v=primo(p)
     v=1;
     for i=2:sqrt(p)
          if(mod(p,i)==0)
               v=0;
               break;

          end
     end
end

La figura che si ottiene con n=100 è la seguente

Un pochino più complicato, ma vale la pena farlo, è numerare i pixel lungo una spirale che si dipana dal centro del quadrato, partendo dal numero 1









e accendere i pixel che corrispondono ai numeri primi. In questo modo si ottiene la spirale di Ulam.

Un programma che la genera è il seguente

function spirale_di_ulam (n)
     centro=n;
     n=2*n+1;
     a=zeros(n);
     lato=0;
     p=1;
     i=centro;
     j=centro;
     giri=centro-1;
% traccia i quattro lati della spirale
     for k=1 : giri
% tratto sud: movimento orizzontale a destra
          lato=lato+1;
          for ell=1 : lato
          p=p+1;
          j=j+1;
          a(i,j)=primo(p);
     end
% tratto est: movimento verticale in alto
          for ell=1 : lato
               p=p+1 ;
               i=i-1 ;
               a(i,j)=primo(p) ;
          end
% tratto nord: movimento orizzontale a sinistra
          lato=lato+1 ;
          for ell=1 : lato
               p=p+1;
               j=j-1;
               a(i,j)=primo(p);
          end
% tratto ovest: movimento verticale in basso
          for ell=1 : lato
               p=p+1;
               i=i+1;
               a(i,j)=primo(p);
          end
     end
% scrivi spirale
     fid=fopen('ulam.pgm', 'w');
     fprintf(fid, 'P2\n');
     fprintf(fid, '%d %d \n', n,n);
     fprintf(fid, '255\n');
     for i=1 : n
          for j=1 : n
               fprintf(fid, '%d\n', a(i,j)*255);
          end
     end
     fclose(fid);
end

Abbiamo spezzato la numerazione degli interi lungo la spirale in quattro parti, ciascuna per ciascun lato della spirale. Infatti la variabile k conta i giri della spirale intorno al suo centro, e per ciascun giro si percorrono i quattro lati.

Il linguaggio Octave, essendo interpretato, ha tempi di elaborazione molto elevati. In particolar modo questo succede in presenza di cicli for annidati. Usando Octave siamo in grado di trattare dimensioni relativamente basse e quindi immagini piccole. Per poter svolgere elaborazioni a dimensione più ampia occorre implementare gli algoritmi con linguaggi più efficienti tipo il linguaggio C o il Fortran 90. Una alternativa accettabile è usare Matlab che usa la stessa sintassi di Octave ma che è molto più efficiente, oltre che ad essere un prodotto commerciale.


Provate a evidenziare le coppie di numeri interi (p,q) tali che p*p+q*q è un numero primo. Per questo create una immagine di dimensione (2m+1)×(2m+1), dove m è un intero assegnato ad esempio m=100, e accendete il pixel in posizione (i,j) se (i-m)*(i-m)+(j-m)*(j-m) è un numero primo. Provate a creare immagini analoghe con condizioni diverse del tipo |p*p-q*q|+k è primo per un fissato k=1,2,3; oppure p*p+q*q+k è primo per un fissato k=1,2,3; oppure p*p+q*q+k è primo sia per k=1 che per k=3; o, ancora p*q+1 è primo. In quest'ultimo caso si ottiene
 

Guardando attentamente questa immagine si riescono a evidenziare particolari forme geometriche.

Provate a evidenziare le coppie (p,q) di numeri primi consecutivi (primi gemelli).

2-Manipolazione dei pixel
Esempio 3. Vediamo come è 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 è f(p)=255-p

function negativo(nome_file)     
    fidr=fopen(nome_file, 'r');               %    aprefile di lettura
    fidw=fopen('negativo.pgm', 'w');    
%    apre file di scrittura
    ch=fscanf(fidr, '%s', '1');                %    legge numero magico come stringa
    fprintf(fidw, '%s\n', ch);                   
%    scrive numero magico come stringa
    n=fscanf(fidr, '%d', '1');                  
%    legge numero colonne come intero
    m=fscanf(fidr, '%d', '1');                 
%    legge numero righe come intero
    fprintf(fidw, '%d %d\n', [ n m]);         
%    scrive numero di righe e di colonne
    liv=fscanf(fidr, '%d', '1');                 
%    legge numero di livelli
    fprintf(fidw, '%d\n', liv);                    
%    scrive numero di livelli
    for i=1:m
        for j=1:n
            pix=fscanf(fidr, '%d', '1');         
%    legge valore pixel (i,j)
            fprintf(fidw,'%d\n', 255-pix);                
%    scrive in negativo il valore del pixel (i,j)
        end
    end
end

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 trasformazione è 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 luminosità 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, modalità RGB
Una immagine a colori può essere memorizzata mediante la modalità RGB (Red, Green, Blue). In questo modo ad ogni pixel viene assegnata una terna di numeri r,g,b, dove r fornisce l'intensità del rosso, g l'intensità del verde e b l'intensità del blu. Il file che viene costruito nel formato PPM (Portable Pixel Map) è organizzato come segue

P3
# eventuali commenti
numero_colonne, numero_righe
massima_luminosità
r(1,1)   g(1,1)   b(1,1)
r(1,2)   g(1,2)   b(1,2)
.
.
.

Ciò che cambia rispetto ad un file b/n è la prima riga che contiene P3 a indicare che è un file a colori nella modalità RGB e la presenza delle terne di pixel (un valore per ogni colore) anziché dei singoli valori. Le terne dei valori rgb possono non stare sulla stessa riga ma essere distribuite su righe consecutive. Nella codifica RAW la stringa P3 viene sostituita da P6.

Maggior dettagli si trovano a netpbm.sourceforge.net

Il formato PNM, acronimo di Portable Any Map, è giusto un'astrazione dei formati PBM, PGM, and PPM. Cioè il termine PNM si riferisce collettivamente a PBM, PGM, e PPM.

Usando octave, dopo aver creato le tre matrici R,G,B che danno la luminosità delle componenti di rosso, verde e blu, per mostrare l'immagine a colori corrispondente basta digitare imshow(R,G,B,[ ]); oppure a seconda della versione di octave, imshow(H,[ ]); dove H è una matrice a tre indici tale che

H(:,:,1)=R; H(:,:,2)=G, H(:,:,3)=B;

Potete provare a effettuare le manipolazioni di contrasto e luminosità 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 è possibile trasformare un'immagine da colori a b/n?
Nella foto che trovate qui di seguito provate a modificare solamente la componente g del verde di ogni pixel (lasciando rosso e blu inalterati) nel seguente modo: se il valore g del pixel è minore o uguale a 200 sostituite g con 200-g altrimenti lasciate g inalterato. L'effetto è quello di mettere in negativo solo alcuni livelli di luminosità del verde.

Immagine originale

Immagine modificata

2.1- Insiemi di Julia e bacini di attrazione

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 z^n-1=0? Ad esempio:
 
Considerate il metodo di Newton applicato all'equazione z3-1=0 che genera la successione definita da

zk+1=zk-(zk3-1)/(3zk2),

a partire da z0. Create una immagine nel seguente modo: per p intero ponete h=1/p e considerate una griglia di (2p+1)×(2p+1) punti zi,j nel quadrato [-1,1]×[-1,1] del piano complesso definiti da wi,j=jh+Iih, i=-p,p, j=-p,p, dove I denota l'unità immaginaria tale che I2=-1. Associate ad ogni zi,j un pixel di una immagine (2p+1)×(2p+1). Posto z0=wi,j considerate la successione zk generata dal metodo di Newton e colorate rispettivamente di rosso verde e blu il pixel che corrisponde al punto wi,j se la successione zk converge a 1, -1/2+i*sqrt(3)/2, -1/2-i*sqrt(3)/2. Come test di convergenza usate la condizione |zk -zk+1| minore di 1/10000.
Fate un analogo disegno dando ai tre colori una intensità diversa a seconda del numero k di iterazioni richieste perché sia soddisfatto il test di convergenza.
Provate a dare ancora una colorazione diversa assegnando la quantità r,g,b di rosso, verde e blu in funzione del numero di iterazioni mediante tre funzioni diverse, ad esempio r=1231k mod 256, g=2753k mod 256, b=3127k mod 256.
Provate a tracciare disegni analoghi per il metodo di Newton applicato alle equazioni: x2-x-1=0, x-x-2=0 e 1-x-3=0 e confrontate i disegni cosi' ottenuti.
Modificate il programma in modo che anziché il quadrato [-1,1]×[-1,1] viene considerato il quadrato di centro (x0,y0) e semilato s.
Ecco alcune immagini cosi' ottenute

Equazione x3-1=0

Equazione x2-x-1=0

Equazione x-x-2=0

Equazione 1-x-3=0

È possibile creare dei filmati come il seguente che traccia i bacini di attrazione del metodo di Newton applicato all'equazione x^p-1=0 con p che varia tra 3 e 4. Il filmato è stato creato da uno studente del corso a.a. 2008-2009.

Esempio 6. Evidenziare i contorni. Sapreste trattare il problema di evidenziare i contorni di una immagine assegnata? Provate a generare le seguenti immagini
 
 

I contorni di una immagine sono i punti in cui la luminosità ha una variazione brusca. Per evidenziarli sarebbe sufficiente creare una nuova immagine con pixel uguali a 0 (nero) dove la luminosità è costante o subisce piccole variazioni e con pixel 255 (bianco) nei punti in cui la luminosità ha forti variazioni. Per una funzione da f:R→ R sufficientemente regolare la variazione è espressa dalla sua derivata prima: intuitivamente |f'| è piccola dove la f ha piccole variazioni è grande in corrispondenza delle grandi variazioni di f. Anche la derivata seconda f” è legata alle variazioni di f. Per una funzione definita su un dominio discreto (1,2,3,...,n) la derivata prima viene sostituita con la differenza prima che nel punto i vale (f(i+1)-f(i-1))/2 e la derivata seconda con la differenza seconda che nel punto i vale f(i-1)-2f(i)+f(i+1). Nel caso di funzioni di due variabili x,y, come accade per le immagini possiamo considerare come indice di salto la somma delle derivate parziali rispetto alla x e rispetto alla y, oppure il laplaciano cioè la somma delle derivate seconde rispetto alla x e rispetto alla y. Nel caso discreto abbiamo rispettivamente (f(i+1,j)-f(i-1,j) +f(i,j+1)-f(i,j-1))/2 e f(i-1,j)+f(i+1,j)+f(i,j-1)+f(i,j+1)-4f(i,j). Quest'ultima espressione è detto laplaciano discreto. Provate a sostituire l'immagine originale con quella ottenuta applicando il laplaciano discreto a tutti i punti dell'immagine riaggiustando il fattore di scala in modo da poter avere sia valori di bianco che valori dii nero. Provate a costruire altri operatori discreti che siano discretizzazioni di derivate di ordine più elevato.

Altri formati    La codifica di tipo PGM, anche nella modalità RAW ha lo svantaggio di essere molto ingombrante. Esistono altri modi piu o meno vantaggiosi di rappresentare file di immagini. Un formato particolarmente usato, che però conduce ad un degrado qualitativo dell'immagine è il formato JPG. Esso si basa su tecniche di compressione ottenute mediante trasformate di coseni. Vedremo più avanti cosa questo significhi.
 

3-Manipolazioni geometriche.
È possibile ruotare una immagine attorno al pixel di coordinate (i0,j0) semplicemente applicando una rotazione alle coordinate di ciascun pixel, cioè 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 che realizza questa trasformazione è il seguente
 

% ruota un'immagine attorno il pixel (i0,j0)
function rotazione(immagine, angolo)
% apre il file 'immagine' e legge l'immagine rgb nelle matrici r,g,b
%
apre il file di output con identificatore fidw e scrive l'intestazione
     ..........
     ..........
% ruota l'immagine attorno al centro

    i0 = round(m/2); a=angolo;
    j0 = round(n/2);

    for i = 1 :  m
        for j = 1  :  n                                            %   seleziona il pixel di posizione (i,j) dell'immagine ruotata
            ip =  round(i0+(i-i0)*cos(a)+(j-j0)*sin(a));        %   calcola il pixel di posizione (ip,jp) dell'immagine di provenienza
            jp = round(j0+(i-i0)*sin(a)-(j-j0)*cos(a));
            if(ip>0  &&  ip<=m  &&  jp>0  && jp<=n)
                red = r(ip,jp);                                  %   se il pixel di provenienza sta nel supporto dell'immagine
                green = g(ip,jp);                             %   prende il valore dell'immagine
                blue = b(ip,jp);
            else
                red = 0;                                            %   altrimenti prende il valore nullo (nero)
                green = 0;
                blue = 0;
            end
            fprintf(fidw, '%d %d %d\n',  red, green, blue);    % scrive i valori nel file
        end
    end
end

Ecco il risultato che si ottiene con angolo=1 radiante

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((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 numero 0.01 che è stato aggiunto ad a, serve per evitare situazioni di singolarità 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 su uno specchio cilindrico o su 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 è riflesso, prosegue oltre il cilindro e interseca in un punto q un piano obliquo Q posto dietro il cilindro.  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 puό essere "riletta" fisicamente riflettendola in uno specchio cilindrico.


Esercizi un po piu semplici sono i seguenti:

1-- Simulare come appare una fotografia se viene arrotolata attorno a un semi cilindro di altezza pari ad una delle due dimensioni della foto e raggio pari all'altra dimensione diviso per pigreco. Per questo fare riferimento alla figura seguente

dove, in sezione, la linea blu rappresenta l'immagine originale ed A un suo generico pixel, mentre la linea rossa rappresenta l'immagine trasformata e B rappresenta il pixel trasformato di A. Quindi, data l'immagine blu calcolate la rossa. Un problema analogo consiste nel calcolare l'immagine blu data la rossa.

2-- Simulare come appare una fotografia se viene posta sotto una lente di vetro a forma di semi cilindro e vista dall'alto, assumendo che il vetro abbia un fattore di rifrazione c=1.5, cioe' gli angoli alfa e beta formati dai raggi incidente e rifratto rispetto al raggio del cilindro sono tali che sin alfa = c sin beta. Per questo, fare riferimento alla seguente figura

dove, in sezione, la linea blu rappresenta l'immagine originale ed A un suo generico pixel, mentre la linea rossa rappresenta l'immagine trasformata e B rappresenta il pixel trasformato di A. In verde e' tracciato il raggio. Quindi, data l'immagine blu calcolare la rossa.

3-- Simulare come appare una immagine se viene riflessa da un cilindro lucido nel modo indicato in figura

L'immagine e' posta a fianco di un quarto di cilindro con superficie a specchio e viene vista dall'alto riflessa. I due raggi incidente e riflesso formano angoli uguali rispetto al raggio del cilindro.

3.1-Manipolazioni geometriche con funzioni di variabile complessa.
Interessanti trasformazioni si ottengono utilizzando funzioni di variabile complessa. Data un'immagine costituita dall'insieme dei pixel a(i,j), per i=1,...,m, j=1,...,n associamo alla coppia (i,j) un numero complesso z=z(i,j), ad esempio z=(j-j0)h + I(i-i0)*h dove abbiamo denotato con I l'unità immaginaria tale che I2=-1, e dove abbiamo scelto i0,j0 ed h a nostro piacere. Oltre all'applicazione (i,j)->z(i,j) consideriamo l'applicazione "inversa" z(i,j)->(i,j) tale che j è la parte intera di Re(z)/h +j0, mentre i è la parte intera di Im(z)/h+i0, dove abbiamo indicato con Re(.) e Im(.) la parte reale e la parte immaginaria di un numero complesso.
Consideriamo poi una funzione di variabile complessa y=f(z), ad esempio f(z)=z2/|z|, e costruiamo questa trasformazione tra coppie di interi (i',j') e (i,j)
(i,j)-> z(i,j), w=f(z), w->(i',j')
Costruiamo l'immagine che nel pixel di coordinate (i,j) ha il valore a(i',j') se (i',j') appartiene a [1,m]x[1,n], ha il valore 0 altrimenti. Come è fatta l'immagine deformata in questo modo?
Cosa accade con semplici funzioni quali f(z)=zt, se t è un numero complesso di modulo 1, oppure se t è un numero reale positivo?
Cosa accade con funzioni più complesse quali f(z)=z(z/|z|)k, per k intero? Oppure con f(z)=z-(z3-1)/(2z2)?

Questa e una traccia di programma Octave per deformare immagini

function trasf=complextransform(foto)
% leggi file
.......
% deforma
       k0=round(m/2);j0=round(n/2);
       h=2/max(m,n);
       trasf=zeros(m,n);

% scandisco supporto della foto trasformata
       for k=1:m
              for j=1:n
% calcolo numero complesso corrispondente
                     z=(j-j0)*h+i*(k-k0)*h;
% trasformo all'indietro
                     w=z^2;
% calcolo le coordinate del pixel corrispondente a w
                     kk=round(k0+imag(w)/h);
                     jj=round(j0+real(w)/h);
% assegno il colore
                     if(1<=kk && kk<=m && 1<=jj && jj<=n)
                            trasf(k,j)=foto(kk,jj);
                     else
                            trasf(k,j)=0;
                     end
              end
       end
       imshow(trasf,[]);
end


Ecco alcuni esempi






Sapreste simulare l'azione di una lente di ingrandimento trasformando una immagine in modo da ingrandirne una parte con continuità e quindi senza strappi lasciando la parte rimanente inalterata? Le seguenti immagini sono state create da studenti del corso dell'aa. 2008-2009.









4-Decomposizione spettrale, filtraggio e compressione.
Ricordiamo che un vettore  v=(v0,...,vn-1)  di n componenti  può essere rappresentato nella "base di Fourier" mediante degli opportuni coefficienti a(i) come
vi=a(0)+a(1) cos(i*2pi/n)+b(1) sin(i*2pi/n)+ a(2) cos(2i*2pi/n)+b(2) sin(2i*2pi/n)+...+a(m-1) cos((m-1)i*2pi/n)+b(m-1) sin((m-1)i*2pi/n)+ a(m) cos(mi*2pi/n)
dove si assume n=2m, con m intero, e pi denota pigreco. I coefficienti a(i) e b(i) possono essere calcolati, a partire dalle componenti di v,  mediante l'algoritmo FFT (Fast Fourier Transform) della trasformata veloce discreta di Fourier per il quale esistono numerose implementazioni in diversi linguaggi di programmazione. La rappresentazione data, riscritta come
vi=a(0)+A(1) cos(i*2pi/n + B(1))+ A(2) cos(2i*2pi/n + B(2))+ ...+A(m-1) cos((m-1)i*2pi/n + B(m-1))+A(m) cos(mi*2pi/n)
permette di interpretare v come una somma di vettori "oscillanti" con frequenze multiple intere di una frequenza fondamentale e dotati di varie ampiezze A(i) e fasi B(i). Questa trasformazione è molto usata dagli ingegneri per filtrare segnali. Infatti la decomposizione in termini di funzioni trigonometriche elementari (decomposizione spettrale) ci permette di conoscere le ampiezze A(i) e le fasi B(i) con cui ogni singola frequenza elementare compare nel "segnale". Con questa informazione possiamo fare manipolazioni di segnali molto efficaci, quali amplificare o ridurre le tonalità acute o gravi di un suono rappresentato in forma digitale mediante il vettore v. Ad esempio si può costruire un nuovo vettore w ottenuto amplificando le componenti in "bassa frequenza" sostituendo ad esempio i valori A(i) con 2A(i) se i è minore di n/4.  Analogamente possiamo amplificare o attenuare le alte o le medie frequenze e trasformare il vettore originale in diversi modi. Se il vettore rappresenta un segnale acustico, ad esempio un brano di musica, le operazioni descritte hanno l'effetto analogo a quello che si ottiene regolando i toni in un impianto hifi. Questo tipo di manipolazione numerica è esattamente quella che svolgono applicazioni quali "winamp" che riproducono musica digitale su di un pc.

La relazione tra le componenti del vettore v e i coefficienti a(i) e b(i) è la seguente: posto u=IDFT(v) il vettore trasformata discreta inversa di Fourier di v definito da

uj=v0+v1*exp(2j*i*pi/n )+v2*exp(4j*i*pi/n)+...+vn-1*exp(2j*i*(n-1)*pi/n)

dove i è l'unità immaginaria, allora vale

a(j)=(uj + conj(uj))/2, j=0,1,...,n/2, b(j)=(uj - conj(uj))/2

dove conj(x) indica il complesso coniugato di x. Per cui il filtro che possiamo costruire opera nel modo seguente:

Si può osservare che se v è reale allora il vettore u è tale che u0 e un/2+1 sono reali mentre uj è il complesso coniugato di un-j . Per cui, affinché un filtro f mantenga la realtà del segnale filtrato s, occorre che f sia ''simmetrico'' nel senso che fj=fn-j, j=1,...,n/2-1. Basta quindi definire il filtro in f0,...,fn/2. Un filtro ''passa alt'' è dato da

fj=1-cos(2*pi*j/n), j=0,...,n/2

infatti, dal grafico della funzione 1-cos(x) si vede che fj riduce l'ampiezza delle componenti in bassa frequenza, mentre un filtro passa basso è

fj=1+cos(2*pi*j/n), j=0,...,n/2

Come agisce invece il filtro seguente?

fj=1-cos(3*pi/2 + 2*pi*j/n), j=0,...,n/2

La cosa che a noi interessa fare adesso è applicare queste decomposizioni spettrali alle righe e alle colonne di una matrice di pixel relativa ad una immagine digitale in modo da poter amplificare o ridurre le "componenti in alta frequenza" cioè  andare ad amplificare o a ridurre il dettaglio dei piccoli particolari. Queste operazioni sono generalmente implementate nei pacchetti di manipolazione digitale di immagini quali gimp, dove si preferisce usare al posto della trasformata discreta di Fourier la trasformata discreta dei coseni. Il nome con cui questa operazione è conosciuta è "sharpening", cioè rendere più "incisa" l'immagine. Nel caso bidimensionale di una immagine mxn, con m ed n pari, il filtro f=(fij) basta definirlo per i=0,..,m/2, j=0,...,n/2 e poi estenderlo per simmetria come nel caso monodimensionale.

In Octave il comando u=fft(v) calcola la DFT del vettore v e ifft(u) calcola la IDFT di u. Nel caso di una matrice A mxn, il comando fft(A,n,2) calcola la DFT delle n colonne di A, mentre il comando fft(A,m,1) calcola la DFT delle m righe di A. Per cui B=fft(fft(A,n,2),m,1) fornisce la DFT bidimensionale (trasformata delle righe e delle colonne) della matrice A. Analogamente opera la ifft.

La function che segue mostra come è semplice implementare l'operazione di filtraggio descritta, una volta che abbiamo a disposizione un programma che ci calcola la trasformata discreta di Fourier e la sua inversa.


La function prende in input il nome del file in cui si trova l'immagine in bianco e nero nel formato pgm, legge l'immagine, aggiusta le sue dimensioni in modo che siano pari, e poi calcola la trasformata discreta di Fourier delle righe e delle colonne, effettua un filtraggio delle componenti in frequenza e ricostruisce l'immagine filtrata.

function c=filtra(filefoto)
% legge immagine
   fidr=fopen(filefoto, 'r');
   ch=fscanf(fidr, '%s', '1');
   n=fscanf(fidr, '%d', '1');
   m=fscanf(fidr, '%d', '1');
   foto=zeros(m,n);
   liv=fscanf(fidr, '%d', '1');
   for i=1:m
      for j=1:n
         foto(i,j)=fscanf(fidr, '%d', '1');
      end
   end

% ridimensiona a valore pari
   if(mod(m,2)==0)
      mm=m;
   else
      mm=m-1;
   end
   if(mod(n,2)==0)
      nn=n;
   else
      nn=n-1;
   end
   a=foto(1:mm,1:nn);
% costruisce il filtro
   filtro=ones(mm,nn);
   for i=1:mm/2+1
      for j=1:nn/2+1
         filtro(i,j)=1+(1-cos(pi*2*(i-1)/mm))*(1-cos(pi*2*(j-1)/nn));
      end
   end
% completo il filtro per simmetria
   filtro(1:mm/2+1, nn/2+2:nn)=filtro(1:mm/2+1, nn/2:-1:2);
   filtro(mm/2+2:mm, 1:nn/2+1)=filtro(mm/2:-1:2, 1:nn/2+1);
   filtro(mm/2+2:mm, nn/2+2:nn)=filtro(mm/2:-1:2, nn/2:-1:2);
% calcolo la IDFT
   ifftfoto=ifft(ifft(a,nn,2),mm,1);
   b=ifftfoto .* filtro;
% calcolo la DFT
   c=fft(fft(b,nn,2),mm,1);
   c=real(c);
% taglio i valori dei pixel tra 0 e 255
   c=min(255,max(0,c));
% mostro l'immagine
   imshow(c,[]);
   end

Ecco un esempio di immagine ottenuta a partire dalla fotografia dei cammelli amplificando le alte frequenze. A sinistra l'immagine filtrata, a destra quella originale. In questo caso, essendo l'immagine a colori, si è fatto il filtraggio sulle tre componenti R,G,B. Si puό vedere il maggior dettaglio nei particolari delle foglie e nei tronchi delle palme.

 

Cosa si ottiene se si applica un filtro che ''lascia passare'' solo le frequenze medie? Provare con il filtro dato dalla funzione f(x)=1-cos(x+3*pi/2) calcolata per x=2*pi*j/n.

Si possono ottenere risultati "eccessivi" con filtraggi strani e fattori di amplificazione che fanno uscire fuori dal segmento [0,255] molti valori dei pixel dell'immagine filtrata. Le immagini che seguono sono state ottenute dagli studenti con valori volutamente anomali del filtraggio o con filtraggi svolti in modo diverso a seconda del canale del colore.
  

La stessa tecnica di filtraggio può essere utilizzata per comprimere un'immagine. Questo avviene memorizzando solamente i valori delle ampiezze che corrispondono alle frequenze più basse e si basa sul fatto che generalmente le "alte frequenze", cioè la ricchezza di dettagli minuscoli, non sono presenti in tutte le parti di una immagine. Quindi decomponendo un'immagine in porzioni più piccole ad esempio 8x8 pixel, e calcolando la rappresentazione spettrale di queste sotto immagini, è possibile rappresentarle in modo abbastanza fedele memorizzando solo le basse frequenze nel caso in cui le alte abbiano valori in ampiezza trascurabili. Questa è l'idea alla base della compressione JPG.
 

5-Restauro di immagini degradate

Fotografie che sono venute sfocate per un diffetto di messa a fuoco dell'obiettivo possono essere in qualche modo rimesse a fuoco se si conosce il tipo di sfocatura. Supponiamo di avere scattato una foto ad una immagine costituita da un solo punto luminoso di intensità unitaria


con l'obiettivo regolato male si otterrà qualcosa tipo l'immagine seguente


dove abbiamo volutamente esagerato l'effetto della sfocatura. Questa nuova immagine sarà definita da dei pixel f(i,j), dove per comodità facciamo scorrere gli indici i e j da -k a k con 2k+1 l'ampiezza del supporto del puntolino sfocato. La tabella di numeri f(i,j) descrive quindi il tipo di sfocatura ed è chiamata in gergo Point-Spread Function, o più semplicemente PSF. I valori f(i,j) devono sommarsi ad 1 poiché la quantità di luminosità presente nell'immagine sfocata deve essere uguale a quella presente nell'immagine originale.
Allora, se a(i,j) per i=1,m, j=1,n sono i pixel di una immagine originale costituita da tutti pixel nulli fuorché un solo puntolino di intensità g in posizione (p,q), l'immagine sfocata b(i,j) avrà pixel uguali a

b(i,j)=g*f(i-p,j-q), |i-p|, |j-q| <= k

mentre b(i,j)=0 altrove.

L'immagine sfocata di una immagine generica costituita da più pixel non nulli sarà la somma delle immagini che si ottengono sfocando i singoli pixel. Vale cioè

b(i,j)=Somma_{p,q} a(p,q)*f(i-p,j-q)

dove la somma è fatta per p=1,m, q=1,n, su tutti gli indici per cui i-p e j-q sono compresi tra -k e k.
Dal punto di vista computazionale per sfocare una immagine in bianco e nero basta calcolare una doppia sommatoria. Per rimettere a fuoco una immagine basta risolvere un sistema lineare di m*n equazioni in m*n incognite, ammesso che la matrice del sistema sia non singolare.
Per immagini a colori la manipolazione va compiuta sui tre canali del rosso, verde e blu. Per una foto scattata con un macchina fotografica da 5 megapixel si devono risolvere tre sistemi di 5 milioni di equazioni e di incognite per poter rimettere a fuoco la foto.

6-Crittografia con le immagini

Una immagine rappresentata come file pnm è  un nascondiglio efficace per riporre informazioni segrete. Pensate che una immagine a colori 512x512, come quella dei cammelli, contiene più di 750.000 byte. Se modifichiamo un solo bit di ciascun pixel abbiamo la possibilità di nascondere quasi 100.000 byte cioè quasi centomila caratteri alfanumerici. D'altra parte se aumentiamo o caliamo di una unità  il valore di ciascun pixel dell'immagine, la tenue variazione di luminosità che otteniamo su ciascuno dei tre colori non risulta percepibile all'occhio umano. Per cui la foto che si ottiene modificandoi i valori numerici in questo modo  appare dal punto visivo identica all'immagine originale.

Nelle immagini che seguono sono stati modificati rispettivamente 1,2,3,4,5,6 bit di ciascun pixel. L'informazione che abbiamo nascosto nell'immagine è quella di parte dell'iimmagine originale stessa, costituita dai valori numerici dei pixel di parte dell'immagine originale dei cammelli.

    
Si osservi che nelle prime tre immagini non si apprezzano particolari variazioni.
L'istruzione che sostituisce i k bit meno significativi del byte pixel con  l'intero h (formato da al più k bit) è la seguente
pixel=pixel-mod(pixel,2**k)+h
Infatti l'istruzione mod(pixel,2**k) fornisce il valore di pixel modulo 2k  cioè  il numero intero formato dai k bit meno significativi di pixel.
Per decodificare l'informazione occorre estrarre l'intero formato dai k bit meno significativi di ciascun pixel. Per cui l'istruzione che si usa è
info=mod(pixel,2**k)
I vari pezzettini di informazione andranno poi "incollati" in un singolo byte per recuperare la lettera corrispondente al byte. Questo compito è lasciato per esercizio.
Per completare il programma che cripta e decodifica sono utili due funzioni: la prima, dato un carattere alfanumerico fornisce il byte con cui viene identificato (codice ASCII); la seconda, dato il codice fornisce il carattere alfanumerico corrispondente.