Immagini, thumbnail, entropia e PHP

In questi giorni ho sviluppato una classe Image per la manipolazione di immagini da includere nel core di GINO, il nostro caro CMS. In futuro aggiungerò altre funzionalità di manipolazione, ma per il momento la cosa che premeva era di poter facilmente generare delle thumbnail.

Quando facciamo inserire agli utenti delle immagini, quasi sempre abbiamo bisogno di generare delle thumb, perché in alcune viste (per esempio delle liste di elementi) non possiamo utilizzare le immagini a risoluzione originale, o meglio, potremmo usarle ma dovremmo rimpicciolirle al volo, con uno spreco di kb di traffico evitabile.

Quindi dobbiamo generare delle thumb. Allora sono possibili diversi approcci. Il primo che viene in mente è quello di generare una thumb dell'immagine quando viene caricata sul server. Un altro possibile approccio è quello di generare le thumb al volo quando viene richiesta la pagina e gestire una sorta di cache per evitare di generare la stessa thumb millemila volte. Questo è l'approccio utilizzato da sorl-thumbnail, un'app django che genera thumb di mestiere.

Ci sono chiaramente pro e contro per ogni metodo. La mia opinione è che i pro derivanti dal secondo approccio siano superiori ai contro. In realtà di contro al momento me ne viene in mente solo uno di una certa rilevanza, il fatto di non riuscire a collegare la thumb all'oggetto cui appartiene l'immagine che la genera. In parole semplici vuol dire che non si riesce a gestire l'eliminazione della thumb quando viene eliminata l'immagine originale in modo semplice. Ma è pur vero che è sufficiente ad intervalli di tempo più o meno ampi eliminare tutte le thumb le quali saranno ricreate alla prima visualizzazione.

Parliamo invece dei vantaggi.

  1. Posso generare thumb di qualunque immagine sia accessibile alla funzione imagecreatefromjpeg (o le equivalenti per altre estensioni)
  2. Non sono limitato alla creazione di 1/2/n thumb di dimensioni prestabilite, come accade quando devo predisporre un meccanismo di generazione thumb legato alla action del form che carica l'immagine. Anzi posso generare tutte le thumb che voglio e di tutte le dimensioni che mi pare a partire dalla stessa immagine.
  3. Se decido dopo 2 mesi che nella vista dettaglio del mio modulo news le thumb le voglio fare 100x100 e non più 80x80, non devo far riprocessare manualmente tutte le immagini! (Ok, questo vantaggio è legato al punto precedente)

Quindi W W W sorl-thumbnail. Appurato questo vediamo come si possa replicare una funzionalità analoga (senza le stesse millemila funzionalità) in php.

Come voglio utilizzare la nuova funzionalità

A me piace partire (quasi sempre) pensando a come voglio utilizzare una cosa, prima di progettarla. Ecco allora io voglio poter creare delle thumb in questo modo all'interno delle mie viste:

$img = new Image($path); 
$thumb = $img->thumb(100, 100); 
$path = $thumb->getPath(); 

Devo quindi implementare un metodo thumb che generi la thumbnail ma solo se questa non è già stata generata in precedenza. Come faccio ad accorgermi di questo?

La key

Ciò che dobbiamo fare non è complicato. Dobbiamo generare al volo la thumbnail di un'immagine A con le caratteristiche fornite (W, H), salvarla su filesystem, tenere traccia su db del path della thumb appena creata e avere un modo di riconoscere che questa è proprio la thumb dell'immagine A di larghezza W e altezza H.

Per fare questo ci creiamo una tabella con colonne key, path, width, height. La key deve essere generata in modo che quando richiedo la creazione di una thumb possa prima controllare se questa esiste già. Chiaramente ogni thumb deve avere la sua key univoca, e dire ogni thumb significa dire ogni prodotto di manipolazione dell'immagine A con parametri differenti.

