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!

Pixel di elaborazione
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 abbiamo bisogno di 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 è che prendi la somma ponderata di un rettangolo di pixel dell'immagine di origine e la utilizzi 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 di affinamento 3 x 3. 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 di convoluzione, la sfocatura della casella. La funzione sfocaggine restituisce la media dei valori dei pixel all'interno della matrice di convergenza. Per farlo, crea una matrice di convoluzione di dimensioni NxN in cui ciascuno dei pesi è 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 prima trasformando l'immagine in scala di grigi, 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. La sfumatura verticale si basa sulla stessa idea, ma utilizza i pixel adiacenti 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 comprendere i concetti di base per scrivere filtri di immagini in JavaScript utilizzando il tag canvas HTML. Ti incoraggio a implementare altri filtri per le immagini, è molto 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 la maggior parte dei filtri semplici in tempo reale, il che ti consente di utilizzarli per la post-elaborazione di video e animazioni.