Compressione di immagini digitali. Algoritmi a confronto. Andrea Favilli 6 Marzo 2014 Ridurre le dimensioni delle immagini digitali penalizzando il meno possibile la qualità visiva: questa l’ambizione di ciò che andremo a trattare. Vedremo i principi che stanno dietro uno degli algoritmi più diffusi, il JPEG, acronimo di Joint Photographic Experts Group, con relativa implementazione e confronto con le possibili alternative. Capiremo quali sono gli effettivi vantaggi che portano tale standard ad una cosı̀ ampia diffusione. Vista l’esigenza di una computazione efficiente e rapida andremo ad utilizzare il pacchetto commerciale Matlab, in luogo dell’alternativa gratuita GNU Octave: il fatto che Matlab abbia a disposizione molti degli algoritmi matematici indispensabili nella nostra trattazione ci ha fatto scartare l’uso di un linguaggio a più basso livello di astrazione, penalizzando l’efficacia finale, ma rimanendo nel puro spirito sperimentale. Useremo shell scripting da terminale Linux per coordinare le operazioni delle singole funzioni e piccole parti di C per poter gestire i file in maniera agevole. Gli algoritmi in oggetto sono essenzialmente tre: il filo conduttore della nostra esperienza sta nel fatto che non opereremo “una tantum” sulla singola immagine, ma al contrario suddivideremo questa in sottounità (di dimensione variabile) e su tali oggetti andremo ad effettuare compressione. Non perdendoci in ulteriori spiegazioni anticipate, iniziamo immediatamente con la trattazione. Alcuni prerequisiti: codifiche di immagini. L’immagine digitale è concepita come un insieme di unità minime (dei piccoli quadratini) chiamate pixel, ciascuno di essi può assumere una certa luminosità e uno specifico tono di colore. La configurazione dei pixel è regolata da una o più matrici scritte secondo opportune regole di codifica: il fatto che un’immagine sia esprimibile in forma matriciale fa da subito riflettere su come molte delle conoscenze messe a disposizione dall’algebra lineare siano utili al trattamento della grafica. Vediamo rapidamente come sono fatti alcuni file: esaminiamo quei formati ad alto dispendio di spazio, costruiti a partire dalle risposte del sensore luminoso delle fotocamere. In poche parole, questo tipo di foto hanno una qualità pressochè assimilabile a quella dell’analogico, ma non sono per l’appunto gestibili. Le immagini in scala di grigio sono codificate nel formato .pgm (portable gray map): è possibile costruire un file di questo tipo prendendo un editor di testo e inserendo determinate informazioni secondo uno specifico ordine. 1 • Un codice identificativo del tipo di file, nel caso del .pgm scriveremo P2. • Eventuali commenti preceduti dal carattere #. • Le dimensioni della foto, rispettivamente larghezza e altezza. • La massima luminosità che il singolo pixel può assumere, lo standard è 255. • I fattori di luminosità dei singoli pixel (valori numerici da 0, il nero, a 255, il bianco). I pixel vengono esplorati per riga da sinistra verso destra. Riportiamo di seguito un possibile esempio: P2 #Immagine di prova. 1024 768 255 198 112 112 113 115 ... Da segnalare il fatto che i fattori di luminosità sono talvolta espressi in forma binaria e dunque non leggibili con un comune editor. Fortunatamente, alcuni software per la grafica (tra cui includiamo il blasonato Gimp) permettono la scelta della formattazione (binaria o ASCII) al momento del salvataggio dell’immagine. Un estensione di ciò che si è appena mostrato è il file .ppm (portable pix map), volto a codificare immagini a colori. Tale formato è caratterizzao da: • Il codice identificativo P3. • Eventuali commenti preceduti da #. • Le dimensioni dell’immagine, rispettivamente larghezza e altezza. • Il massimo indice di intensità colore, di default 255. • Le triple recanti la configurazione di ciascun pixel secondo RGB (vedi paragrafo successivo), i pixel sono esplorati anche in questo caso per riga da sinistra verso destra. I due esempi di codifica che abbiamo visto sono alcuni dei possibili, ma sono quelli che sono utili al nostro scopo. Esistono numerose estensioni di file immagine non compressi (.pnm, .pbm), oltre ad alcune estensioni proprietarie dei produttori di fotocamere (.cr2 per Canon, .arw per Sony) non facilmente leggibili poichè codificate secondo regole RAW (binarie) e secondo criteri per l’appunto non standard. Alcune informazioni sui formati canonici possono essere reperite sul sito http: //netpbm.sourceforge.net/doc/#formats. 2 RGB e YCbCr Come posso esprimere la tonalità di colore che il singolo pixel può assumere? I modelli (detti ”spazi colore”) sono molteplici, scelti soprattutto in funzione dell’applicazione in questione. In questo paragrafo ne presentiamo due, uno necessario alla codifica del .pgm, mentre l’altro utilizzato nell’ambito del JPEG. • Decompongo il colore in un sistema di toni fondamentali, ossia per ciascuna unità ho una tripla di valori corrispondenti alle quantità di rosso di verde e di blu. Tale sistema è per l’appunto detto RGB, da red green blue. In termini complessivi posso pensare ad una foto come alla sovrapposizione di tre immagini monocromatiche: l’una in scala di rossi, la seconda in scala di verdi e la terza in scala di blu. Questo formato è intuitivamente il più naturale, ma allo stesso tempo non è molto conveniente dal punto di vista dell’intepretazione da parte di dispositivi elettronici. • Posso invece pensare il pixel rappresentato da una componenente luminosa (detta ”lumia”) con due componenenti cromatiche (dette appunto ”di crominanza”), abbiamo ancora una volta una tripla di valori: questo spazio colore è detto YCbCr. Una foto è vista come la combinazione della sua corrispettiva in scala di grigi (Y) e di due foto policromatiche opache (Cb e Cr): le tonalita scure (es. marrone) hanno alti livelli sul canale Cr e bassi su Cb, alcune tonalità (come il verde) hanno livelli bassi su entrambi i canali, il Cb invece predilige le tonalità accese (es. azzurro cielo). Un vantaggio evidente di tale sistema, oltre ad essere compatibile con il processo di visualizzazione dei monitor, consiste nella separazione della luce dal colore ed è proprio ciò che lo rende ottimale per il JPEG. Veniamo al punto fondamentale: come eseguo conversioni dallo spazio colore RGB a quello YCbCr? La trasformazione che associa la tripletta dei colori fondamentali a quella dello spazio colore in lumiocrominanza è in realtà un’applicazione lineare. Se R0 , G0 e B 0 sono le componenti RGB shiftate di -128, la tripla Y , Cb, Cr è data da: 0 Y 0, 299 0, 587 0, 114 R Cb = −0, 1687 −0, 3313 0, 5 G0 0, 5 −0, 4187 −0, 0813 B 0 Cr La conversione dell’YCbCr nella forma RGB segue immediatamente dal calcolo dell’applicazione inversa: 0 1 0 1, 402 R Y G0 = 1 −0, 344 −0, 714 Cb B0 1 1, 772 0 Cr Vediamo un esempio di scomposizione nei due spazi colore che abbiamo esaminato. Il codice utilizzato per ricavare le componenti RGB e YCbCr è reperibile all’indirizzo: http://poisson.phc.unipi.it/~afavilli/Software/sep_ componenti/. 3 Figura 1: Immagine originale. Figura 2: Schema RGB. 4 Figura 3: Schema YCbCr. Alcuni prerequisiti: elementi di shell scripting. Il terminale di Linux è uno strumento molto potente, in quanto offre sia la possbilità di immettere comandi in maniera interattiva che creare particolari file di testo eseguibili (in formato .sh) con i quali eseguiremo in maniera del tutto automatica una sequenza di comandi prestabiliti. Quest’ultima consuetudine è chiamata shell scripting: tutto ciò è assai utile al nostro scopo, creeremo infatti uno script per terminale in grado di far eseguire funzioni Matlab e piccoli programmi in C che nel complesso effettueranno compressione e decompressione di immagini. Osserviamo subito un esempio pratico: creiamo un file run.sh con il suddetto contenuto. #!/bin/sh mkdir prova cd prova touch test Leggiamo riga per riga: la stringa #!/bin/sh indica essenzialmente di eseguire tutto ciò che segue da shell, ossia creare nella posizione corrente una cartella denominata prova e di spostarsi all’interno di questa per creare il file test. A questo punto apriamo il terminale, impostiamo l’eseguibilità del file .sh con chmod -x run.sh ed eseguiamo lo script con sh ./run.sh. Verranno eseguite precisamente le istruzioni proposte. Quella seguita finora è una procedura standard per la creazione di script: quan5 do nel presente testo lavoreremo con file .sh si assume ogni volta di dover eseguire il chmod dopo il salvataggio. Illustriamo in maniera molto sintetica alcune opzioni che solitamente risultano ridondanti in esecuzione interattiva, ma sono fondamentali per lo scripting. >>> echo "Come ti chiami?" Come ti chiami? >>> read NOME Andrea >>> echo "Ciao, \${NOME}!" Ciao, Andrea! L’output su schermo si ottiene facilmente con la funzione echo, mentre l’input con read seguito dal nome della variabile. Posso richiamare la variabile in qualsiasi punto dello script racchiudendola tra parentesi graffe, la cui aperta è preceduta dal simbolo $. Ultimo concetto che vediamo è il passaggio di istruzioni ad una riga di comando esterna, come può essere quella di Matlab o di Gnuplot: basta utilizzare una funzione echo con contentimento delle istruzioni da passare seguita dal simbolo | e dal comando che evoca l’applicazione in oggetto. Un esempio: echo "eig(randn(100))" | matlab Siamo ora pronti ad iniziare con gli algoritmi veri e propri. Ricordo che il codice dell’intera sperimentazione (comprese le variazioni non presenti su questo testo) è reperibile qui: http://poisson.phc.unipi.it/~afavilli/Software/ Compr_Immagini/. Primo approccio: una SVD ”a blocchi”. Cerchiamo di ribadire i nostri obiettivi: vogliamo trovare un metodo per poter comprimere una data immagine, cercando il più possibile di mantenere la qualità, sapendo che essenzialmente ho in partenza tre matrici. Un’idea ci giunge da alcuni risultati dell’algebra lineare: considero una matrice A ∈ Rm×n , allora esiste sempre una decomposizione (detta ai valori singolari, SVD) nelle matrici U , Σ, V tale che A = U ΣV T dove U ∈ Rm×m , le cui colonne ui sono dette vettori singolari sinistri, V ∈ Rn×n , le cui colonne vi sono dette vettori singolari destri. U e V sono entrambe matrici ortogonali. Σ ∈ Rm×n è una matrice ”diagonale”, ossia Σij = 0 se i 6= j, Σij = σi se i = j. Gli elementi σi sono detti valori singolari, vale σ1 ≥ . . . ≥ σmin(m,n) . Alla luce di quanto appena detto, possiamo riscrivere la decomposizione in notazione vettoriale: min(m,n) X A= σi ui vTi i=1 6 La proprietà chiave (dalla quale segue l’algoritmo di compressione) consiste nel fatto che la matrice Ak definita come k X Ak = σi ui vTi i=1 è quella (di rango k) che minimizza la funzione X 7→ min kA − Xk2 X ed è quindi una candidata ideale all’approssimazione di A, essendo consapevoli del fatto che posso calcolare Ak conoscendo solo k valori e k vettori singolari destri e sinistri. Definendo uh,i e vk,i come le coordinate h-esima di ui e k-esima di vi rispettivamente, posso infatti esprimere Ak come (Ak )ij = k X σt ui,t vj,t . t=1 Se A è una matrice che rappresenta una delle tre componenti di un’immagine (RGB o YCbCr che sia), posso calcolarne la SVD (non parleremo qui dei metodi per farlo, Matlab ci viene in aiuto), conservarne un certo numero k di valori/vettori singolari e codificare questi in un file: questo è un valido algoritmo di compressione. Per decomprimere mi è sufficiente ricostruire la matrice Ak sfruttando la relazione sopra mostrata. È possibile fissare k min(m, n), solitamente (per un fissato ε > 0) si sceglie k tale che σσ1i < ε. Per sperimentare è tuttavia cosa buona lasciare il valore di taglio all’utente in modo da poter studiare bene il calo di qualità e di spazio in relazione al k scelto. Non realizzeremo precisamente questo algoritmo, ma vogliamo invece operare con una procedura che sia confrontabile con il JPEG che svilupperemo dopo. Quello che svolgeremo è una sorta di ibrido: non eseguiremo compressione SVD sull’intera matrice A, ma fisseremo un valore p ∈ N e divideremo la matrice in blocchetti p × p, su ciascuno di questi blocchetti andremo poi ad eseguire compressione come sopra illustrato. Da notare il fatto che le matrici U e V nella decomposizione di ciascun blocco saranno questa volta quadrate e avremo esattamente p valori singolari per blocco. • Un problema si ha qualora su A ∈ Rm×n , m 6= 0 mod p oppure n 6= 0 mod p: affinchè sia effettivamente possibile la separazione in blocchi equivalenti p × p è necessario modificare la dimensione di A aggiungendo pixel, ottenendo Ã. Più precisamente, se m 6= 0 mod p dovrò aggiungere p − (m mod p) righe di pixel, se invece n 6= 0 mod p aggiungerò p − (n mod p) colonne. Per non creare particolari problemi nella compressione andrò ad assegnare a tali pixels il valore medio tra 0 e 255, ossia 128. Dovrò in sintesi accertarmi che il mio codice includa una fase iniziale con lo scopo di accertarsi la divisibilità in blocchi ed eventualmente creare à come spiegato sopra. Alla fine di questo passaggio ho sicuramente una possibile suddivisione dell’immagine: à = (Bij )ij 7 • Vediamo alcune strategie per risparmiare spazio. Anzitutto è possibile modificare i vettori singolari in modo da non dover conservareqanche i (ij) (ij) (ij) corrispettivi valori singolari: è sufficiente definire ûh = uh σh e q (ij) (ij) (ij) m n v̂h = vh σh , ∀h = 1, . . . , p, ∀i = 1, . . . , p , ∀j = 1, . . . , p . (Gli (ij) (ij) (ij) u , v ,σ sono i vettori singolari e i valori singolari del sottoblocco Bij ). In tale modo ho ottenuto k X (ij) (ij) (ij)T σh uh vh h=1 = k X (ij) (ij)T ûh v̂h h=1 Ciò significa esattamente che posso risparmiare il salvataggio di p valori numerici per blocco, in totale ben ( mn p ) valori! La presenza della radice quadrata nella nostra ridefinizione dei vettori causa però una decimalizzazione degli argomenti: per ogni vettore singolare prenderemo soltanto la parte intera delle componenti moltiplicate per un opportuno fattore γ, sceglieremo ad esempio γ = 20, e riusciremo in tal modo a ridurre ancora la quantità di memoria occupata. • Una questione che può sorgere naturale giunti a questo punto: la conservazione di k vettori singolari sinistri e k destri rappresenta veramente una riduzione di dati rispetto ad una matrice di p2 elementi? Ciascun vettore singolare ha p componenti, in totale ho 2pk elementi da memorizzare in luogo dei canonici p2 : è evidente che ciò è effettivamente vantaggioso solo se k < p2 . Come vedremo si hanno buoni risultati anche con valori di k effettivamente molto piccoli, quindi questo non rappresenta un effettivo problema. • Lamnmatrice che vado a scrivere su file come immagine compressa è M ∈ R p ×2k , cosı̀ costruita: (11) (11) Uk Vk (21) (21) Uk Vk .. .. . . (12) (12) U Vk k (22) (22) Vk Uk M = .. .. . . (13) (13) Uk Vk (23) (23) U Vk k . .. .. . mn n (m ( p p) p p) Uk Vk (ij) (ij) (ij) (ij) (ij) (ij) dove, Uk = (û1 . . . ûk ) e Vk = (v̂1 . . . v̂k ). Essenzialmente vado ad esplorare la matrice à blocco a blocco per colonne, per ciascuna suddivisione vado a calcolare la SVD e accodo i primi k vettori singolari destri e sinistri (opportunamente modificati e rinormalizzati) alla matrice M. 8 Siamo pronti a delineare l’algoritmo per poi giungere alla stesura del codice. 1. Preparazione alla suddivisione in blocchi: eventuale aggiunta di righe e colonne (da A passo ad Ã). 2. Esecuzione della compressione: per ogni blocco Bij calcolo [U (ij) , Σ(ij) , U (ij) ] = (ij) (ij) SV D(Bij ), ricavo poi Uk e Vk eseguendo il taglio ai primi k vettori singolari e facendo ”assorbire” i valori singolari agli stessi vettori, eseguo poi la moltiplicazione per γ prendendo solo la parte intera di ogni componente. Aggiorno la matrice M dopo l’iterazione su ogni blocco. 3. Salvataggio della matrice M che rappresenta l’immagine compressa: discuteremo in seguito le modalità. (ij) 4. Decompressione: recupero da M i vettori singolari e calcolo (Bk )ab = Pk (ij) (ij) (ij) γ −2 h=1 ûa,h v̂b,h . Alla fine ottengo Ãk = (Bk )ij . 5. Recupero dimensioni originali: da Ãk ottengo Ak , approssimazione della A di partenza. 6. Riscrittura di A come .ppm. Queste le istruzioni da eseguire con la sola differenza che le matrici dell’immagine a colori saranno tre: dovrò effettuare la procedura lavorando sulle matrici in parallelo. Prima di vedere il codice è necessario decidere se implementare il tutto sullo spazio colore RGB o su lumiocrominanza: poichè le sperimentazioni effettuate non comportano grandi differenze di rendimento e per il fatto che la YCbCr comporta un numero di operazioni totale maggiore opteremo per la RGB. (Il codice analogo in YCbCr è reperibile all’indirizzo precedentemente menzionato) Iniziamo con un programma in C che prende in ingresso il file foto.ppm e andrà a separare le componenti dell’immagine nei file matr R, matr G e matr B, oltre a careare un piccolo file contentente le dimensioni originali della foto. Ovviamente ciascuna componente non verrà salvata come matrice, ma come sequenza di numeri: recupereremo la forma originale con il reshape() di Matlab. /* Programma che estrae dal file .ppm le componenti RGB separatamente. */ #include <stdio.h> int main() { FILE *f1, *f2, *f3, *f4, *f5; int r, g, b; int alt, largh; f1 = fopen("foto.ppm", "r"); f2 = fopen("matr_R","w"); f3 = fopen("matr_G","w"); f4 = fopen("matr_B","w"); f5 = fopen("dimensione_foto","w"); fseek(f1,2*sizeof(char),SEEK_SET); 9 fscanf(f1,"%d %d", &largh, &alt); fprintf(f5,"%d %d", largh, alt); fclose(f5); fseek(f1,sizeof(int),SEEK_CUR); while (feof(f1) != 1) { fscanf(f1,"%d %d %d", &r, &g, &b); if (feof(f1) != 1) { fprintf(f2,"%d\n", r); fprintf(f3,"%d\n", g); fprintf(f4,"%d\n", b); } } fclose(f1); fclose(f2); fclose(f3); fclose(f4); return 0; } Abbiamo fatto uso di alcune operazioni su file: il comando fopen apre il file specificato in lettura se è presente la stringa ’’r’’, in scirttura con la ’’w’’. I comandi fprintf e fscanf permettono di scrivere/leggere sul file indicato, mentre il comando fseek permette di spostare il cursore di lettura di un numero prefissato di bytes dalla posizione iniziale se specifico SEEK SET, dalla posizione attuale se specifico SEEK CUR, dalla posizione finale se specifico SEEK END. Salvo il file come crea matrici.c, non compilo in quanto lo farò dopo direttamente da script. Scriviamo ora la function Matlab effettivamente adibita alla compressione, questa leggerà le componenti create col programma precedente, la dimensione dell’immagine e da tali dati effettuerà le istruzioni elencate dal punto 1 al punto 3. % Programma che esegue compressione SVD su foto RGB. % k rappresenta la dimensione di ciascun sottoblocco. % m rappresenta il numero dei valori sing. da conservare. % z rappresenta il fattore di normalizzazione (cons. z = 20). function compress_svd(k,m,z) % Prima parte: adattamento componenti alla suddivisione in blocchi. disp(’Adatto componenti RGB...’) dim = load(’dimensione_foto’); largh = dim(1); alt = dim(2); foto_red = load(’matr_R’,’-ascii’); foto_green = load(’matr_G’,’-ascii’); foto_blue = load(’matr_B’,’-ascii’); foto_red = reshape(foto_red,largh,alt)’; foto_green = reshape(foto_green,largh,alt)’; foto_blue = reshape(foto_blue,largh,alt)’; 10 if (mod(largh,k) ~= 0) for i=1:k-mod(largh,k) foto_red(1:end,largh+i) = 128; foto_green(1:end,largh+i) = 128; foto_blue(1:end,largh+i) = 128; end end if (mod(alt,k) ~= 0) for i=1:k-mod(alt,k) foto_red(alt+i,1:end) = 128; foto_green(alt+i,1:end) = 128; foto_blue(alt+i,1:end) = 128; end end foto_red = foto_red(1:alt+k*(mod(alt,k) ~= 0)-mod(alt,k),1:largh+ + k*(mod(largh,k) ~= 0)-mod(largh,k)); foto_green = foto_green(1:alt+k*(mod(alt,k) ~= 0)-mod(alt,k),1:largh+ + k*(mod(largh,k) ~= 0)-mod(largh,k)); foto_blue = foto_blue(1:alt+k*(mod(alt,k) ~= 0)-mod(alt,k),1:largh+ + k*(mod(largh,k) ~= 0)-mod(largh,k)); sz = size(foto_red); dim(1) = sz(2); dim(2) = sz(1); % Seconda parte: estrazione svd e preparazione alla compressione. disp(’Comprimo immagine...’) matr_red = zeros((dim(1)/k)*(dim(2)/k),2*m); matr_green = zeros((dim(1)/k)*(dim(2)/k),2*m); matr_blue = zeros((dim(1)/k)*(dim(2)/k),2*m); cursore_matr = 1; for i=1:k:dim(1)-k+1 for j=1:k:dim(2)-k+1 % ROSSO [U,sigma,V] = svd(foto_red(j:j+k-1,i:i+k-1)); U = U(:,1:m); V = V(:,1:m); for p=1:m U(:,p) = U(:,p)*sqrt(sigma(p,p)); V(:,p) = V(:,p)*sqrt(sigma(p,p)); end matr_red(cursore_matr:cursore_matr+k-1,1:m) = floor(U*z); matr_red(cursore_matr:cursore_matr+k-1,m+1:2*m) = floor(V*z); % VERDE 11 [U,sigma,V] = svd(foto_green(j:j+k-1,i:i+k-1)); U = U(:,1:m); V = V(:,1:m); for p=1:m U(:,p) = U(:,p)*sqrt(sigma(p,p)); V(:,p) = V(:,p)*sqrt(sigma(p,p)); end matr_green(cursore_matr:cursore_matr+k-1,1:m) = floor(U*z); matr_green(cursore_matr:cursore_matr+k-1,m+1:2*m) = floor(V*z); % BLU [U,sigma,V] = svd(foto_blue(j:j+k-1,i:i+k-1)); U = U(:,1:m); V = V(:,1:m); for p=1:m U(:,p) = U(:,p)*sqrt(sigma(p,p)); V(:,p) = V(:,p)*sqrt(sigma(p,p)); end matr_blue(cursore_matr:cursore_matr+k-1,1:m) = floor(U*z); matr_blue(cursore_matr:cursore_matr+k-1,m+1:2*m) = floor(V*z); cursore_matr = cursore_matr+k; end end dim_matr = size(matr_red); save(’dim_matrice_compressa’,’dim_matr’,’-ascii’); file = fopen(’**compressa**’,’w’); fprintf(file,’%d\n’,matr_red); fprintf(file,’%d\n’,matr_green); fprintf(file,’%d\n’,matr_blue); fclose(file); end Vediamo alcuni punti nel programma da chiarire: le matrici vengono caricate da file con l’istruzione load, il cui parametro -ascii specifica a Matlab il fatto che le informazioni non sono codificate nel formato binario proprietario. La matrice di reshape va poi presa trasposta poichè Matlab l’ha ricostruita per colonne, mentre l’immagine era invece codificata per righe. Infine, salviamo le dimensioni della matrice compressa M con il comando save, mentre salviamo la matrice stessa in forma vettoriale (in modo da poter risparmiare memoria evitando spazi vuoti) e per fare questo usiamo i comandi fopen fprintf fscanf in stile C. Abbiamo terminato la fase di compressione: non resta che scrivere lo script .sh in modo da far eseguire il tutto in automatico. #!/bin/bash echo "Dimensione dei sottoblocchi?" read NUM_QUAD 12 echo "Quanti valori singolari vuoi conservare?" read NUM_SV echo "Creo matrici per Octave/Matlab..." gcc -o crea_matrici crea_matrici.c ./crea_matrici echo "compress_svd(${NUM_QUAD},${NUM_SV},20)" | matlab echo "Elimino file non piu’ utili..." rm "matr_R" rm "matr_G" rm "matr_B" echo "Fatto!" Ora che la parte relativa alla compressione è completa, vediamo come ricostruire un’immagine .ppm a partire dal file contenente i valori della matrice M . % Programma che decomprime l’immagine. % k rappresenta la dimensione dei sottoblocchi scelta in compressione. % z rappresenta il fattore di normalizzazione scelto in compressione. % t rappresente il valore di taglio dei vettori singolari. function decompress_svd(t,k,z) dim = load(’dim_matrice_compressa’,’-ascii’); %Carico elementi necessari. file = fopen(’**compressa**’,’r’); matr_red = fscanf(file,’%d’,dim(1)*dim(2)); matr_red = reshape(matr_red,dim(1),dim(2)); matr_green = fscanf(file,’%d’,dim(1)*dim(2)); matr_green = reshape(matr_green,dim(1),dim(2)); matr_blue = fscanf(file,’%d’,dim(1)*dim(2)); matr_blue = reshape(matr_blue,dim(1),dim(2)); fclose(file); dim_foto = load(’dimensione_foto’,’-ascii’); largh = dim_foto(1)+k*(mod(dim_foto(1),k) ~= 0)-mod(dim_foto(1),k); alt = dim_foto(2)+k*(mod(dim_foto(2),k) ~= 0)-mod(dim_foto(2),k); cursore_matr = 0; foto_red = zeros(alt,largh); %Inizializzo matrici dell’immagine. foto_green = zeros(alt,largh); foto_blue = zeros(alt,largh); blocchi_largh = largh/k; blocchi_alt = alt/k; for i=1:blocchi_largh for j=1:blocchi_alt for m=1:k for n=1:k somma = [0, 0, 0]; for o=1:t somma(1) = somma(1) + matr_red(m+cursore_matr,o)* matr_red(n+cursore_matr,t+o); 13 somma(2) = somma(2) + matr_green(m+cursore_matr,o)* matr_green(n+cursore_matr,t+o); somma(3) = somma(3) + matr_blue(m+cursore_matr,o)* matr_blue(n+cursore_matr,t+o); end val = floor(somma./(z^2)); val = max(0,val); val = min(255,val); foto_red(m+(j-1)*k,n+(i-1)*k) = val(1); foto_green(m+(j-1)*k,n+(i-1)*k) = val(2); foto_blue(m+(j-1)*k,n+(i-1)*k) = val(3); end end cursore_matr = cursore_matr+k; end end %Riporto la foto alle dimensioni originali. foto_red = foto_red(1:dim_foto(2),1:dim_foto(1)); %Calcolo trasposta, in modo da scrivere per riga. foto_red = foto_red’; foto_green = foto_green(1:dim_foto(2),1:dim_foto(1)); foto_green = foto_green’; foto_blue = foto_blue(1:dim_foto(2),1:dim_foto(1)); foto_blue = foto_blue’; %Salvo matrici. red = fopen(’matr_R’,’w’); green = fopen(’matr_G’,’w’); blue = fopen(’matr_B’,’w’); fprintf(red,’%d\n’,foto_red); fprintf(green,’%d\n’,foto_green); fprintf(blue,’%d\n’,foto_blue); fclose(red); fclose(green); fclose(blue); end Il codice sopra proposto non fa che eseguire i punti 4 e 5, ossia la ricostruzione della matrice approssimante blocco a blocco a partire dai valori singolari conservati e il recupero delle dimensioni originali contenute nel file dimensione foto creato in compressione. Alla fine di tale procedura resta da riformattare i dati in modo che l’immagine sia nuovamente riconoscibile come .ppm, in pratica è sufficiente ricaricare i tre file in uscita dalla function e costruire un file unico introdotto dal valore di riconoscimento P3. Il tutto è svolto da questo programma in C che salveremo come crea ppm.c. /* Programma che crea un’immagine .ppm a partire dalle matrici RGB. */ #include <stdio.h> 14 int main() { FILE *f1,*f2,*f3,*f4,*f5; int r, g, b, alt, largh; f1 = fopen("matr_R","r"); f2 = fopen("matr_G", "r"); f3 = fopen("matr_B", "r"); f4 = fopen("dimensione_foto","r"); f5 = fopen("decompressa.ppm","w"); fscanf(f4,"%d %d", &largh, &alt); fclose(f4); fprintf(f5,"P3\n%d %d\n255\n", largh, alt); while (feof(f1) == 0) { fscanf(f1,"%d",&r); fscanf(f2,"%d",&g); fscanf(f3,"%d",&b); if (feof(f1) == 0) { fprintf(f5,"%d\n%d\n%d\n",r,g,b); } } fclose(f1); fclose(f2); fclose(f3); fclose(f5); return 0; } Siamo infine pronti a scrivere lo script di esecuzione e concludere cosı̀ il codice relativo all’intero algoritmo. #!/bin/bash echo "Quanti valori singolari hai conservato in compressione?" read NUM_VAL echo "Di che dimensione sono i sottoblocchi?" read NUM_QUAD echo "Decomprimo l’immagine..." echo "decompress_svd(${NUM_VAL},${NUM_QUAD},20)" | matlab echo "Creo file .pgm..." gcc -o crea_ppm crea_ppm.c ./crea_ppm echo "Elimino files non piu’ utili..." rm "matr_R" rm "matr_G" rm "matr_B" echo "Fatto!" Passiamo ora alla sperimentazione vera e propria, considerando i risultati anche in relazione allo spazio occupato dal file di compressione. Per un buon rapporto qualità/memoria scegliamo p = 32: ciò consente una procedura eseguita in tempi ragionevoli (ricordiamo che la singola SVD costa O(p3 )) e allo stesso tempo discreti risultati (ci è sufficiente conservare solo 4 valori singolari per non notare 15 differenze alle dimensioni originali dell’immagine). Vogliamo essenzialmente contraddistinguere la variazione dell’algoritmo SVD da noi proposta con l’applicazione diretta sull’intera matrice immagine: dalle osservazioni emerge il fatto che suddividendo la foto in blocchi riesco a contenere il disturbo generale a scapito tuttavia della definizione dei bordi e di alcuni problemi sulle variazioni di tono. In questo modo la compressione ha un risultato minimo garantito, con una soglia minima di memoria occupata, mentre con il metodo SVD tradizionale si può raggiungere il disturbo totale, non essendo più in grado di distinguere i soggetti della figura. Raggiunto il grado ottimo di compressione per entrambi i metodi, non si nota un effettivo vantaggio in spazio di uno dei due: il lavoro più preciso ottenuto con il nostro metodo lo paghiamo con una maggiore quantità di memoria. Come effettuiamo la sperimentazione? È anzitutto necessario procurarsi un’immagine loseless e questo è un compito non cosı̀ facile, visto che il 99% delle immagini sulla rete sono in realtà già compresse (proprio per gestire una migliore condivisione). Ci vengono in aiuto i produttori stessi di fotocamere che mettono a disposizione delle ”sample pictures” in RAW proprietario, la conversione in formato ASCII .ppm la effettuiamo con Gimp che con l’aiuto dell’add-on Ufraw riesce a gstire molti standard commerciali. Scegliamo in particolare la foto di esempio della Canon EOS 5D Mark III: l’immagine coniene notevoli variazioni di tono e buon dettaglio, nel formato .ppm occupa ben 49,7 MB! Rinominiamo il file come foto.ppm e collochiamolo nella stessa directory dove abbiamo salvato tutti i programmi precedenti. Eseguiamo varie compressioni: conservando 15 valori l’immagine compressa arriverà a pesare 42,9 MB, con 10 valori 29,9 MB, con 4 valori 13,4 MB con 1 solo valore 4,6 MB. Siamo ovviamente abituati nel quotidiano a risultati molto migliori. Vediamo alcune immagini: poichè a dimensioni originali non è veramente possibile cogliere molte differenze (ovviamente sopra una certa soglia) pare più opportuno osservare alcuni particolari opportunamente zoomati, scegliendo di focalizzarsi sulle sfumature chiare e frequenti ombreggiature della corolla di un fiore. Con questo concludiamo l’argomento. 16 Figura 4: l’immagine intera non compressa (.ppm) Figura 5: particolari 15 sv, 4 sv, 3 sv, 1 sv. 17 La trasformata discreta del coseno (DCT) Sia v ∈ Rn , posso ovviamente esprimerlo come combinazione lineare dei vettori della base canonica {e1 , . . . , en }, dove 0 .. . 0 ei = 1 (il valore 1 giace sull’i-esima riga) 0 . .. 0 Ciò significa che posso scrivere v come: v= n X αi ei . i=1 Ovviamente αi = vi : ho enunciato questi principi estremamente banali con lo scopo di mostrare che la rappresentazione su base canonica, seppur fondamentale dal punto di vista operativo e rappresentativo, non fornisce informazioni utili al di fuori delle componenti stesse del vettore. Una base più interessante a tal scopo è quella costruita dai {c1 , . . . , cn }, definiti come: √1 n c1 = ... √1 n q 2 n cos π(i−1) 2n q 3π(i−1) 2 cos n 2n , i = 2, . . . , n ci = .. . q π(2n−1)(i−1) 2 cos n 2n {c1 , . . . , cn } è detta base del coseno; l’applicazione lineare Rn → Rn che associa un vettore v al vettore w dei coefficienti della combinazione su {c1 , . . . , cn } è detta Trasformata discreta del coseno, in sigla DCT. Tra tutti i modelli formulati noi andremo sempre ad utilizzare la DCT-II che può essere riassunta nella relazione che segue. wk = (DCTn (v))k = ζk n X vh cos h=1 π(2h − 1)(k − 1) , k = 1, . . . , n 2n dove, √1 n ζk = q 2 n 18 se k = 1 altrimenti L’applicazione inversa, detta IDCT, associa il vettore dei coeffcienti w al vettore v ottenuto dalla combinazione lineare degli elementi di w sulla base {c1 , . . . , cn }. n X vk = (IDCTn (w))k = ζh wh cos h=1 π(2k − 1)(h − 1) , k = 1, . . . , n 2n Entrambe le procedure sono presenti di stock nella versione R2013a di Matlab: dct(v) calcola la trasformata diretta, mentre idct(w) l’inversa. Da segnalare il fatto che se effettuo la dct oppure l’idct su una matrice, Matlab effettuerà l’applicazione sul vettore ottenuto concatenando le colonne della suddetta. Vediamo di estendere quanto detto finora sullo spazio vettoriale delle matrici Rn×n , poichè è qui che dovremmo applicare la trasformata per i nostri scopi di compressione. Se A ∈ Rn×n , la base canonica è data da {E11 , . . . , E1n , E21 , . . . , E2n , . . . , En1 , . . . , Enn } dove, 0 Eij = ... 0 ... .. 0 .. , (l’1 giace in posizione (i, j)) . 0 . 0 1 0 .. . ... La base del coseno sulle matrici {C11 , . . . , C1n , E21 , . . . , C2n , . . . , Cn1 , . . . , Cnn } è invece cosı̀ strutturata: π(2j − 1)(k − 1) π(2i − 1)(h − 1) cos Chk = ξh θk cos i = 1, . . . , n 2n 2n j = 1, . . . , n √1 √1 se h = 1 se k = 1 n n dove, ξh = q 2 e θk = q 2 . altrimenti altrimenti n n La DCT sarà dunque un’applicazione lineare Rn×n → Rn×n e può essere calcolata termine a termine con la seguente relazione: (B)ij = (DCTn×n (A))ij = ξi θj n X n X (A)hk cos h=1 k=1 π(2k − 1)(j − 1) π(2h − 1)(i − 1) cos 2n 2n Analogo ragionamento per la trasformata inversa. (A)ij = (IDCTn×n (B))ij = n X n X ξh θk (B)hk cos h=1 k=1 19 π(2i − 1)(h − 1) π(2j − 1)(k − 1) cos 2n 2n Come possiamo utilizzare la DCT matriciale e la sua inversa? Sfruttando la function dct possiamo fabbricarci la nostra my dct2 conoscendo la trasformata per colonne della matrice e della rispettiva trasposta. Ci risparmiamo lo sforzo, visto che Matlab integra le funzioni dct2 e idct2 che fanno tutto il lavoro ”sporco” per noi! Matlab inoltre, mediante algorimti efficienti, riesce ad eseguire il tutto in O(n log n). Figura 6: la base del coseno in R8×8 In quest’ultima parte del paragrafo parliamo del motivo fondamentale per cui si è scelto di lavorare sulla DCT e del legame con l’algoritmo JPEG. La trasformata del coseno permette di valutare l’immagine secondo determinate variazioni di tono (come mostrato in Figura 6), conoscendo per ogni tipo di variazione il coefficiente di intensità. Le variazioni di tono più repentine sono dette ”frequenze alte”, metre quelle più graduali sono le ”frequenze basse”: l’occhio umano tende a focalizzarsi sulle alte frequenze trascurando le basse. Considerando anche il fatto che in un’immagine le variazioni graduali avranno inoltre coefficiente più alto, è evidente che la DCT sarà il nucleo imprescindibile di un algoritmo di compressione JPEG. Facciamo un esperimento: prendiamo una foto .ppm, acquisiamola con Matlab e calcoliamone la DCT. Notiamo che le basse frequenze si addenseranno in alto a sinistra (vedi Figura 8), basterà eliminare le altre e avremo la nostra compressione; di questo ne disuteremo con abbondanza nel successivo paragrafo. 20 Figura 7: Immagine scelta come campione. Figura 8: un’imagesc della DCT eseguita sulla matrice dei rossi. I valori più elevati in alto a sinistra. 21 Secondo approccio: l’algoritmo JPEG. Abbiamo già introdotto le conoscenze necessarie, entriamo subito nel vivo enunciando le fasi della compressione JPEG: 1. Separazione dell’immagine in blocchi p×p, di default p = 8, e calcolo della DCT su ogni blocco. 2. Quantizzazione, ossia la tecnica di cernita nei confronti dei coefficenti della DCT corrispondenti alle alte frequenze. 3. Codifica, ovvero la costruzione effettiva del file .jpg che noi non tratteremo poichè lontana dai nostri fini di sperimentazione. Prima di procedere con la spiegazione dettagliata dei punti precedenti, alcune riflessioni sono d’obbligo. • Abbiamo abbondantemente preannunciato che il JPEG si esegue sullo spazio colore di lumiocrominanza e non su RGB: questa oltre ad essere una standardizzazione permette di avere risultati migliori dal punto di vista del colore e delle sfumature, in quanto la compressione è eseguita autonomamente sulla componente luminosa rispetto a quella cromatica. L’YCbCr consente un miglior fattore di risparmio memoria a parità di qualità visiva, a scapito tuttavia di una computazione meno rapida (sarà necessario infatti scrivere del codice di conversione prima e dopo la fase di compressione). Una curiosità derivante dall’uso di tale spazio colore consiste nel differente degrado dell’immagine derivante da eccessiva severità di compressione: anzichè distrurbare la sagoma degli oggetti, come nel RGB, l’algoritmo andrà a colpire le variazioni, rendendo visibili delle vere e proprie ”curve di livello”. • Cerchiamo di motivare la scelta di p = 8. Una suddivisione più fine porterebbe ad un risultato più accurato, ma ad un tempo di esecuzione non propriamente sostenibile; allo stesso tempo una sceltà più larga non sarebbe esente da difetti visivi sull’immagine compressa. Scegliendo p = 32 notiamo che il computer riesce a portare a termine il tutto molto più velocemente, ma con facilità insorgono problemi. Supponiamo che un blocco includa un tratto con alte variazioni (ad es. parte di un petalo del fiore) e il resto a variazione più lieve (ad es. lo sfondo), mentre il blocco adiacente sia esclusivamente a bassa variazione: sarà visibile lo ”sfarfallio” proveniente dalle alte frequenze in contrasto con una tonalità uniforme del secondo blocco. p = 8 rappresenta ancora una volta un buon compromesso. • Dal punto di vista matematico sappiamo che l’approccio SVD è una migliore approssimazione, ma come vedremo il JPEG consente un risparmio di spazio nettamente maggiore con eguale qualità. Quest’algoritmo non si basa infatti sul ricostruire l’immagine di partenza il più fedelmente possibile, ma sull’eliminazione di quelle componenti dell’immagine inutili perchè non percettibili. L’esperienza vince dunque sulla teoria. 22 Descrivendo più precisamente il nostro elenco numerato, quali coefficenti della DCT andremo ad eliminare e quali conserveremo? Un’idea ingenua potrebbe essere l’eliminazione di tutti quei valori il cui modulo è al di sotto di una data soglia: avremmo risultati già notevoli in quanto, come abbiamo visto in Figura 8, le basse frequenze corrispondono ai valori più elevati. Una compressione svolta in questo modo non sarebbe il massimo dell’accuratezza, ma il vero problema è che non sapremmo quantifcare l’effettivo grado di riduzione, dovremmo stabilire una soglia di taglio specifica in base alla foto. La soluzione ortodossa è data dal processo di quantizzazione: il JPEG, una volta ottenuta la DCT del blocco, esegue una divisione delle componenti termine a termine con delle matrici fornite opportunamente riscalate. I valori decimali vengono arrotondati e, come vedremo, molti di essi saranno ridotti a 0. Le matrici di quantizzazione dello standard JPEG sono le seguenti, la prima per la componente di lumia, mentre la seconda per le due componenti di crominanza: 16 11 10 16 24 40 51 61 12 12 14 19 26 58 60 55 14 13 16 24 40 57 69 56 14 17 22 29 51 87 80 62 QY = 18 22 37 56 68 109 103 77 24 35 55 64 81 104 113 92 49 64 78 87 103 121 120 101 72 92 95 98 112 100 103 99 QCb,Cr 17 18 24 47 = 99 99 99 99 18 21 26 66 99 99 99 99 24 26 56 99 99 99 99 99 47 66 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 Non esiste un metodo preciso per determinare le suddette, se non per procedure euristiche: la scelta delle matrici di quantizzazione non è unica e tutt’oggi si lavora per crearne sempre di migliori. Ciascun dispositivo o software in grado di eseguire il JPEG ne ha di proprie, possiamo trovarne alcune all’indirizzo http://www.impulseadventure.com/photo/jpeg-quantization.html. Si osserva che, in conformità alla Figura 8, le matrici di quantizzazione hanno tutte la medesima struttura: i valori in alto a sinistra sono più piccoli, in modo che la cancellazione agisca maggiormente sulle altre aree del blocco. Come anticipato prima, non lavoreremo sulle matrici QY e QCb,Cr direttamente: fornito un intero l ∈ [1, 100] (1 per la massima compressione possibile), riscaleremo le matrici di quantizzazione secondo le seguenti regole, ottenendo Q0Y e Q0Cb,Cr . ( 5000 se l < 50 l µ= 200 − 2l altrimenti Q0ij = b µQij + 50 c 100 23 A questo punto l’algoritmo JPEG procede con la memorizzazione dei coefficenti secondo opportune codifiche esplicitando i dati in esadecimale: tutto ciò è svolto secondo un’opportuno standard (detto ”codifica entropica”) che consente di far aprire il file compresso direttamente ai visualizzatori di immagine, senza implementare decompressioni. Noi trascureremo questa fase, ricorrendo alla gestione abbastanza vantaggiosa delle matrici sparse su Matlab. Andiamo a descrivere il nostro algoritmo: analogamente a quanto svolto per la SVD, da ciascuna componente A dell’immagine costruiamo la à in modo che sia p × p-divisibile. Eseguiamo l’esplorazione blocco a blocco, procedendo per colonne: per ciascun blocco B(i,j) calcoliamo K(i,j) = q[DCTn×n (B(i,j) )], q[.] rappresenta l’operazione di quantizzazione prima descritta. Memorizziamo la mn matrice M ∈ R p ×p cosı̀ strutturata: K(1,1) K(2,1) ... K M = (1,2) K(2,2) .. . K( mp , np ) Vista la larghissima presenza di zeri, useremo l’istruzione sparse(M) di Matlab in modo da salvare solo gli elementi non nulli con la loro effettiva posizione. Il fatto che M abbia notevoli dimensioni in lunghezza consente all’istruzione sparse di far risparmiare un’enorme quantità di memoria, alla stregua della codifica standard. Poichè non ci è possbile leggere il file di compressione come immagine, è necessario implementare la decompressione: per ciascun elemento K(i,j) andremo a ricavarci B 0 (i,j) = q −1 [IDCTn×n (K(i,j) )], dove q −1 [.] è l’operazione di dequantizzazione, ossia la moltiplicazione del blocco termine a termine per gli elementi della matrice di quantizzazione. Ricostruisco Ã0 = (B 0 (i,j) )ij e riporto l’immagine alle dimensioni originali avendo infine A0 . Osserviamo il codice in ogni sua parte: per separare le componenti RGB usiamo lo stesso file crea matrici.c scritto per la SVD, mentre implementiamo una function per la conversione nello spazio colore YCbCr. function RGB_to_YCbCr() % Carico matrici delle componenti RGB. disp(’Carico componenti RGB...’); matr_R = load(’matr_R’,’-ascii’)-128; matr_G = load(’matr_G’,’-ascii’)-128; matr_B = load(’matr_B’,’-ascii’)-128; % Carico dimensioni della foto. dim = load(’dimensione_foto’,’-ascii’); % Inizializzo matrici YCbCr. matr_Y = zeros(dim(2),dim(1)); 24 matr_Cb = zeros(dim(2),dim(1)); matr_Cr = zeros(dim(2),dim(1)); % Matrice di trasformazione. A = [0.299 0.587 0.114; -0.1687 -0.3313 0.5; 0.5 -0.4187 -0.0813]; % Eseguo appl. lineare. disp(’Eseguo conversione a YCbCr...’); for i=1:dim(1)*dim(2) vett = [matr_R(i); matr_G(i); matr_B(i)]; vett = A*vett; matr_Y(i) = vett(1); matr_Cb(i) = vett(2); matr_Cr(i) = vett(3); end % Salvo matrici YCbCr. disp(’Salvo dati...’); Y = fopen(’matr_Y’,’w’); Cb = fopen(’matr_Cb’,’w’); Cr = fopen(’matr_Cr’,’w’); fprintf(Y,’%d\n’,matr_Y); fprintf(Cb,’%d\n’,matr_Cb); fprintf(Cr,’%d\n’,matr_Cr); fclose(Y); fclose(Cb); fclose(Cr); end In questa procedura, come già spiegato all’inizio, abbiamo shiftato tutte le componenti RGB di -128, dopodichè per ogni tripla si è eseguita l’applicazione lineare di passaggio alla lumiocrominanza. Abbiamo salvato le componenti in forma vettoriale nei file matr Y, matr Cb e matr Cr. Dobbiamo ora creare un file contenente le nostre matrici di quantizzazione che chiameremo matrici quantizzazione.mat, per fare ciò usiamo il formato binario proprietario di Matlab. >>> >>> >>> >>> quant_lum = %Riporto la matrice Q_Y; quant_crom = %Riporto la matrice Q_Cb,Cr; save(’matrici_quantizzazione.mat’,’quant_lum’); save(’matrici_quantizzazione.mat’,’quant_crom’,’-append’); Abbiamo tutto il necessario per procedere alla compressione. % Programma che esegue una compressione DCT sulle componenti YCbCr, % mediante quantizzazione. % k rappresenta la dimensione di ciacun sottoblocco in cui viene divisa la foto. % scal è il fattore di scalatura della matrice di quantizzazione, compreso tra 1 e 100. 25 function compress_dct(k,scal) % Prima parte: scalatura delle matrici di quantizzazione. disp(’Scalatura matrici di quantizzazione...’); load(’matrici_quantizzazione.mat’); if (scal < 50) quant_lum = ceil((quant_lum.*(5000/scal)+50*ones(8))./100); quant_crom = ceil((quant_crom.*(5000/scal)+50*ones(8))./100); else quant_lum = ceil((quant_lum.*(200-scal*2)+50*ones(8))./100); quant_crom = ceil((quant_crom.*(200-scal*2)+50*ones(8))./100); end save(’quant_scalata.mat’,’quant_lum’); % Salvo scalatura. save(’quant_scalata.mat’,’quant_crom’,’-append’); % Seconda parte: adattamento immagine alla suddivisione in blocchi. disp(’Adatto componenti...’) dim = load(’dimensione_foto’,’-ascii’); largh = dim(1); alt = dim(2); foto_Y = load(’matr_Y’,’-ascii’); foto_Cb = load(’matr_Cb’,’-ascii’); foto_Cr = load(’matr_Cr’,’-ascii’); foto_Y = reshape(foto_Y,largh,alt)’; foto_Cb = reshape(foto_Cb,largh,alt)’; foto_Cr = reshape(foto_Cr,largh,alt)’; if (mod(largh,k) ~= 0) % Adattamento larghezza foto. for i=1:k-mod(largh,k) foto_Y(1:end,largh+i) = 128; foto_Cb(1:end,largh+i) = 128; foto_Cr(1:end,largh+i) = 128; end end if (mod(alt,k) ~= 0) % Adattamento altezza foto. for i=1:k-mod(alt,k) foto_Y(alt+i,1:end) = 128; foto_Cb(alt+i,1:end) = 128; foto_Cr(alt+i,1:end) = 128; end end foto_Y = foto_Y(1:alt+k*(mod(alt,k) ~= 0)-mod(alt,k),1:largh+ +k*(mod(largh,k) ~= 0)-mod(largh,k)); foto_Cb = foto_Cb(1:alt+k*(mod(alt,k) ~= 0)-mod(alt,k),1:largh+ +k*(mod(largh,k) ~= 0)-mod(largh,k)); 26 foto_Cr = foto_Cr(1:alt+k*(mod(alt,k) ~= 0)-mod(alt,k),1:largh+ +k*(mod(largh,k) ~= 0)-mod(largh,k)); sz = size(foto_Y); dim(1) = sz(2); dim(2) = sz(1); % Terza parte: calcolo della DCT e compressione. disp(’Calcolo DCT e comprimo...’); matr_Y = zeros((dim(1)/k)*(dim(2)/k),k); matr_Cb = zeros((dim(1)/k)*(dim(2)/k),k); matr_Cr = zeros((dim(1)/k)*(dim(2)/k),k); cursore_matr = 1; for i=1:k:dim(1)-k+1 for j=1:k:dim(2)-k+1 trasf_Y = dct2(foto_Y(j:j+k-1,i:i+k-1)); % Calcolo trasformate. trasf_Cb = dct2(foto_Cb(j:j+k-1,i:i+k-1)); trasf_Cr = dct2(foto_Cr(j:j+k-1,i:i+k-1)); trasf_Y = round(trasf_Y./quant_lum); % Eseguo quantizzazione. trasf_Cb = round(trasf_Cb./quant_crom); trasf_Cr = round(trasf_Cr./quant_crom); matr_Y(cursore_matr:cursore_matr+k-1,:) = trasf_Y; matr_Cb(cursore_matr:cursore_matr+k-1,:) = trasf_Cb; matr_Cr(cursore_matr:cursore_matr+k-1,:) = trasf_Cr; cursore_matr = cursore_matr+k; end end disp(’Scrivo i dati...’); matr_Y = sparse(matr_Y); % Acquisizione come matrice sparsa. matr_Cb = sparse(matr_Cb); matr_Cr = sparse(matr_Cr); save(’compressa.mat’,’matr_Y’); save(’compressa.mat’,’matr_Cb’,’-append’); save(’compressa.mat’,’matr_Cr’,’-append’); end Nella prima parte eseguiamo la scalatura delle matrici memorizzate come spiegato in precedenza; unico accorgimento da segnalare è l’uso di ceil, ossia l’arrotondamento all’intero superiore, in modo da non avere zeri in quantizzazione qualora si scelga un fattore di compressione pari a 100. La seconda parte, il ridimensionamento, è perfettamente analoga a quanto svolto per l’SVD. Nella terza parte abbiamo eseguito per ogni unità di tutte le componenti la DCT, la quantizzazione e abbiamo salvato infine il tutto nel file compressa.mat. Da notare l’uso di -append per inserire più matrici nello stesso file binario. La fase di compressione è completata: scriviamo lo scipt .sh. #!/bin/bash echo "Livello di compressione? (1-100)" 27 read SCALA echo "Creo matrice per Octave/Matlab..." gcc -o crea_matrici crea_matrici.c ./crea_matrici echo "RGB_to_YCbCr();compress_dct(8,${SCALA})" | matlab echo "Elimino file non piu’ utili..." rm "matr_R" rm "matr_G" rm "matr_B" rm "matr_Y" rm "matr_Cb" rm "matr_Cr" echo "Fatto!" Lo script di compressione produce i file compressa.mat e dimensione foto a partire da foto.pgm: procediamo ora a creare il codice di decompressione. % Programma che esegue la decompressione calcolando per % ogni blocco la trasformata del coseno inversa a % partire dalla matrice sparsa dei coefficienti conservati. % k rappresenta la suddivisione dei sottoblocchi (k=8 per il JPEG). function decompress_dct(k) %Carico elementi necessari. load(’compressa.mat’); load(’quant_scalata.mat’); dim_foto = load(’dimensione_foto’,’-ascii’); %Recupero della forma completa. matr_Y = full(matr_Y); matr_Cb = full(matr_Cb); matr_Cr = full(matr_Cr); largh = dim_foto(1)+k*(mod(dim_foto(1),k) ~= 0)-mod(dim_foto(1),k); alt = dim_foto(2)+k*(mod(dim_foto(2),k) ~= 0)-mod(dim_foto(2),k); cursore_matr = 1; %Inizializzo matrice dell’immagine. foto_Y = zeros(alt,largh); foto_Cb = zeros(alt,largh); foto_Cr = zeros(alt,largh); blocchi_largh = largh/k; blocchi_alt = alt/k; disp(’Decomprimo la foto...’); for i=1:blocchi_largh for j=1:blocchi_alt %Calcolo l’antitrasformata del coseno. lum = matr_Y(cursore_matr:cursore_matr+k-1,:); 28 lum = lum.*quant_lum; crom1 = matr_Cb(cursore_matr:cursore_matr+k-1,:); crom1 = crom1.*quant_crom; crom2 = matr_Cr(cursore_matr:cursore_matr+k-1,:); crom2 = crom2.*quant_crom; antitrasf_Y = round(idct2(lum)); antitrasf_Cb = round(idct2(crom1)); antitrasf_Cr = round(idct2(crom2)); foto_Y((j-1)*k+1:j*k,(i-1)*k+1:i*k) = antitrasf_Y; foto_Cb((j-1)*k+1:j*k,(i-1)*k+1:i*k) = antitrasf_Cb; foto_Cr((j-1)*k+1:j*k,(i-1)*k+1:i*k) = antitrasf_Cr; cursore_matr = cursore_matr+k; end end %Riporto la foto alle dimensioni originali. foto_Y = foto_Y(1:dim_foto(2),1:dim_foto(1)); foto_Cb = foto_Cb(1:dim_foto(2),1:dim_foto(1)); foto_Cr = foto_Cr(1:dim_foto(2),1:dim_foto(1)); %Calcolo trasposta, in modo da scrivere per riga. foto_Y = foto_Y’; foto_Cb = foto_Cb’; foto_Cr = foto_Cr’; %Salvo matrice. Y = fopen(’matr_Y’,’w’); Cb = fopen(’matr_Cb’,’w’); Cr = fopen(’matr_Cr’,’w’); fprintf(Y,’%d\n’,foto_Y); fprintf(Cb,’%d\n’,foto_Cb); fprintf(Cr,’%d\n’,foto_Cr); fclose(Y); fclose(Cb); fclose(Cr); end Questa function esegue, iterando su ogni blocco, la dequantizzazione e il calcolo dell’IDCT. In modo da avere infine solo valori interi uso l’arrotondamento svolto da round. Salvo i dati in componenti separate sui file matr Y, matr Cb e matr Cr: poichè il file .ppm non può leggere da lumiocrominanza devo rieseguire una conversione in RGB, faccio ciò con il seguente. function YCbCr_to_RGB() % Carico matrici delle componenti YCbCr. disp(’Carico componenti YCbCr...’); matr_Y = load(’matr_Y’,’-ascii’); matr_Cb = load(’matr_Cb’,’-ascii’); matr_Cr = load(’matr_Cr’,’-ascii’); % Carico dimensioni della foto. 29 dim = load(’dimensione_foto’,’-ascii’); % Inizializzo matrici RGB. matr_R = zeros(dim(2),dim(1)); matr_G = zeros(dim(2),dim(1)); matr_B = zeros(dim(2),dim(1)); % Matrice di trasformazione. A = [1 0 1.402; 1 -0.344 -0.714; 1 1.772 0]; % Eseguo appl. lineare. disp(’Eseguo conversione a RGB...’); for i=1:dim(1)*dim(2) vett = [matr_Y(i); matr_Cb(i); matr_Cr(i)]; vett = A*vett; matr_R(i) = round(vett(1)+128); matr_G(i) = round(vett(2)+128); matr_B(i) = round(vett(3)+128); end % Controllo che i dati siano nel range [0,255]. matr_R = max(0,matr_R); matr_R = min(255,matr_R); matr_G = max(0,matr_G); matr_G = min(255,matr_G); matr_B = max(0,matr_B); matr_B = min(255,matr_B); % Salvo matrici RGB. disp(’Salvo dati...’); R = fopen(’matr_R’,’w’); G = fopen(’matr_G’,’w’); B = fopen(’matr_B’,’w’); fprintf(R,’%d\n’,matr_R); fprintf(G,’%d\n’,matr_G); fprintf(B,’%d\n’,matr_B); fclose(R); fclose(G); fclose(B); end Resta solo da ricostruire il .ppm, basta riutilizzare il programma C che abbiamo già salvato come crea ppm.c; per quanto riguarda lo script di esecuzione #!/bin/bash echo "decompress_dct(8);YCbCr_to_RGB()" | matlab echo "Creo file .pgm..." 30 gcc -o crea_ppm crea_ppm.c ./crea_ppm echo "Elimino files non piu’ utili..." rm "matr_Y" rm "matr_Cb" rm "matr_Cr" rm "matr_R" rm "matr_G" rm "matr_B" echo "Fatto!" Siamo pronti a testare il nostro JPEG: in modo da avere un confronto pulito con l’algoritmo SVD precedente useremo la medeisima foto (49,7 MB). Notiamo che dal punto di vista del risparmio di spazio non ci sono termini di paragone: conservando 4 valori singolari avevamo il rapporto ottimale compressione/qualità con 13,4 MB, col JPEG ciò accade impostando un fattore di scalatura 80 e solo 888 KB! Il risultato è veramente eccellente, visto che si tratta di un’immagine ad alta risoluzione, 2808x1784 pixel. Vediamo invece come il JPEG deteriora l’immagine con l’aumentare dei coefficenti portati a 0: si osserva una perdita generale di qualità, il fatto che in maniera sparsa si iniziano a notare quadratini e macchie di colore, nei casi più estremi si ha pure una perdita di naturalezza cromatica. Nell’algoritmo SVD notavamo invece distorsioni nei pressi dei bordi, ma mantenimento sulle aree uniformi: il JPEG ha un comportamento più ”indifferente” rispetto alla natura dell’immagine in oggetto. È sorprendente il fatto che con la conservazione di pochi coefficienti del coseno, e quindi matrici di compressione fortemente sparse, si riesca a riscostruire un immagine altamente fedele all’originale: la DCT infatti ”cattura” e separa i dettagli importanti per la visione da quelli trascurabili e pertanto eliminabili. Inoltre, stabilita la dimensione 8x8 di suddivisione, che per più ragioni è ritenuta adatta, i coefficienti rappresentanti le componenti essenziali sono veramente pochi e ciò non sarebbe effettivamente fattibile con suddivisioni di grandezza magggiore; si andrebbe a perdere in nitidezza. Nella nostra sperimentazione abbiamo ottenuto una dimensione di 535 KB con fattore di sclatura 60, 374 KB con fattore 30, 253 KB con fattore 12, 191 KB con fattore 7, 115 KB con fattore 3: da segnalare la scarsa qualità degli ultimi tre tentativi. 31 Figura 9: l’immagine intera non compressa (.ppm) Figura 10: particolari con fattori di scalatura 60, 30, 12, 7. 32 Un esperimento: variazione della scalatura di quantizzazione. L’implementazione svolta nel paragrafo precedente consente di effettuare la compressione di un immagine a partire da un valore numerico nel range [1, 100]: per valori vicini a 1 otteniamo massimo risparmio di spazio con peggiore qualità, man mano che il valore aumenta crescono resa e spazio. Proviamo ora a far variare la matrice di quantizzazione Q in corso d’opera, in modo da testare più gradi di compressione sulla stessa foto: più precsiamente, se A ∈ Rm×n sta bilisco un vettore l1 . . . lr di livelli di compressione. Andrò a scalare Q mn mn rispetto al fattore l1 nei primi 2 blocchi, rispetto a l2 nei successivi 2 e p r p r cosı̀ via. L’algortimo che presenteremo ha tuttavia delle problematiche qualora r - mn, poichè avremmo problemi di divisione non intera: supponiamo che la dimensione del nostro vettore in input divida il prodotto di altezza e larghezza foto. Ci è necessario modificare solo le function di compressione e decompressione, il resto sarà prettamente identico a quanto visto nel paragrafo precedente. % Programma che esegue la compressione dell’immagine mediante % DCT facendo variare la scalatura della matrice di quantizzazione. % v è il vettore contenente i fattori di compressione al singolo tratto. % Assumiamo length(v) divisibile per il numero di blocchi, per non aver % problemi di fraz. non intero. % k è la dimensione dei sottoblocchi (k=8 per il JPEG). function compress_tratti(v,k) % Carico file necessari. load(’matrici_quantizzazione.mat’); dim = load(’dimensione_foto’,’-ascii’); largh = dim(1); alt = dim(2); foto_Y = load(’matr_Y’,’-ascii’); foto_Cb = load(’matr_Cb’,’-ascii’); foto_Cr = load(’matr_Cr’,’-ascii’); foto_Y = reshape(foto_Y,largh,alt)’; foto_Cb = reshape(foto_Cb,largh,alt)’; foto_Cr = reshape(foto_Cr,largh,alt)’; % Adattamento immagine alla suddivisione in blocchi. if (mod(largh,k) ~= 0) % Adattamento larghezza foto. for i=1:k-mod(largh,k) foto_Y(1:end,largh+i) = 128; foto_Cb(1:end,largh+i) = 128; foto_Cr(1:end,largh+i) = 128; end end if (mod(alt,k) ~= 0) % Adattamento altezza foto. 33 for i=1:k-mod(alt,k) foto_Y(alt+i,1:end) = 128; foto_Cb(alt+i,1:end) = 128; foto_Cr(alt+i,1:end) = 128; end end foto_Y = foto_Y(1:alt+k*(mod(alt,k) ~= 0)-mod(alt,k),1:largh+ +k*(mod(largh,k) ~= 0)-mod(largh,k)); foto_Cb = foto_Cb(1:alt+k*(mod(alt,k) ~= 0)-mod(alt,k),1:largh+ +k*(mod(largh,k) ~= 0)-mod(largh,k)); foto_Cr = foto_Cr(1:alt+k*(mod(alt,k) ~= 0)-mod(alt,k),1:largh+ +k*(mod(largh,k) ~= 0)-mod(largh,k)); sz = size(foto_Y); dim(1) = sz(2); dim(2) = sz(1); % Calcolo il numero di blocchi per tratto di compressione. num = ((dim(1)/k)*(dim(2)/k))/length(v); % Calcolo della DCT e compressione. disp(’Calcolo DCT e comprimo...’); matr_Y = zeros((dim(1)/k)*(dim(2)/k),k); matr_Cb = zeros((dim(1)/k)*(dim(2)/k),k); matr_Cr = zeros((dim(1)/k)*(dim(2)/k),k); cursore_matr = 1; prossima_riscalatura = 1; riscala = 1; % Var. logica che indica necessità % di cambiare scala. curr = 1; % Indicatore del tratto corrente. num_blocchi = 0; % Contatore di blocchi compressi. for i=1:k:dim(1)-k+1 for j=1:k:dim(2)-k+1 if (riscala == 1) % Riscalatura della matrice di quant. if (v(curr) < 50) quant_lum_new = ceil((quant_lum.*(5000/v(curr))+50*ones(8))./100); quant_crom_new = ceil((quant_crom.*(5000/v(curr))+50*ones(8))./100); else quant_lum_new = ceil((quant_lum.*(200-v(curr)*2)+50*ones(8))./100); quant_crom_new = ceil((quant_crom.*(200-v(curr)*2)+50*ones(8))./100); end riscala = 0; curr = curr+1; prossima_riscalatura = prossima_riscalatura+num; end trasf_Y = dct2(foto_Y(j:j+k-1,i:i+k-1)); trasf_Cb = dct2(foto_Cb(j:j+k-1,i:i+k-1)); 34 trasf_Cr = dct2(foto_Cr(j:j+k-1,i:i+k-1)); trasf_Y = round(trasf_Y./quant_lum_new); trasf_Cb = round(trasf_Cb./quant_crom_new); trasf_Cr = round(trasf_Cr./quant_crom_new); matr_Y(cursore_matr:cursore_matr+k-1,:) = trasf_Y; matr_Cb(cursore_matr:cursore_matr+k-1,:) = trasf_Cb; matr_Cr(cursore_matr:cursore_matr+k-1,:) = trasf_Cr; cursore_matr = cursore_matr+k; num_blocchi = num_blocchi+1; if (num_blocchi == prossima_riscalatura-1) riscala = 1; end end end disp(’Scrivo i dati...’); matr_Y = sparse(matr_Y); matr_Cb = sparse(matr_Cb); matr_Cr = sparse(matr_Cr); save(’compressa.mat’,’matr_Y’); save(’compressa.mat’,’matr_Cb’,’-append’); save(’compressa.mat’,’matr_Cr’,’-append’); end Metodicamente non vi sono differenze con l’algoritmo JPEG originale; è utilizzata la variabile logica riscala che segnala la necessita di modificare la matrice di quantizzazione una volta raggiunto un numero di blocchi esaminati pari a prossima riscalatura, quest’ultima variabile è prontamente aggiornata ogni volta. Identici accorgimenti sono utilizzati nella function di decompressione: non sono dunque necessarie ulteriori spiegazioni. % Programma che esegue la decompressione della foto. % v è il vettore contenente i fattori di compressione al singolo tratto. % Assumiamo length(v) divisibile per il numero di blocchi, per non aver % problemi di fraz. non intero. % k è la dimensione dei sottoblocchi (k=8 per il JPEG). function decompress_tratti(v,k) load(’compressa.mat’); %Carico elementi necessari. load(’matrici_quantizzazione.mat’); dim_foto = load(’dimensione_foto’,’-ascii’); matr_Y = full(matr_Y); %Recupero della forma completa. matr_Cb = full(matr_Cb); matr_Cr = full(matr_Cr); largh = dim_foto(1)+k*(mod(dim_foto(1),k) ~= 0)-mod(dim_foto(1),k); alt = dim_foto(2)+k*(mod(dim_foto(2),k) ~= 0)-mod(dim_foto(2),k); cursore_matr = 1; 35 prossima_riscalatura = 1; riscala = 1; curr = 1; num_blocchi = 0; foto_Y = zeros(alt,largh); %Inizializzo matrice dell’immagine. foto_Cb = zeros(alt,largh); foto_Cr = zeros(alt,largh); blocchi_largh = largh/k; blocchi_alt = alt/k; num = (blocchi_largh*blocchi_alt)/length(v); disp(’Decomprimo la foto...’); for i=1:blocchi_largh for j=1:blocchi_alt %Calcolo l’antitrasformata del coseno. if (riscala == 1) % Riscalatura della matrice di quant. if (v(curr) < 50) quant_lum_new = ceil((quant_lum.*(5000/v(curr))+50*ones(8))./100); quant_crom_new = ceil((quant_crom.*(5000/v(curr))+50*ones(8))./100); else quant_lum_new = ceil((quant_lum.*(200-v(curr)*2)+50*ones(8))./100); quant_crom_new = ceil((quant_crom.*(200-v(curr)*2)+50*ones(8))./100); end riscala = 0; curr = curr+1; prossima_riscalatura = prossima_riscalatura+num; end lum = matr_Y(cursore_matr:cursore_matr+k-1,:); lum = lum.*quant_lum_new; crom1 = matr_Cb(cursore_matr:cursore_matr+k-1,:); crom1 = crom1.*quant_crom_new; crom2 = matr_Cr(cursore_matr:cursore_matr+k-1,:); crom2 = crom2.*quant_crom_new; antitrasf_Y = round(idct2(lum)); antitrasf_Cb = round(idct2(crom1)); antitrasf_Cr = round(idct2(crom2)); foto_Y((j-1)*k+1:j*k,(i-1)*k+1:i*k) = antitrasf_Y; foto_Cb((j-1)*k+1:j*k,(i-1)*k+1:i*k) = antitrasf_Cb; foto_Cr((j-1)*k+1:j*k,(i-1)*k+1:i*k) = antitrasf_Cr; cursore_matr = cursore_matr+k; num_blocchi = num_blocchi+1; if (num_blocchi == prossima_riscalatura-1) riscala = 1; end end end foto_Y = foto_Y(1:dim_foto(2),1:dim_foto(1)); 36 foto_Cb = foto_Cb(1:dim_foto(2),1:dim_foto(1)); foto_Cr = foto_Cr(1:dim_foto(2),1:dim_foto(1)); foto_Y = foto_Y’; foto_Cb = foto_Cb’; foto_Cr = foto_Cr’; Y = fopen(’matr_Y’,’w’); Cb = fopen(’matr_Cb’,’w’); Cr = fopen(’matr_Cr’,’w’); fprintf(Y,’%d\n’,foto_Y); fprintf(Cb,’%d\n’,foto_Cb); fprintf(Cr,’%d\n’,foto_Cr); fclose(Y); fclose(Cb); fclose(Cr); end Proviamo quanto abbiamo implementato su un’immagine piuttosto famosa: trattasi di una modella svedese, Lena Soderberg, la cui foto sulla rivista Playboy del Novembre 1972 fu scelta (opportunamente censurata) proprio per testare algoritmi di compressione. Maggiori informazioni, oltre all’immagine stessa, le troviamo su http://www.cs.cmu.edu/ ~chuck/lennapg/. Si utilizza come array di scalature 3 5 10 20 . Figura 11: compressione JPEG a tratti. 37 Terzo approccio: una compressione DST. Ultimo proposito della trattazione è quello di sostituire la DCT con un’altra trasformata discreta tra quelle presenti in letteratura: vogliamo sfruttare la DST, ossia la trasformata discreta del seno. Trattasi di un metodo di compressione puramente a fini sperimentali: vogliamo essenzialmente capire se la DST è adatta quanto la DCT al nostro scopo o meno, cercando anche di fornire motivazioni a tale proposito. Sia v ∈ Rn , considero {s1 , . . . , sn } come base del seno: πi sin n + 1 2 .. si = . n+1 nπi sin n+1 Le trasformate, rispettivamente diretta e inversa, sono applicazioni lineari date dalle seguenti relazioni, ricavabili analogamente a quanto fatto per la DCT. wk = (DSTn (v))k = n X vi sin i=1 πki n+1 n vk = (IDSTn (w))k = 2 X πki wi sin n + 1 i=1 n+1 Considero ora A ∈ Rm×n e voglio definire una DST matriciale. Sullo spazio delle matrici m × n, la base del seno è {S11 , . . . , Sn1 , S21 , . . . , Smn }. ! πkj πhi 4 sin sin Sij = (n + 1)(m + 1) n+1 n + 1 h = 1, . . . , m k = 1, . . . , n Siamo pronti ad estendere le relazioni della trasformata e dell’inversa scritte in precedenza: Bhk = (DSTm×n (A))hk = m X n X Aij sin i=1 j=1 m Ahk = (IDSTm×n (B))hk = πkj πhi sin m+1 n+1 n XX 4 πhi πkj Bij sin sin (m + 1)(n + 1) i=1 j=1 m+1 n+1 Matlab offre una function dst vettoriale, ma non una dst2 matriciale: è facile sfruttare la linearità della trasformata per fabbricarci una nostra dst2. Riducendosi al caso a noi più congeniale, A ∈ Rn×n , si ha infatti: DSTn (DSTn (A)T )T = DSTn×n (A) IDSTn (IDSTn (A)T )T = IDSTn×n (A) Tutto ciò è svolto da queste due function. 38 % Programma che implementa una trasformata discreta del seno in % due variabili, a partire dalla function dst già implementata in % Matlab. function Z=dst2(X) Y = dst(X); Z = dst(Y’)’; end % Programma che implementa una trasformata discreta inversa del seno in % due variabili, a partire dalla function idst già implementata in % Matlab. function Z=idst2(X) Y = idst(X); Z = idst(Y’)’; end Osserviamo alcune peculiarità della trasformata del seno: anzitutto, come vediamo in Figura 12, i coefficienti elevati non andranno più a posizionarsi in alto a sinistra, ma si alterneranno a coefficienti piccoli lungo tutta la superficie, come una sorta di scacchiera. Tutto questo sta a significare che dovremo rinunciare al processo di quantizzazione, o almeno non sarà possibile utilizzare le stesse dello standard JPEG: poichè fabbricare tali matrici è un procedimento empirico tutt’altro che banale eviteremo categoricamente questa fase. Figura 12: imagesc della dst2 di un blocco 8x8: verso il rosso i valori più elevati. Altra caratteristica che possiamo cogliere dalla figura è l’elevato numero di coefficienti elevati, in confronto anche alla DCT: ciò suggerisce minore capacità di compressione nell’uso della DST, ancor prima di osservare qualsiasi risultato. 39 La trasformata del seno riesce a catturare con più difficoltà quelle componenti essenziali dell’immagine che la DCT raccoglie in solo 3/4 coefficienti: la severità di taglio non potrà essere eccessiva, perderemmo infatti qualità in relativa fretta. Passiamo ora al codice che a grandi linee non differirà molto da quello del JPEG, fatta eccezione per la fase di taglio dei valori: qua fisseremo una soglia e andremo ad eliminare quei coefficenti della DST il cui valore assoluto sarà al di sotto della suddetta quantità. Per ottimizzare la computazione, su ogni blocco andremo a far uso di una matrice logica costituita da 0 e 1, con gli 1 sulle posizioni che riterremo necessarie da conservare; basterà eseguire un prodotto termine a termine tra la DST del blocco e tale matrice e avremmo effettuato la compressione dovuta. Sceglieremo uno schema di colore in lumiocrominanza, potremo cosı̀ ricondurci con facilità a quanto fatto in precedenza e allo stesso tempo avremo un confronto più diretto. Utilizziamo i medesimi programmi in C crea matrici.c e crea ppm.c, oltre alle function di conversione dall’RGB all’YCbCr e viceversa. Riportiamo di seguito la function di compressione, quella di decompressione e lo script .sh di esecuzione. % Programma che esegue una compressione mediante DST % sulle componenti YCbCr, eliminando i valori aventi % modulo sotto la soglia data. % k rappresenta la dimensione di ciacun sottoblocco in % cui viene divisa la foto. % z rappresenta il fattore di normalizzazione. function compress_dst(k,soglia,z) % Prima parte: adattamento immagine alla suddivisione in blocchi. disp(’Adatto componenti...’) dim = load(’dimensione_foto’,’-ascii’); largh = dim(1); alt = dim(2); foto_Y = load(’matr_Y’,’-ascii’); foto_Cb = load(’matr_Cb’,’-ascii’); foto_Cr = load(’matr_Cr’,’-ascii’); foto_Y = reshape(foto_Y,largh,alt)’; foto_Cb = reshape(foto_Cb,largh,alt)’; foto_Cr = reshape(foto_Cr,largh,alt)’; if (mod(largh,k) ~= 0) % Adattamento larghezza foto. for i=1:k-mod(largh,k) foto_Y(1:end,largh+i) = 128; foto_Cb(1:end,largh+i) = 128; foto_Cr(1:end,largh+i) = 128; end end if (mod(alt,k) ~= 0) % Adattamento altezza foto. 40 for i=1:k-mod(alt,k) foto_Y(alt+i,1:end) = 128; foto_Cb(alt+i,1:end) = 128; foto_Cr(alt+i,1:end) = 128; end end foto_Y = foto_Y(1:alt+k*(mod(alt,k) ~= 0)-mod(alt,k),1:largh+ +k*(mod(largh,k) ~= 0)-mod(largh,k)); foto_Cb = foto_Cb(1:alt+k*(mod(alt,k) ~= 0)-mod(alt,k),1:largh+ +k*(mod(largh,k) ~= 0)-mod(largh,k)); foto_Cr = foto_Cr(1:alt+k*(mod(alt,k) ~= 0)-mod(alt,k),1:largh+ +k*(mod(largh,k) ~= 0)-mod(largh,k)); sz = size(foto_Y); dim(1) = sz(2); dim(2) = sz(1); % Seconda parte: calcolo della DST e compressione. disp(’Calcolo DST e comprimo...’); matr_Y = zeros((dim(1)/k)*(dim(2)/k),k); matr_Cb = zeros((dim(1)/k)*(dim(2)/k),k); matr_Cr = zeros((dim(1)/k)*(dim(2)/k),k); cursore_matr = 1; for i=1:k:dim(1)-k+1 for j=1:k:dim(2)-k+1 trasf_Y = dst2(foto_Y(j:j+k-1,i:i+k-1)); % Calcolo trasformate. trasf_Cb = dst2(foto_Cb(j:j+k-1,i:i+k-1)); trasf_Cr = dst2(foto_Cr(j:j+k-1,i:i+k-1)); trasf_Y = trasf_Y .* (abs(trasf_Y) > soglia); % Elimino valori sotto la soglia data. trasf_Cb = trasf_Cb .* (abs(trasf_Cb) > soglia); trasf_Cr = trasf_Cr .* (abs(trasf_Cr) > soglia); matr_Y(cursore_matr:cursore_matr+k-1,:) = floor(trasf_Y*z); matr_Cb(cursore_matr:cursore_matr+k-1,:) = floor(trasf_Cb*z); matr_Cr(cursore_matr:cursore_matr+k-1,:) = floor(trasf_Cr*z); cursore_matr = cursore_matr+k; end end disp(’Scrivo i dati...’); matr_Y = sparse(matr_Y); % Acquisizione come matrice sparsa. matr_Cb = sparse(matr_Cb); matr_Cr = sparse(matr_Cr); save(’compressa.mat’,’matr_Y’); save(’compressa.mat’,’matr_Cb’,’-append’); save(’compressa.mat’,’matr_Cr’,’-append’); end 41 # Script di esecuzione: alg. di compressione DST. #!/bin/bash echo "Soglia di compressione?" read SOGLIA echo "Creo matrice per Octave/Matlab..." gcc -o crea_matrici crea_matrici.c ./crea_matrici echo "RGB_to_YCbCr();compress_dst(8,${SOGLIA},20)" | matlab echo "Elimino file non piu’ utili..." rm "matr_R" rm "matr_G" rm "matr_B" rm "matr_Y" rm "matr_Cb" rm "matr_Cr" echo "Fatto!" % Programma che esegue la decompressione calcolando per ogni blocco la trasformata % del coseno inversa a partire dalla matrice sparsa dei coefficienti conservati. % k rappresenta la suddivisione dei sottoblocchi (k=8 per il JPEG). % z rapprensenta il fattore di normalizzazione. function decompress_dst(k,z) load(’compressa.mat’); %Carico elementi necessari. matr_Y = full(matr_Y); %Recupero della forma completa. matr_Cb = full(matr_Cb); matr_Cr = full(matr_Cr); dim_foto = load(’dimensione_foto’,’-ascii’); largh = dim_foto(1)+k*(mod(dim_foto(1),k) ~= 0)-mod(dim_foto(1),k); alt = dim_foto(2)+k*(mod(dim_foto(2),k) ~= 0)-mod(dim_foto(2),k); cursore_matr = 1; foto_Y = zeros(alt,largh); %Inizializzo matrice dell’immagine. foto_Cb = zeros(alt,largh); foto_Cr = zeros(alt,largh); blocchi_largh = largh/k; blocchi_alt = alt/k; disp(’Decomprimo la foto...’); for i=1:blocchi_largh for j=1:blocchi_alt %Calcolo l’antitrasformata del seno. antitrasf_Y = round(idst2(matr_Y(cursore_matr:cursore_matr+k-1,:)/z)); antitrasf_Cb = round(idst2(matr_Cb(cursore_matr:cursore_matr+k-1,:)/z)); antitrasf_Cr = round(idst2(matr_Cr(cursore_matr:cursore_matr+k-1,:)/z)); foto_Y((j-1)*k+1:j*k,(i-1)*k+1:i*k) = antitrasf_Y; foto_Cb((j-1)*k+1:j*k,(i-1)*k+1:i*k) = antitrasf_Cb; 42 foto_Cr((j-1)*k+1:j*k,(i-1)*k+1:i*k) = antitrasf_Cr; cursore_matr = cursore_matr+k; end end foto_Y = foto_Y(1:dim_foto(2),1:dim_foto(1)); foto_Cb = foto_Cb(1:dim_foto(2),1:dim_foto(1)); foto_Cr = foto_Cr(1:dim_foto(2),1:dim_foto(1)); foto_Y = foto_Y’; %Calcolo trasposta, in modo da scrivere per riga. foto_Cb = foto_Cb’; foto_Cr = foto_Cr’; Y = fopen(’matr_Y’,’w’); %Salvo matrice. Cb = fopen(’matr_Cb’,’w’); Cr = fopen(’matr_Cr’,’w’); fprintf(Y,’%d\n’,foto_Y); fprintf(Cb,’%d\n’,foto_Cb); fprintf(Cr,’%d\n’,foto_Cr); fclose(Y); fclose(Cb); fclose(Cr); end # Script di esecuzione: alg. di decompressione DST. #!/bin/bash echo "decompress_dst(8,20);YCbCr_to_RGB()" | matlab echo "Creo file .pgm..." gcc -o crea_ppm crea_ppm.c ./crea_ppm echo "Elimino files non piu’ utili..." rm "matr_Y" rm "matr_Cb" rm "matr_Cr" rm "matr_R" rm "matr_G" rm "matr_B" echo "Fatto!" Effettuiamo ora qualche prova sul programma: considerando ancora una volta l’immagine della Canon (49,7 MB) facciamo variare le soglie di compressione al fine di testare l’effettiva capacita dell’algoritmo DST. Fissando il valore di soglia a 20 non si notano imperfezioni e si riesce a ridurre lo spazio a 7,9 MB; con soglia di taglio 60 si scende a 4,3 MB, con 100 a 3,2 MB, con 200 a 1,9 MB. Da notare che queste ultime tre prove riportano difetti visivi rispetto all’immagine originale: la carenza di qualità si inizia a notare dal perimetro di ogni suddivisione e va ad allargarsi man mano che il taglio è più severo, si ha una visione delle suddivisioni 8x8 sempre più netta. 43 Figura 13: l’immagine intera non compressa (.ppm) Figura 14: particolari con soglie di taglio 20, 60, 100, 200. 44 Conclusioni Siamo giunti alla fine del percorso e vediamo di tirare il punto della situazione: abbiamo effettivamente confrontato un algortimo commerciale quale il JPEG con due tentativi più improvvisati. Se volessimo stilare una sorta di classifica non avremmo ovviamente problemi su chi posizionare al primo posto: un poco più difficile, ma neppur troppo, stabilire invece il migliore tra l’approccio SVD e il DST. È ovvia la superiorità della trasformata del seno in termini di compressione: da un lato abbiamo conservato solo alcuni coefficienti numerici e dall’altro diversi vettori singolari, una mole di dati maggiore. In termini di qualità invece l’SVD non ha nulla a che invidiare agli altri; grazie all’apporto dato dalla suddivisione dell’immagine questo algoritmo lavora egregiamente sulle variazioni non drastiche, mentre presenta dei problemi negli alti cambi di tono solo in caso di una conservazione di valori singolari troppo esigua. Ultima puntualizzazione che mi pare necessaria: il codice che abbiamo scritto non rappresenta il massimo dell’efficienza in termini di tempo di esecuzione, ci proponiamo di ”volare basso” e considerare tutto ciò mera presentazione di esempi e di principi, di confronti tra possbili idee matematiche atte a suggerire metodi di compressione di immagini digitali. 45