Quindi ad esempio possiamo generare la key in questo modo:

 /**
  * @brief Genera una key univoca per un'operazione eseguita su un'immagine
  * @param string $abspath percorso assoluto dell'immagine
  * @param int $width Larghezza dell'immagine dopo l'operazione
  * @param int $height Altezza dell'immagine dopo l'operazione
  * @params array $options Opzioni
  * @return string chiave univoca
  */
  private function toKey($abspath, $width, $height, $options) {
    $json_obj = array(
      'path' => $abspath,
      'time' => filemtime($abspath),
      'width' => $width,
      'height' => $height,
      'options' => $options
    );
    $key = md5(json_encode($json_obj));
    return $key;
  }

La chiave sarà unica (trascurando le collisioni dell'algoritmo crittografico md5) per ogni path, data di ultima modifica, dimensioni finali thumb più eventuali opzioni che al momento non sono implementate.

Ma come la usiamo questa chiave?

Il metodo thumb

Ecco qua

/**
 * @brief Genera una thumb delle dimensioni date
 * @description Le thumb sono generate al volo e tenute in cache su db.
 * Se viene richiesta una thumb già creata viene direttamente
 * restituita. Altrimenti viene creata.
 * @param int|null $width Larghezza della thumb
 * @param int|null $height Altezza della thumb
 * @param array $options Opzioni.
 * Array associativo di opzioni:
 * - 'allow_enlarge': default false. Consente l'allargamento di immagini per soddisfare le dimensioni richieste.
 * @return Image nuovo oggetto immagine wrapper della thumb generata
 */
 public function thumb($width, $height, $options = array()) {
   $key = $this->toKey($this->_abspath, $width, $height, $options);
   if(!$thumb = $this->getThumbFromKey($key)) {
     $thumb = $this->makeThumb($key, $width, $height, $options);
   }
    return $thumb;
 }

Come vedete è abbastanza semplice. Ora non sto a spiegare in dettaglio come funziona il metodo che ricava la thumb a partire dalla chiave, né come salvare su db la chiave, il path e le dimensioni della thumb; sono cose banali.

Il metodo makeThumb si preoccupa di generare la thumb, salvarla su filesystem e salvare su db tutto quanto.

Cazzo c'entra l'entropia?

Fin'ora non ho giustificato il titolo del post, che prometteva di parlare di entropia e immagini. Lo faccio ora.

Il fatto è che se generiamo automaticamente la thumb di un'immagine, o ci limitiamo al solo resize su uno dei lati, ma in questo caso siamo legati all'aspect ratio dell'originale, oppure dobbiamo croppare l'immagine.

Siccome man mano che passa il tempo i siti vanno fatti sempre più fichi altrimenti non se li caga nessuno, sarebbe bello poter gestire thumb quadrate anche con immagini rettangolari, ad esempio.

Per questo abbiamo bisogno del crop, ma per fare il crop dobbiamo decidere larghezza e altezza del taglio ed il punto top left da cui partire a tagliare. Come decidiamo la posizione di questo punto?

Una decisione spesso accettabile sarebbe quella di croppare al centro, ma non sempre le foto hanno la parte più interessante e dinamica al centro, per fortuna. Qui allora entra il discorso dell'entropia. Diciamo che volgiamo croppare l'immagine nella zona a maggior entropia, ovvero nella zona più variabile, con più azione, più variata, meno piatta, più interessante... dell'immagine.

Capito questo sappiamo cosa dobbiamo fare: dobbiamo capire quale subset di px di larghezza W e altezza H dell'immagine originale massimizza l'entropia.

Fortunatamente tanta gente si è già occupata di questo problema, ed io ho preso spunto da qui. Solo che Tim Reynolds ha usato la libreria ImageMagik di PHP, mentre io volevo ottenere un risultato analogo con le GD.

Adesso incollo qua sotto il gist che contiene la classe Image per intero e vediamo più in dettaglio le parti che permettono di croppare l'immagine nella zona a massima entropia.

La parte interessante inizia alla linea 355. Le prime operazioni che effettuiamo servono ad appiattire ancora di più ciò che nell'immagine già tende ad essere piatto.

  1. Cloniamo l'immagine originale (non vogliamo rovinarla)
  2. Applichiamo il filtro di riconoscimento dei contorni, che amplifica i punti dell'immagine i cui la luminosità cambia molto (ricordate che cerchiamo l'entropia massima)
  3. Trasformiamo lo spazio colore in scala di grigi
  4. Facciamo diventare nero tutto quanto è vicino al nero (questa funzione esiste in ImageMagik, invece ho dovuto simularla con le GD)
  5. Applichiamo un blur per spalmare ulteriormente le parti piatte
  6. A questo punto l'immagine è pronta per andare a cercare le coordinate migliori da cui partire per racchiudere la parte con maggiore entropia.

