Filtres d'image avec canevas

Ilmari Heikkinen

Introduction

L'élément Canvas HTML5 peut être utilisé pour créer des filtres d'images. Il vous suffit de dessiner une image sur un canevas, de relire les pixels du canevas et d'exécuter votre filtre sur ceux-ci. Vous pouvez ensuite écrire le résultat sur un nouveau canevas (ou réutiliser l'ancien).

Cela semble simple ? Bien. Au boulot !

Image test d'origine
L'image de test d'origine

Traitement des pixels...

Commencez par récupérer les pixels de l'image:

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

Ensuite, nous avons besoin d'un moyen de filtrer les images. Que diriez-vous d'une méthode filterImage qui renvoie les pixels filtrés à partir d'un filtre et d'une image ?

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

Exécuter des filtres simples

Maintenant que le pipeline de traitement des pixels est configuré, il est temps d'écrire des filtres simples. Pour commencer, convertissons l'image en niveaux de gris.

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

Vous pouvez régler la luminosité en ajoutant une valeur fixe aux pixels:

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

L'attribution d'un seuil à une image est également assez simple. Il vous suffit de comparer la valeur en nuances de gris d'un pixel à la valeur de seuil et de définir la couleur en fonction de cette valeur:

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

Images pivotantes

Les filtres de convolution sont des filtres génériques très utiles pour le traitement d'image. L'idée de base est de prendre la somme pondérée d'un rectangle de pixels de l'image source et de l'utiliser comme valeur de sortie. Les filtres à convolution peuvent être utilisés pour le floutage, l'accentuation, le gaufrage, la détection des bords et bien d'autres choses.

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

Voici un filtre 3x3 qui permet d'améliorer la netteté. Découvrez comment il met l'accent sur le poids sur le pixel central. Pour maintenir la luminosité de l'image, la somme des valeurs de la matrice doit être égale à un.

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

Voici un autre exemple de filtre à convolution : le flou de zone. Le flou encadré génère la moyenne des valeurs de pixels dans la matrice de convolution. La méthode consiste à créer une matrice de convolution de taille NxN où chacune des pondérations est 1 / (NxN). De cette façon, chacun des pixels à l'intérieur de la matrice contribue à hauteur égale à l'image de sortie et la somme des pondérations est égale à 1.

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

Nous pouvons combiner des filtres existants pour créer des filtres d'image plus complexes. Par exemple, écrivons un filtre Sobel. Un filtre Sobel calcule les dégradés verticaux et horizontaux de l'image et combine les images calculées pour trouver les bords de l'image. La façon dont nous implémentons le filtre Sobel ici consiste à mettre l'image en gris, puis à prendre les dégradés horizontaux et verticaux, puis à combiner les images de dégradé pour former l'image finale.

D'un point de vue terminologique, le terme "dégradé" désigne ici le changement de la valeur en pixels au niveau de la position de l'image. Si un pixel a un voisin gauche de valeur 20 et un voisin droit de valeur 50, le dégradé horizontal du pixel est de 30. Le dégradé vertical a la même idée, mais utilise les voisins au-dessus et en dessous.

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
}

Et de nombreux autres filtres à convolution intéressants attendent que vous les découvriez. Par exemple, essayez d'implémenter un filtre Laplace dans le jouet à convolution ci-dessus et observez ce qu'il fait.

Conclusion

Nous espérons que ce petit article vous a été utile pour présenter les concepts de base de l'écriture de filtres d'image en JavaScript à l'aide de la balise HTML Canvas. Je vous invite à ajouter d'autres filtres d'image. C'est plutôt amusant !

Si vous avez besoin de meilleures performances de vos filtres, vous pouvez généralement les transférer afin d'utiliser des nuanceurs de fragments WebGL pour traiter l'image. Avec les nuanceurs, vous pouvez exécuter la plupart des filtres simples en temps réel, ce qui vous permet de les utiliser pour post-traiter la vidéo et les animations.