Filtri per immagini con canvas

Ilmari Heikkinen

Introduzione

L'elemento canvas HTML5 può essere utilizzato per scrivere filtri di immagine. Devi solo disegnare un'immagine su una tela, leggere i pixel della tela e applicare il filtro. Puoi quindi scrivere il risultato su una nuova canvas (o semplicemente riutilizzare quella vecchia).

Sembra semplice, vero? Bene. Al lavoro!

L'immagine di test originale
L'immagine di prova originale

Elaborazione dei pixel

Innanzitutto, recupera i pixel dell'immagine:

Filters = {};
Filters.getPixels = function(img) {
var c = this.getCanvas(img.width, img.height);
var ctx = c.getContext('2d');
ctx.drawImage(img);
return ctx.getImageData(0,0,c.width,c.height);
};

Filters.getCanvas = function(w,h) {
var c = document.createElement('canvas');
c.width = w;
c.height = h;
return c;
};

Ora serve un modo per filtrare le immagini. Che ne dici di un filterImage metodo che prende un filtro e un'immagine e restituisce i pixel filtrati?

Filters.filterImage = function(filter, image, var_args) {
var args = [this.getPixels(image)];
for (var i=2; i<arguments.length; i++) {
args.push(arguments[i]);
}
return filter.apply(null, args);
};

Eseguire filtri semplici

Ora che abbiamo assemblato la pipeline di elaborazione dei pixel, è il momento di scrivere alcuni filtri semplici. Per iniziare, convertiamo l'immagine in scala di grigi.

Filters.grayscale = function(pixels, args) {
var d = pixels.data;
for (var i=0; i<d.length; i+=4) {
var r = d[i];
var g = d[i+1];
var b = d[i+2];
// CIE luminance for the RGB
// The human eye is bad at seeing red and blue, so we de-emphasize them.
var v = 0.2126*r + 0.7152*g + 0.0722*b;
d[i] = d[i+1] = d[i+2] = v
}
return pixels;
};

La luminosità può essere regolata aggiungendo un valore fisso ai pixel:

Filters.brightness = function(pixels, adjustment) {
var d = pixels.data;
for (var i=0; i<d.length; i+=4) {
d[i] += adjustment;
d[i+1] += adjustment;
d[i+2] += adjustment;
}
return pixels;
};

Anche l'applicazione di una soglia a un'immagine è abbastanza semplice. Basta confrontare il valore in scala di grigi di un pixel con il valore di soglia e impostare il colore in base a questo:

Filters.threshold = function(pixels, threshold) {
var d = pixels.data;
for (var i=0; i<d.length; i+=4) {
var r = d[i];
var g = d[i+1];
var b = d[i+2];
var v = (0.2126*r + 0.7152*g + 0.0722*b >= threshold) ? 255 : 0;
d[i] = d[i+1] = d[i+2] = v
}
return pixels;
};

Convoluzione delle immagini

I filtri di convoluzione sono filtri generici molto utili per l'elaborazione delle immagini. L'idea di base è prendere la somma ponderata di un rettangolo di pixel dell'immagine di origine e utilizzarla come valore di output. I filtri di convergenza possono essere utilizzati per sfocatura, affinamento, rilievo, rilevamento dei bordi e molte altre cose.

Filters.tmpCanvas = document.createElement('canvas');
Filters.tmpCtx = Filters.tmpCanvas.getContext('2d');

Filters.createImageData = function(w,h) {
return this.tmpCtx.createImageData(w,h);
};

Filters.convolute = function(pixels, weights, opaque) {
var side = Math.round(Math.sqrt(weights.length));
var halfSide = Math.floor(side/2);
var src = pixels.data;
var sw = pixels.width;
var sh = pixels.height;
// pad output by the convolution matrix
var w = sw;
var h = sh;
var output = Filters.createImageData(w, h);
var dst = output.data;
// go through the destination image pixels
var alphaFac = opaque ? 1 : 0;
for (var y=0; y<h; y++) {
for (var x=0; x<w; x++) {
  var sy = y;
  var sx = x;
  var dstOff = (y*w+x)*4;
  // calculate the weighed sum of the source image pixels that
  // fall under the convolution matrix
  var r=0, g=0, b=0, a=0;
  for (var cy=0; cy<side; cy++) {
    for (var cx=0; cx<side; cx++) {
      var scy = sy + cy - halfSide;
      var scx = sx + cx - halfSide;
      if (scy >= 0 && scy < sh && scx >= 0 && scx < sw) {
        var srcOff = (scy*sw+scx)*4;
        var wt = weights[cy*side+cx];
        r += src[srcOff] * wt;
        g += src[srcOff+1] * wt;
        b += src[srcOff+2] * wt;
        a += src[srcOff+3] * wt;
      }
    }
  }
  dst[dstOff] = r;
  dst[dstOff+1] = g;
  dst[dstOff+2] = b;
  dst[dstOff+3] = a + alphaFac*(255-a);
}
}
return output;
};