Passiamo quindi alla linea 412, il metodo slice è quello che ci permette di calcolare, nelle due direzioni, le porzioni di massima entropia dell'immagine. Il metodo può sembrare complicato ma non lo è.

Consideriamo prima la direzione orizzontale. In pratica tagliamo l'immagine a fettine strette (quanto strette lo decidete voi ed in generale si potrebbe far dipendere dalla larghezza totale dell'immagine), poi cicliamo su ogni fettina e ne calcoliamo l'entropia. Otteniamo cosi n fettine ciascuna con la sua misura di entropia. Ora sappiamo che tagliando a larghezza W possiamo includere solo m fettine, quindi scegliamo le m massimizzando l'entropia massima (data dalla somma dell'entropia di tutte le fettine racchiuse in m). Facciamo la stessa cosa nella direzione y e ci siamo trovati il punto di partenza ottimale (x, y) per tagliare la nostra immagine massimizzando l'entropia.

Rimane da capire come calcolare l'entropia di una fettina. Guardate qui. Proviamo a spiegarlo e saltiamo alla linea 480.

Prima di tutto dobbiamo ricavare l'istogramma dell'immagine. Naturalmente ImageMagik lo fa esponendo un bel metodo pronto all'uso, io me lo sono dovuto scrivere. Non è comunque complicato. Praticamente l'istogramma deve riportare la lista dei colori che compaiono nell'immagine con le relative frequenze. Quindi cicliamo su ogni px dell'imamgine, leggiamo il colore e poi ci creiamo un array di frequenze. Va da sé che più la distribuzione è piatta e più abbiamo entropia (molti colori che compaiono più o meno lo stesso numero di volte), al contrario una distribuzione con un grosso picco indica poca entropia (la maggior parte dell'immagine è blu ad esempio, come un cielo).

Una volta che abbiamo l'istogramma dell'immagine calcoliamo l'entropia con il metodo alla linea 516. Praticamente cicliamo su tutti i colori e incrementiamo l'entropia di una quantità uguale alla probabilità del colore moltiplicata per il logaritmo in base 2 della stessa. La probabilità di cui si parla qui è semplicemente data dalla frequenza divisa per l'area dell'immagine, cioè il numero di px di quel colore diviso il numero totale di px. La presenza del logaritmo fa si che amplifichi le distribuzioni piatte, con tutte probabilità basse. Infatti se la probabilità è piccola, (ed è chiaramente minore di 1) il logaritmo cresce in valore assoluto molto rapidamente con il tendere a zero della probabilità, dando importanza al contributo.

Chiappatevi il grafico del logaritmo:

Ok dopo tutto il pippone vediamo il risultato, come nel post che ho preso come riferimento e linkato più sopra vi mostro un'immagine originale e la thumb ottenuta di dimensioni 100x100.

originale thumb

Hasta la proxima!

Categorie

internet opendata piemonte web jeff php programmazione tutorial curiosita governance vim cucina sviluppo apple hardware imac crisi economia politica torino didattica flash illustrazione ricorrenze lapalisse novita release informazione html5 javascript website musica mootools mercato societa vita lavoro HMI interfaccia utente gino gino-news gino-multimedia modernità usa burocrazia jquery django testing libri nova americana etica impresa solidarietà css comunicazioni trasloco ufficio vita sociale entropia immagini fotografia concorso polymer webcomponents programming crowdfunding progetti finanziamento fallimento opensource deploy otto python