La compressione JPEG

La trasformata coseno, ottenuta dal caso particolare della trasformata
di Fourier per una funzione pari a valori reali, permette di passare da N
coefficienti reali nel dominio spaziale a N coefficienti reali nel
dominio della frequenza, quindi permette di mantenere l'informazione
completa nel dominio della frequenza senza modificare la quantità di
coefficienti richiesti per codificarla. A partire da questa osservazione
e considerando che la rappresentazione di una porzione di immagine nel
dominio delle frequenza porta con alta probabilità ad avere una gran
percentuale di coefficienti nulli o prossimi a zero (per tutte le
frequenza non presenti), considerando inoltre che nel caso delle immagini
quest'effetto si accentua "quadraticamente", si è deciso di utilizzare la
trasformata discreta coseno (dct) come strumento di compressione lossy
delle immagini nello standard JPEG.

Contents

Trasformata coseno di immagini

Vediamo degli esempi di trasformata coseno applicata ad una immagine di
esempio di 8x8 bit, cercando di selezionare una parte complessa di
immagine reale.
% Carico una immagine intera:
img = imtype(imread('cameraman.tif'),'gF');

% Ne prendo un pezzettino:
rect = [123,70,7,7];
img = imcrop(img,rect);

% Calcolo la trasformata coseno:
img_f = dct2(img);

% Vediamola:
imshowbig = @(img)(imshow(imresize(img,8,'nearest')));
figure;
subplot(121); imshowbig(img);
subplot(122); imshowbig(imscale(img_f));
Si noti che il valor medio (coefficiente di frequenze 0,0) è l'unico a
valore elevato, il gli altri sono distribuiti attorno allo zero (con
distribuzione somigliante a una gaussiana) dove la maggior parte dei
coefficienti assume valori molto piccoli, producendo quindi variazioni
piccole nell'immagine.
% Istogramma dei coefficienti:
figure; hist(img_f(:),20);
Sfruttiamo questa caratteristica per eliminare (forzando a zero) una
certa percentuale di coefficienti:
% Seleziono i coefficienti vittima:
mask = abs(img_f)<1e-1;

% Ricostruisco l'immagine:
img_f2 = img_f; img_f2(mask) = 0;
img2 =  idct2(img_f2);

% Confrontiamole:
figure;
subplot(131); imshowbig(img);
subplot(132); imshowbig(img2);
subplot(133); imshowbig(abs(img-img2));
Il risultato non è perfetto (lossy) ma ne è una buona approssimazione.
JPEG tratta blocchi 8x8, usa questa tecnica per ridurre il numero di
coefficienti, comprime tutti i coefficienti medi tramite la compressione
di Huffman (media più frequente meno bit e viceversa), codifica poi gli
altri coefficienti a "serpentina" a partire adl coefficiente medio
(escluso perché già codificato) con una tecnica simile al Run Length
Coding.

NOTA: la matrice dei coefficienti viene moltiplicata per una matrice di pesi (scelta in base al livello di compressione) condivisa dal coder e dal decoder, in questo modo vengono eliminati con più facilità i coefficienti relativi a frequenze con impatto più basso sulla percezione umana delle immagini.

Componenti della trasformata coseno

Osserviamo le componenti della trasformata coseno della nostra immagine
8x8:
% Init:
comps = zeros(64);

% Tutte le componenti:
for i=1:8
    for j=1:8
        comp = zeros(8);
        comp(i,j) = 1;
        comps((i-1)*8+1:i*8,(j-1)*8+1:j*8) = dct2(comp);
    end
end

% Vediamo:
figure; imshowscale(comps);

Un esempio fai-da-te

Proviamo a codificare una immagine, ridurre il numero di coefficienti e
ricostruire il risultato:
% Pesi di quantizzazione:
W = [ 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; ...
	  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 ];

% Carico l'immagine e mi preparo:
img = imtype(imread('cameraman.tif'),'gF');
img2 = zeros(size(img));
img2bis = zeros(size(img));
numZeros = 0;
numZerosBis = 0;
means = zeros(size(img,1)/8,size(img,2)/8);

% Itero sui blocchi:
for x=1:8:size(img,2)-7
    for y=1:8:size(img,1)-7
        % Ottengo il blocco:
        block = img(y:y+7,x:x+7);

        % Lo trasformo:
        block_f = dct2(block);

        % Scelgo gli zeri:
        mask = abs(255*block_f./W) < 0.25;
        maskBis = abs(255*block_f./W) < 1;
        numZeros = numZeros + sum(mask(:));
        numZerosBis = numZerosBis + sum(maskBis(:));

        % Riduco i coefficienti:
        block_f2 = block_f;
        block_f2bis = block_f;
        block_f2(mask) = 0;
        block_f2bis(maskBis) = 0;

        % Ritrasformo:
        block2 = idct2(block_f2);
        block2bis = idct2(block_f2bis);

        % Salvo:
        img2(y:y+7,x:x+7) = block2;
        img2bis(y:y+7,x:x+7) = block2bis;
        means((y-1)/8+1,(x-1)/8+1) = block_f(1);
    end
end

% Vediamole:
figure;
subplot(221); imshow(img);
title('Originale');
subplot(222); imshow(means/8); % Esercizio: scoprire perché divido per 8.
title('Le sole medie');
subplot(223); imshow(img2);
title(sprintf('%2.2f%% di zeri',100*numZeros/numel(img)));
subplot(224); imshow(img2bis);
title(sprintf('%2.2f%% di zeri',100*numZerosBis/numel(img)));