Ecco un filtro 3x3 per aumentare la nitidezza. Guarda come viene applicata la ponderazione al pixel centrale. Per mantenere la luminosità dell'immagine, la somma dei valori della matrice deve essere uguale a 1.

Filters.filterImage(Filters.convolute, image,
[  0, -1,  0,
-1,  5, -1,
  0, -1,  0 ]
);

Ecco un altro esempio di filtro a convoluzione: la sfocatura dei riquadri. La funzione sfocaggine della scatola restituisce la media dei valori dei pixel all'interno della matrice di convoluzione. Per farlo, crea una matrice di convoluzione di dimensioni NxN in cui ciascuno dei pesi sia pari a 1 / (NxN). In questo modo, ogni pixel all'interno della matrice contribuisce in modo uguale all'immagine di output e la somma dei pesi è pari a 1.

Filters.filterImage(Filters.convolute, image,
[ 1/9, 1/9, 1/9,
1/9, 1/9, 1/9,
1/9, 1/9, 1/9 ]
);

Possiamo creare filtri delle immagini più complessi combinando quelli esistenti. Ad esempio, scriviamo un filtro Sobel. Un filtro Sobel calcola i gradienti verticali e orizzontali dell'immagine e combina le immagini calcolate per trovare i bordi nell'immagine. Qui implementiamo il filtro Sobel applicando prima la scala di grigi all'immagine, poi prendendo i gradienti orizzontali e verticali e infine combinando le immagini sfumate per ottenere l'immagine finale.

In termini di terminologia, "gradiente" indica la variazione nel valore del pixel in una posizione dell'immagine. Se un pixel ha un vicino a sinistra con valore 20 e un vicino a destra con valore 50, il gradiente orizzontale al pixel sarà 30. Il gradiente verticale ha la stessa idea, ma utilizza i vicini sopra e sotto.

var grayscale = Filters.filterImage(Filter.grayscale, image);
// Note that ImageData values are clamped between 0 and 255, so we need
// to use a Float32Array for the gradient values because they
// range between -255 and 255.
var vertical = Filters.convoluteFloat32(grayscale,
[ -1, 0, 1,
-2, 0, 2,
-1, 0, 1 ]);
var horizontal = Filters.convoluteFloat32(grayscale,
[ -1, -2, -1,
  0,  0,  0,
  1,  2,  1 ]);
var final_image = Filters.createImageData(vertical.width, vertical.height);
for (var i=0; i<final_image.data.length; i+=4) {
// make the vertical gradient red
var v = Math.abs(vertical.data[i]);
final_image.data[i] = v;
// make the horizontal gradient green
var h = Math.abs(horizontal.data[i]);
final_image.data[i+1] = h;
// and mix in some blue for aesthetics
final_image.data[i+2] = (v+h)/4;
final_image.data[i+3] = 255; // opaque alpha
}

Esistono anche molti altri filtri di convoluzione interessanti che aspettano solo di essere scoperti. Ad esempio, prova a implementare un filtro di Laplace nel giocattolo di convoluzione qui sopra e vedi cosa succede.

Conclusione

Spero che questo breve articolo ti sia stato utile per introdurre i concetti di base per scrivere filtri di immagini in JavaScript utilizzando il tag canvas HTML. Ti suggerisco di implementare altri filtri immagine, è davvero divertente.

Se hai bisogno di prestazioni migliori dai filtri, in genere puoi portarli a utilizzare gli shader di frammenti WebGL per l'elaborazione delle immagini. Con gli Shader, puoi eseguire i filtri più semplici in tempo reale, così da utilizzarli per la post-elaborazione di video e animazioni.