Filtri per immagini con canvas

Ilmari Heikkinen

Introduzione

L'elemento canvas HTML5 può essere utilizzato per scrivere filtri immagine. Quello che devi fare è disegnare un'immagine su un canvas, leggere i pixel del canvas ed eseguire il filtro su di essi. Poi puoi scrivere il risultato su una nuova tela (o riutilizzarla, basta riutilizzare quella precedente).

Sembra semplice? Bene. Al lavoro!

L'immagine di test originale
Immagine di test originale

Elaborazione dei pixel in corso...

Innanzitutto, recupera i pixel 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 dobbiamo trovare un modo per filtrare le immagini. Puoi utilizzare un metodo filterImage che accetta 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);
};

Esecuzione di filtri semplici

Ora che abbiamo creato la pipeline di elaborazione dei pixel, è il momento di scrivere alcuni semplici filtri. 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 regolazione della luminosità può essere eseguita 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 impostare un livello di soglia per un'immagine è molto semplice. È sufficiente confrontare il valore della scala di grigi di un pixel con il valore della soglia e impostare il colore in base a quanto segue:

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;
};

Immagini in continua evoluzione

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 convoluzione possono essere usati per sfocature, nitidezza, goffratura, 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 nitidezza 3 x 3. Scopri come concentra il peso sul pixel centrale. Per mantenere la luminosità dell'immagine, la somma dei valori della matrice dovrebbe essere uno.

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

Ecco un altro esempio di filtro di convoluzione, la sfocatura del riquadro. La sfocatura del riquadro restituisce la media dei valori dei pixel all'interno della matrice di convoluzione. Il modo per farlo è creare una matrice di convoluzione di dimensione N x N dove ciascuna delle ponderazioni è 1 / (N x N). In questo modo, ciascuno dei pixel all'interno della matrice contribuisce in egual misura all'immagine di output e la somma delle ponderazioni è uno.

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 immagine più complessi combinando i filtri esistenti. Ad esempio, scriviamo un filtro Sobel. Un filtro Sobel calcola i gradienti verticali e orizzontali dell'immagine e combina le immagini calcolate per trovarne i bordi. Il modo in cui implementiamo il filtro Sobel qui consiste nel ridimensionare l'immagine in una scala grigia, quindi prendere i gradienti orizzontale e verticale e infine combinare le immagini a gradiente per creare l'immagine finale.

Per quanto riguarda la terminologia, qui il termine "gradiente" indica la modifica del valore dei pixel in una posizione dell'immagine. Se un pixel ha un vicino sinistro con valore 20 e un vicino destro con valore 50, il gradiente orizzontale del 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
}

Ci sono tantissimi altri filtri convoluzioni interessanti che aspettano solo che tu li impieghi. Ad esempio, prova a implementare un filtro Laplace nel giocattolo a convoluzione qui sopra e scopri che cosa fa.

Conclusione

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

Se i filtri richiedono prestazioni migliori, puoi portarli con gli ombreggiatori di frammenti WebGL per l'elaborazione delle immagini. Con gli Shader, puoi eseguire i filtri più semplici in tempo reale e utilizzarli per la post-elaborazione di video e animazioni.